Compare commits

...

14 commits

Author SHA1 Message Date
userquin
369501efca Merge branch 'main' into userquin/feat-track-scroll-position 2023-02-15 16:48:44 +01:00
userquin
97866ebeab Merge branch 'main' into userquin/feat-track-scroll-position
# Conflicts:
#	pages/settings/language/index.vue
2023-02-12 13:33:51 +01:00
userquin
a64c0f4e9b chore: create page-transition composable 2023-02-11 16:20:12 +01:00
userquin
6dde98eb78 chore: restore nav side item click state 2023-02-11 15:58:49 +01:00
userquin
db2140e350 chore: allow scroll to top on nav side 2023-02-11 15:42:37 +01:00
userquin
f3a8778ede chore: expose only required methods 2023-02-11 12:42:01 +01:00
userquin
5acd2224df chore: move path to page when using notification paginator 2023-02-11 12:31:53 +01:00
userquin
26883d6d19 chore: use forceScrollToTop on click hook 2023-02-11 12:31:39 +01:00
userquin
1241921435 chore: simplify logic 2023-02-11 00:23:58 +01:00
userquin
9bc00be29a chore: refactor some names 2023-02-11 00:15:57 +01:00
userquin
bb119d0f8d chore: add custom page scroll track 2023-02-10 23:37:09 +01:00
userquin
3e0b2a3e4b chore: do not apply timeout on pages without scroll tracking 2023-02-10 22:34:34 +01:00
userquin
a7414bb59e chore: update watch 2023-02-10 22:28:53 +01:00
userquin
3270140c3f feat: track scroll position 2023-02-10 22:13:46 +01:00
22 changed files with 194 additions and 11 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View 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)
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -1,4 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
definePageMeta({
noScrollTrack: true,
})
const { t } = useI18n() const { t } = useI18n()
useHeadFixed({ useHeadFixed({

View file

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

View file

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

View file

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

View file

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

View file

@ -1,4 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
definePageMeta({
noScrollTrack: true,
})
const { t } = useI18n() const { t } = useI18n()
useHeadFixed({ useHeadFixed({

View file

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

View file

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

View file

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

View file

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

View file

@ -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' })
}, },
}, },
} }

View 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,
},
},
}
})