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.
217 lines
4.9 KiB
Vue
217 lines
4.9 KiB
Vue
<script lang="ts" setup>
|
|
import { computedAsync } from "@vueuse/core"
|
|
import { computed, ref, watch } from "vue"
|
|
import { useRoute } from "vue-router"
|
|
|
|
import SkeletonLoader from "@/components/SkeletonLoader.vue"
|
|
import { useATProtoLinks } from "@/hooks/useATProtoLinks.hook"
|
|
import { markdownBuilder } from "@/hooks/useMarkdown.hook"
|
|
import { useMarkdownPostRender } from "@/hooks/useMarkdownPostRender.hook"
|
|
import { useNoteOverlay } from "@/hooks/useNoteOverlay.hook"
|
|
import { useRouteQueryStackedNotes } from "@/hooks/useRouteQueryStackedNotes.hook"
|
|
import { getAuthor } from "@/modules/atproto/getAuthor"
|
|
import { getUrl } from "@/modules/atproto/getUrl"
|
|
import { PublicNoteRecord } from "@/modules/atproto/publicNote.types"
|
|
import { fromShortDid } from "@/modules/atproto/shortDid"
|
|
import { withATProtoImages } from "@/modules/atproto/withATProtoImages"
|
|
import { errorMessage } from "@/utils/notif"
|
|
|
|
const props = defineProps<{
|
|
didrkey: string
|
|
index: number
|
|
}>()
|
|
|
|
const didrkey = computed(() => props.didrkey)
|
|
const did = computed(() => fromShortDid(props.didrkey.split("-")[0]))
|
|
const rkey = computed(() => props.didrkey.split("-")[1])
|
|
const classNameId = computed(() => didrkey.value.replaceAll(":", "-"))
|
|
|
|
const index = computed(() => props.index)
|
|
|
|
const author = computedAsync(async () => getAuthor(did.value))
|
|
const url = computedAsync(async () =>
|
|
getUrl({ did: did.value, rkey: rkey.value })
|
|
)
|
|
|
|
const className = computed(() => `stacked-note-${props.index}`)
|
|
const titleClassName = computed(() => `title-${className.value}`)
|
|
|
|
const route = useRoute()
|
|
const mainNoteId = computed(
|
|
() => `${route.params.shortDid}-${route.params.rkey}`
|
|
)
|
|
|
|
const { scrollToFocusedNote } = useRouteQueryStackedNotes()
|
|
const { listenToClick } = useATProtoLinks(className.value, {
|
|
currentAtUri: didrkey,
|
|
mainNoteId
|
|
})
|
|
const { displayNoteOverlay } = useNoteOverlay(className.value, index)
|
|
|
|
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.")
|
|
}
|
|
})
|
|
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
|
|
})
|
|
)
|
|
: ""
|
|
)
|
|
|
|
useMarkdownPostRender(content, () => `.note-${classNameId.value}`, {
|
|
onReady: () => listenToClick(),
|
|
tikz: true
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<div
|
|
class="stacked-note"
|
|
:class="{
|
|
[className]: true,
|
|
overlay: displayNoteOverlay,
|
|
[`note-${classNameId}`]: true
|
|
}"
|
|
>
|
|
<a
|
|
class="title-stacked-note-link"
|
|
@click.prevent="scrollToFocusedNote({ noteId: didrkey })"
|
|
>
|
|
<div
|
|
class="title-stacked-note breadcrumbs text-sm"
|
|
:class="titleClassName"
|
|
>
|
|
<span v-if="author">{{ author.handle }} • </span> {{ title }}
|
|
</div>
|
|
</a>
|
|
<section class="text-content">
|
|
<div v-if="noteNotFound" class="alert alert-error">
|
|
This note no longer exists.
|
|
</div>
|
|
<div class="note-content" v-else-if="content" v-html="content"></div>
|
|
<skeleton-loader v-else-if="!noteNotFound" />
|
|
</section>
|
|
</div>
|
|
</template>
|
|
|
|
<style lang="scss" scoped>
|
|
$border-color: rgba(18, 19, 58, 0.2);
|
|
|
|
.stacked-note {
|
|
padding: 0 1.5rem 1rem;
|
|
background-color: var(--color-base-100);
|
|
color: var(--color-base-content);
|
|
scrollbar-width: none;
|
|
|
|
&.overlay {
|
|
box-shadow: -3px 0 0.4em $border-color;
|
|
}
|
|
|
|
section {
|
|
padding: 0 0.5rem;
|
|
}
|
|
}
|
|
|
|
.offline-ready {
|
|
position: absolute;
|
|
top: 1rem;
|
|
right: 1rem;
|
|
}
|
|
|
|
.title-stacked-note {
|
|
background-color: var(--color-base-100);
|
|
color: var(--color-base-content);
|
|
font-size: 0.8em;
|
|
|
|
ul,
|
|
li {
|
|
margin-top: 0;
|
|
margin-bottom: 0;
|
|
padding-left: 0;
|
|
text-decoration: none;
|
|
}
|
|
}
|
|
|
|
.text-content {
|
|
flex: 1;
|
|
scrollbar-width: none;
|
|
|
|
div {
|
|
height: 100%;
|
|
}
|
|
}
|
|
|
|
.action {
|
|
float: right;
|
|
margin: 0.2rem;
|
|
|
|
img {
|
|
vertical-align: bottom;
|
|
}
|
|
}
|
|
|
|
@media screen and (max-width: 768px) {
|
|
.stacked-note {
|
|
padding: 0 0.75rem 1rem;
|
|
|
|
section {
|
|
padding: 1rem 0;
|
|
overflow-x: auto;
|
|
}
|
|
|
|
.note-content {
|
|
padding: 0;
|
|
scrollbar-width: none;
|
|
}
|
|
}
|
|
}
|
|
|
|
@media screen and (min-width: 769px) {
|
|
.stacked-note {
|
|
border-top: 0;
|
|
border-left: 1px solid $border-color;
|
|
position: sticky;
|
|
top: 0;
|
|
}
|
|
|
|
.title-stacked-note {
|
|
padding: 0 1rem;
|
|
transform-origin: 0 0;
|
|
transform: rotate(90deg);
|
|
}
|
|
|
|
a {
|
|
white-space: nowrap;
|
|
}
|
|
}
|
|
|
|
@media print {
|
|
.stacked-note {
|
|
break-after: always;
|
|
|
|
&.overlay {
|
|
box-shadow: none;
|
|
}
|
|
}
|
|
}
|
|
</style>
|