From ba1492f9773abab84277d62e957668ef2950f77a Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Mon, 18 Feb 2019 09:17:55 +0900 Subject: [PATCH] Refactor client (#4307) * wip * wip * wip * wip * wip * wip * wip * Fix bug * :art: * :art: * :art: --- .../app/desktop/views/components/notes.vue | 83 +++++---- .../views/components/user-list-timeline.vue | 84 +++------ .../app/desktop/views/deck/deck.direct.vue | 82 +++----- .../views/deck/deck.favorites-column.vue | 68 ++----- .../views/deck/deck.featured-column.vue | 30 +-- .../desktop/views/deck/deck.hashtag-tl.vue | 86 +++------ .../app/desktop/views/deck/deck.list-tl.vue | 90 +++------ .../app/desktop/views/deck/deck.mentions.vue | 80 +++----- .../app/desktop/views/deck/deck.notes.vue | 76 ++++---- .../desktop/views/deck/deck.search-column.vue | 82 +++----- src/client/app/desktop/views/deck/deck.tl.vue | 85 +++------ .../views/deck/deck.user-column.home.vue | 78 +++----- src/client/app/desktop/views/home/search.vue | 120 ++++-------- src/client/app/desktop/views/home/tag.vue | 113 ++++------- .../app/desktop/views/home/timeline.core.vue | 106 +++-------- .../app/desktop/views/home/timeline.vue | 166 ++++++++--------- .../app/desktop/views/home/user/user.home.vue | 5 +- .../desktop/views/home/user/user.timeline.vue | 176 ++++++------------ src/client/app/mobile/style.styl | 12 ++ .../app/mobile/views/components/notes.vue | 82 ++++---- .../views/components/user-list-timeline.vue | 91 +++------ .../mobile/views/components/user-timeline.vue | 74 ++------ src/client/app/mobile/views/pages/explore.vue | 15 -- .../app/mobile/views/pages/favorites.vue | 10 - .../app/mobile/views/pages/featured.vue | 10 - .../app/mobile/views/pages/home.timeline.vue | 80 +++----- src/client/app/mobile/views/pages/home.vue | 11 -- src/client/app/mobile/views/pages/note.vue | 12 -- .../app/mobile/views/pages/notifications.vue | 15 -- .../views/pages/received-follow-requests.vue | 11 -- src/client/app/mobile/views/pages/search.vue | 91 +++------ .../app/mobile/views/pages/settings.vue | 3 - src/client/app/mobile/views/pages/tag.vue | 80 +++----- .../app/mobile/views/pages/user-list.vue | 15 -- .../app/mobile/views/pages/user-lists.vue | 17 -- .../app/mobile/views/pages/user/home.vue | 1 - .../app/mobile/views/pages/user/index.vue | 24 +-- 37 files changed, 738 insertions(+), 1526 deletions(-) diff --git a/src/client/app/desktop/views/components/notes.vue b/src/client/app/desktop/views/components/notes.vue index 5cf51d9cc4..d1bf6dcc04 100644 --- a/src/client/app/desktop/views/components/notes.vue +++ b/src/client/app/desktop/views/components/notes.vue @@ -1,10 +1,12 @@ <template> <div class="mk-notes"> + <slot name="header"></slot> + <div class="newer-indicator" :style="{ top: $store.state.uiHeaderHeight + 'px' }" v-show="queue.length > 0"></div> - <slot name="empty" v-if="notes.length == 0 && !fetching && requestInitPromise == null"></slot> + <slot name="empty" v-if="notes.length == 0 && !fetching && inited"></slot> - <mk-error v-if="!fetching && requestInitPromise != null" @retry="resolveInitPromise"/> + <mk-error v-if="!fetching && !inited" @retry="init()"/> <div class="placeholder" v-if="fetching"> <template v-for="i in 10"> @@ -23,8 +25,8 @@ </template> </component> - <footer v-if="more"> - <button @click="loadMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }"> + <footer v-if="cursor != null"> + <button @click="more" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }"> <template v-if="!moreFetching">{{ $t('@.load-more') }}</template> <template v-if="moreFetching"><fa icon="spinner" pulse fixed-width/></template> </button> @@ -43,24 +45,25 @@ const displayLimit = 30; export default Vue.extend({ i18n: i18n(), + components: { XNote }, props: { - more: { - type: Function, - required: false + makePromise: { + required: true } }, data() { return { - requestInitPromise: null as () => Promise<any[]>, notes: [], queue: [], fetching: true, - moreFetching: false + moreFetching: false, + inited: false, + cursor: null }; }, @@ -76,6 +79,10 @@ export default Vue.extend({ } }, + created() { + this.init(); + }, + mounted() { window.addEventListener('scroll', this.onScroll, { passive: true }); }, @@ -97,27 +104,41 @@ export default Vue.extend({ Vue.set((this as any).notes, i, note); }, - init(promiseGenerator: () => Promise<any[]>) { - this.requestInitPromise = promiseGenerator; - this.resolveInitPromise(); - }, - - resolveInitPromise() { + reload() { this.queue = []; this.notes = []; + this.init(); + }, + + init() { this.fetching = true; - - const promise = this.requestInitPromise(); - - promise.then(notes => { - this.notes = notes; - this.requestInitPromise = null; + this.makePromise().then(x => { + if (Array.isArray(x)) { + this.notes = x; + } else { + this.notes = x.notes; + this.cursor = x.cursor; + } + this.inited = true; this.fetching = false; + this.$emit('inited'); }, e => { this.fetching = false; }); }, + more() { + if (this.cursor == null || this.moreFetching) return; + this.moreFetching = true; + this.makePromise(this.cursor).then(x => { + this.notes = this.notes.concat(x.notes); + this.cursor = x.cursor; + this.moreFetching = false; + }, e => { + this.moreFetching = false; + }); + }, + prepend(note, silent = false) { // 弾く if (shouldMuteNote(this.$store.state.i, this.$store.state.settings, note)) return; @@ -151,10 +172,6 @@ export default Vue.extend({ this.notes.push(note); }, - tail() { - return this.notes[this.notes.length - 1]; - }, - releaseQueue() { for (const n of this.queue) { this.prepend(n, true); @@ -162,15 +179,6 @@ export default Vue.extend({ this.queue = []; }, - async loadMore() { - if (this.more == null) return; - if (this.moreFetching) return; - - this.moreFetching = true; - await this.more(); - this.moreFetching = false; - }, - onScroll() { if (this.isScrollTop()) { this.releaseQueue(); @@ -178,7 +186,7 @@ export default Vue.extend({ if (this.$store.state.settings.fetchOnScroll !== false) { const current = window.scrollY + window.innerHeight; - if (current > document.body.offsetHeight - 8) this.loadMore(); + if (current > document.body.offsetHeight - 8) this.more(); } } } @@ -187,6 +195,11 @@ export default Vue.extend({ <style lang="stylus" scoped> .mk-notes + background var(--face) + box-shadow var(--shadow) + border-radius var(--round) + overflow hidden + .transition .mk-notes-enter .mk-notes-leave-to diff --git a/src/client/app/desktop/views/components/user-list-timeline.vue b/src/client/app/desktop/views/components/user-list-timeline.vue index 8afd95a68e..d61de06eed 100644 --- a/src/client/app/desktop/views/components/user-list-timeline.vue +++ b/src/client/app/desktop/views/components/user-list-timeline.vue @@ -1,6 +1,10 @@ <template> <div> - <mk-notes ref="timeline" :more="existMore ? more : null"/> + <mk-notes ref="timeline" :make-promise="makePromise" @inited="() => $emit('loaded')"> + <template slot="header"> + <slot></slot> + </template> + </mk-notes> </div> </template> @@ -13,10 +17,28 @@ export default Vue.extend({ props: ['list'], data() { return { - fetching: true, - moreFetching: false, - existMore: false, - connection: null + connection: null, + makePromise: cursor => this.$root.api('notes/user-list-timeline', { + listId: this.list.id, + limit: fetchLimit + 1, + untilId: cursor ? cursor : undefined, + includeMyRenotes: this.$store.state.settings.showMyRenotes, + includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, + includeLocalRenotes: this.$store.state.settings.showLocalRenotes + }).then(notes => { + if (notes.length == fetchLimit + 1) { + notes.pop(); + return { + notes: notes, + cursor: notes[notes.length - 1].id + }; + } else { + return { + notes: notes, + cursor: null + }; + } + }) }; }, watch: { @@ -37,63 +59,15 @@ export default Vue.extend({ this.connection.on('note', this.onNote); this.connection.on('userAdded', this.onUserAdded); this.connection.on('userRemoved', this.onUserRemoved); - - this.fetch(); - }, - fetch() { - this.fetching = true; - - (this.$refs.timeline as any).init(() => new Promise((res, rej) => { - this.$root.api('notes/user-list-timeline', { - listId: this.list.id, - limit: fetchLimit + 1, - includeMyRenotes: this.$store.state.settings.showMyRenotes, - includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, - includeLocalRenotes: this.$store.state.settings.showLocalRenotes - }).then(notes => { - if (notes.length == fetchLimit + 1) { - notes.pop(); - this.existMore = true; - } - res(notes); - this.fetching = false; - this.$emit('loaded'); - }, rej); - })); - }, - more() { - this.moreFetching = true; - - const promise = this.$root.api('notes/user-list-timeline', { - listId: this.list.id, - limit: fetchLimit + 1, - untilId: (this.$refs.timeline as any).tail().id, - includeMyRenotes: this.$store.state.settings.showMyRenotes, - includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, - includeLocalRenotes: this.$store.state.settings.showLocalRenotes - }); - - promise.then(notes => { - if (notes.length == fetchLimit + 1) { - notes.pop(); - } else { - this.existMore = false; - } - for (const n of notes) (this.$refs.timeline as any).append(n); - this.moreFetching = false; - }); - - return promise; }, onNote(note) { - // Prepend a note (this.$refs.timeline as any).prepend(note); }, onUserAdded() { - this.fetch(); + (this.$refs.timeline as any).reload(); }, onUserRemoved() { - this.fetch(); + (this.$refs.timeline as any).reload(); } } }); diff --git a/src/client/app/desktop/views/deck/deck.direct.vue b/src/client/app/desktop/views/deck/deck.direct.vue index c6c2b99233..2618363b14 100644 --- a/src/client/app/desktop/views/deck/deck.direct.vue +++ b/src/client/app/desktop/views/deck/deck.direct.vue @@ -1,5 +1,5 @@ <template> -<x-notes ref="timeline" :more="existMore ? more : null"/> +<x-notes ref="timeline" :make-promise="makePromise" @inited="() => $emit('loaded')"/> </template> <script lang="ts"> @@ -13,23 +13,36 @@ export default Vue.extend({ XNotes }, - props: { - }, - data() { return { - fetching: true, - moreFetching: false, - existMore: false, - connection: null + connection: null, + makePromise: cursor => this.$root.api('notes/mentions', { + limit: fetchLimit + 1, + untilId: cursor ? cursor : undefined, + includeMyRenotes: this.$store.state.settings.showMyRenotes, + includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, + includeLocalRenotes: this.$store.state.settings.showLocalRenotes, + visibility: 'specified' + }).then(notes => { + if (notes.length == fetchLimit + 1) { + notes.pop(); + return { + notes: notes, + cursor: notes[notes.length - 1].id + }; + } else { + return { + notes: notes, + cursor: null + }; + } + }) }; }, mounted() { this.connection = this.$root.stream.useSharedConnection('main'); this.connection.on('mention', this.onNote); - - this.fetch(); }, beforeDestroy() { @@ -37,55 +50,6 @@ export default Vue.extend({ }, methods: { - fetch() { - this.fetching = true; - - (this.$refs.timeline as any).init(() => new Promise((res, rej) => { - this.$root.api('notes/mentions', { - limit: fetchLimit + 1, - includeMyRenotes: this.$store.state.settings.showMyRenotes, - includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, - includeLocalRenotes: this.$store.state.settings.showLocalRenotes, - visibility: 'specified' - }).then(notes => { - if (notes.length == fetchLimit + 1) { - notes.pop(); - this.existMore = true; - } - res(notes); - this.fetching = false; - this.$emit('loaded'); - }, rej); - })); - }, - - more() { - this.moreFetching = true; - - const promise = this.$root.api('notes/mentions', { - limit: fetchLimit + 1, - untilId: (this.$refs.timeline as any).tail().id, - includeMyRenotes: this.$store.state.settings.showMyRenotes, - includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, - includeLocalRenotes: this.$store.state.settings.showLocalRenotes, - visibility: 'specified' - }); - - promise.then(notes => { - if (notes.length == fetchLimit + 1) { - notes.pop(); - } else { - this.existMore = false; - } - for (const n of notes) { - (this.$refs.timeline as any).append(n); - } - this.moreFetching = false; - }); - - return promise; - }, - onNote(note) { // Prepend a note if (note.visibility == 'specified') { diff --git a/src/client/app/desktop/views/deck/deck.favorites-column.vue b/src/client/app/desktop/views/deck/deck.favorites-column.vue index 3c2b50dee8..e0d5f8a339 100644 --- a/src/client/app/desktop/views/deck/deck.favorites-column.vue +++ b/src/client/app/desktop/views/deck/deck.favorites-column.vue @@ -5,7 +5,7 @@ </span> <div> - <x-notes ref="timeline" :more="existMore ? more : null"/> + <x-notes ref="timeline" :make-promise="makePromise" @inited="() => $emit('loaded')"/> </div> </x-column> </template> @@ -28,58 +28,28 @@ export default Vue.extend({ data() { return { - fetching: true, - moreFetching: false, - existMore: false, + makePromise: cursor => this.$root.api('i/favorites', { + limit: fetchLimit + 1, + untilId: cursor ? cursor : undefined, + }).then(notes => { + notes = notes.map(x => x.note); + if (notes.length == fetchLimit + 1) { + notes.pop(); + return { + notes: notes, + cursor: notes[notes.length - 1].id + }; + } else { + return { + notes: notes, + cursor: null + }; + } + }) }; }, - mounted() { - this.fetch(); - }, - methods: { - fetch() { - this.fetching = true; - - (this.$refs.timeline as any).init(() => new Promise((res, rej) => { - this.$root.api('i/favorites', { - limit: fetchLimit + 1, - }).then(notes => { - if (notes.length == fetchLimit + 1) { - notes.pop(); - this.existMore = true; - } - res(notes.map(x => x.note)); - this.fetching = false; - this.$emit('loaded'); - }, rej); - })); - }, - - more() { - this.moreFetching = true; - - const promise = this.$root.api('i/favorites', { - limit: fetchLimit + 1, - untilId: (this.$refs.timeline as any).tail().id, - }); - - promise.then(notes => { - if (notes.length == fetchLimit + 1) { - notes.pop(); - } else { - this.existMore = false; - } - for (const n of notes) { - (this.$refs.timeline as any).append(n); - } - this.moreFetching = false; - }); - - return promise; - }, - focus() { this.$refs.timeline.focus(); } diff --git a/src/client/app/desktop/views/deck/deck.featured-column.vue b/src/client/app/desktop/views/deck/deck.featured-column.vue index 78a5a7e3f5..e654c1eaae 100644 --- a/src/client/app/desktop/views/deck/deck.featured-column.vue +++ b/src/client/app/desktop/views/deck/deck.featured-column.vue @@ -5,7 +5,7 @@ </span> <div> - <x-notes ref="timeline" :more="null"/> + <x-notes ref="timeline" :make-promise="makePromise" @inited="() => $emit('loaded')"/> </div> </x-column> </template> @@ -27,31 +27,17 @@ export default Vue.extend({ data() { return { - fetching: true, - faNewspaper + faNewspaper, + makePromise: cursor => this.$root.api('notes/featured', { + limit: 20, + }).then(notes => { + notes.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); + return notes; + }) }; }, - mounted() { - this.fetch(); - }, - methods: { - fetch() { - this.fetching = true; - - (this.$refs.timeline as any).init(() => new Promise((res, rej) => { - this.$root.api('notes/featured', { - limit: 20, - }).then(notes => { - notes.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); - res(notes); - this.fetching = false; - this.$emit('loaded'); - }, rej); - })); - }, - focus() { this.$refs.timeline.focus(); } diff --git a/src/client/app/desktop/views/deck/deck.hashtag-tl.vue b/src/client/app/desktop/views/deck/deck.hashtag-tl.vue index 9a70733fda..e19c134849 100644 --- a/src/client/app/desktop/views/deck/deck.hashtag-tl.vue +++ b/src/client/app/desktop/views/deck/deck.hashtag-tl.vue @@ -1,5 +1,5 @@ <template> -<x-notes ref="timeline" :more="existMore ? more : null" :media-view="mediaView"/> +<x-notes ref="timeline" :make-promise="makePromise" :media-view="mediaView" @inited="() => $emit('loaded')"/> </template> <script lang="ts"> @@ -32,16 +32,35 @@ export default Vue.extend({ data() { return { - fetching: true, - moreFetching: false, - existMore: false, - connection: null + connection: null, + makePromise: cursor => this.$root.api('notes/search_by_tag', { + limit: fetchLimit + 1, + untilId: cursor ? cursor : undefined, + withFiles: this.mediaOnly, + includeMyRenotes: this.$store.state.settings.showMyRenotes, + includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, + includeLocalRenotes: this.$store.state.settings.showLocalRenotes, + query: this.tagTl.query + }).then(notes => { + if (notes.length == fetchLimit + 1) { + notes.pop(); + return { + notes: notes, + cursor: notes[notes.length - 1].id + }; + } else { + return { + notes: notes, + cursor: null + }; + } + }) }; }, watch: { mediaOnly() { - this.fetch(); + this.$refs.timeline.reload(); } }, @@ -51,8 +70,6 @@ export default Vue.extend({ q: this.tagTl.query }); this.connection.on('note', this.onNote); - - this.fetch(); }, beforeDestroy() { @@ -60,61 +77,8 @@ export default Vue.extend({ }, methods: { - fetch() { - this.fetching = true; - - (this.$refs.timeline as any).init(() => new Promise((res, rej) => { - this.$root.api('notes/search_by_tag', { - limit: fetchLimit + 1, - withFiles: this.mediaOnly, - includeMyRenotes: this.$store.state.settings.showMyRenotes, - includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, - includeLocalRenotes: this.$store.state.settings.showLocalRenotes, - query: this.tagTl.query - }).then(notes => { - if (notes.length == fetchLimit + 1) { - notes.pop(); - this.existMore = true; - } - res(notes); - this.fetching = false; - this.$emit('loaded'); - }, rej); - })); - }, - - more() { - this.moreFetching = true; - - const promise = this.$root.api('notes/search_by_tag', { - limit: fetchLimit + 1, - untilId: (this.$refs.timeline as any).tail().id, - withFiles: this.mediaOnly, - includeMyRenotes: this.$store.state.settings.showMyRenotes, - includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, - includeLocalRenotes: this.$store.state.settings.showLocalRenotes, - query: this.tagTl.query - }); - - promise.then(notes => { - if (notes.length == fetchLimit + 1) { - notes.pop(); - } else { - this.existMore = false; - } - for (const n of notes) { - (this.$refs.timeline as any).append(n); - } - this.moreFetching = false; - }); - - return promise; - }, - onNote(note) { if (this.mediaOnly && note.files.length == 0) return; - - // Prepend a note (this.$refs.timeline as any).prepend(note); }, diff --git a/src/client/app/desktop/views/deck/deck.list-tl.vue b/src/client/app/desktop/views/deck/deck.list-tl.vue index 68fbbb3ff9..7166263295 100644 --- a/src/client/app/desktop/views/deck/deck.list-tl.vue +++ b/src/client/app/desktop/views/deck/deck.list-tl.vue @@ -1,5 +1,5 @@ <template> -<x-notes ref="timeline" :more="existMore ? more : null" :media-view="mediaView"/> +<x-notes ref="timeline" :make-promise="makePromise" :media-view="mediaView" @inited="() => $emit('loaded')"/> </template> <script lang="ts"> @@ -32,16 +32,35 @@ export default Vue.extend({ data() { return { - fetching: true, - moreFetching: false, - existMore: false, - connection: null + connection: null, + makePromise: cursor => this.$root.api('notes/user-list-timeline', { + listId: this.list.id, + limit: fetchLimit + 1, + untilId: cursor ? cursor : undefined, + withFiles: this.mediaOnly, + includeMyRenotes: this.$store.state.settings.showMyRenotes, + includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, + includeLocalRenotes: this.$store.state.settings.showLocalRenotes + }).then(notes => { + if (notes.length == fetchLimit + 1) { + notes.pop(); + return { + notes: notes, + cursor: notes[notes.length - 1].id + }; + } else { + return { + notes: notes, + cursor: null + }; + } + }) }; }, watch: { mediaOnly() { - this.fetch(); + this.$refs.timeline.reload(); } }, @@ -53,8 +72,6 @@ export default Vue.extend({ this.connection.on('note', this.onNote); this.connection.on('userAdded', this.onUserAdded); this.connection.on('userRemoved', this.onUserRemoved); - - this.fetch(); }, beforeDestroy() { @@ -62,70 +79,17 @@ export default Vue.extend({ }, methods: { - fetch() { - this.fetching = true; - - (this.$refs.timeline as any).init(() => new Promise((res, rej) => { - this.$root.api('notes/user-list-timeline', { - listId: this.list.id, - limit: fetchLimit + 1, - withFiles: this.mediaOnly, - includeMyRenotes: this.$store.state.settings.showMyRenotes, - includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, - includeLocalRenotes: this.$store.state.settings.showLocalRenotes - }).then(notes => { - if (notes.length == fetchLimit + 1) { - notes.pop(); - this.existMore = true; - } - res(notes); - this.fetching = false; - this.$emit('loaded'); - }, rej); - })); - }, - - more() { - this.moreFetching = true; - - const promise = this.$root.api('notes/user-list-timeline', { - listId: this.list.id, - limit: fetchLimit + 1, - untilId: (this.$refs.timeline as any).tail().id, - withFiles: this.mediaOnly, - includeMyRenotes: this.$store.state.settings.showMyRenotes, - includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, - includeLocalRenotes: this.$store.state.settings.showLocalRenotes - }); - - promise.then(notes => { - if (notes.length == fetchLimit + 1) { - notes.pop(); - } else { - this.existMore = false; - } - for (const n of notes) { - (this.$refs.timeline as any).append(n); - } - this.moreFetching = false; - }); - - return promise; - }, - onNote(note) { if (this.mediaOnly && note.files.length == 0) return; - - // Prepend a note (this.$refs.timeline as any).prepend(note); }, onUserAdded() { - this.fetch(); + this.$refs.timeline.reload(); }, onUserRemoved() { - this.fetch(); + this.$refs.timeline.reload(); }, focus() { diff --git a/src/client/app/desktop/views/deck/deck.mentions.vue b/src/client/app/desktop/views/deck/deck.mentions.vue index 5fcabde5d6..1efd778226 100644 --- a/src/client/app/desktop/views/deck/deck.mentions.vue +++ b/src/client/app/desktop/views/deck/deck.mentions.vue @@ -1,5 +1,5 @@ <template> -<x-notes ref="timeline" :more="existMore ? more : null"/> +<x-notes ref="timeline" :make-promise="makePromise" @inited="() => $emit('loaded')"/> </template> <script lang="ts"> @@ -13,23 +13,35 @@ export default Vue.extend({ XNotes }, - props: { - }, - data() { return { - fetching: true, - moreFetching: false, - existMore: false, - connection: null + connection: null, + makePromise: cursor => this.$root.api('notes/mentions', { + limit: fetchLimit + 1, + untilId: cursor ? cursor : undefined, + includeMyRenotes: this.$store.state.settings.showMyRenotes, + includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, + includeLocalRenotes: this.$store.state.settings.showLocalRenotes + }).then(notes => { + if (notes.length == fetchLimit + 1) { + notes.pop(); + return { + notes: notes, + cursor: notes[notes.length - 1].id + }; + } else { + return { + notes: notes, + cursor: null + }; + } + }) }; }, mounted() { this.connection = this.$root.stream.useSharedConnection('main'); this.connection.on('mention', this.onNote); - - this.fetch(); }, beforeDestroy() { @@ -37,55 +49,7 @@ export default Vue.extend({ }, methods: { - fetch() { - this.fetching = true; - - (this.$refs.timeline as any).init(() => new Promise((res, rej) => { - this.$root.api('notes/mentions', { - limit: fetchLimit + 1, - includeMyRenotes: this.$store.state.settings.showMyRenotes, - includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, - includeLocalRenotes: this.$store.state.settings.showLocalRenotes - }).then(notes => { - if (notes.length == fetchLimit + 1) { - notes.pop(); - this.existMore = true; - } - res(notes); - this.fetching = false; - this.$emit('loaded'); - }, rej); - })); - }, - - more() { - this.moreFetching = true; - - const promise = this.$root.api('notes/mentions', { - limit: fetchLimit + 1, - untilId: (this.$refs.timeline as any).tail().id, - includeMyRenotes: this.$store.state.settings.showMyRenotes, - includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, - includeLocalRenotes: this.$store.state.settings.showLocalRenotes - }); - - promise.then(notes => { - if (notes.length == fetchLimit + 1) { - notes.pop(); - } else { - this.existMore = false; - } - for (const n of notes) { - (this.$refs.timeline as any).append(n); - } - this.moreFetching = false; - }); - - return promise; - }, - onNote(note) { - // Prepend a note (this.$refs.timeline as any).prepend(note); }, diff --git a/src/client/app/desktop/views/deck/deck.notes.vue b/src/client/app/desktop/views/deck/deck.notes.vue index 260d75a884..e7fa9fd52a 100644 --- a/src/client/app/desktop/views/deck/deck.notes.vue +++ b/src/client/app/desktop/views/deck/deck.notes.vue @@ -1,6 +1,8 @@ <template> <div class="eamppglmnmimdhrlzhplwpvyeaqmmhxu"> - <slot name="empty" v-if="notes.length == 0 && !fetching && requestInitPromise == null"></slot> + <slot name="empty" v-if="notes.length == 0 && !fetching && inited"></slot> + + <mk-error v-if="!fetching && !inited" @retry="init()"/> <div class="placeholder" v-if="fetching"> <template v-for="i in 10"> @@ -8,8 +10,6 @@ </template> </div> - <mk-error v-if="!fetching && requestInitPromise != null" @retry="resolveInitPromise"/> - <!-- トランジションを有効にするとなぜかメモリリークする --> <component :is="!$store.state.device.reduceMotion ? 'transition-group' : 'div'" name="mk-notes" class="transition notes" ref="notes" tag="div"> <template v-for="(note, i) in _notes"> @@ -27,8 +27,8 @@ </template> </component> - <footer v-if="more"> - <button @click="loadMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }"> + <footer v-if="cursor != null"> + <button @click="more" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }"> <template v-if="!moreFetching">{{ $t('@.load-more') }}</template> <template v-if="moreFetching"><fa icon="spinner" pulse fixed-width/></template> </button> @@ -40,13 +40,13 @@ import Vue from 'vue'; import i18n from '../../../i18n'; import shouldMuteNote from '../../../common/scripts/should-mute-note'; - import XNote from '../components/note.vue'; const displayLimit = 20; export default Vue.extend({ i18n: i18n(), + components: { XNote }, @@ -54,9 +54,8 @@ export default Vue.extend({ inject: ['column', 'isScrollTop', 'count'], props: { - more: { - type: Function, - required: false + makePromise: { + required: true }, mediaView: { type: Boolean, @@ -68,11 +67,12 @@ export default Vue.extend({ data() { return { rootEl: null, - requestInitPromise: null as () => Promise<any[]>, notes: [], queue: [], fetching: true, - moreFetching: false + moreFetching: false, + inited: false, + cursor: null }; }, @@ -97,6 +97,7 @@ export default Vue.extend({ created() { this.column.$on('top', this.onTop); this.column.$on('bottom', this.onBottom); + this.init(); }, beforeDestroy() { @@ -113,27 +114,41 @@ export default Vue.extend({ Vue.set((this as any).notes, i, note); }, - init(promiseGenerator: () => Promise<any[]>) { - this.requestInitPromise = promiseGenerator; - this.resolveInitPromise(); - }, - - resolveInitPromise() { + reload() { this.queue = []; this.notes = []; + this.init(); + }, + + init() { this.fetching = true; - - const promise = this.requestInitPromise(); - - promise.then(notes => { - this.notes = notes; - this.requestInitPromise = null; + this.makePromise().then(x => { + if (Array.isArray(x)) { + this.notes = x; + } else { + this.notes = x.notes; + this.cursor = x.cursor; + } + this.inited = true; this.fetching = false; + this.$emit('inited'); }, e => { this.fetching = false; }); }, + more() { + if (this.cursor == null || this.moreFetching) return; + this.moreFetching = true; + this.makePromise(this.cursor).then(x => { + this.notes = this.notes.concat(x.notes); + this.cursor = x.cursor; + this.moreFetching = false; + }, e => { + this.moreFetching = false; + }); + }, + prepend(note, silent = false) { // 弾く if (shouldMuteNote(this.$store.state.i, this.$store.state.settings, note)) return; @@ -160,10 +175,6 @@ export default Vue.extend({ this.notes.push(note); }, - tail() { - return this.notes[this.notes.length - 1]; - }, - releaseQueue() { for (const n of this.queue) { this.prepend(n, true); @@ -171,21 +182,12 @@ export default Vue.extend({ this.queue = []; }, - async loadMore() { - if (this.more == null) return; - if (this.moreFetching) return; - - this.moreFetching = true; - await this.more(); - this.moreFetching = false; - }, - onTop() { this.releaseQueue(); }, onBottom() { - this.loadMore(); + this.more(); } } }); diff --git a/src/client/app/desktop/views/deck/deck.search-column.vue b/src/client/app/desktop/views/deck/deck.search-column.vue index d732f524da..cc719bdf5c 100644 --- a/src/client/app/desktop/views/deck/deck.search-column.vue +++ b/src/client/app/desktop/views/deck/deck.search-column.vue @@ -5,7 +5,7 @@ </span> <div> - <x-notes ref="timeline" :more="existMore ? more : null"/> + <x-notes ref="timeline" :make-promise="makePromise" @inited="() => $emit('loaded')"/> </div> </x-column> </template> @@ -25,12 +25,24 @@ export default Vue.extend({ data() { return { - fetching: true, - moreFetching: false, - existMore: false, - offset: 0, - empty: false, - notAvailable: false + makePromise: cursor => this.$root.api('notes/search', { + limit: limit + 1, + offset: cursor ? cursor : undefined, + query: this.q + }).then(notes => { + if (notes.length == limit + 1) { + notes.pop(); + return { + notes: notes, + cursor: cursor ? cursor + limit : limit + }; + } else { + return { + notes: notes, + cursor: null + }; + } + }) }; }, @@ -41,59 +53,9 @@ export default Vue.extend({ }, watch: { - $route: 'fetch' - }, - - created() { - this.fetch(); - }, - - methods: { - fetch() { - this.fetching = true; - - (this.$refs.timeline as any).init(() => new Promise((res, rej) => { - this.$root.api('notes/search', { - limit: limit + 1, - offset: this.offset, - query: this.q - }).then(notes => { - if (notes.length == 0) this.empty = true; - if (notes.length == limit + 1) { - notes.pop(); - this.existMore = true; - } - res(notes); - this.fetching = false; - }, (e: string) => { - this.fetching = false; - if (e === 'searching not available') this.notAvailable = true; - }); - })); - }, - more() { - this.offset += limit; - - const promise = this.$root.api('notes/search', { - limit: limit + 1, - offset: this.offset, - query: this.q - }); - - promise.then(notes => { - if (notes.length == limit + 1) { - notes.pop(); - } else { - this.existMore = false; - } - for (const n of notes) { - (this.$refs.timeline as any).append(n); - } - this.moreFetching = false; - }); - - return promise; + $route() { + this.$refs.timeline.reload(); } - } + }, }); </script> diff --git a/src/client/app/desktop/views/deck/deck.tl.vue b/src/client/app/desktop/views/deck/deck.tl.vue index 16f268f2c1..263c2a0820 100644 --- a/src/client/app/desktop/views/deck/deck.tl.vue +++ b/src/client/app/desktop/views/deck/deck.tl.vue @@ -6,7 +6,7 @@ </p> <p class="desc">{{ $t('disabled-timeline.description') }}</p> </div> -<x-notes v-else ref="timeline" :more="existMore ? more : null" :media-view="mediaView"/> +<x-notes v-else ref="timeline" :make-promise="makePromise" :media-view="mediaView" @inited="() => $emit('loaded')"/> </template> <script lang="ts"> @@ -44,12 +44,10 @@ export default Vue.extend({ data() { return { - fetching: true, - moreFetching: false, - existMore: false, connection: null, disabled: false, - faMinusCircle + faMinusCircle, + makePromise: null }; }, @@ -79,6 +77,28 @@ export default Vue.extend({ } }, + created() { + this.makePromise = cursor => this.$root.api(this.endpoint, { + limit: fetchLimit + 1, + untilDate: cursor ? undefined : (this.date ? this.date.getTime() : undefined), + untilId: cursor ? cursor : undefined, + ...this.baseQuery, ...this.query + }).then(notes => { + if (notes.length == fetchLimit + 1) { + notes.pop(); + return { + notes: notes, + cursor: notes[notes.length - 1].id + }; + } else { + return { + notes: notes, + cursor: null + }; + } + }); + }, + mounted() { this.connection = this.stream; @@ -93,8 +113,6 @@ export default Vue.extend({ meta.disableLocalTimeline && ['local', 'hybrid'].includes(this.src) || meta.disableGlobalTimeline && ['global'].includes(this.src)); }); - - this.fetch(); }, beforeDestroy() { @@ -102,64 +120,13 @@ export default Vue.extend({ }, methods: { - fetch() { - this.fetching = true; - - (this.$refs.timeline as any).init(() => new Promise((res, rej) => { - this.$root.api(this.endpoint, { - limit: fetchLimit + 1, - withFiles: this.mediaOnly, - includeMyRenotes: this.$store.state.settings.showMyRenotes, - includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, - includeLocalRenotes: this.$store.state.settings.showLocalRenotes - }).then(notes => { - if (notes.length == fetchLimit + 1) { - notes.pop(); - this.existMore = true; - } - res(notes); - this.fetching = false; - this.$emit('loaded'); - }, rej); - })); - }, - - more() { - this.moreFetching = true; - - const promise = this.$root.api(this.endpoint, { - limit: fetchLimit + 1, - withFiles: this.mediaOnly, - untilId: (this.$refs.timeline as any).tail().id, - includeMyRenotes: this.$store.state.settings.showMyRenotes, - includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, - includeLocalRenotes: this.$store.state.settings.showLocalRenotes - }); - - promise.then(notes => { - if (notes.length == fetchLimit + 1) { - notes.pop(); - } else { - this.existMore = false; - } - for (const n of notes) { - (this.$refs.timeline as any).append(n); - } - this.moreFetching = false; - }); - - return promise; - }, - onNote(note) { if (this.mediaOnly && note.files.length == 0) return; - - // Prepend a note (this.$refs.timeline as any).prepend(note); }, onChangeFollowing() { - this.fetch(); + (this.$refs.timeline as any).reload(); }, focus() { diff --git a/src/client/app/desktop/views/deck/deck.user-column.home.vue b/src/client/app/desktop/views/deck/deck.user-column.home.vue index 966c5bdb1b..ff13bc3124 100644 --- a/src/client/app/desktop/views/deck/deck.user-column.home.vue +++ b/src/client/app/desktop/views/deck/deck.user-column.home.vue @@ -26,7 +26,7 @@ <ui-container> <span slot="header"><fa :icon="['far', 'comment-alt']"/> {{ $t('timeline') }}</span> <div> - <x-notes ref="timeline" :more="existMore ? fetchMoreNotes : null"/> + <x-notes ref="timeline" :make-promise="makePromise" @inited="() => $emit('loaded')"/> </div> </ui-container> </div> @@ -35,7 +35,6 @@ <script lang="ts"> import Vue from 'vue'; import i18n from '../../../i18n'; -import parseAcct from '../../../../../misc/acct/parse'; import XNotes from './deck.notes.vue'; import XNote from '../components/note.vue'; import { concat } from '../../../../../prelude/array'; @@ -45,6 +44,7 @@ const fetchLimit = 10; export default Vue.extend({ i18n: i18n('deck/deck.user-column.vue'), + components: { XNotes, XNote @@ -59,10 +59,30 @@ export default Vue.extend({ data() { return { - existMore: false, - moreFetching: false, withFiles: false, images: [], + makePromise: cursor => this.$root.api('users/notes', { + userId: this.user.id, + limit: fetchLimit + 1, + untilId: cursor ? cursor : undefined, + withFiles: this.withFiles, + includeMyRenotes: this.$store.state.settings.showMyRenotes, + includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, + includeLocalRenotes: this.$store.state.settings.showLocalRenotes + }).then(notes => { + if (notes.length == fetchLimit + 1) { + notes.pop(); + return { + notes: notes, + cursor: notes[notes.length - 1].id + }; + } else { + return { + notes: notes, + cursor: null + }; + } + }) }; }, @@ -72,10 +92,6 @@ export default Vue.extend({ methods: { fetch() { - this.$nextTick(() => { - (this.$refs.timeline as any).init(() => this.initTl()); - }); - const image = [ 'image/jpeg', 'image/png', @@ -177,52 +193,6 @@ export default Vue.extend({ chart.render(); }); }, - - initTl() { - return new Promise((res, rej) => { - this.$root.api('users/notes', { - userId: this.user.id, - limit: fetchLimit + 1, - untilDate: new Date().getTime() + 1000 * 86400 * 365, - withFiles: this.withFiles, - includeMyRenotes: this.$store.state.settings.showMyRenotes, - includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, - includeLocalRenotes: this.$store.state.settings.showLocalRenotes - }).then(notes => { - if (notes.length == fetchLimit + 1) { - notes.pop(); - this.existMore = true; - } - res(notes); - }, rej); - }); - }, - - fetchMoreNotes() { - this.moreFetching = true; - - const promise = this.$root.api('users/notes', { - userId: this.user.id, - limit: fetchLimit + 1, - untilDate: new Date((this.$refs.timeline as any).tail().createdAt).getTime(), - withFiles: this.withFiles, - includeMyRenotes: this.$store.state.settings.showMyRenotes, - includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, - includeLocalRenotes: this.$store.state.settings.showLocalRenotes - }); - - promise.then(notes => { - if (notes.length == fetchLimit + 1) { - notes.pop(); - } else { - this.existMore = false; - } - for (const n of notes) (this.$refs.timeline as any).append(n); - this.moreFetching = false; - }); - - return promise; - } } }); </script> diff --git a/src/client/app/desktop/views/home/search.vue b/src/client/app/desktop/views/home/search.vue index 993467b4bf..cd21bd5b2a 100644 --- a/src/client/app/desktop/views/home/search.vue +++ b/src/client/app/desktop/views/home/search.vue @@ -1,13 +1,10 @@ <template> -<div class="oxgbmvii"> - <div class="notes"> - <header> +<div> + <mk-notes ref="timeline" :make-promise="makePromise" @inited="inited"> + <header slot="header" class="oxgbmvii"> <span><fa icon="search"/> {{ q }}</span> </header> - <p v-if="!fetching && notAvailable">{{ $t('not-available') }}</p> - <p v-if="!fetching && empty"><fa icon="search"/> {{ $t('not-found', { q }) }}</p> - <mk-notes ref="timeline" :more="existMore ? more : null"/> - </div> + </mk-notes> </div> </template> @@ -22,27 +19,40 @@ export default Vue.extend({ i18n: i18n('desktop/views/pages/search.vue'), data() { return { - fetching: true, - moreFetching: false, - existMore: false, - offset: 0, - empty: false, - notAvailable: false + makePromise: cursor => this.$root.api('notes/search', { + limit: limit + 1, + offset: cursor ? cursor : undefined, + query: this.q + }).then(notes => { + if (notes.length == limit + 1) { + notes.pop(); + return { + notes: notes, + cursor: cursor ? cursor + limit : limit + }; + } else { + return { + notes: notes, + cursor: null + }; + } + }) }; }, - watch: { - $route: 'fetch' - }, computed: { q(): string { return this.$route.query.q; } }, + watch: { + $route() { + this.$refs.timeline.reload(); + } + }, mounted() { document.addEventListener('keydown', this.onDocumentKeydown); window.addEventListener('scroll', this.onScroll, { passive: true }); - - this.fetch(); + Progress.start(); }, beforeDestroy() { document.removeEventListener('keydown', this.onDocumentKeydown); @@ -56,75 +66,23 @@ export default Vue.extend({ } } }, - fetch() { - this.fetching = true; - Progress.start(); - - (this.$refs.timeline as any).init(() => new Promise((res, rej) => { - this.$root.api('notes/search', { - limit: limit + 1, - offset: this.offset, - query: this.q - }).then(notes => { - if (notes.length == 0) this.empty = true; - if (notes.length == limit + 1) { - notes.pop(); - this.existMore = true; - } - res(notes); - this.fetching = false; - Progress.done(); - }, (e: string) => { - this.fetching = false; - Progress.done(); - if (e === 'searching not available') this.notAvailable = true; - }); - })); + inited() { + Progress.done(); }, - more() { - this.offset += limit; - - const promise = this.$root.api('notes/search', { - limit: limit + 1, - offset: this.offset, - query: this.q - }); - - promise.then(notes => { - if (notes.length == limit + 1) { - notes.pop(); - } else { - this.existMore = false; - } - for (const n of notes) { - (this.$refs.timeline as any).append(n); - } - this.moreFetching = false; - }); - - return promise; - } } }); </script> <style lang="stylus" scoped> .oxgbmvii - > .notes - background var(--face) - box-shadow var(--shadow) - border-radius var(--round) - overflow hidden + padding 0 8px + z-index 10 + background var(--faceHeader) + box-shadow 0 var(--lineWidth) var(--desktopTimelineHeaderShadow) - > header - padding 0 8px - z-index 10 - background var(--faceHeader) - box-shadow 0 var(--lineWidth) var(--desktopTimelineHeaderShadow) - - > span - padding 0 8px - font-size 0.9em - line-height 42px - color var(--text) + > span + padding 0 8px + font-size 0.9em + line-height 42px + color var(--text) </style> diff --git a/src/client/app/desktop/views/home/tag.vue b/src/client/app/desktop/views/home/tag.vue index 182c8f3512..2f9854c074 100644 --- a/src/client/app/desktop/views/home/tag.vue +++ b/src/client/app/desktop/views/home/tag.vue @@ -1,7 +1,10 @@ <template> <div> - <p :class="$style.empty" v-if="!fetching && empty"><fa icon="search"/> {{ $t('no-posts-found', { q: $route.params.tag }) }}</p> - <mk-notes ref="timeline" :class="$style.notes" :more="existMore ? more : null"/> + <mk-notes ref="timeline" :make-promise="makePromise" @inited="inited"> + <header class="wqraeznr" slot="header"> + <span><fa icon="hashtag"/> {{ $route.params.tag }}</span> + </header> + </mk-notes> </div> </template> @@ -16,21 +19,35 @@ export default Vue.extend({ i18n: i18n('desktop/views/pages/tag.vue'), data() { return { - fetching: true, - moreFetching: false, - existMore: false, - offset: 0, - empty: false + makePromise: cursor => this.$root.api('notes/search_by_tag', { + limit: limit + 1, + offset: cursor ? cursor : undefined, + tag: this.$route.params.tag + }).then(notes => { + if (notes.length == limit + 1) { + notes.pop(); + return { + notes: notes, + cursor: cursor ? cursor + limit : limit + }; + } else { + return { + notes: notes, + cursor: null + }; + } + }) }; }, watch: { - $route: 'fetch' + $route() { + this.$refs.timeline.reload(); + } }, mounted() { document.addEventListener('keydown', this.onDocumentKeydown); window.addEventListener('scroll', this.onScroll, { passive: true }); - - this.fetch(); + Progress.start(); }, beforeDestroy() { document.removeEventListener('keydown', this.onDocumentKeydown); @@ -44,73 +61,23 @@ export default Vue.extend({ } } }, - fetch() { - this.fetching = true; - Progress.start(); - - (this.$refs.timeline as any).init(() => new Promise((res, rej) => { - this.$root.api('notes/search_by_tag', { - limit: limit + 1, - offset: this.offset, - tag: this.$route.params.tag - }).then(notes => { - if (notes.length == 0) this.empty = true; - if (notes.length == limit + 1) { - notes.pop(); - this.existMore = true; - } - res(notes); - this.fetching = false; - Progress.done(); - }, rej); - })); + inited() { + Progress.done(); }, - more() { - this.offset += limit; - - const promise = this.$root.api('notes/search_by_tag', { - limit: limit + 1, - offset: this.offset, - tag: this.$route.params.tag - }); - - promise.then(notes => { - if (notes.length == limit + 1) { - notes.pop(); - } else { - this.existMore = false; - } - for (const n of notes) { - (this.$refs.timeline as any).append(n); - } - this.moreFetching = false; - }); - - return promise; - } } }); </script> -<style lang="stylus" module> -.notes - background var(--face) - box-shadow var(--shadow) - border-radius var(--round) - overflow hidden - -.empty - display block - margin 0 auto - padding 32px - max-width 400px - text-align center - color #999 - - > [data-icon] - display block - margin-bottom 16px - font-size 3em - color #ccc +<style lang="stylus" scoped> +.wqraeznr + padding 0 8px + z-index 10 + background var(--faceHeader) + box-shadow 0 var(--lineWidth) var(--desktopTimelineHeaderShadow) + > span + padding 0 8px + font-size 0.9em + line-height 42px + color var(--text) </style> diff --git a/src/client/app/desktop/views/home/timeline.core.vue b/src/client/app/desktop/views/home/timeline.core.vue index 704ca48ae4..efffc0b4de 100644 --- a/src/client/app/desktop/views/home/timeline.core.vue +++ b/src/client/app/desktop/views/home/timeline.core.vue @@ -5,8 +5,11 @@ <router-link to="/explore">{{ $t('@.empty-timeline-info.explore') }}</router-link> </div> - <mk-notes ref="timeline" :more="existMore ? more : null"> - <p :class="$style.empty" slot="empty"> + <mk-notes ref="timeline" :make-promise="makePromise" @inited="() => $emit('loaded')"> + <template slot="header"> + <slot></slot> + </template> + <p slot="empty"> <fa :icon="['far', 'comments']"/>{{ $t('empty') }} </p> </mk-notes> @@ -21,6 +24,7 @@ const fetchLimit = 10; export default Vue.extend({ i18n: i18n('desktop/views/components/timeline.core.vue'), + props: { src: { type: String, @@ -33,9 +37,6 @@ export default Vue.extend({ data() { return { - fetching: true, - moreFetching: false, - existMore: false, connection: null, date: null, baseQuery: { @@ -44,21 +45,18 @@ export default Vue.extend({ includeLocalRenotes: this.$store.state.settings.showLocalRenotes }, query: {}, - endpoint: null + endpoint: null, + makePromise: null }; }, computed: { alone(): boolean { return this.$store.state.i.followingCount == 0; - }, - - canFetchMore(): boolean { - return !this.moreFetching && !this.fetching && this.existMore; } }, - mounted() { + created() { const prepend = note => { (this.$refs.timeline as any).prepend(note); }; @@ -109,7 +107,25 @@ export default Vue.extend({ this.connection.on('mention', onNote); } - this.fetch(); + this.makePromise = cursor => this.$root.api(this.endpoint, { + limit: fetchLimit + 1, + untilDate: cursor ? undefined : (this.date ? this.date.getTime() : undefined), + untilId: cursor ? cursor : undefined, + ...this.baseQuery, ...this.query + }).then(notes => { + if (notes.length == fetchLimit + 1) { + notes.pop(); + return { + notes: notes, + cursor: notes[notes.length - 1].id + }; + } else { + return { + notes: notes, + cursor: null + }; + } + }); }, beforeDestroy() { @@ -117,57 +133,8 @@ export default Vue.extend({ }, methods: { - fetch() { - this.fetching = true; - - (this.$refs.timeline as any).init(() => new Promise((res, rej) => { - this.$root.api(this.endpoint, Object.assign({ - limit: fetchLimit + 1, - untilDate: this.date ? this.date.getTime() : undefined - }, this.baseQuery, this.query)).then(notes => { - if (notes.length == fetchLimit + 1) { - notes.pop(); - this.existMore = true; - } - res(notes); - this.fetching = false; - this.$emit('loaded'); - }, rej); - })); - }, - - more() { - if (!this.canFetchMore) return; - - this.moreFetching = true; - - const promise = this.$root.api(this.endpoint, Object.assign({ - limit: fetchLimit + 1, - untilId: (this.$refs.timeline as any).tail().id - }, this.baseQuery, this.query)); - - promise.then(notes => { - if (notes.length == fetchLimit + 1) { - notes.pop(); - } else { - this.existMore = false; - } - for (const n of notes) { - (this.$refs.timeline as any).append(n); - } - this.moreFetching = false; - }); - - return promise; - }, - focus() { (this.$refs.timeline as any).focus(); - }, - - warp(date) { - this.date = date; - this.fetch(); } } }); @@ -186,20 +153,3 @@ export default Vue.extend({ margin 0 0 8px 0 </style> - -<style lang="stylus" module> -.empty - display block - margin 0 auto - padding 32px - max-width 400px - text-align center - color #999 - - > [data-icon] - display block - margin-bottom 16px - font-size 3em - color #ccc - -</style> diff --git a/src/client/app/desktop/views/home/timeline.vue b/src/client/app/desktop/views/home/timeline.vue index 2f42b9723f..573cc95a9e 100644 --- a/src/client/app/desktop/views/home/timeline.vue +++ b/src/client/app/desktop/views/home/timeline.vue @@ -1,29 +1,23 @@ <template> -<div class="mk-timeline"> +<div class="pwbzawku"> <mk-post-form class="form" v-if="$store.state.settings.showPostFormOnTopOfTl"/> <div class="main"> - <header> - <span :data-active="src == 'home'" @click="src = 'home'"><fa icon="home"/> {{ $t('home') }}</span> - <span :data-active="src == 'local'" @click="src = 'local'" v-if="enableLocalTimeline"><fa :icon="['far', 'comments']"/> {{ $t('local') }}</span> - <span :data-active="src == 'hybrid'" @click="src = 'hybrid'" v-if="enableLocalTimeline"><fa icon="share-alt"/> {{ $t('hybrid') }}</span> - <span :data-active="src == 'global'" @click="src = 'global'" v-if="enableGlobalTimeline"><fa icon="globe"/> {{ $t('global') }}</span> - <span :data-active="src == 'tag'" @click="src = 'tag'" v-if="tagTl"><fa icon="hashtag"/> {{ tagTl.title }}</span> - <span :data-active="src == 'list'" @click="src = 'list'" v-if="list"><fa icon="list"/> {{ list.title }}</span> - <div class="buttons"> - <button :data-active="src == 'mentions'" @click="src = 'mentions'" :title="$t('mentions')"><fa icon="at"/><i class="badge" v-if="$store.state.i.hasUnreadMentions"><fa icon="circle"/></i></button> - <button :data-active="src == 'messages'" @click="src = 'messages'" :title="$t('messages')"><fa :icon="['far', 'envelope']"/><i class="badge" v-if="$store.state.i.hasUnreadSpecifiedNotes"><fa icon="circle"/></i></button> - <button @click="chooseTag" :title="$t('hashtag')" ref="tagButton"><fa icon="hashtag"/></button> - <button @click="chooseList" :title="$t('list')" ref="listButton"><fa icon="list"/></button> - </div> - </header> - <x-core v-if="src == 'home'" ref="tl" key="home" src="home"/> - <x-core v-if="src == 'local'" ref="tl" key="local" src="local"/> - <x-core v-if="src == 'hybrid'" ref="tl" key="hybrid" src="hybrid"/> - <x-core v-if="src == 'global'" ref="tl" key="global" src="global"/> - <x-core v-if="src == 'mentions'" ref="tl" key="mentions" src="mentions"/> - <x-core v-if="src == 'messages'" ref="tl" key="messages" src="messages"/> - <x-core v-if="src == 'tag'" ref="tl" key="tag" src="tag" :tag-tl="tagTl"/> - <mk-user-list-timeline v-if="src == 'list'" ref="tl" :key="list.id" :list="list"/> + <component :is="src == 'list' ? 'mk-user-list-timeline' : 'x-core'" ref="tl" v-bind="options"> + <header class="zahtxcqi"> + <span :data-active="src == 'home'" @click="src = 'home'"><fa icon="home"/> {{ $t('home') }}</span> + <span :data-active="src == 'local'" @click="src = 'local'" v-if="enableLocalTimeline"><fa :icon="['far', 'comments']"/> {{ $t('local') }}</span> + <span :data-active="src == 'hybrid'" @click="src = 'hybrid'" v-if="enableLocalTimeline"><fa icon="share-alt"/> {{ $t('hybrid') }}</span> + <span :data-active="src == 'global'" @click="src = 'global'" v-if="enableGlobalTimeline"><fa icon="globe"/> {{ $t('global') }}</span> + <span :data-active="src == 'tag'" @click="src = 'tag'" v-if="tagTl"><fa icon="hashtag"/> {{ tagTl.title }}</span> + <span :data-active="src == 'list'" @click="src = 'list'" v-if="list"><fa icon="list"/> {{ list.title }}</span> + <div class="buttons"> + <button :data-active="src == 'mentions'" @click="src = 'mentions'" :title="$t('mentions')"><fa icon="at"/><i class="badge" v-if="$store.state.i.hasUnreadMentions"><fa icon="circle"/></i></button> + <button :data-active="src == 'messages'" @click="src = 'messages'" :title="$t('messages')"><fa :icon="['far', 'envelope']"/><i class="badge" v-if="$store.state.i.hasUnreadSpecifiedNotes"><fa icon="circle"/></i></button> + <button @click="chooseTag" :title="$t('hashtag')" ref="tagButton"><fa icon="hashtag"/></button> + <button @click="chooseList" :title="$t('list')" ref="listButton"><fa icon="list"/></button> + </div> + </header> + </component> </div> </div> </template> @@ -51,6 +45,16 @@ export default Vue.extend({ }; }, + computed: { + options(): any { + return { + ...(this.src == 'list' ? { list: this.list } : { src: this.src }), + ...(this.src == 'tag' ? { tagTl: this.tagTl } : {}), + key: this.src == 'list' ? this.list.id : this.src + } + } + }, + watch: { src() { this.saveSrc(); @@ -186,88 +190,82 @@ export default Vue.extend({ </script> <style lang="stylus" scoped> -.mk-timeline +.pwbzawku > .form margin-bottom 16px box-shadow var(--shadow) border-radius var(--round) - > .main - background var(--face) - box-shadow var(--shadow) - border-radius var(--round) - overflow hidden + .zahtxcqi + padding 0 8px + z-index 10 + background var(--faceHeader) + box-shadow 0 var(--lineWidth) var(--desktopTimelineHeaderShadow) - > header - padding 0 8px - z-index 10 - background var(--faceHeader) - box-shadow 0 var(--lineWidth) var(--desktopTimelineHeaderShadow) + > .buttons + position absolute + z-index 2 + top 0 + right 0 + padding-right 8px - > .buttons - position absolute - z-index 2 - top 0 - right 0 - padding-right 8px - - > button - padding 0 8px - font-size 0.9em - line-height 42px - color var(--faceTextButton) - - > .badge - position absolute - top -4px - right 4px - font-size 10px - color var(--notificationIndicator) - - &:hover - color var(--faceTextButtonHover) - - &[data-active] - color var(--primary) - cursor default - - &:before - content "" - display block - position absolute - bottom 0 - left 0 - width 100% - height 2px - background var(--primary) - - > span - display inline-block - padding 0 10px + > button + padding 0 8px + font-size 0.9em line-height 42px - font-size 12px - user-select none + color var(--faceTextButton) + + > .badge + position absolute + top -4px + right 4px + font-size 10px + color var(--notificationIndicator) + + &:hover + color var(--faceTextButtonHover) &[data-active] color var(--primary) cursor default - font-weight bold &:before content "" display block position absolute bottom 0 - left -8px - width calc(100% + 16px) + left 0 + width 100% height 2px background var(--primary) - &:not([data-active]) - color var(--desktopTimelineSrc) - cursor pointer + > span + display inline-block + padding 0 10px + line-height 42px + font-size 12px + user-select none - &:hover - color var(--desktopTimelineSrcHover) + &[data-active] + color var(--primary) + cursor default + font-weight bold + + &:before + content "" + display block + position absolute + bottom 0 + left -8px + width calc(100% + 16px) + height 2px + background var(--primary) + + &:not([data-active]) + color var(--desktopTimelineSrc) + cursor pointer + + &:hover + color var(--desktopTimelineSrcHover) </style> diff --git a/src/client/app/desktop/views/home/user/user.home.vue b/src/client/app/desktop/views/home/user/user.home.vue index 3a999b5739..65aa5e1c8a 100644 --- a/src/client/app/desktop/views/home/user/user.home.vue +++ b/src/client/app/desktop/views/home/user/user.home.vue @@ -10,7 +10,7 @@ </ui-container> </div> <x-photos :user="user"/> - <x-timeline class="timeline" ref="tl" :user="user"/> + <x-timeline ref="tl" :user="user"/> </div> </template> @@ -51,7 +51,4 @@ export default Vue.extend({ > * margin-bottom 16px - > .timeline - box-shadow var(--shadow) - </style> diff --git a/src/client/app/desktop/views/home/user/user.timeline.vue b/src/client/app/desktop/views/home/user/user.timeline.vue index 0571ce76f1..edbced8170 100644 --- a/src/client/app/desktop/views/home/user/user.timeline.vue +++ b/src/client/app/desktop/views/home/user/user.timeline.vue @@ -1,12 +1,12 @@ <template> -<div class="oh5y2r7l5lx8j6jj791ykeiwgihheguk"> - <header> - <span :data-active="mode == 'default'" @click="mode = 'default'"><fa :icon="['far', 'comment-alt']"/> {{ $t('default') }}</span> - <span :data-active="mode == 'with-replies'" @click="mode = 'with-replies'"><fa icon="comments"/> {{ $t('with-replies') }}</span> - <span :data-active="mode == 'with-media'" @click="mode = 'with-media'"><fa :icon="['far', 'images']"/> {{ $t('with-media') }}</span> - <span :data-active="mode == 'my-posts'" @click="mode = 'my-posts'"><fa icon="user"/> {{ $t('my-posts') }}</span> - </header> - <mk-notes ref="timeline" :more="existMore ? more : null"> +<div> + <mk-notes ref="timeline" :make-promise="makePromise" @inited="() => $emit('loaded')"> + <header slot="header" class="oh5y2r7l5lx8j6jj791ykeiwgihheguk"> + <span :data-active="mode == 'default'" @click="mode = 'default'"><fa :icon="['far', 'comment-alt']"/> {{ $t('default') }}</span> + <span :data-active="mode == 'with-replies'" @click="mode = 'with-replies'"><fa icon="comments"/> {{ $t('with-replies') }}</span> + <span :data-active="mode == 'with-media'" @click="mode = 'with-media'"><fa :icon="['far', 'images']"/> {{ $t('with-media') }}</span> + <span :data-active="mode == 'my-posts'" @click="mode = 'my-posts'"><fa icon="user"/> {{ $t('my-posts') }}</span> + </header> <p class="empty" slot="empty"><fa :icon="['far', 'comments']"/>{{ $t('empty') }}</p> </mk-notes> </div> @@ -20,29 +20,47 @@ const fetchLimit = 10; export default Vue.extend({ i18n: i18n('desktop/views/pages/user/user.timeline.vue'), + props: ['user'], data() { return { fetching: true, - moreFetching: false, - existMore: false, mode: 'default', unreadCount: 0, - date: null + date: null, + makePromise: cursor => this.$root.api('users/notes', { + userId: this.user.id, + limit: fetchLimit + 1, + includeReplies: this.mode == 'with-replies', + includeMyRenotes: this.mode != 'my-posts', + withFiles: this.mode == 'with-media', + untilId: cursor ? cursor : undefined + }).then(notes => { + if (notes.length == fetchLimit + 1) { + notes.pop(); + return { + notes: notes, + cursor: notes[notes.length - 1].id + }; + } else { + return { + notes: notes, + cursor: null + }; + } + }) }; }, watch: { mode() { - this.fetch(); + (this.$refs.timeline as any).reload(); } }, mounted() { document.addEventListener('keydown', this.onDocumentKeydown); - - this.fetch(() => this.$emit('loaded')); }, beforeDestroy() { @@ -58,58 +76,9 @@ export default Vue.extend({ } }, - fetch(cb?) { - this.fetching = true; - (this.$refs.timeline as any).init(() => new Promise((res, rej) => { - this.$root.api('users/notes', { - userId: this.user.id, - limit: fetchLimit + 1, - untilDate: this.date ? this.date.getTime() : new Date().getTime() + 1000 * 86400 * 365, - includeReplies: this.mode == 'with-replies', - includeMyRenotes: this.mode != 'my-posts', - withFiles: this.mode == 'with-media' - }).then(notes => { - if (notes.length == fetchLimit + 1) { - notes.pop(); - this.existMore = true; - } - res(notes); - this.fetching = false; - if (cb) cb(); - }, rej); - })); - }, - - more() { - this.moreFetching = true; - - const promise = this.$root.api('users/notes', { - userId: this.user.id, - limit: fetchLimit + 1, - includeReplies: this.mode == 'with-replies', - includeMyRenotes: this.mode != 'my-posts', - withFiles: this.mode == 'with-media', - untilDate: new Date((this.$refs.timeline as any).tail().createdAt).getTime() - }); - - promise.then(notes => { - if (notes.length == fetchLimit + 1) { - notes.pop(); - } else { - this.existMore = false; - } - for (const n of notes) { - (this.$refs.timeline as any).append(n); - } - this.moreFetching = false; - }); - - return promise; - }, - warp(date) { this.date = date; - this.fetch(); + (this.$refs.timeline as any).reload(); } } }); @@ -117,59 +86,38 @@ export default Vue.extend({ <style lang="stylus" scoped> .oh5y2r7l5lx8j6jj791ykeiwgihheguk - background var(--face) - border-radius var(--round) - overflow hidden + padding 0 8px + z-index 10 + background var(--faceHeader) + box-shadow 0 1px var(--desktopTimelineHeaderShadow) - > header - padding 0 8px - z-index 10 - background var(--faceHeader) - box-shadow 0 1px var(--desktopTimelineHeaderShadow) + > span + display inline-block + padding 0 10px + line-height 42px + font-size 12px + user-select none - > span - display inline-block - padding 0 10px - line-height 42px - font-size 12px - user-select none + &[data-active] + color var(--primary) + cursor default + font-weight bold - &[data-active] - color var(--primary) - cursor default - font-weight bold - - &:before - content "" - display block - position absolute - bottom 0 - left -8px - width calc(100% + 16px) - height 2px - background var(--primary) - - &:not([data-active]) - color var(--desktopTimelineSrc) - cursor pointer - - &:hover - color var(--desktopTimelineSrcHover) - - > .mk-notes - - > .empty - display block - margin 0 auto - padding 32px - max-width 400px - text-align center - color var(--text) - - > [data-icon] + &:before + content "" display block - margin-bottom 16px - font-size 3em - color var(--faceHeaderText); + position absolute + bottom 0 + left -8px + width calc(100% + 16px) + height 2px + background var(--primary) + + &:not([data-active]) + color var(--desktopTimelineSrc) + cursor pointer + + &:hover + color var(--desktopTimelineSrcHover) </style> diff --git a/src/client/app/mobile/style.styl b/src/client/app/mobile/style.styl index 095e5266fd..3a4fc9c0c6 100644 --- a/src/client/app/mobile/style.styl +++ b/src/client/app/mobile/style.styl @@ -9,3 +9,15 @@ html height 100% background var(--bg) + +main + width 100% + max-width 680px + margin 0 auto + padding 8px + + @media (min-width 500px) + padding 16px + + @media (min-width 600px) + padding 32px diff --git a/src/client/app/mobile/views/components/notes.vue b/src/client/app/mobile/views/components/notes.vue index 1d0375cfa9..9b4e7a3895 100644 --- a/src/client/app/mobile/views/components/notes.vue +++ b/src/client/app/mobile/views/components/notes.vue @@ -1,8 +1,8 @@ <template> -<div class="mk-notes"> - <slot name="head"></slot> +<div class="ivaojijs"> + <slot name="empty" v-if="notes.length == 0 && !fetching && inited"></slot> - <slot name="empty" v-if="notes.length == 0 && !fetching && requestInitPromise == null"></slot> + <mk-error v-if="!fetching && !inited" @retry="init()"/> <div class="placeholder" v-if="fetching"> <template v-for="i in 10"> @@ -10,8 +10,6 @@ </template> </div> - <mk-error v-if="!fetching && requestInitPromise != null" @retry="resolveInitPromise"/> - <!-- トランジションを有効にするとなぜかメモリリークする --> <component :is="!$store.state.device.reduceMotion ? 'transition-group' : 'div'" name="mk-notes" class="transition" tag="div"> <template v-for="(note, i) in _notes"> @@ -23,8 +21,8 @@ </template> </component> - <footer v-if="more"> - <button @click="loadMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }"> + <footer v-if="cursor != null"> + <button @click="more" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }"> <template v-if="!moreFetching">{{ $t('@.load-more') }}</template> <template v-if="moreFetching"><fa icon="spinner" pulse fixed-width/></template> </button> @@ -41,20 +39,21 @@ const displayLimit = 30; export default Vue.extend({ i18n: i18n(), + props: { - more: { - type: Function, - required: false + makePromise: { + required: true } }, data() { return { - requestInitPromise: null as () => Promise<any[]>, notes: [], queue: [], fetching: true, - moreFetching: false + moreFetching: false, + inited: false, + cursor: null }; }, @@ -80,6 +79,10 @@ export default Vue.extend({ } }, + created() { + this.init(); + }, + mounted() { window.addEventListener('scroll', this.onScroll, { passive: true }); }, @@ -97,27 +100,41 @@ export default Vue.extend({ Vue.set((this as any).notes, i, note); }, - init(promiseGenerator: () => Promise<any[]>) { - this.requestInitPromise = promiseGenerator; - this.resolveInitPromise(); - }, - - resolveInitPromise() { + reload() { this.queue = []; this.notes = []; + this.init(); + }, + + init() { this.fetching = true; - - const promise = this.requestInitPromise(); - - promise.then(notes => { - this.notes = notes; - this.requestInitPromise = null; + this.makePromise().then(x => { + if (Array.isArray(x)) { + this.notes = x; + } else { + this.notes = x.notes; + this.cursor = x.cursor; + } + this.inited = true; this.fetching = false; + this.$emit('inited'); }, e => { this.fetching = false; }); }, + more() { + if (this.cursor == null || this.moreFetching) return; + this.moreFetching = true; + this.makePromise(this.cursor).then(x => { + this.notes = this.notes.concat(x.notes); + this.cursor = x.cursor; + this.moreFetching = false; + }, e => { + this.moreFetching = false; + }); + }, + prepend(note, silent = false) { // 弾く if (shouldMuteNote(this.$store.state.i, this.$store.state.settings, note)) return; @@ -144,10 +161,6 @@ export default Vue.extend({ this.notes.push(note); }, - tail() { - return this.notes[this.notes.length - 1]; - }, - releaseQueue() { for (const n of this.queue) { this.prepend(n, true); @@ -155,15 +168,6 @@ export default Vue.extend({ this.queue = []; }, - async loadMore() { - if (this.more == null) return; - if (this.moreFetching) return; - - this.moreFetching = true; - await this.more(); - this.moreFetching = false; - }, - onScroll() { if (this.isScrollTop()) { this.releaseQueue(); @@ -176,7 +180,7 @@ export default Vue.extend({ if (this.$el.offsetHeight == 0) return; const current = window.scrollY + window.innerHeight; - if (current > document.body.offsetHeight - 8) this.loadMore(); + if (current > document.body.offsetHeight - 8) this.more(); } } } @@ -184,7 +188,7 @@ export default Vue.extend({ </script> <style lang="stylus" scoped> -.mk-notes +.ivaojijs overflow hidden background var(--face) border-radius 8px diff --git a/src/client/app/mobile/views/components/user-list-timeline.vue b/src/client/app/mobile/views/components/user-list-timeline.vue index d90051710b..e67d7931f7 100644 --- a/src/client/app/mobile/views/components/user-list-timeline.vue +++ b/src/client/app/mobile/views/components/user-list-timeline.vue @@ -1,6 +1,6 @@ <template> <div> - <mk-notes ref="timeline" :more="existMore ? more : null"/> + <mk-notes ref="timeline" :make-promise="makePromise" @inited="() => $emit('loaded')"/> </div> </template> @@ -14,19 +14,31 @@ export default Vue.extend({ data() { return { - fetching: true, - moreFetching: false, - existMore: false, - connection: null + connection: null, + makePromise: cursor => this.$root.api('notes/user-list-timeline', { + listId: this.list.id, + limit: fetchLimit + 1, + untilId: cursor ? cursor : undefined, + includeMyRenotes: this.$store.state.settings.showMyRenotes, + includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, + includeLocalRenotes: this.$store.state.settings.showLocalRenotes + }).then(notes => { + if (notes.length == fetchLimit + 1) { + notes.pop(); + return { + notes: notes, + cursor: notes[notes.length - 1].id + }; + } else { + return { + notes: notes, + cursor: null + }; + } + }) }; }, - computed: { - canFetchMore(): boolean { - return !this.moreFetching && !this.fetching && this.existMore; - } - }, - watch: { $route: 'init' }, @@ -48,59 +60,6 @@ export default Vue.extend({ this.connection.on('note', this.onNote); this.connection.on('userAdded', this.onUserAdded); this.connection.on('userRemoved', this.onUserRemoved); - - this.fetch(); - }, - - fetch() { - this.fetching = true; - - (this.$refs.timeline as any).init(() => new Promise((res, rej) => { - this.$root.api('notes/user-list-timeline', { - listId: this.list.id, - limit: fetchLimit + 1, - includeMyRenotes: this.$store.state.settings.showMyRenotes, - includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, - includeLocalRenotes: this.$store.state.settings.showLocalRenotes - }).then(notes => { - if (notes.length == fetchLimit + 1) { - notes.pop(); - this.existMore = true; - } - res(notes); - this.fetching = false; - this.$emit('loaded'); - }, rej); - })); - }, - - more() { - if (!this.canFetchMore) return; - - this.moreFetching = true; - - const promise = this.$root.api('notes/user-list-timeline', { - listId: this.list.id, - limit: fetchLimit + 1, - untilId: (this.$refs.timeline as any).tail().id, - includeMyRenotes: this.$store.state.settings.showMyRenotes, - includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, - includeLocalRenotes: this.$store.state.settings.showLocalRenotes - }); - - promise.then(notes => { - if (notes.length == fetchLimit + 1) { - notes.pop(); - } else { - this.existMore = false; - } - for (const n of notes) { - (this.$refs.timeline as any).append(n); - } - this.moreFetching = false; - }); - - return promise; }, onNote(note) { @@ -109,11 +68,11 @@ export default Vue.extend({ }, onUserAdded() { - this.fetch(); + (this.$refs.timeline as any).reload(); }, onUserRemoved() { - this.fetch(); + (this.$refs.timeline as any).reload(); } } }); diff --git a/src/client/app/mobile/views/components/user-timeline.vue b/src/client/app/mobile/views/components/user-timeline.vue index 0d0bbc4073..e85a0d177c 100644 --- a/src/client/app/mobile/views/components/user-timeline.vue +++ b/src/client/app/mobile/views/components/user-timeline.vue @@ -1,6 +1,6 @@ <template> <div class="mk-user-timeline"> - <mk-notes ref="timeline" :more="existMore ? more : null"> + <mk-notes ref="timeline" :make-promise="makePromise" @inited="() => $emit('loaded')"> <div slot="empty"> <fa :icon="['far', 'comments']"/> {{ withMedia ? this.$t('no-notes-with-media') : this.$t('no-notes') }} @@ -17,73 +17,31 @@ const fetchLimit = 10; export default Vue.extend({ i18n: i18n('mobile/views/components/user-timeline.vue'), + props: ['user', 'withMedia'], data() { return { - fetching: true, - existMore: false, - moreFetching: false - }; - }, - - computed: { - canFetchMore(): boolean { - return !this.moreFetching && !this.fetching && this.existMore; - } - }, - - mounted() { - this.fetch(); - }, - - methods: { - fetch() { - this.fetching = true; - (this.$refs.timeline as any).init(() => new Promise((res, rej) => { - this.$root.api('users/notes', { - userId: this.user.id, - withFiles: this.withMedia, - limit: fetchLimit + 1, - untilDate: new Date().getTime() + 1000 * 86400 * 365 - }).then(notes => { - if (notes.length == fetchLimit + 1) { - notes.pop(); - this.existMore = true; - } - res(notes); - this.fetching = false; - this.$emit('loaded'); - }, rej); - })); - }, - - more() { - if (!this.canFetchMore) return; - - this.moreFetching = true; - - const promise = this.$root.api('users/notes', { + makePromise: cursor => this.$root.api('users/notes', { userId: this.user.id, - withFiles: this.withMedia, limit: fetchLimit + 1, - untilDate: new Date((this.$refs.timeline as any).tail().createdAt).getTime() - }); - - promise.then(notes => { + withFiles: this.withMedia, + untilId: cursor ? cursor : undefined + }).then(notes => { if (notes.length == fetchLimit + 1) { notes.pop(); + return { + notes: notes, + cursor: notes[notes.length - 1].id + }; } else { - this.existMore = false; + return { + notes: notes, + cursor: null + }; } - for (const n of notes) { - (this.$refs.timeline as any).append(n); - } - this.moreFetching = false; - }); - - return promise; - } + }) + }; } }); </script> diff --git a/src/client/app/mobile/views/pages/explore.vue b/src/client/app/mobile/views/pages/explore.vue index 2955c9a50b..c861f2dfc4 100644 --- a/src/client/app/mobile/views/pages/explore.vue +++ b/src/client/app/mobile/views/pages/explore.vue @@ -26,18 +26,3 @@ export default Vue.extend({ }, }); </script> - -<style lang="stylus" scoped> -main - width 100% - max-width 680px - margin 0 auto - padding 8px - - @media (min-width 500px) - padding 16px - - @media (min-width 600px) - padding 32px - -</style> diff --git a/src/client/app/mobile/views/pages/favorites.vue b/src/client/app/mobile/views/pages/favorites.vue index 61dd1526ba..9fcaf566e3 100644 --- a/src/client/app/mobile/views/pages/favorites.vue +++ b/src/client/app/mobile/views/pages/favorites.vue @@ -76,21 +76,11 @@ export default Vue.extend({ <style lang="stylus" scoped> main - width 100% - max-width 680px - margin 0 auto - padding 8px - > * > .post margin-bottom 8px @media (min-width 500px) - padding 16px - > * > .post margin-bottom 16px - @media (min-width 600px) - padding 32px - </style> diff --git a/src/client/app/mobile/views/pages/featured.vue b/src/client/app/mobile/views/pages/featured.vue index 9122673be1..cab7b7243e 100644 --- a/src/client/app/mobile/views/pages/featured.vue +++ b/src/client/app/mobile/views/pages/featured.vue @@ -51,21 +51,11 @@ export default Vue.extend({ <style lang="stylus" scoped> main - width 100% - max-width 680px - margin 0 auto - padding 8px - > * > .post margin-bottom 8px @media (min-width 500px) - padding 16px - > * > .post margin-bottom 16px - @media (min-width 600px) - padding 32px - </style> diff --git a/src/client/app/mobile/views/pages/home.timeline.vue b/src/client/app/mobile/views/pages/home.timeline.vue index b768a9ccc8..2fa155892e 100644 --- a/src/client/app/mobile/views/pages/home.timeline.vue +++ b/src/client/app/mobile/views/pages/home.timeline.vue @@ -7,7 +7,7 @@ </div> </ui-container> - <mk-notes ref="timeline" :more="existMore ? more : null"> + <mk-notes ref="timeline" :make-promise="makePromise" @inited="() => $emit('loaded')"> <div slot="empty"> <fa :icon="['far', 'comments']"/>{{ $t('empty') }} </div> @@ -36,9 +36,6 @@ export default Vue.extend({ data() { return { - fetching: true, - moreFetching: false, - existMore: false, streamManager: null, connection: null, unreadCount: 0, @@ -49,21 +46,18 @@ export default Vue.extend({ includeLocalRenotes: this.$store.state.settings.showLocalRenotes }, query: {}, - endpoint: null + endpoint: null, + makePromise: null }; }, computed: { alone(): boolean { return this.$store.state.i.followingCount == 0; - }, - - canFetchMore(): boolean { - return !this.moreFetching && !this.fetching && this.existMore; } }, - mounted() { + created() { const prepend = note => { (this.$refs.timeline as any).prepend(note); }; @@ -114,7 +108,25 @@ export default Vue.extend({ this.connection.on('mention', onNote); } - this.fetch(); + this.makePromise = cursor => this.$root.api(this.endpoint, { + limit: fetchLimit + 1, + untilDate: cursor ? undefined : (this.date ? this.date.getTime() : undefined), + untilId: cursor ? cursor : undefined, + ...this.baseQuery, ...this.query + }).then(notes => { + if (notes.length == fetchLimit + 1) { + notes.pop(); + return { + notes: notes, + cursor: notes[notes.length - 1].id + }; + } else { + return { + notes: notes, + cursor: null + }; + } + }); }, beforeDestroy() { @@ -122,57 +134,13 @@ export default Vue.extend({ }, methods: { - fetch() { - this.fetching = true; - - (this.$refs.timeline as any).init(() => new Promise((res, rej) => { - this.$root.api(this.endpoint, Object.assign({ - limit: fetchLimit + 1, - untilDate: this.date ? this.date.getTime() : undefined - }, this.baseQuery, this.query)).then(notes => { - if (notes.length == fetchLimit + 1) { - notes.pop(); - this.existMore = true; - } - res(notes); - this.fetching = false; - this.$emit('loaded'); - }, rej); - })); - }, - - more() { - if (!this.canFetchMore) return; - - this.moreFetching = true; - - const promise = this.$root.api(this.endpoint, Object.assign({ - limit: fetchLimit + 1, - untilId: (this.$refs.timeline as any).tail().id - }, this.baseQuery, this.query)); - - promise.then(notes => { - if (notes.length == fetchLimit + 1) { - notes.pop(); - } else { - this.existMore = false; - } - for (const n of notes) { - (this.$refs.timeline as any).append(n); - } - this.moreFetching = false; - }); - - return promise; - }, - focus() { (this.$refs.timeline as any).focus(); }, warp(date) { this.date = date; - this.fetch(); + (this.$refs.timeline as any).reload(); } } }); diff --git a/src/client/app/mobile/views/pages/home.vue b/src/client/app/mobile/views/pages/home.vue index a663c1da99..7f6a1b8238 100644 --- a/src/client/app/mobile/views/pages/home.vue +++ b/src/client/app/mobile/views/pages/home.vue @@ -233,17 +233,6 @@ main font-size 10px color var(--notificationIndicator) - > .tl - max-width 680px - margin 0 auto - padding 8px - - @media (min-width 500px) - padding 16px - - @media (min-width 600px) - padding 32px - </style> <style lang="stylus" module> diff --git a/src/client/app/mobile/views/pages/note.vue b/src/client/app/mobile/views/pages/note.vue index 79757ea374..f22601a3f7 100644 --- a/src/client/app/mobile/views/pages/note.vue +++ b/src/client/app/mobile/views/pages/note.vue @@ -56,18 +56,6 @@ export default Vue.extend({ <style lang="stylus" scoped> main text-align center - padding 8px - - @media (min-width 500px) - padding 16px - - @media (min-width 600px) - padding 32px - - > div - margin 0 auto - padding 0 - max-width 600px > footer margin-top 16px diff --git a/src/client/app/mobile/views/pages/notifications.vue b/src/client/app/mobile/views/pages/notifications.vue index c6e5b646f2..fd84a21c15 100644 --- a/src/client/app/mobile/views/pages/notifications.vue +++ b/src/client/app/mobile/views/pages/notifications.vue @@ -39,18 +39,3 @@ export default Vue.extend({ } }); </script> - -<style lang="stylus" scoped> -main - width 100% - max-width 680px - margin 0 auto - padding 8px - - @media (min-width 500px) - padding 16px - - @media (min-width 600px) - padding 32px - -</style> diff --git a/src/client/app/mobile/views/pages/received-follow-requests.vue b/src/client/app/mobile/views/pages/received-follow-requests.vue index 1b8323e834..df0cf109cd 100644 --- a/src/client/app/mobile/views/pages/received-follow-requests.vue +++ b/src/client/app/mobile/views/pages/received-follow-requests.vue @@ -57,17 +57,6 @@ export default Vue.extend({ <style lang="stylus" scoped> main - width 100% - max-width 680px - margin 0 auto - padding 8px - - @media (min-width 500px) - padding 16px - - @media (min-width 600px) - padding 32px - > div display flex padding 16px diff --git a/src/client/app/mobile/views/pages/search.vue b/src/client/app/mobile/views/pages/search.vue index 669e0b740b..0bfc1c0384 100644 --- a/src/client/app/mobile/views/pages/search.vue +++ b/src/client/app/mobile/views/pages/search.vue @@ -3,8 +3,7 @@ <span slot="header"><fa icon="search"/> {{ q }}</span> <main> - <p :class="$style.empty" v-if="!fetching && empty"><fa icon="search"/> {{ $t('not-found', { q }) }}</p> - <mk-notes ref="timeline" :more="existMore ? more : null"/> + <mk-notes ref="timeline" :make-promise="makePromise" @inited="inited"/> </main> </mk-ui> </template> @@ -20,15 +19,30 @@ export default Vue.extend({ i18n: i18n('mobile/views/pages/search.vue'), data() { return { - fetching: true, - moreFetching: false, - existMore: false, - empty: false, - offset: 0 + makePromise: cursor => this.$root.api('notes/search', { + limit: limit + 1, + offset: cursor ? cursor : undefined, + query: this.q + }).then(notes => { + if (notes.length == limit + 1) { + notes.pop(); + return { + notes: notes, + cursor: cursor ? cursor + limit : limit + }; + } else { + return { + notes: notes, + cursor: null + }; + } + }) }; }, watch: { - $route: 'fetch' + $route() { + this.$refs.timeline.reload(); + } }, computed: { q(): string { @@ -37,68 +51,11 @@ export default Vue.extend({ }, mounted() { document.title = `%i18n:@search%: ${this.q} | ${this.$root.instanceName}`; - - this.fetch(); }, methods: { - fetch() { - this.fetching = true; - Progress.start(); - - (this.$refs.timeline as any).init(() => new Promise((res, rej) => { - this.$root.api('notes/search', { - limit: limit + 1, - offset: this.offset, - query: this.q - }).then(notes => { - if (notes.length == 0) this.empty = true; - if (notes.length == limit + 1) { - notes.pop(); - this.existMore = true; - } - res(notes); - this.fetching = false; - Progress.done(); - }, rej); - })); + inited() { + Progress.done(); }, - more() { - this.offset += limit; - - const promise = this.$root.api('notes/search', { - limit: limit + 1, - offset: this.offset, - query: this.q - }); - - promise.then(notes => { - if (notes.length == limit + 1) { - notes.pop(); - } else { - this.existMore = false; - } - for (const n of notes) { - (this.$refs.timeline as any).append(n); - } - this.moreFetching = false; - }); - - return promise; - } } }); </script> - -<style lang="stylus" module> -.notes - margin 8px auto - max-width 500px - width calc(100% - 16px) - background #fff - border-radius 8px - box-shadow 0 0 0 1px rgba(#000, 0.2) - - @media (min-width 500px) - margin 16px auto - width calc(100% - 32px) -</style> diff --git a/src/client/app/mobile/views/pages/settings.vue b/src/client/app/mobile/views/pages/settings.vue index f26b9af6f4..17f0c2f146 100644 --- a/src/client/app/mobile/views/pages/settings.vue +++ b/src/client/app/mobile/views/pages/settings.vue @@ -383,9 +383,6 @@ export default Vue.extend({ <style lang="stylus" scoped> main - margin 0 auto - max-width 600px - width 100% > .signed-in-as margin 16px diff --git a/src/client/app/mobile/views/pages/tag.vue b/src/client/app/mobile/views/pages/tag.vue index ecd523dab2..53129ed20b 100644 --- a/src/client/app/mobile/views/pages/tag.vue +++ b/src/client/app/mobile/views/pages/tag.vue @@ -3,8 +3,7 @@ <span slot="header"><span style="margin-right:4px;"><fa icon="hashtag"/></span>{{ $route.params.tag }}</span> <main> - <p v-if="!fetching && empty"><fa icon="search"/> {{ $t('no-posts-found', { q: $route.params.tag }) }}</p> - <mk-notes ref="timeline" :more="existMore ? more : null"/> + <mk-notes ref="timeline" :make-promise="makePromise" @inited="inited"/> </main> </mk-ui> </template> @@ -20,66 +19,35 @@ export default Vue.extend({ i18n: i18n('mobile/views/pages/tag.vue'), data() { return { - fetching: true, - moreFetching: false, - existMore: false, - offset: 0, - empty: false + makePromise: cursor => this.$root.api('notes/search_by_tag', { + limit: limit + 1, + offset: cursor ? cursor : undefined, + tag: this.$route.params.tag + }).then(notes => { + if (notes.length == limit + 1) { + notes.pop(); + return { + notes: notes, + cursor: cursor ? cursor + limit : limit + }; + } else { + return { + notes: notes, + cursor: null + }; + } + }) }; }, watch: { - $route: 'fetch' - }, - mounted() { - this.$nextTick(() => { - this.fetch(); - }); + $route() { + this.$refs.timeline.reload(); + } }, methods: { - fetch() { - this.fetching = true; - Progress.start(); - - (this.$refs.timeline as any).init(() => new Promise((res, rej) => { - this.$root.api('notes/search_by_tag', { - limit: limit + 1, - offset: this.offset, - tag: this.$route.params.tag - }).then(notes => { - if (notes.length == 0) this.empty = true; - if (notes.length == limit + 1) { - notes.pop(); - this.existMore = true; - } - res(notes); - this.fetching = false; - Progress.done(); - }, rej); - })); + inited() { + Progress.done(); }, - more() { - this.offset += limit; - - const promise = this.$root.api('notes/search_by_tag', { - limit: limit + 1, - offset: this.offset, - tag: this.$route.params.tag - }); - - promise.then(notes => { - if (notes.length == limit + 1) { - notes.pop(); - } else { - this.existMore = false; - } - for (const n of notes) { - (this.$refs.timeline as any).append(n); - } - this.moreFetching = false; - }); - - return promise; - } } }); </script> diff --git a/src/client/app/mobile/views/pages/user-list.vue b/src/client/app/mobile/views/pages/user-list.vue index cf2dd134fd..ad6e314767 100644 --- a/src/client/app/mobile/views/pages/user-list.vue +++ b/src/client/app/mobile/views/pages/user-list.vue @@ -46,18 +46,3 @@ export default Vue.extend({ } }); </script> - -<style lang="stylus" scoped> -main - width 100% - max-width 680px - margin 0 auto - padding 8px - - @media (min-width 500px) - padding 16px - - @media (min-width 600px) - padding 32px - -</style> diff --git a/src/client/app/mobile/views/pages/user-lists.vue b/src/client/app/mobile/views/pages/user-lists.vue index dc9d47de3c..530357576f 100644 --- a/src/client/app/mobile/views/pages/user-lists.vue +++ b/src/client/app/mobile/views/pages/user-lists.vue @@ -53,20 +53,3 @@ export default Vue.extend({ } }); </script> - -<style lang="stylus" scoped> - - -main - width 100% - max-width 680px - margin 0 auto - padding 8px - - @media (min-width 500px) - padding 16px - - @media (min-width 600px) - padding 32px - -</style> diff --git a/src/client/app/mobile/views/pages/user/home.vue b/src/client/app/mobile/views/pages/user/home.vue index 98b4f44476..f9b7e7f90a 100644 --- a/src/client/app/mobile/views/pages/user/home.vue +++ b/src/client/app/mobile/views/pages/user/home.vue @@ -57,7 +57,6 @@ export default Vue.extend({ <style lang="stylus" scoped> .root.home - max-width 600px margin 0 auto > .mk-note-detail diff --git a/src/client/app/mobile/views/pages/user/index.vue b/src/client/app/mobile/views/pages/user/index.vue index d7fb3d4d58..a2360e7417 100644 --- a/src/client/app/mobile/views/pages/user/index.vue +++ b/src/client/app/mobile/views/pages/user/index.vue @@ -3,7 +3,7 @@ <template slot="header" v-if="!fetching"><img :src="avator" alt=""> <mk-user-name :user="user"/> </template> - <main v-if="!fetching"> + <div class="wwtwuxyh" v-if="!fetching"> <div class="is-suspended" v-if="user.isSuspended"><p><fa icon="exclamation-triangle"/> {{ $t('@.user-suspended') }}</p></div> <div class="is-remote" v-if="user.host != null"><p><fa icon="exclamation-triangle"/> {{ $t('@.is-remote-user') }}<a :href="user.url || user.uri" target="_blank">{{ $t('@.view-on-remote') }}</a></p></div> <header> @@ -65,15 +65,15 @@ <a :data-active="page == 'media'" @click="page = 'media'"><fa icon="image"/> {{ $t('media') }}</a> </div> </nav> - <div class="body"> + <main> <template v-if="$route.name == 'user'"> <x-home v-if="page == 'home'" :user="user"/> <mk-user-timeline v-if="page == 'notes'" :user="user" key="tl"/> <mk-user-timeline v-if="page == 'media'" :user="user" :with-media="true" key="media"/> </template> <router-view :user="user"></router-view> - </div> - </main> + </main> + </div> </mk-ui> </template> @@ -146,7 +146,7 @@ export default Vue.extend({ </script> <style lang="stylus" scoped> -main +.wwtwuxyh $bg = var(--face) > .is-suspended @@ -314,7 +314,7 @@ main display flex justify-content center margin 0 auto - max-width 600px + max-width 616px > a display block @@ -335,16 +335,4 @@ main color var(--primary) border-color var(--primary) - > .body - max-width 680px - margin 0 auto - padding 8px - color var(--text) - - @media (min-width 500px) - padding 16px - - @media (min-width 600px) - padding 32px - </style>