Four components duplicated the watch -> nextTick -> listenToClick -> runTikz shape. Collapse to useMarkdownPostRender, which also accepts optional flags for Mermaid, Shikiji and image hydration so StackedNote no longer needs its own inline conditional chain.
288 lines
6.9 KiB
Vue
288 lines
6.9 KiB
Vue
<script setup lang="ts">
|
|
import { computedAsync } from "@vueuse/core"
|
|
import { useTitle } from "@vueuse/core"
|
|
import { computed, ref, watch } from "vue"
|
|
import { useRouter } from "vue-router"
|
|
|
|
import HomeButton from "@/components/HomeButton.vue"
|
|
import SkeletonLoader from "@/components/SkeletonLoader.vue"
|
|
import StackedPublicNote from "@/components/StackedPublicNote.vue"
|
|
import ThemeSwap from "@/components/ThemeSwap.vue"
|
|
import { useATProtoLinks } from "@/hooks/useATProtoLinks.hook"
|
|
import { markdownBuilder } from "@/hooks/useMarkdown.hook"
|
|
import { useMarkdownPostRender } from "@/hooks/useMarkdownPostRender.hook"
|
|
import { useResizeContainer } from "@/hooks/useResizeContainer.hook"
|
|
import { useRouteQueryStackedNotes } from "@/hooks/useRouteQueryStackedNotes.hook"
|
|
import { getAuthor } from "@/modules/atproto/getAuthor"
|
|
import { getUrl } from "@/modules/atproto/getUrl"
|
|
import type { PublicNoteRecord } from "@/modules/atproto/publicNote.types"
|
|
import { fromShortDid } from "@/modules/atproto/shortDid"
|
|
import { withATProtoImages } from "@/modules/atproto/withATProtoImages"
|
|
import { displayLanguage } from "@/utils/displayLanguage"
|
|
import { downloadFont } from "@/utils/downloadFont"
|
|
import { errorMessage } from "@/utils/notif"
|
|
import { slugify } from "@/utils/slugify"
|
|
|
|
const props = defineProps<{ shortDid: string; rkey: string; slug?: string }>()
|
|
const router = useRouter()
|
|
const did = computed(() => fromShortDid(props.shortDid))
|
|
const rkey = computed(() => props.rkey)
|
|
|
|
const author = computedAsync(async () => getAuthor(did.value))
|
|
const url = computedAsync(
|
|
async () => getUrl({ did: did.value, rkey: rkey.value }),
|
|
null
|
|
)
|
|
|
|
const noteNotFound = ref(false)
|
|
const noteRecord = computedAsync(async () => {
|
|
if (!url.value) return null
|
|
const response = await fetch(url.value)
|
|
if (!response.ok) {
|
|
noteNotFound.value = true
|
|
return null
|
|
}
|
|
return response.json() as Promise<PublicNoteRecord>
|
|
})
|
|
|
|
watch(noteNotFound, (notFound) => {
|
|
if (notFound) {
|
|
errorMessage("This note no longer exists.")
|
|
router.replace({ name: "SpaceCowboy" })
|
|
}
|
|
})
|
|
|
|
watch(noteRecord, () => {
|
|
if (
|
|
noteRecord.value?.value.title &&
|
|
props.slug &&
|
|
props.slug !== slugify(noteRecord.value.value.title)
|
|
) {
|
|
router.replace({ name: "SpaceCowboy" })
|
|
return
|
|
}
|
|
|
|
if (noteRecord.value?.value.fontFamily) {
|
|
downloadFont(noteRecord.value.value.fontFamily)
|
|
}
|
|
|
|
if (noteRecord.value?.value.fontSize) {
|
|
const root = document.documentElement
|
|
root.style.setProperty(
|
|
"--font-size",
|
|
`${noteRecord.value.value.fontSize}pt`
|
|
)
|
|
}
|
|
})
|
|
|
|
const { toHTML } = markdownBuilder()
|
|
|
|
const title = computed(() => noteRecord.value?.value.title)
|
|
const content = computed(() =>
|
|
noteRecord.value?.value.content && author.value
|
|
? toHTML(
|
|
withATProtoImages(noteRecord.value.value.content, {
|
|
pds: author.value.pds,
|
|
did: did.value
|
|
})
|
|
)
|
|
: ""
|
|
)
|
|
|
|
const breadcrumb = computed(() =>
|
|
title.value
|
|
? author.value?.handle
|
|
? `${title.value} • ${author.value.handle}`
|
|
: title.value
|
|
: `Remanso`
|
|
)
|
|
|
|
useTitle(breadcrumb)
|
|
|
|
const publishedAt = computed(() =>
|
|
noteRecord.value?.value.publishedAt
|
|
? new Date(noteRecord.value?.value.publishedAt).toLocaleDateString()
|
|
: null
|
|
)
|
|
const language = computed(() =>
|
|
noteRecord.value?.value.language
|
|
? displayLanguage(noteRecord.value.value.language)
|
|
: null
|
|
)
|
|
|
|
const mainNoteId = computed(() => `${props.shortDid}-${props.rkey}`)
|
|
|
|
const { stackedNotes, scrollToFocusedNote } = useRouteQueryStackedNotes()
|
|
const { listenToClick } = useATProtoLinks("note-display", { mainNoteId })
|
|
useResizeContainer("note-container", stackedNotes)
|
|
|
|
useMarkdownPostRender(content, () => ".public-note-view .note-display", {
|
|
onReady: () => listenToClick(),
|
|
tikz: true
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<main class="public-note-view repo-note note-container">
|
|
<div class="note article">
|
|
<div class="header">
|
|
<home-button />
|
|
<theme-swap />
|
|
</div>
|
|
<div class="subheader">
|
|
<span
|
|
class="badge badge-author badge-soft badge-accent"
|
|
v-if="author && content"
|
|
>
|
|
<template v-if="language">
|
|
<span>{{ language }}</span>
|
|
<span> • </span>
|
|
</template>
|
|
<router-link
|
|
:to="{ name: 'PublicNoteListByDidView', params: { shortDid } }"
|
|
class="link link-hover"
|
|
>
|
|
{{ author.handle }}
|
|
</router-link>
|
|
<template v-if="publishedAt">
|
|
<span> • </span>
|
|
<span>{{ publishedAt }}</span>
|
|
</template>
|
|
</span>
|
|
<div class="badge skeleton h-4 w-50" v-else></div>
|
|
</div>
|
|
<div class="repo-title-breadcrumb">
|
|
<a
|
|
class="title-stacked-note-link"
|
|
@click.prevent="scrollToFocusedNote()"
|
|
v-if="breadcrumb"
|
|
>{{ breadcrumb }}</a
|
|
>
|
|
</div>
|
|
|
|
<article class="note-display" v-if="content" v-html="content"></article>
|
|
<skeleton-loader v-else-if="!noteNotFound" />
|
|
</div>
|
|
<stacked-public-note
|
|
v-for="(stackedNote, index) in stackedNotes"
|
|
:key="stackedNote"
|
|
class="note"
|
|
:index="index"
|
|
:didrkey="stackedNote"
|
|
/>
|
|
</main>
|
|
</template>
|
|
|
|
<style lang="scss">
|
|
.public-note-view {
|
|
display: flex;
|
|
flex: 1;
|
|
width: 100%;
|
|
|
|
.header {
|
|
margin-top: 1rem;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
flex-wrap: wrap;
|
|
gap: 1rem;
|
|
}
|
|
|
|
.subheader {
|
|
margin: 1rem auto 0;
|
|
}
|
|
|
|
h1 {
|
|
font-size: 2rem;
|
|
}
|
|
|
|
.article {
|
|
padding: 0 2rem;
|
|
scrollbar-width: none;
|
|
left: 0;
|
|
top: 0;
|
|
}
|
|
|
|
&.content {
|
|
.title,
|
|
h1,
|
|
h2,
|
|
h3,
|
|
h4,
|
|
h5,
|
|
h6,
|
|
strong {
|
|
color: var(--color-base-content);
|
|
}
|
|
|
|
table {
|
|
color: var(--color-base-content);
|
|
background-color: var(--color-base-100);
|
|
|
|
thead {
|
|
th {
|
|
color: var(--color-base-content);
|
|
}
|
|
}
|
|
}
|
|
|
|
blockquote {
|
|
background-color: var(--color-base-100);
|
|
color: var(--color-base-content);
|
|
}
|
|
}
|
|
|
|
.note {
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow-y: auto;
|
|
height: 100vh;
|
|
width: 100%;
|
|
|
|
.title {
|
|
text-align: left;
|
|
}
|
|
}
|
|
|
|
@media screen and (min-width: 769px) {
|
|
background-color: var(--note-canvas-bg);
|
|
|
|
.repo-title-breadcrumb {
|
|
padding: 0.5rem 1rem 0;
|
|
transform-origin: 0 0;
|
|
transform: rotate(90deg);
|
|
font-size: 0.8em;
|
|
text-wrap: nowrap;
|
|
|
|
a {
|
|
color: var(--color-base-content);
|
|
display: block;
|
|
text-align: center;
|
|
}
|
|
}
|
|
|
|
.note {
|
|
min-width: var(--note-width);
|
|
max-width: var(--note-width);
|
|
background-color: var(--color-base-100);
|
|
}
|
|
|
|
.article {
|
|
box-shadow: var(--note-sheet-shadow);
|
|
}
|
|
}
|
|
|
|
@media screen and (max-width: 768px) {
|
|
flex-wrap: wrap;
|
|
|
|
.repo-title-breadcrumb {
|
|
display: none;
|
|
}
|
|
|
|
.article article {
|
|
margin-top: 48px;
|
|
}
|
|
}
|
|
}
|
|
</style>
|