mirror of
https://github.com/versia-pub/frontend.git
synced 2026-01-26 04:16:02 +01:00
refactor: ♻️ Simplify Note code with a provide/inject pattern
Some checks failed
Some checks failed
This commit is contained in:
parent
b23ed66401
commit
f5918cc7f9
|
|
@ -6,7 +6,7 @@
|
|||
:title="m.drab_tense_turtle_comfort()"
|
||||
:disabled="!authStore.isSignedIn"
|
||||
>
|
||||
{{ numberFormat(replyCount) }}
|
||||
{{ numberFormat(note.replies_count) }}
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
:icon="Heart"
|
||||
|
|
@ -15,7 +15,7 @@
|
|||
:disabled="!authStore.isSignedIn"
|
||||
:class="liked && '*:fill-red-600 *:text-red-600'"
|
||||
>
|
||||
{{ numberFormat(likeCount) }}
|
||||
{{ numberFormat(note.favourites_count) }}
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
:icon="Repeat"
|
||||
|
|
@ -24,7 +24,7 @@
|
|||
:disabled="!authStore.isSignedIn"
|
||||
:class="reblogged && '*:text-green-600'"
|
||||
>
|
||||
{{ numberFormat(reblogCount) }}
|
||||
{{ numberFormat(note.reblogs_count) }}
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
:icon="Quote"
|
||||
|
|
@ -51,22 +51,12 @@ import * as m from "~~/paraglide/messages.js";
|
|||
import { getLocale } from "~~/paraglide/runtime";
|
||||
import { confirmModalService } from "../modals/composable";
|
||||
import ActionButton from "./action-button.vue";
|
||||
import { key } from "./provider";
|
||||
import type { UnicodeEmoji } from "./reactions/picker/emoji";
|
||||
import Picker from "./reactions/picker/index.vue";
|
||||
|
||||
const { noteId } = defineProps<{
|
||||
replyCount: number;
|
||||
likeCount: number;
|
||||
reblogCount: number;
|
||||
apiNoteString: string;
|
||||
noteId: string;
|
||||
isRemote: boolean;
|
||||
url: string;
|
||||
remoteUrl?: string;
|
||||
authorId: string;
|
||||
liked: boolean;
|
||||
reblogged: boolean;
|
||||
}>();
|
||||
// biome-ignore lint/style/noNonNullAssertion: We want an error if not provided
|
||||
const { note } = inject(key)!;
|
||||
|
||||
const emit = defineEmits<{
|
||||
edit: [];
|
||||
|
|
@ -78,6 +68,9 @@ const emit = defineEmits<{
|
|||
const { play } = useAudio();
|
||||
const authStore = useAuthStore();
|
||||
|
||||
const liked = note.favourited ?? false;
|
||||
const reblogged = note.reblogged ?? false;
|
||||
|
||||
const like = async () => {
|
||||
if (preferences.confirm_actions.value.includes("like")) {
|
||||
const confirmation = await confirmModalService.confirm({
|
||||
|
|
@ -94,7 +87,7 @@ const like = async () => {
|
|||
|
||||
play("like");
|
||||
const id = toast.loading(m.slimy_candid_tiger_read());
|
||||
const { data } = await authStore.client.favouriteStatus(noteId);
|
||||
const { data } = await authStore.client.favouriteStatus(note.id);
|
||||
toast.dismiss(id);
|
||||
toast.success(m.mealy_slow_buzzard_commend());
|
||||
useEvent("note:edit", data);
|
||||
|
|
@ -115,7 +108,7 @@ const unlike = async () => {
|
|||
}
|
||||
|
||||
const id = toast.loading(m.busy_active_leopard_strive());
|
||||
const { data } = await authStore.client.unfavouriteStatus(noteId);
|
||||
const { data } = await authStore.client.unfavouriteStatus(note.id);
|
||||
toast.dismiss(id);
|
||||
toast.success(m.fresh_direct_bear_affirm());
|
||||
useEvent("note:edit", data);
|
||||
|
|
@ -136,7 +129,7 @@ const reblog = async () => {
|
|||
}
|
||||
|
||||
const id = toast.loading(m.late_sunny_cobra_scold());
|
||||
const { data } = await authStore.client.reblogStatus(noteId);
|
||||
const { data } = await authStore.client.reblogStatus(note.id);
|
||||
toast.dismiss(id);
|
||||
toast.success(m.weird_moving_hawk_lift());
|
||||
useEvent(
|
||||
|
|
@ -160,7 +153,7 @@ const unreblog = async () => {
|
|||
}
|
||||
|
||||
const id = toast.loading(m.white_sharp_gorilla_embrace());
|
||||
const { data } = await authStore.client.unreblogStatus(noteId);
|
||||
const { data } = await authStore.client.unreblogStatus(note.id);
|
||||
toast.dismiss(id);
|
||||
toast.success(m.royal_polite_moose_catch());
|
||||
useEvent("note:edit", data);
|
||||
|
|
@ -172,7 +165,7 @@ const react = async (emoji: z.infer<typeof CustomEmoji> | UnicodeEmoji) => {
|
|||
? (emoji as UnicodeEmoji).unicode
|
||||
: `:${(emoji as z.infer<typeof CustomEmoji>).shortcode}:`;
|
||||
|
||||
const { data } = await authStore.client.createEmojiReaction(noteId, text);
|
||||
const { data } = await authStore.client.createEmojiReaction(note.id, text);
|
||||
|
||||
toast.dismiss(id);
|
||||
toast.success(m.main_least_turtle_fall());
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div class="flex flex-col gap-1">
|
||||
<p class="text-sm leading-6 wrap-anywhere">
|
||||
{{ contentWarning || m.sour_seemly_bird_hike() }}
|
||||
{{ note.spoiler_text || m.sour_seemly_bird_hike() }}
|
||||
</p>
|
||||
<Button
|
||||
@click="hidden = !hidden"
|
||||
|
|
@ -11,9 +11,7 @@
|
|||
>
|
||||
{{ hidden ? m.bald_direct_turtle_win() :
|
||||
m.known_flaky_cockroach_dash() }}
|
||||
{{ characterCount > 0 ? ` (${characterCount} characters` : "" }}
|
||||
{{ attachmentCount > 0 ? `${characterCount > 0 ? " · " : " ("}${attachmentCount} file(s)` : "" }}
|
||||
{{ (characterCount > 0 || attachmentCount > 0) ? ")" : "" }}
|
||||
{{ constructText() }}
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -21,14 +19,33 @@
|
|||
<script lang="ts" setup>
|
||||
import * as m from "~~/paraglide/messages.js";
|
||||
import { Button } from "../ui/button";
|
||||
import { key } from "./provider";
|
||||
|
||||
const { contentWarning, characterCount, attachmentCount } = defineProps<{
|
||||
contentWarning?: string;
|
||||
characterCount: number;
|
||||
attachmentCount: number;
|
||||
}>();
|
||||
// biome-ignore lint/style/noNonNullAssertion: We want an error if not provided
|
||||
const { note } = inject(key)!;
|
||||
|
||||
const attachmentCount = note.media_attachments.length;
|
||||
const characterCount = note.text?.length || 0;
|
||||
|
||||
const hidden = defineModel<boolean>({
|
||||
default: true,
|
||||
});
|
||||
|
||||
const constructText = () => {
|
||||
const parts: string[] = [];
|
||||
|
||||
if (characterCount > 0) {
|
||||
parts.push(
|
||||
`${characterCount} character${characterCount === 1 ? "" : "s"}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (attachmentCount > 0) {
|
||||
parts.push(
|
||||
`${attachmentCount} file${attachmentCount === 1 ? "" : "s"}`,
|
||||
);
|
||||
}
|
||||
|
||||
return parts.length > 0 ? ` (${parts.join(" · ")})` : "";
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,51 +1,40 @@
|
|||
<template>
|
||||
<ContentWarning
|
||||
v-if="(sensitive || contentWarning) && preferences.show_content_warning"
|
||||
:content-warning="contentWarning"
|
||||
:character-count="characterCount ?? 0"
|
||||
:attachment-count="attachments.length"
|
||||
v-if="(note.sensitive || note.spoiler_text) && preferences.show_content_warning"
|
||||
v-model="hidden"
|
||||
/>
|
||||
|
||||
<OverflowGuard
|
||||
v-if="content"
|
||||
v-if="note.content"
|
||||
:character-count="characterCount"
|
||||
:class="(hidden && preferences.show_content_warning) && 'hidden'"
|
||||
>
|
||||
<Prose v-html="content" v-render-emojis="emojis"></Prose>
|
||||
<Prose v-html="note.content" v-render-emojis="note.emojis"></Prose>
|
||||
</OverflowGuard>
|
||||
|
||||
<Attachments
|
||||
v-if="attachments.length > 0"
|
||||
:attachments="attachments"
|
||||
v-if="note.media_attachments.length > 0"
|
||||
:attachments="note.media_attachments"
|
||||
:class="(hidden && preferences.show_content_warning) && 'hidden'"
|
||||
/>
|
||||
|
||||
<div v-if="quote" class="mt-4 rounded border overflow-hidden">
|
||||
<Note :note="quote" :hide-actions="true" :small-layout="true" />
|
||||
<div v-if="note.quote" class="mt-4 rounded border overflow-hidden">
|
||||
<Note :note="note.quote" :hide-actions="true" :small-layout="true" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Attachment, CustomEmoji, Status } from "@versia/client/schemas";
|
||||
import type { z } from "zod";
|
||||
import Attachments from "./attachments.vue";
|
||||
import ContentWarning from "./content-warning.vue";
|
||||
import Note from "./note.vue";
|
||||
import OverflowGuard from "./overflow-guard.vue";
|
||||
import Prose from "./prose.vue";
|
||||
import { key } from "./provider";
|
||||
|
||||
const { content, plainContent, sensitive, contentWarning } = defineProps<{
|
||||
plainContent?: string;
|
||||
content: string;
|
||||
quote?: NonNullable<z.infer<typeof Status.shape.quote>>;
|
||||
emojis: z.infer<typeof CustomEmoji>[];
|
||||
attachments: z.infer<typeof Attachment>[];
|
||||
sensitive: boolean;
|
||||
contentWarning?: string;
|
||||
}>();
|
||||
// biome-ignore lint/style/noNonNullAssertion: We want an error if not provided
|
||||
const { note } = inject(key)!;
|
||||
|
||||
const hidden = ref(sensitive || !!contentWarning);
|
||||
const hidden = ref(note.sensitive || !!note.spoiler_text);
|
||||
|
||||
const characterCount = plainContent?.length;
|
||||
const characterCount = note.text?.length;
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -2,14 +2,17 @@
|
|||
<div class="flex items-start justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<NuxtLink :href="urlAsPath">
|
||||
<Avatar :src="author.avatar" :name="author.display_name" />
|
||||
<Avatar
|
||||
:src="note.account.avatar"
|
||||
:name="note.account.display_name"
|
||||
/>
|
||||
</NuxtLink>
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<div class="flex items-center gap-1">
|
||||
<span
|
||||
class="text-sm font-semibold"
|
||||
v-render-emojis="author.emojis"
|
||||
>{{ author.display_name }}</span
|
||||
v-render-emojis="note.account.emojis"
|
||||
>{{ note.account.display_name }}</span
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
|
|
@ -23,16 +26,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Menu
|
||||
:api-note-string="apiNoteString"
|
||||
:url="noteUrl"
|
||||
:remote-url="remoteUrl"
|
||||
:is-remote="isRemote"
|
||||
:author-id="author.id"
|
||||
@edit="emit('edit')"
|
||||
:note-id="noteId"
|
||||
@delete="emit('delete')"
|
||||
>
|
||||
<Menu @edit="emit('edit')" @delete="emit('delete')">
|
||||
<Button variant="ghost" size="icon">
|
||||
<Ellipsis />
|
||||
</Button>
|
||||
|
|
@ -41,36 +35,25 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Account, Status } from "@versia/client/schemas";
|
||||
import type {
|
||||
UseTimeAgoMessages,
|
||||
UseTimeAgoUnitNamesDefault,
|
||||
} from "@vueuse/core";
|
||||
import { AtSign, Ellipsis, Globe, Lock, LockOpen } from "lucide-vue-next";
|
||||
import type { z } from "zod";
|
||||
import { getLocale } from "~~/paraglide/runtime";
|
||||
import { Ellipsis } from "lucide-vue-next";
|
||||
import Avatar from "../profiles/avatar.vue";
|
||||
import { Button } from "../ui/button";
|
||||
import Menu from "./menu.vue";
|
||||
import { key } from "./provider";
|
||||
|
||||
const { createdAt, noteUrl, author, authorUrl } = defineProps<{
|
||||
cornerAvatar?: string;
|
||||
visibility: z.infer<typeof Status.shape.visibility>;
|
||||
noteUrl: string;
|
||||
createdAt: Date;
|
||||
author: z.infer<typeof Account>;
|
||||
authorUrl: string;
|
||||
remoteUrl?: string;
|
||||
apiNoteString: string;
|
||||
isRemote: boolean;
|
||||
noteId: string;
|
||||
}>();
|
||||
// biome-ignore lint/style/noNonNullAssertion: We want an error if not provided
|
||||
const { note } = inject(key)!;
|
||||
|
||||
const [username, instance] = author.acct.split("@");
|
||||
const [username, instance] = note.account.acct.split("@");
|
||||
const digitRegex = /\d/;
|
||||
const urlAsPath = new URL(authorUrl).pathname;
|
||||
const noteUrlAsPath = new URL(noteUrl).pathname;
|
||||
const timeAgo = useTimeAgo(createdAt, {
|
||||
const accountUrl = wrapUrl(`/@${note.account.acct}`);
|
||||
const urlAsPath = new URL(accountUrl).pathname;
|
||||
|
||||
const timeAgo = useTimeAgo(note.created_at, {
|
||||
messages: {
|
||||
justNow: "now",
|
||||
past: (n) => (n.match(digitRegex) ? `${n}` : n),
|
||||
|
|
@ -85,33 +68,9 @@ const timeAgo = useTimeAgo(createdAt, {
|
|||
invalid: "",
|
||||
} as UseTimeAgoMessages<UseTimeAgoUnitNamesDefault>,
|
||||
});
|
||||
const fullTime = new Intl.DateTimeFormat(getLocale(), {
|
||||
dateStyle: "medium",
|
||||
timeStyle: "short",
|
||||
}).format(createdAt);
|
||||
const popupOpen = ref(false);
|
||||
|
||||
const emit = defineEmits<{
|
||||
edit: [];
|
||||
delete: [];
|
||||
}>();
|
||||
|
||||
const visibilities = {
|
||||
public: {
|
||||
icon: Globe,
|
||||
text: "This note is public: it can be seen by anyone.",
|
||||
},
|
||||
unlisted: {
|
||||
icon: LockOpen,
|
||||
text: "This note is unlisted: it can be seen by anyone with the link.",
|
||||
},
|
||||
private: {
|
||||
icon: Lock,
|
||||
text: "This note is private: it can only be seen by followers.",
|
||||
},
|
||||
direct: {
|
||||
icon: AtSign,
|
||||
text: "This note is direct: it can only be seen by mentioned users.",
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -21,15 +21,10 @@ import {
|
|||
} from "@/components/ui/dropdown-menu";
|
||||
import { confirmModalService } from "~/components/modals/composable.ts";
|
||||
import * as m from "~~/paraglide/messages.js";
|
||||
import { key } from "./provider";
|
||||
|
||||
const { authorId, noteId } = defineProps<{
|
||||
apiNoteString: string;
|
||||
isRemote: boolean;
|
||||
url: string;
|
||||
remoteUrl?: string;
|
||||
authorId: string;
|
||||
noteId: string;
|
||||
}>();
|
||||
// biome-ignore lint/style/noNonNullAssertion: We want an error if not provided
|
||||
const { note, isRemote } = inject(key)!;
|
||||
|
||||
const emit = defineEmits<{
|
||||
edit: [];
|
||||
|
|
@ -38,7 +33,9 @@ const emit = defineEmits<{
|
|||
|
||||
const { copy } = useClipboard();
|
||||
const authStore = useAuthStore();
|
||||
const authorIsMe = authStore.isSignedIn && authorId === authStore.account?.id;
|
||||
const url = wrapUrl(`/@${note.account.acct}/${note.id}`);
|
||||
const authorIsMe =
|
||||
authStore.isSignedIn && note.account.id === authStore.account?.id;
|
||||
|
||||
const copyText = (text: string) => {
|
||||
copy(text);
|
||||
|
|
@ -68,7 +65,7 @@ const _delete = async () => {
|
|||
}
|
||||
|
||||
const id = toast.loading(m.new_funny_fox_boil());
|
||||
await authStore.client.deleteStatus(noteId);
|
||||
await authStore.client.deleteStatus(note.id);
|
||||
toast.dismiss(id);
|
||||
|
||||
toast.success(m.green_tasty_bumblebee_beam());
|
||||
|
|
@ -91,11 +88,14 @@ const _delete = async () => {
|
|||
<Pencil />
|
||||
{{ m.front_lime_grizzly_persist() }}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem as="button" @click="copyText(apiNoteString)">
|
||||
<DropdownMenuItem
|
||||
as="button"
|
||||
@click="copyText(JSON.stringify(note, null, 4))"
|
||||
>
|
||||
<Code />
|
||||
{{ m.yummy_moving_scallop_sail() }}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem as="button" @click="copyText(noteId)">
|
||||
<DropdownMenuItem as="button" @click="copyText(note.id)">
|
||||
<Hash />
|
||||
{{ m.sunny_zany_jellyfish_pop() }}
|
||||
</DropdownMenuItem>
|
||||
|
|
@ -108,8 +108,8 @@ const _delete = async () => {
|
|||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
as="button"
|
||||
v-if="isRemote && remoteUrl"
|
||||
@click="copyText(remoteUrl)"
|
||||
v-if="isRemote && note.url"
|
||||
@click="copyText(note.url)"
|
||||
>
|
||||
<Link />
|
||||
{{ m.solid_witty_zebra_walk() }}
|
||||
|
|
@ -119,7 +119,7 @@ const _delete = async () => {
|
|||
v-if="isRemote"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
:href="remoteUrl"
|
||||
:href="note.url"
|
||||
>
|
||||
<ExternalLink />
|
||||
{{ m.active_trite_lark_inspire() }}
|
||||
|
|
@ -142,7 +142,10 @@ const _delete = async () => {
|
|||
<Flag />
|
||||
{{ m.great_few_jaguar_rise() }}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem as="button" @click="blockUser(authorId)">
|
||||
<DropdownMenuItem
|
||||
as="button"
|
||||
@click="blockUser(note.account.id)"
|
||||
>
|
||||
<Ban />
|
||||
{{ m.misty_soft_sparrow_vent() }}
|
||||
</DropdownMenuItem>
|
||||
|
|
|
|||
|
|
@ -8,63 +8,25 @@
|
|||
class="absolute left-0 top-0 bottom-0 w-2 bg-border rounded-tl-md"
|
||||
/>
|
||||
<CardHeader as="header" class="space-y-2">
|
||||
<ReblogHeader
|
||||
v-if="note.reblog"
|
||||
:avatar="note.account.avatar"
|
||||
:display-name="note.account.display_name"
|
||||
:url="reblogAccountUrl"
|
||||
:emojis="note.account.emojis"
|
||||
/>
|
||||
<ReblogHeader v-if="note.reblog" />
|
||||
<Header
|
||||
:author="noteToUse.account"
|
||||
:author-url="accountUrl"
|
||||
:corner-avatar="note.reblog ? note.account.avatar : undefined"
|
||||
:note-url="url"
|
||||
:is-remote="isRemote"
|
||||
:remote-url="noteToUse.url ?? undefined"
|
||||
:api-note-string="JSON.stringify(noteToUse, null, 4)"
|
||||
:visibility="noteToUse.visibility"
|
||||
:created-at="new Date(noteToUse.created_at)"
|
||||
@edit="useEvent('composer:edit', noteToUse)"
|
||||
@delete="useEvent('note:delete', noteToUse)"
|
||||
:note-id="noteToUse.id"
|
||||
class="z-1"
|
||||
/>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-2">
|
||||
<Content
|
||||
:content="noteToUse.content"
|
||||
:quote="note.quote ?? undefined"
|
||||
:attachments="noteToUse.media_attachments"
|
||||
:plain-content="noteToUse.text ?? undefined"
|
||||
:emojis="noteToUse.emojis"
|
||||
:sensitive="noteToUse.sensitive"
|
||||
:content-warning="noteToUse.spoiler_text"
|
||||
/>
|
||||
<Content />
|
||||
<Reactions
|
||||
v-if="noteToUse.reactions && noteToUse.reactions.length > 0"
|
||||
:reactions="noteToUse.reactions"
|
||||
:emojis="noteToUse.emojis"
|
||||
:status-id="noteToUse.id"
|
||||
/>
|
||||
</CardContent>
|
||||
<CardFooter v-if="!hideActions">
|
||||
<Actions
|
||||
:reply-count="noteToUse.replies_count"
|
||||
:like-count="noteToUse.favourites_count"
|
||||
:url="url"
|
||||
:api-note-string="JSON.stringify(noteToUse, null, 4)"
|
||||
:reblog-count="noteToUse.reblogs_count"
|
||||
:remote-url="noteToUse.url ?? undefined"
|
||||
:is-remote="isRemote"
|
||||
:author-id="noteToUse.account.id"
|
||||
@edit="useEvent('composer:edit', noteToUse)"
|
||||
@reply="useEvent('composer:reply', noteToUse)"
|
||||
@quote="useEvent('composer:quote', noteToUse)"
|
||||
@delete="useEvent('note:delete', noteToUse)"
|
||||
:note-id="noteToUse.id"
|
||||
:liked="noteToUse.favourited ?? false"
|
||||
:reblogged="noteToUse.reblogged ?? false"
|
||||
/>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
|
|
@ -77,6 +39,7 @@ import { Card, CardContent, CardFooter, CardHeader } from "../ui/card";
|
|||
import Actions from "./actions.vue";
|
||||
import Content from "./content.vue";
|
||||
import Header from "./header.vue";
|
||||
import { key } from "./provider";
|
||||
import Reactions from "./reactions/index.vue";
|
||||
import ReblogHeader from "./reblog-header.vue";
|
||||
|
||||
|
|
@ -99,8 +62,9 @@ const noteToUse = computed(() =>
|
|||
: (note as z.infer<typeof Status>),
|
||||
);
|
||||
|
||||
const url = wrapUrl(`/@${noteToUse.value.account.acct}/${noteToUse.value.id}`);
|
||||
const accountUrl = wrapUrl(`/@${noteToUse.value.account.acct}`);
|
||||
const reblogAccountUrl = wrapUrl(`/@${note.account.acct}`);
|
||||
const isRemote = noteToUse.value.account.acct.includes("@");
|
||||
provide(key, {
|
||||
note: noteToUse.value,
|
||||
rebloggerNote: note.reblog ? (note as z.infer<typeof Status>) : undefined,
|
||||
isRemote: noteToUse.value.account.acct.includes("@"),
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div
|
||||
:class="[
|
||||
'prose prose-sm block relative dark:prose-invert duration-200 !max-w-full break-words prose-a:no-underline hover:prose-a:underline',
|
||||
'prose prose-sm block relative dark:prose-invert duration-200 max-w-full! wrap-break-word prose-a:no-underline hover:prose-a:underline',
|
||||
$style.content,
|
||||
]"
|
||||
>
|
||||
|
|
|
|||
11
app/components/notes/provider.ts
Normal file
11
app/components/notes/provider.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import type { Status } from "@versia/client/schemas";
|
||||
import type { InjectionKey } from "vue";
|
||||
import type { z } from "zod";
|
||||
|
||||
type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
|
||||
|
||||
export const key = Symbol() as InjectionKey<{
|
||||
note: PartialBy<z.infer<typeof Status>, "reblog" | "quote">;
|
||||
isRemote: boolean;
|
||||
rebloggerNote?: z.infer<typeof Status>;
|
||||
}>;
|
||||
|
|
@ -1,23 +1,18 @@
|
|||
<template>
|
||||
<div class="flex flex-row gap-1 flex-wrap">
|
||||
<Reaction
|
||||
v-for="reaction in reactions"
|
||||
v-for="reaction in note.reactions"
|
||||
:key="reaction.name"
|
||||
:reaction="reaction"
|
||||
:emoji="emojis.find(e => `:${e.shortcode}:` === reaction.name)"
|
||||
:status-id="statusId"
|
||||
:emoji="note.emojis.find(e => `:${e.shortcode}:` === reaction.name)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { CustomEmoji, NoteReaction } from "@versia/client/schemas";
|
||||
import type { z } from "zod";
|
||||
import { key } from "../provider";
|
||||
import Reaction from "./reaction.vue";
|
||||
|
||||
const { statusId, reactions, emojis } = defineProps<{
|
||||
statusId: string;
|
||||
reactions: z.infer<typeof NoteReaction>[];
|
||||
emojis: z.infer<typeof CustomEmoji>[];
|
||||
}>();
|
||||
// biome-ignore lint/style/noNonNullAssertion: We want an error if not provided
|
||||
const { note } = inject(key)!;
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -61,14 +61,16 @@ import {
|
|||
} from "~/components/ui/hover-card";
|
||||
import * as m from "~~/paraglide/messages.js";
|
||||
import { getLocale } from "~~/paraglide/runtime.js";
|
||||
import { key } from "../provider";
|
||||
|
||||
const { reaction, emoji, statusId } = defineProps<{
|
||||
statusId: string;
|
||||
const { reaction, emoji } = defineProps<{
|
||||
reaction: z.infer<typeof NoteReaction>;
|
||||
emoji?: z.infer<typeof CustomEmoji>;
|
||||
}>();
|
||||
|
||||
const authStore = useAuthStore();
|
||||
// biome-ignore lint/style/noNonNullAssertion: We want an error if not provided
|
||||
const { note } = inject(key)!;
|
||||
|
||||
const formatNumber = (number: number) =>
|
||||
new Intl.NumberFormat(getLocale(), {
|
||||
|
|
@ -80,7 +82,7 @@ const formatNumber = (number: number) =>
|
|||
const accounts = ref<z.infer<typeof Account>[] | null>(null);
|
||||
|
||||
const refreshReactions = async () => {
|
||||
const { data } = await authStore.client.getStatusReactions(statusId);
|
||||
const { data } = await authStore.client.getStatusReactions(note.id);
|
||||
const accountIds =
|
||||
data.find((r) => r.name === reaction.name)?.account_ids.slice(0, 10) ??
|
||||
[];
|
||||
|
|
@ -95,7 +97,7 @@ const react = async () => {
|
|||
const id = toast.loading(m.gray_stale_antelope_roam());
|
||||
|
||||
const { data } = await authStore.client.createEmojiReaction(
|
||||
statusId,
|
||||
note.id,
|
||||
reaction.name,
|
||||
);
|
||||
|
||||
|
|
@ -108,7 +110,7 @@ const unreact = async () => {
|
|||
const id = toast.loading(m.many_weary_bat_intend());
|
||||
|
||||
const { data } = await authStore.client.deleteEmojiReaction(
|
||||
statusId,
|
||||
note.id,
|
||||
reaction.name,
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -4,9 +4,15 @@
|
|||
class="flex-row px-2 py-1 items-center gap-2 hover:bg-muted duration-100 text-sm"
|
||||
>
|
||||
<Repeat class="size-4 text-primary" />
|
||||
<Avatar class="size-6 border" :src="avatar" :name="displayName" />
|
||||
<span class="font-semibold" v-render-emojis="emojis"
|
||||
>{{ displayName }}</span
|
||||
<Avatar
|
||||
class="size-6 border"
|
||||
:src="rebloggerNote.account.avatar"
|
||||
:name="rebloggerNote.account.display_name"
|
||||
/>
|
||||
<span
|
||||
class="font-semibold"
|
||||
v-render-emojis="rebloggerNote.account.emojis"
|
||||
>{{ rebloggerNote.account.display_name }}</span
|
||||
>
|
||||
{{ m.large_vivid_horse_catch() }}
|
||||
</Card>
|
||||
|
|
@ -14,19 +20,21 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { CustomEmoji } from "@versia/client/schemas";
|
||||
import { Repeat } from "lucide-vue-next";
|
||||
import type { z } from "zod";
|
||||
import * as m from "~~/paraglide/messages.js";
|
||||
import Avatar from "../profiles/avatar.vue";
|
||||
import { Card } from "../ui/card";
|
||||
import { key } from "./provider";
|
||||
|
||||
const { url } = defineProps<{
|
||||
avatar: string;
|
||||
displayName: string;
|
||||
emojis: z.infer<typeof CustomEmoji>[];
|
||||
url: string;
|
||||
}>();
|
||||
// biome-ignore lint/style/noNonNullAssertion: We want an error if not provided
|
||||
const { rebloggerNote } = inject(key)!;
|
||||
|
||||
const urlAsPath = new URL(url).pathname;
|
||||
if (!rebloggerNote) {
|
||||
throw new Error(
|
||||
"ReblogHeader must be used with a rebloggerNote in context",
|
||||
);
|
||||
}
|
||||
|
||||
const reblogAccountUrl = wrapUrl(`/@${rebloggerNote.account.acct}`);
|
||||
const urlAsPath = new URL(reblogAccountUrl).pathname;
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ const props = defineProps<{
|
|||
|
||||
const emit = defineEmits<(e: "update") => void>();
|
||||
|
||||
const loadMoreTrigger = ref<HTMLElement | null>(null);
|
||||
const loadMoreTrigger = useTemplateRef("loadMoreTrigger");
|
||||
|
||||
useIntersectionObserver(loadMoreTrigger, ([observer]) => {
|
||||
if (observer?.isIntersecting && !props.isLoading && !props.hasReachedEnd) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue