mirror of
https://github.com/versia-pub/frontend.git
synced 2025-12-06 08:28:20 +01:00
refactor: 🔥 Remove old Note code
This commit is contained in:
parent
c6f8ba081d
commit
0b6acd98dd
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
· <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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -1,7 +1,5 @@
|
|||
import type { Account, Emoji } from "@versia/client/types";
|
||||
import { renderToString } from "vue/server-renderer";
|
||||
import { SettingIds, type Settings } from "~/settings";
|
||||
import MentionComponent from "../components/social-elements/notes/mention.vue";
|
||||
|
||||
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;
|
||||
|
|
@ -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;
|
||||
},
|
||||
{ immediate: true },
|
||||
|
|
|
|||
|
|
@ -4,14 +4,12 @@
|
|||
<slot />
|
||||
</Sidebar>
|
||||
<ComposerDialog />
|
||||
<AttachmentDialog />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ComposerDialog from "~/components/composer/dialog.vue";
|
||||
import SquarePattern from "~/components/graphics/square-pattern.vue";
|
||||
import Sidebar from "~/components/sidebars/sidebar.vue";
|
||||
import AttachmentDialog from "~/components/social-elements/notes/attachment-dialog.vue";
|
||||
|
||||
const { n } = useMagicKeys();
|
||||
const activeElement = useActiveElement();
|
||||
|
|
|
|||
|
|
@ -1,18 +1,18 @@
|
|||
<template>
|
||||
<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">
|
||||
<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>
|
||||
<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 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" />
|
||||
</div>
|
||||
</div> -->
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Note from "~/components/social-elements/notes/note.vue";
|
||||
import Note from "~/components/notes/note.vue";
|
||||
|
||||
definePageMeta({
|
||||
layout: "app",
|
||||
|
|
|
|||
Loading…
Reference in a new issue