fix: atproto oauth dev redirect, tab isolation, and concurrent load guard

- Use buildLoopbackClientId(window.location) for dev to include port in redirect URI
- Bind Vite dev server to 127.0.0.1 explicitly
- Remove scope override in signInRedirect (use metadata default)
- Clear OAuth callback params from URL after session restore
- Replace follows badge with DaisyUI tabs (All / Following)
- Use separate PublicNoteList instances per tab to isolate v-infinite-scroll state
- Add isLoading guard in onLoadMore to prevent concurrent fetches
This commit is contained in:
Julien Calixte
2026-03-10 14:18:41 +01:00
parent a234d590bd
commit c721338dc0
5 changed files with 66 additions and 43 deletions

View File

@@ -19,6 +19,8 @@ const initializeAuth = async () => {
did.value = session.did
handle.value = resolvedHandle
await saveSession(session.did, resolvedHandle)
window.history.replaceState(null, '', window.location.pathname)
} else {
const stored = await loadSession()
did.value = stored?.did ?? ''

View File

@@ -15,6 +15,7 @@ export function usePublicNoteList(options?: UsePublicNoteListOptions) {
const canLoadMore = computed(() => cursor.value !== undefined)
const onLoadMore = async () => {
if (isLoading.value) return
isLoading.value = true
const path = options?.did?.value ? `/${options.did.value}/notes` : "/notes"

View File

@@ -1,7 +1,8 @@
import { BrowserOAuthClient } from '@atproto/oauth-client-browser'
import { BrowserOAuthClient, buildLoopbackClientId } from '@atproto/oauth-client-browser'
const CLIENT_ID = import.meta.env.DEV
? 'http://localhost'
const getClientId = () =>
import.meta.env.DEV
? buildLoopbackClientId(new URL(window.location.origin))
: 'https://remanso.space/client-metadata.json'
let clientPromise: Promise<BrowserOAuthClient> | null = null
@@ -9,7 +10,7 @@ let clientPromise: Promise<BrowserOAuthClient> | null = null
export const getOAuthClient = (): Promise<BrowserOAuthClient> => {
if (!clientPromise) {
clientPromise = BrowserOAuthClient.load({
clientId: CLIENT_ID,
clientId: getClientId(),
handleResolver: 'https://bsky.social',
})
}
@@ -18,7 +19,7 @@ export const getOAuthClient = (): Promise<BrowserOAuthClient> => {
export const signInWithHandle = async (handle: string): Promise<void> => {
const client = await getOAuthClient()
await client.signInRedirect(handle, { scope: 'atproto transition:generic' })
await client.signInRedirect(handle)
}
export const restoreSession = async () => {

View File

@@ -5,11 +5,15 @@ import SignInAtproto from "@/components/SignInAtproto.vue"
import { useATProtoLogin } from "@/hooks/useATProtoLogin.hook"
import { useFollows } from "@/hooks/useFollows.hook"
import { usePublicNoteList } from "@/hooks/usePublicNoteList.hook"
import { ref } from "vue"
const { did, isLoggedIn } = useATProtoLogin()
const { follows } = useFollows(did)
const { notes, isLoading, canLoadMore, onLoadMore, getAuthor } =
usePublicNoteList({ followsFilter: follows })
const tab = ref<'all' | 'following'>('all')
const all = usePublicNoteList()
const following = usePublicNoteList({ followsFilter: follows })
</script>
<template>
@@ -19,28 +23,48 @@ const { notes, isLoading, canLoadMore, onLoadMore, getAuthor } =
<h1>Remanso notes</h1>
<sign-in-atproto />
</div>
<div v-if="isLoggedIn && follows.size > 0" class="follows-badge">
Showing follows only
<div v-if="isLoggedIn" role="tablist" class="tabs tabs-border">
<a role="tab" class="tab" :class="{ 'tab-active': tab === 'all' }" @click="tab = 'all'">All</a>
<a role="tab" class="tab" :class="{ 'tab-active': tab === 'following' }" @click="tab = 'following'">Following</a>
</div>
<div v-if="isLoading"></div>
<div v-else>
<PublicNoteList
:notes="notes"
:can-load-more="canLoadMore"
:on-load-more="onLoadMore"
v-if="tab === 'all'"
:notes="all.notes.value"
:can-load-more="all.canLoadMore.value"
:on-load-more="all.onLoadMore"
>
<template #meta="{ note }">
<router-link
v-if="getAuthor(note.did)"
:to="{
name: 'PublicNoteListByDidView',
params: { did: note.did },
}"
v-if="all.getAuthor(note.did)"
:to="{ name: 'PublicNoteListByDidView', params: { did: note.did } }"
class="link link-hover"
>
{{ getAuthor(note.did) }}
{{ all.getAuthor(note.did) }}
</router-link>
<template v-if="note.publishedAt">
<span>&nbsp;&nbsp;</span>
<span>{{ new Date(note.publishedAt).toLocaleDateString() }}</span>
</template>
<div v-else class="skeleton h-4 w-20"></div>
</template>
</PublicNoteList>
<PublicNoteList
v-else
:notes="following.notes.value"
:can-load-more="following.canLoadMore.value"
:on-load-more="following.onLoadMore"
>
<template #meta="{ note }">
<router-link
v-if="following.getAuthor(note.did)"
:to="{ name: 'PublicNoteListByDidView', params: { did: note.did } }"
class="link link-hover"
>
{{ following.getAuthor(note.did) }}
</router-link>
<template v-if="note.publishedAt">
<span>&nbsp;&nbsp;</span>
<span>{{ new Date(note.publishedAt).toLocaleDateString() }}</span>
@@ -48,7 +72,6 @@ const { notes, isLoading, canLoadMore, onLoadMore, getAuthor } =
<div v-else class="skeleton h-4 w-20"></div>
</template>
</PublicNoteList>
</div>
</main>
</template>
@@ -83,11 +106,4 @@ const { notes, isLoading, canLoadMore, onLoadMore, getAuthor } =
overflow-y: auto;
}
}
.follows-badge {
font-size: 0.8rem;
opacity: 0.7;
text-align: center;
margin-bottom: 0.5rem;
}
</style>

View File

@@ -77,6 +77,9 @@ export default defineConfig(({ command }) => {
config.define = {
global: {},
}
config.server = {
host: '127.0.0.1',
}
}
return config