summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorsyuilo <syuilotan@yahoo.co.jp>2019-05-21 08:57:10 +0900
committersyuilo <syuilotan@yahoo.co.jp>2019-05-21 08:57:10 +0900
commitc26168f22e73e4a595cf9dedd7ba0d3478f48752 (patch)
tree60ba7060b7eaa7c6c37a53057927ea40b89e3f4a /src
parentMerge branch 'develop' (diff)
parent11.18.0 (diff)
downloadmisskey-c26168f22e73e4a595cf9dedd7ba0d3478f48752.tar.gz
misskey-c26168f22e73e4a595cf9dedd7ba0d3478f48752.tar.bz2
misskey-c26168f22e73e4a595cf9dedd7ba0d3478f48752.zip
Merge branch 'develop'
Diffstat (limited to 'src')
-rw-r--r--src/client/app/common/scripts/note-subscriber.ts2
-rw-r--r--src/client/app/common/scripts/paging.ts171
-rw-r--r--src/client/app/common/views/components/avatars.vue27
-rw-r--r--src/client/app/common/views/components/forkit.vue1
-rw-r--r--src/client/app/common/views/components/index.ts4
-rw-r--r--src/client/app/common/views/components/messaging.vue58
-rw-r--r--src/client/app/common/views/components/nav.vue2
-rw-r--r--src/client/app/common/views/components/poll-editor.vue4
-rw-r--r--src/client/app/common/views/components/ui/pagination.vue36
-rw-r--r--src/client/app/common/views/components/user-list.vue63
-rw-r--r--src/client/app/common/views/deck/deck.direct-column.vue41
-rw-r--r--src/client/app/common/views/deck/deck.direct.vue65
-rw-r--r--src/client/app/common/views/deck/deck.favorites-column.vue58
-rw-r--r--src/client/app/common/views/deck/deck.featured-column.vue46
-rw-r--r--src/client/app/common/views/deck/deck.hashtag-tl.vue38
-rw-r--r--src/client/app/common/views/deck/deck.list-tl.vue38
-rw-r--r--src/client/app/common/views/deck/deck.mentions-column.vue38
-rw-r--r--src/client/app/common/views/deck/deck.mentions.vue61
-rw-r--r--src/client/app/common/views/deck/deck.notes.vue140
-rw-r--r--src/client/app/common/views/deck/deck.notification.vue17
-rw-r--r--src/client/app/common/views/deck/deck.notifications.vue104
-rw-r--r--src/client/app/common/views/deck/deck.search-column.vue27
-rw-r--r--src/client/app/common/views/deck/deck.tl.vue38
-rw-r--r--src/client/app/common/views/deck/deck.user-column.home.vue49
-rw-r--r--src/client/app/common/views/pages/explore.vue67
-rw-r--r--src/client/app/common/views/pages/favorites.vue44
-rw-r--r--src/client/app/common/views/pages/featured.vue44
-rw-r--r--src/client/app/common/views/pages/followers.vue29
-rw-r--r--src/client/app/common/views/pages/following.vue29
-rw-r--r--src/client/app/common/views/pages/pages.vue106
-rw-r--r--src/client/app/common/views/pages/share.vue15
-rw-r--r--src/client/app/common/views/pages/user-groups.vue17
-rw-r--r--src/client/app/common/views/pages/user-lists.vue5
-rw-r--r--src/client/app/common/views/widgets/post-form.vue3
-rw-r--r--src/client/app/desktop/script.ts17
-rw-r--r--src/client/app/desktop/views/components/detail-notes.vue56
-rw-r--r--src/client/app/desktop/views/components/index.ts4
-rw-r--r--src/client/app/desktop/views/components/notes.vue179
-rw-r--r--src/client/app/desktop/views/components/notifications.vue66
-rw-r--r--src/client/app/desktop/views/components/post-form-window.vue8
-rw-r--r--src/client/app/desktop/views/components/post-form.vue11
-rw-r--r--src/client/app/desktop/views/components/renote-form.vue6
-rw-r--r--src/client/app/desktop/views/components/user-list-timeline.vue37
-rw-r--r--src/client/app/desktop/views/home/favorites.vue83
-rw-r--r--src/client/app/desktop/views/home/featured.vue55
-rw-r--r--src/client/app/desktop/views/home/search.vue27
-rw-r--r--src/client/app/desktop/views/home/tag.vue27
-rw-r--r--src/client/app/desktop/views/home/timeline.core.vue33
-rw-r--r--src/client/app/desktop/views/home/timeline.vue6
-rw-r--r--src/client/app/desktop/views/home/user/user.timeline.vue41
-rw-r--r--src/client/app/init.ts12
-rw-r--r--src/client/app/mobile/script.ts28
-rw-r--r--src/client/app/mobile/views/components/detail-notes.vue52
-rw-r--r--src/client/app/mobile/views/components/index.ts2
-rw-r--r--src/client/app/mobile/views/components/notes.vue180
-rw-r--r--src/client/app/mobile/views/components/notification.vue17
-rw-r--r--src/client/app/mobile/views/components/notifications.vue85
-rw-r--r--src/client/app/mobile/views/components/post-form-dialog.vue7
-rw-r--r--src/client/app/mobile/views/components/post-form.vue9
-rw-r--r--src/client/app/mobile/views/components/ui.header.vue1
-rw-r--r--src/client/app/mobile/views/components/user-list-timeline.vue39
-rw-r--r--src/client/app/mobile/views/components/user-timeline.vue35
-rw-r--r--src/client/app/mobile/views/pages/favorites.vue86
-rw-r--r--src/client/app/mobile/views/pages/featured.vue61
-rw-r--r--src/client/app/mobile/views/pages/home.timeline.vue33
-rw-r--r--src/client/app/mobile/views/pages/search.vue37
-rw-r--r--src/client/app/mobile/views/pages/tag.vue27
-rw-r--r--src/client/app/mobile/views/pages/user/home.vue2
-rw-r--r--src/models/repositories/note-favorite.ts31
-rw-r--r--src/server/api/endpoints/i/favorites.ts13
-rw-r--r--src/server/api/endpoints/users/get-frequently-replied-users.ts4
-rw-r--r--src/server/api/openapi/schemas.ts4
-rw-r--r--src/services/chart/charts/classes/drive.ts4
-rw-r--r--src/services/chart/charts/classes/notes.ts4
-rw-r--r--src/services/chart/charts/classes/per-user-following.ts6
-rw-r--r--src/services/chart/charts/classes/users.ts4
-rw-r--r--src/services/note/delete.ts19
-rw-r--r--src/tools/clean-remote-files.ts4
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`);