refactor: 🔥 Remove old Note code

This commit is contained in:
Jesse Wierzbinski 2024-12-01 17:26:51 +01:00
parent c6f8ba081d
commit 0b6acd98dd
No known key found for this signature in database
13 changed files with 6 additions and 785 deletions

View file

@ -1,57 +0,0 @@
<template>
<HeadlessTransitionRoot as="template" :show="lightbox">
<Dialog.Root v-model:open="lightbox" :close-on-escape="true" :close-on-interact-outside="true"
@update:open="o => lightbox = o">
<Teleport to="body">
<Dialog.Positioner class="z-50">
<HeadlessTransitionChild as="template" enter="ease-out duration-200" enter-from="opacity-0"
enter-to="opacity-100" leave="ease-in duration-200" leave-from="opacity-100"
leave-to="opacity-0">
<Dialog.Backdrop class="fixed inset-0 bg-black/70 !z-40" @click="lightbox = false" />
</HeadlessTransitionChild>
<Dialog.Content
class="w-screen h-screen flex !z-50 justify-center items-center flex-col overflow-hidden p-10 fixed inset-0">
<div class="w-full absolute inset-x-0 top-0 p-10 shrink text-gray-400 flex flex-row gap-3">
<a @click.stop :href="attachment?.url" target="_blank" download class="ml-auto">
<iconify-icon icon="tabler:download" width="1.5rem" height="1.5rem" />
<span class="sr-only">Close</span>
</a>
<button @click.stop="lightbox = false">
<iconify-icon icon="tabler:x" width="1.5rem" height="1.5rem" />
<span class="sr-only">Close</span>
</button>
</div>
<HeadlessTransitionChild as="template" enter="ease-out duration-200"
enter-from="opacity-0 sm:scale-95" enter-to="opacity-100 sm:scale-100"
leave="ease-in duration-200" leave-from="opacity-100 sm:scale-100"
leave-to="opacity-0 sm:scale-95">
<img @click.stop v-if="attachment?.type === 'image'" class="rounded max-w-[70%] max-h-[70%]"
:src="attachment.url" :alt="attachment.description ?? ''"
:title="attachment.description ?? ''" />
</HeadlessTransitionChild>
<span @click.stop v-if="attachment?.description"
class="text-gray-300 rounded mt-6 -mb-20 px-4 py-2 max-w-xl ring-1 ring-white/5 bg-dark-900 max-h-40 overflow-y-auto">
{{ attachment.description }}
</span>
</Dialog.Content>
</Dialog.Positioner>
</Teleport>
</Dialog.Root>
</HeadlessTransitionRoot>
</template>t
<script lang="ts" setup>
import { Dialog } from "@ark-ui/vue";
import type { Attachment } from "@versia/client/types";
const lightbox = ref(false);
const attachment = ref<Attachment | null>(null);
useListen("attachment:view", async (a) => {
attachment.value = a;
await nextTick();
lightbox.value = true;
});
</script>

View file

@ -1,75 +0,0 @@
<template>
<div class="aspect-video relative">
<div tabindex="0" aria-label="Open attachment in lightbox" role="button"
class="w-full h-full rounded ring-white/5 shadow overflow-hidden ring-1 hover:ring-2 duration-100 relative">
<img v-if="attachment.type === 'image'"
class="object-cover w-full h-full rounded duration-150 hover:scale-[102%] ease-in-out"
:src="attachment.url" :alt="attachment.description ?? undefined" @click="openLightbox"
@keydown="openLightbox" />
<video v-else-if="attachment.type === 'video'" class="object-contain w-full h-full rounded aspect-video"
controls :alt="attachment.description ?? undefined" :src="attachment.url">
Your browser does not support the video tag.
</video>
<a v-else class="bg-dark-800 w-full h-full rounded flex items-center justify-center" :href="attachment.url"
target="_blank" download>
<div class="flex flex-col items-center gap-2 text-center max-w-56 overflow-hidden text-ellipsis">
<iconify-icon icon="tabler:file" width="none" class="size-10 text-gray-300" />
<p class="text-gray-300 text-sm font-mono">{{ getFilename(attachment.url) }}</p>
<p class="text-gray-300 text-xs" v-if="attachment.meta?.length">{{
formatBytes(Number(attachment.meta?.length)) }}</p>
</div>
</a>
</div>
<!-- Alt text viewer -->
<Popover.Root :positioning="{
strategy: 'fixed',
}" v-if="attachment.description">
<Popover.Trigger aria-hidden="true"
class="absolute top-2 right-2 p-1 bg-dark-800 ring-1 ring-white/5 text-white text-xs rounded size-8">
<iconify-icon icon="tabler:alt" width="none" class="size-6" />
</Popover.Trigger>
<Popover.Positioner class="!z-10">
<Popover.Content class="p-4 bg-dark-400 rounded text-sm ring-1 ring-dark-100 shadow-lg text-gray-300">
<Popover.Description>{{ attachment.description }}</Popover.Description>
</Popover.Content>
</Popover.Positioner>
</Popover.Root>
</div>
</template>
<script lang="ts" setup>
import { Popover } from "@ark-ui/vue";
import type { Attachment } from "@versia/client/types";
const props = defineProps<{
attachment: Attachment;
}>();
const openLightbox = () => {
useEvent("attachment:view", props.attachment);
};
const formatBytes = (bytes: number) => {
if (bytes === 0) {
return "0 Bytes";
}
const k = 1000;
const dm = 2;
const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${Number.parseFloat((bytes / k ** i).toFixed(dm))} ${sizes[i]}`;
};
const getFilename = (url: string) => {
// Handle proxy case
if (url.includes("/media/proxy")) {
// Decode last part of URL as base64url, which is the real URL
const realUrl = atob(url.split("/").pop() ?? "");
return decodeURIComponent(
realUrl.substring(realUrl.lastIndexOf("/") + 1),
);
}
const path = new URL(url).pathname;
return path.substring(path.lastIndexOf("/") + 1);
};
</script>

View file

@ -1,129 +0,0 @@
<template>
<div v-if="small" class="flex flex-row">
<NuxtLink :href="accountUrl" class="shrink-0">
<Avatar :src="note?.account.avatar" :alt="`${note?.account.acct}'s avatar`"
class="size-6 rounded ring-1 ring-white/5" />
<span class="sr-only">Account profile</span>
</NuxtLink>
<div class="flex flex-col items-start justify-around ml-4 grow overflow-hidden">
<div class="flex flex-row text-sm items-center justify-between w-full">
<NuxtLink :href="accountUrl" class="font-semibold text-gray-200 line-clamp-1 break-all">
<Skeleton :enabled="!note" :min-width="90" :max-width="170" shape="rect">
<span v-html="display_name"></span>
</Skeleton>
</NuxtLink>
<NuxtLink :href="noteUrl" class="text-gray-300 ml-2 line-clamp-1 break-all shrink-0">
<Skeleton :enabled="!note" :min-width="50" :max-width="100" shape="rect">
{{ timeAgo }}
</Skeleton>
</NuxtLink>
</div>
</div>
</div>
<div v-else class="flex flex-row gap-x-4">
<UserCard :account="note?.account">
<NuxtLink :href="accountUrl" class="shrink-0">
<Avatar :src="note?.account.avatar" :alt="`${note?.account.acct}'s avatar`"
class="h-12 w-12 rounded ring-1 ring-white/5" />
<span class="sr-only">Account profile</span>
</NuxtLink>
</UserCard>
<div class="flex flex-col items-start justify-around grow overflow-hidden">
<div class="flex flex-row items-center justify-between w-full">
<NuxtLink :href="accountUrl" class="font-semibold text-gray-200 line-clamp-1 break-all">
<Skeleton :enabled="!note" :min-width="90" :max-width="170" shape="rect">
<span v-html="display_name"></span>
</Skeleton>
</NuxtLink>
<NuxtLink v-if="note" :href="noteUrl" class="text-gray-300 text-sm ml-2 line-clamp-1 break-all shrink-0"
:title="visibilities[note.visibility].text">
<iconify-icon :icon="visibilities[note.visibility].icon" width="1.25rem" height="1.25rem"
class="text-gray-400" aria-hidden="true" />
<span class="sr-only">{{ visibilities[note.visibility].text }}</span>
</NuxtLink>
</div>
<div class="flex flex-row items-center justify-between w-full group">
<span class="text-gray-300 text-sm line-clamp-1 break-all w-full">
<Skeleton :enabled="!note" :min-width="130" :max-width="250" shape="rect">
<div class="group-hover:hidden">
<span class="font-bold bg-gradient-to-tr from-primary-300 via-purple-300 to-indigo-400 text-transparent bg-clip-text">@{{ username }}</span><span class="text-gray-500">{{ instance && "@" }}{{ instance }}</span>
&middot; <span
class="text-gray-400 cursor-help ml-auto" :alt="fullTime"
:title="fullTime">
<Skeleton :enabled="!note" :min-width="10" :max-width="50" shape="rect">
{{ timeAgo }}
</Skeleton>
</span >
</div>
<span @click="copyAccount" v-if="!hasCopied"
class="hidden select-none w-full group-hover:flex cursor-pointer items-center gap-x-1">
<iconify-icon icon="tabler:clipboard" height="1rem" width="1rem" class="text-gray-200"
aria-hidden="true" />
Click to copy
</span>
<span v-else class="hidden group-hover:flex select-none items-center gap-x-1">
<iconify-icon icon="tabler:check" height="1rem" width="1rem" class="text-green-500"
aria-hidden="true" />
Copied!
</span>
</Skeleton>
</span>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import type { Status } from "@versia/client/types";
import Avatar from "~/components/avatars/avatar.vue";
import Skeleton from "~/components/skeleton/Skeleton.vue";
import UserCard from "~/components/social-elements/users/UserCard.vue";
const props = defineProps<{
note?: Status;
small?: boolean;
}>();
const username = props.note?.account.acct.split("@")[0];
const instance = props.note?.account.acct.split("@")[1];
const { display_name } = useParsedAccount(props.note?.account, settings);
const noteUrl = props.note && `/@${props.note.account.acct}/${props.note.id}`;
const accountUrl = props.note && `/@${props.note.account.acct}`;
const timeAgo = useTimeAgo(props.note?.created_at ?? 0, {});
const fullTime = Intl.DateTimeFormat("default", {
dateStyle: "medium",
timeStyle: "short",
}).format(new Date(props.note?.created_at ?? 0));
const hasCopied = ref(false);
const { copy } = useClipboard();
const copyAccount = () => {
if (props.note) {
copy(`@${props.note.account.acct}`);
hasCopied.value = true;
setTimeout(() => {
hasCopied.value = false;
}, 2000);
}
};
const visibilities = {
public: {
icon: "tabler:world",
text: "This note is public: it can be seen by anyone.",
},
unlisted: {
icon: "tabler:lock-open",
text: "This note is unlisted: it can be seen by anyone with the link.",
},
private: {
icon: "tabler:lock",
text: "This note is private: it can only be seen by followers.",
},
direct: {
icon: "tabler:mail",
text: "This note is direct: it can only be seen by mentioned users.",
},
};
</script>

View file

@ -1,9 +0,0 @@
<template>
<button class="group disabled:opacity-70 max-w-28 disabled:cursor-not-allowed hover:enabled:bg-dark-800 duration-200 rounded flex flex-1 flex-row items-center justify-center">
<slot />
</button>
</template>
<script lang="ts" setup>
</script>

View file

@ -1,85 +0,0 @@
<template>
<div
class="mt-6 flex flex-row items-stretch relative justify-around text-sm h-10">
<InteractionButton @click="useEvent('note:reply', note)"
:disabled="!identity">
<iconify-icon width="1.25rem" height="1.25rem" icon="tabler:arrow-back-up"
class="text-gray-200 group-hover:group-enabled:text-blue-600" aria-hidden="true" />
<span class="text-gray-400 mt-0.5 ml-2" v-if="note.replies_count">{{ numberFormat(note.replies_count) }}</span>
</InteractionButton>
<InteractionButton @click="likeFn" :disabled="!identity">
<iconify-icon width="1.25rem" height="1.25rem" icon="tabler:heart" v-if="!note.favourited"
class="size-5 text-gray-200 group-hover:group-enabled:text-primary2-600" aria-hidden="true" />
<iconify-icon width="1.25rem" height="1.25rem" icon="tabler:heart-filled" v-else
class="size-5 text-primary2-600 group-hover:group-enabled:text-gray-200" aria-hidden="true" />
<span class="text-gray-400 mt-0.5 ml-2" v-if="note.favourites_count">{{ numberFormat(note.favourites_count) }}</span>
</InteractionButton>
<InteractionButton @click="reblogFn" :disabled="!identity">
<iconify-icon width="1.25rem" height="1.25rem" icon="tabler:repeat" v-if="!note.reblogged"
class="size-5 text-gray-200 group-hover:group-enabled:text-green-600" aria-hidden="true" />
<iconify-icon width="1.25rem" height="1.25rem" icon="tabler:repeat" v-else
class="size-5 text-green-600 group-hover:group-enabled:text-gray-200" aria-hidden="true" />
<span class="text-gray-400 mt-0.5 ml-2" v-if="note.reblogs_count">{{ numberFormat(note.reblogs_count) }}</span>
</InteractionButton>
<InteractionButton @click="useEvent('note:quote', note)"
:disabled="!identity">
<iconify-icon width="1.25rem" height="1.25rem" icon="tabler:quote"
class="size-5 text-gray-200 group-hover:group-enabled:text-blue-600" aria-hidden="true" />
</InteractionButton>
<NoteMenu v-model:note="note" :url="url" :remove="remove" />
</div>
</template>
<script lang="ts" setup>
import type { Status } from "@versia/client/types";
import NoteMenu from "../note-menu.vue";
import InteractionButton from "./button.vue";
defineProps<{
url: string;
remove: () => Promise<void>;
}>();
const note = defineModel<Status>("note", {
required: true,
});
const numberFormat = (number = 0) =>
new Intl.NumberFormat(undefined, {
notation: "compact",
compactDisplay: "short",
maximumFractionDigits: 1,
}).format(number);
const likeFn = async () => {
if (note.value.favourited) {
const output = await client.value.unfavouriteStatus(note.value.id);
if (output?.data) {
note.value = output.data;
}
} else {
const output = await client.value.favouriteStatus(note.value.id);
if (output?.data) {
note.value = output.data;
}
}
};
const reblogFn = async () => {
if (note.value?.reblogged) {
const output = await client.value.unreblogStatus(note.value.id);
if (output?.data) {
note.value = output.data;
}
} else {
const output = await client.value.reblogStatus(note.value.id);
if (output?.data.reblog) {
note.value = output.data.reblog;
}
}
};
</script>

View file

@ -1,16 +0,0 @@
<template>
<a :href="`/@${account.acct}`" target="_blank"
class="shrink break-all rounded bg-dark-200 ring-1 ring-white/5 ring-inset text-primary2-200 px-2 py-1 not-prose font-semibold cursor-pointer [&:not(:last-child)]:mr-1 duration-200 hover:bg-primary2-600/30">
<img class="size-[1em] rounded ring-1 ring-white/5 !inline align-middle mb-1 mr-1" :src="account.avatar"
:alt="`${account.acct}'s avatar'`" />
{{ account.display_name || account.acct }}
</a>
</template>
<script lang="ts" setup>
import type { Account } from "@versia/client/types";
defineProps<{
account: Account;
}>();
</script>

View file

@ -1,109 +0,0 @@
<template>
<div v-if="!collapsed" class="mt-6">
<Skeleton :enabled="!props.note || !loaded" :min-width="50" :max-width="100" width-unit="%" shape="rect"
type="content">
<div v-if="content"
:class="['prose block relative prose-invert duration-200 !max-w-full break-words prose-a:no-underline', $style.content]"
v-html="content">
</div>
</Skeleton>
<div v-if="note && note.media_attachments.length > 0"
class="[&:not(:first-child)]:mt-6 grid grid-cols-2 gap-4 [&>*]:aspect-square [&:has(>:last-child:nth-child(1))>*]:aspect-video [&:has(>:last-child:nth-child(1))]:block">
<Attachment v-for="attachment of note.media_attachments" :key="attachment.id" :attachment="attachment" />
</div>
<Note v-if="isQuote && note?.quote" :element="note?.quote" :small="true" class="mt-4 !rounded" />
</div>
<div v-else
class="rounded text-center ring-1 !max-w-full ring-white/10 h-52 mt-6 prose prose-invert p-4 flex flex-col justify-center items-center">
<strong v-if="note?.sensitive" class="max-w-64">This note was tagged as containing sensitive
content</strong>
<!-- Spoiler text is it's specified -->
<span v-if="note?.spoiler_text" class="mt-2 break-all">{{ note.spoiler_text
}}</span>
<Button theme="secondary" @click="collapsed = false" class="mt-4">Show content</Button>
</div>
</template>
<script lang="ts" setup>
import type { Status } from "@versia/client/types";
import Skeleton from "~/components/skeleton/Skeleton.vue";
import Button from "~/packages/ui/components/buttons/button.vue";
import Attachment from "./attachment.vue";
import Note from "./note.vue";
const props = defineProps<{
content: string | null;
note?: Status;
loaded?: boolean;
url: string;
shouldHide?: boolean;
isQuote?: boolean;
}>();
const collapsed = ref(props.shouldHide);
</script>
<style module>
.content pre:has(code) {
word-wrap: normal;
background: transparent;
background-color: #ffffff0d;
border-radius: .25rem;
-webkit-hyphens: none;
hyphens: none;
margin-top: 1rem;
overflow-x: auto;
padding: .75rem 1rem;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
white-space: pre;
word-break: normal;
word-spacing: normal;
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), 0 0 #0000;
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
--tw-ring-color: hsla(0, 0%, 100%, .1)
}
.content pre code {
display: block;
padding: 0
}
.content code:not(pre code)::after,
.content code:not(pre code)::before {
content: ""
}
.content ol li input[type=checkbox],
.content ul li input[type=checkbox] {
border-radius:.25rem;
margin-bottom:0.2rem;
margin-right:.5rem;
margin-top:0;
vertical-align: middle;
--tw-text-opacity:1;
color: var(--theme-primary-400);
}
.content code:not(pre code) {
border-radius: .25rem;
padding: .25rem .5rem;
word-wrap: break-word;
background: transparent;
background-color: #ffffff0d;
-webkit-hyphens: none;
hyphens: none;
margin-top: 1rem;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), 0 0 #0000;
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
--tw-ring-color: hsla(0, 0%, 100%, .1)
}
</style>

View file

@ -1,156 +0,0 @@
<template>
<AdaptiveDropdown>
<template #button>
<InteractionButton>
<iconify-icon width="1.25rem" height="1.25rem" icon="tabler:dots" class="size-5 text-gray-200"
aria-hidden="true" />
<span class="sr-only">Open menu</span>
</InteractionButton>
</template>
<template #items>
<Menu.ItemGroup>
<Menu.Item value="" v-if="isMyAccount">
<ButtonDropdown @click="note && useEvent('note:edit', note)" icon="tabler:pencil" class="w-full">
Edit
</ButtonDropdown>
</Menu.Item>
<Menu.Item value="">
<ButtonDropdown @click="copy(JSON.stringify(note, null, 4))" icon="tabler:code"
class="w-full">
Copy API
response
</ButtonDropdown>
</Menu.Item>
<Menu.Item value="">
<ButtonDropdown @click="copy(url)" icon="tabler:link" class="w-full">
Copy link
</ButtonDropdown>
</Menu.Item>
<Menu.Item value="" v-if="note?.url && isRemote">
<ButtonDropdown @click="copy(note.url)" icon="tabler:link" class="w-full">
Copy link (origin)
</ButtonDropdown>
</Menu.Item>
<Menu.Item value="" v-if="note?.url && isRemote">
<ButtonDropdown @click="openBlank(note.url)" icon="tabler:external-link" class="w-full">
View on remote
</ButtonDropdown>
</Menu.Item>
<Menu.Item value="" v-if="isMyAccount">
<ButtonDropdown @click="remove" icon="tabler:backspace" :disabled="!identity"
class="w-full border-r-2 border-red-500">
Delete
</ButtonDropdown>
</Menu.Item>
</Menu.ItemGroup>
<hr class="border-white/10 rounded" v-if="identity" />
<Menu.ItemGroup v-if="identity">
<Menu.Item value="">
<ButtonDropdown @click="note && useEvent('note:reply', note)" icon="tabler:arrow-back-up"
class="w-full">
Reply
</ButtonDropdown>
</Menu.Item>
<Menu.Item value="">
<ButtonDropdown @click="likeFn" icon="tabler:heart" class="w-full" v-if="!note?.favourited">
Like
</ButtonDropdown>
<ButtonDropdown @click="likeFn" icon="tabler:heart-filled" class="w-full" v-else>
Unlike
</ButtonDropdown>
</Menu.Item>
<Menu.Item value="">
<ButtonDropdown @click="reblogFn" icon="tabler:repeat" class="w-full" v-if="!note?.reblogged">
Reblog
</ButtonDropdown>
<ButtonDropdown @click="reblogFn" icon="tabler:repeat" class="w-full" v-else>
Unreblog
</ButtonDropdown>
</Menu.Item>
<Menu.Item value="">
<ButtonDropdown @click="note && useEvent('note:quote', note)" icon="tabler:quote" class="w-full">
Quote
</ButtonDropdown>
</Menu.Item>
</Menu.ItemGroup>
<hr class="border-white/10 rounded" v-if="identity" />
<Menu.ItemGroup v-if="identity">
<Menu.Item value="">
<ButtonDropdown @click="note && useEvent('note:report', note)" icon="tabler:flag" class="w-full"
:disabled="!permissions.includes(RolePermission.ManageOwnReports)">
Report
</ButtonDropdown>
</Menu.Item>
<Menu.Item value="" v-if="permissions.includes(RolePermission.ManageAccounts)">
<ButtonDropdown icon="tabler:shield-bolt" class="w-full">
Open moderation panel
</ButtonDropdown>
</Menu.Item>
</Menu.ItemGroup>
</template>
</AdaptiveDropdown>
</template>
<script lang="ts" setup>
import { Menu } from "@ark-ui/vue";
import { RolePermission, type Status } from "@versia/client/types";
import ButtonDropdown from "~/components/buttons/button-dropdown.vue";
import AdaptiveDropdown from "~/components/dropdowns/AdaptiveDropdown.vue";
import InteractionButton from "./interactions/button.vue";
defineProps<{
url: string;
remove: () => Promise<void>;
}>();
const note = defineModel<Status>("note", {
required: true,
});
const openBlank = (url: string) => window.open(url, "_blank");
const { copy } = useClipboard();
const isMyAccount = computed(
() => identity.value?.account.id === note.value?.account.id,
);
const isRemote = computed(() => note.value?.account.acct.includes("@"));
const permissions = usePermissions();
const likeFn = async () => {
if (!note.value) {
return;
}
if (note.value.favourited) {
const output = await client.value.unfavouriteStatus(note.value.id);
if (output.data) {
note.value = output.data;
}
} else {
const output = await client.value.favouriteStatus(note.value.id);
if (output.data) {
note.value = output.data;
}
}
};
const reblogFn = async () => {
if (!note.value) {
return;
}
if (note.value.reblogged) {
const output = await client.value.unreblogStatus(note.value.id);
if (output.data) {
note.value = output.data;
}
} else {
const output = await client.value.reblogStatus(note.value.id);
if (output.data.reblog) {
note.value = output.data.reblog;
}
}
};
</script>

