Refactoring

This commit is contained in:
syuilo 2019-05-21 03:07:11 +09:00
parent 262d5ead51
commit 5511b6e013
No known key found for this signature in database
GPG Key ID: BDC4C49D06AB9D69
41 changed files with 764 additions and 1408 deletions

View File

@ -144,8 +144,6 @@ export default prop => ({
break; break;
} }
} }
this.$emit(`update:${prop}`, this.$_ns_note_);
}, },
} }
}); });

View File

@ -0,0 +1,169 @@
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;
}, e => {
this.fetching = false;
});
},
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()) {
// 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();
}
}
});

View File

@ -2,13 +2,13 @@
<ui-container :body-togglable="true"> <ui-container :body-togglable="true">
<template #header><slot></slot></template> <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="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> <p>{{ $t('no-users') }}</p>
</div> </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"/> <mk-avatar class="avatar" :user="user"/>
<div class="body" v-if="!iconOnly"> <div class="body" v-if="!iconOnly">
<div class="name"> <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/> <mk-follow-button class="follow-button" v-if="$store.getters.isSignedIn && user.id != $store.state.i.id" :user="user" mini/>
</div> </div>
</div> </div>
<button class="more" :class="{ fetching: fetchingMoreUsers }" v-if="cursor != null" @click="fetchMoreUsers()" :disabled="fetchingMoreUsers"> <button class="more" :class="{ fetching: moreFetching }" v-if="more" @click="fetchMoreUsers()" :disabled="moreFetching">
<template v-if="fetchingMoreUsers"><fa icon="spinner" pulse fixed-width/></template>{{ fetchingMoreUsers ? $t('@.loading') : $t('@.load-more') }} <template v-if="moreFetching"><fa icon="spinner" pulse fixed-width/></template>{{ moreFetching ? $t('@.loading') : $t('@.load-more') }}
</button> </button>
</div> </div>
</ui-container> </ui-container>
@ -31,60 +31,31 @@
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import i18n from '../../../i18n'; import i18n from '../../../i18n';
import paging from '../../../common/scripts/paging';
export default Vue.extend({ export default Vue.extend({
i18n: i18n('common/views/components/user-list.vue'), i18n: i18n('common/views/components/user-list.vue'),
mixins: [
paging({}),
],
props: { props: {
makePromise: { pagination: {
required: true required: true
}, },
extract: {
required: false
},
iconOnly: { iconOnly: {
type: Boolean, type: Boolean,
default: false default: false
} }
}, },
data() { computed: {
return { users() {
fetching: true, return this.extract ? this.extract(this.items) : this.items;
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;
});
} }
} }
}); });

View File

