From 3ae824c3542b0e232b9847ae702956e631acb276 Mon Sep 17 00:00:00 2001 From: syuilo <syuilotan@yahoo.co.jp> Date: Mon, 26 Feb 2018 00:39:05 +0900 Subject: [PATCH] :v: --- src/api/endpoints/posts/create.ts | 24 +++++++- .../desktop/views/components/post-detail.vue | 29 +++++++++ .../desktop/views/components/posts.post.vue | 35 ++++++++++- .../app/desktop/views/components/timeline.vue | 14 ++++- src/web/app/desktop/views/pages/search.vue | 55 ++++++++++++----- .../mobile/views/components/post-detail.vue | 25 ++++++++ .../mobile/views/components/posts.post.vue | 30 ++++++++-- src/web/app/mobile/views/pages/search.vue | 59 +++++++++++++------ 8 files changed, 227 insertions(+), 44 deletions(-) diff --git a/src/api/endpoints/posts/create.ts b/src/api/endpoints/posts/create.ts index 0fa52221f9..075e1ac9f0 100644 --- a/src/api/endpoints/posts/create.ts +++ b/src/api/endpoints/posts/create.ts @@ -31,6 +31,10 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => { const [text, textErr] = $(params.text).optional.string().pipe(isValidText).$; if (textErr) return rej('invalid text'); + // Get 'tags' parameter + const [tags = [], tagsErr] = $(params.tags).optional.array('string').unique().eachQ(t => t.range(1, 32)).$; + if (tagsErr) return rej('invalid tags'); + // Get 'media_ids' parameter const [mediaIds, mediaIdsErr] = $(params.media_ids).optional.array('id').unique().range(1, 4).$; if (mediaIdsErr) return rej('invalid media_ids'); @@ -205,6 +209,23 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => { } } + let tokens = null; + if (text) { + // Analyze + tokens = parse(text); + + // Extract hashtags + const hashtags = tokens + .filter(t => t.type == 'hashtag') + .map(t => t.hashtag); + + hashtags.forEach(tag => { + if (tags.indexOf(tag) == -1) { + tags.push(tag); + } + }); + } + // 投稿を作成 const post = await Post.insert({ created_at: new Date(), @@ -215,6 +236,7 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => { repost_id: repost ? repost._id : undefined, poll: poll, text: text, + tags: tags, user_id: user._id, app_id: app ? app._id : null, @@ -423,8 +445,6 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => { // If has text content if (text) { - // Analyze - const tokens = parse(text); /* // Extract a hashtags const hashtags = tokens diff --git a/src/web/app/desktop/views/components/post-detail.vue b/src/web/app/desktop/views/components/post-detail.vue index c453867dfb..1e31752fe7 100644 --- a/src/web/app/desktop/views/components/post-detail.vue +++ b/src/web/app/desktop/views/components/post-detail.vue @@ -44,6 +44,9 @@ <mk-images :images="p.media"/> </div> <mk-poll v-if="p.poll" :post="p"/> + <div class="tags" v-if="p.tags && p.tags.length > 0"> + <router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=${tag}`">{{ tag }}</router-link> + </div> </div> <footer> <mk-reactions-viewer :post="p"/> @@ -306,6 +309,32 @@ export default Vue.extend({ > .mk-url-preview margin-top 8px + > .tags + > * + margin 0 8px 0 0 + padding 0 8px 0 16px + font-size 90% + color #8d969e + background #edf0f3 + border-radius 4px + + &:before + content "" + display block + position absolute + top 0 + bottom 0 + left 4px + width 8px + height 8px + margin auto 0 + background #fff + border-radius 100% + + &:hover + text-decoration none + background #e2e7ec + > footer font-size 1.2em diff --git a/src/web/app/desktop/views/components/posts.post.vue b/src/web/app/desktop/views/components/posts.post.vue index 4898de0b6b..382a8de97d 100644 --- a/src/web/app/desktop/views/components/posts.post.vue +++ b/src/web/app/desktop/views/components/posts.post.vue @@ -38,6 +38,9 @@ </p> <a class="reply" v-if="p.reply">%fa:reply%</a> <mk-post-html v-if="p.ast" :ast="p.ast" :i="os.i" :class="$style.text"/> + <div class="tags" v-if="p.tags && p.tags.length > 0"> + <router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=${tag}`">{{ tag }}</router-link> + </div> <a class="quote" v-if="p.repost">RP:</a> <mk-url-preview v-for="url in urls" :url="url" :key="url"/> </div> @@ -342,9 +345,9 @@ export default Vue.extend({ display block float left margin 0 16px 10px 0 - position -webkit-sticky - position sticky - top 74px + //position -webkit-sticky + //position sticky + //top 74px > .avatar display block @@ -428,6 +431,32 @@ export default Vue.extend({ font-style oblique color #a0bf46 + > .tags + > * + margin 0 8px 0 0 + padding 0 8px 0 16px + font-size 90% + color #8d969e + background #edf0f3 + border-radius 4px + + &:before + content "" + display block + position absolute + top 0 + bottom 0 + left 4px + width 8px + height 8px + margin auto 0 + background #fff + border-radius 100% + + &:hover + text-decoration none + background #e2e7ec + > .mk-poll font-size 80% diff --git a/src/web/app/desktop/views/components/timeline.vue b/src/web/app/desktop/views/components/timeline.vue index eef62104eb..0d16d60df9 100644 --- a/src/web/app/desktop/views/components/timeline.vue +++ b/src/web/app/desktop/views/components/timeline.vue @@ -24,6 +24,7 @@ export default Vue.extend({ return { fetching: true, moreFetching: false, + existMore: false, posts: [], connection: null, connectionId: null, @@ -62,8 +63,13 @@ export default Vue.extend({ this.fetching = true; (this as any).api('posts/timeline', { + limit: 11, until_date: this.date ? this.date.getTime() : undefined }).then(posts => { + if (posts.length == 11) { + posts.pop(); + this.existMore = true; + } this.posts = posts; this.fetching = false; this.$emit('loaded'); @@ -71,11 +77,17 @@ export default Vue.extend({ }); }, more() { - if (this.moreFetching || this.fetching || this.posts.length == 0) return; + if (this.moreFetching || this.fetching || this.posts.length == 0 || !this.existMore) return; this.moreFetching = true; (this as any).api('posts/timeline', { + limit: 11, until_id: this.posts[this.posts.length - 1].id }).then(posts => { + if (posts.length == 11) { + posts.pop(); + } else { + this.existMore = false; + } this.posts = this.posts.concat(posts); this.moreFetching = false; }); diff --git a/src/web/app/desktop/views/pages/search.vue b/src/web/app/desktop/views/pages/search.vue index b8e8db2e79..afd37c8cee 100644 --- a/src/web/app/desktop/views/pages/search.vue +++ b/src/web/app/desktop/views/pages/search.vue @@ -1,13 +1,13 @@ <template> <mk-ui> <header :class="$style.header"> - <h1>{{ query }}</h1> + <h1>{{ q }}</h1> </header> <div :class="$style.loading" v-if="fetching"> <mk-ellipsis-icon/> </div> - <p :class="$style.empty" v-if="empty">%fa:search%「{{ query }}」に関する投稿は見つかりませんでした。</p> - <mk-posts ref="timeline" :class="$style.posts"> + <p :class="$style.empty" v-if="!fetching && empty">%fa:search%「{{ q }}」に関する投稿は見つかりませんでした。</p> + <mk-posts ref="timeline" :class="$style.posts" :posts="posts"> <div slot="footer"> <template v-if="!moreFetching">%fa:search%</template> <template v-if="moreFetching">%fa:spinner .pulse .fw%</template> @@ -21,33 +21,34 @@ import Vue from 'vue'; import Progress from '../../../common/scripts/loading'; import parse from '../../../common/scripts/parse-search-query'; -const limit = 30; +const limit = 20; export default Vue.extend({ - props: ['query'], data() { return { fetching: true, moreFetching: false, + existMore: false, offset: 0, posts: [] }; }, + watch: { + $route: 'fetch' + }, computed: { empty(): boolean { return this.posts.length == 0; + }, + q(): string { + return this.$route.query.q; } }, mounted() { - Progress.start(); - document.addEventListener('keydown', this.onDocumentKeydown); window.addEventListener('scroll', this.onScroll); - (this as any).api('posts/search', parse(this.query)).then(posts => { - this.posts = posts; - this.fetching = false; - }); + this.fetch(); }, beforeDestroy() { document.removeEventListener('keydown', this.onDocumentKeydown); @@ -61,16 +62,38 @@ export default Vue.extend({ } } }, + fetch() { + this.fetching = true; + Progress.start(); + + (this as any).api('posts/search', Object.assign({ + limit: limit + 1, + offset: this.offset + }, parse(this.q))).then(posts => { + if (posts.length == limit + 1) { + posts.pop(); + this.existMore = true; + } + this.posts = posts; + this.fetching = false; + Progress.done(); + }); + }, more() { - if (this.moreFetching || this.fetching || this.posts.length == 0) return; + if (this.moreFetching || this.fetching || this.posts.length == 0 || !this.existMore) return; this.offset += limit; this.moreFetching = true; - return (this as any).api('posts/search', Object.assign({}, parse(this.query), { - limit: limit, + return (this as any).api('posts/search', Object.assign({ + limit: limit + 1, offset: this.offset - })).then(posts => { - this.moreFetching = false; + }, parse(this.q))).then(posts => { + if (posts.length == limit + 1) { + posts.pop(); + } else { + this.existMore = false; + } this.posts = this.posts.concat(posts); + this.moreFetching = false; }); }, onScroll() { diff --git a/src/web/app/mobile/views/components/post-detail.vue b/src/web/app/mobile/views/components/post-detail.vue index 05138607ff..a83744ec4d 100644 --- a/src/web/app/mobile/views/components/post-detail.vue +++ b/src/web/app/mobile/views/components/post-detail.vue @@ -39,6 +39,9 @@ </header> <div class="body"> <mk-post-html v-if="p.ast" :ast="p.ast" :i="os.i" :class="$style.text"/> + <div class="tags" v-if="p.tags && p.tags.length > 0"> + <router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=${tag}`">{{ tag }}</router-link> + </div> <mk-url-preview v-for="url in urls" :url="url" :key="url"/> <div class="media" v-if="p.media"> <mk-images :images="p.media"/> @@ -312,6 +315,28 @@ export default Vue.extend({ display block max-width 100% + > .tags + > * + margin 0 8px 0 0 + padding 0 8px 0 16px + font-size 90% + color #8d969e + background #edf0f3 + border-radius 4px + + &:before + content "" + display block + position absolute + top 0 + bottom 0 + left 4px + width 8px + height 8px + margin auto 0 + background #fff + border-radius 100% + > .time font-size 16px color #c0c0c0 diff --git a/src/web/app/mobile/views/components/posts.post.vue b/src/web/app/mobile/views/components/posts.post.vue index ae1dfc59ab..e26c337f5c 100644 --- a/src/web/app/mobile/views/components/posts.post.vue +++ b/src/web/app/mobile/views/components/posts.post.vue @@ -35,6 +35,9 @@ %fa:reply% </a> <mk-post-html v-if="p.ast" :ast="p.ast" :i="os.i" :class="$style.text"/> + <div class="tags" v-if="p.tags && p.tags.length > 0"> + <router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=${tag}`">{{ tag }}</router-link> + </div> <mk-url-preview v-for="url in urls" :url="url" :key="url"/> <a class="quote" v-if="p.repost != null">RP:</a> </div> @@ -346,10 +349,7 @@ export default Vue.extend({ font-size 1.1em color #717171 - > .dummy - display none - - mk-url-preview + .mk-url-preview margin-top 8px > .channel @@ -364,6 +364,28 @@ export default Vue.extend({ font-style oblique color #a0bf46 + > .tags + > * + margin 0 8px 0 0 + padding 0 8px 0 16px + font-size 90% + color #8d969e + background #edf0f3 + border-radius 4px + + &:before + content "" + display block + position absolute + top 0 + bottom 0 + left 4px + width 8px + height 8px + margin auto 0 + background #fff + border-radius 100% + [data-is-me]:after content "you" padding 0 4px diff --git a/src/web/app/mobile/views/pages/search.vue b/src/web/app/mobile/views/pages/search.vue index b6e114a82b..cbab504e3c 100644 --- a/src/web/app/mobile/views/pages/search.vue +++ b/src/web/app/mobile/views/pages/search.vue @@ -1,10 +1,10 @@ <template> <mk-ui> - <span slot="header">%fa:search% {{ query }}</span> + <span slot="header">%fa:search% {{ q }}</span> <main v-if="!fetching"> <mk-posts :class="$style.posts" :posts="posts"> - <span v-if="posts.length == 0">{{ '%i18n:mobile.tags.mk-search-posts.empty%'.replace('{}', query) }}</span> - <button v-if="canFetchMore" @click="more" :disabled="fetching" slot="tail"> + <span v-if="posts.length == 0">{{ '%i18n:mobile.tags.mk-search-posts.empty%'.replace('{}', q) }}</span> + <button v-if="existMore" @click="more" :disabled="fetching" slot="tail"> <span v-if="!fetching">%i18n:mobile.tags.mk-timeline.load-more%</span> <span v-if="fetching">%i18n:common.loading%<mk-ellipsis/></span> </button> @@ -18,38 +18,61 @@ import Vue from 'vue'; import Progress from '../../../common/scripts/loading'; import parse from '../../../common/scripts/parse-search-query'; -const limit = 30; +const limit = 20; export default Vue.extend({ - props: ['query'], data() { return { fetching: true, + existMore: false, posts: [], offset: 0 }; }, + watch: { + $route: 'fetch' + }, + computed: { + q(): string { + return this.$route.query.q; + } + }, mounted() { - document.title = `%i18n:mobile.tags.mk-search-page.search%: ${this.query} | Misskey`; + document.title = `%i18n:mobile.tags.mk-search-page.search%: ${this.q} | Misskey`; document.documentElement.style.background = '#313a42'; - Progress.start(); - - (this as any).api('posts/search', Object.assign({}, parse(this.query), { - limit: limit - })).then(posts => { - this.posts = posts; - this.fetching = false; - Progress.done(); - }); + this.fetch(); }, methods: { + fetch() { + this.fetching = true; + Progress.start(); + + (this as any).api('posts/search', Object.assign({ + limit: limit + 1 + }, parse(this.q))).then(posts => { + if (posts.length == limit + 1) { + posts.pop(); + this.existMore = true; + } + this.posts = posts; + this.fetching = false; + Progress.done(); + }); + }, more() { this.offset += limit; - return (this as any).api('posts/search', Object.assign({}, parse(this.query), { - limit: limit, + (this as any).api('posts/search', Object.assign({ + limit: limit + 1, offset: this.offset - })); + }, parse(this.q))).then(posts => { + if (posts.length == limit + 1) { + posts.pop(); + } else { + this.existMore = false; + } + this.posts = this.posts.concat(posts); + }); } } });