diff options
| author | syuilo <syuilotan@yahoo.co.jp> | 2019-05-21 08:57:10 +0900 |
|---|---|---|
| committer | syuilo <syuilotan@yahoo.co.jp> | 2019-05-21 08:57:10 +0900 |
| commit | c26168f22e73e4a595cf9dedd7ba0d3478f48752 (patch) | |
| tree | 60ba7060b7eaa7c6c37a53057927ea40b89e3f4a /src | |
| parent | Merge branch 'develop' (diff) | |
| parent | 11.18.0 (diff) | |
| download | misskey-c26168f22e73e4a595cf9dedd7ba0d3478f48752.tar.gz misskey-c26168f22e73e4a595cf9dedd7ba0d3478f48752.tar.bz2 misskey-c26168f22e73e4a595cf9dedd7ba0d3478f48752.zip | |
Merge branch 'develop'
Diffstat (limited to 'src')
78 files changed, 1114 insertions, 1835 deletions
diff --git a/src/client/app/common/scripts/note-subscriber.ts b/src/client/app/common/scripts/note-subscriber.ts index d881fe01ce..5b31a9f9d0 100644 --- a/src/client/app/common/scripts/note-subscriber.ts +++ b/src/client/app/common/scripts/note-subscriber.ts @@ -144,8 +144,6 @@ export default prop => ({ break; } } - - this.$emit(`update:${prop}`, this.$_ns_note_); }, } }); diff --git a/src/client/app/common/scripts/paging.ts b/src/client/app/common/scripts/paging.ts new file mode 100644 index 0000000000..c13a607e6f --- /dev/null +++ b/src/client/app/common/scripts/paging.ts @@ -0,0 +1,171 @@ +import Vue from 'vue'; + +export default (opts) => ({ + data() { + return { + items: [], + queue: [], + fetching: true, + moreFetching: false, + inited: false, + more: false + }; + }, + + computed: { + empty(): boolean { + return this.items.length == 0 && !this.fetching && this.inited; + }, + + error(): boolean { + return !this.fetching && !this.inited; + } + }, + + watch: { + queue(x) { + if (opts.onQueueChanged) opts.onQueueChanged(this, x); + }, + + pagination() { + this.init(); + } + }, + + created() { + opts.displayLimit = opts.displayLimit || 30; + this.init(); + }, + + mounted() { + if (opts.captureWindowScroll) { + this.isScrollTop = () => { + return window.scrollY <= 8; + }; + + window.addEventListener('scroll', this.onWindowScroll, { passive: true }); + } + }, + + beforeDestroy() { + if (opts.captureWindowScroll) { + window.removeEventListener('scroll', this.onWindowScroll); + } + }, + + methods: { + updateItem(i, item) { + Vue.set((this as any).items, i, item); + }, + + reload() { + this.queue = []; + this.items = []; + this.init(); + }, + + async init() { + this.fetching = true; + let params = typeof this.pagination.params === 'function' ? this.pagination.params(true) : this.pagination.params; + if (params && params.then) params = await params; + await this.$root.api(this.pagination.endpoint, { + limit: (this.pagination.limit || 10) + 1, + ...params + }).then(x => { + if (x.length == (this.pagination.limit || 10) + 1) { + x.pop(); + this.items = x; + this.more = true; + } else { + this.items = x; + this.more = false; + } + this.inited = true; + this.fetching = false; + if (opts.onInited) opts.onInited(this); + }, e => { + this.fetching = false; + if (opts.onInited) opts.onInited(this); + }); + }, + + async fetchMore() { + if (!this.more || this.moreFetching || this.items.length === 0) return; + this.moreFetching = true; + let params = typeof this.pagination.params === 'function' ? this.pagination.params(false) : this.pagination.params; + if (params && params.then) params = await params; + await this.$root.api(this.pagination.endpoint, { + limit: (this.pagination.limit || 10) + 1, + untilId: this.items[this.items.length - 1].id, + ...params + }).then(x => { + if (x.length == (this.pagination.limit || 10) + 1) { + x.pop(); + this.items = this.items.concat(x); + this.more = true; + } else { + this.items = this.items.concat(x); + this.more = false; + } + this.moreFetching = false; + }, e => { + this.moreFetching = false; + }); + }, + + prepend(item, silent = false) { + if (opts.onPrepend) { + const cancel = opts.onPrepend(this, item, silent); + if (cancel) return; + } + + if (this.isScrollTop == null || this.isScrollTop()) { + // Prepend the item + this.items.unshift(item); + + // オーバーフローしたら古い投稿は捨てる + if (this.items.length >= opts.displayLimit) { + this.items = this.items.slice(0, opts.displayLimit); + this.more = true; + } + } else { + this.queue.push(item); + } + }, + + append(item) { + this.items.push(item); + }, + + releaseQueue() { + for (const n of this.queue) { + this.prepend(n, true); + } + this.queue = []; + }, + + onWindowScroll() { + if (this.isScrollTop()) { + this.onTop(); + } + + if (this.$store.state.settings.fetchOnScroll) { + // 親要素が display none だったら弾く + // https://github.com/syuilo/misskey/issues/1569 + // http://d.hatena.ne.jp/favril/20091105/1257403319 + if (this.$el.offsetHeight == 0) return; + + const current = window.scrollY + window.innerHeight; + if (current > document.body.offsetHeight - 8) this.onBottom(); + } + }, + + onTop() { + this.releaseQueue(); + }, + + onBottom() { + this.fetchMore(); + } + } +}); diff --git a/src/client/app/common/views/components/avatars.vue b/src/client/app/common/views/components/avatars.vue new file mode 100644 index 0000000000..0dc1ece3bf --- /dev/null +++ b/src/client/app/common/views/components/avatars.vue @@ -0,0 +1,27 @@ +<template> +<div> + <mk-avatar v-for="user in us" :user="user" :key="user.id" style="width:32px;height:32px;"/> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; + +export default Vue.extend({ + props: { + userIds: { + required: true + }, + }, + data() { + return { + us: [] + }; + }, + async created() { + this.us = await this.$root.api('users/show', { + userIds: this.userIds + }); + } +}); +</script> diff --git a/src/client/app/common/views/components/forkit.vue b/src/client/app/common/views/components/forkit.vue index d652b846a4..328e3ca7b0 100644 --- a/src/client/app/common/views/components/forkit.vue +++ b/src/client/app/common/views/components/forkit.vue @@ -19,7 +19,6 @@ export default Vue.extend({ }); </script> - <style lang="stylus" scoped> .a display block diff --git a/src/client/app/common/views/components/index.ts b/src/client/app/common/views/components/index.ts index 174fa36c00..d5392fb8cd 100644 --- a/src/client/app/common/views/components/index.ts +++ b/src/client/app/common/views/components/index.ts @@ -21,7 +21,6 @@ import avatar from './avatar.vue'; import nav from './nav.vue'; import misskeyFlavoredMarkdown from './misskey-flavored-markdown.vue'; import poll from './poll.vue'; -import pollEditor from './poll-editor.vue'; import reactionIcon from './reaction-icon.vue'; import reactionsViewer from './reactions-viewer.vue'; import time from './time.vue'; @@ -46,6 +45,7 @@ import uiSelect from './ui/select.vue'; import uiInfo from './ui/info.vue'; import uiMargin from './ui/margin.vue'; import uiHr from './ui/hr.vue'; +import uiPagination from './ui/pagination.vue'; import formButton from './ui/form/button.vue'; import formRadio from './ui/form/radio.vue'; @@ -70,7 +70,6 @@ Vue.component('mk-acct', acct); Vue.component('mk-avatar', avatar); Vue.component('mk-nav', nav); Vue.component('mk-poll', poll); -Vue.component('mk-poll-editor', pollEditor); Vue.component('mk-reaction-icon', reactionIcon); Vue.component('mk-reactions-viewer', reactionsViewer); Vue.component('mk-time', time); @@ -95,5 +94,6 @@ Vue.component('ui-select', uiSelect); Vue.component('ui-info', uiInfo); Vue.component('ui-margin', uiMargin); Vue.component('ui-hr', uiHr); +Vue.component('ui-pagination', uiPagination); Vue.component('form-button', formButton); Vue.component('form-radio', formRadio); diff --git a/src/client/app/common/views/components/messaging.vue b/src/client/app/common/views/components/messaging.vue index c7c70a4ce5..cdd35ee8ab 100644 --- a/src/client/app/common/views/components/messaging.vue +++ b/src/client/app/common/views/components/messaging.vue @@ -21,42 +21,23 @@ </div> </div> <div class="history" v-if="messages.length > 0"> - <div class="title">{{ $t('user') }}</div> <a v-for="message in messages" class="user" - :href="`/i/messaging/${getAcct(isMe(message) ? message.recipient : message.user)}`" + :href="message.groupId ? `/i/messaging/group/${message.groupId}` : `/i/messaging/${getAcct(isMe(message) ? message.recipient : message.user)}`" :data-is-me="isMe(message)" - :data-is-read="message.isRead" - @click.prevent="navigate(isMe(message) ? message.recipient : message.user)" + :data-is-read="message.groupId ? message.reads.includes($store.state.i.id) : message.isRead" + @click.prevent="message.groupId ? navigateGroup(message.group) : navigate(isMe(message) ? message.recipient : message.user)" :key="message.id" > <div> - <mk-avatar class="avatar" :user="isMe(message) ? message.recipient : message.user"/> - <header> - <span class="name"><mk-user-name :user="isMe(message) ? message.recipient : message.user"/></span> - <span class="username">@{{ isMe(message) ? message.recipient : message.user | acct }}</span> + <mk-avatar class="avatar" :user="message.groupId ? message.user : isMe(message) ? message.recipient : message.user"/> + <header v-if="message.groupId"> + <span class="name">{{ message.group.name }}</span> <mk-time :time="message.createdAt"/> </header> - <div class="body"> - <p class="text"><span class="me" v-if="isMe(message)">{{ $t('you') }}:</span>{{ message.text }}</p> - </div> - </div> - </a> - </div> - <div class="history" v-if="groupMessages.length > 0"> - <div class="title">{{ $t('group') }}</div> - <a v-for="message in groupMessages" - class="user" - :href="`/i/messaging/group/${message.groupId}`" - :data-is-me="isMe(message)" - :data-is-read="message.reads.includes($store.state.i.id)" - @click.prevent="navigateGroup(message.group)" - :key="message.id" - > - <div> - <mk-avatar class="avatar" :user="message.user"/> - <header> - <span class="name">{{ message.group.name }}</span> + <header v-else> + <span class="name"><mk-user-name :user="isMe(message) ? message.recipient : message.user"/></span> + <span class="username">@{{ isMe(message) ? message.recipient : message.user | acct }}</span> <mk-time :time="message.createdAt"/> </header> <div class="body"> @@ -97,7 +78,6 @@ export default Vue.extend({ fetching: true, moreFetching: false, messages: [], - groupMessages: [], q: null, result: [], connection: null, @@ -110,10 +90,11 @@ export default Vue.extend({ this.connection.on('message', this.onMessage); this.connection.on('read', this.onRead); - this.$root.api('messaging/history', { group: false }).then(messages => { + this.$root.api('messaging/history', { group: false }).then(userMessages => { this.$root.api('messaging/history', { group: true }).then(groupMessages => { + const messages = userMessages.concat(groupMessages); + messages.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); this.messages = messages; - this.groupMessages = groupMessages; this.fetching = false; }); }); @@ -134,8 +115,8 @@ export default Vue.extend({ this.messages.unshift(message); } else if (message.groupId) { - this.groupMessages = this.groupMessages.filter(m => m.groupId !== message.groupId); - this.groupMessages.unshift(message); + this.messages = this.messages.filter(m => m.groupId !== message.groupId); + this.messages.unshift(message); } }, onRead(ids) { @@ -243,9 +224,6 @@ export default Vue.extend({ font-size 0.8em > .history - > .title - padding 8px - > a &:last-child border-bottom none @@ -384,14 +362,6 @@ export default Vue.extend({ color rgba(#000, 0.3) > .history - > .title - padding 6px 16px - margin 0 auto - max-width 500px - background rgba(0, 0, 0, 0.05) - color var(--text) - font-size 85% - > a display block text-decoration none diff --git a/src/client/app/common/views/components/nav.vue b/src/client/app/common/views/components/nav.vue index da26fd1b8e..41b65604de 100644 --- a/src/client/app/common/views/components/nav.vue +++ b/src/client/app/common/views/components/nav.vue @@ -29,7 +29,7 @@ export default Vue.extend({ ToSUrl: null } }, - + mounted() { this.$root.getMeta(true).then(meta => { this.repositoryUrl = meta.repositoryUrl; diff --git a/src/client/app/common/views/components/poll-editor.vue b/src/client/app/common/views/components/poll-editor.vue index 88d7311f5c..ed1d02aa2c 100644 --- a/src/client/app/common/views/components/poll-editor.vue +++ b/src/client/app/common/views/components/poll-editor.vue @@ -1,5 +1,5 @@ <template> -<div class="mk-poll-editor"> +<div class="zmdxowus"> <p class="caution" v-if="choices.length < 2"> <fa icon="exclamation-triangle"/>{{ $t('no-only-one-choice') }} </p> @@ -134,7 +134,7 @@ export default Vue.extend({ </script> <style lang="stylus" scoped> -.mk-poll-editor +.zmdxowus padding 8px > .caution diff --git a/src/client/app/common/views/components/ui/pagination.vue b/src/client/app/common/views/components/ui/pagination.vue new file mode 100644 index 0000000000..67aa89d369 --- /dev/null +++ b/src/client/app/common/views/components/ui/pagination.vue @@ -0,0 +1,36 @@ +<template> +<div class="mwermpua" v-if="!fetching"> + <sequential-entrance animation="entranceFromTop" delay="25"> + <slot :items="items"></slot> + </sequential-entrance> + <div class="more" v-if="more"> + <ui-button @click="fetchMore()">{{ $t('@.load-more') }}</ui-button> + </div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import paging from '../../../scripts/paging'; + +export default Vue.extend({ + mixins: [ + paging({ + captureWindowScroll: false, + }), + ], + + props: { + pagination: { + required: true + }, + }, +}); +</script> + +<style lang="stylus" scoped> +.mwermpua + > .more + margin-top 16px + +</style> diff --git a/src/client/app/common/views/components/user-list.vue b/src/client/app/common/views/components/user-list.vue index 6761886b09..d6cf750016 100644 --- a/src/client/app/common/views/components/user-list.vue +++ b/src/client/app/common/views/components/user-list.vue @@ -2,13 +2,13 @@ <ui-container :body-togglable="true"> <template #header><slot></slot></template> - <mk-error v-if="!fetching && !inited" @retry="init()"/> + <mk-error v-if="error" @retry="init()"/> <div class="efvhhmdq" :class="{ iconOnly }" v-size="[{ lt: 500, class: 'narrow' }]"> - <div class="no-users" v-if="inited && us.length == 0"> + <div class="no-users" v-if="empty"> <p>{{ $t('no-users') }}</p> </div> - <div class="user" v-for="user in us" :key="user.id"> + <div class="user" v-for="user in users" :key="user.id"> <mk-avatar class="avatar" :user="user"/> <div class="body" v-if="!iconOnly"> <div class="name"> @@ -21,8 +21,8 @@ <mk-follow-button class="follow-button" v-if="$store.getters.isSignedIn && user.id != $store.state.i.id" :user="user" mini/> </div> </div> - <button class="more" :class="{ fetching: fetchingMoreUsers }" v-if="cursor != null" @click="fetchMoreUsers()" :disabled="fetchingMoreUsers"> - <template v-if="fetchingMoreUsers"><fa icon="spinner" pulse fixed-width/></template>{{ fetchingMoreUsers ? $t('@.loading') : $t('@.load-more') }} + <button class="more" :class="{ fetching: moreFetching }" v-if="more" @click="fetchMoreUsers()" :disabled="moreFetching"> + <template v-if="moreFetching"><fa icon="spinner" pulse fixed-width/></template>{{ moreFetching ? $t('@.loading') : $t('@.load-more') }} </button> </div> </ui-container> @@ -31,60 +31,31 @@ <script lang="ts"> import Vue from 'vue'; import i18n from '../../../i18n'; +import paging from '../../../common/scripts/paging'; export default Vue.extend({ i18n: i18n('common/views/components/user-list.vue'), + mixins: [ + paging({}), + ], + props: { - makePromise: { + pagination: { required: true }, + extract: { + required: false + }, iconOnly: { type: Boolean, default: false } }, - data() { - return { - fetching: true, - fetchingMoreUsers: false, - us: [], - inited: false, - more: false - }; - }, - - created() { - this.init(); - }, - - methods: { - async init() { - this.fetching = true; - await (this.makePromise()).then(x => { - if (Array.isArray(x)) { - this.us = x; - } else { - this.us = x.users; - this.cursor = x.cursor; - } - this.inited = true; - this.fetching = false; - }, e => { - this.fetching = false; - }); - }, - - async fetchMoreUsers() { - this.fetchingMoreUsers = true; - await (this.makePromise(this.cursor)).then(x => { - this.us = this.us.concat(x.users); - this.cursor = x.cursor; - this.fetchingMoreUsers = false; - }, e => { - this.fetchingMoreUsers = false; - }); + computed: { + users() { + return this.extract ? this.extract(this.items) : this.items; } } }); diff --git a/src/client/app/common/views/deck/deck.direct-column.vue b/src/client/app/common/views/deck/deck.direct-column.vue index c68a361a9f..66d34520af 100644 --- a/src/client/app/common/views/deck/deck.direct-column.vue +++ b/src/client/app/common/views/deck/deck.direct-column.vue @@ -2,7 +2,7 @@ <x-column :name="name" :column="column" :is-stacked="isStacked"> <template #header><fa :icon="['far', 'envelope']"/>{{ name }}</template> - <x-direct/> + <x-notes ref="timeline" :pagination="pagination" @inited="() => $emit('loaded')"/> </x-column> </template> @@ -10,13 +10,14 @@ import Vue from 'vue'; import i18n from '../../../i18n'; import XColumn from './deck.column.vue'; -import XDirect from './deck.direct.vue'; +import XNotes from './deck.notes.vue'; export default Vue.extend({ i18n: i18n(), + components: { XColumn, - XDirect + XNotes }, props: { @@ -30,6 +31,22 @@ export default Vue.extend({ } }, + data() { + return { + connection: null, + pagination: { + endpoint: 'notes/mentions', + limit: 10, + params: { + includeMyRenotes: this.$store.state.settings.showMyRenotes, + includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, + includeLocalRenotes: this.$store.state.settings.showLocalRenotes, + visibility: 'specified' + } + } + }; + }, + computed: { name(): string { if (this.column.name) return this.column.name; @@ -37,9 +54,25 @@ export default Vue.extend({ } }, + mounted() { + this.connection = this.$root.stream.useSharedConnection('main'); + this.connection.on('mention', this.onNote); + }, + + beforeDestroy() { + this.connection.dispose(); + }, + methods: { + onNote(note) { + // Prepend a note + if (note.visibility == 'specified') { + (this.$refs.timeline as any).prepend(note); + } + }, + focus() { - this.$refs.tl.focus(); + this.$refs.timeline.focus(); } } }); diff --git a/src/client/app/common/views/deck/deck.direct.vue b/src/client/app/common/views/deck/deck.direct.vue deleted file mode 100644 index 29db5cb7f3..0000000000 --- a/src/client/app/common/views/deck/deck.direct.vue +++ /dev/null @@ -1,65 +0,0 @@ -<template> -<x-notes ref="timeline" :make-promise="makePromise" @inited="() => $emit('loaded')"/> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import XNotes from './deck.notes.vue'; - -const fetchLimit = 10; - -export default Vue.extend({ - components: { - XNotes - }, - - data() { - return { - 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, - more: true - }; - } else { - return { - notes: notes, - more: false - }; - } - }) - }; - }, - - mounted() { - this.connection = this.$root.stream.useSharedConnection('main'); - this.connection.on('mention', this.onNote); - }, - - beforeDestroy() { - this.connection.dispose(); - }, - - methods: { - onNote(note) { - // Prepend a note - if (note.visibility == 'specified') { - (this.$refs.timeline as any).prepend(note); - } - }, - - focus() { - this.$refs.timeline.focus(); - } - } -}); -</script> diff --git a/src/client/app/common/views/deck/deck.favorites-column.vue b/src/client/app/common/views/deck/deck.favorites-column.vue deleted file mode 100644 index 526b998f87..0000000000 --- a/src/client/app/common/views/deck/deck.favorites-column.vue +++ /dev/null @@ -1,58 +0,0 @@ -<template> -<x-column> - <template #header> - <fa :icon="['fa', 'star']"/>{{ $t('@.favorites') }} - </template> - - <div> - <x-notes ref="timeline" :make-promise="makePromise" @inited="() => $emit('loaded')"/> - </div> -</x-column> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import XColumn from './deck.column.vue'; -import XNotes from './deck.notes.vue'; - -const fetchLimit = 10; - -export default Vue.extend({ - i18n: i18n(), - - components: { - XColumn, - XNotes, - }, - - data() { - return { - 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, - more: true - }; - } else { - return { - notes: notes, - more: false - }; - } - }) - }; - }, - - methods: { - focus() { - this.$refs.timeline.focus(); - } - } -}); -</script> diff --git a/src/client/app/common/views/deck/deck.featured-column.vue b/src/client/app/common/views/deck/deck.featured-column.vue deleted file mode 100644 index d2c44e74ce..0000000000 --- a/src/client/app/common/views/deck/deck.featured-column.vue +++ /dev/null @@ -1,46 +0,0 @@ -<template> -<x-column> - <template #header> - <fa :icon="faNewspaper"/>{{ $t('@.featured-notes') }} - </template> - - <div> - <x-notes ref="timeline" :make-promise="makePromise" @inited="() => $emit('loaded')"/> - </div> -</x-column> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import XColumn from './deck.column.vue'; -import XNotes from './deck.notes.vue'; -import { faNewspaper } from '@fortawesome/free-solid-svg-icons'; - -export default Vue.extend({ - i18n: i18n(), - - components: { - XColumn, - XNotes, - }, - - data() { - return { - faNewspaper, - makePromise: cursor => this.$root.api('notes/featured', { - limit: 30, - }).then(notes => { - notes.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); - return notes; - }) - }; - }, - - methods: { - focus() { - this.$refs.timeline.focus(); - } - } -}); -</script> diff --git a/src/client/app/common/views/deck/deck.hashtag-tl.vue b/src/client/app/common/views/deck/deck.hashtag-tl.vue index 6f89f6a23d..94d2efc430 100644 --- a/src/client/app/common/views/deck/deck.hashtag-tl.vue +++ b/src/client/app/common/views/deck/deck.hashtag-tl.vue @@ -1,13 +1,11 @@ <template> -<x-notes ref="timeline" :make-promise="makePromise" @inited="() => $emit('loaded')"/> +<x-notes ref="timeline" :pagination="pagination" @inited="() => $emit('loaded')"/> </template> <script lang="ts"> import Vue from 'vue'; import XNotes from './deck.notes.vue'; -const fetchLimit = 10; - export default Vue.extend({ components: { XNotes @@ -28,28 +26,18 @@ export default Vue.extend({ data() { return { 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, - more: true - }; - } else { - return { - notes: notes, - more: false - }; - } - }) + pagination: { + endpoint: 'notes/search-by-tag', + limit: 10, + params: init => ({ + untilDate: init ? undefined : (this.date ? this.date.getTime() : 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 + }) + } }; }, diff --git a/src/client/app/common/views/deck/deck.list-tl.vue b/src/client/app/common/views/deck/deck.list-tl.vue index 24080ad4ea..26d6ea9d58 100644 --- a/src/client/app/common/views/deck/deck.list-tl.vue +++ b/src/client/app/common/views/deck/deck.list-tl.vue @@ -1,13 +1,11 @@ <template> -<x-notes ref="timeline" :make-promise="makePromise" @inited="() => $emit('loaded')"/> +<x-notes ref="timeline" :pagination="pagination" @inited="() => $emit('loaded')"/> </template> <script lang="ts"> import Vue from 'vue'; import XNotes from './deck.notes.vue'; -const fetchLimit = 10; - export default Vue.extend({ components: { XNotes @@ -28,28 +26,18 @@ export default Vue.extend({ data() { return { 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, - more: true - }; - } else { - return { - notes: notes, - more: false - }; - } - }) + pagination: { + endpoint: 'notes/user-list-timeline', + limit: 10, + params: init => ({ + listId: this.list.id, + untilDate: init ? undefined : (this.date ? this.date.getTime() : undefined), + withFiles: this.mediaOnly, + includeMyRenotes: this.$store.state.settings.showMyRenotes, + includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, + includeLocalRenotes: this.$store.state.settings.showLocalRenotes + }) + } }; }, diff --git a/src/client/app/common/views/deck/deck.mentions-column.vue b/src/client/app/common/views/deck/deck.mentions-column.vue index b7f3290d0d..12d7b2a16b 100644 --- a/src/client/app/common/views/deck/deck.mentions-column.vue +++ b/src/client/app/common/views/deck/deck.mentions-column.vue @@ -2,7 +2,7 @@ <x-column :name="name" :column="column" :is-stacked="isStacked"> <template #header><fa icon="at"/>{{ name }}</template> - <x-mentions ref="tl"/> + <x-notes ref="timeline" :pagination="pagination" @inited="() => $emit('loaded')"/> </x-column> </template> @@ -10,13 +10,14 @@ import Vue from 'vue'; import i18n from '../../../i18n'; import XColumn from './deck.column.vue'; -import XMentions from './deck.mentions.vue'; +import XNotes from './deck.notes.vue'; export default Vue.extend({ i18n: i18n(), + components: { XColumn, - XMentions + XNotes }, props: { @@ -30,6 +31,22 @@ export default Vue.extend({ } }, + data() { + return { + connection: null, + pagination: { + endpoint: 'notes/mentions', + limit: 10, + params: init => ({ + untilDate: init ? undefined : (this.date ? this.date.getTime() : undefined), + includeMyRenotes: this.$store.state.settings.showMyRenotes, + includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, + includeLocalRenotes: this.$store.state.settings.showLocalRenotes + }) + } + }; + }, + computed: { name(): string { if (this.column.name) return this.column.name; @@ -37,9 +54,22 @@ export default Vue.extend({ } }, + mounted() { + this.connection = this.$root.stream.useSharedConnection('main'); + this.connection.on('mention', this.onNote); + }, + + beforeDestroy() { + this.connection.dispose(); + }, + methods: { + onNote(note) { + (this.$refs.timeline as any).prepend(note); + }, + focus() { - this.$refs.tl.focus(); + this.$refs.timeline.focus(); } } }); diff --git a/src/client/app/common/views/deck/deck.mentions.vue b/src/client/app/common/views/deck/deck.mentions.vue deleted file mode 100644 index 153b4cd052..0000000000 --- a/src/client/app/common/views/deck/deck.mentions.vue +++ /dev/null @@ -1,61 +0,0 @@ -<template> -<x-notes ref="timeline" :make-promise="makePromise" @inited="() => $emit('loaded')"/> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import XNotes from './deck.notes.vue'; - -const fetchLimit = 10; - -export default Vue.extend({ - components: { - XNotes - }, - - data() { - return { - 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, - more: true - }; - } else { - return { - notes: notes, - more: false - }; - } - }) - }; - }, - - mounted() { - this.connection = this.$root.stream.useSharedConnection('main'); - this.connection.on('mention', this.onNote); - }, - - beforeDestroy() { - this.connection.dispose(); - }, - - methods: { - onNote(note) { - (this.$refs.timeline as any).prepend(note); - }, - - focus() { - this.$refs.timeline.focus(); - } - } -}); -</script> diff --git a/src/client/app/common/views/deck/deck.notes.vue b/src/client/app/common/views/deck/deck.notes.vue index cc655b242f..5081d1f998 100644 --- a/src/client/app/common/views/deck/deck.notes.vue +++ b/src/client/app/common/views/deck/deck.notes.vue @@ -1,8 +1,8 @@ <template> <div class="eamppglmnmimdhrlzhplwpvyeaqmmhxu"> - <div class="empty" v-if="notes.length == 0 && !fetching && inited">{{ $t('@.no-notes') }}</div> + <div class="empty" v-if="empty">{{ $t('@.no-notes') }}</div> - <mk-error v-if="!fetching && !inited" @retry="init()"/> + <mk-error v-if="error" @retry="init()"/> <div class="placeholder" v-if="fetching"> <template v-for="i in 10"> @@ -16,7 +16,6 @@ <mk-note :note="note" :key="note.id" - @update:note="onNoteUpdated(i, $event)" :compact="true" /> <p class="date" :key="note.id + '_date'" v-if="i != notes.length - 1 && note._date != _notes[i + 1]._date"> @@ -39,33 +38,47 @@ import Vue from 'vue'; import i18n from '../../../i18n'; import shouldMuteNote from '../../../common/scripts/should-mute-note'; - -const displayLimit = 20; +import paging from '../../../common/scripts/paging'; export default Vue.extend({ i18n: i18n(), inject: ['column', 'isScrollTop', 'count'], + mixins: [ + paging({ + limit: 20, + + onQueueChanged: (self, q) => { + self.count(q.length); + }, + + onPrepend: (self, note, silent) => { + // 弾く + if (shouldMuteNote(self.$store.state.i, self.$store.state.settings, note)) return false; + + // タブが非表示またはスクロール位置が最上部ではないならタイトルで通知 + if (document.hidden || !self.isScrollTop()) { + self.$store.commit('pushBehindNote', note); + } + } + }), + ], + props: { - makePromise: { + pagination: { required: true + }, + extract: { + required: false } }, - data() { - return { - rootEl: null, - notes: [], - queue: [], - fetching: true, - moreFetching: false, - inited: false, - more: false - }; - }, - computed: { + notes() { + return this.extract ? this.extract(this.items) : this.items; + }, + _notes(): any[] { return (this.notes as any).map(note => { const date = new Date(note.createdAt).getDate(); @@ -77,15 +90,6 @@ export default Vue.extend({ } }, - watch: { - queue(q) { - this.count(q.length); - }, - makePromise() { - this.init(); - } - }, - created() { this.column.$on('top', this.onTop); this.column.$on('bottom', this.onBottom); @@ -101,88 +105,6 @@ export default Vue.extend({ focus() { (this.$refs.notes as any).children[0].focus ? (this.$refs.notes as any).children[0].focus() : (this.$refs.notes as any).$el.children[0].focus(); }, - - onNoteUpdated(i, note) { - Vue.set((this as any).notes, i, note); - }, - - reload() { - this.init(); - }, - - async init() { - this.queue = []; - this.notes = []; - this.fetching = true; - await (this.makePromise()).then(x => { - if (Array.isArray(x)) { - this.notes = x; - } else { - this.notes = x.notes; - this.more = x.more; - } - this.inited = true; - this.fetching = false; - this.$emit('inited'); - }, e => { - this.fetching = false; - }); - }, - - async fetchMore() { - if (!this.more || this.moreFetching) return; - this.moreFetching = true; - await (this.makePromise(this.notes[this.notes.length - 1].id)).then(x => { - this.notes = this.notes.concat(x.notes); - this.more = x.more; - this.moreFetching = false; - }, e => { - this.moreFetching = false; - }); - }, - - prepend(note, silent = false) { - // 弾く - if (shouldMuteNote(this.$store.state.i, this.$store.state.settings, note)) return; - - // タブが非表示ならタイトルで通知 - if (document.hidden) { - this.$store.commit('pushBehindNote', note); - } - - if (this.isScrollTop()) { - // Prepend the note - this.notes.unshift(note); - - // オーバーフローしたら古い投稿は捨てる - if (this.notes.length >= displayLimit) { - this.notes = this.notes.slice(0, displayLimit); - this.more = true; - } - } else { - this.queue.push(note); - } - }, - - append(note) { - this.notes.push(note); - this.cursor = this.notes[this.notes.length - 1].id - }, - - releaseQueue() { - for (const n of this.queue) { - this.prepend(n, true); - } - this.queue = []; - }, - - onTop() { - this.releaseQueue(); - }, - - onBottom() { - this.fetchMore(); - } } }); </script> diff --git a/src/client/app/common/views/deck/deck.notification.vue b/src/client/app/common/views/deck/deck.notification.vue index 3ced7b7e23..8a21bedb91 100644 --- a/src/client/app/common/views/deck/deck.notification.vue +++ b/src/client/app/common/views/deck/deck.notification.vue @@ -81,15 +81,15 @@ </div> <template v-if="notification.type == 'quote'"> - <mk-note :note="notification.note" @update:note="onNoteUpdated"/> + <mk-note :note="notification.note"/> </template> <template v-if="notification.type == 'reply'"> - <mk-note :note="notification.note" @update:note="onNoteUpdated"/> + <mk-note :note="notification.note"/> </template> <template v-if="notification.type == 'mention'"> - <mk-note :note="notification.note" @update:note="onNoteUpdated"/> + <mk-note :note="notification.note"/> </template> </div> </template> @@ -105,17 +105,6 @@ export default Vue.extend({ getNoteSummary }; }, - methods: { - onNoteUpdated(note) { - switch (this.notification.type) { - case 'quote': - case 'reply': - case 'mention': - Vue.set(this.notification, 'note', note); - break; - } - } - } }); </script> diff --git a/src/client/app/common/views/deck/deck.notifications.vue b/src/client/app/common/views/deck/deck.notifications.vue index bb93e5e75e..e01d0023f6 100644 --- a/src/client/app/common/views/deck/deck.notifications.vue +++ b/src/client/app/common/views/deck/deck.notifications.vue @@ -10,16 +10,17 @@ <component :is="!$store.state.device.reduceMotion ? 'transition-group' : 'div'" name="mk-notifications" class="transition notifications" tag="div"> <template v-for="(notification, i) in _notifications"> <x-notification class="notification" :notification="notification" :key="notification.id"/> - <p class="date" v-if="i != notifications.length - 1 && notification._date != _notifications[i + 1]._date" :key="notification.id + '-time'"> + <p class="date" v-if="i != items.length - 1 && notification._date != _notifications[i + 1]._date" :key="notification.id + '-time'"> <span><fa icon="angle-up"/>{{ notification._datetext }}</span> <span><fa icon="angle-down"/>{{ _notifications[i + 1]._datetext }}</span> </p> </template> </component> - <button class="more" :class="{ fetching: fetchingMoreNotifications }" v-if="moreNotifications" @click="fetchMoreNotifications" :disabled="fetchingMoreNotifications"> - <template v-if="fetchingMoreNotifications"><fa icon="spinner" pulse fixed-width/></template>{{ fetchingMoreNotifications ? this.$t('@.loading') : this.$t('@.load-more') }} + <button class="more" :class="{ fetching: moreFetching }" v-if="more" @click="fetchMore" :disabled="moreFetching"> + <template v-if="moreFetching"><fa icon="spinner" pulse fixed-width/></template>{{ moreFetching ? this.$t('@.loading') : this.$t('@.load-more') }} </button> - <p class="empty" v-if="notifications.length == 0 && !fetching">{{ $t('empty') }}</p> + <p class="empty" v-if="empty">{{ $t('empty') }}</p> + <mk-error v-if="error" @retry="init()"/> </div> </template> @@ -27,31 +28,38 @@ import Vue from 'vue'; import i18n from '../../../i18n'; import XNotification from './deck.notification.vue'; - -const displayLimit = 20; +import paging from '../../../common/scripts/paging'; export default Vue.extend({ i18n: i18n(), + components: { XNotification }, inject: ['column', 'isScrollTop', 'count'], + mixins: [ + paging({ + onQueueChanged: (self, q) => { + self.count(q.length); + }, + }), + ], + data() { return { - fetching: true, - fetchingMoreNotifications: false, - notifications: [], - queue: [], - moreNotifications: false, - connection: null + connection: null, + pagination: { + endpoint: 'i/notifications', + limit: 20, + } }; }, computed: { _notifications(): any[] { - return (this.notifications as any).map(notification => { + return (this.items as any).map(notification => { const date = new Date(notification.createdAt).getDate(); const month = new Date(notification.createdAt).getMonth() + 1; notification._date = date; @@ -61,33 +69,12 @@ export default Vue.extend({ } }, - watch: { - queue(q) { - this.count(q.length); - } - }, - mounted() { this.connection = this.$root.stream.useSharedConnection('main'); - this.connection.on('notification', this.onNotification); this.column.$on('top', this.onTop); this.column.$on('bottom', this.onBottom); - - const max = 10; - - this.$root.api('i/notifications', { - limit: max + 1 - }).then(notifications => { - if (notifications.length == max + 1) { - this.moreNotifications = true; - notifications.pop(); - } - - this.notifications = notifications; - this.fetching = false; - }); }, beforeDestroy() { @@ -98,26 +85,6 @@ export default Vue.extend({ }, methods: { - fetchMoreNotifications() { - this.fetchingMoreNotifications = true; - - const max = 20; - - this.$root.api('i/notifications', { - limit: max + 1, - untilId: this.notifications[this.notifications.length - 1].id - }).then(notifications => { - if (notifications.length == max + 1) { - this.moreNotifications = true; - notifications.pop(); - } else { - this.moreNotifications = false; - } - this.notifications = this.notifications.concat(notifications); - this.fetchingMoreNotifications = false; - }); - }, - onNotification(notification) { // TODO: ユーザーが画面を見てないと思われるとき(ブラウザやタブがアクティブじゃないなど)は送信しない this.$root.stream.send('readNotification', { @@ -126,35 +93,6 @@ export default Vue.extend({ this.prepend(notification); }, - - prepend(notification) { - if (this.isScrollTop()) { - // Prepend the notification - this.notifications.unshift(notification); - - // オーバーフローしたら古い通知は捨てる - if (this.notifications.length >= displayLimit) { - this.notifications = this.notifications.slice(0, displayLimit); - } - } else { - this.queue.push(notification); - } - }, - - releaseQueue() { - for (const n of this.queue) { - this.prepend(n); - } - this.queue = []; - }, - - onTop() { - this.releaseQueue(); - }, - - onBottom() { - this.fetchMoreNotifications(); - } } }); </script> diff --git a/src/client/app/common/views/deck/deck.search-column.vue b/src/client/app/common/views/deck/deck.search-column.vue index 17ee2ef454..a2d1142fbe 100644 --- a/src/client/app/common/views/deck/deck.search-column.vue +++ b/src/client/app/common/views/deck/deck.search-column.vue @@ -5,7 +5,7 @@ </template> <div> - <x-notes ref="timeline" :make-promise="makePromise" @inited="() => $emit('loaded')"/> + <x-notes ref="timeline" :pagination="pagination" @inited="() => $emit('loaded')"/> </div> </x-column> </template> @@ -16,8 +16,6 @@ import XColumn from './deck.column.vue'; import XNotes from './deck.notes.vue'; import { genSearchQuery } from '../../../common/scripts/gen-search-query'; -const limit = 20; - export default Vue.extend({ components: { XColumn, @@ -26,24 +24,11 @@ export default Vue.extend({ data() { return { - makePromise: async cursor => this.$root.api('notes/search', { - limit: limit + 1, - offset: cursor ? cursor : undefined, - ...(await genSearchQuery(this, this.q)) - }).then(notes => { - if (notes.length == limit + 1) { - notes.pop(); - return { - notes: notes, - cursor: cursor ? cursor + limit : limit - }; - } else { - return { - notes: notes, - more: false - }; - } - }) + pagination: { + endpoint: 'notes/search', + limit: 20, + params: () => genSearchQuery(this, this.q) + } }; }, diff --git a/src/client/app/common/views/deck/deck.tl.vue b/src/client/app/common/views/deck/deck.tl.vue index 9284f06ee1..e6c716070a 100644 --- a/src/client/app/common/views/deck/deck.tl.vue +++ b/src/client/app/common/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" :make-promise="makePromise" @inited="() => $emit('loaded')"/> +<x-notes v-else ref="timeline" :pagination="pagination" @inited="() => $emit('loaded')"/> </template> <script lang="ts"> @@ -15,8 +15,6 @@ import XNotes from './deck.notes.vue'; import { faMinusCircle } from '@fortawesome/free-solid-svg-icons'; import i18n from '../../../i18n'; -const fetchLimit = 10; - export default Vue.extend({ i18n: i18n('deck'), @@ -42,7 +40,7 @@ export default Vue.extend({ connection: null, disabled: false, faMinusCircle, - makePromise: null + pagination: null }; }, @@ -73,27 +71,17 @@ export default Vue.extend({ }, created() { - this.makePromise = cursor => this.$root.api(this.endpoint, { - 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, - more: true - }; - } else { - return { - notes: notes, - more: false - }; - } - }); + this.pagination = { + endpoint: this.endpoint, + limit: 10, + params: init => ({ + untilDate: init ? undefined : (this.date ? this.date.getTime() : undefined), + withFiles: this.mediaOnly, + includeMyRenotes: this.$store.state.settings.showMyRenotes, + includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, + includeLocalRenotes: this.$store.state.settings.showLocalRenotes + }) + }; }, mounted() { diff --git a/src/client/app/common/views/deck/deck.user-column.home.vue b/src/client/app/common/views/deck/deck.user-column.home.vue index f4b9b98097..ad05d6548b 100644 --- a/src/client/app/common/views/deck/deck.user-column.home.vue +++ b/src/client/app/common/views/deck/deck.user-column.home.vue @@ -30,7 +30,7 @@ <ui-container> <template #header><fa :icon="['far', 'comment-alt']"/> {{ $t('timeline') }}</template> <div> - <x-notes ref="timeline" :make-promise="makePromise" @inited="() => $emit('loaded')"/> + <x-notes ref="timeline" :pagination="pagination" @inited="() => $emit('loaded')"/> </div> </ui-container> </div> @@ -43,8 +43,6 @@ import XNotes from './deck.notes.vue'; import { concat } from '../../../../../prelude/array'; import ApexCharts from 'apexcharts'; -const fetchLimit = 10; - export default Vue.extend({ i18n: i18n('deck/deck.user-column.vue'), @@ -63,49 +61,38 @@ export default Vue.extend({ return { withFiles: false, images: [], - makePromise: null, chart: null as ApexCharts }; }, + computed: { + pagination() { + return { + endpoint: 'users/notes', + limit: 10, + params: init => ({ + userId: this.user.id, + untilDate: init ? undefined : (this.date ? this.date.getTime() : undefined), + withFiles: this.withFiles, + includeMyRenotes: this.$store.state.settings.showMyRenotes, + includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, + includeLocalRenotes: this.$store.state.settings.showLocalRenotes + }) + } + } + }, + watch: { user() { this.fetch(); - this.genPromiseMaker(); } }, created() { this.fetch(); - this.genPromiseMaker(); }, methods: { - genPromiseMaker() { - this.makePromise = cursor => this.$root.api('users/notes', { - userId: this.user.id, - limit: fetchLimit + 1, - untilDate: cursor ? cursor : 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(); - return { - notes: notes, - more: true - }; - } else { - return { - notes: notes, - more: false - }; - } - }); - }, - fetch() { const image = [ 'image/jpeg', diff --git a/src/client/app/common/views/pages/explore.vue b/src/client/app/common/views/pages/explore.vue index bf0d7ab574..f28760e516 100644 --- a/src/client/app/common/views/pages/explore.vue +++ b/src/client/app/common/views/pages/explore.vue @@ -18,24 +18,24 @@ </div> </ui-container> - <mk-user-list v-if="tag != null" :make-promise="tagUsers" :key="`${tag}-local`"> + <mk-user-list v-if="tag != null" :pagination="tagUsers" :key="`${tag}-local`"> <fa :icon="faHashtag" fixed-width/>{{ tag }} </mk-user-list> - <mk-user-list v-if="tag != null" :make-promise="tagRemoteUsers" :key="`${tag}-remote`"> + <mk-user-list v-if="tag != null" :pagination="tagRemoteUsers" :key="`${tag}-remote`"> <fa :icon="faHashtag" fixed-width/>{{ tag }} ({{ $t('federated') }}) </mk-user-list> <template v-if="tag == null"> - <mk-user-list :make-promise="pinnedUsers"> + <mk-user-list :pagination="pinnedUsers"> <fa :icon="faBookmark" fixed-width/>{{ $t('pinned-users') }} </mk-user-list> - <mk-user-list :make-promise="popularUsers"> + <mk-user-list :pagination="popularUsers"> <fa :icon="faChartLine" fixed-width/>{{ $t('popular-users') }} </mk-user-list> - <mk-user-list :make-promise="recentlyUpdatedUsers"> + <mk-user-list :pagination="recentlyUpdatedUsers"> <fa :icon="faCommentAlt" fixed-width/>{{ $t('recently-updated-users') }} </mk-user-list> - <mk-user-list :make-promise="recentlyRegisteredUsers"> + <mk-user-list :pagination="recentlyRegisteredUsers"> <fa :icon="faPlus" fixed-width/>{{ $t('recently-registered-users') }} </mk-user-list> </template> @@ -60,24 +60,21 @@ export default Vue.extend({ data() { return { - pinnedUsers: () => this.$root.api('pinned-users'), - popularUsers: () => this.$root.api('users', { + pinnedUsers: { endpoint: 'pinned-users' }, + popularUsers: { endpoint: 'users', limit: 10, params: { state: 'alive', origin: 'local', sort: '+follower', - limit: 10 - }), - recentlyUpdatedUsers: () => this.$root.api('users', { + } }, + recentlyUpdatedUsers: { endpoint: 'users', limit: 10, params: { origin: 'local', sort: '+updatedAt', - limit: 10 - }), - recentlyRegisteredUsers: () => this.$root.api('users', { + } }, + recentlyRegisteredUsers: { endpoint: 'users', limit: 10, params: { origin: 'local', state: 'alive', sort: '+createdAt', - limit: 10 - }), + } }, tagsLocal: [], tagsRemote: [], stats: null, @@ -88,24 +85,30 @@ export default Vue.extend({ }, computed: { - tagUsers(): () => Promise<any> { - return () => this.$root.api('hashtags/users', { - tag: this.tag, - state: 'alive', - origin: 'local', - sort: '+follower', - limit: 30 - }); + tagUsers(): any { + return { + endpoint: 'hashtags/users', + limit: 30, + params: { + tag: this.tag, + state: 'alive', + origin: 'local', + sort: '+follower', + } + }; }, - tagRemoteUsers(): () => Promise<any> { - return () => this.$root.api('hashtags/users', { - tag: this.tag, - state: 'alive', - origin: 'remote', - sort: '+follower', - limit: 30 - }); + tagRemoteUsers(): any { + return { + endpoint: 'hashtags/users', + limit: 30, + params: { + tag: this.tag, + state: 'alive', + origin: 'remote', + sort: '+follower', + } + }; }, }, diff --git a/src/client/app/common/views/pages/favorites.vue b/src/client/app/common/views/pages/favorites.vue new file mode 100644 index 0000000000..36403dde52 --- /dev/null +++ b/src/client/app/common/views/pages/favorites.vue @@ -0,0 +1,44 @@ +<template> +<div> + <component :is="notesComponent" :pagination="pagination" :extract="items => items.map(item => item.note)"/> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faStar } from '@fortawesome/free-solid-svg-icons'; +import i18n from '../../../i18n'; +//import Progress from '../../../common/scripts/loading'; + +export default Vue.extend({ + i18n: i18n(), + + props: { + platform: { + type: String, + required: true + } + }, + + data() { + return { + pagination: { + endpoint: 'i/favorites', + limit: 10, + }, + + notesComponent: + this.platform === 'desktop' ? () => import('../../../desktop/views/components/detail-notes.vue').then(m => m.default) : + this.platform === 'mobile' ? () => import('../../../mobile/views/components/detail-notes.vue').then(m => m.default) : + this.platform === 'deck' ? () => import('../deck/deck.notes.vue').then(m => m.default) : null + }; + }, + + created() { + this.$emit('init', { + title: this.$t('@.favorites'), + icon: faStar + }); + }, +}); +</script> diff --git a/src/client/app/common/views/pages/featured.vue b/src/client/app/common/views/pages/featured.vue new file mode 100644 index 0000000000..161511998f --- /dev/null +++ b/src/client/app/common/views/pages/featured.vue @@ -0,0 +1,44 @@ +<template> +<div> + <component :is="notesComponent" :pagination="pagination"/> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faNewspaper } from '@fortawesome/free-solid-svg-icons'; +import i18n from '../../../i18n'; +//import Progress from '../../../common/scripts/loading'; + +export default Vue.extend({ + i18n: i18n(), + + props: { + platform: { + type: String, + required: true + } + }, + + data() { + return { + pagination: { + endpoint: 'notes/featured', + limit: 29, + }, + + notesComponent: + this.platform === 'desktop' ? () => import('../../../desktop/views/components/detail-notes.vue').then(m => m.default) : + this.platform === 'mobile' ? () => import('../../../mobile/views/components/detail-notes.vue').then(m => m.default) : + this.platform === 'deck' ? () => import('../deck/deck.notes.vue').then(m => m.default) : null + }; + }, + + created() { + this.$emit('init', { + title: this.$t('@.featured-notes'), + icon: faNewspaper + }); + }, +}); +</script> diff --git a/src/client/app/common/views/pages/followers.vue b/src/client/app/common/views/pages/followers.vue index 1d68d71e80..b546e69ae3 100644 --- a/src/client/app/common/views/pages/followers.vue +++ b/src/client/app/common/views/pages/followers.vue @@ -1,7 +1,5 @@ <template> -<div> - <mk-user-list :make-promise="makePromise">{{ $t('@.followers') }}</mk-user-list> -</div> +<mk-user-list :pagination="pagination" :extract="items => items.map(item => item.follower)">{{ $t('@.followers') }}</mk-user-list> </template> <script lang="ts"> @@ -9,31 +7,18 @@ import Vue from 'vue'; import parseAcct from '../../../../../misc/acct/parse'; import i18n from '../../../i18n'; -const fetchLimit = 30; - export default Vue.extend({ i18n: i18n(), data() { return { - makePromise: cursor => this.$root.api('users/followers', { - ...parseAcct(this.$route.params.user), - limit: fetchLimit + 1, - untilId: cursor ? cursor : undefined, - }).then(followings => { - if (followings.length == fetchLimit + 1) { - followings.pop(); - return { - users: followings.map(following => following.follower), - cursor: followings[followings.length - 1].id - }; - } else { - return { - users: followings.map(following => following.follower), - more: false - }; + pagination: { + endpoint: 'users/followers', + limit: 30, + params: { + ...parseAcct(this.$route.params.user), } - }), + }, }; }, }); diff --git a/src/client/app/common/views/pages/following.vue b/src/client/app/common/views/pages/following.vue index b65d335314..4e584c19d9 100644 --- a/src/client/app/common/views/pages/following.vue +++ b/src/client/app/common/views/pages/following.vue @@ -1,7 +1,5 @@ <template> -<div> - <mk-user-list :make-promise="makePromise">{{ $t('@.following') }}</mk-user-list> -</div> +<mk-user-list :pagination="pagination" :extract="items => items.map(item => item.followee)">{{ $t('@.following') }}</mk-user-list> </template> <script lang="ts"> @@ -9,31 +7,18 @@ import Vue from 'vue'; import parseAcct from '../../../../../misc/acct/parse'; import i18n from '../../../i18n'; -const fetchLimit = 30; - export default Vue.extend({ i18n: i18n(), data() { return { - makePromise: cursor => this.$root.api('users/following', { - ...parseAcct(this.$route.params.user), - limit: fetchLimit + 1, - untilId: cursor ? cursor : undefined, - }).then(followings => { - if (followings.length == fetchLimit + 1) { - followings.pop(); - return { - users: followings.map(following => following.followee), - cursor: followings[followings.length - 1].id - }; - } else { - return { - users: followings.map(following => following.followee), - more: false - }; + pagination: { + endpoint: 'users/following', + limit: 30, + params: { + ...parseAcct(this.$route.params.user), } - }), + }, }; }, }); diff --git a/src/client/app/common/views/pages/pages.vue b/src/client/app/common/views/pages/pages.vue index 9b0a19d455..d0a56ac2fa 100644 --- a/src/client/app/common/views/pages/pages.vue +++ b/src/client/app/common/views/pages/pages.vue @@ -2,22 +2,20 @@ <div> <ui-container :body-togglable="true"> <template #header><fa :icon="faEdit" fixed-width/>{{ $t('my-pages') }}</template> - <div class="rknalgpo" v-if="!fetching"> + <div class="rknalgpo my"> <ui-button class="new" @click="create()"><fa :icon="faPlus"/></ui-button> - <sequential-entrance animation="entranceFromTop" delay="25" tag="div" class="pages"> - <x-page-preview v-for="page in pages" class="page" :page="page" :key="page.id"/> - </sequential-entrance> - <ui-button v-if="existMore" @click="fetchMore()" style="margin-top:16px;">{{ $t('@.load-more') }}</ui-button> + <ui-pagination :pagination="myPagesPagination" #default="{items}"> + <x-page-preview v-for="page in items" class="ckltabjg" :page="page" :key="page.id"/> + </ui-pagination> </div> </ui-container> <ui-container :body-togglable="true"> <template #header><fa :icon="faHeart" fixed-width/>{{ $t('liked-pages') }}</template> - <div class="rknalgpo" v-if="!fetching"> - <sequential-entrance animation="entranceFromTop" delay="25" tag="div" class="pages"> - <x-page-preview v-for="like in likes" class="page" :page="like.page" :key="like.page.id"/> - </sequential-entrance> - <ui-button v-if="existMoreLikes" @click="fetchMoreLiked()">{{ $t('@.load-more') }}</ui-button> + <div class="rknalgpo"> + <ui-pagination :pagination="likedPagesPagination" #default="{items}"> + <x-page-preview v-for="like in items" class="ckltabjg" :page="like.page" :key="like.page.id"/> + </ui-pagination> </div> </ui-container> </div> @@ -28,7 +26,6 @@ import Vue from 'vue'; import { faPlus, faEdit } from '@fortawesome/free-solid-svg-icons'; import { faStickyNote, faHeart } from '@fortawesome/free-regular-svg-icons'; import i18n from '../../../i18n'; -import Progress from '../../scripts/loading'; import XPagePreview from '../../views/components/page-preview.vue'; export default Vue.extend({ @@ -38,87 +35,24 @@ export default Vue.extend({ }, data() { return { - fetching: true, - pages: [], - existMore: false, - moreFetching: false, - likes: [], - existMoreLikes: false, - moreLikesFetching: false, + myPagesPagination: { + endpoint: 'i/pages', + limit: 5, + }, + likedPagesPagination: { + endpoint: 'i/page-likes', + limit: 5, + }, faStickyNote, faPlus, faEdit, faHeart }; }, created() { - this.fetch(); - this.$emit('init', { title: this.$t('@.pages'), icon: faStickyNote }); }, methods: { - async fetch() { - Progress.start(); - this.fetching = true; - - const pages = await this.$root.api('i/pages', { - limit: 11 - }); - - if (pages.length == 11) { - this.existMore = true; - pages.pop(); - } - - const likes = await this.$root.api('i/page-likes', { - limit: 11 - }); - - if (likes.length == 11) { - this.existMoreLikes = true; - likes.pop(); - } - - this.pages = pages; - this.likes = likes; - this.fetching = false; - - Progress.done(); - }, - fetchMore() { - this.moreFetching = true; - this.$root.api('i/pages', { - limit: 11, - untilId: this.pages[this.pages.length - 1].id - }).then(pages => { - if (pages.length == 11) { - this.existMore = true; - pages.pop(); - } else { - this.existMore = false; - } - - this.pages = this.pages.concat(pages); - this.moreFetching = false; - }); - }, - fetchMoreLiked() { - this.moreLikesFetching = true; - this.$root.api('i/page-likes', { - limit: 11, - untilId: this.likes[this.likes.length - 1].id - }).then(pages => { - if (pages.length == 11) { - this.existMoreLikes = true; - pages.pop(); - } else { - this.existMoreLikes = false; - } - - this.likes = this.likes.concat(pages); - this.moreLikesFetching = false; - }); - }, create() { this.$router.push(`/i/pages/new`); } @@ -130,14 +64,14 @@ export default Vue.extend({ .rknalgpo padding 16px - > .new - margin-bottom 16px + &.my .ckltabjg:first-child + margin-top 16px - > * > .page:not(:last-child) + .ckltabjg:not(:last-child) margin-bottom 8px @media (min-width 500px) - > * > .page:not(:last-child) + .ckltabjg:not(:last-child) margin-bottom 16px </style> diff --git a/src/client/app/common/views/pages/share.vue b/src/client/app/common/views/pages/share.vue index 0452b25dfc..293a9bcfb5 100644 --- a/src/client/app/common/views/pages/share.vue +++ b/src/client/app/common/views/pages/share.vue @@ -3,7 +3,7 @@ <h1>{{ $t('share-with', { name }) }}</h1> <div> <mk-signin v-if="!$store.getters.isSignedIn"/> - <mk-post-form v-else-if="!posted" :initial-text="template" :instant="true" @posted="posted = true"/> + <x-post-form v-else-if="!posted" :initial-text="template" :instant="true" @posted="posted = true"/> <p v-if="posted" class="posted"><fa icon="check"/></p> </div> <ui-button class="close" v-if="posted" @click="close">{{ $t('@.close') }}</ui-button> @@ -16,6 +16,9 @@ import i18n from '../../../i18n'; export default Vue.extend({ i18n: i18n('mobile/views/pages/share.vue'), + components: { + XPostForm: () => import('../../../desktop/views/components/post-form.vue').then(m => m.default) + }, data() { return { name: null, @@ -35,15 +38,15 @@ export default Vue.extend({ return t.trim(); } }, - methods: { - close() { - window.close(); - } - }, mounted() { this.$root.getMeta().then(meta => { this.name = meta.name || 'Misskey'; }); + }, + methods: { + close() { + window.close(); + } } }); </script> diff --git a/src/client/app/common/views/pages/user-groups.vue b/src/client/app/common/views/pages/user-groups.vue index 2a89196e55..2fee46c3a1 100644 --- a/src/client/app/common/views/pages/user-groups.vue +++ b/src/client/app/common/views/pages/user-groups.vue @@ -1,6 +1,6 @@ <template> <div> - <ui-container> + <ui-container :body-togglable="true"> <template #header><fa :icon="faUsers"/> {{ $t('owned-groups') }}</template> <ui-margin> <ui-button @click="add"><fa :icon="faPlus"/> {{ $t('create-group') }}</ui-button> @@ -9,26 +9,29 @@ <ui-hr/> <ui-margin> <router-link :to="`/i/groups/${group.id}`">{{ group.name }}</router-link> + <x-avatars :user-ids="group.userIds" style="margin-top:8px;"/> </ui-margin> </div> </ui-container> - <ui-container> + <ui-container :body-togglable="true"> <template #header><fa :icon="faUsers"/> {{ $t('joined-groups') }}</template> <div class="hwgkdrbl" v-for="(group, i) in joinedGroups" :key="group.id"> <ui-hr v-if="i != 0"/> <ui-margin> - <router-link :to="`/i/groups/${group.id}`">{{ group.name }}</router-link> + <div>{{ group.name }}</div> + <x-avatars :user-ids="group.userIds" style="margin-top:8px;"/> </ui-margin> </div> </ui-container> - <ui-container> + <ui-container :body-togglable="true"> <template #header><fa :icon="faEnvelopeOpenText"/> {{ $t('invites') }}</template> <div class="fvlojuur" v-for="(invite, i) in invites" :key="invite.id"> <ui-hr v-if="i != 0"/> <ui-margin> <div class="name">{{ invite.group.name }}</div> + <x-avatars :user-ids="invite.group.userIds" style="margin-top:8px;"/> <ui-horizon-group> <ui-button @click="acceptInvite(invite)"><fa :icon="faCheck"/> {{ $t('accept-invite') }}</ui-button> <ui-button @click="rejectInvite(invite)"><fa :icon="faBan"/> {{ $t('reject-invite') }}</ui-button> @@ -41,11 +44,15 @@ <script lang="ts"> import Vue from 'vue'; -import i18n from '../../../i18n'; import { faUsers, faPlus, faCheck, faBan, faEnvelopeOpenText } from '@fortawesome/free-solid-svg-icons'; +import i18n from '../../../i18n'; +import XAvatars from '../../views/components/avatars.vue'; export default Vue.extend({ i18n: i18n('common/views/components/user-groups.vue'), + components: { + XAvatars + }, data() { return { ownedGroups: [], diff --git a/src/client/app/common/views/pages/user-lists.vue b/src/client/app/common/views/pages/user-lists.vue index 4c09eca6ce..29085935cb 100644 --- a/src/client/app/common/views/pages/user-lists.vue +++ b/src/client/app/common/views/pages/user-lists.vue @@ -8,6 +8,7 @@ <ui-hr/> <ui-margin> <router-link :to="`/i/lists/${list.id}`">{{ list.name }}</router-link> + <x-avatars :user-ids="list.userIds" style="margin-top:8px;"/> </ui-margin> </div> </ui-container> @@ -17,9 +18,13 @@ import Vue from 'vue'; import i18n from '../../../i18n'; import { faListUl, faPlus } from '@fortawesome/free-solid-svg-icons'; +import XAvatars from '../../views/components/avatars.vue'; export default Vue.extend({ i18n: i18n('common/views/components/user-lists.vue'), + components: { + XAvatars + }, data() { return { fetching: true, diff --git a/src/client/app/common/views/widgets/post-form.vue b/src/client/app/common/views/widgets/post-form.vue index d8617bea58..e180290f95 100644 --- a/src/client/app/common/views/widgets/post-form.vue +++ b/src/client/app/common/views/widgets/post-form.vue @@ -38,7 +38,6 @@ import define from '../../../common/define-widget'; import i18n from '../../../i18n'; import insertTextAtCursor from 'insert-text-at-cursor'; -import XPostFormAttaches from '../components/post-form-attaches.vue'; export default define({ name: 'post-form', @@ -49,7 +48,7 @@ export default define({ i18n: i18n('desktop/views/widgets/post-form.vue'), components: { - XPostFormAttaches + XPostFormAttaches: () => import('../components/post-form-attaches.vue').then(m => m.default) }, data() { diff --git a/src/client/app/desktop/script.ts b/src/client/app/desktop/script.ts index c6479f477c..845f8ee5c0 100644 --- a/src/client/app/desktop/script.ts +++ b/src/client/app/desktop/script.ts @@ -13,7 +13,6 @@ import fuckAdBlock from '../common/scripts/fuck-ad-block'; import composeNotification from '../common/scripts/compose-notification'; import MkHome from './views/home/home.vue'; -import MkDeck from '../common/views/deck/deck.vue'; import MkSelectDrive from './views/pages/selectdrive.vue'; import MkDrive from './views/pages/drive.vue'; import MkMessagingRoom from './views/pages/messaging-room.vue'; @@ -25,7 +24,6 @@ import MkSettings from './views/pages/settings.vue'; import DeckColumn from '../common/views/deck/deck.column-template.vue'; import Ctx from './views/components/context-menu.vue'; -import PostFormWindow from './views/components/post-form-window.vue'; import RenoteFormWindow from './views/components/renote-form-window.vue'; import MkChooseFileFromDriveWindow from './views/components/choose-file-from-drive-window.vue'; import MkChooseFolderFromDriveWindow from './views/components/choose-folder-from-drive-window.vue'; @@ -62,12 +60,13 @@ init(async (launch, os) => { }); if (o.cb) vm.$once('closed', o.cb); } else { - const vm = this.$root.new(PostFormWindow, { + this.$root.newAsync(() => import('./views/components/post-form-window.vue').then(m => m.default), { reply: o.reply, mention: o.mention, animation: o.animation == null ? true : o.animation + }).then(vm => { + if (o.cb) vm.$once('closed', o.cb); }); - if (o.cb) vm.$once('closed', o.cb); } }, @@ -129,7 +128,7 @@ init(async (launch, os) => { mode: 'history', routes: [ os.store.state.device.inDeckMode - ? { path: '/', name: 'index', component: MkDeck, children: [ + ? { path: '/', name: 'index', component: () => import('../common/views/deck/deck.vue').then(m => m.default), children: [ { path: '/@:user', component: () => import('../common/views/deck/deck.user-column.vue').then(m => m.default), children: [ { path: '', name: 'user', component: () => import('../common/views/deck/deck.user-column.home.vue').then(m => m.default) }, { path: 'following', component: () => import('../common/views/pages/following.vue').then(m => m.default) }, @@ -138,10 +137,10 @@ init(async (launch, os) => { { path: '/notes/:note', name: 'note', component: () => import('../common/views/deck/deck.note-column.vue').then(m => m.default) }, { path: '/search', component: () => import('../common/views/deck/deck.search-column.vue').then(m => m.default) }, { path: '/tags/:tag', name: 'tag', component: () => import('../common/views/deck/deck.hashtag-column.vue').then(m => m.default) }, - { path: '/featured', name: 'featured', component: () => import('../common/views/deck/deck.featured-column.vue').then(m => m.default) }, + { path: '/featured', name: 'featured', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/featured.vue').then(m => m.default), platform: 'deck' }) }, { path: '/explore', name: 'explore', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/explore.vue').then(m => m.default) }) }, { path: '/explore/tags/:tag', name: 'explore-tag', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/explore.vue').then(m => m.default), tag: route.params.tag }) }, - { path: '/i/favorites', component: () => import('../common/views/deck/deck.favorites-column.vue').then(m => m.default) }, + { path: '/i/favorites', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/favorites.vue').then(m => m.default), platform: 'deck' }) }, { path: '/i/pages', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/pages.vue').then(m => m.default) }) }, { path: '/i/lists', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/user-lists.vue').then(m => m.default) }) }, { path: '/i/lists/:listId', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/user-list-editor.vue').then(m => m.default), listId: route.params.listId }) }, @@ -158,10 +157,10 @@ init(async (launch, os) => { { path: '/notes/:note', name: 'note', component: () => import('./views/home/note.vue').then(m => m.default) }, { path: '/search', component: () => import('./views/home/search.vue').then(m => m.default) }, { path: '/tags/:tag', name: 'tag', component: () => import('./views/home/tag.vue').then(m => m.default) }, - { path: '/featured', name: 'featured', component: () => import('./views/home/featured.vue').then(m => m.default) }, + { path: '/featured', name: 'featured', component: () => import('../common/views/pages/featured.vue').then(m => m.default), props: { platform: 'desktop' } }, { path: '/explore', name: 'explore', component: () => import('../common/views/pages/explore.vue').then(m => m.default) }, { path: '/explore/tags/:tag', name: 'explore-tag', props: true, component: () => import('../common/views/pages/explore.vue').then(m => m.default) }, - { path: '/i/favorites', component: () => import('./views/home/favorites.vue').then(m => m.default) }, + { path: '/i/favorites', component: () => import('../common/views/pages/favorites.vue').then(m => m.default), props: { platform: 'desktop' } }, { path: '/i/pages', component: () => import('../common/views/pages/pages.vue').then(m => m.default) }, { path: '/i/lists', component: () => import('../common/views/pages/user-lists.vue').then(m => m.default) }, { path: '/i/lists/:listId', props: true, component: () => import('../common/views/pages/user-list-editor.vue').then(m => m.default) }, diff --git a/src/client/app/desktop/views/components/detail-notes.vue b/src/client/app/desktop/views/components/detail-notes.vue new file mode 100644 index 0000000000..e50dda7c6f --- /dev/null +++ b/src/client/app/desktop/views/components/detail-notes.vue @@ -0,0 +1,56 @@ +<template> +<div class="ecsvsegy" v-if="!fetching"> + <sequential-entrance animation="entranceFromTop" delay="25"> + <template v-for="note in notes"> + <mk-note-detail class="post" :note="note" :key="note.id"/> + </template> + </sequential-entrance> + <div class="more" v-if="more"> + <ui-button inline @click="fetchMore()">{{ $t('@.load-more') }}</ui-button> + </div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import i18n from '../../../i18n'; +import paging from '../../../common/scripts/paging'; + +export default Vue.extend({ + i18n: i18n(), + + mixins: [ + paging({ + captureWindowScroll: true, + }), + ], + + props: { + pagination: { + required: true + }, + extract: { + required: false + } + }, + + computed: { + notes() { + return this.extract ? this.extract(this.items) : this.items; + } + } +}); +</script> + +<style lang="stylus" scoped> +.ecsvsegy + margin 0 auto + + > * > .post + margin-bottom 16px + + > .more + margin 32px 16px 16px 16px + text-align center + +</style> diff --git a/src/client/app/desktop/views/components/index.ts b/src/client/app/desktop/views/components/index.ts index 76ab7b5fec..0cc44e1bbd 100644 --- a/src/client/app/desktop/views/components/index.ts +++ b/src/client/app/desktop/views/components/index.ts @@ -6,11 +6,9 @@ import note from './note.vue'; import notes from './notes.vue'; import subNoteContent from './sub-note-content.vue'; import window from './window.vue'; -import noteFormWindow from './post-form-window.vue'; import renoteFormWindow from './renote-form-window.vue'; import mediaVideo from './media-video.vue'; import notifications from './notifications.vue'; -import noteForm from './post-form.vue'; import renoteForm from './renote-form.vue'; import notePreview from './note-preview.vue'; import noteDetail from './note-detail.vue'; @@ -25,11 +23,9 @@ Vue.component('mk-note', note); Vue.component('mk-notes', notes); Vue.component('mk-sub-note-content', subNoteContent); Vue.component('mk-window', window); -Vue.component('mk-post-form-window', noteFormWindow); Vue.component('mk-renote-form-window', renoteFormWindow); Vue.component('mk-media-video', mediaVideo); Vue.component('mk-notifications', notifications); -Vue.component('mk-post-form', noteForm); Vue.component('mk-renote-form', renoteForm); Vue.component('mk-note-preview', notePreview); Vue.component('mk-note-detail', noteDetail); diff --git a/src/client/app/desktop/views/components/notes.vue b/src/client/app/desktop/views/components/notes.vue index 8a77a4560c..cb8b9c3ce3 100644 --- a/src/client/app/desktop/views/components/notes.vue +++ b/src/client/app/desktop/views/components/notes.vue @@ -4,9 +4,9 @@ <div class="newer-indicator" :style="{ top: $store.state.uiHeaderHeight + 'px' }" v-show="queue.length > 0"></div> - <div class="empty" v-if="notes.length == 0 && !fetching && inited">{{ $t('@.no-notes') }}</div> + <div class="empty" v-if="empty">{{ $t('@.no-notes') }}</div> - <mk-error v-if="!fetching && !inited" @retry="init()"/> + <mk-error v-if="error" @retry="init()"/> <div class="placeholder" v-if="fetching"> <template v-for="i in 10"> @@ -17,8 +17,8 @@ <!-- トランジションを有効にするとなぜかメモリリークする --> <component :is="!$store.state.device.reduceMotion ? 'transition-group' : 'div'" name="mk-notes" class="notes transition" tag="div" ref="notes"> <template v-for="(note, i) in _notes"> - <mk-note :note="note" :key="note.id" @update:note="onNoteUpdated(i, $event)" :compact="true" ref="note"/> - <p class="date" :key="note.id + '_date'" v-if="i != notes.length - 1 && note._date != _notes[i + 1]._date"> + <mk-note :note="note" :key="note.id" :compact="true" ref="note"/> + <p class="date" :key="note.id + '_date'" v-if="i != items.length - 1 && note._date != _notes[i + 1]._date"> <span><fa icon="angle-up"/>{{ note._datetext }}</span> <span><fa icon="angle-down"/>{{ _notes[i + 1]._datetext }}</span> </p> @@ -39,153 +39,66 @@ import Vue from 'vue'; import i18n from '../../../i18n'; import * as config from '../../../config'; import shouldMuteNote from '../../../common/scripts/should-mute-note'; - -const displayLimit = 30; +import paging from '../../../common/scripts/paging'; export default Vue.extend({ i18n: i18n(), + mixins: [ + paging({ + captureWindowScroll: true, + + onQueueChanged: (self, x) => { + if (x.length > 0) { + self.$store.commit('indicate', true); + } else { + self.$store.commit('indicate', false); + } + }, + + onPrepend: (self, note, silent) => { + // 弾く + if (shouldMuteNote(self.$store.state.i, self.$store.state.settings, note)) return false; + + // タブが非表示またはスクロール位置が最上部ではないならタイトルで通知 + if (document.hidden || !self.isScrollTop()) { + self.$store.commit('pushBehindNote', note); + } + + if (self.isScrollTop()) { + // サウンドを再生する + if (self.$store.state.device.enableSounds && !silent) { + const sound = new Audio(`${config.url}/assets/post.mp3`); + sound.volume = self.$store.state.device.soundVolume; + sound.play(); + } + } + } + }), + ], + props: { - makePromise: { + pagination: { required: true - } - }, - - data() { - return { - notes: [], - queue: [], - fetching: true, - moreFetching: false, - inited: false, - more: false - }; + }, }, computed: { _notes(): any[] { - return (this.notes as any).map(note => { - const date = new Date(note.createdAt).getDate(); - const month = new Date(note.createdAt).getMonth() + 1; - note._date = date; - note._datetext = this.$t('@.month-and-day').replace('{month}', month.toString()).replace('{day}', date.toString()); - return note; + return (this.items as any).map(item => { + const date = new Date(item.createdAt).getDate(); + const month = new Date(item.createdAt).getMonth() + 1; + item._date = date; + item._datetext = this.$t('@.month-and-day').replace('{month}', month.toString()).replace('{day}', date.toString()); + return item; }); } }, - created() { - this.init(); - }, - - mounted() { - window.addEventListener('scroll', this.onScroll, { passive: true }); - }, - - beforeDestroy() { - window.removeEventListener('scroll', this.onScroll); - }, - methods: { - isScrollTop() { - return window.scrollY <= 8; - }, - focus() { (this.$refs.notes as any).children[0].focus ? (this.$refs.notes as any).children[0].focus() : (this.$refs.notes as any).$el.children[0].focus(); }, - - onNoteUpdated(i, note) { - Vue.set((this as any).notes, i, note); - }, - - reload() { - this.queue = []; - this.notes = []; - this.init(); - }, - - async init() { - this.fetching = true; - await (this.makePromise()).then(x => { - if (Array.isArray(x)) { - this.notes = x; - } else { - this.notes = x.notes; - this.more = x.more; - } - this.inited = true; - this.fetching = false; - this.$emit('inited'); - }, e => { - this.fetching = false; - }); - }, - - async fetchMore() { - if (!this.more || this.moreFetching || this.notes.length === 0) return; - this.moreFetching = true; - this.makePromise(this.notes[this.notes.length - 1].id).then(x => { - this.notes = this.notes.concat(x.notes); - this.more = x.more; - this.moreFetching = false; - }, e => { - this.moreFetching = false; - }); - }, - - prepend(note, silent = false) { - // 弾く - if (shouldMuteNote(this.$store.state.i, this.$store.state.settings, note)) return; - - // タブが非表示またはスクロール位置が最上部ではないならタイトルで通知 - if (document.hidden || !this.isScrollTop()) { - this.$store.commit('pushBehindNote', note); - } - - if (this.isScrollTop()) { - // Prepend the note - this.notes.unshift(note); - - // サウンドを再生する - if (this.$store.state.device.enableSounds && !silent) { - const sound = new Audio(`${config.url}/assets/post.mp3`); - sound.volume = this.$store.state.device.soundVolume; - sound.play(); - } - - // オーバーフローしたら古い投稿は捨てる - if (this.notes.length >= displayLimit) { - this.notes = this.notes.slice(0, displayLimit); - this.more = true; - } - } else { - this.queue.push(note); - } - }, - - append(note) { - this.notes.push(note); - this.cursor = this.notes[this.notes.length - 1].id - }, - - releaseQueue() { - for (const n of this.queue) { - this.prepend(n, true); - } - this.queue = []; - }, - - onScroll() { - if (this.isScrollTop()) { - this.releaseQueue(); - } - - if (this.$store.state.settings.fetchOnScroll) { - const current = window.scrollY + window.innerHeight; - if (current > document.body.offsetHeight - 8) this.fetchMore(); - } - } } }); </script> diff --git a/src/client/app/desktop/views/components/notifications.vue b/src/client/app/desktop/views/components/notifications.vue index 0bf0132926..9ca06f4118 100644 --- a/src/client/app/desktop/views/components/notifications.vue +++ b/src/client/app/desktop/views/components/notifications.vue @@ -6,7 +6,7 @@ </template> </div> - <div class="notifications" v-if="notifications.length != 0"> + <div class="notifications" v-if="!empty"> <!-- トランジションを有効にするとなぜかメモリリークする --> <component :is="!$store.state.device.reduceMotion ? 'transition-group' : 'div'" name="mk-notifications" class="transition" tag="div"> <template v-for="(notification, i) in _notifications"> @@ -125,17 +125,18 @@ </template> </div> - <p class="date" v-if="i != notifications.length - 1 && notification._date != _notifications[i + 1]._date" :key="notification.id + '-time'"> + <p class="date" v-if="i != items.length - 1 && notification._date != _notifications[i + 1]._date" :key="notification.id + '-time'"> <span><fa icon="angle-up"/>{{ notification._datetext }}</span> <span><fa icon="angle-down"/>{{ _notifications[i + 1]._datetext }}</span> </p> </template> </component> </div> - <button class="more" :class="{ fetching: fetchingMoreNotifications }" v-if="moreNotifications" @click="fetchMoreNotifications" :disabled="fetchingMoreNotifications"> - <template v-if="fetchingMoreNotifications"><fa icon="spinner" pulse fixed-width/></template>{{ fetchingMoreNotifications ? $t('@.loading') : $t('@.load-more') }} + <button class="more" :class="{ fetching: moreFetching }" v-if="more" @click="fetchMore" :disabled="moreFetching"> + <template v-if="moreFetching"><fa icon="spinner" pulse fixed-width/></template>{{ moreFetching ? $t('@.loading') : $t('@.load-more') }} </button> - <p class="empty" v-if="notifications.length == 0 && !fetching">{{ $t('empty') }}</p> + <p class="empty" v-if="empty">{{ $t('empty') }}</p> + <mk-error v-if="error" @retry="init()"/> </div> </template> @@ -143,23 +144,29 @@ import Vue from 'vue'; import i18n from '../../../i18n'; import getNoteSummary from '../../../../../misc/get-note-summary'; +import paging from '../../../common/scripts/paging'; export default Vue.extend({ i18n: i18n(), + + mixins: [ + paging({}), + ], + data() { return { - fetching: true, - fetchingMoreNotifications: false, - notifications: [], - moreNotifications: false, connection: null, - getNoteSummary + getNoteSummary, + pagination: { + endpoint: 'i/notifications', + limit: 10, + } }; }, computed: { _notifications(): any[] { - return (this.notifications as any).map(notification => { + return (this.items as any).map(notification => { const date = new Date(notification.createdAt).getDate(); const month = new Date(notification.createdAt).getMonth() + 1; notification._date = date; @@ -171,22 +178,7 @@ export default Vue.extend({ mounted() { this.connection = this.$root.stream.useSharedConnection('main'); - this.connection.on('notification', this.onNotification); - - const max = 10; - - this.$root.api('i/notifications', { - limit: max + 1 - }).then(notifications => { - if (notifications.length == max + 1) { - this.moreNotifications = true; - notifications.pop(); - } - - this.notifications = notifications; - this.fetching = false; - }); }, beforeDestroy() { @@ -194,33 +186,13 @@ export default Vue.extend({ }, methods: { - fetchMoreNotifications() { - this.fetchingMoreNotifications = true; - - const max = 30; - - this.$root.api('i/notifications', { - limit: max + 1, - untilId: this.notifications[this.notifications.length - 1].id - }).then(notifications => { - if (notifications.length == max + 1) { - this.moreNotifications = true; - notifications.pop(); - } else { - this.moreNotifications = false; - } - this.notifications = this.notifications.concat(notifications); - this.fetchingMoreNotifications = false; - }); - }, - onNotification(notification) { // TODO: ユーザーが画面を見てないと思われるとき(ブラウザやタブがアクティブじゃないなど)は送信しない this.$root.stream.send('readNotification', { id: notification.id }); - this.notifications.unshift(notification); + this.prepend(notification); } } }); diff --git a/src/client/app/desktop/views/components/post-form-window.vue b/src/client/app/desktop/views/components/post-form-window.vue index 1d718bef15..ae5f0af71d 100644 --- a/src/client/app/desktop/views/components/post-form-window.vue +++ b/src/client/app/desktop/views/components/post-form-window.vue @@ -12,7 +12,7 @@ <div class="mk-post-form-window--body"> <mk-note-preview v-if="reply" class="notePreview" :note="reply"/> - <mk-post-form ref="form" + <x-post-form ref="form" :reply="reply" :mention="mention" @posted="onPosted" @@ -27,9 +27,15 @@ <script lang="ts"> import Vue from 'vue'; import i18n from '../../../i18n'; +import XPostForm from './post-form.vue'; export default Vue.extend({ i18n: i18n('desktop/views/components/post-form-window.vue'), + + components: { + XPostForm + }, + props: { reply: { type: Object, diff --git a/src/client/app/desktop/views/components/post-form.vue b/src/client/app/desktop/views/components/post-form.vue index 0307ff305e..64652e0eb9 100644 --- a/src/client/app/desktop/views/components/post-form.vue +++ b/src/client/app/desktop/views/components/post-form.vue @@ -1,5 +1,5 @@ <template> -<div class="mk-post-form" +<div class="gjisdzwh" @dragover.stop="onDragover" @dragenter="onDragenter" @dragleave="onDragleave" @@ -28,7 +28,7 @@ <fa :icon="['far', 'laugh']"/> </button> <x-post-form-attaches class="files" :class="{ with: poll }" :files="files"/> - <mk-poll-editor v-if="poll" ref="poll" @destroyed="poll = false" @updated="onPollUpdate()"/> + <x-poll-editor class="poll-editor" v-if="poll" ref="poll" @destroyed="poll = false" @updated="onPollUpdate()"/> </div> </div> <mk-uploader ref="uploader" @uploaded="attachMedia" @change="onChangeUploadings"/> @@ -72,7 +72,8 @@ export default Vue.extend({ components: { MkVisibilityChooser, - XPostFormAttaches + XPostFormAttaches, + XPollEditor: () => import('../../../common/views/components/poll-editor.vue').then(m => m.default) }, props: { @@ -518,7 +519,7 @@ export default Vue.extend({ </script> <style lang="stylus" scoped> -.mk-post-form +.gjisdzwh display block padding 16px background var(--desktopPostFormBg) @@ -617,7 +618,7 @@ export default Vue.extend({ border-bottom solid 1px var(--primaryAlpha01) !important border-radius 0 - > .mk-poll-editor + > .poll-editor background var(--desktopPostFormTextareaBg) border solid 1px var(--primaryAlpha01) border-top none diff --git a/src/client/app/desktop/views/components/renote-form.vue b/src/client/app/desktop/views/components/renote-form.vue index d8f38bedfc..53fbf0ff30 100644 --- a/src/client/app/desktop/views/components/renote-form.vue +++ b/src/client/app/desktop/views/components/renote-form.vue @@ -10,7 +10,7 @@ </footer> </template> <template v-if="quote"> - <mk-post-form ref="form" :renote="note" @posted="onChildFormPosted"/> + <x-post-form ref="form" :renote="note" @posted="onChildFormPosted"/> </template> </div> </template> @@ -22,6 +22,10 @@ import i18n from '../../../i18n'; export default Vue.extend({ i18n: i18n('desktop/views/components/renote-form.vue'), + components: { + XPostForm: () => import('./post-form.vue').then(m => m.default) + }, + props: { note: { type: Object, 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 4437b838f2..dae282ec5c 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,6 @@ <template> <div> - <mk-notes ref="timeline" :make-promise="makePromise" @inited="() => $emit('loaded')"> + <mk-notes ref="timeline" :pagination="pagination" @inited="() => $emit('loaded')"> <template #header> <slot></slot> </template> @@ -11,36 +11,23 @@ <script lang="ts"> import Vue from 'vue'; -const fetchLimit = 10; - export default Vue.extend({ props: ['list'], data() { return { connection: null, date: null, - makePromise: cursor => this.$root.api('notes/user-list-timeline', { - listId: this.list.id, - limit: fetchLimit + 1, - untilId: cursor ? cursor : undefined, - untilDate: cursor ? undefined : (this.date ? this.date.getTime() : 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, - more: true - }; - } else { - return { - notes: notes, - more: false - }; - } - }) + pagination: { + endpoint: 'notes/user-list-timeline', + limit: 10, + params: init => ({ + listId: this.list.id, + untilDate: init ? undefined : (this.date ? this.date.getTime() : undefined), + includeMyRenotes: this.$store.state.settings.showMyRenotes, + includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, + includeLocalRenotes: this.$store.state.settings.showLocalRenotes + }) + } }; }, watch: { diff --git a/src/client/app/desktop/views/home/favorites.vue b/src/client/app/desktop/views/home/favorites.vue deleted file mode 100644 index 951de97498..0000000000 --- a/src/client/app/desktop/views/home/favorites.vue +++ /dev/null @@ -1,83 +0,0 @@ -<template> -<div class="ecsvsegy" v-if="!fetching"> - <sequential-entrance animation="entranceFromTop" delay="25"> - <template v-for="favorite in favorites"> - <mk-note-detail class="post" :note="favorite.note" :key="favorite.note.id"/> - </template> - </sequential-entrance> - <div class="more" v-if="existMore"> - <ui-button inline @click="fetchMore()">{{ $t('@.load-more') }}</ui-button> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import Progress from '../../../common/scripts/loading'; - -export default Vue.extend({ - i18n: i18n('.vue'), - data() { - return { - fetching: true, - favorites: [], - existMore: false, - moreFetching: false - }; - }, - created() { - this.fetch(); - }, - methods: { - fetch() { - Progress.start(); - this.fetching = true; - - this.$root.api('i/favorites', { - limit: 11 - }).then(favorites => { - if (favorites.length == 11) { - this.existMore = true; - favorites.pop(); - } - - this.favorites = favorites; - this.fetching = false; - - Progress.done(); - }); - }, - fetchMore() { - this.moreFetching = true; - this.$root.api('i/favorites', { - limit: 11, - untilId: this.favorites[this.favorites.length - 1].id - }).then(favorites => { - if (favorites.length == 11) { - this.existMore = true; - favorites.pop(); - } else { - this.existMore = false; - } - - this.favorites = this.favorites.concat(favorites); - this.moreFetching = false; - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.ecsvsegy - margin 0 auto - - > * > .post - margin-bottom 16px - - > .more - margin 32px 16px 16px 16px - text-align center - -</style> diff --git a/src/client/app/desktop/views/home/featured.vue b/src/client/app/desktop/views/home/featured.vue deleted file mode 100644 index 1719023289..0000000000 --- a/src/client/app/desktop/views/home/featured.vue +++ /dev/null @@ -1,55 +0,0 @@ -<template> -<div class="glowckho" v-if="!fetching"> - <sequential-entrance animation="entranceFromTop" delay="25"> - <template v-for="note in notes"> - <mk-note-detail class="post" :note="note" :key="note.id"/> - </template> - </sequential-entrance> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import Progress from '../../../common/scripts/loading'; - -export default Vue.extend({ - data() { - return { - fetching: true, - notes: [], - }; - }, - created() { - this.fetch(); - }, - methods: { - fetch() { - Progress.start(); - this.fetching = true; - - this.$root.api('notes/featured', { - limit: 30 - }).then(notes => { - notes.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); - this.notes = notes; - this.fetching = false; - - Progress.done(); - }); - }, - } -}); -</script> - -<style lang="stylus" scoped> -.glowckho - margin 0 auto - - > * > .post - margin-bottom 16px - - > .more - margin 32px 16px 16px 16px - text-align center - -</style> diff --git a/src/client/app/desktop/views/home/search.vue b/src/client/app/desktop/views/home/search.vue index 50c6456158..06b354b133 100644 --- a/src/client/app/desktop/views/home/search.vue +++ b/src/client/app/desktop/views/home/search.vue @@ -1,6 +1,6 @@ <template> <div> - <mk-notes ref="timeline" :make-promise="makePromise" @inited="inited"> + <mk-notes ref="timeline" :pagination="pagination" @inited="inited"> <template #header> <header class="oxgbmvii"> <span><fa icon="search"/> {{ q }}</span> @@ -16,30 +16,15 @@ import i18n from '../../../i18n'; import Progress from '../../../common/scripts/loading'; import { genSearchQuery } from '../../../common/scripts/gen-search-query'; -const limit = 20; - export default Vue.extend({ i18n: i18n('desktop/views/pages/search.vue'), data() { return { - makePromise: async cursor => this.$root.api('notes/search', { - limit: limit + 1, - offset: cursor ? cursor : undefined, - ...(await genSearchQuery(this, this.q)) - }).then(notes => { - if (notes.length == limit + 1) { - notes.pop(); - return { - notes: notes, - cursor: cursor ? cursor + limit : limit - }; - } else { - return { - notes: notes, - more: false - }; - } - }) + pagination: { + endpoint: 'notes/search', + limit: 20, + params: () => genSearchQuery(this, this.q) + } }; }, computed: { diff --git a/src/client/app/desktop/views/home/tag.vue b/src/client/app/desktop/views/home/tag.vue index 92f5a47528..b87dc1255d 100644 --- a/src/client/app/desktop/views/home/tag.vue +++ b/src/client/app/desktop/views/home/tag.vue @@ -1,6 +1,6 @@ <template> <div> - <mk-notes ref="timeline" :make-promise="makePromise" @inited="inited"> + <mk-notes ref="timeline" :pagination="pagination" @inited="inited"> <template #header> <header class="wqraeznr"> <span><fa icon="hashtag"/> {{ $route.params.tag }}</span> @@ -15,30 +15,17 @@ import Vue from 'vue'; import i18n from '../../../i18n'; import Progress from '../../../common/scripts/loading'; -const limit = 20; - export default Vue.extend({ i18n: i18n('desktop/views/pages/tag.vue'), data() { return { - 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, - more: false - }; + pagination: { + endpoint: 'notes/search-by-tag', + limit: 20, + params: { + tag: this.$route.params.tag } - }) + } }; }, watch: { diff --git a/src/client/app/desktop/views/home/timeline.core.vue b/src/client/app/desktop/views/home/timeline.core.vue index 654a1cc434..aae7dbc60e 100644 --- a/src/client/app/desktop/views/home/timeline.core.vue +++ b/src/client/app/desktop/views/home/timeline.core.vue @@ -1,6 +1,6 @@ <template> <div> - <mk-notes ref="timeline" :make-promise="makePromise" @inited="() => $emit('loaded')"> + <mk-notes ref="timeline" :pagination="pagination" @inited="() => $emit('loaded')"> <template #header> <slot></slot> <div v-if="src == 'home' && alone" class="ibpylqas"> @@ -16,8 +16,6 @@ import Vue from 'vue'; import i18n from '../../../i18n'; -const fetchLimit = 10; - export default Vue.extend({ i18n: i18n('desktop/views/components/timeline.core.vue'), @@ -42,7 +40,7 @@ export default Vue.extend({ }, query: {}, endpoint: null, - makePromise: null + pagination: null }; }, @@ -109,25 +107,14 @@ export default Vue.extend({ this.connection.on('mention', onNote); } - 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, - more: true - }; - } else { - return { - notes: notes, - more: false - }; - } - }); + this.pagination = { + endpoint: this.endpoint, + limit: 10, + params: init => ({ + untilDate: init ? undefined : (this.date ? this.date.getTime() : undefined), + ...this.baseQuery, ...this.query + }) + }; }, methods: { diff --git a/src/client/app/desktop/views/home/timeline.vue b/src/client/app/desktop/views/home/timeline.vue index 3f9681a047..d8da6e4e26 100644 --- a/src/client/app/desktop/views/home/timeline.vue +++ b/src/client/app/desktop/views/home/timeline.vue @@ -1,6 +1,6 @@ <template> <div class="pwbzawku"> - <mk-post-form class="form" :class="{ shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners }" v-if="$store.state.settings.showPostFormOnTopOfTl"/> + <x-post-form class="form" :class="{ shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners }" v-if="$store.state.settings.showPostFormOnTopOfTl"/> <div class="main"> <component :is="src == 'list' ? 'mk-user-list-timeline' : 'x-core'" ref="tl" v-bind="options"> <header class="zahtxcqi"> @@ -31,8 +31,10 @@ import MkSettingsWindow from '../components/settings-window.vue'; export default Vue.extend({ i18n: i18n('desktop/views/components/timeline.vue'), + components: { - XCore + XCore, + XPostForm: () => import('../components/post-form.vue').then(m => m.default) }, data() { 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 0435d67dc7..2a97f2c96e 100644 --- a/src/client/app/desktop/views/home/user/user.timeline.vue +++ b/src/client/app/desktop/views/home/user/user.timeline.vue @@ -1,8 +1,8 @@ <template> <div> - <mk-notes ref="timeline" :make-promise="makePromise" @inited="() => $emit('loaded')"> + <mk-notes ref="timeline" :pagination="pagination" @inited="() => $emit('loaded')"> <template #header> - <header class="oh5y2r7l5lx8j6jj791ykeiwgihheguk"> + <header class="kugajpep"> <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> @@ -17,8 +17,6 @@ import Vue from 'vue'; import i18n from '../../../../i18n'; -const fetchLimit = 10; - export default Vue.extend({ i18n: i18n('desktop/views/pages/user/user.timeline.vue'), @@ -30,28 +28,17 @@ export default Vue.extend({ mode: 'default', unreadCount: 0, 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', - untilDate: cursor ? undefined : (this.date ? this.date.getTime() : undefined), - untilId: cursor ? cursor : undefined - }).then(notes => { - if (notes.length == fetchLimit + 1) { - notes.pop(); - return { - notes: notes, - more: true - }; - } else { - return { - notes: notes, - more: false - }; - } - }) + pagination: { + endpoint: 'users/notes', + limit: 10, + params: init => ({ + userId: this.user.id, + untilDate: init ? undefined : (this.date ? this.date.getTime() : undefined), + includeReplies: this.mode == 'with-replies', + includeMyRenotes: this.mode != 'my-posts', + withFiles: this.mode == 'with-media', + }) + } }; }, @@ -88,7 +75,7 @@ export default Vue.extend({ </script> <style lang="stylus" scoped> -.oh5y2r7l5lx8j6jj791ykeiwgihheguk +.kugajpep padding 0 8px z-index 10 background var(--faceHeader) diff --git a/src/client/app/init.ts b/src/client/app/init.ts index 230d8c3f1e..da7baff4fe 100644 --- a/src/client/app/init.ts +++ b/src/client/app/init.ts @@ -456,6 +456,18 @@ export default (callback: (launch: (router: VueRouter) => [Vue, MiOS], os: MiOS) document.body.appendChild(x.$el); return x; }, + newAsync(vm, props) { + return new Promise((res) => { + vm().then(vm => { + const x = new vm({ + parent: this, + propsData: props + }).$mount(); + document.body.appendChild(x.$el); + res(x); + }); + }); + }, dialog(opts) { const vm = this.new(Dialog, opts); const p: any = new Promise((res) => { diff --git a/src/client/app/mobile/script.ts b/src/client/app/mobile/script.ts index 360da01496..d33bafbb0f 100644 --- a/src/client/app/mobile/script.ts +++ b/src/client/app/mobile/script.ts @@ -11,16 +11,13 @@ import './style.styl'; import init from '../init'; import MkIndex from './views/pages/index.vue'; -import MkDeck from '../common/views/deck/deck.vue'; import MkSignup from './views/pages/signup.vue'; import MkSelectDrive from './views/pages/selectdrive.vue'; import MkDrive from './views/pages/drive.vue'; -import MkWidgets from './views/pages/widgets.vue'; import MkMessaging from './views/pages/messaging.vue'; import MkMessagingRoom from './views/pages/messaging-room.vue'; import MkNote from './views/pages/note.vue'; import MkSearch from './views/pages/search.vue'; -import MkFavorites from './views/pages/favorites.vue'; import UI from './views/pages/ui.vue'; import MkReversi from './views/pages/games/reversi.vue'; import MkTag from './views/pages/tag.vue'; @@ -29,7 +26,6 @@ import MkFollow from '../common/views/pages/follow.vue'; import MkNotFound from '../common/views/pages/not-found.vue'; import DeckColumn from '../common/views/deck/deck.column-template.vue'; -import PostForm from './views/components/post-form-dialog.vue'; import FileChooser from './views/components/drive-file-chooser.vue'; import FolderChooser from './views/components/drive-folder-chooser.vue'; @@ -54,16 +50,16 @@ init((launch, os) => { document.documentElement.style.overflow = 'auto'; } - const vm = this.$root.new(PostForm, { + this.$root.newAsync(() => import('./views/components/post-form-dialog.vue').then(m => m.default), { reply: o.reply, mention: o.mention, renote: o.renote + }).then(vm => { + vm.$once('cancel', recover); + vm.$once('posted', recover); + if (o.cb) vm.$once('closed', o.cb); + (vm as any).focus(); }); - - vm.$once('cancel', recover); - vm.$once('posted', recover); - if (o.cb) vm.$once('closed', o.cb); - (vm as any).focus(); }, $chooseDriveFile(opts) { @@ -114,7 +110,7 @@ init((launch, os) => { mode: 'history', routes: [ ...(os.store.state.device.inDeckMode - ? [{ path: '/', name: 'index', component: MkDeck, children: [ + ? [{ path: '/', name: 'index', component: () => import('../common/views/deck/deck.vue').then(m => m.default), children: [ { path: '/@:user', component: () => import('../common/views/deck/deck.user-column.vue').then(m => m.default), children: [ { path: '', name: 'user', component: () => import('../common/views/deck/deck.user-column.home.vue').then(m => m.default) }, { path: 'following', component: () => import('../common/views/pages/following.vue').then(m => m.default) }, @@ -123,10 +119,10 @@ init((launch, os) => { { path: '/notes/:note', name: 'note', component: () => import('../common/views/deck/deck.note-column.vue').then(m => m.default) }, { path: '/search', component: () => import('../common/views/deck/deck.search-column.vue').then(m => m.default) }, { path: '/tags/:tag', name: 'tag', component: () => import('../common/views/deck/deck.hashtag-column.vue').then(m => m.default) }, - { path: '/featured', name: 'featured', component: () => import('../common/views/deck/deck.featured-column.vue').then(m => m.default) }, + { path: '/featured', name: 'featured', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/featured.vue').then(m => m.default), platform: 'deck' }) }, { path: '/explore', name: 'explore', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/explore.vue').then(m => m.default) }) }, { path: '/explore/tags/:tag', name: 'explore-tag', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/explore.vue').then(m => m.default), tag: route.params.tag }) }, - { path: '/i/favorites', component: () => import('../common/views/deck/deck.favorites-column.vue').then(m => m.default) }, + { path: '/i/favorites', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/favorites.vue').then(m => m.default), platform: 'deck' }) }, { path: '/i/pages', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/pages.vue').then(m => m.default) }) }, { path: '/i/lists', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/user-lists.vue').then(m => m.default) }) }, { path: '/i/lists/:listId', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/user-list-editor.vue').then(m => m.default), listId: route.params.listId }) }, @@ -138,14 +134,14 @@ init((launch, os) => { ]), { path: '/signup', name: 'signup', component: MkSignup }, { path: '/i/settings', name: 'settings', component: () => import('./views/pages/settings.vue').then(m => m.default) }, - { path: '/i/favorites', name: 'favorites', component: MkFavorites }, + { path: '/i/favorites', name: 'favorites', component: UI, props: route => ({ component: () => import('../common/views/pages/favorites.vue').then(m => m.default), platform: 'mobile' }) }, { path: '/i/pages', name: 'pages', component: UI, props: route => ({ component: () => import('../common/views/pages/pages.vue').then(m => m.default) }) }, { path: '/i/lists', name: 'user-lists', component: UI, props: route => ({ component: () => import('../common/views/pages/user-lists.vue').then(m => m.default) }) }, { path: '/i/lists/:list', component: UI, props: route => ({ component: () => import('../common/views/pages/user-list-editor.vue').then(m => m.default), listId: route.params.list }) }, { path: '/i/groups', name: 'user-groups', component: UI, props: route => ({ component: () => import('../common/views/pages/user-groups.vue').then(m => m.default) }) }, { path: '/i/groups/:group', component: UI, props: route => ({ component: () => import('../common/views/pages/user-group-editor.vue').then(m => m.default), groupId: route.params.group }) }, { path: '/i/follow-requests', name: 'follow-requests', component: UI, props: route => ({ component: () => import('../common/views/pages/follow-requests.vue').then(m => m.default) }) }, - { path: '/i/widgets', name: 'widgets', component: MkWidgets }, + { path: '/i/widgets', name: 'widgets', component: () => import('./views/pages/widgets.vue').then(m => m.default) }, { path: '/i/messaging', name: 'messaging', component: MkMessaging }, { path: '/i/messaging/group/:group', component: MkMessagingRoom }, { path: '/i/messaging/:user', component: MkMessagingRoom }, @@ -157,7 +153,7 @@ init((launch, os) => { { path: '/selectdrive', component: MkSelectDrive }, { path: '/search', component: MkSearch }, { path: '/tags/:tag', component: MkTag }, - { path: '/featured', name: 'featured', component: () => import('./views/pages/featured.vue').then(m => m.default) }, + { path: '/featured', name: 'featured', component: UI, props: route => ({ component: () => import('../common/views/pages/featured.vue').then(m => m.default), platform: 'mobile' }) }, { path: '/explore', name: 'explore', component: UI, props: route => ({ component: () => import('../common/views/pages/explore.vue').then(m => m.default) }) }, { path: '/explore/tags/:tag', name: 'explore-tag', component: UI, props: route => ({ component: () => import('../common/views/pages/explore.vue').then(m => m.default), tag: route.params.tag }) }, { path: '/share', component: MkShare }, diff --git a/src/client/app/mobile/views/components/detail-notes.vue b/src/client/app/mobile/views/components/detail-notes.vue new file mode 100644 index 0000000000..bab7949534 --- /dev/null +++ b/src/client/app/mobile/views/components/detail-notes.vue @@ -0,0 +1,52 @@ +<template> +<div class="fdcvngpy"> + <sequential-entrance animation="entranceFromTop" delay="25"> + <template v-for="note in notes"> + <mk-note-detail class="post" :note="note" :key="note.id"/> + </template> + </sequential-entrance> + <ui-button v-if="more" @click="fetchMore()">{{ $t('@.load-more') }}</ui-button> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import i18n from '../../../i18n'; +import paging from '../../../common/scripts/paging'; + +export default Vue.extend({ + i18n: i18n(), + + mixins: [ + paging({ + captureWindowScroll: true, + }), + ], + + props: { + pagination: { + required: true + }, + extract: { + required: false + } + }, + + computed: { + notes() { + return this.extract ? this.extract(this.items) : this.items; + } + } +}); +</script> + +<style lang="stylus" scoped> +.fdcvngpy + > * > .post + margin-bottom 8px + + @media (min-width 500px) + > * > .post + margin-bottom 16px + +</style> diff --git a/src/client/app/mobile/views/components/index.ts b/src/client/app/mobile/views/components/index.ts index 4df347ef4c..4e10d80f92 100644 --- a/src/client/app/mobile/views/components/index.ts +++ b/src/client/app/mobile/views/components/index.ts @@ -14,7 +14,6 @@ import notificationPreview from './notification-preview.vue'; import userTimeline from './user-timeline.vue'; import userListTimeline from './user-list-timeline.vue'; import uiContainer from './ui-container.vue'; -import postForm from './post-form.vue'; Vue.component('mk-ui', ui); Vue.component('mk-note', note); @@ -30,4 +29,3 @@ Vue.component('mk-notification-preview', notificationPreview); Vue.component('mk-user-timeline', userTimeline); Vue.component('mk-user-list-timeline', userListTimeline); Vue.component('ui-container', uiContainer); -Vue.component('mk-post-form', postForm); diff --git a/src/client/app/mobile/views/components/notes.vue b/src/client/app/mobile/views/components/notes.vue index 6cba1f4e28..1a0cd5cc24 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="ivaojijs" :class="{ shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners }"> - <div class="empty" v-if="notes.length == 0 && !fetching && inited">{{ $t('@.no-notes') }}</div> + <div class="empty" v-if="empty">{{ $t('@.no-notes') }}</div> - <mk-error v-if="!fetching && !inited" @retry="init()"/> + <mk-error v-if="error" @retry="init()"/> <div class="placeholder" v-if="fetching"> <template v-for="i in 10"> @@ -13,8 +13,8 @@ <!-- トランジションを有効にするとなぜかメモリリークする --> <component :is="!$store.state.device.reduceMotion ? 'transition-group' : 'div'" name="mk-notes" class="transition" tag="div"> <template v-for="(note, i) in _notes"> - <mk-note :note="note" :key="note.id" @update:note="onNoteUpdated(i, $event)"/> - <p class="date" :key="note.id + '_date'" v-if="i != notes.length - 1 && note._date != _notes[i + 1]._date"> + <mk-note :note="note" :key="note.id"/> + <p class="date" :key="note.id + '_date'" v-if="i != items.length - 1 && note._date != _notes[i + 1]._date"> <span><fa icon="angle-up"/>{{ note._datetext }}</span> <span><fa icon="angle-down"/>{{ _notes[i + 1]._datetext }}</span> </p> @@ -34,158 +34,56 @@ import Vue from 'vue'; import i18n from '../../../i18n'; import shouldMuteNote from '../../../common/scripts/should-mute-note'; - -const displayLimit = 30; +import paging from '../../../common/scripts/paging'; export default Vue.extend({ i18n: i18n(), - props: { - makePromise: { - required: true - } - }, - - data() { - return { - notes: [], - queue: [], - fetching: true, - moreFetching: false, - inited: false, - more: false - }; - }, - - computed: { - _notes(): any[] { - return (this.notes as any).map(note => { - const date = new Date(note.createdAt).getDate(); - const month = new Date(note.createdAt).getMonth() + 1; - note._date = date; - note._datetext = this.$t('@.month-and-day').replace('{month}', month.toString()).replace('{day}', date.toString()); - return note; - }); - } - }, - - watch: { - queue(x) { - if (x.length > 0) { - this.$store.commit('indicate', true); - } else { - this.$store.commit('indicate', false); - } - } - }, - - created() { - this.init(); - }, - - mounted() { - window.addEventListener('scroll', this.onScroll, { passive: true }); - }, - - beforeDestroy() { - window.removeEventListener('scroll', this.onScroll); - }, - - methods: { - isScrollTop() { - return window.scrollY <= 8; - }, - - onNoteUpdated(i, note) { - Vue.set((this as any).notes, i, note); - }, + mixins: [ + paging({ + captureWindowScroll: true, - reload() { - this.queue = []; - this.notes = []; - this.init(); - }, - - async init() { - this.fetching = true; - await (this.makePromise()).then(x => { - if (Array.isArray(x)) { - this.notes = x; + onQueueChanged: (self, x) => { + if (x.length > 0) { + self.$store.commit('indicate', true); } else { - this.notes = x.notes; - this.more = x.more; + self.$store.commit('indicate', false); } - this.inited = true; - this.fetching = false; - this.$emit('inited'); - }, e => { - this.fetching = false; - }); - }, - - async fetchMore() { - if (!this.more || this.moreFetching || this.notes.length === 0) return; - this.moreFetching = true; - await (this.makePromise(this.notes[this.notes.length - 1].id)).then(x => { - this.notes = this.notes.concat(x.notes); - this.more = x.more; - this.moreFetching = false; - }, e => { - this.moreFetching = false; - }); - }, + }, - prepend(note, silent = false) { - // 弾く - if (shouldMuteNote(this.$store.state.i, this.$store.state.settings, note)) return; + onPrepend: (self, note) => { + // 弾く + if (shouldMuteNote(self.$store.state.i, self.$store.state.settings, note)) return false; - // タブが非表示またはスクロール位置が最上部ではないならタイトルで通知 - if (document.hidden || !this.isScrollTop()) { - this.$store.commit('pushBehindNote', note); - } - - if (this.isScrollTop()) { - // Prepend the note - this.notes.unshift(note); - - // オーバーフローしたら古い投稿は捨てる - if (this.notes.length >= displayLimit) { - this.notes = this.notes.slice(0, displayLimit); - this.more = true; + // タブが非表示またはスクロール位置が最上部ではないならタイトルで通知 + if (document.hidden || !self.isScrollTop()) { + self.$store.commit('pushBehindNote', note); } - } else { - this.queue.push(note); - } - }, - - append(note) { - this.notes.push(note); - this.cursor = this.notes[this.notes.length - 1].id - }, - - releaseQueue() { - for (const n of this.queue) { - this.prepend(n, true); - } - this.queue = []; - }, + }, - onScroll() { - if (this.isScrollTop()) { - this.releaseQueue(); + onInited: (self) => { + self.$emit('loaded'); } + }), + ], - if (this.$store.state.settings.fetchOnScroll) { - // 親要素が display none だったら弾く - // https://github.com/syuilo/misskey/issues/1569 - // http://d.hatena.ne.jp/favril/20091105/1257403319 - if (this.$el.offsetHeight == 0) return; + props: { + pagination: { + required: true + }, + }, - const current = window.scrollY + window.innerHeight; - if (current > document.body.offsetHeight - 8) this.fetchMore(); - } + computed: { + _notes(): any[] { + return (this.items as any).map(item => { + const date = new Date(item.createdAt).getDate(); + const month = new Date(item.createdAt).getMonth() + 1; + item._date = date; + item._datetext = this.$t('@.month-and-day').replace('{month}', month.toString()).replace('{day}', date.toString()); + return item; + }); } - } + }, }); </script> diff --git a/src/client/app/mobile/views/components/notification.vue b/src/client/app/mobile/views/components/notification.vue index 1128a76000..62df76cba5 100644 --- a/src/client/app/mobile/views/components/notification.vue +++ b/src/client/app/mobile/views/components/notification.vue @@ -71,15 +71,15 @@ </div> <template v-if="notification.type == 'quote'"> - <mk-note :note="notification.note" @update:note="onNoteUpdated"/> + <mk-note :note="notification.note"/> </template> <template v-if="notification.type == 'reply'"> - <mk-note :note="notification.note" @update:note="onNoteUpdated"/> + <mk-note :note="notification.note"/> </template> <template v-if="notification.type == 'mention'"> - <mk-note :note="notification.note" @update:note="onNoteUpdated"/> + <mk-note :note="notification.note"/> </template> </div> </template> @@ -95,17 +95,6 @@ export default Vue.extend({ getNoteSummary }; }, - methods: { - onNoteUpdated(note) { - switch (this.notification.type) { - case 'quote': - case 'reply': - case 'mention': - Vue.set(this.notification, 'note', note); - break; - } - } - } }); </script> diff --git a/src/client/app/mobile/views/components/notifications.vue b/src/client/app/mobile/views/components/notifications.vue index a0edab65ec..f793f5d685 100644 --- a/src/client/app/mobile/views/components/notifications.vue +++ b/src/client/app/mobile/views/components/notifications.vue @@ -10,41 +10,49 @@ <component :is="!$store.state.device.reduceMotion ? 'transition-group' : 'div'" name="mk-notifications" class="transition notifications" tag="div"> <template v-for="(notification, i) in _notifications"> <mk-notification :notification="notification" :key="notification.id"/> - <p class="date" :key="notification.id + '_date'" v-if="i != notifications.length - 1 && notification._date != _notifications[i + 1]._date"> + <p class="date" :key="notification.id + '_date'" v-if="i != items.length - 1 && notification._date != _notifications[i + 1]._date"> <span><fa icon="angle-up"/>{{ notification._datetext }}</span> <span><fa icon="angle-down"/>{{ _notifications[i + 1]._datetext }}</span> </p> </template> </component> - <button class="more" v-if="moreNotifications" @click="fetchMoreNotifications" :disabled="fetchingMoreNotifications"> - <template v-if="fetchingMoreNotifications"><fa icon="spinner" pulse fixed-width/></template> - {{ fetchingMoreNotifications ? $t('@.loading') : $t('@.load-more') }} + <button class="more" v-if="more" @click="fetchMore" :disabled="moreFetching"> + <template v-if="moreFetching"><fa icon="spinner" pulse fixed-width/></template> + {{ moreFetching ? $t('@.loading') : $t('@.load-more') }} </button> - <p class="empty" v-if="notifications.length == 0 && !fetching">{{ $t('empty') }}</p> + <p class="empty" v-if="empty">{{ $t('empty') }}</p> + + <mk-error v-if="error" @retry="init()"/> </div> </template> <script lang="ts"> import Vue from 'vue'; import i18n from '../../../i18n'; +import paging from '../../../common/scripts/paging'; export default Vue.extend({ i18n: i18n('mobile/views/components/notifications.vue'), + + mixins: [ + paging({}), + ], + data() { return { - fetching: true, - fetchingMoreNotifications: false, - notifications: [], - moreNotifications: false, - connection: null + connection: null, + pagination: { + endpoint: 'i/notifications', + limit: 15, + } }; }, computed: { _notifications(): any[] { - return (this.notifications as any).map(notification => { + return (this.items as any).map(notification => { const date = new Date(notification.createdAt).getDate(); const month = new Date(notification.createdAt).getMonth() + 1; notification._date = date; @@ -55,76 +63,23 @@ export default Vue.extend({ }, mounted() { - window.addEventListener('scroll', this.onScroll, { passive: true }); - this.connection = this.$root.stream.useSharedConnection('main'); - this.connection.on('notification', this.onNotification); - - const max = 15; - - this.$root.api('i/notifications', { - limit: max + 1 - }).then(notifications => { - if (notifications.length == max + 1) { - this.moreNotifications = true; - notifications.pop(); - } - - this.notifications = notifications; - this.fetching = false; - this.$emit('fetched'); - }); }, beforeDestroy() { - window.removeEventListener('scroll', this.onScroll); this.connection.dispose(); }, methods: { - fetchMoreNotifications() { - if (this.fetchingMoreNotifications) return; - - this.fetchingMoreNotifications = true; - - const max = 30; - - this.$root.api('i/notifications', { - limit: max + 1, - untilId: this.notifications[this.notifications.length - 1].id - }).then(notifications => { - if (notifications.length == max + 1) { - this.moreNotifications = true; - notifications.pop(); - } else { - this.moreNotifications = false; - } - this.notifications = this.notifications.concat(notifications); - this.fetchingMoreNotifications = false; - }); - }, - onNotification(notification) { // TODO: ユーザーが画面を見てないと思われるとき(ブラウザやタブがアクティブじゃないなど)は送信しない this.$root.stream.send('readNotification', { id: notification.id }); - this.notifications.unshift(notification); + this.prepend(notification); }, - - onScroll() { - if (this.$store.state.settings.fetchOnScroll) { - // 親要素が display none だったら弾く - // https://github.com/syuilo/misskey/issues/1569 - // http://d.hatena.ne.jp/favril/20091105/1257403319 - if (this.$el.offsetHeight == 0) return; - - const current = window.scrollY + window.innerHeight; - if (current > document.body.offsetHeight - 8) this.fetchMoreNotifications(); - } - } } }); </script> diff --git a/src/client/app/mobile/views/components/post-form-dialog.vue b/src/client/app/mobile/views/components/post-form-dialog.vue index 672c76b289..a6801be0ef 100644 --- a/src/client/app/mobile/views/components/post-form-dialog.vue +++ b/src/client/app/mobile/views/components/post-form-dialog.vue @@ -2,7 +2,7 @@ <div class="ulveipglmagnxfgvitaxyszerjwiqmwl"> <div class="bg" ref="bg"></div> <div class="main" ref="main"> - <mk-post-form ref="form" + <x-post-form ref="form" :reply="reply" :renote="renote" :mention="mention" @@ -17,8 +17,13 @@ <script lang="ts"> import Vue from 'vue'; import anime from 'animejs'; +import XPostForm from './post-form.vue'; export default Vue.extend({ + components: { + XPostForm + }, + props: { reply: { type: Object, diff --git a/src/client/app/mobile/views/components/post-form.vue b/src/client/app/mobile/views/components/post-form.vue index 326ff57c1f..815122b28e 100644 --- a/src/client/app/mobile/views/components/post-form.vue +++ b/src/client/app/mobile/views/components/post-form.vue @@ -1,5 +1,5 @@ <template> -<div class="mk-post-form"> +<div class="gafaadew"> <div class="form"> <header> <button class="cancel" @click="cancel"><fa icon="times"/></button> @@ -22,7 +22,7 @@ <input v-show="useCw" ref="cw" v-model="cw" :placeholder="$t('annotations')" v-autocomplete="{ model: 'cw' }"> <textarea v-model="text" ref="text" :disabled="posting" :placeholder="placeholder" v-autocomplete="{ model: 'text' }"></textarea> <x-post-form-attaches class="attaches" :files="files"/> - <mk-poll-editor v-if="poll" ref="poll" @destroyed="poll = false" @updated="onPollUpdate()"/> + <x-poll-editor v-if="poll" ref="poll" @destroyed="poll = false" @updated="onPollUpdate()"/> <mk-uploader ref="uploader" @uploaded="attachMedia" @change="onChangeUploadings"/> <footer> <button class="upload" @click="chooseFile"><fa icon="upload"/></button> @@ -64,7 +64,8 @@ import XPostFormAttaches from '../../../common/views/components/post-form-attach export default Vue.extend({ i18n: i18n('mobile/views/components/post-form.vue'), components: { - XPostFormAttaches + XPostFormAttaches, + XPollEditor: () => import('../../../common/views/components/poll-editor.vue').then(m => m.default) }, props: { @@ -386,7 +387,7 @@ export default Vue.extend({ </script> <style lang="stylus" scoped> -.mk-post-form +.gafaadew max-width 500px width calc(100% - 16px) margin 8px auto diff --git a/src/client/app/mobile/views/components/ui.header.vue b/src/client/app/mobile/views/components/ui.header.vue index b636041d63..f20f64e7ff 100644 --- a/src/client/app/mobile/views/components/ui.header.vue +++ b/src/client/app/mobile/views/components/ui.header.vue @@ -1,6 +1,5 @@ <template> <div class="header" ref="root" :class="{ shadow: $store.state.device.useShadow }"> - <p class="warn" v-if="env != 'production'">{{ $t('@.do-not-use-in-production') }} <a href="/assets/flush.html?force">Flush</a></p> <div class="main" ref="main"> <div class="backdrop"></div> <div class="content" ref="mainContainer"> 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 73bc6f3034..d9aa1dad8a 100644 --- a/src/client/app/mobile/views/components/user-list-timeline.vue +++ b/src/client/app/mobile/views/components/user-list-timeline.vue @@ -1,14 +1,10 @@ <template> -<div> - <mk-notes ref="timeline" :make-promise="makePromise" @inited="() => $emit('loaded')"/> -</div> +<mk-notes ref="timeline" :pagination="pagination" @inited="() => $emit('loaded')"/> </template> <script lang="ts"> import Vue from 'vue'; -const fetchLimit = 10; - export default Vue.extend({ props: ['list'], @@ -16,28 +12,17 @@ export default Vue.extend({ return { connection: null, date: null, - makePromise: cursor => this.$root.api('notes/user-list-timeline', { - listId: this.list.id, - limit: fetchLimit + 1, - untilId: cursor ? cursor : undefined, - untilDate: cursor ? undefined : (this.date ? this.date.getTime() : 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, - more: true - }; - } else { - return { - notes: notes, - more: false - }; - } - }) + pagination: { + endpoint: 'notes/user-list-timeline', + limit: 10, + params: init => ({ + listId: this.list.id, + untilDate: init ? undefined : (this.date ? this.date.getTime() : undefined), + includeMyRenotes: this.$store.state.settings.showMyRenotes, + includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, + includeLocalRenotes: this.$store.state.settings.showLocalRenotes + }) + } }; }, diff --git a/src/client/app/mobile/views/components/user-timeline.vue b/src/client/app/mobile/views/components/user-timeline.vue index 8c7c8c6d7d..3b6baa76be 100644 --- a/src/client/app/mobile/views/components/user-timeline.vue +++ b/src/client/app/mobile/views/components/user-timeline.vue @@ -1,15 +1,11 @@ <template> -<div class="mk-user-timeline"> - <mk-notes ref="timeline" :make-promise="makePromise" @inited="() => $emit('loaded')"/> -</div> +<mk-notes ref="timeline" :pagination="pagination" @inited="() => $emit('loaded')"/> </template> <script lang="ts"> import Vue from 'vue'; import i18n from '../../../i18n'; -const fetchLimit = 10; - export default Vue.extend({ i18n: i18n('mobile/views/components/user-timeline.vue'), @@ -18,26 +14,15 @@ export default Vue.extend({ data() { return { date: null, - makePromise: cursor => this.$root.api('users/notes', { - userId: this.user.id, - limit: fetchLimit + 1, - withFiles: this.withMedia, - untilDate: cursor ? undefined : (this.date ? this.date.getTime() : undefined), - untilId: cursor ? cursor : undefined - }).then(notes => { - if (notes.length == fetchLimit + 1) { - notes.pop(); - return { - notes: notes, - more: true - }; - } else { - return { - notes: notes, - more: false - }; - } - }) + pagination: { + endpoint: 'users/notes', + limit: 10, + params: init => ({ + userId: this.user.id, + withFiles: this.withMedia, + untilDate: init ? undefined : (this.date ? this.date.getTime() : undefined), + }) + } }; }, diff --git a/src/client/app/mobile/views/pages/favorites.vue b/src/client/app/mobile/views/pages/favorites.vue deleted file mode 100644 index 92823db7cc..0000000000 --- a/src/client/app/mobile/views/pages/favorites.vue +++ /dev/null @@ -1,86 +0,0 @@ -<template> -<mk-ui> - <template #header><span style="margin-right:4px;"><fa icon="star"/></span>{{ $t('@.favorites') }}</template> - - <main> - <sequential-entrance animation="entranceFromTop" delay="25"> - <template v-for="favorite in favorites"> - <mk-note-detail class="post" :note="favorite.note" :key="favorite.note.id"/> - </template> - </sequential-entrance> - <ui-button v-if="existMore" @click="fetchMore()">{{ $t('@.load-more') }}</ui-button> - </main> -</mk-ui> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import Progress from '../../../common/scripts/loading'; - -export default Vue.extend({ - i18n: i18n(), - data() { - return { - fetching: true, - favorites: [], - existMore: false, - moreFetching: false - }; - }, - created() { - this.fetch(); - }, - mounted() { - document.title = `${this.$root.instanceName} | ${this.$t('@.favorites')}`; - }, - methods: { - fetch() { - Progress.start(); - this.fetching = true; - - this.$root.api('i/favorites', { - limit: 11 - }).then(favorites => { - if (favorites.length == 11) { - this.existMore = true; - favorites.pop(); - } - - this.favorites = favorites; - this.fetching = false; - - Progress.done(); - }); - }, - fetchMore() { - this.moreFetching = true; - this.$root.api('i/favorites', { - limit: 11, - untilId: this.favorites[this.favorites.length - 1].id - }).then(favorites => { - if (favorites.length == 11) { - this.existMore = true; - favorites.pop(); - } else { - this.existMore = false; - } - - this.favorites = this.favorites.concat(favorites); - this.moreFetching = false; - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -main - > * > .post - margin-bottom 8px - - @media (min-width 500px) - > * > .post - margin-bottom 16px - -</style> diff --git a/src/client/app/mobile/views/pages/featured.vue b/src/client/app/mobile/views/pages/featured.vue deleted file mode 100644 index 667e199b58..0000000000 --- a/src/client/app/mobile/views/pages/featured.vue +++ /dev/null @@ -1,61 +0,0 @@ -<template> -<mk-ui> - <template #header><span style="margin-right:4px;"><fa :icon="faNewspaper"/></span>{{ $t('@.featured-notes') }}</template> - - <main> - <sequential-entrance animation="entranceFromTop" delay="25"> - <template v-for="note in notes"> - <mk-note-detail class="post" :note="note" :key="note.id"/> - </template> - </sequential-entrance> - </main> -</mk-ui> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import Progress from '../../../common/scripts/loading'; -import { faNewspaper } from '@fortawesome/free-solid-svg-icons'; - -export default Vue.extend({ - i18n: i18n(''), - data() { - return { - fetching: true, - notes: [], - faNewspaper - }; - }, - created() { - this.fetch(); - }, - methods: { - fetch() { - Progress.start(); - this.fetching = true; - - this.$root.api('notes/featured', { - limit: 30 - }).then(notes => { - notes.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); - this.notes = notes; - this.fetching = false; - - Progress.done(); - }); - }, - } -}); -</script> - -<style lang="stylus" scoped> -main - > * > .post - margin-bottom 8px - - @media (min-width 500px) - > * > .post - margin-bottom 16px - -</style> diff --git a/src/client/app/mobile/views/pages/home.timeline.vue b/src/client/app/mobile/views/pages/home.timeline.vue index e0754e88b4..f115458092 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" :make-promise="makePromise" @inited="() => $emit('loaded')"/> + <mk-notes ref="timeline" :pagination="pagination" @loaded="() => $emit('loaded')"/> </div> </template> @@ -15,8 +15,6 @@ import Vue from 'vue'; import i18n from '../../../i18n'; -const fetchLimit = 10; - export default Vue.extend({ i18n: i18n('mobile/views/pages/home.timeline.vue'), @@ -43,7 +41,7 @@ export default Vue.extend({ }, query: {}, endpoint: null, - makePromise: null + pagination: null }; }, @@ -110,25 +108,14 @@ export default Vue.extend({ this.connection.on('mention', onNote); } - 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, - more: true - }; - } else { - return { - notes: notes, - more: false - }; - } - }); + this.pagination = { + endpoint: this.endpoint, + limit: 10, + params: init => ({ + untilDate: init ? undefined : (this.date ? this.date.getTime() : undefined), + ...this.baseQuery, ...this.query + }) + }; }, methods: { diff --git a/src/client/app/mobile/views/pages/search.vue b/src/client/app/mobile/views/pages/search.vue index 45f3837907..dca1ffd40a 100644 --- a/src/client/app/mobile/views/pages/search.vue +++ b/src/client/app/mobile/views/pages/search.vue @@ -3,7 +3,7 @@ <template #header><fa icon="search"/> {{ q }}</template> <main> - <mk-notes ref="timeline" :make-promise="makePromise" @inited="inited"/> + <mk-notes ref="timeline" :pagination="pagination" @inited="inited"/> </main> </mk-ui> </template> @@ -14,42 +14,27 @@ import i18n from '../../../i18n'; import Progress from '../../../common/scripts/loading'; import { genSearchQuery } from '../../../common/scripts/gen-search-query'; -const limit = 20; - export default Vue.extend({ i18n: i18n('mobile/views/pages/search.vue'), data() { return { - makePromise: async cursor => this.$root.api('notes/search', { - limit: limit + 1, - untilId: cursor ? cursor : undefined, - ...(await genSearchQuery(this, this.q)) - }).then(notes => { - if (notes.length == limit + 1) { - notes.pop(); - return { - notes: notes, - more: true - }; - } else { - return { - notes: notes, - more: false - }; - } - }) + pagination: { + endpoint: 'notes/search', + limit: 20, + params: () => genSearchQuery(this, this.q) + } }; }, - watch: { - $route() { - this.$refs.timeline.reload(); - } - }, computed: { q(): string { return this.$route.query.q; } }, + watch: { + $route() { + this.$refs.timeline.reload(); + } + }, mounted() { document.title = `${this.$t('search')}: ${this.q} | ${this.$root.instanceName}`; }, diff --git a/src/client/app/mobile/views/pages/tag.vue b/src/client/app/mobile/views/pages/tag.vue index f41cf1f18c..19482ec382 100644 --- a/src/client/app/mobile/views/pages/tag.vue +++ b/src/client/app/mobile/views/pages/tag.vue @@ -3,7 +3,7 @@ <template #header><span style="margin-right:4px;"><fa icon="hashtag"/></span>{{ $route.params.tag }}</template> <main> - <mk-notes ref="timeline" :make-promise="makePromise" @inited="inited"/> + <mk-notes ref="timeline" :pagination="pagination" @inited="inited"/> </main> </mk-ui> </template> @@ -13,30 +13,17 @@ import Vue from 'vue'; import i18n from '../../../i18n'; import Progress from '../../../common/scripts/loading'; -const limit = 20; - export default Vue.extend({ i18n: i18n('mobile/views/pages/tag.vue'), data() { return { - makePromise: cursor => this.$root.api('notes/search-by-tag', { - limit: limit + 1, - untilId: cursor ? cursor : undefined, - tag: this.$route.params.tag - }).then(notes => { - if (notes.length == limit + 1) { - notes.pop(); - return { - notes: notes, - more: true - }; - } else { - return { - notes: notes, - more: false - }; + pagination: { + endpoint: 'notes/search-by-tag', + limit: 20, + params: { + tag: this.$route.params.tag } - }) + } }; }, watch: { diff --git a/src/client/app/mobile/views/pages/user/home.vue b/src/client/app/mobile/views/pages/user/home.vue index 00c63c288d..1d7b0a4e6d 100644 --- a/src/client/app/mobile/views/pages/user/home.vue +++ b/src/client/app/mobile/views/pages/user/home.vue @@ -19,8 +19,6 @@ <x-activity :user="user"/> </div> </ui-container> - <mk-user-list :make-promise="makeFrequentlyRepliedUsersPromise" :icon-only="true"><fa icon="users"/> {{ $t('frequently-replied-users') }}</mk-user-list> - <mk-user-list v-if="$store.getters.isSignedIn && $store.state.i.id !== user.id" :make-promise="makeFollowersYouKnowPromise" :icon-only="true"><fa icon="users"/> {{ $t('followers-you-know') }}</mk-user-list> </div> </template> diff --git a/src/models/repositories/note-favorite.ts b/src/models/repositories/note-favorite.ts index b5875802fb..01064f3065 100644 --- a/src/models/repositories/note-favorite.ts +++ b/src/models/repositories/note-favorite.ts @@ -2,6 +2,7 @@ import { EntityRepository, Repository } from 'typeorm'; import { NoteFavorite } from '../entities/note-favorite'; import { Notes } from '..'; import { ensure } from '../../prelude/ensure'; +import { types, bool } from '../../misc/schema'; @EntityRepository(NoteFavorite) export class NoteFavoriteRepository extends Repository<NoteFavorite> { @@ -26,3 +27,33 @@ export class NoteFavoriteRepository extends Repository<NoteFavorite> { return Promise.all(favorites.map(x => this.pack(x, me))); } } + +export const packedNoteFavoriteSchema = { + type: types.object, + optional: bool.false, nullable: bool.false, + properties: { + id: { + type: types.string, + optional: bool.false, nullable: bool.false, + format: 'id', + description: 'The unique identifier for this favorite.', + example: 'xxxxxxxxxx', + }, + createdAt: { + type: types.string, + optional: bool.false, nullable: bool.false, + format: 'date-time', + description: 'The date that the favorite was created.' + }, + note: { + type: types.object, + optional: bool.false, nullable: bool.false, + ref: 'Note', + }, + noteId: { + type: types.string, + optional: bool.false, nullable: bool.false, + format: 'id', + }, + }, +}; diff --git a/src/server/api/endpoints/i/favorites.ts b/src/server/api/endpoints/i/favorites.ts index aad706545a..d1e90dd15c 100644 --- a/src/server/api/endpoints/i/favorites.ts +++ b/src/server/api/endpoints/i/favorites.ts @@ -3,6 +3,7 @@ import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; import { NoteFavorites } from '../../../../models'; import { makePaginationQuery } from '../../common/make-pagination-query'; +import { types, bool } from '../../../../misc/schema'; export const meta = { desc: { @@ -29,7 +30,17 @@ export const meta = { untilId: { validator: $.optional.type(ID), }, - } + }, + + res: { + type: types.array, + optional: bool.false, nullable: bool.false, + items: { + type: types.object, + optional: bool.false, nullable: bool.false, + ref: 'NoteFavorite', + } + }, }; export default define(meta, async (ps, user) => { diff --git a/src/server/api/endpoints/users/get-frequently-replied-users.ts b/src/server/api/endpoints/users/get-frequently-replied-users.ts index 420936c089..24d1bd194c 100644 --- a/src/server/api/endpoints/users/get-frequently-replied-users.ts +++ b/src/server/api/endpoints/users/get-frequently-replied-users.ts @@ -4,7 +4,7 @@ import define from '../../define'; import { maximum } from '../../../../prelude/array'; import { ApiError } from '../../error'; import { getUser } from '../../common/getters'; -import { Not, In } from 'typeorm'; +import { Not, In, IsNull } from 'typeorm'; import { Notes, Users } from '../../../../models'; import { types, bool } from '../../../../misc/schema'; @@ -58,7 +58,7 @@ export default define(meta, async (ps, me) => { const recentNotes = await Notes.find({ where: { userId: user.id, - replyId: Not(null) + replyId: Not(IsNull()) }, order: { id: -1 diff --git a/src/server/api/openapi/schemas.ts b/src/server/api/openapi/schemas.ts index 32f69bdef3..0a49607f05 100644 --- a/src/server/api/openapi/schemas.ts +++ b/src/server/api/openapi/schemas.ts @@ -14,6 +14,7 @@ import { packedNoteReactionSchema } from '../../../models/repositories/note-reac import { packedHashtagSchema } from '../../../models/repositories/hashtag'; import { packedPageSchema } from '../../../models/repositories/page'; import { packedUserGroupSchema } from '../../../models/repositories/user-group'; +import { packedNoteFavoriteSchema } from '../../../models/repositories/note-favorite'; export function convertSchemaToOpenApiSchema(schema: Schema) { const res: any = schema; @@ -71,13 +72,14 @@ export const schemas = { App: convertSchemaToOpenApiSchema(packedAppSchema), MessagingMessage: convertSchemaToOpenApiSchema(packedMessagingMessageSchema), Note: convertSchemaToOpenApiSchema(packedNoteSchema), + NoteReaction: convertSchemaToOpenApiSchema(packedNoteReactionSchema), + NoteFavorite: convertSchemaToOpenApiSchema(packedNoteFavoriteSchema), Notification: convertSchemaToOpenApiSchema(packedNotificationSchema), DriveFile: convertSchemaToOpenApiSchema(packedDriveFileSchema), DriveFolder: convertSchemaToOpenApiSchema(packedDriveFolderSchema), Following: convertSchemaToOpenApiSchema(packedFollowingSchema), Muting: convertSchemaToOpenApiSchema(packedMutingSchema), Blocking: convertSchemaToOpenApiSchema(packedBlockingSchema), - NoteReaction: convertSchemaToOpenApiSchema(packedNoteReactionSchema), Hashtag: convertSchemaToOpenApiSchema(packedHashtagSchema), Page: convertSchemaToOpenApiSchema(packedPageSchema), }; diff --git a/src/services/chart/charts/classes/drive.ts b/src/services/chart/charts/classes/drive.ts index ae52df19ac..c3bcacb7df 100644 --- a/src/services/chart/charts/classes/drive.ts +++ b/src/services/chart/charts/classes/drive.ts @@ -2,7 +2,7 @@ import autobind from 'autobind-decorator'; import Chart, { Obj, DeepPartial } from '../../core'; import { SchemaType } from '../../../../misc/schema'; import { DriveFiles } from '../../../../models'; -import { Not } from 'typeorm'; +import { Not, IsNull } from 'typeorm'; import { DriveFile } from '../../../../models/entities/drive-file'; import { name, schema } from '../schemas/drive'; @@ -31,7 +31,7 @@ export default class DriveChart extends Chart<DriveLog> { protected async fetchActual(): Promise<DeepPartial<DriveLog>> { const [localCount, remoteCount, localSize, remoteSize] = await Promise.all([ DriveFiles.count({ userHost: null }), - DriveFiles.count({ userHost: Not(null) }), + DriveFiles.count({ userHost: Not(IsNull()) }), DriveFiles.clacDriveUsageOfLocal(), DriveFiles.clacDriveUsageOfRemote() ]); diff --git a/src/services/chart/charts/classes/notes.ts b/src/services/chart/charts/classes/notes.ts index 85ccf000d8..815061c445 100644 --- a/src/services/chart/charts/classes/notes.ts +++ b/src/services/chart/charts/classes/notes.ts @@ -2,7 +2,7 @@ import autobind from 'autobind-decorator'; import Chart, { Obj, DeepPartial } from '../../core'; import { SchemaType } from '../../../../misc/schema'; import { Notes } from '../../../../models'; -import { Not } from 'typeorm'; +import { Not, IsNull } from 'typeorm'; import { Note } from '../../../../models/entities/note'; import { name, schema } from '../schemas/notes'; @@ -29,7 +29,7 @@ export default class NotesChart extends Chart<NotesLog> { protected async fetchActual(): Promise<DeepPartial<NotesLog>> { const [localCount, remoteCount] = await Promise.all([ Notes.count({ userHost: null }), - Notes.count({ userHost: Not(null) }) + Notes.count({ userHost: Not(IsNull()) }) ]); return { diff --git a/src/services/chart/charts/classes/per-user-following.ts b/src/services/chart/charts/classes/per-user-following.ts index f3809a7c94..8295c0cb0d 100644 --- a/src/services/chart/charts/classes/per-user-following.ts +++ b/src/services/chart/charts/classes/per-user-following.ts @@ -2,7 +2,7 @@ import autobind from 'autobind-decorator'; import Chart, { Obj, DeepPartial } from '../../core'; import { SchemaType } from '../../../../misc/schema'; import { Followings, Users } from '../../../../models'; -import { Not } from 'typeorm'; +import { Not, IsNull } from 'typeorm'; import { User } from '../../../../models/entities/user'; import { name, schema } from '../schemas/per-user-following'; @@ -45,8 +45,8 @@ export default class PerUserFollowingChart extends Chart<PerUserFollowingLog> { ] = await Promise.all([ Followings.count({ followerId: group, followeeHost: null }), Followings.count({ followeeId: group, followerHost: null }), - Followings.count({ followerId: group, followeeHost: Not(null) }), - Followings.count({ followeeId: group, followerHost: Not(null) }) + Followings.count({ followerId: group, followeeHost: Not(IsNull()) }), + Followings.count({ followeeId: group, followerHost: Not(IsNull()) }) ]); return { diff --git a/src/services/chart/charts/classes/users.ts b/src/services/chart/charts/classes/users.ts index eec30de8dc..47e4caa1b7 100644 --- a/src/services/chart/charts/classes/users.ts +++ b/src/services/chart/charts/classes/users.ts @@ -2,7 +2,7 @@ import autobind from 'autobind-decorator'; import Chart, { Obj, DeepPartial } from '../../core'; import { SchemaType } from '../../../../misc/schema'; import { Users } from '../../../../models'; -import { Not } from 'typeorm'; +import { Not, IsNull } from 'typeorm'; import { User } from '../../../../models/entities/user'; import { name, schema } from '../schemas/users'; @@ -29,7 +29,7 @@ export default class UsersChart extends Chart<UsersLog> { protected async fetchActual(): Promise<DeepPartial<UsersLog>> { const [localCount, remoteCount] = await Promise.all([ Users.count({ host: null }), - Users.count({ host: Not(null) }) + Users.count({ host: Not(IsNull()) }) ]); return { diff --git a/src/services/note/delete.ts b/src/services/note/delete.ts index c03c742ee1..95776f7287 100644 --- a/src/services/note/delete.ts +++ b/src/services/note/delete.ts @@ -8,7 +8,6 @@ import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc'; import { User } from '../../models/entities/user'; import { Note } from '../../models/entities/note'; import { Notes, Users, Followings, Instances } from '../../models'; -import { Not } from 'typeorm'; import { notesChart, perUserNotesChart, instanceChart } from '../chart'; /** @@ -38,13 +37,21 @@ export default async function(user: User, note: Note, quiet = false) { if (Users.isLocalUser(user)) { const content = renderActivity(renderDelete(renderTombstone(`${config.url}/notes/${note.id}`), user)); - const followings = await Followings.find({ - followeeId: user.id, - followerHost: Not(null) + const queue: string[] = []; + + const followers = await Followings.find({ + followeeId: note.userId }); - for (const following of followings) { - deliver(user, content, following.followerInbox); + for (const following of followers) { + if (Followings.isRemoteFollower(following)) { + const inbox = following.followerSharedInbox || following.followerInbox; + if (!queue.includes(inbox)) queue.push(inbox); + } + } + + for (const inbox of queue) { + deliver(user as any, content, inbox); } } //#endregion diff --git a/src/tools/clean-remote-files.ts b/src/tools/clean-remote-files.ts index e722552e14..633a6fc04d 100644 --- a/src/tools/clean-remote-files.ts +++ b/src/tools/clean-remote-files.ts @@ -1,14 +1,14 @@ import * as promiseLimit from 'promise-limit'; import del from '../services/drive/delete-file'; import { DriveFiles } from '../models'; -import { Not } from 'typeorm'; +import { Not, IsNull } from 'typeorm'; import { DriveFile } from '../models/entities/drive-file'; import { ensure } from '../prelude/ensure'; const limit = promiseLimit(16); DriveFiles.find({ - userHost: Not(null) + userHost: Not(IsNull()) }).then(async files => { console.log(`there is ${files.length} files`); |