mirror of
https://activitypub.software/TransFem-org/Sharkey.git
synced 2024-12-15 05:05:42 +01:00
parent
a10be38d0e
commit
586c11251a
8 changed files with 374 additions and 677 deletions
|
@ -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>
|
||||
<transition name="fade" mode="out-in">
|
||||
<MkLoading v-if="fetching"/>
|
||||
|
||||
<MkError v-else-if="error" @retry="init()"/>
|
||||
|
||||
<div v-else-if="empty" class="_fullinfo">
|
||||
<MkPagination ref="pagingComponent" :pagination="pagination">
|
||||
<template #empty>
|
||||
<div class="_fullinfo">
|
||||
<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
|
||||
<div>{{ $ts.noNotes }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<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>
|
||||
|
||||
<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 }">
|
||||
<div class="giivymft" :class="{ noGap }">
|
||||
<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)"/>
|
||||
</XList>
|
||||
|
||||
<div v-show="more && !reversed" style="margin-top: var(--margin);">
|
||||
<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>
|
||||
</transition>
|
||||
</template>
|
||||
</MkPagination>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import paging from '@/scripts/paging';
|
||||
import XNote from './note.vue';
|
||||
import XList from './date-separated-list.vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import XNote from '@/components/note.vue';
|
||||
import XList from '@/components/date-separated-list.vue';
|
||||
import MkPagination from '@/components/ui/pagination.vue';
|
||||
import { Paging } from '@/components/ui/pagination.vue';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
XNote, XList, MkButton,
|
||||
},
|
||||
const props = defineProps<{
|
||||
pagination: Paging;
|
||||
noGap?: boolean;
|
||||
}>();
|
||||
|
||||
mixins: [
|
||||
paging({
|
||||
before: (self) => {
|
||||
self.$emit('before');
|
||||
},
|
||||
const pagingComponent = ref<InstanceType<typeof MkPagination>>();
|
||||
|
||||
after: (self, e) => {
|
||||
self.$emit('after', e);
|
||||
}
|
||||
}),
|
||||
],
|
||||
const updated = (oldValue, newValue) => {
|
||||
const i = pagingComponent.value.items.findIndex(n => n === oldValue);
|
||||
pagingComponent.value.items[i] = newValue;
|
||||
};
|
||||
|
||||
props: {
|
||||
pagination: {
|
||||
required: true
|
||||
defineExpose({
|
||||
prepend: (note) => {
|
||||
pagingComponent.value?.prepend(note);
|
||||
},
|
||||
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>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.125s ease;
|
||||
}
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.giivymft {
|
||||
&.noGap {
|
||||
> .notes {
|
||||
|
|
|
@ -1,117 +1,53 @@
|
|||
<template>
|
||||
<transition name="fade" mode="out-in">
|
||||
<MkLoading v-if="fetching"/>
|
||||
<MkPagination ref="pagingComponent" :pagination="pagination">
|
||||
<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()"/>
|
||||
|
||||
<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">
|
||||
<template #default="{ items: notifications }">
|
||||
<XList v-slot="{ item: notification }" class="elsfgstc" :items="notifications" :no-gap="true">
|
||||
<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"/>
|
||||
</XList>
|
||||
|
||||
<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">
|
||||
<template v-if="!moreFetching">{{ $ts.loadMore }}</template>
|
||||
<template v-if="moreFetching"><MkLoading inline/></template>
|
||||
</MkButton>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
</MkPagination>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType, markRaw } from 'vue';
|
||||
import paging from '@/scripts/paging';
|
||||
import XNotification from './notification.vue';
|
||||
import XList from './date-separated-list.vue';
|
||||
import XNote from './note.vue';
|
||||
<script lang="ts" setup>
|
||||
import { defineComponent, PropType, markRaw, onUnmounted, onMounted, computed, ref } from 'vue';
|
||||
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 { stream } from '@/stream';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import { $i } from '@/account';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
XNotification,
|
||||
XList,
|
||||
XNote,
|
||||
MkButton,
|
||||
},
|
||||
const props = defineProps<{
|
||||
includeTypes?: PropType<typeof notificationTypes[number][]>;
|
||||
unreadOnly?: boolean;
|
||||
}>();
|
||||
|
||||
mixins: [
|
||||
paging({}),
|
||||
],
|
||||
const pagingComponent = ref<InstanceType<typeof MkPagination>>();
|
||||
|
||||
props: {
|
||||
includeTypes: {
|
||||
type: Array as PropType<typeof notificationTypes[number][]>,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
unreadOnly: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
const allIncludeTypes = computed(() => props.includeTypes ?? notificationTypes.filter(x => !$i.mutingNotificationTypes.includes(x)));
|
||||
|
||||
data() {
|
||||
return {
|
||||
connection: null,
|
||||
pagination: {
|
||||
endpoint: 'i/notifications',
|
||||
const pagination: Paging = {
|
||||
endpoint: 'i/notifications' as const,
|
||||
limit: 10,
|
||||
params: () => ({
|
||||
includeTypes: this.allIncludeTypes || undefined,
|
||||
unreadOnly: this.unreadOnly,
|
||||
})
|
||||
},
|
||||
};
|
||||
},
|
||||
params: computed(() => ({
|
||||
includeTypes: allIncludeTypes.value || undefined,
|
||||
unreadOnly: props.unreadOnly,
|
||||
})),
|
||||
};
|
||||
|
||||
computed: {
|
||||
allIncludeTypes() {
|
||||
return this.includeTypes ?? notificationTypes.filter(x => !this.$i.mutingNotificationTypes.includes(x));
|
||||
}
|
||||
},
|
||||
|
||||
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);
|
||||
const onNotification = (notification) => {
|
||||
const isMuted = !allIncludeTypes.value.includes(notification.type);
|
||||
if (isMuted || document.visibilityState === 'visible') {
|
||||
stream.send('readNotification', {
|
||||
id: notification.id
|
||||
|
@ -119,41 +55,31 @@ export default defineComponent({
|
|||
}
|
||||
|
||||
if (!isMuted) {
|
||||
this.prepend({
|
||||
pagingComponent.value.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
|
||||
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>
|
||||
|
||||
<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 {
|
||||
background: var(--panel);
|
||||
}
|
||||
|
|
|
@ -13,43 +13,247 @@
|
|||
</slot>
|
||||
</div>
|
||||
|
||||
<div v-else class="cxiknjgy">
|
||||
<div v-else ref="rootEl">
|
||||
<slot :items="items"></slot>
|
||||
<div v-show="more" key="_more_" class="more _gap">
|
||||
<MkButton 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>
|
||||
<template v-if="moreFetching"><MkLoading inline/></template>
|
||||
<div v-show="more" key="_more_" class="cxiknjgy _gap">
|
||||
<MkButton v-if="!moreFetching" v-appear="($store.state.enableInfiniteScroll && !disableAutoLoad) ? fetchMore : null" class="button" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary @click="fetchMore">
|
||||
{{ $ts.loadMore }}
|
||||
</MkButton>
|
||||
<MkLoading v-else class="loading"/>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import MkButton from './button.vue';
|
||||
import paging from '@/scripts/paging';
|
||||
<script lang="ts" setup>
|
||||
import { computed, ComputedRef, isRef, markRaw, onActivated, onDeactivated, Ref, ref, watch } from 'vue';
|
||||
import * as misskey from 'misskey-js';
|
||||
import * as os from '@/os';
|
||||
import { onScrollTop, isTopVisible, getScrollPosition, getScrollContainer } from '@/scripts/scroll';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkButton
|
||||
},
|
||||
const SECOND_FETCH_LIMIT = 30;
|
||||
|
||||
mixins: [
|
||||
paging({}),
|
||||
],
|
||||
export type Paging<E extends keyof misskey.Endpoints = keyof misskey.Endpoints> = {
|
||||
endpoint: E;
|
||||
limit: number;
|
||||
params?: misskey.Endpoints[E]['req'] | ComputedRef<misskey.Endpoints[E]['req']>;
|
||||
|
||||
props: {
|
||||
pagination: {
|
||||
required: true
|
||||
},
|
||||
/**
|
||||
* 検索APIのような、ページング不可なエンドポイントを利用する場合
|
||||
* (そのようなAPIをこの関数で使うのは若干矛盾してるけど)
|
||||
*/
|
||||
noPaging?: boolean;
|
||||
|
||||
disableAutoLoad: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
/**
|
||||
* items 配列の中身を逆順にする(新しい方が最後)
|
||||
*/
|
||||
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>
|
||||
|
||||
|
@ -64,11 +268,9 @@ export default defineComponent({
|
|||
}
|
||||
|
||||
.cxiknjgy {
|
||||
> .more > .button {
|
||||
> .button {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
height: 48px;
|
||||
min-width: 150px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,91 +1,39 @@
|
|||
<template>
|
||||
<MkError v-if="error" @retry="init()"/>
|
||||
|
||||
<div v-else class="efvhhmdq _isolated">
|
||||
<div v-if="empty" class="no-users">
|
||||
<p>{{ $ts.noUsers }}</p>
|
||||
<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>
|
||||
<div class="users">
|
||||
</template>
|
||||
|
||||
<template #default="{ items: users }">
|
||||
<div class="efvhhmdq">
|
||||
<MkUserInfo v-for="user in users" :key="user.id" class="user" :user="user"/>
|
||||
</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>
|
||||
</MkPagination>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import paging from '@/scripts/paging';
|
||||
import MkUserInfo from './user-info.vue';
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import MkUserInfo from '@/components/user-info.vue';
|
||||
import MkPagination from '@/components/ui/pagination.vue';
|
||||
import { Paging } from '@/components/ui/pagination.vue';
|
||||
import { userPage } from '@/filters/user';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkUserInfo,
|
||||
},
|
||||
const props = defineProps<{
|
||||
pagination: Paging;
|
||||
noGap?: boolean;
|
||||
}>();
|
||||
|
||||
mixins: [
|
||||
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
|
||||
}
|
||||
});
|
||||
const pagingComponent = ref<InstanceType<typeof MkPagination>>();
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.efvhhmdq {
|
||||
> .no-users {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
> .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>
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
|
||||
<FormSection>
|
||||
<template #label>{{ $ts.signinHistory }}</template>
|
||||
<FormPagination :pagination="pagination">
|
||||
<MkPagination :pagination="pagination">
|
||||
<template v-slot="{items}">
|
||||
<div>
|
||||
<div v-for="item in items" :key="item.id" v-panel class="timnmucd">
|
||||
|
@ -25,7 +25,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</FormPagination>
|
||||
</MkPagination>
|
||||
</FormSection>
|
||||
|
||||
<FormSection>
|
||||
|
@ -42,7 +42,7 @@ import { defineComponent } from 'vue';
|
|||
import FormSection from '@/components/form/section.vue';
|
||||
import FormSlot from '@/components/form/slot.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 * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
|
@ -51,7 +51,7 @@ export default defineComponent({
|
|||
components: {
|
||||
FormSection,
|
||||
FormButton,
|
||||
FormPagination,
|
||||
MkPagination,
|
||||
FormSlot,
|
||||
X2fa,
|
||||
},
|
||||
|
|
|
@ -1,60 +1,36 @@
|
|||
<template>
|
||||
<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="replies">{{ $ts.notesAndReplies }}</option>
|
||||
<option value="files">{{ $ts.withFiles }}</option>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import * as misskey from 'misskey-js';
|
||||
import XNotes from '@/components/notes.vue';
|
||||
import MkTab from '@/components/tab.vue';
|
||||
import * as os from '@/os';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
XNotes,
|
||||
MkTab,
|
||||
},
|
||||
const props = defineProps<{
|
||||
user: misskey.entities.UserDetailed;
|
||||
}>();
|
||||
|
||||
props: {
|
||||
user: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
const include = ref<string | null>(null);
|
||||
|
||||
data() {
|
||||
return {
|
||||
date: null,
|
||||
with_: null,
|
||||
pagination: {
|
||||
endpoint: 'users/notes',
|
||||
const pagination = {
|
||||
endpoint: 'users/notes' as const,
|
||||
limit: 10,
|
||||
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();
|
||||
},
|
||||
},
|
||||
});
|
||||
params: computed(() => ({
|
||||
userId: props.user.id,
|
||||
includeReplies: include.value === 'replies',
|
||||
withFiles: include.value === 'files',
|
||||
})),
|
||||
};
|
||||
</script>
|
||||
|
||||
<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 a new issue