View file

@ -1,98 +0,0 @@
<template>
<article
:class="['relative flex flex-col', borders && 'first:rounded-t last:rounded-b ring-1 ring-white/5', background && 'bg-dark-800 hover:bg-dark-700 duration-200']">
<note v-if="renderReplies && isReply && reply && !threadView" :thread-view="true" :element="reply"
:borders="false" :render-replies="false" :thread-view-top="false" />
<div class="relative">
<div v-if="threadView && outputtedNote"
class="h-[calc(100%-2rem)] w-[0.1rem] rounded bg-gray-600 absolute top-[4.5rem] left-12">
</div>
<div v-if="threadViewTop && reply && outputtedNote"
class="h-[1.5rem] w-[0.1rem] rounded bg-gray-600 absolute top-0 left-12">
</div>
<div :class="[padding && 'p-6', 'z-10']">
<!-- Overlay that blocks clicks for disabled notes -->
<div v-if="disabled" class="absolute z-10 inset-0 hover:cursor-not-allowed">
</div>
<div v-if="reblog" class="mb-4 flex flex-row gap-2 items-center text-primary2-400">
<Skeleton :enabled="!loaded" shape="rect" class="!h-6" :min-width="40" :max-width="100"
width-unit="%">
<iconify-icon width="1.5rem" height="1.5rem" icon="tabler:repeat" class="size-6"
aria-hidden="true" />
<Avatar v-if="reblog.avatar" :src="reblog.avatar" :alt="`${reblog.acct}'s avatar'`"
class="size-6 rounded shrink-0 ring-1 ring-white/10" />
<span><strong v-html="reblogDisplayName"></strong> reblogged</span>
</Skeleton>
</div>
<!-- <ReplyHeader v-if="isReply && !threadView" :account_id="outputtedNote?.in_reply_to_account_id ?? null" /> -->
<Header :note="outputtedNote" :small="small" />
<NoteContent :class="threadView && 'ml-16'" :note="outputtedNote" :loaded="loaded" :url="url"
:content="content" :is-quote="isQuote" :should-hide="shouldHide" />
<Skeleton class="!h-10 w-full mt-6" :enabled="!props.element || !loaded"
v-if="!small || !showInteractions">
<InteractionRow v-if="showInteractions && outputtedNote && !threadView" :note="outputtedNote"
:url="url" :remove="remove" />
</Skeleton>
</div>
</div>
</article>
</template>
<script lang="ts" setup>
import type { Status } from "@versia/client/types";
import Avatar from "~/components/avatars/avatar.vue";
import Skeleton from "~/components/skeleton/Skeleton.vue";
import Header from "./header.vue";
import InteractionRow from "./interactions/row.vue";
import NoteContent from "./note-content.vue";
const props = withDefaults(
defineProps<{
element?: MaybeRef<Status>;
small?: boolean;
disabled?: boolean;
showInteractions?: boolean;
threadView?: boolean;
threadViewTop?: boolean;
renderReplies?: boolean;
padding?: boolean;
borders?: boolean;
background?: boolean;
}>(),
{
showInteractions: true,
padding: true,
borders: true,
renderReplies: true,
background: true,
threadViewTop: true,
},
);
const noteRef = ref(props.element);
useListen("composer:send-edit", (note) => {
if (note.id === noteRef.value?.id) {
noteRef.value = note;
}
});
const {
loaded,
note: outputtedNote,
remove,
content,
shouldHide,
url,
isQuote,
reblog,
isReply,
reblogDisplayName,
} = useNoteData(noteRef, client, settings);
const inReplyToId = computed(
() => outputtedNote?.value?.in_reply_to_id ?? null,
);
const reply = useNote(client, inReplyToId);
</script>

