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:
43
src/hooks/usePublicNoteList.hook.ts
Normal file
43
src/hooks/usePublicNoteList.hook.ts
Normal 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 }
|
||||||
|
}
|
||||||
@@ -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"),
|
||||||
|
|||||||
87
src/views/PublicNoteListByDidView.vue
Normal file
87
src/views/PublicNoteListByDidView.vue
Normal 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>
|
||||||
@@ -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"
|
||||||
> • {{
|
> • {{
|
||||||
new Date(note.publishedAt).toLocaleDateString()
|
new Date(note.publishedAt).toLocaleDateString()
|
||||||
|
|||||||
@@ -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"> • {{ publishedAt }}</span>
|
<span v-if="publishedAt"> • {{ publishedAt }}</span>
|
||||||
</span>
|
</span>
|
||||||
<article class="note-display" v-html="content"></article>
|
<article class="note-display" v-html="content"></article>
|
||||||
|
|||||||
Reference in New Issue
Block a user