This commit is contained in:
syuilo 2017-08-30 17:31:39 +09:00
parent 61be5c0d10
commit e7415dd42b
14 changed files with 315 additions and 60 deletions

View file

@ -2,6 +2,10 @@ ChangeLog (Release Notes)
========================= =========================
主に notable な changes を書いていきます 主に notable な changes を書いていきます
unreleased
----------
* New: 投稿のピン留め (#746)
2508 (2017/08/30) 2508 (2017/08/30)
----------------- -----------------
* New: モバイル版のユーザーページのアクティビティチャートを変更 * New: モバイル版のユーザーページのアクティビティチャートを変更

View file

@ -77,6 +77,10 @@ common:
show-result: "Show result" show-result: "Show result"
voted: "Voted" voted: "Voted"
mk-post-menu:
pin: "Pin"
pinned: "Pinned"
mk-reaction-picker: mk-reaction-picker:
choose-reaction: "Pick your reaction" choose-reaction: "Pick your reaction"

View file

@ -77,6 +77,10 @@ common:
show-result: "結果を見る" show-result: "結果を見る"
voted: "投票済み" voted: "投票済み"
mk-post-menu:
pin: "ピン留め"
pinned: "ピン留めしました"
mk-reaction-picker: mk-reaction-picker:
choose-reaction: "リアクションを選択" choose-reaction: "リアクションを選択"

View file

@ -167,6 +167,10 @@ const endpoints: Endpoint[] = [
name: 'i/regenerate_token', name: 'i/regenerate_token',
withCredential: true withCredential: true
}, },
{
name: 'i/pin',
kind: 'account-write'
},
{ {
name: 'i/appdata/get', name: 'i/appdata/get',
withCredential: true withCredential: true

View file

@ -0,0 +1,44 @@
/**
* Module dependencies
*/
import $ from 'cafy';
import User from '../../models/user';
import Post from '../../models/post';
import serialize from '../../serializers/user';
/**
* Pin post
*
* @param {any} params
* @param {any} user
* @return {Promise<any>}
*/
module.exports = async (params, user) => new Promise(async (res, rej) => {
// Get 'post_id' parameter
const [postId, postIdErr] = $(params.post_id).id().$;
if (postIdErr) return rej('invalid post_id param');
// Fetch pinee
const post = await Post.findOne({
_id: postId,
user_id: user._id
});
if (post === null) {
return rej('post not found');
}
await User.update(user._id, {
$set: {
pinned_post_id: post._id
}
});
// Serialize
const iObj = await serialize(user, user, {
detail: true
});
// Send response
res(iObj);
});

View file

@ -4,6 +4,7 @@
import * as mongo from 'mongodb'; import * as mongo from 'mongodb';
import deepcopy = require('deepcopy'); import deepcopy = require('deepcopy');
import User from '../models/user'; import User from '../models/user';
import serializePost from './post';
import Following from '../models/following'; import Following from '../models/following';
import getFriends from '../common/get-friends'; import getFriends from '../common/get-friends';
import config from '../../conf'; import config from '../../conf';
@ -116,7 +117,14 @@ export default (
_user.is_followed = follow2 !== null; _user.is_followed = follow2 !== null;
} }
if (me && !me.equals(_user.id) && opts.detail) { if (opts.detail) {
if (_user.pinned_post_id) {
_user.pinned_post = await serializePost(_user.pinned_post_id, me, {
detail: true
});
}
if (me && !me.equals(_user.id)) {
const myFollowingIds = await getFriends(me); const myFollowingIds = await getFriends(me);
// Get following you know count // Get following you know count
@ -135,6 +143,7 @@ export default (
}); });
_user.followers_you_know_count = followersYouKnowCount; _user.followers_you_know_count = followersYouKnowCount;
} }
}
resolve(_user); resolve(_user);
}); });

View file

