parent
a10be38d0e
commit
586c11251a
|
@ -1,44 +0,0 @@
|
||||||
<template>
|
|
||||||
<FormSlot>
|
|
||||||
<template #label><slot name="label"></slot></template>
|
|
||||||
<div class="abcaccfa">
|
|
||||||
<slot :items="items"></slot>
|
|
||||||
<div v-if="empty" key="_empty_" class="empty">
|
|
||||||
<slot name="empty"></slot>
|
|
||||||
</div>
|
|
||||||
<MkButton v-show="more" class="button" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" @click="fetchMore">
|
|
||||||
<template v-if="!moreFetching">{{ $ts.loadMore }}</template>
|
|
||||||
<template v-if="moreFetching"><MkLoading inline/></template>
|
|
||||||
</MkButton>
|
|
||||||
</div>
|
|
||||||
</FormSlot>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent } from 'vue';
|
|
||||||
import MkButton from '@/components/ui/button.vue';
|
|
||||||
import FormSlot from './slot.vue';
|
|
||||||
import paging from '@/scripts/paging';
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
components: {
|
|
||||||
MkButton,
|
|
||||||
FormSlot,
|
|
||||||
},
|
|
||||||
|
|
||||||
mixins: [
|
|
||||||
paging({}),
|
|
||||||
],
|
|
||||||
|
|
||||||
props: {
|
|
||||||
pagination: {
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.abcaccfa {
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,114 +1,49 @@
|
||||||
<template>
|
<template>
|
||||||
<transition name="fade" mode="out-in">
|
<MkPagination ref="pagingComponent" :pagination="pagination">
|
||||||
<MkLoading v-if="fetching"/>
|
<template #empty>
|
||||||
|
<div class="_fullinfo">
|
||||||
<MkError v-else-if="error" @retry="init()"/>
|
<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
|
||||||
|
<div>{{ $ts.noNotes }}</div>
|
||||||
<div v-else-if="empty" class="_fullinfo">
|
|
||||||
<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
|
|
||||||
<div>{{ $ts.noNotes }}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else class="giivymft" :class="{ noGap }">
|
|
||||||
<div v-show="more && reversed" style="margin-bottom: var(--margin);">
|
|
||||||
<MkButton style="margin: 0 auto;" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" @click="fetchMoreFeature">
|
|
||||||
<template v-if="!moreFetching">{{ $ts.loadMore }}</template>
|
|
||||||
<template v-if="moreFetching"><MkLoading inline/></template>
|
|
||||||
</MkButton>
|
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<XList ref="notes" v-slot="{ item: note }" :items="notes" :direction="reversed ? 'up' : 'down'" :reversed="reversed" :no-gap="noGap" :ad="true" class="notes">
|
<template #default="{ items: notes }">
|
||||||
<XNote :key="note._featuredId_ || note._prId_ || note.id" class="qtqtichx" :note="note" @update:note="updated(note, $event)"/>
|
<div class="giivymft" :class="{ noGap }">
|
||||||
</XList>
|
<XList ref="notes" v-slot="{ item: note }" :items="notes" :direction="pagination.reversed ? 'up' : 'down'" :reversed="pagination.reversed" :no-gap="noGap" :ad="true" class="notes">
|
||||||
|
<XNote :key="note._featuredId_ || note._prId_ || note.id" class="qtqtichx" :note="note" @update:note="updated(note, $event)"/>
|
||||||
<div v-show="more && !reversed" style="margin-top: var(--margin);">
|
</XList>
|
||||||
<MkButton v-appear="$store.state.enableInfiniteScroll ? fetchMore : null" style="margin: 0 auto;" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" @click="fetchMore">
|
|
||||||
<template v-if="!moreFetching">{{ $ts.loadMore }}</template>
|
|
||||||
<template v-if="moreFetching"><MkLoading inline/></template>
|
|
||||||
</MkButton>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
</transition>
|
</MkPagination>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts" setup>
|
||||||
import { defineComponent } from 'vue';
|
import { ref } from 'vue';
|
||||||
import paging from '@/scripts/paging';
|
import XNote from '@/components/note.vue';
|
||||||
import XNote from './note.vue';
|
import XList from '@/components/date-separated-list.vue';
|
||||||
import XList from './date-separated-list.vue';
|
import MkPagination from '@/components/ui/pagination.vue';
|
||||||
import MkButton from '@/components/ui/button.vue';
|
import { Paging } from '@/components/ui/pagination.vue';
|
||||||
|
|
||||||
export default defineComponent({
|
const props = defineProps<{
|
||||||
components: {
|
pagination: Paging;
|
||||||
XNote, XList, MkButton,
|
noGap?: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const pagingComponent = ref<InstanceType<typeof MkPagination>>();
|
||||||
|
|
||||||
|
const updated = (oldValue, newValue) => {
|
||||||
|
const i = pagingComponent.value.items.findIndex(n => n === oldValue);
|
||||||
|
pagingComponent.value.items[i] = newValue;
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
prepend: (note) => {
|
||||||
|
pagingComponent.value?.prepend(note);
|
||||||
},
|
},
|
||||||
|
|
||||||
mixins: [
|
|
||||||
paging({
|
|
||||||
before: (self) => {
|
|
||||||
self.$emit('before');
|
|
||||||
},
|
|
||||||
|
|
||||||
after: (self, e) => {
|
|
||||||
self.$emit('after', e);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
|
|
||||||
props: {
|
|
||||||
pagination: {
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
prop: {
|
|
||||||
type: String,
|
|
||||||
required: false
|
|
||||||
},
|
|
||||||
noGap: {
|
|
||||||
type: Boolean,
|
|
||||||
required: false,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
emits: ['before', 'after'],
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
notes(): any[] {
|
|
||||||
return this.prop ? this.items.map(item => item[this.prop]) : this.items;
|
|
||||||
},
|
|
||||||
|
|
||||||
reversed(): boolean {
|
|
||||||
return this.pagination.reversed;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
updated(oldValue, newValue) {
|
|
||||||
const i = this.notes.findIndex(n => n === oldValue);
|
|
||||||
if (this.prop) {
|
|
||||||
this.items[i][this.prop] = newValue;
|
|
||||||
} else {
|
|
||||||
this.items[i] = newValue;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
focus() {
|
|
||||||
this.$refs.notes.focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.fade-enter-active,
|
|
||||||
.fade-leave-active {
|
|
||||||
transition: opacity 0.125s ease;
|
|
||||||
}
|
|
||||||
.fade-enter-from,
|
|
||||||
.fade-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.giivymft {
|
.giivymft {
|
||||||
&.noGap {
|
&.noGap {
|
||||||
> .notes {
|
> .notes {
|
||||||
|
|
|
@ -1,159 +1,85 @@
|
||||||
<template>
|
<template>
|
||||||
<transition name="fade" mode="out-in">
|
<MkPagination ref="pagingComponent" :pagination="pagination">
|
||||||
<MkLoading v-if="fetching"/>
|
<template #empty>
|
||||||
|
<div class="_fullinfo">
|
||||||
|
<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
|
||||||
|
<div>{{ $ts.noNotifications }}</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<MkError v-else-if="error" @retry="init()"/>
|
<template #default="{ items: notifications }">
|
||||||
|
<XList v-slot="{ item: notification }" class="elsfgstc" :items="notifications" :no-gap="true">
|
||||||
<p v-else-if="empty" class="mfcuwfyp">{{ $ts.noNotifications }}</p>
|
|
||||||
|
|
||||||
<div v-else>
|
|
||||||
<XList v-slot="{ item: notification }" class="elsfgstc" :items="items" :no-gap="true">
|
|
||||||
<XNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id" :note="notification.note" @update:note="noteUpdated(notification.note, $event)"/>
|
<XNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id" :note="notification.note" @update:note="noteUpdated(notification.note, $event)"/>
|
||||||
<XNotification v-else :key="notification.id" :notification="notification" :with-time="true" :full="true" class="_panel notification"/>
|
<XNotification v-else :key="notification.id" :notification="notification" :with-time="true" :full="true" class="_panel notification"/>
|
||||||
</XList>
|
</XList>
|
||||||
|
</template>
|
||||||
<MkButton v-show="more" v-appear="$store.state.enableInfiniteScroll ? fetchMore : null" primary style="margin: var(--margin) auto;" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" @click="fetchMore">
|
</MkPagination>
|
||||||
<template v-if="!moreFetching">{{ $ts.loadMore }}</template>
|
|
||||||
<template v-if="moreFetching"><MkLoading inline/></template>
|
|
||||||
</MkButton>
|
|
||||||
</div>
|
|
||||||
</transition>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts" setup>
|
||||||
import { defineComponent, PropType, markRaw } from 'vue';
|
import { defineComponent, PropType, markRaw, onUnmounted, onMounted, computed, ref } from 'vue';
|
||||||
import paging from '@/scripts/paging';
|
|
||||||
import XNotification from './notification.vue';
|
|
||||||
import XList from './date-separated-list.vue';
|
|
||||||
import XNote from './note.vue';
|
|
||||||
import { notificationTypes } from 'misskey-js';
|
import { notificationTypes } from 'misskey-js';
|
||||||
|
import MkPagination from '@/components/ui/pagination.vue';
|
||||||
|
import { Paging } from '@/components/ui/pagination.vue';
|
||||||
|
import XNotification from '@/components/notification.vue';
|
||||||
|
import XList from '@/components/date-separated-list.vue';
|
||||||
|
import XNote from '@/components/note.vue';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import { stream } from '@/stream';
|
import { stream } from '@/stream';
|
||||||
import MkButton from '@/components/ui/button.vue';
|
import { $i } from '@/account';
|
||||||
|
|
||||||
export default defineComponent({
|
const props = defineProps<{
|
||||||
components: {
|
includeTypes?: PropType<typeof notificationTypes[number][]>;
|
||||||
XNotification,
|
unreadOnly?: boolean;
|
||||||
XList,
|
}>();
|
||||||
XNote,
|
|
||||||
MkButton,
|
|
||||||
},
|
|
||||||
|
|
||||||
mixins: [
|
const pagingComponent = ref<InstanceType<typeof MkPagination>>();
|
||||||
paging({}),
|
|
||||||
],
|
|
||||||
|
|
||||||
props: {
|
const allIncludeTypes = computed(() => props.includeTypes ?? notificationTypes.filter(x => !$i.mutingNotificationTypes.includes(x)));
|
||||||
includeTypes: {
|
|
||||||
type: Array as PropType<typeof notificationTypes[number][]>,
|
|
||||||
required: false,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
unreadOnly: {
|
|
||||||
type: Boolean,
|
|
||||||
required: false,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
const pagination: Paging = {
|
||||||
return {
|
endpoint: 'i/notifications' as const,
|
||||||
connection: null,
|
limit: 10,
|
||||||
pagination: {
|
params: computed(() => ({
|
||||||
endpoint: 'i/notifications',
|
includeTypes: allIncludeTypes.value || undefined,
|
||||||
limit: 10,
|
unreadOnly: props.unreadOnly,
|
||||||
params: () => ({
|
})),
|
||||||
includeTypes: this.allIncludeTypes || undefined,
|
};
|
||||||
unreadOnly: this.unreadOnly,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
const onNotification = (notification) => {
|
||||||
allIncludeTypes() {
|
const isMuted = !allIncludeTypes.value.includes(notification.type);
|
||||||
return this.includeTypes ?? notificationTypes.filter(x => !this.$i.mutingNotificationTypes.includes(x));
|
if (isMuted || document.visibilityState === 'visible') {
|
||||||
}
|
stream.send('readNotification', {
|
||||||
},
|
id: notification.id
|
||||||
|
});
|
||||||
watch: {
|
|
||||||
includeTypes: {
|
|
||||||
handler() {
|
|
||||||
this.reload();
|
|
||||||
},
|
|
||||||
deep: true
|
|
||||||
},
|
|
||||||
unreadOnly: {
|
|
||||||
handler() {
|
|
||||||
this.reload();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// TODO: vue/vuexのバグか仕様かは不明なものの、プロフィール更新するなどして $i が更新されると、
|
|
||||||
// mutingNotificationTypes に変化が無くてもこのハンドラーが呼び出され無駄なリロードが発生するのを直す
|
|
||||||
'$i.mutingNotificationTypes': {
|
|
||||||
handler() {
|
|
||||||
if (this.includeTypes === null) {
|
|
||||||
this.reload();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
deep: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
mounted() {
|
|
||||||
this.connection = markRaw(stream.useChannel('main'));
|
|
||||||
this.connection.on('notification', this.onNotification);
|
|
||||||
},
|
|
||||||
|
|
||||||
beforeUnmount() {
|
|
||||||
this.connection.dispose();
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
onNotification(notification) {
|
|
||||||
const isMuted = !this.allIncludeTypes.includes(notification.type);
|
|
||||||
if (isMuted || document.visibilityState === 'visible') {
|
|
||||||
stream.send('readNotification', {
|
|
||||||
id: notification.id
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isMuted) {
|
|
||||||
this.prepend({
|
|
||||||
...notification,
|
|
||||||
isRead: document.visibilityState === 'visible'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
noteUpdated(oldValue, newValue) {
|
|
||||||
const i = this.items.findIndex(n => n.note === oldValue);
|
|
||||||
this.items[i] = {
|
|
||||||
...this.items[i],
|
|
||||||
note: newValue
|
|
||||||
};
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isMuted) {
|
||||||
|
pagingComponent.value.prepend({
|
||||||
|
...notification,
|
||||||
|
isRead: document.visibilityState === 'visible'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const noteUpdated = (oldValue, newValue) => {
|
||||||
|
const i = pagingComponent.value.items.findIndex(n => n.note === oldValue);
|
||||||
|
pagingComponent.value.items[i] = {
|
||||||
|
...pagingComponent.value.items[i],
|
||||||
|
note: newValue,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const connection = stream.useChannel('main');
|
||||||
|
connection.on('notification', onNotification);
|
||||||
|
onUnmounted(() => {
|
||||||
|
connection.dispose();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.fade-enter-active,
|
|
||||||
.fade-leave-active {
|
|
||||||
transition: opacity 0.125s ease;
|
|
||||||
}
|
|
||||||
.fade-enter-from,
|
|
||||||
.fade-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mfcuwfyp {
|
|
||||||
margin: 0;
|
|
||||||
padding: 16px;
|
|
||||||
text-align: center;
|
|
||||||
color: var(--fg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.elsfgstc {
|
.elsfgstc {
|
||||||
background: var(--panel);
|
background: var(--panel);
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,43 +13,247 @@
|
||||||
</slot>
|
</slot>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="cxiknjgy">
|
<div v-else ref="rootEl">
|
||||||
<slot :items="items"></slot>
|
<slot :items="items"></slot>
|
||||||
<div v-show="more" key="_more_" class="more _gap">
|
<div v-show="more" key="_more_" class="cxiknjgy _gap">
|
||||||
<MkButton v-appear="($store.state.enableInfiniteScroll && !disableAutoLoad) ? fetchMore : null" class="button" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary @click="fetchMore">
|
<MkButton v-if="!moreFetching" v-appear="($store.state.enableInfiniteScroll && !disableAutoLoad) ? fetchMore : null" class="button" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary @click="fetchMore">
|
||||||
<template v-if="!moreFetching">{{ $ts.loadMore }}</template>
|
{{ $ts.loadMore }}
|
||||||
<template v-if="moreFetching"><MkLoading inline/></template>
|
|
||||||
</MkButton>
|
</MkButton>
|
||||||
|
<MkLoading v-else class="loading"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts" setup>
|
||||||
import { defineComponent } from 'vue';
|
import { computed, ComputedRef, isRef, markRaw, onActivated, onDeactivated, Ref, ref, watch } from 'vue';
|
||||||
import MkButton from './button.vue';
|
import * as misskey from 'misskey-js';
|
||||||
import paging from '@/scripts/paging';
|
import * as os from '@/os';
|
||||||
|
import { onScrollTop, isTopVisible, getScrollPosition, getScrollContainer } from '@/scripts/scroll';
|
||||||
|
import MkButton from '@/components/ui/button.vue';
|
||||||
|
|
||||||
export default defineComponent({
|
const SECOND_FETCH_LIMIT = 30;
|
||||||
components: {
|
|
||||||
MkButton
|
|
||||||
},
|
|
||||||
|
|
||||||
mixins: [
|
export type Paging<E extends keyof misskey.Endpoints = keyof misskey.Endpoints> = {
|
||||||
paging({}),
|
endpoint: E;
|
||||||
],
|
limit: number;
|
||||||
|
params?: misskey.Endpoints[E]['req'] | ComputedRef<misskey.Endpoints[E]['req']>;
|
||||||
|
|
||||||
props: {
|
/**
|
||||||
pagination: {
|
* 検索APIのような、ページング不可なエンドポイントを利用する場合
|
||||||
required: true
|
* (そのようなAPIをこの関数で使うのは若干矛盾してるけど)
|
||||||
},
|
*/
|
||||||
|
noPaging?: boolean;
|
||||||
|
|
||||||
disableAutoLoad: {
|
/**
|
||||||
type: Boolean,
|
* items 配列の中身を逆順にする(新しい方が最後)
|
||||||
required: false,
|
*/
|
||||||
default: false,
|
reversed?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{
|
||||||
|
pagination: Paging;
|
||||||
|
disableAutoLoad?: boolean;
|
||||||
|
displayLimit?: number;
|
||||||
|
}>(), {
|
||||||
|
displayLimit: 30,
|
||||||
|
});
|
||||||
|
|
||||||
|
const rootEl = ref<HTMLElement>();
|
||||||
|
const items = ref([]);
|
||||||
|
const queue = ref([]);
|
||||||
|
const offset = ref(0);
|
||||||
|
const fetching = ref(true);
|
||||||
|
const moreFetching = ref(false);
|
||||||
|
const inited = ref(false);
|
||||||
|
const more = ref(false);
|
||||||
|
const backed = ref(false); // 遡り中か否か
|
||||||
|
const isBackTop = ref(false);
|
||||||
|
const empty = computed(() => items.value.length === 0 && !fetching.value && inited.value);
|
||||||
|
const error = computed(() => !fetching.value && !inited.value);
|
||||||
|
|
||||||
|
const init = async () => {
|
||||||
|
queue.value = [];
|
||||||
|
fetching.value = true;
|
||||||
|
const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {};
|
||||||
|
await os.api(props.pagination.endpoint, {
|
||||||
|
...params,
|
||||||
|
limit: props.pagination.noPaging ? (props.pagination.limit || 10) : (props.pagination.limit || 10) + 1,
|
||||||
|
}).then(res => {
|
||||||
|
for (let i = 0; i < res.length; i++) {
|
||||||
|
const item = res[i];
|
||||||
|
markRaw(item);
|
||||||
|
if (props.pagination.reversed) {
|
||||||
|
if (i === res.length - 2) item._shouldInsertAd_ = true;
|
||||||
|
} else {
|
||||||
|
if (i === 3) item._shouldInsertAd_ = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
if (!props.pagination.noPaging && (res.length > (props.pagination.limit || 10))) {
|
||||||
|
res.pop();
|
||||||
|
items.value = props.pagination.reversed ? [...res].reverse() : res;
|
||||||
|
more.value = true;
|
||||||
|
} else {
|
||||||
|
items.value = props.pagination.reversed ? [...res].reverse() : res;
|
||||||
|
more.value = false;
|
||||||
|
}
|
||||||
|
offset.value = res.length;
|
||||||
|
inited.value = true;
|
||||||
|
fetching.value = false;
|
||||||
|
}, e => {
|
||||||
|
fetching.value = false;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const reload = () => {
|
||||||
|
items.value = [];
|
||||||
|
init();
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchMore = async () => {
|
||||||
|
if (!more.value || fetching.value || moreFetching.value || items.value.length === 0) return;
|
||||||
|
moreFetching.value = true;
|
||||||
|
backed.value = true;
|
||||||
|
const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {};
|
||||||
|
await os.api(props.pagination.endpoint, {
|
||||||
|
...params,
|
||||||
|
limit: SECOND_FETCH_LIMIT + 1,
|
||||||
|
...(props.pagination.offsetMode ? {
|
||||||
|
offset: offset.value,
|
||||||
|
} : {
|
||||||
|
untilId: props.pagination.reversed ? items.value[0].id : items.value[items.value.length - 1].id,
|
||||||
|
}),
|
||||||
|
}).then(res => {
|
||||||
|
for (let i = 0; i < res.length; i++) {
|
||||||
|
const item = res[i];
|
||||||
|
markRaw(item);
|
||||||
|
if (props.pagination.reversed) {
|
||||||
|
if (i === res.length - 9) item._shouldInsertAd_ = true;
|
||||||
|
} else {
|
||||||
|
if (i === 10) item._shouldInsertAd_ = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (res.length > SECOND_FETCH_LIMIT) {
|
||||||
|
res.pop();
|
||||||
|
items.value = props.pagination.reversed ? [...res].reverse().concat(items.value) : items.value.concat(res);
|
||||||
|
more.value = true;
|
||||||
|
} else {
|
||||||
|
items.value = props.pagination.reversed ? [...res].reverse().concat(items.value) : items.value.concat(res);
|
||||||
|
more.value = false;
|
||||||
|
}
|
||||||
|
offset.value += res.length;
|
||||||
|
moreFetching.value = false;
|
||||||
|
}, e => {
|
||||||
|
moreFetching.value = false;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchMoreAhead = async () => {
|
||||||
|
if (!more.value || fetching.value || moreFetching.value || items.value.length === 0) return;
|
||||||
|
moreFetching.value = true;
|
||||||
|
const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {};
|
||||||
|
await os.api(props.pagination.endpoint, {
|
||||||
|
...params,
|
||||||
|
limit: SECOND_FETCH_LIMIT + 1,
|
||||||
|
...(props.pagination.offsetMode ? {
|
||||||
|
offset: offset.value,
|
||||||
|
} : {
|
||||||
|
sinceId: props.pagination.reversed ? items.value[0].id : items.value[items.value.length - 1].id,
|
||||||
|
}),
|
||||||
|
}).then(res => {
|
||||||
|
for (const item of res) {
|
||||||
|
markRaw(item);
|
||||||
|
}
|
||||||
|
if (res.length > SECOND_FETCH_LIMIT) {
|
||||||
|
res.pop();
|
||||||
|
items.value = props.pagination.reversed ? [...res].reverse().concat(items.value) : items.value.concat(res);
|
||||||
|
more.value = true;
|
||||||
|
} else {
|
||||||
|
items.value = props.pagination.reversed ? [...res].reverse().concat(items.value) : items.value.concat(res);
|
||||||
|
more.value = false;
|
||||||
|
}
|
||||||
|
offset.value += res.length;
|
||||||
|
moreFetching.value = false;
|
||||||
|
}, e => {
|
||||||
|
moreFetching.value = false;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const prepend = (item) => {
|
||||||
|
if (props.pagination.reversed) {
|
||||||
|
const container = getScrollContainer(rootEl.value);
|
||||||
|
const pos = getScrollPosition(rootEl.value);
|
||||||
|
const viewHeight = container.clientHeight;
|
||||||
|
const height = container.scrollHeight;
|
||||||
|
const isBottom = (pos + viewHeight > height - 32);
|
||||||
|
if (isBottom) {
|
||||||
|
// オーバーフローしたら古いアイテムは捨てる
|
||||||
|
if (items.value.length >= props.displayLimit) {
|
||||||
|
// このやり方だとVue 3.2以降アニメーションが動かなくなる
|
||||||
|
//items.value = items.value.slice(-props.displayLimit);
|
||||||
|
while (items.value.length >= props.displayLimit) {
|
||||||
|
items.value.shift();
|
||||||
|
}
|
||||||
|
more.value = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
items.value.push(item);
|
||||||
|
// TODO
|
||||||
|
} else {
|
||||||
|
const isTop = isBackTop.value || (document.body.contains(rootEl.value) && isTopVisible(rootEl.value));
|
||||||
|
console.log(item, top);
|
||||||
|
|
||||||
|
if (isTop) {
|
||||||
|
// Prepend the item
|
||||||
|
items.value.unshift(item);
|
||||||
|
|
||||||
|
// オーバーフローしたら古いアイテムは捨てる
|
||||||
|
if (items.value.length >= props.displayLimit) {
|
||||||
|
// このやり方だとVue 3.2以降アニメーションが動かなくなる
|
||||||
|
//this.items = items.value.slice(0, props.displayLimit);
|
||||||
|
while (items.value.length >= props.displayLimit) {
|
||||||
|
items.value.pop();
|
||||||
|
}
|
||||||
|
more.value = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
queue.value.push(item);
|
||||||
|
onScrollTop(rootEl.value, () => {
|
||||||
|
for (const item of queue.value) {
|
||||||
|
prepend(item);
|
||||||
|
}
|
||||||
|
queue.value = [];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const append = (item) => {
|
||||||
|
items.value.push(item);
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(props.pagination.params, init, { deep: true });
|
||||||
|
watch(queue, (a, b) => {
|
||||||
|
if (a.length === 0 && b.length === 0) return;
|
||||||
|
this.$emit('queue', queue.value.length);
|
||||||
|
}, { deep: true });
|
||||||
|
|
||||||
|
init();
|
||||||
|
|
||||||
|
onActivated(() => {
|
||||||
|
isBackTop.value = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
onDeactivated(() => {
|
||||||
|
isBackTop.value = window.scrollY === 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
items,
|
||||||
|
reload,
|
||||||
|
fetchMoreAhead,
|
||||||
|
prepend,
|
||||||
|
append,
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -64,11 +268,9 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
|
|
||||||
.cxiknjgy {
|
.cxiknjgy {
|
||||||
> .more > .button {
|
> .button {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
height: 48px;
|
|
||||||
min-width: 150px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,91 +1,39 @@
|
||||||
<template>
|
<template>
|
||||||
<MkError v-if="error" @retry="init()"/>
|
<MkPagination ref="pagingComponent" :pagination="pagination">
|
||||||
|
<template #empty>
|
||||||
|
<div class="_fullinfo">
|
||||||
|
<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
|
||||||
|
<div>{{ $ts.noUsers }}</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<div v-else class="efvhhmdq _isolated">
|
<template #default="{ items: users }">
|
||||||
<div v-if="empty" class="no-users">
|
<div class="efvhhmdq">
|
||||||
<p>{{ $ts.noUsers }}</p>
|
<MkUserInfo v-for="user in users" :key="user.id" class="user" :user="user"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="users">
|
</template>
|
||||||
<MkUserInfo v-for="user in users" :key="user.id" class="user" :user="user"/>
|
</MkPagination>
|
||||||
</div>
|
|
||||||
<button v-show="more" v-appear="$store.state.enableInfiniteScroll ? fetchMore : null" class="more" :class="{ fetching: moreFetching }" :disabled="moreFetching" @click="fetchMore">
|
|
||||||
<template v-if="moreFetching"><i class="fas fa-spinner fa-pulse fa-fw"></i></template>{{ moreFetching ? $ts.loading : $ts.loadMore }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts" setup>
|
||||||
import { defineComponent } from 'vue';
|
import { ref } from 'vue';
|
||||||
import paging from '@/scripts/paging';
|
import MkUserInfo from '@/components/user-info.vue';
|
||||||
import MkUserInfo from './user-info.vue';
|
import MkPagination from '@/components/ui/pagination.vue';
|
||||||
|
import { Paging } from '@/components/ui/pagination.vue';
|
||||||
import { userPage } from '@/filters/user';
|
import { userPage } from '@/filters/user';
|
||||||
|
|
||||||
export default defineComponent({
|
const props = defineProps<{
|
||||||
components: {
|
pagination: Paging;
|
||||||
MkUserInfo,
|
noGap?: boolean;
|
||||||
},
|
}>();
|
||||||
|
|
||||||
mixins: [
|
const pagingComponent = ref<InstanceType<typeof MkPagination>>();
|
||||||
paging({}),
|
|
||||||
],
|
|
||||||
|
|
||||||
props: {
|
|
||||||
pagination: {
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
extract: {
|
|
||||||
required: false
|
|
||||||
},
|
|
||||||
expanded: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
users() {
|
|
||||||
return this.extract ? this.extract(this.items) : this.items;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
userPage
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.efvhhmdq {
|
.efvhhmdq {
|
||||||
> .no-users {
|
display: grid;
|
||||||
text-align: center;
|
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
||||||
}
|
grid-gap: var(--margin);
|
||||||
|
|
||||||
> .users {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
|
||||||
grid-gap: var(--margin);
|
|
||||||
}
|
|
||||||
|
|
||||||
> .more {
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
padding: 16px;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: rgba(#000, 0.025);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
background: rgba(#000, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.fetching {
|
|
||||||
cursor: wait;
|
|
||||||
}
|
|
||||||
|
|
||||||
> i {
|
|
||||||
margin-right: 4px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
|
|
||||||
<FormSection>
|
<FormSection>
|
||||||
<template #label>{{ $ts.signinHistory }}</template>
|
<template #label>{{ $ts.signinHistory }}</template>
|
||||||
<FormPagination :pagination="pagination">
|
<MkPagination :pagination="pagination">
|
||||||
<template v-slot="{items}">
|
<template v-slot="{items}">
|
||||||
<div>
|
<div>
|
||||||
<div v-for="item in items" :key="item.id" v-panel class="timnmucd">
|
<div v-for="item in items" :key="item.id" v-panel class="timnmucd">
|
||||||
|
@ -25,7 +25,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</FormPagination>
|
</MkPagination>
|
||||||
</FormSection>
|
</FormSection>
|
||||||
|
|
||||||
<FormSection>
|
<FormSection>
|
||||||
|
@ -42,7 +42,7 @@ import { defineComponent } from 'vue';
|
||||||
import FormSection from '@/components/form/section.vue';
|
import FormSection from '@/components/form/section.vue';
|
||||||
import FormSlot from '@/components/form/slot.vue';
|
import FormSlot from '@/components/form/slot.vue';
|
||||||
import FormButton from '@/components/ui/button.vue';
|
import FormButton from '@/components/ui/button.vue';
|
||||||
import FormPagination from '@/components/form/pagination.vue';
|
import MkPagination from '@/components/ui/pagination.vue';
|
||||||
import X2fa from './2fa.vue';
|
import X2fa from './2fa.vue';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import * as symbols from '@/symbols';
|
import * as symbols from '@/symbols';
|
||||||
|
@ -51,7 +51,7 @@ export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
FormSection,
|
FormSection,
|
||||||
FormButton,
|
FormButton,
|
||||||
FormPagination,
|
MkPagination,
|
||||||
FormSlot,
|
FormSlot,
|
||||||
X2fa,
|
X2fa,
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,60 +1,36 @@
|
||||||
<template>
|
<template>
|
||||||
<div v-sticky-container class="yrzkoczt">
|
<div v-sticky-container class="yrzkoczt">
|
||||||
<MkTab v-model="with_" class="tab">
|
<MkTab v-model="include" class="tab">
|
||||||
<option :value="null">{{ $ts.notes }}</option>
|
<option :value="null">{{ $ts.notes }}</option>
|
||||||
<option value="replies">{{ $ts.notesAndReplies }}</option>
|
<option value="replies">{{ $ts.notesAndReplies }}</option>
|
||||||
<option value="files">{{ $ts.withFiles }}</option>
|
<option value="files">{{ $ts.withFiles }}</option>
|
||||||
</MkTab>
|
</MkTab>
|
||||||
<XNotes ref="timeline" :no-gap="true" :pagination="pagination" @before="$emit('before')" @after="e => $emit('after', e)"/>
|
<XNotes ref="timeline" :no-gap="true" :pagination="pagination"/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts" setup>
|
||||||
import { defineComponent } from 'vue';
|
import { ref, computed } from 'vue';
|
||||||
|
import * as misskey from 'misskey-js';
|
||||||
import XNotes from '@/components/notes.vue';
|
import XNotes from '@/components/notes.vue';
|
||||||
import MkTab from '@/components/tab.vue';
|
import MkTab from '@/components/tab.vue';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
|
|
||||||
export default defineComponent({
|
const props = defineProps<{
|
||||||
components: {
|
user: misskey.entities.UserDetailed;
|
||||||
XNotes,
|
}>();
|
||||||
MkTab,
|
|
||||||
},
|
|
||||||
|
|
||||||
props: {
|
const include = ref<string | null>(null);
|
||||||
user: {
|
|
||||||
type: Object,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
const pagination = {
|
||||||
return {
|
endpoint: 'users/notes' as const,
|
||||||
date: null,
|
limit: 10,
|
||||||
with_: null,
|
params: computed(() => ({
|
||||||
pagination: {
|
userId: props.user.id,
|
||||||
endpoint: 'users/notes',
|
includeReplies: include.value === 'replies',
|
||||||
limit: 10,
|
withFiles: include.value === 'files',
|
||||||
params: init => ({
|
})),
|
||||||
userId: this.user.id,
|
};
|
||||||
includeReplies: this.with_ === 'replies',
|
|
||||||
withFiles: this.with_ === 'files',
|
|
||||||
untilDate: init ? undefined : (this.date ? this.date.getTime() : undefined),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
watch: {
|
|
||||||
user() {
|
|
||||||
this.$refs.timeline.reload();
|
|
||||||
},
|
|
||||||
|
|
||||||
with_() {
|
|
||||||
this.$refs.timeline.reload();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
|
@ -1,246 +0,0 @@
|
||||||
import { markRaw } from 'vue';
|
|
||||||
import * as os from '@/os';
|
|
||||||
import { onScrollTop, isTopVisible, getScrollPosition, getScrollContainer } from './scroll';
|
|
||||||
|
|
||||||
const SECOND_FETCH_LIMIT = 30;
|
|
||||||
|
|
||||||
// reversed: items 配列の中身を逆順にする(新しい方が最後)
|
|
||||||
|
|
||||||
export default (opts) => ({
|
|
||||||
emits: ['queue'],
|
|
||||||
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
items: [],
|
|
||||||
queue: [],
|
|
||||||
offset: 0,
|
|
||||||
fetching: true,
|
|
||||||
moreFetching: false,
|
|
||||||
inited: false,
|
|
||||||
more: false,
|
|
||||||
backed: false, // 遡り中か否か
|
|
||||||
isBackTop: false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
empty(): boolean {
|
|
||||||
return this.items.length === 0 && !this.fetching && this.inited;
|
|
||||||
},
|
|
||||||
|
|
||||||
error(): boolean {
|
|
||||||
return !this.fetching && !this.inited;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
watch: {
|
|
||||||
pagination: {
|
|
||||||
handler() {
|
|
||||||
this.init();
|
|
||||||
},
|
|
||||||
deep: true
|
|
||||||
},
|
|
||||||
|
|
||||||
queue: {
|
|
||||||
handler(a, b) {
|
|
||||||
if (a.length === 0 && b.length === 0) return;
|
|
||||||
this.$emit('queue', this.queue.length);
|
|
||||||
},
|
|
||||||
deep: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
created() {
|
|
||||||
opts.displayLimit = opts.displayLimit || 30;
|
|
||||||
this.init();
|
|
||||||
},
|
|
||||||
|
|
||||||
activated() {
|
|
||||||
this.isBackTop = false;
|
|
||||||
},
|
|
||||||
|
|
||||||
deactivated() {
|
|
||||||
this.isBackTop = window.scrollY === 0;
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
reload() {
|
|
||||||
this.items = [];
|
|
||||||
this.init();
|
|
||||||
},
|
|
||||||
|
|
||||||
replaceItem(finder, data) {
|
|
||||||
const i = this.items.findIndex(finder);
|
|
||||||
this.items[i] = data;
|
|
||||||
},
|
|
||||||
|
|
||||||
removeItem(finder) {
|
|
||||||
const i = this.items.findIndex(finder);
|
|
||||||
this.items.splice(i, 1);
|
|
||||||
},
|
|
||||||
|
|
||||||
async init() {
|
|
||||||
this.queue = [];
|
|
||||||
this.fetching = true;
|
|
||||||
if (opts.before) opts.before(this);
|
|
||||||
let params = typeof this.pagination.params === 'function' ? this.pagination.params(true) : this.pagination.params;
|
|
||||||
if (params && params.then) params = await params;
|
|
||||||
if (params === null) return;
|
|
||||||
const endpoint = typeof this.pagination.endpoint === 'function' ? this.pagination.endpoint() : this.pagination.endpoint;
|
|
||||||
await os.api(endpoint, {
|
|
||||||
...params,
|
|
||||||
limit: this.pagination.noPaging ? (this.pagination.limit || 10) : (this.pagination.limit || 10) + 1,
|
|
||||||
}).then(items => {
|
|
||||||
for (let i = 0; i < items.length; i++) {
|
|
||||||
const item = items[i];
|
|
||||||
markRaw(item);
|
|
||||||
if (this.pagination.reversed) {
|
|
||||||
if (i === items.length - 2) item._shouldInsertAd_ = true;
|
|
||||||
} else {
|
|
||||||
if (i === 3) item._shouldInsertAd_ = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!this.pagination.noPaging && (items.length > (this.pagination.limit || 10))) {
|
|
||||||
items.pop();
|
|
||||||
this.items = this.pagination.reversed ? [...items].reverse() : items;
|
|
||||||
this.more = true;
|
|
||||||
} else {
|
|
||||||
this.items = this.pagination.reversed ? [...items].reverse() : items;
|
|
||||||
this.more = false;
|
|
||||||
}
|
|
||||||
this.offset = items.length;
|
|
||||||
this.inited = true;
|
|
||||||
this.fetching = false;
|
|
||||||
if (opts.after) opts.after(this, null);
|
|
||||||
}, e => {
|
|
||||||
this.fetching = false;
|
|
||||||
if (opts.after) opts.after(this, e);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
async fetchMore() {
|
|
||||||
if (!this.more || this.fetching || this.moreFetching || this.items.length === 0) return;
|
|
||||||
this.moreFetching = true;
|
|
||||||
this.backed = true;
|
|
||||||
let params = typeof this.pagination.params === 'function' ? this.pagination.params(false) : this.pagination.params;
|
|
||||||
if (params && params.then) params = await params;
|
|
||||||
const endpoint = typeof this.pagination.endpoint === 'function' ? this.pagination.endpoint() : this.pagination.endpoint;
|
|
||||||
await os.api(endpoint, {
|
|
||||||
...params,
|
|
||||||
limit: SECOND_FETCH_LIMIT + 1,
|
|
||||||
...(this.pagination.offsetMode ? {
|
|
||||||
offset: this.offset,
|
|
||||||
} : {
|
|
||||||
untilId: this.pagination.reversed ? this.items[0].id : this.items[this.items.length - 1].id,
|
|
||||||
}),
|
|
||||||
}).then(items => {
|
|
||||||
for (let i = 0; i < items.length; i++) {
|
|
||||||
const item = items[i];
|
|
||||||
markRaw(item);
|
|
||||||
if (this.pagination.reversed) {
|
|
||||||
if (i === items.length - 9) item._shouldInsertAd_ = true;
|
|
||||||
} else {
|
|
||||||
if (i === 10) item._shouldInsertAd_ = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (items.length > SECOND_FETCH_LIMIT) {
|
|
||||||
items.pop();
|
|
||||||
this.items = this.pagination.reversed ? [...items].reverse().concat(this.items) : this.items.concat(items);
|
|
||||||
this.more = true;
|
|
||||||
} else {
|
|
||||||
this.items = this.pagination.reversed ? [...items].reverse().concat(this.items) : this.items.concat(items);
|
|
||||||
this.more = false;
|
|
||||||
}
|
|
||||||
this.offset += items.length;
|
|
||||||
this.moreFetching = false;
|
|
||||||
}, e => {
|
|
||||||
this.moreFetching = false;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
async fetchMoreFeature() {
|
|
||||||
if (!this.more || this.fetching || 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;
|
|
||||||
const endpoint = typeof this.pagination.endpoint === 'function' ? this.pagination.endpoint() : this.pagination.endpoint;
|
|
||||||
await os.api(endpoint, {
|
|
||||||
...params,
|
|
||||||
limit: SECOND_FETCH_LIMIT + 1,
|
|
||||||
...(this.pagination.offsetMode ? {
|
|
||||||
offset: this.offset,
|
|
||||||
} : {
|
|
||||||
sinceId: this.pagination.reversed ? this.items[0].id : this.items[this.items.length - 1].id,
|
|
||||||
}),
|
|
||||||
}).then(items => {
|
|
||||||
for (const item of items) {
|
|
||||||
markRaw(item);
|
|
||||||
}
|
|
||||||
if (items.length > SECOND_FETCH_LIMIT) {
|
|
||||||
items.pop();
|
|
||||||
this.items = this.pagination.reversed ? [...items].reverse().concat(this.items) : this.items.concat(items);
|
|
||||||
this.more = true;
|
|
||||||
} else {
|
|
||||||
this.items = this.pagination.reversed ? [...items].reverse().concat(this.items) : this.items.concat(items);
|
|
||||||
this.more = false;
|
|
||||||
}
|
|
||||||
this.offset += items.length;
|
|
||||||
this.moreFetching = false;
|
|
||||||
}, e => {
|
|
||||||
this.moreFetching = false;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
prepend(item) {
|
|
||||||
if (this.pagination.reversed) {
|
|
||||||
const container = getScrollContainer(this.$el);
|
|
||||||
const pos = getScrollPosition(this.$el);
|
|
||||||
const viewHeight = container.clientHeight;
|
|
||||||
const height = container.scrollHeight;
|
|
||||||
const isBottom = (pos + viewHeight > height - 32);
|
|
||||||
if (isBottom) {
|
|
||||||
// オーバーフローしたら古いアイテムは捨てる
|
|
||||||
if (this.items.length >= opts.displayLimit) {
|
|
||||||
// このやり方だとVue 3.2以降アニメーションが動かなくなる
|
|
||||||
//this.items = this.items.slice(-opts.displayLimit);
|
|
||||||
while (this.items.length >= opts.displayLimit) {
|
|
||||||
this.items.shift();
|
|
||||||
}
|
|
||||||
this.more = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.items.push(item);
|
|
||||||
// TODO
|
|
||||||
} else {
|
|
||||||
const isTop = this.isBackTop || (document.body.contains(this.$el) && isTopVisible(this.$el));
|
|
||||||
|
|
||||||
if (isTop) {
|
|
||||||
// Prepend the item
|
|
||||||
this.items.unshift(item);
|
|
||||||
|
|
||||||
// オーバーフローしたら古いアイテムは捨てる
|
|
||||||
if (this.items.length >= opts.displayLimit) {
|
|
||||||
// このやり方だとVue 3.2以降アニメーションが動かなくなる
|
|
||||||
//this.items = this.items.slice(0, opts.displayLimit);
|
|
||||||
while (this.items.length >= opts.displayLimit) {
|
|
||||||
this.items.pop();
|
|
||||||
}
|
|
||||||
this.more = true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.queue.push(item);
|
|
||||||
onScrollTop(this.$el, () => {
|
|
||||||
for (const item of this.queue) {
|
|
||||||
this.prepend(item);
|
|
||||||
}
|
|
||||||
this.queue = [];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
append(item) {
|
|
||||||
this.items.push(item);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
});
|
|
Loading…
Reference in New Issue