summaryrefslogtreecommitdiff
path: root/packages/client/src
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2022-01-09 21:35:35 +0900
committersyuilo <Syuilotan@yahoo.co.jp>2022-01-09 21:35:35 +0900
commit586c11251a8c0e7ca9f8f3bbaad9bf745e6ef948 (patch)
tree75b572d160488a29bcca19b12a72072bb2fa458e /packages/client/src
parentbye chat ui (diff)
downloadmisskey-586c11251a8c0e7ca9f8f3bbaad9bf745e6ef948.tar.gz
misskey-586c11251a8c0e7ca9f8f3bbaad9bf745e6ef948.tar.bz2
misskey-586c11251a8c0e7ca9f8f3bbaad9bf745e6ef948.zip
wip: migrate paging components to composition api
#7681
Diffstat (limited to 'packages/client/src')
-rw-r--r--packages/client/src/components/form/pagination.vue44
-rw-r--r--packages/client/src/components/notes.vue127
-rw-r--r--packages/client/src/components/notifications.vue194
-rw-r--r--packages/client/src/components/ui/pagination.vue258
-rw-r--r--packages/client/src/components/user-list.vue104
-rw-r--r--packages/client/src/pages/settings/security.vue8
-rw-r--r--packages/client/src/pages/user/index.timeline.vue60
-rw-r--r--packages/client/src/scripts/paging.ts246
8 files changed, 369 insertions, 672 deletions
diff --git a/packages/client/src/components/form/pagination.vue b/packages/client/src/components/form/pagination.vue
deleted file mode 100644
index 3d3b40a783..0000000000
--- a/packages/client/src/components/form/pagination.vue
+++ /dev/null
@@ -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>
diff --git a/packages/client/src/components/notes.vue b/packages/client/src/components/notes.vue
index 4136f72b1b..82703d71c7 100644
--- a/packages/client/src/components/notes.vue
+++ b/packages/client/src/components/notes.vue
@@ -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">
- <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>
+<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>
- <XList ref="notes" v-slot="{ item: note }" :items="notes" :direction="reversed ? 'up' : 'down'" :reversed="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>
+ <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>
- </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
- },
- prop: {
- type: String,
- required: false
- },
- noGap: {
- type: Boolean,
- required: false,
- default: false
- },
+defineExpose({
+ prepend: (note) => {
+ pagingComponent.value?.prepend(note);
},
-
- 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 {
diff --git a/packages/client/src/components/notifications.vue b/packages/client/src/components/notifications.vue
index 328888c355..aa4b480694 100644
--- a/packages/client/src/components/notifications.vue
+++ b/packages/client/src/components/notifications.vue
@@ -1,159 +1,85 @@
<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';
-
-export default defineComponent({
- components: {
- XNotification,
- XList,
- XNote,
- MkButton,
- },
+import { $i } from '@/account';
- mixins: [
- paging({}),
- ],
+const props = defineProps<{
+ includeTypes?: PropType<typeof notificationTypes[number][]>;
+ unreadOnly?: boolean;
+}>();
- props: {
- includeTypes: {
- type: Array as PropType<typeof notificationTypes[number][]>,
- required: false,
- default: null,
- },
- unreadOnly: {
- type: Boolean,
- required: false,
- default: false,
- },
- },
+const pagingComponent = ref<InstanceType<typeof MkPagination>>();
- data() {
- return {
- connection: null,
- pagination: {
- endpoint: 'i/notifications',
- limit: 10,
- params: () => ({
- includeTypes: this.allIncludeTypes || undefined,
- unreadOnly: this.unreadOnly,
- })
- },
- };
- },
+const allIncludeTypes = computed(() => props.includeTypes ?? notificationTypes.filter(x => !$i.mutingNotificationTypes.includes(x)));
- computed: {
- allIncludeTypes() {
- return this.includeTypes ?? notificationTypes.filter(x => !this.$i.mutingNotificationTypes.includes(x));
- }
- },
+const pagination: Paging = {
+ endpoint: 'i/notifications' as const,
+ limit: 10,
+ params: computed(() => ({
+ includeTypes: allIncludeTypes.value || undefined,
+ unreadOnly: props.unreadOnly,
+ })),
+};
- 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();
- },
+const onNotification = (notification) => {
+ const isMuted = !allIncludeTypes.value.includes(notification.type);
+ if (isMuted || document.visibilityState === 'visible') {
+ stream.send('readNotification', {
+ id: notification.id
+ });
+ }
- methods: {
- onNotification(notification) {
- const isMuted = !this.allIncludeTypes.includes(notification.type);
- if (isMuted || document.visibilityState === 'visible') {
- stream.send('readNotification', {
- id: notification.id
- });
- }
+ if (!isMuted) {
+ pagingComponent.value.prepend({
+ ...notification,
+ isRead: document.visibilityState === 'visible'
+ });
+ }
+};
- if (!isMuted) {
- this.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,
+ };
+};
- noteUpdated(oldValue, newValue) {
- const i = this.items.findIndex(n => n.note === oldValue);
- this.items[i] = {
- ...this.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);
}
diff --git a/packages/client/src/components/ui/pagination.vue b/packages/client/src/components/ui/pagination.vue
index 64af4a54f7..79744e528d 100644
--- a/packages/client/src/components/ui/pagination.vue
+++ b/packages/client/src/components/ui/pagination.vue
@@ -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>
diff --git a/packages/client/src/components/user-list.vue b/packages/client/src/components/user-list.vue
index 2148dab608..3e273721c7 100644
--- a/packages/client/src/components/user-list.vue
+++ b/packages/client/src/components/user-list.vue
@@ -1,91 +1,39 @@
<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">
- <div v-if="empty" class="no-users">
- <p>{{ $ts.noUsers }}</p>
- </div>
- <div class="users">
- <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 #default="{ items: users }">
+ <div class="efvhhmdq">
+ <MkUserInfo v-for="user in users" :key="user.id" class="user" :user="user"/>
+ </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;
- }
- }
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
+ grid-gap: var(--margin);
}
</style>
diff --git a/packages/client/src/pages/settings/security.vue b/packages/client/src/pages/settings/security.vue
index 82a21d5b16..03f2d6300b 100644
--- a/packages/client/src/pages/settings/security.vue
+++ b/packages/client/src/pages/settings/security.vue
@@ -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,
},
diff --git a/packages/client/src/pages/user/index.timeline.vue b/packages/client/src/pages/user/index.timeline.vue
index 2ffa496979..7396a76efe 100644
--- a/packages/client/src/pages/user/index.timeline.vue
+++ b/packages/client/src/pages/user/index.timeline.vue
@@ -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',
- 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();
- },
- },
-});
+const pagination = {
+ endpoint: 'users/notes' as const,
+ limit: 10,
+ params: computed(() => ({
+ userId: props.user.id,
+ includeReplies: include.value === 'replies',
+ withFiles: include.value === 'files',
+ })),
+};
</script>
<style lang="scss" scoped>
diff --git a/packages/client/src/scripts/paging.ts b/packages/client/src/scripts/paging.ts
deleted file mode 100644
index ef63ecc450..0000000000
--- a/packages/client/src/scripts/paging.ts
+++ /dev/null
@@ -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);
- },
- }
-});