@ -28,3 +28,4 @@ require('./reaction-picker.tag');
require('./reactions-viewer.tag'); require('./reactions-viewer.tag');
require('./reaction-icon.tag'); require('./reaction-icon.tag');
require('./weekly-activity-chart.tag'); require('./weekly-activity-chart.tag');
require('./post-menu.tag');

View file

@ -0,0 +1,134 @@
<mk-post-menu>
<div class="backdrop" ref="backdrop" onclick={ close }></div>
<div class="popover { compact: opts.compact }" ref="popover">
<button if={ post.user_id === I.id } onclick={ pin }>%i18n:common.tags.mk-post-menu.pin%</button>
</div>
<style>
$border-color = rgba(27, 31, 35, 0.15)
:scope
display block
position initial
> .backdrop
position fixed
top 0
left 0
z-index 10000
width 100%
height 100%
background rgba(0, 0, 0, 0.1)
opacity 0
> .popover
position absolute
z-index 10001
background #fff
border 1px solid $border-color
border-radius 4px
box-shadow 0 3px 12px rgba(27, 31, 35, 0.15)
transform scale(0.5)
opacity 0
$balloon-size = 16px
&:not(.compact)
margin-top $balloon-size
transform-origin center -($balloon-size)
&:before
content ""
display block
position absolute
top -($balloon-size * 2)
left s('calc(50% - %s)', $balloon-size)
border-top solid $balloon-size transparent
border-left solid $balloon-size transparent
border-right solid $balloon-size transparent
border-bottom solid $balloon-size $border-color
&:after
content ""
display block
position absolute
top -($balloon-size * 2) + 1.5px
left s('calc(50% - %s)', $balloon-size)
border-top solid $balloon-size transparent
border-left solid $balloon-size transparent
border-right solid $balloon-size transparent
border-bottom solid $balloon-size #fff
> button
display block
</style>
<script>
import anime from 'animejs';
this.mixin('i');
this.mixin('api');
this.post = this.opts.post;
this.source = this.opts.source;
this.on('mount', () => {
const rect = this.source.getBoundingClientRect();
const width = this.refs.popover.offsetWidth;
const height = this.refs.popover.offsetHeight;
if (this.opts.compact) {
const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2);
const y = rect.top + window.pageYOffset + (this.source.offsetHeight / 2);
this.refs.popover.style.left = (x - (width / 2)) + 'px';
this.refs.popover.style.top = (y - (height / 2)) + 'px';
} else {
const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2);
const y = rect.top + window.pageYOffset + this.source.offsetHeight;
this.refs.popover.style.left = (x - (width / 2)) + 'px';
this.refs.popover.style.top = y + 'px';
}
anime({
targets: this.refs.backdrop,
opacity: 1,
duration: 100,
easing: 'linear'
});
anime({
targets: this.refs.popover,
opacity: 1,
scale: [0.5, 1],
duration: 500
});
});
this.pin = () => {
this.api('i/pin', {
post_id: this.post.id
}).then(() => {
if (this.opts.cb) this.opts.cb('pinned', '%i18n:common.tags.mk-post-menu.pinned%');
this.unmount();
});
};
this.close = () => {
this.refs.backdrop.style.pointerEvents = 'none';
anime({
targets: this.refs.backdrop,
opacity: 0,
duration: 200,
easing: 'linear'
});
this.refs.popover.style.pointerEvents = 'none';
anime({
targets: this.refs.popover,
opacity: 0,
scale: 0.5,
duration: 200,
easing: 'easeInBack',
complete: () => this.unmount()
});
};
</script>
</mk-post-menu>

View file