View file

@ -1,22 +0,0 @@
<template>
<NuxtLink :href="`/@${account?.acct}`" class="mb-4 flex flex-row gap-2 items-center text-gray-300 opacity-70">
<Skeleton :enabled="!account" shape="rect" class="!h-6" :min-width="40" :max-width="100" width-unit="%">
<iconify-icon icon="tabler:arrow-back-up" width="1.5rem" height="1.5rem" aria-hidden="true" />
<span class="shrink-0">Replying to</span>
<Avatar v-if="account?.avatar" :src="account?.avatar" :alt="`${account?.acct}'s avatar'`"
class="size-5 shrink-0 rounded ring-1 ring-white/10" />
<strong class="line-clamp-1">{{ account?.display_name || account?.acct }}</strong>
</Skeleton>
</NuxtLink>
</template>
<script lang="ts" setup>
import Avatar from "~/components/avatars/avatar.vue";
import Skeleton from "~/components/skeleton/Skeleton.vue";
const props = defineProps<{
account_id: string | null;
}>();
const account = useAccount(client, props.account_id);
</script>

View file

@ -1,7 +1,5 @@
import type { Account, Emoji } from "@versia/client/types"; import type { Account, Emoji } from "@versia/client/types";
import { renderToString } from "vue/server-renderer";
import { SettingIds, type Settings } from "~/settings"; import { SettingIds, type Settings } from "~/settings";
import MentionComponent from "../components/social-elements/notes/mention.vue";
const emojisRegex = const emojisRegex =
/\p{RI}\p{RI}|\p{Emoji}(\p{EMod}|\uFE0F\u20E3?|[\u{E0020}-\u{E007E}]+\u{E007F})?(\u200D(\p{RI}\p{RI}|\p{Emoji}(\p{EMod}|\uFE0F\u20E3?|[\u{E0020}-\u{E007E}]+\u{E007F})?))*/gu; /\p{RI}\p{RI}|\p{Emoji}(\p{EMod}|\uFE0F\u20E3?|[\u{E0020}-\u{E007E}]+\u{E007F})?(\u200D(\p{RI}\p{RI}|\p{Emoji}(\p{EMod}|\uFE0F\u20E3?|[\u{E0020}-\u{E007E}]+\u{E007F})?))*/gu;
@ -70,25 +68,6 @@ export const useParsedContent = (
); );
} }
// Replace links containing mentions with interactive mentions
const links = contentHtml.querySelectorAll("a");
for (const link of links) {
const mention = toValue(mentions).find(
(m) => link.textContent === `@${m.acct}`,
);
if (!mention) {
continue;
}
const renderedMention = h(MentionComponent);
renderedMention.props = {
account: mention,
};
link.outerHTML = await renderToString(renderedMention);
}
result.value = contentHtml.innerHTML; result.value = contentHtml.innerHTML;
}, },
{ immediate: true }, { immediate: true },

