forked from Mirrors/elk
Compare commits
14 commits
main
...
userquin/f
Author | SHA1 | Date | |
---|---|---|---|
|
369501efca | ||
|
97866ebeab | ||
|
a64c0f4e9b | ||
|
6dde98eb78 | ||
|
db2140e350 | ||
|
f3a8778ede | ||
|
5acd2224df | ||
|
26883d6d19 | ||
|
1241921435 | ||
|
9bc00be29a | ||
|
bb119d0f8d | ||
|
3e0b2a3e4b | ||
|
a7414bb59e | ||
|
3270140c3f |
22 changed files with 194 additions and 11 deletions
|
@ -49,7 +49,7 @@ const { items, prevItems, update, state, endAnchor, error } = usePaginator(pagin
|
||||||
|
|
||||||
nuxtApp.hook('elk-logo:click', () => {
|
nuxtApp.hook('elk-logo:click', () => {
|
||||||
update()
|
update()
|
||||||
nuxtApp.$scrollToTop()
|
nuxtApp.$trackScroll.forceScrollToTop()
|
||||||
})
|
})
|
||||||
|
|
||||||
function createEntry(item: any) {
|
function createEntry(item: any) {
|
||||||
|
|
|
@ -16,6 +16,23 @@ defineSlots<{
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
|
const allowScrollTop = ref(false)
|
||||||
|
|
||||||
|
usePageTransition({
|
||||||
|
beforeEach: () => {
|
||||||
|
allowScrollTop.value = false
|
||||||
|
},
|
||||||
|
afterHydrated: () => {
|
||||||
|
if (typeof props.to === 'string')
|
||||||
|
allowScrollTop.value = router.currentRoute.value.fullPath === props.to
|
||||||
|
else
|
||||||
|
allowScrollTop.value = router.currentRoute.value.name === props.to.name
|
||||||
|
},
|
||||||
|
onTransitionError: () => {
|
||||||
|
allowScrollTop.value = false
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
useCommand({
|
useCommand({
|
||||||
scope: 'Navigation',
|
scope: 'Navigation',
|
||||||
|
|
||||||
|
@ -51,7 +68,7 @@ const noUserVisual = computed(() => isHydrated.value && props.userOnly && !curre
|
||||||
:active-class="activeClass"
|
:active-class="activeClass"
|
||||||
group focus:outline-none disabled:pointer-events-none
|
group focus:outline-none disabled:pointer-events-none
|
||||||
:tabindex="noUserDisable ? -1 : null"
|
:tabindex="noUserDisable ? -1 : null"
|
||||||
@click="$scrollToTop"
|
@click="allowScrollTop && $trackScroll.forceScrollToTop()"
|
||||||
>
|
>
|
||||||
<CommonTooltip :disabled="!isMediumOrLargeScreen" :content="text" placement="right">
|
<CommonTooltip :disabled="!isMediumOrLargeScreen" :content="text" placement="right">
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -4,11 +4,14 @@ import { DynamicScrollerItem } from 'vue-virtual-scroller'
|
||||||
import type { Paginator, WsEvents, mastodon } from 'masto'
|
import type { Paginator, WsEvents, mastodon } from 'masto'
|
||||||
import type { GroupedAccountLike, NotificationSlot } from '~/types'
|
import type { GroupedAccountLike, NotificationSlot } from '~/types'
|
||||||
|
|
||||||
const { paginator, stream } = defineProps<{
|
const { path, paginator, stream } = defineProps<{
|
||||||
|
path: string
|
||||||
paginator: Paginator<mastodon.v1.Notification[], mastodon.v1.ListNotificationsParams>
|
paginator: Paginator<mastodon.v1.Notification[], mastodon.v1.ListNotificationsParams>
|
||||||
stream?: Promise<WsEvents>
|
stream?: Promise<WsEvents>
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const nuxtApp = useNuxtApp()
|
||||||
|
|
||||||
const virtualScroller = false // TODO: fix flickering issue with virtual scroll
|
const virtualScroller = false // TODO: fix flickering issue with virtual scroll
|
||||||
|
|
||||||
const groupCapacity = Number.MAX_VALUE // No limit
|
const groupCapacity = Number.MAX_VALUE // No limit
|
||||||
|
@ -113,6 +116,8 @@ function groupItems(items: mastodon.v1.Notification[]): NotificationSlot[] {
|
||||||
// Finalize remaining groups
|
// Finalize remaining groups
|
||||||
processGroup()
|
processGroup()
|
||||||
|
|
||||||
|
nuxtApp.$trackScroll.restoreCustomPageScroll()
|
||||||
|
|
||||||
return results
|
return results
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
defineProps<{ path: string }>()
|
||||||
// Default limit is 20 notifications, and servers are normally caped to 30
|
// Default limit is 20 notifications, and servers are normally caped to 30
|
||||||
const paginator = useMastoClient().v1.notifications.list({ limit: 30, types: ['mention'] })
|
const paginator = useMastoClient().v1.notifications.list({ limit: 30, types: ['mention'] })
|
||||||
const stream = $(useStreaming(client => client.v1.stream.streamUser()))
|
const stream = $(useStreaming(client => client.v1.stream.streamUser()))
|
||||||
|
@ -8,5 +9,5 @@ onActivated(clearNotifications)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<NotificationPaginator v-bind="{ paginator, stream }" />
|
<NotificationPaginator v-bind="{ path, paginator, stream }" />
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
defineProps<{ path: string }>()
|
||||||
|
|
||||||
// Default limit is 20 notifications, and servers are normally caped to 30
|
// Default limit is 20 notifications, and servers are normally caped to 30
|
||||||
const paginator = useMastoClient().v1.notifications.list({ limit: 30 })
|
const paginator = useMastoClient().v1.notifications.list({ limit: 30 })
|
||||||
const stream = useStreaming(client => client.v1.stream.streamUser())
|
const stream = useStreaming(client => client.v1.stream.streamUser())
|
||||||
|
@ -8,5 +10,5 @@ onActivated(clearNotifications)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<NotificationPaginator v-bind="{ paginator, stream }" />
|
<NotificationPaginator v-bind="{ path, paginator, stream }" />
|
||||||
</template>
|
</template>
|
||||||
|
|
29
composables/page-transiton.ts
Normal file
29
composables/page-transiton.ts
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
export const usePageTransition = (options: {
|
||||||
|
beforeEach?: typeof noop
|
||||||
|
afterHydrated?: typeof noop
|
||||||
|
onTransitionError?: typeof noop
|
||||||
|
}) => {
|
||||||
|
const nuxtApp = useNuxtApp()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
if (options.beforeEach) {
|
||||||
|
router.beforeEach(() => {
|
||||||
|
options.beforeEach?.()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.onTransitionError) {
|
||||||
|
router.onError(() => {
|
||||||
|
options.onTransitionError?.()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.afterHydrated) {
|
||||||
|
const nuxtHook = () => {
|
||||||
|
if (isHydrated.value)
|
||||||
|
options.afterHydrated?.()
|
||||||
|
}
|
||||||
|
nuxtApp.hooks.hook('app:suspense:resolve', nuxtHook)
|
||||||
|
nuxtApp.hooks.hook('page:finish', nuxtHook)
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,8 +3,11 @@ const { t } = useI18n()
|
||||||
useHeadFixed({
|
useHeadFixed({
|
||||||
title: () => `${t('tab.notifications_all')} | ${t('nav.notifications')}`,
|
title: () => `${t('tab.notifications_all')} | ${t('nav.notifications')}`,
|
||||||
})
|
})
|
||||||
|
onMounted(() => {
|
||||||
|
useNuxtApp().$trackScroll.registerCustomRoute('/notifications')
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<TimelineNotifications v-if="isHydrated" />
|
<TimelineNotifications v-if="isHydrated" path="/notifications" />
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -3,8 +3,12 @@ const { t } = useI18n()
|
||||||
useHeadFixed({
|
useHeadFixed({
|
||||||
title: () => `${t('tab.notifications_mention')} | ${t('nav.notifications')}`,
|
title: () => `${t('tab.notifications_mention')} | ${t('nav.notifications')}`,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
useNuxtApp().$trackScroll.registerCustomRoute('/notification/mention')
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<TimelineMentions v-if="isHydrated" />
|
<TimelineMentions v-if="isHydrated" path="/notifications/mention" />
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -1,4 +1,8 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
definePageMeta({
|
||||||
|
noScrollTrack: true,
|
||||||
|
})
|
||||||
|
|
||||||
const buildInfo = useBuildInfo()
|
const buildInfo = useBuildInfo()
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,9 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
definePageMeta({
|
||||||
|
noScrollTrack: true,
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div min-h-screen flex justify-center items-center>
|
<div min-h-screen flex justify-center items-center>
|
||||||
<div text-center flex="~ col gap-2" items-center>
|
<div text-center flex="~ col gap-2" items-center>
|
||||||
|
|
|
@ -1,4 +1,8 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
definePageMeta({
|
||||||
|
noScrollTrack: true,
|
||||||
|
})
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
useHeadFixed({
|
useHeadFixed({
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { ElkTranslationStatus } from '~/types/translation-status'
|
import type { ElkTranslationStatus } from '~/types/translation-status'
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
noScrollTrack: true,
|
||||||
|
})
|
||||||
|
|
||||||
const { t, locale } = useI18n()
|
const { t, locale } = useI18n()
|
||||||
|
|
||||||
const translationStatus: ElkTranslationStatus = await import('~/elk-translation-status.json').then(m => m.default)
|
const translationStatus: ElkTranslationStatus = await import('~/elk-translation-status.json').then(m => m.default)
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
middleware: 'auth',
|
middleware: 'auth',
|
||||||
|
noScrollTrack: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
middleware: 'auth',
|
middleware: 'auth',
|
||||||
|
noScrollTrack: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
|
@ -4,6 +4,7 @@ definePageMeta({
|
||||||
if (!useAppConfig().pwaEnabled)
|
if (!useAppConfig().pwaEnabled)
|
||||||
return navigateTo('/settings/notifications')
|
return navigateTo('/settings/notifications')
|
||||||
}],
|
}],
|
||||||
|
noScrollTrack: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
|
@ -1,4 +1,8 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
definePageMeta({
|
||||||
|
noScrollTrack: true,
|
||||||
|
})
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
useHeadFixed({
|
useHeadFixed({
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { mastodon } from 'masto'
|
import type { mastodon } from 'masto'
|
||||||
import { ofetch } from 'ofetch'
|
|
||||||
import { useForm } from 'slimeform'
|
import { useForm } from 'slimeform'
|
||||||
import { parse } from 'ultrahtml'
|
import { parse } from 'ultrahtml'
|
||||||
import type { Component } from 'vue'
|
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
middleware: 'auth',
|
middleware: 'auth',
|
||||||
|
noScrollTrack: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
middleware: 'auth',
|
middleware: 'auth',
|
||||||
|
noScrollTrack: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
middleware: 'auth',
|
middleware: 'auth',
|
||||||
|
noScrollTrack: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
|
@ -3,6 +3,10 @@
|
||||||
import { fileOpen } from 'browser-fs-access'
|
import { fileOpen } from 'browser-fs-access'
|
||||||
import type { UserLogin } from '~/types'
|
import type { UserLogin } from '~/types'
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
noScrollTrack: true,
|
||||||
|
})
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
useHeadFixed({
|
useHeadFixed({
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
export default defineNuxtPlugin(() => {
|
export default defineNuxtPlugin((/* nuxtApp */) => {
|
||||||
return {
|
return {
|
||||||
provide: {
|
provide: {
|
||||||
scrollToTop: () => {
|
scrollToTop: () => {
|
||||||
window.scrollTo({ top: 0, left: 0, behavior: 'smooth' })
|
// if (typeof force === 'boolean' && force)
|
||||||
|
// nuxtApp.$trackScroll.forceScroll()
|
||||||
|
|
||||||
|
// window.scrollTo({ top: 0, left: 0, behavior: 'smooth' })
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
89
plugins/track-scroll-position.client.ts
Normal file
89
plugins/track-scroll-position.client.ts
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
export default defineNuxtPlugin(() => {
|
||||||
|
const route = useRoute()
|
||||||
|
const track = ref(false)
|
||||||
|
const { y } = useWindowScroll()
|
||||||
|
const storage = useLocalStorage<Record<string, number>>('elk-track-scroll', {})
|
||||||
|
const customRoutes = new Set<string>()
|
||||||
|
|
||||||
|
const forceScrollToTop = () => {
|
||||||
|
storage.value[route.fullPath] = 0
|
||||||
|
window.scrollTo({ top: 0, left: 0, behavior: 'smooth' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const restoreScrollCallback = (ignoreCustomRoutes: boolean) => {
|
||||||
|
const path = route.fullPath
|
||||||
|
return nextTick().then(() => {
|
||||||
|
if (route.meta?.noScrollTrack) {
|
||||||
|
forceScrollToTop()
|
||||||
|
return Promise.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise<void>((resolve, reject) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
const fullPath = route.fullPath
|
||||||
|
if (path !== fullPath) {
|
||||||
|
reject(new Error('navigation canceled'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const r = ignoreCustomRoutes ? undefined : customRoutes.has(fullPath)
|
||||||
|
if (r) {
|
||||||
|
reject(new Error('custom routed detected'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const scrollPosition = storage.value[fullPath]
|
||||||
|
if (scrollPosition)
|
||||||
|
window.scrollTo(0, scrollPosition)
|
||||||
|
|
||||||
|
// required for custom routes: first call will be rejected
|
||||||
|
// we need to enable scroll tracking again, it is disabled
|
||||||
|
if (!track.value) {
|
||||||
|
nextTick().then(() => {
|
||||||
|
track.value = true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve()
|
||||||
|
}, 600)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const restoreScroll = () => restoreScrollCallback(false)
|
||||||
|
|
||||||
|
const restoreCustomPageScroll = () => restoreScrollCallback(true)
|
||||||
|
|
||||||
|
usePageTransition({
|
||||||
|
beforeEach: () => {
|
||||||
|
track.value = false
|
||||||
|
},
|
||||||
|
afterHydrated: () => {
|
||||||
|
restoreScroll().then(() => {
|
||||||
|
track.value = true
|
||||||
|
}).catch(noop)
|
||||||
|
},
|
||||||
|
onTransitionError: () => {
|
||||||
|
track.value = true
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
watch([track, y, () => route], ([trackEnabled, scrollPosition, r]) => {
|
||||||
|
if (trackEnabled && (!r.meta || !r.meta?.noScrollTrack))
|
||||||
|
storage.value[r.fullPath] = Math.floor(scrollPosition)
|
||||||
|
}, { immediate: true, flush: 'pre' })
|
||||||
|
|
||||||
|
const registerCustomRoute = (path: string) => {
|
||||||
|
customRoutes.add(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
provide: {
|
||||||
|
trackScroll: {
|
||||||
|
forceScrollToTop,
|
||||||
|
registerCustomRoute,
|
||||||
|
restoreCustomPageScroll,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
Loading…
Reference in a new issue