feat: add public notes by author page

Extract note-list fetching into usePublicNoteList composable, add
/pub/:did route to view notes from a single author, and make author
aliases clickable links in both the note list and note view.
This commit is contained in:
Julien Calixte
2026-02-15 13:18:59 +01:00
parent bf73f08cb2
commit ff8795581e
5 changed files with 156 additions and 42 deletions

View File

@@ -0,0 +1,43 @@
import { Author, getAka } from "@/modules/atproto/getAka"
import { PublicNoteListItem } from "@/modules/note/models/Note"
import { computedAsync } from "@vueuse/core"
import { computed, ref, Ref } from "vue"
export function usePublicNoteList(did?: Ref<string | undefined>) {
const isLoading = ref(false)
const notes = ref<PublicNoteListItem[]>([])
const cursor = ref<string | null | undefined>(null)
const canLoadMore = computed(() => cursor.value !== undefined)
const onLoadMore = async () => {
isLoading.value = true
const path = did?.value ? `/${did.value}/notes` : "/notes"
const noteAPI = new URL(path, "https://api.litenote.li212.fr")
if (cursor.value) {
noteAPI.searchParams.set("cursor", cursor.value)
}
const response = await fetch(noteAPI)
const data: { notes: PublicNoteListItem[]; cursor: string | undefined } =
await response.json()
notes.value.push(...data.notes)
cursor.value = data.cursor
isLoading.value = false
}
const aka = computedAsync<Map<string, Author>>(async () => {
if (notes.value.length === 0) {
return new Map()
}
return getAka(new Set(notes.value.map((n) => n.did)))
}, new Map())
const getAlias = (did: string) =>
aka.value.has(did) ? aka.value.get(did)?.alias : ""
return { notes, isLoading, canLoadMore, onLoadMore, aka, getAlias }
}

View File

@@ -20,12 +20,18 @@ const routes: Array<RouteRecordRaw> = [
component: () => import("@/views/PublicNoteListView.vue"), component: () => import("@/views/PublicNoteListView.vue"),
}, },
{ {
path: "/notes", path: "/pub",
name: "PublicNoteListView", name: "PublicNoteListView",
component: () => import("@/views/PublicNoteListView.vue"), component: () => import("@/views/PublicNoteListView.vue"),
}, },
{ {
path: "/notes/:did/:rkey", path: "/pub/:did",
name: "PublicNoteListByDidView",
props: true,
component: () => import("@/views/PublicNoteListByDidView.vue"),
},
{
path: "/pub/:did/:rkey",
name: "PublicNoteView", name: "PublicNoteView",
props: true, props: true,
component: () => import("@/views/PublicNoteView.vue"), component: () => import("@/views/PublicNoteView.vue"),

View File

@@ -0,0 +1,87 @@
<script setup lang="ts">
import BackButton from "@/components/BackButton.vue"
import { usePublicNoteList } from "@/hooks/usePublicNoteList.hook"
import { getUniqueAka } from "@/modules/atproto/getAka"
import { computedAsync } from "@vueuse/core"
import { computed } from "vue"
import { vInfiniteScroll } from "@vueuse/components"
const props = defineProps<{ did: string }>()
const did = computed(() => props.did)
const { notes, isLoading, canLoadMore, onLoadMore } = usePublicNoteList(did)
const author = computedAsync(async () => getUniqueAka(did.value))
</script>
<template>
<main class="public-note-list-view">
<h1>{{ author?.alias ?? did }}</h1>
<back-button class="back-button" />
<div v-if="isLoading"></div>
<div v-else>
<ul
class="list rounded-box shadow-sm"
v-infinite-scroll="[onLoadMore, { canLoadMore: () => canLoadMore }]"
>
<li v-for="note in notes" class="list-row">
<div class="list-col">
<router-link
:to="{
name: 'PublicNoteView',
params: { did: note.did, rkey: note.rkey },
}"
class="btn btn-link"
>{{ note.title }}</router-link
>
<div class="text-xs opacity-80 alias">
<span v-if="note.publishedAt">
{{ new Date(note.publishedAt).toLocaleDateString() }}
</span>
</div>
</div>
</li>
</ul>
</div>
</main>
</template>
<style scoped lang="scss">
.public-note-list-view {
display: flex;
flex: 1;
flex-direction: column;
margin-left: 1rem;
h1 {
margin-top: 1rem;
}
.back-button {
display: flex;
gap: 0.5rem;
align-items: center;
}
li {
display: flex;
.list-col {
display: flex;
flex-direction: column;
flex: 1;
}
a {
text-align: left;
}
.alias {
text-align: right;
display: flex;
justify-content: flex-end;
}
}
}
</style>

View File

@@ -1,44 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import BackButton from "@/components/BackButton.vue" import BackButton from "@/components/BackButton.vue"
import { Author, getAka } from "@/modules/atproto/getAka" import { usePublicNoteList } from "@/hooks/usePublicNoteList.hook"
import { PublicNoteListItem } from "@/modules/note/models/Note"
import { computedAsync } from "@vueuse/core"
import { computed, ref } from "vue"
import { vInfiniteScroll } from "@vueuse/components" import { vInfiniteScroll } from "@vueuse/components"
const isLoading = ref(false) const { notes, isLoading, canLoadMore, onLoadMore, getAlias } =
const notes = ref<PublicNoteListItem[]>([]) usePublicNoteList()
const cursor = ref<string | null | undefined>(null)
const canLoadMore = computed(() => cursor.value !== undefined)
const onLoadMore = async () => {
isLoading.value = true
const noteAPI = new URL("/notes", "https://api.litenote.li212.fr")
if (cursor.value) {
noteAPI.searchParams.set("cursor", cursor.value)
}
const response = await fetch(noteAPI)
const data: { notes: PublicNoteListItem[]; cursor: string | undefined } =
await response.json()
notes.value.push(...data.notes)
cursor.value = data.cursor
isLoading.value = false
}
const aka = computedAsync<Map<string, Author>>(async () => {
if (notes.value.length === 0) {
return new Map()
}
return getAka(new Set(notes.value.map((n) => n.did)))
}, new Map())
const getAlias = (did: string) =>
aka.value.has(did) ? aka.value.get(did)?.alias : ""
</script> </script>
<template> <template>
@@ -63,9 +29,16 @@ const getAlias = (did: string) =>
> >
<div class="text-xs opacity-80 alias"> <div class="text-xs opacity-80 alias">
<span v-if="getAlias(note.did)"> <router-link
v-if="getAlias(note.did)"
:to="{
name: 'PublicNoteListByDidView',
params: { did: note.did },
}"
class="link link-hover"
>
{{ getAlias(note.did) }} {{ getAlias(note.did) }}
</span> </router-link>
<span v-if="note.publishedAt" <span v-if="note.publishedAt"
>&nbsp;&nbsp;{{ >&nbsp;&nbsp;{{
new Date(note.publishedAt).toLocaleDateString() new Date(note.publishedAt).toLocaleDateString()

View File

@@ -90,7 +90,12 @@ watch(
</div> </div>
<span class="badge badge-author" v-if="author"> <span class="badge badge-author" v-if="author">
<router-link
:to="{ name: 'PublicNoteListByDidView', params: { did: did } }"
class="link link-hover"
>
{{ author.alias }} {{ author.alias }}
</router-link>
<span v-if="publishedAt">&nbsp;&nbsp;{{ publishedAt }}</span> <span v-if="publishedAt">&nbsp;&nbsp;{{ publishedAt }}</span>
</span> </span>
<article class="note-display" v-html="content"></article> <article class="note-display" v-html="content"></article>