View file

@ -4,14 +4,12 @@
<slot /> <slot />
</Sidebar> </Sidebar>
<ComposerDialog /> <ComposerDialog />
<AttachmentDialog />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import ComposerDialog from "~/components/composer/dialog.vue"; import ComposerDialog from "~/components/composer/dialog.vue";
import SquarePattern from "~/components/graphics/square-pattern.vue"; import SquarePattern from "~/components/graphics/square-pattern.vue";
import Sidebar from "~/components/sidebars/sidebar.vue"; import Sidebar from "~/components/sidebars/sidebar.vue";
import AttachmentDialog from "~/components/social-elements/notes/attachment-dialog.vue";
const { n } = useMagicKeys(); const { n } = useMagicKeys();
const activeElement = useActiveElement(); const activeElement = useActiveElement();

View file

@ -1,18 +1,18 @@
<template> <template>
<div v-if="loaded" :defer="true" class="mx-auto max-w-2xl w-full pb-72"> <div v-if="loaded" :defer="true" class="mx-auto max-w-2xl w-full pb-72">
<Note v-for="note of context?.ancestors" :render-replies="false" :thread-view="true" :borders="false" :element="note" /> <Note v-for="note of context?.ancestors" :note="note" />
<div ref="element" class="first:rounded-t last:rounded-b overflow-hidden"> <div ref="element" class="first:rounded-t last:rounded-b overflow-hidden">
<Note class="!rounded-none border-2 -m-[2px] border-primary-500" v-if="note" :render-replies="false":element="note" /> <Note class="!rounded-none border-2 -m-[2px] border-primary-500" v-if="note" :note="note" />
</div> </div>
<Note v-for="note of context?.descendants" :element="note" :render-replies="false" :thread-view="note.id !== context?.descendants.at(-1)?.id" :borders="false" :thread-view-top="true" /> <Note v-for="note of context?.descendants" :note="note" />
</div> </div>
<div v-else class="mx-auto max-w-2xl w-full overflow-y-auto"> <!-- <div v-else class="mx-auto max-w-2xl w-full overflow-y-auto">
<Note v-for="_ of 5" :skeleton="true" /> <Note v-for="_ of 5" :skeleton="true" />
</div> </div> -->
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import Note from "~/components/social-elements/notes/note.vue"; import Note from "~/components/notes/note.vue";
definePageMeta({ definePageMeta({
layout: "app", layout: "app",