diff --git a/src/web/app/desktop/-tags/pages/user.tag b/src/web/app/desktop/-tags/pages/user.tag deleted file mode 100644 index abed2ef021..0000000000 --- a/src/web/app/desktop/-tags/pages/user.tag +++ /dev/null @@ -1,27 +0,0 @@ -<mk-user-page> - <mk-ui ref="ui"> - <mk-user ref="user" user={ parent.user } page={ parent.opts.page }/> - </mk-ui> - <style lang="stylus" scoped> - :scope - display block - </style> - <script lang="typescript"> - import Progress from '../../../common/scripts/loading'; - - this.user = this.opts.user; - - this.on('mount', () => { - Progress.start(); - - this.$refs.ui.refs.user.on('user-fetched', user => { - Progress.set(0.5); - document.title = user.name + ' | Misskey'; - }); - - this.$refs.ui.refs.user.on('loaded', () => { - Progress.done(); - }); - }); - </script> -</mk-user-page> diff --git a/src/web/app/desktop/-tags/user.tag b/src/web/app/desktop/-tags/user.tag deleted file mode 100644 index 8221926f45..0000000000 --- a/src/web/app/desktop/-tags/user.tag +++ /dev/null @@ -1,852 +0,0 @@ -<mk-user> - <div class="user" v-if="!fetching"> - <header> - <mk-user-header user={ user }/> - </header> - <mk-user-home v-if="page == 'home'" user={ user }/> - <mk-user-graphs v-if="page == 'graphs'" user={ user }/> - </div> - <style lang="stylus" scoped> - :scope - display block - - > .user - > header - > mk-user-header - overflow hidden - - </style> - <script lang="typescript"> - this.mixin('api'); - - this.username = this.opts.user; - this.page = this.opts.page ? this.opts.page : 'home'; - this.fetching = true; - this.user = null; - - this.on('mount', () => { - this.api('users/show', { - username: this.username - }).then(user => { - this.update({ - fetching: false, - user: user - }); - this.$emit('loaded'); - }); - }); - </script> -</mk-user> - -<mk-user-header data-is-dark-background={ user.banner_url != null }> - <div class="banner-container" style={ user.banner_url ? 'background-image: url(' + user.banner_url + '?thumbnail&size=2048)' : '' }> - <div class="banner" ref="banner" style={ user.banner_url ? 'background-image: url(' + user.banner_url + '?thumbnail&size=2048)' : '' } @click="onUpdateBanner"></div> - </div> - <div class="fade"></div> - <div class="container"> - <img class="avatar" src={ user.avatar_url + '?thumbnail&size=150' } alt="avatar"/> - <div class="title"> - <p class="name" href={ '/' + user.username }>{ user.name }</p> - <p class="username">@{ user.username }</p> - <p class="location" v-if="user.profile.location">%fa:map-marker%{ user.profile.location }</p> - </div> - <footer> - <a href={ '/' + user.username } data-active={ parent.page == 'home' }>%fa:home%概要</a> - <a href={ '/' + user.username + '/media' } data-active={ parent.page == 'media' }>%fa:image%メディア</a> - <a href={ '/' + user.username + '/graphs' } data-active={ parent.page == 'graphs' }>%fa:chart-bar%グラフ</a> - </footer> - </div> - <style lang="stylus" scoped> - :scope - $banner-height = 320px - $footer-height = 58px - - display block - background #f7f7f7 - box-shadow 0 1px 1px rgba(0, 0, 0, 0.075) - - &[data-is-dark-background] - > .banner-container - > .banner - background-color #383838 - - > .fade - background linear-gradient(transparent, rgba(0, 0, 0, 0.7)) - - > .container - > .title - color #fff - - > .name - text-shadow 0 0 8px #000 - - > .banner-container - height $banner-height - overflow hidden - background-size cover - background-position center - - > .banner - height 100% - background-color #f5f5f5 - background-size cover - background-position center - - > .fade - $fade-hight = 78px - - position absolute - top ($banner-height - $fade-hight) - left 0 - width 100% - height $fade-hight - - > .container - max-width 1200px - margin 0 auto - - > .avatar - display block - position absolute - bottom 16px - left 16px - z-index 2 - width 160px - height 160px - margin 0 - border solid 3px #fff - border-radius 8px - box-shadow 1px 1px 3px rgba(0, 0, 0, 0.2) - - > .title - position absolute - bottom $footer-height - left 0 - width 100% - padding 0 0 8px 195px - color #656565 - font-family '游ゴシック', 'YuGothic', 'ヒラギノ角ゴ ProN W3', 'Hiragino Kaku Gothic ProN', 'Meiryo', 'メイリオ', sans-serif - - > .name - display block - margin 0 - line-height 40px - font-weight bold - font-size 2em - - > .username - > .location - display inline-block - margin 0 16px 0 0 - line-height 20px - opacity 0.8 - - > i - margin-right 4px - - > footer - z-index 1 - height $footer-height - padding-left 195px - - > a - display inline-block - margin 0 - padding 0 16px - height $footer-height - line-height $footer-height - color #555 - - &[data-active] - border-bottom solid 4px $theme-color - - > i - margin-right 6px - - > button - display block - position absolute - top 0 - right 0 - margin 8px - padding 0 - width $footer-height - 16px - line-height $footer-height - 16px - 2px - font-size 1.2em - color #777 - border solid 1px #eee - border-radius 4px - - &:hover - color #555 - border solid 1px #ddd - - </style> - <script lang="typescript"> - import updateBanner from '../scripts/update-banner'; - - this.mixin('i'); - - this.user = this.opts.user; - - this.on('mount', () => { - window.addEventListener('load', this.scroll); - window.addEventListener('scroll', this.scroll); - window.addEventListener('resize', this.scroll); - }); - - this.on('unmount', () => { - window.removeEventListener('load', this.scroll); - window.removeEventListener('scroll', this.scroll); - window.removeEventListener('resize', this.scroll); - }); - - this.scroll = () => { - const top = window.scrollY; - - const z = 1.25; // 奥行き(小さいほど奥) - const pos = -(top / z); - this.$refs.banner.style.backgroundPosition = `center calc(50% - ${pos}px)`; - - const blur = top / 32 - if (blur <= 10) this.$refs.banner.style.filter = `blur(${blur}px)`; - }; - - this.onUpdateBanner = () => { - if (!this.SIGNIN || this.I.id != this.user.id) return; - - updateBanner(this.I, i => { - this.user.banner_url = i.banner_url; - this.update(); - }); - }; - </script> -</mk-user-header> - -<mk-user-profile> - <div class="friend-form" v-if="SIGNIN && I.id != user.id"> - <mk-big-follow-button user={ user }/> - <p class="followed" v-if="user.is_followed">%i18n:desktop.tags.mk-user.follows-you%</p> - <p v-if="user.is_muted">%i18n:desktop.tags.mk-user.muted% <a @click="unmute">%i18n:desktop.tags.mk-user.unmute%</a></p> - <p v-if="!user.is_muted"><a @click="mute">%i18n:desktop.tags.mk-user.mute%</a></p> - </div> - <div class="description" v-if="user.description">{ user.description }</div> - <div class="birthday" v-if="user.profile.birthday"> - <p>%fa:birthday-cake%{ user.profile.birthday.replace('-', '年').replace('-', '月') + '日' } ({ age(user.profile.birthday) }歳)</p> - </div> - <div class="twitter" v-if="user.twitter"> - <p>%fa:B twitter%<a href={ 'https://twitter.com/' + user.twitter.screen_name } target="_blank">@{ user.twitter.screen_name }</a></p> - </div> - <div class="status"> - <p class="posts-count">%fa:angle-right%<a>{ user.posts_count }</a><b>ポスト</b></p> - <p class="following">%fa:angle-right%<a @click="showFollowing">{ user.following_count }</a>人を<b>フォロー</b></p> - <p class="followers">%fa:angle-right%<a @click="showFollowers">{ user.followers_count }</a>人の<b>フォロワー</b></p> - </div> - <style lang="stylus" scoped> - :scope - display block - background #fff - border solid 1px rgba(0, 0, 0, 0.075) - border-radius 6px - - > *:first-child - border-top none !important - - > .friend-form - padding 16px - border-top solid 1px #eee - - > mk-big-follow-button - width 100% - - > .followed - margin 12px 0 0 0 - padding 0 - text-align center - line-height 24px - font-size 0.8em - color #71afc7 - background #eefaff - border-radius 4px - - > .description - padding 16px - color #555 - border-top solid 1px #eee - - > .birthday - padding 16px - color #555 - border-top solid 1px #eee - - > p - margin 0 - - > i - margin-right 8px - - > .twitter - padding 16px - color #555 - border-top solid 1px #eee - - > p - margin 0 - - > i - margin-right 8px - - > .status - padding 16px - color #555 - border-top solid 1px #eee - - > p - margin 8px 0 - - > i - margin-left 8px - margin-right 8px - - </style> - <script lang="typescript"> - this.age = require('s-age'); - - this.mixin('i'); - this.mixin('api'); - - this.user = this.opts.user; - - this.showFollowing = () => { - riot.mount(document.body.appendChild(document.createElement('mk-user-following-window')), { - user: this.user - }); - }; - - this.showFollowers = () => { - riot.mount(document.body.appendChild(document.createElement('mk-user-followers-window')), { - user: this.user - }); - }; - - this.mute = () => { - this.api('mute/create', { - user_id: this.user.id - }).then(() => { - this.user.is_muted = true; - this.update(); - }, e => { - alert('error'); - }); - }; - - this.unmute = () => { - this.api('mute/delete', { - user_id: this.user.id - }).then(() => { - this.user.is_muted = false; - this.update(); - }, e => { - alert('error'); - }); - }; - </script> -</mk-user-profile> - -<mk-user-photos> - <p class="title">%fa:camera%%i18n:desktop.tags.mk-user.photos.title%</p> - <p class="initializing" v-if="initializing">%fa:spinner .pulse .fw%%i18n:desktop.tags.mk-user.photos.loading%<mk-ellipsis/></p> - <div class="stream" v-if="!initializing && images.length > 0"> - <template each={ image in images }> - <div class="img" style={ 'background-image: url(' + image.url + '?thumbnail&size=256)' }></div> - </template> - </div> - <p class="empty" v-if="!initializing && images.length == 0">%i18n:desktop.tags.mk-user.photos.no-photos%</p> - <style lang="stylus" scoped> - :scope - display block - background #fff - border solid 1px rgba(0, 0, 0, 0.075) - border-radius 6px - - > .title - z-index 1 - margin 0 - padding 0 16px - line-height 42px - font-size 0.9em - font-weight bold - color #888 - box-shadow 0 1px rgba(0, 0, 0, 0.07) - - > i - margin-right 4px - - > .stream - display -webkit-flex - display -moz-flex - display -ms-flex - display flex - justify-content center - flex-wrap wrap - padding 8px - - > .img - flex 1 1 33% - width 33% - height 80px - background-position center center - background-size cover - background-clip content-box - border solid 2px transparent - - > .initializing - > .empty - margin 0 - padding 16px - text-align center - color #aaa - - > i - margin-right 4px - - </style> - <script lang="typescript"> - import isPromise from '../../common/scripts/is-promise'; - - this.mixin('api'); - - this.images = []; - this.initializing = true; - this.user = null; - this.userPromise = isPromise(this.opts.user) - ? this.opts.user - : Promise.resolve(this.opts.user); - - this.on('mount', () => { - this.userPromise.then(user => { - this.update({ - user: user - }); - - this.api('users/posts', { - user_id: this.user.id, - with_media: true, - limit: 9 - }).then(posts => { - this.initializing = false; - posts.forEach(post => { - post.media.forEach(media => { - if (this.images.length < 9) this.images.push(media); - }); - }); - this.update(); - }); - }); - }); - </script> -</mk-user-photos> - -<mk-user-frequently-replied-users> - <p class="title">%fa:users%%i18n:desktop.tags.mk-user.frequently-replied-users.title%</p> - <p class="initializing" v-if="initializing">%fa:spinner .pulse .fw%%i18n:desktop.tags.mk-user.frequently-replied-users.loading%<mk-ellipsis/></p> - <div class="user" v-if="!initializing && users.length != 0" each={ _user in users }> - <a class="avatar-anchor" href={ '/' + _user.username }> - <img class="avatar" src={ _user.avatar_url + '?thumbnail&size=42' } alt="" data-user-preview={ _user.id }/> - </a> - <div class="body"> - <a class="name" href={ '/' + _user.username } data-user-preview={ _user.id }>{ _user.name }</a> - <p class="username">@{ _user.username }</p> - </div> - <mk-follow-button user={ _user }/> - </div> - <p class="empty" v-if="!initializing && users.length == 0">%i18n:desktop.tags.mk-user.frequently-replied-users.no-users%</p> - <style lang="stylus" scoped> - :scope - display block - background #fff - border solid 1px rgba(0, 0, 0, 0.075) - border-radius 6px - - > .title - z-index 1 - margin 0 - padding 0 16px - line-height 42px - font-size 0.9em - font-weight bold - color #888 - box-shadow 0 1px rgba(0, 0, 0, 0.07) - - > i - margin-right 4px - - > .initializing - > .empty - margin 0 - padding 16px - text-align center - color #aaa - - > i - margin-right 4px - - > .user - padding 16px - border-bottom solid 1px #eee - - &:last-child - border-bottom none - - &:after - content "" - display block - clear both - - > .avatar-anchor - display block - float left - margin 0 12px 0 0 - - > .avatar - display block - width 42px - height 42px - margin 0 - border-radius 8px - vertical-align bottom - - > .body - float left - width calc(100% - 54px) - - > .name - margin 0 - font-size 16px - line-height 24px - color #555 - - > .username - display block - margin 0 - font-size 15px - line-height 16px - color #ccc - - > mk-follow-button - position absolute - top 16px - right 16px - - </style> - <script lang="typescript"> - 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, - limit: 4 - }).then(docs => { - this.update({ - users: docs.map(doc => doc.user), - initializing: false - }); - }); - }); - </script> -</mk-user-frequently-replied-users> - -<mk-user-followers-you-know> - <p class="title">%fa:users%%i18n:desktop.tags.mk-user.followers-you-know.title%</p> - <p class="initializing" v-if="initializing">%fa:spinner .pulse .fw%%i18n:desktop.tags.mk-user.followers-you-know.loading%<mk-ellipsis/></p> - <div v-if="!initializing && users.length > 0"> - <template each={ user in users }> - <a href={ '/' + user.username }><img src={ user.avatar_url + '?thumbnail&size=64' } alt={ user.name }/></a> - </template> - </div> - <p class="empty" v-if="!initializing && users.length == 0">%i18n:desktop.tags.mk-user.followers-you-know.no-users%</p> - <style lang="stylus" scoped> - :scope - display block - background #fff - border solid 1px rgba(0, 0, 0, 0.075) - border-radius 6px - - > .title - z-index 1 - margin 0 - padding 0 16px - line-height 42px - font-size 0.9em - font-weight bold - color #888 - box-shadow 0 1px rgba(0, 0, 0, 0.07) - - > i - margin-right 4px - - > div - padding 8px - - > a - display inline-block - margin 4px - - > img - width 48px - height 48px - vertical-align bottom - border-radius 100% - - > .initializing - > .empty - margin 0 - padding 16px - text-align center - color #aaa - - > i - margin-right 4px - - </style> - <script lang="typescript"> - this.mixin('api'); - - this.user = this.opts.user; - this.initializing = true; - - this.on('mount', () => { - this.api('users/followers', { - user_id: this.user.id, - iknow: true, - limit: 16 - }).then(x => { - this.update({ - users: x.users, - initializing: false - }); - }); - }); - </script> -</mk-user-followers-you-know> - -<mk-user-home> - <div> - <div ref="left"> - <mk-user-profile user={ user }/> - <mk-user-photos user={ user }/> - <mk-user-followers-you-know v-if="SIGNIN && I.id !== user.id" user={ user }/> - <p>%i18n:desktop.tags.mk-user.last-used-at%: <b><mk-time time={ user.last_used_at }/></b></p> - </div> - </div> - <main> - <mk-post-detail v-if="user.pinned_post" post={ user.pinned_post } compact={ true }/> - <mk-user-timeline ref="tl" user={ user }/> - </main> - <div> - <div ref="right"> - <mk-calendar-widget warp={ warp } start={ new Date(user.created_at) }/> - <mk-activity-widget user={ user }/> - <mk-user-frequently-replied-users user={ user }/> - <div class="nav"><mk-nav-links/></div> - </div> - </div> - <style lang="stylus" scoped> - :scope - display flex - justify-content center - margin 0 auto - max-width 1200px - - > main - > div > div - > *:not(:last-child) - margin-bottom 16px - - > main - padding 16px - width calc(100% - 275px * 2) - - > mk-user-timeline - border solid 1px rgba(0, 0, 0, 0.075) - border-radius 6px - - > div - width 275px - margin 0 - - &:first-child > div - padding 16px 0 16px 16px - - > p - display block - margin 0 - padding 0 12px - text-align center - font-size 0.8em - color #aaa - - &:last-child > div - padding 16px 16px 16px 0 - - > .nav - padding 16px - font-size 12px - color #aaa - background #fff - border solid 1px rgba(0, 0, 0, 0.075) - border-radius 6px - - a - color #999 - - i - color #ccc - - </style> - <script lang="typescript"> - import ScrollFollower from '../scripts/scroll-follower'; - - this.mixin('i'); - - this.user = this.opts.user; - - this.on('mount', () => { - this.$refs.tl.on('loaded', () => { - this.$emit('loaded'); - }); - - this.scrollFollowerLeft = new ScrollFollower(this.$refs.left, this.parent.root.getBoundingClientRect().top); - this.scrollFollowerRight = new ScrollFollower(this.$refs.right, this.parent.root.getBoundingClientRect().top); - }); - - this.on('unmount', () => { - this.scrollFollowerLeft.dispose(); - this.scrollFollowerRight.dispose(); - }); - - this.warp = date => { - this.$refs.tl.warp(date); - }; - </script> -</mk-user-home> - -<mk-user-graphs> - <section> - <div> - <h1>%fa:pencil-alt%投稿</h1> - <mk-user-graphs-activity-chart user={ opts.user }/> - </div> - </section> - <section> - <div> - <h1>フォロー/フォロワー</h1> - <mk-user-friends-graph user={ opts.user }/> - </div> - </section> - <section> - <div> - <h1>いいね</h1> - <mk-user-likes-graph user={ opts.user }/> - </div> - </section> - <style lang="stylus" scoped> - :scope - display block - - > section - margin 16px 0 - color #666 - border-bottom solid 1px rgba(0, 0, 0, 0.1) - - > div - max-width 1200px - margin 0 auto - padding 0 16px - - > h1 - margin 0 0 16px 0 - padding 0 - font-size 1.3em - - > i - margin-right 8px - - </style> - <script lang="typescript"> - this.on('mount', () => { - this.$emit('loaded'); - }); - </script> -</mk-user-graphs> - -<mk-user-graphs-activity-chart> - <svg v-if="data" ref="canvas" viewBox="0 0 365 1" preserveAspectRatio="none"> - <g each={ d, i in data.reverse() }> - <rect width="0.8" riot-height={ d.postsH } - riot-x={ i + 0.1 } riot-y={ 1 - d.postsH - d.repliesH - d.repostsH } - fill="#41ddde"/> - <rect width="0.8" riot-height={ d.repliesH } - riot-x={ i + 0.1 } riot-y={ 1 - d.repliesH - d.repostsH } - fill="#f7796c"/> - <rect width="0.8" riot-height={ d.repostsH } - riot-x={ i + 0.1 } riot-y={ 1 - d.repostsH } - fill="#a1de41"/> - </g> - </svg> - <p>直近1年間分の統計です。一番右が現在で、一番左が1年前です。青は通常の投稿、赤は返信、緑はRepostをそれぞれ表しています。</p> - <p> - <span>だいたい*1日に<b>{ averageOfAllTypePostsEachDays }回</b>投稿(返信、Repost含む)しています。</span><br> - <span>だいたい*1日に<b>{ averageOfPostsEachDays }回</b>投稿(通常の)しています。</span><br> - <span>だいたい*1日に<b>{ averageOfRepliesEachDays }回</b>返信しています。</span><br> - <span>だいたい*1日に<b>{ averageOfRepostsEachDays }回</b>Repostしています。</span><br> - </p> - <p>* 中央値</p> - - <style lang="stylus" scoped> - :scope - display block - - > svg - display block - width 100% - height 180px - - > rect - transform-origin center - - </style> - <script lang="typescript"> - import getMedian from '../../common/scripts/get-median'; - - this.mixin('api'); - - this.user = this.opts.user; - - this.on('mount', () => { - this.api('aggregation/users/activity', { - user_id: this.user.id, - limit: 365 - }).then(data => { - data.forEach(d => d.total = d.posts + d.replies + d.reposts); - this.peak = Math.max.apply(null, data.map(d => d.total)); - data.forEach(d => { - d.postsH = d.posts / this.peak; - d.repliesH = d.replies / this.peak; - d.repostsH = d.reposts / this.peak; - }); - - this.update({ - data, - averageOfAllTypePostsEachDays: getMedian(data.map(d => d.total)), - averageOfPostsEachDays: getMedian(data.map(d => d.posts)), - averageOfRepliesEachDays: getMedian(data.map(d => d.replies)), - averageOfRepostsEachDays: getMedian(data.map(d => d.reposts)) - }); - }); - }); - </script> -</mk-user-graphs-activity-chart> diff --git a/src/web/app/desktop/views/pages/user/user-followers-you-know.vue b/src/web/app/desktop/views/pages/user/user-followers-you-know.vue new file mode 100644 index 0000000000..4190081750 --- /dev/null +++ b/src/web/app/desktop/views/pages/user/user-followers-you-know.vue @@ -0,0 +1,79 @@ +<template> +<div class="mk-user-followers-you-know"> + <p class="title">%fa:users%%i18n:desktop.tags.mk-user.followers-you-know.title%</p> + <p class="initializing" v-if="initializing">%fa:spinner .pulse .fw%%i18n:desktop.tags.mk-user.followers-you-know.loading%<mk-ellipsis/></p> + <div v-if="!initializing && users.length > 0"> + <template each={ user in users }> + <a href={ '/' + user.username }><img src={ user.avatar_url + '?thumbnail&size=64' } alt={ user.name }/></a> + </template> + </div> + <p class="empty" v-if="!initializing && users.length == 0">%i18n:desktop.tags.mk-user.followers-you-know.no-users%</p> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +export default Vue.extend({ + props: ['user'], + data() { + return { + users: [], + fetching: true + }; + }, + mounted() { + this.$root.$data.os.api('users/followers', { + user_id: this.user.id, + iknow: true, + limit: 16 + }).then(x => { + this.fetching = false; + this.users = x.users; + }); + } +}); +</script> + +<style lang="stylus" scoped> +.mk-user-followers-you-know + background #fff + border solid 1px rgba(0, 0, 0, 0.075) + border-radius 6px + + > .title + z-index 1 + margin 0 + padding 0 16px + line-height 42px + font-size 0.9em + font-weight bold + color #888 + box-shadow 0 1px rgba(0, 0, 0, 0.07) + + > i + margin-right 4px + + > div + padding 8px + + > a + display inline-block + margin 4px + + > img + width 48px + height 48px + vertical-align bottom + border-radius 100% + + > .initializing + > .empty + margin 0 + padding 16px + text-align center + color #aaa + + > i + margin-right 4px + +</style> diff --git a/src/web/app/desktop/views/pages/user/user-friends.vue b/src/web/app/desktop/views/pages/user/user-friends.vue new file mode 100644 index 0000000000..eed8748978 --- /dev/null +++ b/src/web/app/desktop/views/pages/user/user-friends.vue @@ -0,0 +1,117 @@ +<template> +<div class="mk-user-friends"> + <p class="title">%fa:users%%i18n:desktop.tags.mk-user.frequently-replied-users.title%</p> + <p class="initializing" v-if="fetching">%fa:spinner .pulse .fw%%i18n:desktop.tags.mk-user.frequently-replied-users.loading%<mk-ellipsis/></p> + <div class="user" v-if="!fetching && users.length != 0" each={ _user in users }> + <a class="avatar-anchor" href={ '/' + _user.username }> + <img class="avatar" src={ _user.avatar_url + '?thumbnail&size=42' } alt="" data-user-preview={ _user.id }/> + </a> + <div class="body"> + <a class="name" href={ '/' + _user.username } data-user-preview={ _user.id }>{ _user.name }</a> + <p class="username">@{ _user.username }</p> + </div> + <mk-follow-button user={ _user }/> + </div> + <p class="empty" v-if="!fetching && users.length == 0">%i18n:desktop.tags.mk-user.frequently-replied-users.no-users%</p> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +export default Vue.extend({ + props: ['user'], + data() { + return { + users: [], + fetching: true + }; + }, + mounted() { + this.$root.$data.os.api('users/get_frequently_replied_users', { + user_id: this.user.id, + limit: 4 + }).then(docs => { + this.fetching = false; + this.users = docs.map(doc => doc.user); + }); + } +}); +</script> + +<style lang="stylus" scoped> +.mk-user-friends + background #fff + border solid 1px rgba(0, 0, 0, 0.075) + border-radius 6px + + > .title + z-index 1 + margin 0 + padding 0 16px + line-height 42px + font-size 0.9em + font-weight bold + color #888 + box-shadow 0 1px rgba(0, 0, 0, 0.07) + + > i + margin-right 4px + + > .initializing + > .empty + margin 0 + padding 16px + text-align center + color #aaa + + > i + margin-right 4px + + > .user + padding 16px + border-bottom solid 1px #eee + + &:last-child + border-bottom none + + &:after + content "" + display block + clear both + + > .avatar-anchor + display block + float left + margin 0 12px 0 0 + + > .avatar + display block + width 42px + height 42px + margin 0 + border-radius 8px + vertical-align bottom + + > .body + float left + width calc(100% - 54px) + + > .name + margin 0 + font-size 16px + line-height 24px + color #555 + + > .username + display block + margin 0 + font-size 15px + line-height 16px + color #ccc + + > mk-follow-button + position absolute + top 16px + right 16px + +</style> diff --git a/src/web/app/desktop/views/pages/user/user-header.vue b/src/web/app/desktop/views/pages/user/user-header.vue new file mode 100644 index 0000000000..07f206d241 --- /dev/null +++ b/src/web/app/desktop/views/pages/user/user-header.vue @@ -0,0 +1,189 @@ +<template> +<div class="mk-user-header" :data-is-dark-background="user.banner_url != null"> + <div class="banner-container" :style="user.banner_url ? `background-image: url(${user.banner_url}?thumbnail&size=2048)` : ''"> + <div class="banner" ref="banner" :style="user.banner_url ? `background-image: url(${user.banner_url}?thumbnail&size=2048)` : ''" @click="onBannerClick"></div> + </div> + <div class="fade"></div> + <div class="container"> + <img class="avatar" :src="`${user.avatar_url}?thumbnail&size=150`" alt="avatar"/> + <div class="title"> + <p class="name">{{ user.name }}</p> + <p class="username">@{{ user.username }}</p> + <p class="location" v-if="user.profile.location">%fa:map-marker%{{ user.profile.location }}</p> + </div> + <footer> + <a :href="`/${user.username}`" :data-active="$parent.page == 'home'">%fa:home%概要</a> + <a :href="`/${user.username}/media`" :data-active="$parent.page == 'media'">%fa:image%メディア</a> + <a :href="`/${user.username}/graphs`" :data-active="$parent.page == 'graphs'">%fa:chart-bar%グラフ</a> + </footer> + </div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import updateBanner from '../../../scripts/update-banner'; + +export default Vue.extend({ + props: ['user'], + mounted() { + window.addEventListener('load', this.onScroll); + window.addEventListener('scroll', this.onScroll); + window.addEventListener('resize', this.onScroll); + }, + beforeDestroy() { + window.removeEventListener('load', this.onScroll); + window.removeEventListener('scroll', this.onScroll); + window.removeEventListener('resize', this.onScroll); + }, + methods: { + onScroll() { + const banner = this.$refs.banner as any; + + const top = window.scrollY; + + const z = 1.25; // 奥行き(小さいほど奥) + const pos = -(top / z); + banner.style.backgroundPosition = `center calc(50% - ${pos}px)`; + + const blur = top / 32 + if (blur <= 10) banner.style.filter = `blur(${blur}px)`; + }, + + onBannerClick() { + if (!this.$root.$data.os.isSignedIn || this.$root.$data.os.i.id != this.user.id) return; + + updateBanner(this.$root.$data.os.i, i => { + this.user.banner_url = i.banner_url; + }); + } + } +}); +</script> + +<style lang="stylus" scoped> +.mk-user-header + $banner-height = 320px + $footer-height = 58px + + overflow hidden + background #f7f7f7 + box-shadow 0 1px 1px rgba(0, 0, 0, 0.075) + + &[data-is-dark-background] + > .banner-container + > .banner + background-color #383838 + + > .fade + background linear-gradient(transparent, rgba(0, 0, 0, 0.7)) + + > .container + > .title + color #fff + + > .name + text-shadow 0 0 8px #000 + + > .banner-container + height $banner-height + overflow hidden + background-size cover + background-position center + + > .banner + height 100% + background-color #f5f5f5 + background-size cover + background-position center + + > .fade + $fade-hight = 78px + + position absolute + top ($banner-height - $fade-hight) + left 0 + width 100% + height $fade-hight + + > .container + max-width 1200px + margin 0 auto + + > .avatar + display block + position absolute + bottom 16px + left 16px + z-index 2 + width 160px + height 160px + margin 0 + border solid 3px #fff + border-radius 8px + box-shadow 1px 1px 3px rgba(0, 0, 0, 0.2) + + > .title + position absolute + bottom $footer-height + left 0 + width 100% + padding 0 0 8px 195px + color #656565 + font-family '游ゴシック', 'YuGothic', 'ヒラギノ角ゴ ProN W3', 'Hiragino Kaku Gothic ProN', 'Meiryo', 'メイリオ', sans-serif + + > .name + display block + margin 0 + line-height 40px + font-weight bold + font-size 2em + + > .username + > .location + display inline-block + margin 0 16px 0 0 + line-height 20px + opacity 0.8 + + > i + margin-right 4px + + > footer + z-index 1 + height $footer-height + padding-left 195px + + > a + display inline-block + margin 0 + padding 0 16px + height $footer-height + line-height $footer-height + color #555 + + &[data-active] + border-bottom solid 4px $theme-color + + > i + margin-right 6px + + > button + display block + position absolute + top 0 + right 0 + margin 8px + padding 0 + width $footer-height - 16px + line-height $footer-height - 16px - 2px + font-size 1.2em + color #777 + border solid 1px #eee + border-radius 4px + + &:hover + color #555 + border solid 1px #ddd + +</style> diff --git a/src/web/app/desktop/views/pages/user/user-home.vue b/src/web/app/desktop/views/pages/user/user-home.vue new file mode 100644 index 0000000000..926a1f571e --- /dev/null +++ b/src/web/app/desktop/views/pages/user/user-home.vue @@ -0,0 +1,90 @@ +<template> +<div class="mk-user-home"> + <div> + <div ref="left"> + <mk-user-profile :user="user"/> + <mk-user-photos :user="user"/> + <mk-user-followers-you-know v-if="$root.$data.os.isSignedIn && $root.$data.os.i.id != user.id" :user="user"/> + <p>%i18n:desktop.tags.mk-user.last-used-at%: <b><mk-time :time="user.last_used_at"/></b></p> + </div> + </div> + <main> + <mk-post-detail v-if="user.pinned_post" :post="user.pinned_post" compact/> + <mk-user-timeline ref="tl" :user="user"/> + </main> + <div> + <div ref="right"> + <mk-calendar-widget @warp="warp" :start="new Date(user.created_at)"/> + <mk-activity-widget :user="user"/> + <mk-user-friends :user="user"/> + <div class="nav"><mk-nav-links/></div> + </div> + </div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +export default Vue.extend({ + props: ['user'], + methods: { + warp(date) { + (this.$refs.tl as any).warp(date); + } + } +}); +</script> + +<style lang="stylus" scoped> +.mk-user-home + display flex + justify-content center + margin 0 auto + max-width 1200px + + > main + > div > div + > *:not(:last-child) + margin-bottom 16px + + > main + padding 16px + width calc(100% - 275px * 2) + + > mk-user-timeline + border solid 1px rgba(0, 0, 0, 0.075) + border-radius 6px + + > div + width 275px + margin 0 + + &:first-child > div + padding 16px 0 16px 16px + + > p + display block + margin 0 + padding 0 12px + text-align center + font-size 0.8em + color #aaa + + &:last-child > div + padding 16px 16px 16px 0 + + > .nav + padding 16px + font-size 12px + color #aaa + background #fff + border solid 1px rgba(0, 0, 0, 0.075) + border-radius 6px + + a + color #999 + + i + color #ccc + +</style> diff --git a/src/web/app/desktop/views/pages/user/user-photos.vue b/src/web/app/desktop/views/pages/user/user-photos.vue new file mode 100644 index 0000000000..fc51b9789f --- /dev/null +++ b/src/web/app/desktop/views/pages/user/user-photos.vue @@ -0,0 +1,89 @@ +<template> +<div class="mk-user-photos"> + <p class="title">%fa:camera%%i18n:desktop.tags.mk-user.photos.title%</p> + <p class="initializing" v-if="fetching">%fa:spinner .pulse .fw%%i18n:desktop.tags.mk-user.photos.loading%<mk-ellipsis/></p> + <div class="stream" v-if="!fetching && images.length > 0"> + <div v-for="image in images" :key="image.id" + class="img" + :style="`background-image: url(${image.url}?thumbnail&size=256)`" + ></div> + </div> + <p class="empty" v-if="!fetching && images.length == 0">%i18n:desktop.tags.mk-user.photos.no-photos%</p> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +export default Vue.extend({ + props: ['user'], + data() { + return { + images: [], + fetching: true + }; + }, + mounted() { + this.$root.$data.os.api('users/posts', { + user_id: this.user.id, + with_media: true, + limit: 9 + }).then(posts => { + this.fetching = false; + posts.forEach(post => { + post.media.forEach(media => { + if (this.images.length < 9) this.images.push(media); + }); + }); + }); + } +}); +</script> + +<style lang="stylus" scoped> +.mk-user-photos + background #fff + border solid 1px rgba(0, 0, 0, 0.075) + border-radius 6px + + > .title + z-index 1 + margin 0 + padding 0 16px + line-height 42px + font-size 0.9em + font-weight bold + color #888 + box-shadow 0 1px rgba(0, 0, 0, 0.07) + + > i + margin-right 4px + + > .stream + display -webkit-flex + display -moz-flex + display -ms-flex + display flex + justify-content center + flex-wrap wrap + padding 8px + + > .img + flex 1 1 33% + width 33% + height 80px + background-position center center + background-size cover + background-clip content-box + border solid 2px transparent + + > .initializing + > .empty + margin 0 + padding 16px + text-align center + color #aaa + + > i + margin-right 4px + +</style> diff --git a/src/web/app/desktop/views/pages/user/user-profile.vue b/src/web/app/desktop/views/pages/user/user-profile.vue new file mode 100644 index 0000000000..6b88b47acd --- /dev/null +++ b/src/web/app/desktop/views/pages/user/user-profile.vue @@ -0,0 +1,142 @@ +<template> +<div class="mk-user-profile"> + <div class="friend-form" v-if="$root.$data.os.isSignedIn && $root.$data.os.i.id != user.id"> + <mk-follow-button :user="user" size="big"/> + <p class="followed" v-if="user.is_followed">%i18n:desktop.tags.mk-user.follows-you%</p> + <p v-if="user.is_muted">%i18n:desktop.tags.mk-user.muted% <a @click="unmute">%i18n:desktop.tags.mk-user.unmute%</a></p> + <p v-if="!user.is_muted"><a @click="mute">%i18n:desktop.tags.mk-user.mute%</a></p> + </div> + <div class="description" v-if="user.description">{{ user.description }}</div> + <div class="birthday" v-if="user.profile.birthday"> + <p>%fa:birthday-cake%{{ user.profile.birthday.replace('-', '年').replace('-', '月') + '日' }} ({{ age }}歳)</p> + </div> + <div class="twitter" v-if="user.twitter"> + <p>%fa:B twitter%<a :href="`https://twitter.com/${user.twitter.screen_name}`" target="_blank">@{{ user.twitter.screen_name }}</a></p> + </div> + <div class="status"> + <p class="posts-count">%fa:angle-right%<a>{{ user.posts_count }}</a><b>投稿</b></p> + <p class="following">%fa:angle-right%<a @click="showFollowing">{{ user.following_count }}</a>人を<b>フォロー</b></p> + <p class="followers">%fa:angle-right%<a @click="showFollowers">{{ user.followers_count }}</a>人の<b>フォロワー</b></p> + </div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +const age = require('s-age'); + +export default Vue.extend({ + props: ['user'], + computed: { + age(): number { + return age(this.user.profile.birthday); + } + }, + methods: { + showFollowing() { + document.body.appendChild(new MkUserFollowingWindow({ + parent: this, + propsData: { + user: this.user + } + }).$mount().$el); + }, + + showFollowers() { + document.body.appendChild(new MkUserFollowersWindow({ + parent: this, + propsData: { + user: this.user + } + }).$mount().$el); + }, + + mute() { + this.$root.$data.os.api('mute/create', { + user_id: this.user.id + }).then(() => { + this.user.is_muted = true; + }, e => { + alert('error'); + }); + }, + + unmute() { + this.$root.$data.os.api('mute/delete', { + user_id: this.user.id + }).then(() => { + this.user.is_muted = false; + }, e => { + alert('error'); + }); + } + } +}); +</script> + +<style lang="stylus" scoped> +.mk-user-profile + background #fff + border solid 1px rgba(0, 0, 0, 0.075) + border-radius 6px + + > *:first-child + border-top none !important + + > .friend-form + padding 16px + border-top solid 1px #eee + + > mk-big-follow-button + width 100% + + > .followed + margin 12px 0 0 0 + padding 0 + text-align center + line-height 24px + font-size 0.8em + color #71afc7 + background #eefaff + border-radius 4px + + > .description + padding 16px + color #555 + border-top solid 1px #eee + + > .birthday + padding 16px + color #555 + border-top solid 1px #eee + + > p + margin 0 + + > i + margin-right 8px + + > .twitter + padding 16px + color #555 + border-top solid 1px #eee + + > p + margin 0 + + > i + margin-right 8px + + > .status + padding 16px + color #555 + border-top solid 1px #eee + + > p + margin 8px 0 + + > i + margin-left 8px + margin-right 8px + +</style> diff --git a/src/web/app/desktop/views/pages/user/user.vue b/src/web/app/desktop/views/pages/user/user.vue new file mode 100644 index 0000000000..109ee6037e --- /dev/null +++ b/src/web/app/desktop/views/pages/user/user.vue @@ -0,0 +1,43 @@ +<template> +<mk-ui> + <div class="user" v-if="!fetching"> + <mk-user-header :user="user"/> + <mk-user-home v-if="page == 'home'" :user="user"/> + <mk-user-graphs v-if="page == 'graphs'" :user="user"/> + </div> +</mk-ui> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import Progress from '../../../common/scripts/loading'; + +export default Vue.extend({ + props: { + username: { + type: String + }, + page: { + default: 'home' + } + }, + data() { + return { + fetching: true, + user: null + }; + }, + mounted() { + Progress.start(); + this.$root.$data.os.api('users/show', { + username: this.username + }).then(user => { + this.fetching = false; + this.user = user; + Progress.done(); + document.title = user.name + ' | Misskey'; + }); + } +}); +</script> +