@ -1,13 +1,11 @@
<template> <template>
<x-notes ref="timeline" :make-promise="makePromise" @inited="() => $emit('loaded')"/> <x-notes ref="timeline" :pagination="pagination" @inited="() => $emit('loaded')"/>
</template> </template>
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import XNotes from './deck.notes.vue'; import XNotes from './deck.notes.vue';
const fetchLimit = 10;
export default Vue.extend({ export default Vue.extend({
components: { components: {
XNotes XNotes
@ -16,27 +14,16 @@ export default Vue.extend({
data() { data() {
return { return {
connection: null, connection: null,
makePromise: cursor => this.$root.api('notes/mentions', { pagination: {
limit: fetchLimit + 1, endpoint: 'notes/mentions',
untilId: cursor ? cursor : undefined, limit: 10,
params: {
includeMyRenotes: this.$store.state.settings.showMyRenotes, includeMyRenotes: this.$store.state.settings.showMyRenotes,
includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
includeLocalRenotes: this.$store.state.settings.showLocalRenotes, includeLocalRenotes: this.$store.state.settings.showLocalRenotes,
visibility: 'specified' visibility: 'specified'
}).then(notes => {
if (notes.length == fetchLimit + 1) {
notes.pop();
return {
notes: notes,
more: true
};
} else {
return {
notes: notes,
more: false
};
} }
}) }
}; };
}, },

View File

@ -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>

View File

@ -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>

View File

@ -1,13 +1,11 @@
<template> <template>
<x-notes ref="timeline" :make-promise="makePromise" @inited="() => $emit('loaded')"/> <x-notes ref="timeline" :pagination="pagination" @inited="() => $emit('loaded')"/>
</template> </template>
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import XNotes from './deck.notes.vue'; import XNotes from './deck.notes.vue';
const fetchLimit = 10;
export default Vue.extend({ export default Vue.extend({
components: { components: {
XNotes XNotes
@ -28,28 +26,18 @@ export default Vue.extend({
data() { data() {
return { return {
connection: null, connection: null,
makePromise: cursor => this.$root.api('notes/search-by-tag', { pagination: {
limit: fetchLimit + 1, endpoint: 'notes/search-by-tag',
untilId: cursor ? cursor : undefined, limit: 10,
params: init => ({
untilDate: init ? undefined : (this.date ? this.date.getTime() : undefined),
withFiles: this.mediaOnly, withFiles: this.mediaOnly,
includeMyRenotes: this.$store.state.settings.showMyRenotes, includeMyRenotes: this.$store.state.settings.showMyRenotes,
includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
includeLocalRenotes: this.$store.state.settings.showLocalRenotes, includeLocalRenotes: this.$store.state.settings.showLocalRenotes,
query: this.tagTl.query query: this.tagTl.query
}).then(notes => {
if (notes.length == fetchLimit + 1) {
notes.pop();
return {
notes: notes,
more: true
};
} else {
return {
notes: notes,
more: false
};
}
}) })
}
}; };
}, },

View File

@ -1,13 +1,11 @@
<template> <template>
<x-notes ref="timeline" :make-promise="makePromise" @inited="() => $emit('loaded')"/> <x-notes ref="timeline" :pagination="pagination" @inited="() => $emit('loaded')"/>
</template> </template>
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import XNotes from './deck.notes.vue'; import XNotes from './deck.notes.vue';
const fetchLimit = 10;
export default Vue.extend({ export default Vue.extend({
components: { components: {
XNotes XNotes
@ -28,28 +26,18 @@ export default Vue.extend({
data() { data() {
return { return {
connection: null, connection: null,
makePromise: cursor => this.$root.api('notes/user-list-timeline', { pagination: {
endpoint: 'notes/user-list-timeline',
limit: 10,
params: init => ({
listId: this.list.id, listId: this.list.id,
limit: fetchLimit + 1, untilDate: init ? undefined : (this.date ? this.date.getTime() : undefined),
untilId: cursor ? cursor : undefined,
withFiles: this.mediaOnly, withFiles: this.mediaOnly,
includeMyRenotes: this.$store.state.settings.showMyRenotes, includeMyRenotes: this.$store.state.settings.showMyRenotes,
includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
includeLocalRenotes: this.$store.state.settings.showLocalRenotes 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
};
}
}) })
}
}; };
}, },

View File

@ -1,13 +1,11 @@
<template> <template>
<x-notes ref="timeline" :make-promise="makePromise" @inited="() => $emit('loaded')"/> <x-notes ref="timeline" :pagination="pagination" @inited="() => $emit('loaded')"/>
</template> </template>
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import XNotes from './deck.notes.vue'; import XNotes from './deck.notes.vue';
const fetchLimit = 10;
export default Vue.extend({ export default Vue.extend({
components: { components: {
XNotes XNotes
@ -16,26 +14,16 @@ export default Vue.extend({
data() { data() {
return { return {
connection: null, connection: null,
makePromise: cursor => this.$root.api('notes/mentions', { pagination: {
limit: fetchLimit + 1, endpoint: 'notes/mentions',
untilId: cursor ? cursor : undefined, limit: 10,
params: init => ({
untilDate: init ? undefined : (this.date ? this.date.getTime() : undefined),
includeMyRenotes: this.$store.state.settings.showMyRenotes, includeMyRenotes: this.$store.state.settings.showMyRenotes,
includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
includeLocalRenotes: this.$store.state.settings.showLocalRenotes 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
};
}
}) })
}
}; };
}, },

View File

@ -1,8 +1,8 @@
<template> <template>
<div class="eamppglmnmimdhrlzhplwpvyeaqmmhxu"> <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"> <div class="placeholder" v-if="fetching">
<template v-for="i in 10"> <template v-for="i in 10">
@ -16,7 +16,6 @@
<mk-note <mk-note
:note="note" :note="note"
:key="note.id" :key="note.id"
@update:note="onNoteUpdated(i, $event)"
:compact="true" :compact="true"
/> />
<p class="date" :key="note.id + '_date'" v-if="i != notes.length - 1 && note._date != _notes[i + 1]._date"> <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 Vue from 'vue';
import i18n from '../../../i18n'; import i18n from '../../../i18n';
import shouldMuteNote from '../../../common/scripts/should-mute-note'; import shouldMuteNote from '../../../common/scripts/should-mute-note';
import paging from '../../../common/scripts/paging';
const displayLimit = 20;
export default Vue.extend({ export default Vue.extend({
i18n: i18n(), i18n: i18n(),
inject: ['column', 'isScrollTop', 'count'], inject: ['column', 'isScrollTop', 'count'],
mixins: [
paging({
displayLimit: 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: { props: {
makePromise: { pagination: {
required: true required: true
},
extract: {
required: false
} }
}, },
data() { computed: {
return { notes() {
rootEl: null, return this.extract ? this.extract(this.items) : this.items;
notes: [],
queue: [],
fetching: true,
moreFetching: false,
inited: false,
more: false
};
}, },
computed: {
_notes(): any[] { _notes(): any[] {
return (this.notes as any).map(note => { return (this.notes as any).map(note => {
const date = new Date(note.createdAt).getDate(); 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() { created() {
this.column.$on('top', this.onTop); this.column.$on('top', this.onTop);
this.column.$on('bottom', this.onBottom); this.column.$on('bottom', this.onBottom);
@ -101,87 +105,6 @@ export default Vue.extend({
focus() { 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(); (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);
},
releaseQueue() {
for (const n of this.queue) {
this.prepend(n, true);
}
this.queue = [];
},
onTop() {
this.releaseQueue();
},
onBottom() {
this.fetchMore();
}
} }
}); });
</script> </script>

View File

@ -81,15 +81,15 @@
</div> </div>
<template v-if="notification.type == 'quote'"> <template v-if="notification.type == 'quote'">
<mk-note :note="notification.note" @update:note="onNoteUpdated"/> <mk-note :note="notification.note"/>
</template> </template>
<template v-if="notification.type == 'reply'"> <template v-if="notification.type == 'reply'">
<mk-note :note="notification.note" @update:note="onNoteUpdated"/> <mk-note :note="notification.note"/>
</template> </template>
<template v-if="notification.type == 'mention'"> <template v-if="notification.type == 'mention'">
<mk-note :note="notification.note" @update:note="onNoteUpdated"/> <mk-note :note="notification.note"/>
</template> </template>
</div> </div>
</template> </template>
@ -105,17 +105,6 @@ export default Vue.extend({
getNoteSummary getNoteSummary
}; };
}, },
methods: {
onNoteUpdated(note) {
switch (this.notification.type) {
case 'quote':
case 'reply':
case 'mention':
Vue.set(this.notification, 'note', note);
break;
}
}
}
}); });
</script> </script>

View File

@ -5,7 +5,7 @@
</template> </template>
<div> <div>
<x-notes ref="timeline" :make-promise="makePromise" @inited="() => $emit('loaded')"/> <x-notes ref="timeline" :pagination="pagination" @inited="() => $emit('loaded')"/>
</div> </div>
</x-column> </x-column>
</template> </template>
@ -16,8 +16,6 @@ import XColumn from './deck.column.vue';
import XNotes from './deck.notes.vue'; import XNotes from './deck.notes.vue';
import { genSearchQuery } from '../../../common/scripts/gen-search-query'; import { genSearchQuery } from '../../../common/scripts/gen-search-query';
const limit = 20;
export default Vue.extend({ export default Vue.extend({
components: { components: {
XColumn, XColumn,
@ -26,24 +24,11 @@ export default Vue.extend({
data() { data() {
return { return {
makePromise: async cursor => this.$root.api('notes/search', { pagination: {
limit: limit + 1, endpoint: 'notes/search',
offset: cursor ? cursor : undefined, limit: 20,
...(await genSearchQuery(this, this.q)) params: () => 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
};
} }
})
}; };
}, },

View File

@ -6,7 +6,7 @@
</p> </p>
<p class="desc">{{ $t('disabled-timeline.description') }}</p> <p class="desc">{{ $t('disabled-timeline.description') }}</p>
</div> </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> </template>
<script lang="ts"> <script lang="ts">
@ -15,8 +15,6 @@ import XNotes from './deck.notes.vue';
import { faMinusCircle } from '@fortawesome/free-solid-svg-icons'; import { faMinusCircle } from '@fortawesome/free-solid-svg-icons';
import i18n from '../../../i18n'; import i18n from '../../../i18n';
const fetchLimit = 10;
export default Vue.extend({ export default Vue.extend({
i18n: i18n('deck'), i18n: i18n('deck'),
@ -42,7 +40,7 @@ export default Vue.extend({
connection: null, connection: null,
disabled: false, disabled: false,
faMinusCircle, faMinusCircle,
makePromise: null pagination: null
}; };
}, },
@ -73,27 +71,17 @@ export default Vue.extend({
}, },
created() { created() {
this.makePromise = cursor => this.$root.api(this.endpoint, { this.pagination = {
limit: fetchLimit + 1, endpoint: this.endpoint,
untilId: cursor ? cursor : undefined, limit: 10,
params: init => ({
untilDate: init ? undefined : (this.date ? this.date.getTime() : undefined),
withFiles: this.mediaOnly, withFiles: this.mediaOnly,
includeMyRenotes: this.$store.state.settings.showMyRenotes, includeMyRenotes: this.$store.state.settings.showMyRenotes,
includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
includeLocalRenotes: this.$store.state.settings.showLocalRenotes 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() { mounted() {

View File

@ -30,7 +30,7 @@
<ui-container> <ui-container>
<template #header><fa :icon="['far', 'comment-alt']"/> {{ $t('timeline') }}</template> <template #header><fa :icon="['far', 'comment-alt']"/> {{ $t('timeline') }}</template>
<div> <div>
<x-notes ref="timeline" :make-promise="makePromise" @inited="() => $emit('loaded')"/> <x-notes ref="timeline" :pagination="pagination" @inited="() => $emit('loaded')"/>
</div> </div>
</ui-container> </ui-container>
</div> </div>
@ -43,8 +43,6 @@ import XNotes from './deck.notes.vue';
import { concat } from '../../../../../prelude/array'; import { concat } from '../../../../../prelude/array';
import ApexCharts from 'apexcharts'; import ApexCharts from 'apexcharts';
const fetchLimit = 10;
export default Vue.extend({ export default Vue.extend({
i18n: i18n('deck/deck.user-column.vue'), i18n: i18n('deck/deck.user-column.vue'),
@ -63,49 +61,38 @@ export default Vue.extend({
return { return {
withFiles: false, withFiles: false,
images: [], images: [],
makePromise: null,
chart: null as ApexCharts 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: { watch: {
user() { user() {
this.fetch(); this.fetch();
this.genPromiseMaker();
} }
}, },
created() { created() {
this.fetch(); this.fetch();
this.genPromiseMaker();
}, },
methods: { methods: {
genPromiseMaker() {
this.makePromise = cursor => this.$root.api('users/notes', {
userId: this.user.id,
limit: fetchLimit + 1,
untilId: cursor ? cursor : undefined,
withFiles: this.withFiles,
includeMyRenotes: this.$store.state.settings.showMyRenotes,
includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
includeLocalRenotes: this.$store.state.settings.showLocalRenotes
}).then(notes => {
if (notes.length == fetchLimit + 1) {
notes.pop();
return {
notes: notes,
more: true
};
} else {
return {
notes: notes,
more: false
};
}
});
},
fetch() { fetch() {
const image = [ const image = [
'image/jpeg', 'image/jpeg',

View File

@ -18,24 +18,24 @@
</div> </div>
</ui-container> </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 }} <fa :icon="faHashtag" fixed-width/>{{ tag }}
</mk-user-list> </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') }}) <fa :icon="faHashtag" fixed-width/>{{ tag }} ({{ $t('federated') }})
</mk-user-list> </mk-user-list>
<template v-if="tag == null"> <template v-if="tag == null">
<mk-user-list :make-promise="pinnedUsers"> <mk-user-list :pagination="pinnedUsers">
<fa :icon="faBookmark" fixed-width/>{{ $t('pinned-users') }} <fa :icon="faBookmark" fixed-width/>{{ $t('pinned-users') }}
</mk-user-list> </mk-user-list>
<mk-user-list :make-promise="popularUsers"> <mk-user-list :pagination="popularUsers">
<fa :icon="faChartLine" fixed-width/>{{ $t('popular-users') }} <fa :icon="faChartLine" fixed-width/>{{ $t('popular-users') }}
</mk-user-list> </mk-user-list>
<mk-user-list :make-promise="recentlyUpdatedUsers"> <mk-user-list :pagination="recentlyUpdatedUsers">
<fa :icon="faCommentAlt" fixed-width/>{{ $t('recently-updated-users') }} <fa :icon="faCommentAlt" fixed-width/>{{ $t('recently-updated-users') }}
</mk-user-list> </mk-user-list>
<mk-user-list :make-promise="recentlyRegisteredUsers"> <mk-user-list :pagination="recentlyRegisteredUsers">
<fa :icon="faPlus" fixed-width/>{{ $t('recently-registered-users') }} <fa :icon="faPlus" fixed-width/>{{ $t('recently-registered-users') }}
</mk-user-list> </mk-user-list>
</template> </template>
@ -60,24 +60,21 @@ export default Vue.extend({
data() { data() {
return { return {
pinnedUsers: () => this.$root.api('pinned-users'), pinnedUsers: { endpoint: 'pinned-users' },
popularUsers: () => this.$root.api('users', { popularUsers: { endpoint: 'users', limit: 10, params: {
state: 'alive', state: 'alive',
origin: 'local', origin: 'local',
sort: '+follower', sort: '+follower',
limit: 10 } },
}), recentlyUpdatedUsers: { endpoint: 'users', limit: 10, params: {
recentlyUpdatedUsers: () => this.$root.api('users', {
origin: 'local', origin: 'local',
sort: '+updatedAt', sort: '+updatedAt',
limit: 10 } },
}), recentlyRegisteredUsers: { endpoint: 'users', limit: 10, params: {
recentlyRegisteredUsers: () => this.$root.api('users', {
origin: 'local', origin: 'local',
state: 'alive', state: 'alive',
sort: '+createdAt', sort: '+createdAt',
limit: 10 } },
}),
tagsLocal: [], tagsLocal: [],
tagsRemote: [], tagsRemote: [],
stats: null, stats: null,
@ -88,24 +85,30 @@ export default Vue.extend({
}, },
computed: { computed: {
tagUsers(): () => Promise<any> { tagUsers(): any {
return () => this.$root.api('hashtags/users', { return {
endpoint: 'hashtags/users',
limit: 30,
params: {
tag: this.tag, tag: this.tag,
state: 'alive', state: 'alive',
origin: 'local', origin: 'local',
sort: '+follower', sort: '+follower',
limit: 30 }
}); };
}, },
tagRemoteUsers(): () => Promise<any> { tagRemoteUsers(): any {
return () => this.$root.api('hashtags/users', { return {
endpoint: 'hashtags/users',
limit: 30,
params: {
tag: this.tag, tag: this.tag,
state: 'alive', state: 'alive',
origin: 'remote', origin: 'remote',
sort: '+follower', sort: '+follower',
limit: 30 }
}); };
}, },
}, },

View File

@ -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>

View File

@ -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: 30,
},
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>

View File

@ -1,7 +1,5 @@
<template> <template>
<div> <mk-user-list :pagination="pagination" :extract="items => items.map(item => item.follower)">{{ $t('@.followers') }}</mk-user-list>
<mk-user-list :make-promise="makePromise">{{ $t('@.followers') }}</mk-user-list>
</div>
</template> </template>
<script lang="ts"> <script lang="ts">
@ -9,31 +7,18 @@ import Vue from 'vue';
import parseAcct from '../../../../../misc/acct/parse'; import parseAcct from '../../../../../misc/acct/parse';
import i18n from '../../../i18n'; import i18n from '../../../i18n';
const fetchLimit = 30;
export default Vue.extend({ export default Vue.extend({
i18n: i18n(), i18n: i18n(),
data() { data() {
return { return {
makePromise: cursor => this.$root.api('users/followers', { pagination: {
endpoint: 'users/followers',
limit: 30,
params: {
...parseAcct(this.$route.params.user), ...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
};
} }
}), },
}; };
}, },
}); });

View File

@ -1,7 +1,5 @@
<template> <template>
<div> <mk-user-list :pagination="pagination" :extract="items => items.map(item => item.followee)">{{ $t('@.following') }}</mk-user-list>
<mk-user-list :make-promise="makePromise">{{ $t('@.following') }}</mk-user-list>
</div>
</template> </template>
<script lang="ts"> <script lang="ts">
@ -9,31 +7,18 @@ import Vue from 'vue';
import parseAcct from '../../../../../misc/acct/parse'; import parseAcct from '../../../../../misc/acct/parse';
import i18n from '../../../i18n'; import i18n from '../../../i18n';
const fetchLimit = 30;
export default Vue.extend({ export default Vue.extend({
i18n: i18n(), i18n: i18n(),
data() { data() {
return { return {
makePromise: cursor => this.$root.api('users/following', { pagination: {
endpoint: 'users/following',
limit: 30,
params: {
...parseAcct(this.$route.params.user), ...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
};
} }
}), },
}; };
}, },
}); });

View File

@ -138,10 +138,10 @@ init(async (launch, os) => {
{ path: '/notes/:note', name: 'note', component: () => import('../common/views/deck/deck.note-column.vue').then(m => m.default) }, { 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: '/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: '/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', 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: '/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/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', 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 }) }, { 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 +158,10 @@ init(async (launch, os) => {
{ path: '/notes/:note', name: 'note', component: () => import('./views/home/note.vue').then(m => m.default) }, { 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: '/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: '/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', 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: '/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/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', 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) }, { path: '/i/lists/:listId', props: true, component: () => import('../common/views/pages/user-list-editor.vue').then(m => m.default) },

View File

@ -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>

View File

@ -4,9 +4,9 @@
<div class="newer-indicator" :style="{ top: $store.state.uiHeaderHeight + 'px' }" v-show="queue.length > 0"></div> <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"> <div class="placeholder" v-if="fetching">
<template v-for="i in 10"> <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"> <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"> <template v-for="(note, i) in _notes">
<mk-note :note="note" :key="note.id" @update:note="onNoteUpdated(i, $event)" :compact="true" ref="note"/> <mk-note :note="note" :key="note.id" :compact="true" ref="note"/>
<p class="date" :key="note.id + '_date'" v-if="i != notes.length - 1 && note._date != _notes[i + 1]._date"> <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-up"/>{{ note._datetext }}</span>
<span><fa icon="angle-down"/>{{ _notes[i + 1]._datetext }}</span> <span><fa icon="angle-down"/>{{ _notes[i + 1]._datetext }}</span>
</p> </p>
@ -39,152 +39,66 @@ import Vue from 'vue';
import i18n from '../../../i18n'; import i18n from '../../../i18n';
import * as config from '../../../config'; import * as config from '../../../config';
import shouldMuteNote from '../../../common/scripts/should-mute-note'; import shouldMuteNote from '../../../common/scripts/should-mute-note';
import paging from '../../../common/scripts/paging';
const displayLimit = 30;
export default Vue.extend({ export default Vue.extend({
i18n: i18n(), i18n: i18n(),
props: { mixins: [
makePromise: { paging({
required: true captureWindowScroll: true,
onQueueChanged: (self, x) => {
if (x.length > 0) {
self.$store.commit('indicate', true);
} else {
self.$store.commit('indicate', false);
} }
}, },
data() { onPrepend: (self, note, silent) => {
return { //
notes: [], if (shouldMuteNote(self.$store.state.i, self.$store.state.settings, note)) return false;
queue: [],
fetching: true, //
moreFetching: false, if (document.hidden || !self.isScrollTop()) {
inited: false, self.$store.commit('pushBehindNote', note);
more: false }
};
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: {
pagination: {
required: true
},
}, },
computed: { computed: {
_notes(): any[] { _notes(): any[] {
return (this.notes as any).map(note => { return (this.items as any).map(item => {
const date = new Date(note.createdAt).getDate(); const date = new Date(item.createdAt).getDate();
const month = new Date(note.createdAt).getMonth() + 1; const month = new Date(item.createdAt).getMonth() + 1;
note._date = date; item._date = date;
note._datetext = this.$t('@.month-and-day').replace('{month}', month.toString()).replace('{day}', date.toString()); item._datetext = this.$t('@.month-and-day').replace('{month}', month.toString()).replace('{day}', date.toString());
return note; return item;
}); });
} }
}, },
created() {
this.init();
},
mounted() {
window.addEventListener('scroll', this.onScroll, { passive: true });
},
beforeDestroy() {
window.removeEventListener('scroll', this.onScroll);
},
methods: { methods: {
isScrollTop() {
return window.scrollY <= 8;
},
focus() { 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(); (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);
},
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> </script>

View File

@ -1,6 +1,6 @@
<template> <template>
<div> <div>
<mk-notes ref="timeline" :make-promise="makePromise" @inited="() => $emit('loaded')"> <mk-notes ref="timeline" :pagination="pagination" @inited="() => $emit('loaded')">
<template #header> <template #header>
<slot></slot> <slot></slot>
</template> </template>
@ -11,36 +11,23 @@
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
const fetchLimit = 10;
export default Vue.extend({ export default Vue.extend({
props: ['list'], props: ['list'],
data() { data() {
return { return {
connection: null, connection: null,
date: null, date: null,
makePromise: cursor => this.$root.api('notes/user-list-timeline', { pagination: {
endpoint: 'notes/user-list-timeline',
limit: 10,
params: init => ({
listId: this.list.id, listId: this.list.id,
limit: fetchLimit + 1, untilDate: init ? undefined : (this.date ? this.date.getTime() : undefined),
untilId: cursor ? cursor : undefined,
untilDate: cursor ? undefined : (this.date ? this.date.getTime() : undefined),
includeMyRenotes: this.$store.state.settings.showMyRenotes, includeMyRenotes: this.$store.state.settings.showMyRenotes,
includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
includeLocalRenotes: this.$store.state.settings.showLocalRenotes 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
};
}
}) })
}
}; };
}, },
watch: { watch: {

View File

@ -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>

View File

@ -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>

View File

@ -1,6 +1,6 @@
<template> <template>
<div> <div>
<mk-notes ref="timeline" :make-promise="makePromise" @inited="inited"> <mk-notes ref="timeline" :pagination="pagination" @inited="inited">
<template #header> <template #header>
<header class="oxgbmvii"> <header class="oxgbmvii">
<span><fa icon="search"/> {{ q }}</span> <span><fa icon="search"/> {{ q }}</span>
@ -16,30 +16,15 @@ import i18n from '../../../i18n';
import Progress from '../../../common/scripts/loading'; import Progress from '../../../common/scripts/loading';
import { genSearchQuery } from '../../../common/scripts/gen-search-query'; import { genSearchQuery } from '../../../common/scripts/gen-search-query';
const limit = 20;
export default Vue.extend({ export default Vue.extend({
i18n: i18n('desktop/views/pages/search.vue'), i18n: i18n('desktop/views/pages/search.vue'),
data() { data() {
return { return {
makePromise: async cursor => this.$root.api('notes/search', { pagination: {
limit: limit + 1, endpoint: 'notes/search',
offset: cursor ? cursor : undefined, limit: 20,
...(await genSearchQuery(this, this.q)) params: () => 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
};
} }
})
}; };
}, },
computed: { computed: {

View File

@ -1,6 +1,6 @@
<template> <template>
<div> <div>
<mk-notes ref="timeline" :make-promise="makePromise" @inited="inited"> <mk-notes ref="timeline" :pagination="pagination" @inited="inited">
<template #header> <template #header>
<header class="wqraeznr"> <header class="wqraeznr">
<span><fa icon="hashtag"/> {{ $route.params.tag }}</span> <span><fa icon="hashtag"/> {{ $route.params.tag }}</span>
@ -15,30 +15,17 @@ import Vue from 'vue';
import i18n from '../../../i18n'; import i18n from '../../../i18n';
import Progress from '../../../common/scripts/loading'; import Progress from '../../../common/scripts/loading';
const limit = 20;
export default Vue.extend({ export default Vue.extend({
i18n: i18n('desktop/views/pages/tag.vue'), i18n: i18n('desktop/views/pages/tag.vue'),
data() { data() {
return { return {
makePromise: cursor => this.$root.api('notes/search-by-tag', { pagination: {
limit: limit + 1, endpoint: 'notes/search-by-tag',
offset: cursor ? cursor : undefined, limit: 20,
params: {
tag: this.$route.params.tag 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
};
} }
}) }
}; };
}, },
watch: { watch: {

View File

@ -1,6 +1,6 @@
<template> <template>
<div> <div>
<mk-notes ref="timeline" :make-promise="makePromise" @inited="() => $emit('loaded')"> <mk-notes ref="timeline" :pagination="pagination" @inited="() => $emit('loaded')">
<template #header> <template #header>
<slot></slot> <slot></slot>
<div v-if="src == 'home' && alone" class="ibpylqas"> <div v-if="src == 'home' && alone" class="ibpylqas">
@ -16,8 +16,6 @@
import Vue from 'vue'; import Vue from 'vue';
import i18n from '../../../i18n'; import i18n from '../../../i18n';
const fetchLimit = 10;
export default Vue.extend({ export default Vue.extend({
i18n: i18n('desktop/views/components/timeline.core.vue'), i18n: i18n('desktop/views/components/timeline.core.vue'),
@ -42,7 +40,7 @@ export default Vue.extend({
}, },
query: {}, query: {},
endpoint: null, endpoint: null,
makePromise: null pagination: null
}; };
}, },
@ -109,25 +107,14 @@ export default Vue.extend({
this.connection.on('mention', onNote); this.connection.on('mention', onNote);
} }
this.makePromise = cursor => this.$root.api(this.endpoint, { this.pagination = {
limit: fetchLimit + 1, endpoint: this.endpoint,
untilDate: cursor ? undefined : (this.date ? this.date.getTime() : undefined), limit: 10,
untilId: cursor ? cursor : undefined, params: init => ({
untilDate: init ? undefined : (this.date ? this.date.getTime() : undefined),
...this.baseQuery, ...this.query ...this.baseQuery, ...this.query
}).then(notes => { })
if (notes.length == fetchLimit + 1) {
notes.pop();
return {
notes: notes,
more: true
}; };
} else {
return {
notes: notes,
more: false
};
}
});
}, },
methods: { methods: {

View File

@ -1,8 +1,8 @@
<template> <template>
<div> <div>
<mk-notes ref="timeline" :make-promise="makePromise" @inited="() => $emit('loaded')"> <mk-notes ref="timeline" :pagination="pagination" @inited="() => $emit('loaded')">
<template #header> <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 == '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-replies'" @click="mode = 'with-replies'"><fa icon="comments"/> {{ $t('with-replies') }}</span>
<span :data-active="mode == 'with-media'" @click="mode = 'with-media'"><fa :icon="['far', 'images']"/> {{ $t('with-media') }}</span> <span :data-active="mode == 'with-media'" @click="mode = 'with-media'"><fa :icon="['far', 'images']"/> {{ $t('with-media') }}</span>
@ -17,8 +17,6 @@
import Vue from 'vue'; import Vue from 'vue';
import i18n from '../../../../i18n'; import i18n from '../../../../i18n';
const fetchLimit = 10;
export default Vue.extend({ export default Vue.extend({
i18n: i18n('desktop/views/pages/user/user.timeline.vue'), i18n: i18n('desktop/views/pages/user/user.timeline.vue'),
@ -30,28 +28,17 @@ export default Vue.extend({
mode: 'default', mode: 'default',
unreadCount: 0, unreadCount: 0,
date: null, date: null,
makePromise: cursor => this.$root.api('users/notes', { pagination: {
endpoint: 'users/notes',
limit: 10,
params: init => ({
userId: this.user.id, userId: this.user.id,
limit: fetchLimit + 1, untilDate: init ? undefined : (this.date ? this.date.getTime() : undefined),
includeReplies: this.mode == 'with-replies', includeReplies: this.mode == 'with-replies',
includeMyRenotes: this.mode != 'my-posts', includeMyRenotes: this.mode != 'my-posts',
withFiles: this.mode == 'with-media', 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
};
}
}) })
}
}; };
}, },
@ -88,7 +75,7 @@ export default Vue.extend({
</script> </script>
<style lang="stylus" scoped> <style lang="stylus" scoped>
.oh5y2r7l5lx8j6jj791ykeiwgihheguk .kugajpep
padding 0 8px padding 0 8px
z-index 10 z-index 10
background var(--faceHeader) background var(--faceHeader)

View File

@ -123,10 +123,10 @@ init((launch, os) => {
{ path: '/notes/:note', name: 'note', component: () => import('../common/views/deck/deck.note-column.vue').then(m => m.default) }, { 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: '/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: '/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', 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: '/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/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', 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 }) }, { 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,7 +138,7 @@ init((launch, os) => {
]), ]),
{ path: '/signup', name: 'signup', component: MkSignup }, { path: '/signup', name: 'signup', component: MkSignup },
{ path: '/i/settings', name: 'settings', component: () => import('./views/pages/settings.vue').then(m => m.default) }, { 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/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', 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/lists/:list', component: UI, props: route => ({ component: () => import('../common/views/pages/user-list-editor.vue').then(m => m.default), listId: route.params.list }) },
@ -157,7 +157,7 @@ init((launch, os) => {
{ path: '/selectdrive', component: MkSelectDrive }, { path: '/selectdrive', component: MkSelectDrive },
{ path: '/search', component: MkSearch }, { path: '/search', component: MkSearch },
{ path: '/tags/:tag', component: MkTag }, { 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', 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: '/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 }, { path: '/share', component: MkShare },

View File

@ -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>

View File

@ -1,8 +1,8 @@
<template> <template>
<div class="ivaojijs" :class="{ shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners }"> <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"> <div class="placeholder" v-if="fetching">
<template v-for="i in 10"> <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"> <component :is="!$store.state.device.reduceMotion ? 'transition-group' : 'div'" name="mk-notes" class="transition" tag="div">
<template v-for="(note, i) in _notes"> <template v-for="(note, i) in _notes">
<mk-note :note="note" :key="note.id" @update:note="onNoteUpdated(i, $event)"/> <mk-note :note="note" :key="note.id"/>
<p class="date" :key="note.id + '_date'" v-if="i != notes.length - 1 && note._date != _notes[i + 1]._date"> <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-up"/>{{ note._datetext }}</span>
<span><fa icon="angle-down"/>{{ _notes[i + 1]._datetext }}</span> <span><fa icon="angle-down"/>{{ _notes[i + 1]._datetext }}</span>
</p> </p>
@ -34,157 +34,52 @@
import Vue from 'vue'; import Vue from 'vue';
import i18n from '../../../i18n'; import i18n from '../../../i18n';
import shouldMuteNote from '../../../common/scripts/should-mute-note'; import shouldMuteNote from '../../../common/scripts/should-mute-note';
import paging from '../../../common/scripts/paging';
const displayLimit = 30;
export default Vue.extend({ export default Vue.extend({
i18n: i18n(), i18n: i18n(),
props: { mixins: [
makePromise: { paging({
required: true captureWindowScroll: true,
onQueueChanged: (self, x) => {
if (x.length > 0) {
self.$store.commit('indicate', true);
} else {
self.$store.commit('indicate', false);
} }
}, },
data() { onPrepend: (self, note) => {
return { //
notes: [], if (shouldMuteNote(self.$store.state.i, self.$store.state.settings, note)) return false;
queue: [],
fetching: true, //
moreFetching: false, if (document.hidden || !self.isScrollTop()) {
inited: false, self.$store.commit('pushBehindNote', note);
more: false }
}; }
}),
],
props: {
pagination: {
required: true
},
}, },
computed: { computed: {
_notes(): any[] { _notes(): any[] {
return (this.notes as any).map(note => { return (this.items as any).map(item => {
const date = new Date(note.createdAt).getDate(); const date = new Date(item.createdAt).getDate();
const month = new Date(note.createdAt).getMonth() + 1; const month = new Date(item.createdAt).getMonth() + 1;
note._date = date; item._date = date;
note._datetext = this.$t('@.month-and-day').replace('{month}', month.toString()).replace('{day}', date.toString()); item._datetext = this.$t('@.month-and-day').replace('{month}', month.toString()).replace('{day}', date.toString());
return note; return item;
}); });
} }
}, },
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);
},
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;
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.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;
}
} else {
this.queue.push(note);
}
},
append(note) {
this.notes.push(note);
},
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) {
// 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.fetchMore();
}
}
}
}); });
</script> </script>

View File

@ -71,15 +71,15 @@
</div> </div>
<template v-if="notification.type == 'quote'"> <template v-if="notification.type == 'quote'">
<mk-note :note="notification.note" @update:note="onNoteUpdated"/> <mk-note :note="notification.note"/>
</template> </template>
<template v-if="notification.type == 'reply'"> <template v-if="notification.type == 'reply'">
<mk-note :note="notification.note" @update:note="onNoteUpdated"/> <mk-note :note="notification.note"/>
</template> </template>
<template v-if="notification.type == 'mention'"> <template v-if="notification.type == 'mention'">
<mk-note :note="notification.note" @update:note="onNoteUpdated"/> <mk-note :note="notification.note"/>
</template> </template>
</div> </div>
</template> </template>
@ -95,17 +95,6 @@ export default Vue.extend({
getNoteSummary getNoteSummary
}; };
}, },
methods: {
onNoteUpdated(note) {
switch (this.notification.type) {
case 'quote':
case 'reply':
case 'mention':
Vue.set(this.notification, 'note', note);
break;
}
}
}
}); });
</script> </script>

View File

@ -10,41 +10,49 @@
<component :is="!$store.state.device.reduceMotion ? 'transition-group' : 'div'" name="mk-notifications" class="transition notifications" tag="div"> <component :is="!$store.state.device.reduceMotion ? 'transition-group' : 'div'" name="mk-notifications" class="transition notifications" tag="div">
<template v-for="(notification, i) in _notifications"> <template v-for="(notification, i) in _notifications">
<mk-notification :notification="notification" :key="notification.id"/> <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-up"/>{{ notification._datetext }}</span>
<span><fa icon="angle-down"/>{{ _notifications[i + 1]._datetext }}</span> <span><fa icon="angle-down"/>{{ _notifications[i + 1]._datetext }}</span>
</p> </p>
</template> </template>
</component> </component>
<button class="more" v-if="moreNotifications" @click="fetchMoreNotifications" :disabled="fetchingMoreNotifications"> <button class="more" v-if="more" @click="fetchMore" :disabled="moreFetching">
<template v-if="fetchingMoreNotifications"><fa icon="spinner" pulse fixed-width/></template> <template v-if="moreFetching"><fa icon="spinner" pulse fixed-width/></template>
{{ fetchingMoreNotifications ? $t('@.loading') : $t('@.load-more') }} {{ moreFetching ? $t('@.loading') : $t('@.load-more') }}
</button> </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> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import i18n from '../../../i18n'; import i18n from '../../../i18n';
import paging from '../../../common/scripts/paging';
export default Vue.extend({ export default Vue.extend({
i18n: i18n('mobile/views/components/notifications.vue'), i18n: i18n('mobile/views/components/notifications.vue'),
mixins: [
paging({}),
],
data() { data() {
return { return {
fetching: true, connection: null,
fetchingMoreNotifications: false, pagination: {
notifications: [], endpoint: 'i/notifications',
moreNotifications: false, limit: 15,
connection: null }
}; };
}, },
computed: { computed: {
_notifications(): any[] { _notifications(): any[] {
return (this.notifications as any).map(notification => { return (this.items as any).map(notification => {
const date = new Date(notification.createdAt).getDate(); const date = new Date(notification.createdAt).getDate();
const month = new Date(notification.createdAt).getMonth() + 1; const month = new Date(notification.createdAt).getMonth() + 1;
notification._date = date; notification._date = date;
@ -55,76 +63,23 @@ export default Vue.extend({
}, },
mounted() { mounted() {
window.addEventListener('scroll', this.onScroll, { passive: true });
this.connection = this.$root.stream.useSharedConnection('main'); this.connection = this.$root.stream.useSharedConnection('main');
this.connection.on('notification', this.onNotification); 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() { beforeDestroy() {
window.removeEventListener('scroll', this.onScroll);
this.connection.dispose(); this.connection.dispose();
}, },
methods: { 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) { onNotification(notification) {
// TODO: () // TODO: ()
this.$root.stream.send('readNotification', { this.$root.stream.send('readNotification', {
id: notification.id 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> </script>

View File

@ -1,14 +1,10 @@
<template> <template>
<div> <mk-notes ref="timeline" :pagination="pagination" @inited="() => $emit('loaded')"/>
<mk-notes ref="timeline" :make-promise="makePromise" @inited="() => $emit('loaded')"/>
</div>
</template> </template>
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
const fetchLimit = 10;
export default Vue.extend({ export default Vue.extend({
props: ['list'], props: ['list'],
@ -16,28 +12,17 @@ export default Vue.extend({
return { return {
connection: null, connection: null,
date: null, date: null,
makePromise: cursor => this.$root.api('notes/user-list-timeline', { pagination: {
endpoint: 'notes/user-list-timeline',
limit: 10,
params: init => ({
listId: this.list.id, listId: this.list.id,
limit: fetchLimit + 1, untilDate: init ? undefined : (this.date ? this.date.getTime() : undefined),
untilId: cursor ? cursor : undefined,
untilDate: cursor ? undefined : (this.date ? this.date.getTime() : undefined),
includeMyRenotes: this.$store.state.settings.showMyRenotes, includeMyRenotes: this.$store.state.settings.showMyRenotes,
includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
includeLocalRenotes: this.$store.state.settings.showLocalRenotes 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
};
}
}) })
}
}; };
}, },

View File

@ -1,15 +1,11 @@
<template> <template>
<div class="mk-user-timeline"> <mk-notes ref="timeline" :pagination="pagination" @inited="() => $emit('loaded')"/>
<mk-notes ref="timeline" :make-promise="makePromise" @inited="() => $emit('loaded')"/>
</div>
</template> </template>
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import i18n from '../../../i18n'; import i18n from '../../../i18n';
const fetchLimit = 10;
export default Vue.extend({ export default Vue.extend({
i18n: i18n('mobile/views/components/user-timeline.vue'), i18n: i18n('mobile/views/components/user-timeline.vue'),
@ -18,26 +14,15 @@ export default Vue.extend({
data() { data() {
return { return {
date: null, date: null,
makePromise: cursor => this.$root.api('users/notes', { pagination: {
endpoint: 'users/notes',
limit: 10,
params: init => ({
userId: this.user.id, userId: this.user.id,
limit: fetchLimit + 1,
withFiles: this.withMedia, withFiles: this.withMedia,
untilDate: cursor ? undefined : (this.date ? this.date.getTime() : undefined), untilDate: init ? 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
};
}
}) })
}
}; };
}, },

View File

@ -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>

View File

@ -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>

View File

@ -7,7 +7,7 @@
</div> </div>
</ui-container> </ui-container>
<mk-notes ref="timeline" :make-promise="makePromise" @inited="() => $emit('loaded')"/> <mk-notes ref="timeline" :pagination="pagination" @inited="() => $emit('loaded')"/>
</div> </div>
</template> </template>
@ -15,8 +15,6 @@
import Vue from 'vue'; import Vue from 'vue';
import i18n from '../../../i18n'; import i18n from '../../../i18n';
const fetchLimit = 10;
export default Vue.extend({ export default Vue.extend({
i18n: i18n('mobile/views/pages/home.timeline.vue'), i18n: i18n('mobile/views/pages/home.timeline.vue'),
@ -43,7 +41,7 @@ export default Vue.extend({
}, },
query: {}, query: {},
endpoint: null, endpoint: null,
makePromise: null pagination: null
}; };
}, },
@ -110,25 +108,14 @@ export default Vue.extend({
this.connection.on('mention', onNote); this.connection.on('mention', onNote);
} }
this.makePromise = cursor => this.$root.api(this.endpoint, { this.pagination = {
limit: fetchLimit + 1, endpoint: this.endpoint,
untilDate: cursor ? undefined : (this.date ? this.date.getTime() : undefined), limit: 10,
untilId: cursor ? cursor : undefined, params: init => ({
untilDate: init ? undefined : (this.date ? this.date.getTime() : undefined),
...this.baseQuery, ...this.query ...this.baseQuery, ...this.query
}).then(notes => { })
if (notes.length == fetchLimit + 1) {
notes.pop();
return {
notes: notes,
more: true
}; };
} else {
return {
notes: notes,
more: false
};
}
});
}, },
methods: { methods: {

View File

@ -3,7 +3,7 @@
<template #header><fa icon="search"/> {{ q }}</template> <template #header><fa icon="search"/> {{ q }}</template>
<main> <main>
<mk-notes ref="timeline" :make-promise="makePromise" @inited="inited"/> <mk-notes ref="timeline" :pagination="pagination" @inited="inited"/>
</main> </main>
</mk-ui> </mk-ui>
</template> </template>
@ -14,42 +14,27 @@ import i18n from '../../../i18n';
import Progress from '../../../common/scripts/loading'; import Progress from '../../../common/scripts/loading';
import { genSearchQuery } from '../../../common/scripts/gen-search-query'; import { genSearchQuery } from '../../../common/scripts/gen-search-query';
const limit = 20;
export default Vue.extend({ export default Vue.extend({
i18n: i18n('mobile/views/pages/search.vue'), i18n: i18n('mobile/views/pages/search.vue'),
data() { data() {
return { return {
makePromise: async cursor => this.$root.api('notes/search', { pagination: {
limit: limit + 1, endpoint: 'notes/search',
untilId: cursor ? cursor : undefined, limit: 20,
...(await genSearchQuery(this, this.q)) params: () => genSearchQuery(this, this.q)
}).then(notes => {
if (notes.length == limit + 1) {
notes.pop();
return {
notes: notes,
more: true
};
} else {
return {
notes: notes,
more: false
};
} }
})
}; };
}, },
watch: {
$route() {
this.$refs.timeline.reload();
}
},
computed: { computed: {
q(): string { q(): string {
return this.$route.query.q; return this.$route.query.q;
} }
}, },
watch: {
$route() {
this.$refs.timeline.reload();
}
},
mounted() { mounted() {
document.title = `${this.$t('search')}: ${this.q} | ${this.$root.instanceName}`; document.title = `${this.$t('search')}: ${this.q} | ${this.$root.instanceName}`;
}, },

View File

@ -3,7 +3,7 @@
<template #header><span style="margin-right:4px;"><fa icon="hashtag"/></span>{{ $route.params.tag }}</template> <template #header><span style="margin-right:4px;"><fa icon="hashtag"/></span>{{ $route.params.tag }}</template>
<main> <main>
<mk-notes ref="timeline" :make-promise="makePromise" @inited="inited"/> <mk-notes ref="timeline" :pagination="pagination" @inited="inited"/>
</main> </main>
</mk-ui> </mk-ui>
</template> </template>
@ -13,30 +13,17 @@ import Vue from 'vue';
import i18n from '../../../i18n'; import i18n from '../../../i18n';
import Progress from '../../../common/scripts/loading'; import Progress from '../../../common/scripts/loading';
const limit = 20;
export default Vue.extend({ export default Vue.extend({
i18n: i18n('mobile/views/pages/tag.vue'), i18n: i18n('mobile/views/pages/tag.vue'),
data() { data() {
return { return {
makePromise: cursor => this.$root.api('notes/search-by-tag', { pagination: {
limit: limit + 1, endpoint: 'notes/search-by-tag',
untilId: cursor ? cursor : undefined, limit: 20,
params: {
tag: this.$route.params.tag 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
};
} }
}) }
}; };
}, },
watch: { watch: {