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:
Julien Calixte
2026-03-19 18:12:52 +01:00
parent 52561496b4
commit ddabe5082d
5 changed files with 20 additions and 12 deletions

View File

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

View File

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

View File

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

View File

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

View File

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