Implement #746
This commit is contained in:
parent
61be5c0d10
commit
e7415dd42b
14 changed files with 315 additions and 60 deletions
|
@ -2,6 +2,10 @@ ChangeLog (Release Notes)
|
|||
=========================
|
||||
主に notable な changes を書いていきます
|
||||
|
||||
unreleased
|
||||
----------
|
||||
* New: 投稿のピン留め (#746)
|
||||
|
||||
2508 (2017/08/30)
|
||||
-----------------
|
||||
* New: モバイル版のユーザーページのアクティビティチャートを変更
|
||||
|
|
|
@ -77,6 +77,10 @@ common:
|
|||
show-result: "Show result"
|
||||
voted: "Voted"
|
||||
|
||||
mk-post-menu:
|
||||
pin: "Pin"
|
||||
pinned: "Pinned"
|
||||
|
||||
mk-reaction-picker:
|
||||
choose-reaction: "Pick your reaction"
|
||||
|
||||
|
|
|
@ -77,6 +77,10 @@ common:
|
|||
show-result: "結果を見る"
|
||||
voted: "投票済み"
|
||||
|
||||
mk-post-menu:
|
||||
pin: "ピン留め"
|
||||
pinned: "ピン留めしました"
|
||||
|
||||
mk-reaction-picker:
|
||||
choose-reaction: "リアクションを選択"
|
||||
|
||||
|
|
|
@ -167,6 +167,10 @@ const endpoints: Endpoint[] = [
|
|||
name: 'i/regenerate_token',
|
||||
withCredential: true
|
||||
},
|
||||
{
|
||||
name: 'i/pin',
|
||||
kind: 'account-write'
|
||||
},
|
||||
{
|
||||
name: 'i/appdata/get',
|
||||
withCredential: true
|
||||
|
|
44
src/api/endpoints/i/pin.ts
Normal file
44
src/api/endpoints/i/pin.ts
Normal 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);
|
||||
});
|
|
@ -4,6 +4,7 @@
|
|||
import * as mongo from 'mongodb';
|
||||
import deepcopy = require('deepcopy');
|
||||
import User from '../models/user';
|
||||
import serializePost from './post';
|
||||
import Following from '../models/following';
|
||||
import getFriends from '../common/get-friends';
|
||||
import config from '../../conf';
|
||||
|
@ -116,7 +117,14 @@ export default (
|
|||
_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);
|
||||
|
||||
// Get following you know count
|
||||
|
@ -135,6 +143,7 @@ export default (
|
|||
});
|
||||
_user.followers_you_know_count = followersYouKnowCount;
|
||||
}
|
||||
}
|
||||
|
||||
resolve(_user);
|
||||
});
|
||||
|
|
|
@ -28,3 +28,4 @@ require('./reaction-picker.tag');
|
|||
require('./reactions-viewer.tag');
|
||||
require('./reaction-icon.tag');
|
||||
require('./weekly-activity-chart.tag');
|
||||
require('./post-menu.tag');
|
||||
|
|
134
src/web/app/common/tags/post-menu.tag
Normal file
134
src/web/app/common/tags/post-menu.tag
Normal 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>
|
|
@ -43,16 +43,18 @@
|
|||
</div>
|
||||
<footer>
|
||||
<mk-reactions-viewer post={ p }/>
|
||||
<button onclick={ reply } title="返信"><i class="fa fa-reply"></i>
|
||||
<p class="count" if={ p.replies_count > 0 }>{ p.replies_count }</p>
|
||||
<button onclick={ reply } title="返信">
|
||||
<i class="fa fa-reply"></i><p class="count" if={ p.replies_count > 0 }>{ p.replies_count }</p>
|
||||
</button>
|
||||
<button onclick={ repost } title="Repost"><i class="fa fa-retweet"></i>
|
||||
<p class="count" if={ p.repost_count > 0 }>{ p.repost_count }</p>
|
||||
<button onclick={ repost } title="Repost">
|
||||
<i class="fa fa-retweet"></i><p class="count" if={ p.repost_count > 0 }>{ p.repost_count }</p>
|
||||
</button>
|
||||
<button class={ reacted: p.my_reaction != null } onclick={ react } ref="reactButton" title="リアクション"><i class="fa fa-plus"></i>
|
||||
<p class="count" if={ p.reactions_count > 0 }>{ p.reactions_count }</p>
|
||||
<button class={ reacted: p.my_reaction != null } onclick={ react } ref="reactButton" title="リアクション">
|
||||
<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><i class="fa fa-ellipsis-h"></i></button>
|
||||
</footer>
|
||||
</article>
|
||||
<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.contextFetching = true;
|
||||
|
||||
|
|
|
@ -128,16 +128,16 @@
|
|||
</div>
|
||||
<footer>
|
||||
<mk-reactions-viewer post={ p } ref="reactionsViewer"/>
|
||||
<button onclick={ reply } title="%i18n:desktop.tags.mk-timeline-post.reply%"><i class="fa fa-reply"></i>
|
||||
<p class="count" if={ p.replies_count > 0 }>{ p.replies_count }</p>
|
||||
<button onclick={ reply } title="%i18n:desktop.tags.mk-timeline-post.reply%">
|
||||
<i class="fa fa-reply"></i><p class="count" if={ p.replies_count > 0 }>{ p.replies_count }</p>
|
||||
</button>
|
||||
<button onclick={ repost } title="%i18n:desktop.tags.mk-timeline-post.repost%"><i class="fa fa-retweet"></i>
|
||||
<p class="count" if={ p.repost_count > 0 }>{ p.repost_count }</p>
|
||||
<button onclick={ repost } title="%i18n:desktop.tags.mk-timeline-post.repost%">
|
||||
<i class="fa fa-retweet"></i><p class="count" if={ p.repost_count > 0 }>{ p.repost_count }</p>
|
||||
</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>
|
||||
<p class="count" if={ p.reactions_count > 0 }>{ p.reactions_count }</p>
|
||||
<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><p class="count" if={ p.reactions_count > 0 }>{ p.reactions_count }</p>
|
||||
</button>
|
||||
<button>
|
||||
<button onclick={ menu } ref="menuButton">
|
||||
<i class="fa fa-ellipsis-h"></i>
|
||||
</button>
|
||||
<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.update({
|
||||
isDetailOpened: !this.isDetailOpened
|
||||
|
|
|
@ -2,7 +2,9 @@
|
|||
<mk-ui ref="ui">
|
||||
<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>
|
||||
<div>
|
||||
<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>
|
||||
</main>
|
||||
</mk-ui>
|
||||
|
@ -13,6 +15,16 @@
|
|||
main
|
||||
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
|
||||
display inline-block
|
||||
|
||||
|
|
|
@ -38,24 +38,26 @@
|
|||
</div>
|
||||
<mk-poll if={ p.poll } post={ p }/>
|
||||
</div>
|
||||
<a class="time" href={ url }>
|
||||
<a class="time" href={ '/' + p.user.username + '/' + p.id }>
|
||||
<mk-time time={ p.created_at } mode="detail"/>
|
||||
</a>
|
||||
<footer>
|
||||
<mk-reactions-viewer post={ p }/>
|
||||
<button onclick={ reply } title="%i18n:mobile.tags.mk-post-detail.reply%"><i class="fa fa-reply"></i>
|
||||
<p class="count" if={ p.replies_count > 0 }>{ p.replies_count }</p>
|
||||
<button onclick={ reply } title="%i18n:mobile.tags.mk-post-detail.reply%">
|
||||
<i class="fa fa-reply"></i><p class="count" if={ p.replies_count > 0 }>{ p.replies_count }</p>
|
||||
</button>
|
||||
<button onclick={ repost } title="Repost"><i class="fa fa-retweet"></i>
|
||||
<p class="count" if={ p.repost_count > 0 }>{ p.repost_count }</p>
|
||||
<button onclick={ repost } title="Repost">
|
||||
<i class="fa fa-retweet"></i><p class="count" if={ p.repost_count > 0 }>{ p.repost_count }</p>
|
||||
</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>
|
||||
<p class="count" if={ p.reactions_count > 0 }>{ p.reactions_count }</p>
|
||||
<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><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><i class="fa fa-ellipsis-h"></i></button>
|
||||
</footer>
|
||||
</article>
|
||||
<div class="replies">
|
||||
<div class="replies" if={ !compact }>
|
||||
<virtual each={ post in replies }>
|
||||
<mk-post-detail-sub post={ post }/>
|
||||
</virtual>
|
||||
|
@ -64,19 +66,14 @@
|
|||
:scope
|
||||
display block
|
||||
overflow hidden
|
||||
margin 8px auto
|
||||
margin 0 auto
|
||||
padding 0
|
||||
max-width 500px
|
||||
width calc(100% - 16px)
|
||||
width 100%
|
||||
text-align left
|
||||
background #fff
|
||||
border-radius 8px
|
||||
box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2)
|
||||
|
||||
@media (min-width 500px)
|
||||
margin 16px auto
|
||||
width calc(100% - 32px)
|
||||
|
||||
> .fetching
|
||||
padding 64px 0
|
||||
|
||||
|
@ -269,6 +266,7 @@
|
|||
|
||||
this.mixin('api');
|
||||
|
||||
this.compact = this.opts.compact;
|
||||
this.post = this.opts.post;
|
||||
this.isRepost = this.post.repost != null;
|
||||
this.p = this.isRepost ? this.post.repost : this.post;
|
||||
|
@ -299,6 +297,7 @@
|
|||
}
|
||||
|
||||
// Get replies
|
||||
if (!this.compact) {
|
||||
this.api('posts/replies', {
|
||||
post_id: this.p.id,
|
||||
limit: 8
|
||||
|
@ -307,6 +306,7 @@
|
|||
replies: replies
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
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.contextFetching = true;
|
||||
|
||||
|
|
|
@ -181,14 +181,17 @@
|
|||
</div>
|
||||
<footer>
|
||||
<mk-reactions-viewer post={ p } ref="reactionsViewer"/>
|
||||
<button onclick={ reply }><i class="fa fa-reply"></i>
|
||||
<p class="count" if={ p.replies_count > 0 }>{ p.replies_count }</p>
|
||||
<button onclick={ reply }>
|
||||
<i class="fa fa-reply"></i><p class="count" if={ p.replies_count > 0 }>{ p.replies_count }</p>
|
||||
</button>
|
||||
<button onclick={ repost } title="Repost"><i class="fa fa-retweet"></i>
|
||||
<p class="count" if={ p.repost_count > 0 }>{ p.repost_count }</p>
|
||||
<button onclick={ repost } title="Repost">
|
||||
<i class="fa fa-retweet"></i><p class="count" if={ p.repost_count > 0 }>{ p.repost_count }</p>
|
||||
</button>
|
||||
<button class={ reacted: p.my_reaction != null } onclick={ react } ref="reactButton"><i class="fa fa-plus"></i>
|
||||
<p class="count" if={ p.reactions_count > 0 }>{ p.reactions_count }</p>
|
||||
<button class={ reacted: p.my_reaction != null } onclick={ react } ref="reactButton">
|
||||
<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>
|
||||
</footer>
|
||||
</div>
|
||||
|
@ -558,6 +561,14 @@
|
|||
compact: true
|
||||
});
|
||||
};
|
||||
|
||||
this.menu = () => {
|
||||
riot.mount(document.body.appendChild(document.createElement('mk-post-menu')), {
|
||||
source: this.refs.menuButton,
|
||||
post: this.p,
|
||||
compact: true
|
||||
});
|
||||
};
|
||||
</script>
|
||||
</mk-timeline-post>
|
||||
|
||||
|
|
|
@ -215,6 +215,7 @@
|
|||
</mk-user>
|
||||
|
||||
<mk-user-overview>
|
||||
<mk-post-detail if={ user.pinned_post } post={ user.pinned_post } compact={ true }/>
|
||||
<section class="recent-posts">
|
||||
<h2><i class="fa fa-comments-o"></i>%i18n:mobile.tags.mk-user-overview.recent-posts%</h2>
|
||||
<div>
|
||||
|
@ -240,6 +241,9 @@
|
|||
max-width 600px
|
||||
margin 0 auto
|
||||
|
||||
> mk-post-detail
|
||||
margin 0 0 8px 0
|
||||
|
||||
> section
|
||||
background #eee
|
||||
border-radius 8px
|
||||
|
|
Loading…
Reference in a new issue