diff --git a/src/web/app/desktop/-tags/ellipsis-icon.tag b/src/web/app/desktop/-tags/ellipsis-icon.tag deleted file mode 100644 index 619f0d84f7..0000000000 --- a/src/web/app/desktop/-tags/ellipsis-icon.tag +++ /dev/null @@ -1,37 +0,0 @@ -<mk-ellipsis-icon> - <div></div> - <div></div> - <div></div> - <style lang="stylus" scoped> - :scope - display block - width 70px - margin 0 auto - text-align center - - > div - display inline-block - width 18px - height 18px - background-color rgba(0, 0, 0, 0.3) - border-radius 100% - animation bounce 1.4s infinite ease-in-out both - - &:nth-child(1) - animation-delay 0s - - &:nth-child(2) - margin 0 6px - animation-delay 0.16s - - &:nth-child(3) - animation-delay 0.32s - - @keyframes bounce - 0%, 80%, 100% - transform scale(0) - 40% - transform scale(1) - - </style> -</mk-ellipsis-icon> diff --git a/src/web/app/desktop/-tags/home-widgets/timeline.tag b/src/web/app/desktop/-tags/home-widgets/timeline.tag deleted file mode 100644 index 4668ebfa87..0000000000 --- a/src/web/app/desktop/-tags/home-widgets/timeline.tag +++ /dev/null @@ -1,143 +0,0 @@ -<mk-timeline-home-widget> - <mk-following-setuper v-if="noFollowing"/> - <div class="loading" v-if="isLoading"> - <mk-ellipsis-icon/> - </div> - <p class="empty" v-if="isEmpty && !isLoading">%fa:R comments%自分の投稿や、自分がフォローしているユーザーの投稿が表示されます。</p> - <mk-timeline ref="timeline" hide={ isLoading }> - <yield to="footer"> - <template v-if="!parent.moreLoading">%fa:moon%</template> - <template v-if="parent.moreLoading">%fa:spinner .pulse .fw%</template> - </yield/> - </mk-timeline> - <style lang="stylus" scoped> - :scope - display block - background #fff - border solid 1px rgba(0, 0, 0, 0.075) - border-radius 6px - - > mk-following-setuper - border-bottom solid 1px #eee - - > .loading - padding 64px 0 - - > .empty - display block - margin 0 auto - padding 32px - max-width 400px - text-align center - color #999 - - > [data-fa] - display block - margin-bottom 16px - font-size 3em - color #ccc - - </style> - <script lang="typescript"> - this.mixin('i'); - this.mixin('api'); - - this.mixin('stream'); - this.connection = this.stream.getConnection(); - this.connectionId = this.stream.use(); - - this.isLoading = true; - this.isEmpty = false; - this.moreLoading = false; - this.noFollowing = this.I.following_count == 0; - - this.on('mount', () => { - this.connection.on('post', this.onStreamPost); - this.connection.on('follow', this.onStreamFollow); - this.connection.on('unfollow', this.onStreamUnfollow); - - document.addEventListener('keydown', this.onDocumentKeydown); - window.addEventListener('scroll', this.onScroll); - - this.load(() => this.$emit('loaded')); - }); - - this.on('unmount', () => { - this.connection.off('post', this.onStreamPost); - this.connection.off('follow', this.onStreamFollow); - this.connection.off('unfollow', this.onStreamUnfollow); - this.stream.dispose(this.connectionId); - - document.removeEventListener('keydown', this.onDocumentKeydown); - window.removeEventListener('scroll', this.onScroll); - }); - - this.onDocumentKeydown = e => { - if (e.target.tagName != 'INPUT' && e.target.tagName != 'TEXTAREA') { - if (e.which == 84) { // t - this.$refs.timeline.focus(); - } - } - }; - - this.load = (cb) => { - this.update({ - isLoading: true - }); - - this.api('posts/timeline', { - until_date: this.date ? this.date.getTime() : undefined - }).then(posts => { - this.update({ - isLoading: false, - isEmpty: posts.length == 0 - }); - this.$refs.timeline.setPosts(posts); - if (cb) cb(); - }); - }; - - this.more = () => { - if (this.moreLoading || this.isLoading || this.$refs.timeline.posts.length == 0) return; - this.update({ - moreLoading: true - }); - this.api('posts/timeline', { - until_id: this.$refs.timeline.tail().id - }).then(posts => { - this.update({ - moreLoading: false - }); - this.$refs.timeline.prependPosts(posts); - }); - }; - - this.onStreamPost = post => { - this.update({ - isEmpty: false - }); - this.$refs.timeline.addPost(post); - }; - - this.onStreamFollow = () => { - this.load(); - }; - - this.onStreamUnfollow = () => { - this.load(); - }; - - this.onScroll = () => { - const current = window.scrollY + window.innerHeight; - if (current > document.body.offsetHeight - 8) this.more(); - }; - - this.warp = date => { - this.update({ - date: date - }); - - this.load(); - }; - </script> -</mk-timeline-home-widget> diff --git a/src/web/app/desktop/-tags/pages/entrance.tag b/src/web/app/desktop/-tags/pages/entrance.tag deleted file mode 100644 index 56cec34909..0000000000 --- a/src/web/app/desktop/-tags/pages/entrance.tag +++ /dev/null @@ -1,342 +0,0 @@ -<mk-entrance> - <main> - <div> - <h1>どこにいても、ここにあります</h1> - <p>ようこそ! MisskeyはTwitter風ミニブログSNSです――思ったこと、共有したいことをシンプルに書き残せます。タイムラインを見れば、皆の反応や皆がどう思っているのかもすぐにわかります。</p> - <p v-if="stats">これまでに{ stats.posts_count }投稿されました</p> - </div> - <div> - <mk-entrance-signin v-if="mode == 'signin'"/> - <mk-entrance-signup v-if="mode == 'signup'"/> - <div class="introduction" v-if="mode == 'introduction'"> - <mk-introduction/> - <button @click="signin">わかった</button> - </div> - </div> - </main> - <mk-forkit/> - <footer> - <div> - <mk-nav-links/> - <p class="c">{ _COPYRIGHT_ }</p> - </div> - </footer> - <!-- ↓ https://github.com/riot/riot/issues/2134 (将来的)--> - <style data-disable-scope="data-disable-scope"> - #wait { - right: auto; - left: 15px; - } - </style> - <style lang="stylus" scoped> - :scope - $width = 1000px - - display block - - &:before - content "" - display block - position fixed - width 100% - height 100% - background rgba(0, 0, 0, 0.3) - - > main - display block - max-width $width - margin 0 auto - padding 64px 0 0 0 - padding-bottom 16px - - &:after - content "" - display block - clear both - - > div:first-child - position absolute - top 64px - left 0 - width calc(100% - 500px) - color #fff - text-shadow 0 0 32px rgba(0, 0, 0, 0.5) - font-weight bold - - > p:last-child - padding 1em 0 0 0 - border-top solid 1px #fff - - > div:last-child - float right - - > .introduction - max-width 360px - margin 0 auto - color #777 - - > mk-introduction - padding 32px - background #fff - box-shadow 0 4px 16px rgba(0, 0, 0, 0.2) - - > button - display block - margin 16px auto 0 auto - color #666 - - &:hover - text-decoration underline - - > footer - * - color #fff !important - text-shadow 0 0 8px #000 - font-weight bold - - > div - max-width $width - margin 0 auto - padding 16px 0 - text-align center - border-top solid 1px #fff - - > .c - margin 0 - line-height 64px - font-size 10px - - </style> - <script lang="typescript"> - this.mixin('api'); - - this.mode = 'signin'; - - this.on('mount', () => { - document.documentElement.style.backgroundColor = '#444'; - - this.api('meta').then(meta => { - const img = meta.top_image ? meta.top_image : '/assets/desktop/index.jpg'; - document.documentElement.style.backgroundImage = `url("${ img }")`; - document.documentElement.style.backgroundSize = 'cover'; - document.documentElement.style.backgroundPosition = 'center'; - }); - - this.api('stats').then(stats => { - this.update({ - stats - }); - }); - }); - - this.signup = () => { - this.update({ - mode: 'signup' - }); - }; - - this.signin = () => { - this.update({ - mode: 'signin' - }); - }; - - this.introduction = () => { - this.update({ - mode: 'introduction' - }); - }; - </script> -</mk-entrance> - -<mk-entrance-signin> - <a class="help" href={ _DOCS_URL_ + '/help' } title="お困りですか?">%fa:question%</a> - <div class="form"> - <h1><img v-if="user" src={ user.avatar_url + '?thumbnail&size=32' }/> - <p>{ user ? user.name : 'アカウント' }</p> - </h1> - <mk-signin ref="signin"/> - </div> - <a href={ _API_URL_ + '/signin/twitter' }>Twitterでサインイン</a> - <div class="divider"><span>or</span></div> - <button class="signup" @click="parent.signup">新規登録</button><a class="introduction" @click="introduction">Misskeyについて</a> - <style lang="stylus" scoped> - :scope - display block - width 290px - margin 0 auto - text-align center - - &:hover - > .help - opacity 1 - - > .help - cursor pointer - display block - position absolute - top 0 - right 0 - z-index 1 - margin 0 - padding 0 - font-size 1.2em - color #999 - border none - outline none - background transparent - opacity 0 - transition opacity 0.1s ease - - &:hover - color #444 - - &:active - color #222 - - > [data-fa] - padding 14px - - > .form - padding 10px 28px 16px 28px - background #fff - box-shadow 0px 4px 16px rgba(0, 0, 0, 0.2) - - > h1 - display block - margin 0 - padding 0 - height 54px - line-height 54px - text-align center - text-transform uppercase - font-size 1em - font-weight bold - color rgba(0, 0, 0, 0.5) - border-bottom solid 1px rgba(0, 0, 0, 0.1) - - > p - display inline - margin 0 - padding 0 - - > img - display inline-block - top 10px - width 32px - height 32px - margin-right 8px - border-radius 100% - - &[src=''] - display none - - > .divider - padding 16px 0 - text-align center - - &:before - &:after - content "" - display block - position absolute - top 50% - width 45% - height 1px - border-top solid 1px rgba(0, 0, 0, 0.1) - - &:before - left 0 - - &:after - right 0 - - > * - z-index 1 - padding 0 8px - color #fff - text-shadow 0 0 8px rgba(0, 0, 0, 0.5) - - > .signup - width 100% - line-height 56px - font-size 1em - color #fff - background $theme-color - border-radius 64px - - &:hover - background lighten($theme-color, 5%) - - &:active - background darken($theme-color, 5%) - - > .introduction - display inline-block - margin-top 16px - font-size 12px - color #666 - - </style> - <script lang="typescript"> - this.on('mount', () => { - this.$refs.signin.on('user', user => { - this.update({ - user: user - }); - }); - }); - - this.introduction = () => { - this.parent.introduction(); - }; - </script> -</mk-entrance-signin> - -<mk-entrance-signup> - <mk-signup/> - <button class="cancel" type="button" @click="parent.signin" title="キャンセル">%fa:times%</button> - <style lang="stylus" scoped> - :scope - display block - width 368px - margin 0 auto - - &:hover - > .cancel - opacity 1 - - > mk-signup - padding 18px 32px 0 32px - background #fff - box-shadow 0px 4px 16px rgba(0, 0, 0, 0.2) - - > .cancel - cursor pointer - display block - position absolute - top 0 - right 0 - z-index 1 - margin 0 - padding 0 - font-size 1.2em - color #999 - border none - outline none - box-shadow none - background transparent - opacity 0 - transition opacity 0.1s ease - - &:hover - color #555 - - &:active - color #222 - - > [data-fa] - padding 14px - - </style> -</mk-entrance-signup> diff --git a/src/web/app/desktop/views/components/ellipsis-icon.vue b/src/web/app/desktop/views/components/ellipsis-icon.vue new file mode 100644 index 0000000000..c54a7db29d --- /dev/null +++ b/src/web/app/desktop/views/components/ellipsis-icon.vue @@ -0,0 +1,37 @@ +<template> +<div class="mk-ellipsis-icon"> + <div></div><div></div><div></div> +</div> +</template> + +<style lang="stylus" scoped> +.mk-ellipsis-icon + width 70px + margin 0 auto + text-align center + + > div + display inline-block + width 18px + height 18px + background-color rgba(0, 0, 0, 0.3) + border-radius 100% + animation bounce 1.4s infinite ease-in-out both + + &:nth-child(1) + animation-delay 0s + + &:nth-child(2) + margin 0 6px + animation-delay 0.16s + + &:nth-child(3) + animation-delay 0.32s + + @keyframes bounce + 0%, 80%, 100% + transform scale(0) + 40% + transform scale(1) + +</style> diff --git a/src/web/app/desktop/views/components/index.ts b/src/web/app/desktop/views/components/index.ts index 71a049a62c..a529537442 100644 --- a/src/web/app/desktop/views/components/index.ts +++ b/src/web/app/desktop/views/components/index.ts @@ -11,13 +11,15 @@ import uiHeaderSearch from './ui-header-search.vue'; import uiNotification from './ui-notification.vue'; import home from './home.vue'; import timeline from './timeline.vue'; -import timelinePost from './timeline-post.vue'; -import timelinePostSub from './timeline-post-sub.vue'; +import posts from './posts.vue'; +import postsPost from './posts-post.vue'; +import postsPostSub from './posts-post-sub.vue'; import subPostContent from './sub-post-content.vue'; import window from './window.vue'; import postFormWindow from './post-form-window.vue'; import repostFormWindow from './repost-form-window.vue'; import analogClock from './analog-clock.vue'; +import ellipsisIcon from './ellipsis-icon.vue'; Vue.component('mk-ui', ui); Vue.component('mk-ui-header', uiHeader); @@ -30,10 +32,12 @@ Vue.component('mk-ui-header-search', uiHeaderSearch); Vue.component('mk-ui-notification', uiNotification); Vue.component('mk-home', home); Vue.component('mk-timeline', timeline); -Vue.component('mk-timeline-post', timelinePost); -Vue.component('mk-timeline-post-sub', timelinePostSub); +Vue.component('mk-posts', posts); +Vue.component('mk-posts-post', postsPost); +Vue.component('mk-posts-post-sub', postsPostSub); Vue.component('mk-sub-post-content', subPostContent); Vue.component('mk-window', window); Vue.component('mk-post-form-window', postFormWindow); Vue.component('mk-repost-form-window', repostFormWindow); Vue.component('mk-analog-clock', analogClock); +Vue.component('mk-ellipsis-icon', ellipsisIcon); diff --git a/src/web/app/desktop/views/components/timeline-post-sub.vue b/src/web/app/desktop/views/components/posts-post-sub.vue similarity index 96% rename from src/web/app/desktop/views/components/timeline-post-sub.vue rename to src/web/app/desktop/views/components/posts-post-sub.vue index 1209396996..89aeb04829 100644 --- a/src/web/app/desktop/views/components/timeline-post-sub.vue +++ b/src/web/app/desktop/views/components/posts-post-sub.vue @@ -1,5 +1,5 @@ <template> -<div class="mk-timeline-post-sub" :title="title"> +<div class="mk-posts-post-sub" :title="title"> <a class="avatar-anchor" :href="`/${post.user.username}`"> <img class="avatar" :src="`${post.user.avatar_url}?thumbnail&size=64`" alt="avatar" :v-user-preview="post.user_id"/> </a> @@ -33,7 +33,7 @@ export default Vue.extend({ </script> <style lang="stylus" scoped> -.mk-timeline-post-sub +.mk-posts-post-sub margin 0 padding 0 font-size 0.9em diff --git a/src/web/app/desktop/views/components/timeline-post.vue b/src/web/app/desktop/views/components/posts-post.vue similarity index 98% rename from src/web/app/desktop/views/components/timeline-post.vue rename to src/web/app/desktop/views/components/posts-post.vue index 6c3d525d59..9991d145e4 100644 --- a/src/web/app/desktop/views/components/timeline-post.vue +++ b/src/web/app/desktop/views/components/posts-post.vue @@ -1,7 +1,7 @@ <template> -<div class="mk-timeline-post" tabindex="-1" :title="title" @keydown="onKeyDown" @dblclick="onDblClick"> +<div class="mk-posts-post" tabindex="-1" :title="title" @keydown="onKeyDown" @dblclick="onDblClick"> <div class="reply-to" v-if="p.reply"> - <mk-timeline-post-sub post="p.reply"/> + <mk-posts-post-sub post="p.reply"/> </div> <div class="repost" v-if="isRepost"> <p> @@ -242,7 +242,7 @@ export default Vue.extend({ </script> <style lang="stylus" scoped> -.mk-timeline-post +.mk-posts-post margin 0 padding 0 background #fff diff --git a/src/web/app/desktop/views/components/posts.vue b/src/web/app/desktop/views/components/posts.vue new file mode 100644 index 0000000000..b685bff6af --- /dev/null +++ b/src/web/app/desktop/views/components/posts.vue @@ -0,0 +1,69 @@ +<template> +<div class="mk-posts"> + <template v-for="(post, i) in _posts"> + <mk-posts-post :post.sync="post" :key="post.id"/> + <p class="date" :key="post.id + '-time'" v-if="i != _posts.length - 1 && _post._date != _posts[i + 1]._date"><span>%fa:angle-up%{{ post._datetext }}</span><span>%fa:angle-down%{{ _posts[i + 1]._datetext }}</span></p> + </template> + <footer> + <slot name="footer"></slot> + </footer> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; + +export default Vue.extend({ + props: { + posts: { + type: Array, + default: () => [] + } + }, + computed: { + _posts(): any[] { + return (this.posts as any).map(post => { + const date = new Date(post.created_at).getDate(); + const month = new Date(post.created_at).getMonth() + 1; + post._date = date; + post._datetext = `${month}月 ${date}日`; + return post; + }); + } + }, + methods: { + focus() { + (this.$el as any).children[0].focus(); + } + } +}); +</script> + +<style lang="stylus" scoped> +.mk-posts + + > .date + display block + margin 0 + line-height 32px + font-size 14px + text-align center + color #aaa + background #fdfdfd + border-bottom solid 1px #eaeaea + + span + margin 0 16px + + [data-fa] + margin-right 8px + + > footer + padding 16px + text-align center + color #ccc + border-top solid 1px #eaeaea + border-bottom-left-radius 4px + border-bottom-right-radius 4px + +</style> diff --git a/src/web/app/desktop/views/components/timeline.vue b/src/web/app/desktop/views/components/timeline.vue index c580e59f66..b24e78fe45 100644 --- a/src/web/app/desktop/views/components/timeline.vue +++ b/src/web/app/desktop/views/components/timeline.vue @@ -1,12 +1,11 @@ <template> <div class="mk-timeline"> - <template v-for="(post, i) in _posts"> - <mk-timeline-post :post.sync="post" :key="post.id"/> - <p class="date" :key="post.id + '-time'" v-if="i != _posts.length - 1 && _post._date != _posts[i + 1]._date"><span>%fa:angle-up%{{ post._datetext }}</span><span>%fa:angle-down%{{ _posts[i + 1]._datetext }}</span></p> - </template> - <footer> - <slot name="footer"></slot> - </footer> + <mk-following-setuper v-if="alone"/> + <div class="loading" v-if="fetching"> + <mk-ellipsis-icon/> + </div> + <p class="empty" v-if="posts.length == 0 && !fetching">%fa:R comments%自分の投稿や、自分がフォローしているユーザーの投稿が表示されます。</p> + <mk-posts :posts="posts" ref="timeline"/> </div> </template> @@ -15,28 +14,85 @@ import Vue from 'vue'; export default Vue.extend({ props: { - posts: { - type: Array, - default: [] + date: { + type: Date, + required: false } }, + data() { + return { + fetching: true, + moreFetching: false, + posts: [], + connection: null, + connectionId: null + }; + }, computed: { - _posts(): any[] { - return this.posts.map(post => { - const date = new Date(post.created_at).getDate(); - const month = new Date(post.created_at).getMonth() + 1; - post._date = date; - post._datetext = `${month}月 ${date}日`; - return post; - }); - }, - tail(): any { - return this.posts[this.posts.length - 1]; + alone(): boolean { + return this.$root.$data.os.i.following_count == 0; } }, + mounted() { + this.connection = this.$root.$data.os.stream.getConnection(); + this.connectionId = this.$root.$data.os.stream.use(); + + this.connection.on('post', this.onPost); + this.connection.on('follow', this.onChangeFollowing); + this.connection.on('unfollow', this.onChangeFollowing); + + document.addEventListener('keydown', this.onKeydown); + window.addEventListener('scroll', this.onScroll); + + this.fetch(); + }, + beforeDestroy() { + this.connection.off('post', this.onPost); + this.connection.off('follow', this.onChangeFollowing); + this.connection.off('unfollow', this.onChangeFollowing); + this.$root.$data.os.stream.dispose(this.connectionId); + + document.removeEventListener('keydown', this.onKeydown); + window.removeEventListener('scroll', this.onScroll); + }, methods: { - focus() { - (this.$el as any).children[0].focus(); + fetch(cb?) { + this.fetching = true; + + this.$root.$data.os.api('posts/timeline', { + until_date: this.date ? (this.date as any).getTime() : undefined + }).then(posts => { + this.fetching = false; + this.posts = posts; + if (cb) cb(); + }); + }, + more() { + if (this.moreFetching || this.fetching || this.posts.length == 0) return; + this.moreFetching = true; + this.$root.$data.os.api('posts/timeline', { + until_id: this.posts[this.posts.length - 1].id + }).then(posts => { + this.moreFetching = false; + this.posts.unshift(posts); + }); + }, + onPost(post) { + this.posts.unshift(post); + }, + onChangeFollowing() { + this.fetch(); + }, + onScroll() { + const current = window.scrollY + window.innerHeight; + if (current > document.body.offsetHeight - 8) this.more(); + }, + onKeydown(e) { + if (e.target.tagName != 'INPUT' && e.target.tagName != 'TEXTAREA') { + if (e.which == 84) { // t + (this.$refs.timeline as any).focus(); + } + } } } }); @@ -44,29 +100,28 @@ export default Vue.extend({ <style lang="stylus" scoped> .mk-timeline + background #fff + border solid 1px rgba(0, 0, 0, 0.075) + border-radius 6px - > .date + > mk-following-setuper + border-bottom solid 1px #eee + + > .loading + padding 64px 0 + + > .empty display block - margin 0 - line-height 32px - font-size 14px + margin 0 auto + padding 32px + max-width 400px text-align center - color #aaa - background #fdfdfd - border-bottom solid 1px #eaeaea + color #999 - span - margin 0 16px - - [data-fa] - margin-right 8px - - > footer - padding 16px - text-align center - color #ccc - border-top solid 1px #eaeaea - border-bottom-left-radius 4px - border-bottom-right-radius 4px + > [data-fa] + display block + margin-bottom 16px + font-size 3em + color #ccc </style>