summaryrefslogtreecommitdiff
path: root/src/web/app
diff options
context:
space:
mode:
authorsyuilo <syuilotan@yahoo.co.jp>2018-02-15 00:32:13 +0900
committersyuilo <syuilotan@yahoo.co.jp>2018-02-15 00:32:13 +0900
commitc70e56dd1d60b1c49fce6aef3824effc6582db76 (patch)
tree0c74bcf98ab227bb745e4e90d7d453e496ea942b /src/web/app
parentwip (diff)
downloadmisskey-c70e56dd1d60b1c49fce6aef3824effc6582db76.tar.gz
misskey-c70e56dd1d60b1c49fce6aef3824effc6582db76.tar.bz2
misskey-c70e56dd1d60b1c49fce6aef3824effc6582db76.zip
wip
Diffstat (limited to 'src/web/app')
-rw-r--r--src/web/app/desktop/-tags/pages/user.tag27
-rw-r--r--src/web/app/desktop/-tags/user.tag852
-rw-r--r--src/web/app/desktop/views/pages/user/user-followers-you-know.vue79
-rw-r--r--src/web/app/desktop/views/pages/user/user-friends.vue117
-rw-r--r--src/web/app/desktop/views/pages/user/user-header.vue189
-rw-r--r--src/web/app/desktop/views/pages/user/user-home.vue90
-rw-r--r--src/web/app/desktop/views/pages/user/user-photos.vue89
-rw-r--r--src/web/app/desktop/views/pages/user/user-profile.vue142
-rw-r--r--src/web/app/desktop/views/pages/user/user.vue43
9 files changed, 749 insertions, 879 deletions
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>
+