@ -43,16 +43,18 @@
</div> </div>
<footer> <footer>
<mk-reactions-viewer post={ p }/> <mk-reactions-viewer post={ p }/>
<button onclick={ reply } title="返信"><i class="fa fa-reply"></i> <button onclick={ reply } title="返信">
<p class="count" if={ p.replies_count > 0 }>{ p.replies_count }</p> <i class="fa fa-reply"></i><p class="count" if={ p.replies_count > 0 }>{ p.replies_count }</p>
</button> </button>
<button onclick={ repost } title="Repost"><i class="fa fa-retweet"></i> <button onclick={ repost } title="Repost">
<p class="count" if={ p.repost_count > 0 }>{ p.repost_count }</p> <i class="fa fa-retweet"></i><p class="count" if={ p.repost_count > 0 }>{ p.repost_count }</p>
</button> </button>
<button class={ reacted: p.my_reaction != null } onclick={ react } ref="reactButton" title="リアクション"><i class="fa fa-plus"></i> <button class={ reacted: p.my_reaction != null } onclick={ react } ref="reactButton" title="リアクション">
<p class="count" if={ p.reactions_count > 0 }>{ p.reactions_count }</p> <i class="fa fa-plus"></i><p class="count" if={ p.reactions_count > 0 }>{ p.reactions_count }</p>
</button>
<button onclick={ menu } ref="menuButton">
<i class="fa fa-ellipsis-h"></i>
</button> </button>
<button><i class="fa fa-ellipsis-h"></i></button>
</footer> </footer>
</article> </article>
<div class="replies"> <div class="replies">
@ -315,6 +317,13 @@
}); });
}; };
this.menu = () => {
riot.mount(document.body.appendChild(document.createElement('mk-post-menu')), {
source: this.refs.menuButton,
post: this.p
});
};
this.loadContext = () => { this.loadContext = () => {
this.contextFetching = true; this.contextFetching = true;

View file

@ -128,16 +128,16 @@
</div> </div>
<footer> <footer>
<mk-reactions-viewer post={ p } ref="reactionsViewer"/> <mk-reactions-viewer post={ p } ref="reactionsViewer"/>
<button onclick={ reply } title="%i18n:desktop.tags.mk-timeline-post.reply%"><i class="fa fa-reply"></i> <button onclick={ reply } title="%i18n:desktop.tags.mk-timeline-post.reply%">
<p class="count" if={ p.replies_count > 0 }>{ p.replies_count }</p> <i class="fa fa-reply"></i><p class="count" if={ p.replies_count > 0 }>{ p.replies_count }</p>
</button> </button>
<button onclick={ repost } title="%i18n:desktop.tags.mk-timeline-post.repost%"><i class="fa fa-retweet"></i> <button onclick={ repost } title="%i18n:desktop.tags.mk-timeline-post.repost%">
<p class="count" if={ p.repost_count > 0 }>{ p.repost_count }</p> <i class="fa fa-retweet"></i><p class="count" if={ p.repost_count > 0 }>{ p.repost_count }</p>
</button> </button>
<button class={ reacted: p.my_reaction != null } onclick={ react } ref="reactButton" title="%i18n:desktop.tags.mk-timeline-post.add-reaction%"><i class="fa fa-plus"></i> <button class={ reacted: p.my_reaction != null } onclick={ react } ref="reactButton" title="%i18n:desktop.tags.mk-timeline-post.add-reaction%">
<p class="count" if={ p.reactions_count > 0 }>{ p.reactions_count }</p> <i class="fa fa-plus"></i><p class="count" if={ p.reactions_count > 0 }>{ p.reactions_count }</p>
</button> </button>
<button> <button onclick={ menu } ref="menuButton">
<i class="fa fa-ellipsis-h"></i> <i class="fa fa-ellipsis-h"></i>
</button> </button>
<button onclick={ toggleDetail } title="%i18n:desktop.tags.mk-timeline-post.detail"> <button onclick={ toggleDetail } title="%i18n:desktop.tags.mk-timeline-post.detail">
@ -525,6 +525,13 @@
}); });
}; };
this.menu = () => {
riot.mount(document.body.appendChild(document.createElement('mk-post-menu')), {
source: this.refs.menuButton,
post: this.p
});
};
this.toggleDetail = () => { this.toggleDetail = () => {
this.update({ this.update({
isDetailOpened: !this.isDetailOpened isDetailOpened: !this.isDetailOpened

View file

@ -2,7 +2,9 @@
<mk-ui ref="ui"> <mk-ui ref="ui">
<main if={ !parent.fetching }> <main if={ !parent.fetching }>
<a if={ parent.post.next } href={ parent.post.next }><i class="fa fa-angle-up"></i>%i18n:mobile.tags.mk-post-page.next%</a> <a if={ parent.post.next } href={ parent.post.next }><i class="fa fa-angle-up"></i>%i18n:mobile.tags.mk-post-page.next%</a>
<div>
<mk-post-detail ref="post" post={ parent.post }/> <mk-post-detail ref="post" post={ parent.post }/>
</div>
<a if={ parent.post.prev } href={ parent.post.prev }><i class="fa fa-angle-down"></i>%i18n:mobile.tags.mk-post-page.prev%</a> <a if={ parent.post.prev } href={ parent.post.prev }><i class="fa fa-angle-down"></i>%i18n:mobile.tags.mk-post-page.prev%</a>
</main> </main>
</mk-ui> </mk-ui>
@ -13,6 +15,16 @@
main main
text-align center text-align center
> div
margin 8px auto
padding 0
max-width 500px
width calc(100% - 16px)
@media (min-width 500px)
margin 16px auto
width calc(100% - 32px)
> a > a
display inline-block display inline-block

View file

@ -38,24 +38,26 @@
</div> </div>
<mk-poll if={ p.poll } post={ p }/> <mk-poll if={ p.poll } post={ p }/>
</div> </div>
<a class="time" href={ url }> <a class="time" href={ '/' + p.user.username + '/' + p.id }>
<mk-time time={ p.created_at } mode="detail"/> <mk-time time={ p.created_at } mode="detail"/>
</a> </a>
<footer> <footer>
<mk-reactions-viewer post={ p }/> <mk-reactions-viewer post={ p }/>
<button onclick={ reply } title="%i18n:mobile.tags.mk-post-detail.reply%"><i class="fa fa-reply"></i> <button onclick={ reply } title="%i18n:mobile.tags.mk-post-detail.reply%">
<p class="count" if={ p.replies_count > 0 }>{ p.replies_count }</p> <i class="fa fa-reply"></i><p class="count" if={ p.replies_count > 0 }>{ p.replies_count }</p>
</button> </button>
<button onclick={ repost } title="Repost"><i class="fa fa-retweet"></i> <button onclick={ repost } title="Repost">
<p class="count" if={ p.repost_count > 0 }>{ p.repost_count }</p> <i class="fa fa-retweet"></i><p class="count" if={ p.repost_count > 0 }>{ p.repost_count }</p>
</button> </button>
<button class={ reacted: p.my_reaction != null } onclick={ react } ref="reactButton" title="%i18n:mobile.tags.mk-post-detail.reaction%"><i class="fa fa-plus"></i> <button class={ reacted: p.my_reaction != null } onclick={ react } ref="reactButton" title="%i18n:mobile.tags.mk-post-detail.reaction%">
<p class="count" if={ p.reactions_count > 0 }>{ p.reactions_count }</p> <i class="fa fa-plus"></i><p class="count" if={ p.reactions_count > 0 }>{ p.reactions_count }</p>
</button>
<button onclick={ menu } ref="menuButton">
<i class="fa fa-ellipsis-h"></i>
</button> </button>
<button><i class="fa fa-ellipsis-h"></i></button>
</footer> </footer>
</article> </article>
<div class="replies"> <div class="replies" if={ !compact }>
<virtual each={ post in replies }> <virtual each={ post in replies }>
<mk-post-detail-sub post={ post }/> <mk-post-detail-sub post={ post }/>
</virtual> </virtual>
@ -64,19 +66,14 @@
:scope :scope
display block display block
overflow hidden overflow hidden
margin 8px auto margin 0 auto
padding 0 padding 0
max-width 500px width 100%
width calc(100% - 16px)
text-align left text-align left
background #fff background #fff
border-radius 8px border-radius 8px
box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2) box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2)
@media (min-width 500px)
margin 16px auto
width calc(100% - 32px)
> .fetching > .fetching
padding 64px 0 padding 64px 0
@ -269,6 +266,7 @@
this.mixin('api'); this.mixin('api');
this.compact = this.opts.compact;
this.post = this.opts.post; this.post = this.opts.post;
this.isRepost = this.post.repost != null; this.isRepost = this.post.repost != null;
this.p = this.isRepost ? this.post.repost : this.post; this.p = this.isRepost ? this.post.repost : this.post;
@ -299,6 +297,7 @@
} }
// Get replies // Get replies
if (!this.compact) {
this.api('posts/replies', { this.api('posts/replies', {
post_id: this.p.id, post_id: this.p.id,
limit: 8 limit: 8
@ -307,6 +306,7 @@
replies: replies replies: replies
}); });
}); });
}
}); });
this.reply = () => { this.reply = () => {
@ -332,6 +332,14 @@
}); });
}; };
this.menu = () => {
riot.mount(document.body.appendChild(document.createElement('mk-post-menu')), {
source: this.refs.menuButton,
post: this.p,
compact: true
});
};
this.loadContext = () => { this.loadContext = () => {
this.contextFetching = true; this.contextFetching = true;

View file

@ -181,14 +181,17 @@
</div> </div>
<footer> <footer>
<mk-reactions-viewer post={ p } ref="reactionsViewer"/> <mk-reactions-viewer post={ p } ref="reactionsViewer"/>
<button onclick={ reply }><i class="fa fa-reply"></i> <button onclick={ reply }>
<p class="count" if={ p.replies_count > 0 }>{ p.replies_count }</p> <i class="fa fa-reply"></i><p class="count" if={ p.replies_count > 0 }>{ p.replies_count }</p>
</button> </button>
<button onclick={ repost } title="Repost"><i class="fa fa-retweet"></i> <button onclick={ repost } title="Repost">
<p class="count" if={ p.repost_count > 0 }>{ p.repost_count }</p> <i class="fa fa-retweet"></i><p class="count" if={ p.repost_count > 0 }>{ p.repost_count }</p>
</button> </button>
<button class={ reacted: p.my_reaction != null } onclick={ react } ref="reactButton"><i class="fa fa-plus"></i> <button class={ reacted: p.my_reaction != null } onclick={ react } ref="reactButton">
<p class="count" if={ p.reactions_count > 0 }>{ p.reactions_count }</p> <i class="fa fa-plus"></i><p class="count" if={ p.reactions_count > 0 }>{ p.reactions_count }</p>
</button>
<button class={ reacted: p.my_reaction != null } onclick={ react } ref="reactButton">
<i class="fa fa-ellipsis-h"></i>
</button> </button>
</footer> </footer>
</div> </div>
@ -558,6 +561,14 @@
compact: true compact: true
}); });
}; };
this.menu = () => {
riot.mount(document.body.appendChild(document.createElement('mk-post-menu')), {
source: this.refs.menuButton,
post: this.p,
compact: true
});
};
</script> </script>
</mk-timeline-post> </mk-timeline-post>

View file

@ -215,6 +215,7 @@
</mk-user> </mk-user>
<mk-user-overview> <mk-user-overview>
<mk-post-detail if={ user.pinned_post } post={ user.pinned_post } compact={ true }/>
<section class="recent-posts"> <section class="recent-posts">
<h2><i class="fa fa-comments-o"></i>%i18n:mobile.tags.mk-user-overview.recent-posts%</h2> <h2><i class="fa fa-comments-o"></i>%i18n:mobile.tags.mk-user-overview.recent-posts%</h2>
<div> <div>
@ -240,6 +241,9 @@
max-width 600px max-width 600px
margin 0 auto margin 0 auto
> mk-post-detail
margin 0 0 8px 0
> section > section
background #eee background #eee
border-radius 8px border-radius 8px