Merge branch 'main' of github.com:remanso-space/remanso
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<script lang="ts" setup>
|
||||
import GoBack from '@/components/GoBack.vue'
|
||||
import GoBack from "@/components/GoBack.vue"
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
104
src/views/PublicNoteListByDidView.vue
Normal file
104
src/views/PublicNoteListByDidView.vue
Normal file
@@ -0,0 +1,104 @@
|
||||
<script setup lang="ts">
|
||||
import BackButton from "@/components/BackButton.vue"
|
||||
import { usePublicNoteList } from "@/hooks/usePublicNoteList.hook"
|
||||
import { getAuthor } from "@/modules/atproto/getAuthor"
|
||||
import { slugify } from "@/utils/slugify"
|
||||
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 () => getAuthor(did.value))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="public-note-list-view">
|
||||
<div class="header">
|
||||
<back-button class="back-button" :fallback="{ name: 'Home' }" />
|
||||
<h1>{{ author?.handle ?? did }}</h1>
|
||||
</div>
|
||||
<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, slug: slugify(note.title) },
|
||||
}"
|
||||
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;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
|
||||
.header {
|
||||
margin-top: 1rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
h1 {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 769px) {
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,64 +1,64 @@
|
||||
<script setup lang="ts">
|
||||
import BackButton from "@/components/BackButton.vue"
|
||||
import { Author, getAka } from "@/modules/atproto/getAka"
|
||||
import { PublicNoteListItem } from "@/modules/note/models/Note"
|
||||
import { computedAsync, useAsyncState } from "@vueuse/core"
|
||||
import { usePublicNoteList } from "@/hooks/usePublicNoteList.hook"
|
||||
import { slugify } from "@/utils/slugify"
|
||||
import { vInfiniteScroll } from "@vueuse/components"
|
||||
|
||||
const { state, isLoading } = useAsyncState<{
|
||||
notes: PublicNoteListItem[]
|
||||
}>(
|
||||
async () => {
|
||||
const response = await fetch("https://api.litenote.li212.fr/notes")
|
||||
return response.json()
|
||||
},
|
||||
{ notes: [] },
|
||||
)
|
||||
|
||||
const aka = computedAsync<Map<string, Author>>(async () => {
|
||||
if (state.value.notes.length === 0) {
|
||||
return new Map()
|
||||
}
|
||||
|
||||
return getAka(new Set(state.value.notes.map((n) => n.did)))
|
||||
}, new Map())
|
||||
|
||||
const getAlias = (did: string) =>
|
||||
aka.value.has(did) ? aka.value.get(did)?.alias : ""
|
||||
const { notes, isLoading, canLoadMore, onLoadMore, getAuthor } =
|
||||
usePublicNoteList()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="public-note-list-view">
|
||||
<h1>Remanso notes</h1>
|
||||
<div class="header">
|
||||
<back-button class="back-button" :fallback="{ name: 'Home' }" />
|
||||
<h1>Remanso notes</h1>
|
||||
</div>
|
||||
<div v-if="isLoading"></div>
|
||||
<div v-else>
|
||||
<ul class="list rounded-box shadow-sm">
|
||||
<li v-for="note in state.notes" class="list-row">
|
||||
<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 },
|
||||
params: {
|
||||
did: note.did,
|
||||
rkey: note.rkey,
|
||||
slug: slugify(note.title),
|
||||
},
|
||||
}"
|
||||
class="btn btn-link"
|
||||
>{{ note.title }}</router-link
|
||||
>
|
||||
|
||||
<div class="text-xs opacity-90 alias">
|
||||
<span v-if="getAlias(note.did)">
|
||||
{{ getAlias(note.did) }}
|
||||
</span>
|
||||
<span v-if="note.publishedAt"
|
||||
> • {{
|
||||
<div class="text-xs opacity-80 alias">
|
||||
<router-link
|
||||
v-if="getAuthor(note.did)"
|
||||
:to="{
|
||||
name: 'PublicNoteListByDidView',
|
||||
params: { did: note.did },
|
||||
}"
|
||||
class="link link-hover"
|
||||
>
|
||||
{{ getAuthor(note.did) }}
|
||||
</router-link>
|
||||
|
||||
<template v-if="note.publishedAt">
|
||||
<span> • </span>
|
||||
<span>{{
|
||||
new Date(note.publishedAt).toLocaleDateString()
|
||||
}}
|
||||
</span>
|
||||
}}</span>
|
||||
</template>
|
||||
<div v-else class="skeleton h-4 w-20"></div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<BackButton class="back-button" />
|
||||
</main>
|
||||
</template>
|
||||
|
||||
@@ -67,10 +67,20 @@ const getAlias = (did: string) =>
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
margin-left: 1rem;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
|
||||
.header {
|
||||
margin-top: 1rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-top: 1rem;
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.back-button {
|
||||
@@ -98,5 +108,9 @@ const getAlias = (did: string) =>
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 769px) {
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,103 +2,83 @@
|
||||
import { useATProtoLinks } from "@/hooks/useATProtoLinks.hook"
|
||||
import { markdownBuilder } from "@/hooks/useMarkdown.hook"
|
||||
import BackButton from "@/components/BackButton.vue"
|
||||
import StackedPublicNote from "@/components/StackedPublicNote.vue"
|
||||
import { useRouteQueryStackedNotes } from "@/hooks/useRouteQueryStackedNotes.hook"
|
||||
import { getUniqueAka } from "@/modules/atproto/getAka"
|
||||
import { getAuthor } from "@/modules/atproto/getAuthor"
|
||||
import type { PublicNoteRecord } from "@/modules/atproto/publicNote.types"
|
||||
import { withATProtoImages } from "@/modules/atproto/withATProtoImages"
|
||||
import { getUrl } from "@/modules/atproto/getUrl"
|
||||
import { downloadFont } from "@/utils/downloadFont"
|
||||
import { slugify } from "@/utils/slugify"
|
||||
import { computedAsync } from "@vueuse/core"
|
||||
import { computed, nextTick, watch } from "vue"
|
||||
import { useRouter } from "vue-router"
|
||||
import { useResizeContainer } from "@/hooks/useResizeContainer.hook"
|
||||
import ThemeSwap from "@/components/ThemeSwap.vue"
|
||||
|
||||
export interface Root {
|
||||
uri: string
|
||||
cid: string
|
||||
value: Value
|
||||
}
|
||||
|
||||
export interface Value {
|
||||
$type: string
|
||||
title: string
|
||||
images: Image[]
|
||||
content: string
|
||||
createdAt: string
|
||||
publishedAt: string
|
||||
theme?: string
|
||||
fontFamily?: string
|
||||
fontSize?: string
|
||||
}
|
||||
|
||||
export interface Image {
|
||||
alt: string
|
||||
image: Image2
|
||||
}
|
||||
|
||||
export interface Image2 {
|
||||
$type: string
|
||||
ref: Ref
|
||||
mimeType: string
|
||||
size: number
|
||||
}
|
||||
|
||||
export interface Ref {
|
||||
$link: string
|
||||
}
|
||||
|
||||
const props = defineProps<{ did: string; rkey: string }>()
|
||||
const props = defineProps<{ did: string; rkey: string; slug?: string }>()
|
||||
const router = useRouter()
|
||||
const did = computed(() => props.did)
|
||||
const rkey = computed(() => props.rkey)
|
||||
const { scrollToFocusedNote } = useRouteQueryStackedNotes()
|
||||
|
||||
const author = computedAsync(async () => getUniqueAka(did.value))
|
||||
const url = computedAsync(async () =>
|
||||
getUrl({ did: did.value, rkey: rkey.value }),
|
||||
const author = computedAsync(async () => getAuthor(did.value))
|
||||
const url = computedAsync(
|
||||
async () => getUrl({ did: did.value, rkey: rkey.value }),
|
||||
null,
|
||||
)
|
||||
|
||||
const article = computedAsync(async () =>
|
||||
url.value ? ((await fetch(url.value).then()).json() as Promise<Root>) : null,
|
||||
const noteRecord = computedAsync(async () =>
|
||||
url.value
|
||||
? ((await fetch(url.value).then()).json() as Promise<PublicNoteRecord>)
|
||||
: null,
|
||||
)
|
||||
|
||||
watch(article, () => {
|
||||
if (article.value?.value.fontFamily) {
|
||||
downloadFont(article.value.value.fontFamily)
|
||||
watch(noteRecord, () => {
|
||||
if (
|
||||
noteRecord.value?.value.title &&
|
||||
props.slug &&
|
||||
props.slug !== slugify(noteRecord.value.value.title)
|
||||
) {
|
||||
router.replace({ name: "SpaceCowboy" })
|
||||
return
|
||||
}
|
||||
|
||||
if (article.value?.value.fontSize) {
|
||||
if (noteRecord.value?.value.fontFamily) {
|
||||
downloadFont(noteRecord.value.value.fontFamily)
|
||||
}
|
||||
|
||||
if (noteRecord.value?.value.fontSize) {
|
||||
const root = document.documentElement
|
||||
root.style.setProperty("--font-size", `${article.value.value.fontSize}pt`)
|
||||
root.style.setProperty(
|
||||
"--font-size",
|
||||
`${noteRecord.value.value.fontSize}pt`,
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
const { toHTML } = markdownBuilder()
|
||||
const withATProtoImages = (markdown: string) => {
|
||||
if (!author.value) {
|
||||
return markdown
|
||||
}
|
||||
|
||||
const endpoint = author.value.endpoint
|
||||
|
||||
const imageLinkPattern = /!\[([^\]]*)\]\((bafkrei[a-z0-9]+)\)/g
|
||||
|
||||
return markdown.replace(imageLinkPattern, (_, altText, cid) => {
|
||||
const imageUrl = new URL("/xrpc/com.atproto.sync.getBlob", endpoint)
|
||||
imageUrl.searchParams.set("did", did.value)
|
||||
imageUrl.searchParams.set("cid", cid)
|
||||
return `})`
|
||||
})
|
||||
}
|
||||
|
||||
const title = computed(() => article.value?.value.title)
|
||||
const title = computed(() => noteRecord.value?.value.title)
|
||||
const content = computed(() =>
|
||||
article.value?.value.content
|
||||
? toHTML(withATProtoImages(article.value?.value.content))
|
||||
noteRecord.value?.value.content && author.value
|
||||
? toHTML(
|
||||
withATProtoImages(noteRecord.value.value.content, {
|
||||
pds: author.value.pds,
|
||||
did: did.value,
|
||||
}),
|
||||
)
|
||||
: "",
|
||||
)
|
||||
|
||||
const publishedAt = computed(() =>
|
||||
article.value?.value.publishedAt
|
||||
? new Date(article.value?.value.publishedAt).toLocaleDateString()
|
||||
noteRecord.value?.value.publishedAt
|
||||
? new Date(noteRecord.value?.value.publishedAt).toLocaleDateString()
|
||||
: null,
|
||||
)
|
||||
|
||||
const { stackedNotes, scrollToFocusedNote } = useRouteQueryStackedNotes()
|
||||
const { listenToClick } = useATProtoLinks("note-display")
|
||||
useResizeContainer("note-container", stackedNotes)
|
||||
|
||||
watch(
|
||||
content,
|
||||
@@ -111,8 +91,32 @@ watch(
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="public-note-view repo-note">
|
||||
<div class="public-note-view repo-note note-container">
|
||||
<div class="note article">
|
||||
<div class="header">
|
||||
<back-button
|
||||
:fallback="{ name: 'PublicNoteListByDidView', params: { did } }"
|
||||
:prefer-fallback="false"
|
||||
/>
|
||||
<theme-swap />
|
||||
|
||||
<span
|
||||
class="badge badge-author badge-soft badge-accent"
|
||||
v-if="author && content"
|
||||
>
|
||||
<router-link
|
||||
:to="{ name: 'PublicNoteListByDidView', params: { did: did } }"
|
||||
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"
|
||||
@@ -122,13 +126,15 @@ watch(
|
||||
>
|
||||
</div>
|
||||
|
||||
<span class="badge badge-author" v-if="author">
|
||||
{{ author.alias }}
|
||||
<span v-if="publishedAt"> • {{ publishedAt }}</span>
|
||||
</span>
|
||||
<article class="note-display" v-html="content"></article>
|
||||
<BackButton />
|
||||
</div>
|
||||
<stacked-public-note
|
||||
v-for="(stackedNote, index) in stackedNotes"
|
||||
:key="stackedNote"
|
||||
class="note"
|
||||
:index="index"
|
||||
:at-uri="stackedNote"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -137,32 +143,22 @@ watch(
|
||||
display: flex;
|
||||
flex: 1;
|
||||
|
||||
.back-button {
|
||||
position: absolute;
|
||||
left: 1.5rem;
|
||||
top: 0.4rem;
|
||||
.header {
|
||||
margin-top: 1rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.badge-author {
|
||||
position: absolute;
|
||||
top: 0.4rem;
|
||||
right: 2rem;
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.article {
|
||||
position: sticky;
|
||||
padding: 0 2rem;
|
||||
scrollbar-width: none;
|
||||
|
||||
article {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
&.content {
|
||||
@@ -211,6 +207,7 @@ watch(
|
||||
transform-origin: 0 0;
|
||||
transform: rotate(90deg);
|
||||
font-size: 0.8em;
|
||||
text-wrap: nowrap;
|
||||
|
||||
a {
|
||||
color: var(--color-base-content);
|
||||
@@ -224,5 +221,21 @@ watch(
|
||||
max-width: var(--note-width);
|
||||
}
|
||||
}
|
||||
|
||||
.note {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
flex-wrap: wrap;
|
||||
|
||||
.repo-title-breadcrumb {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.article article {
|
||||
margin-top: 48px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -6,8 +6,13 @@ import { markdownBuilder } from "@/hooks/useMarkdown.hook"
|
||||
import { queryFileContent } from "@/modules/repo/services/repo"
|
||||
import { decodeBase64ToUTF8 } from "@/utils/decodeBase64ToUTF8"
|
||||
|
||||
type Prop = {
|
||||
user: string
|
||||
repo: string
|
||||
}
|
||||
|
||||
const FluxNote = defineAsyncComponent(() => import("@/components/FluxNote.vue"))
|
||||
const props = defineProps<{ user: string; repo: string }>()
|
||||
const props = defineProps<Prop>()
|
||||
const user = computed(() => props.user)
|
||||
const repo = computed(() => props.repo)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user