feat: show skeleton loaders while ATProto identity resolves
- Show skeleton in PublicNoteView and StackedPublicNote while note content is pending author resolution - Show skeleton h1 in PublicNoteListByDidView while author loads - Show skeleton in SignInAtproto until auth state is known - Load cached session from IndexedDB before OAuth restore so the homepage resolves immediately without waiting for network
This commit is contained in:
@@ -3,7 +3,7 @@ import { ref } from "vue"
|
|||||||
|
|
||||||
import { useATProtoLogin } from "@/hooks/useATProtoLogin.hook"
|
import { useATProtoLogin } from "@/hooks/useATProtoLogin.hook"
|
||||||
|
|
||||||
const { handle, isLoggedIn, signIn, signOut } = useATProtoLogin()
|
const { handle, isLoggedIn, isATProtoReady, signIn, signOut } = useATProtoLogin()
|
||||||
|
|
||||||
withDefaults(
|
withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
@@ -24,13 +24,14 @@ const onSignIn = () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="isLoggedIn" class="sign-in-atproto is-signed-in">
|
<div v-if="!isATProtoReady" class="skeleton h-8 w-40"></div>
|
||||||
|
<div v-else-if="isLoggedIn" class="sign-in-atproto is-signed-in">
|
||||||
<span>{{ handle }}</span>
|
<span>{{ handle }}</span>
|
||||||
<button class="btn btn-sm" @click="signOut" v-if="withSignOut">
|
<button class="btn btn-sm" @click="signOut" v-if="withSignOut">
|
||||||
Sign out
|
Sign out
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="sign-in-atproto join">
|
<div v-else-if="!isLoggedIn" class="sign-in-atproto join">
|
||||||
<input
|
<input
|
||||||
v-model="inputHandle"
|
v-model="inputHandle"
|
||||||
class="input input-sm join-item"
|
class="input input-sm join-item"
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { withATProtoImages } from "@/modules/atproto/withATProtoImages"
|
|||||||
import { getAuthor } from "@/modules/atproto/getAuthor"
|
import { getAuthor } from "@/modules/atproto/getAuthor"
|
||||||
import { fromShortDid } from "@/modules/atproto/shortDid"
|
import { fromShortDid } from "@/modules/atproto/shortDid"
|
||||||
import { PublicNoteRecord } from "@/modules/atproto/publicNote.types"
|
import { PublicNoteRecord } from "@/modules/atproto/publicNote.types"
|
||||||
|
import SkeletonLoader from "@/components/SkeletonLoader.vue"
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
didrkey: string
|
didrkey: string
|
||||||
@@ -99,7 +100,8 @@ watch(
|
|||||||
<div v-if="noteNotFound" class="alert alert-error">
|
<div v-if="noteNotFound" class="alert alert-error">
|
||||||
This note no longer exists.
|
This note no longer exists.
|
||||||
</div>
|
</div>
|
||||||
<div class="note-content" v-else v-html="content"></div>
|
<div class="note-content" v-else-if="content" v-html="content"></div>
|
||||||
|
<skeleton-loader v-else-if="!noteNotFound" />
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -10,8 +10,13 @@ const handle = ref<string | null>(null)
|
|||||||
let init = true
|
let init = true
|
||||||
|
|
||||||
const initializeAuth = async () => {
|
const initializeAuth = async () => {
|
||||||
const session = await restoreSession()
|
// Load cached session from IndexedDB first (fast, local) so the UI can render immediately
|
||||||
|
const stored = await loadSession()
|
||||||
|
did.value = stored?.did ?? ''
|
||||||
|
handle.value = stored?.handle ?? ''
|
||||||
|
|
||||||
|
// Then restore OAuth session in the background (may involve network)
|
||||||
|
const session = await restoreSession()
|
||||||
if (session) {
|
if (session) {
|
||||||
const author = await getAuthor(session.did)
|
const author = await getAuthor(session.did)
|
||||||
const resolvedHandle = author?.handle ?? ''
|
const resolvedHandle = author?.handle ?? ''
|
||||||
@@ -21,10 +26,6 @@ const initializeAuth = async () => {
|
|||||||
await saveSession(session.did, resolvedHandle)
|
await saveSession(session.did, resolvedHandle)
|
||||||
|
|
||||||
window.history.replaceState(null, '', window.location.pathname + window.location.search)
|
window.history.replaceState(null, '', window.location.pathname + window.location.search)
|
||||||
} else {
|
|
||||||
const stored = await loadSession()
|
|
||||||
did.value = stored?.did ?? ''
|
|
||||||
handle.value = stored?.handle ?? ''
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import HomeButton from "@/components/HomeButton.vue"
|
import HomeButton from "@/components/HomeButton.vue"
|
||||||
import PublicNoteList from "@/components/PublicNoteList.vue"
|
import PublicNoteList from "@/components/PublicNoteList.vue"
|
||||||
|
import SkeletonLoader from "@/components/SkeletonLoader.vue"
|
||||||
import { usePublicNoteList } from "@/hooks/usePublicNoteList.hook"
|
import { usePublicNoteList } from "@/hooks/usePublicNoteList.hook"
|
||||||
import { getAuthor } from "@/modules/atproto/getAuthor"
|
import { getAuthor } from "@/modules/atproto/getAuthor"
|
||||||
import { fromShortDid } from "@/modules/atproto/shortDid"
|
import { fromShortDid } from "@/modules/atproto/shortDid"
|
||||||
@@ -19,9 +20,10 @@ const author = computedAsync(async () => getAuthor(did.value))
|
|||||||
<main class="public-note-list-view">
|
<main class="public-note-list-view">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<home-button class="back-button" />
|
<home-button class="back-button" />
|
||||||
<h1>{{ author?.handle ?? did }}</h1>
|
<h1 v-if="author">{{ author.handle }}</h1>
|
||||||
|
<div v-else class="skeleton h-8 w-40"></div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="isLoading"></div>
|
<skeleton-loader v-if="isLoading" />
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<PublicNoteList
|
<PublicNoteList
|
||||||
:notes="notes"
|
:notes="notes"
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { useRouter } from "vue-router"
|
|||||||
import { errorMessage } from "@/utils/notif"
|
import { errorMessage } from "@/utils/notif"
|
||||||
import { useResizeContainer } from "@/hooks/useResizeContainer.hook"
|
import { useResizeContainer } from "@/hooks/useResizeContainer.hook"
|
||||||
import ThemeSwap from "@/components/ThemeSwap.vue"
|
import ThemeSwap from "@/components/ThemeSwap.vue"
|
||||||
|
import SkeletonLoader from "@/components/SkeletonLoader.vue"
|
||||||
import { useTitle } from "@vueuse/core"
|
import { useTitle } from "@vueuse/core"
|
||||||
import { displayLanguage } from "@/utils/displayLanguage"
|
import { displayLanguage } from "@/utils/displayLanguage"
|
||||||
|
|
||||||
@@ -163,7 +164,8 @@ watch(
|
|||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<article class="note-display" v-html="content"></article>
|
<article class="note-display" v-if="content" v-html="content"></article>
|
||||||
|
<skeleton-loader v-else-if="!noteNotFound" />
|
||||||
</div>
|
</div>
|
||||||
<stacked-public-note
|
<stacked-public-note
|
||||||
v-for="(stackedNote, index) in stackedNotes"
|
v-for="(stackedNote, index) in stackedNotes"
|
||||||
|
|||||||
Reference in New Issue
Block a user