refactor: ♻️ Simplify Note code with a provide/inject pattern
Some checks failed
CodeQL / Analyze (javascript) (push) Failing after 1s
Deploy to GitHub Pages / build (push) Failing after 0s
Deploy to GitHub Pages / deploy (push) Has been skipped
Docker / build (push) Failing after 0s
Mirror to Codeberg / Mirror (push) Failing after 0s

This commit is contained in:
Jesse Wierzbinski 2026-01-09 23:10:45 +01:00
parent b23ed66401
commit f5918cc7f9
No known key found for this signature in database
12 changed files with 140 additions and 199 deletions

View file

@ -6,7 +6,7 @@
:title="m.drab_tense_turtle_comfort()" :title="m.drab_tense_turtle_comfort()"
:disabled="!authStore.isSignedIn" :disabled="!authStore.isSignedIn"
> >
{{ numberFormat(replyCount) }} {{ numberFormat(note.replies_count) }}
</ActionButton> </ActionButton>
<ActionButton <ActionButton
:icon="Heart" :icon="Heart"
@ -15,7 +15,7 @@
:disabled="!authStore.isSignedIn" :disabled="!authStore.isSignedIn"
:class="liked && '*:fill-red-600 *:text-red-600'" :class="liked && '*:fill-red-600 *:text-red-600'"
> >
{{ numberFormat(likeCount) }} {{ numberFormat(note.favourites_count) }}
</ActionButton> </ActionButton>
<ActionButton <ActionButton
:icon="Repeat" :icon="Repeat"
@ -24,7 +24,7 @@
:disabled="!authStore.isSignedIn" :disabled="!authStore.isSignedIn"
:class="reblogged && '*:text-green-600'" :class="reblogged && '*:text-green-600'"
> >
{{ numberFormat(reblogCount) }} {{ numberFormat(note.reblogs_count) }}
</ActionButton> </ActionButton>
<ActionButton <ActionButton
:icon="Quote" :icon="Quote"
@ -51,22 +51,12 @@ import * as m from "~~/paraglide/messages.js";
import { getLocale } from "~~/paraglide/runtime"; import { getLocale } from "~~/paraglide/runtime";
import { confirmModalService } from "../modals/composable"; import { confirmModalService } from "../modals/composable";
import ActionButton from "./action-button.vue"; import ActionButton from "./action-button.vue";
import { key } from "./provider";
import type { UnicodeEmoji } from "./reactions/picker/emoji"; import type { UnicodeEmoji } from "./reactions/picker/emoji";
import Picker from "./reactions/picker/index.vue"; import Picker from "./reactions/picker/index.vue";
const { noteId } = defineProps<{ // biome-ignore lint/style/noNonNullAssertion: We want an error if not provided
replyCount: number; const { note } = inject(key)!;
likeCount: number;
reblogCount: number;
apiNoteString: string;
noteId: string;
isRemote: boolean;
url: string;
remoteUrl?: string;
authorId: string;
liked: boolean;
reblogged: boolean;
}>();
const emit = defineEmits<{ const emit = defineEmits<{
edit: []; edit: [];
@ -78,6 +68,9 @@ const emit = defineEmits<{
const { play } = useAudio(); const { play } = useAudio();
const authStore = useAuthStore(); const authStore = useAuthStore();
const liked = note.favourited ?? false;
const reblogged = note.reblogged ?? false;
const like = async () => { const like = async () => {
if (preferences.confirm_actions.value.includes("like")) { if (preferences.confirm_actions.value.includes("like")) {
const confirmation = await confirmModalService.confirm({ const confirmation = await confirmModalService.confirm({
@ -94,7 +87,7 @@ const like = async () => {
play("like"); play("like");
const id = toast.loading(m.slimy_candid_tiger_read()); 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.dismiss(id);
toast.success(m.mealy_slow_buzzard_commend()); toast.success(m.mealy_slow_buzzard_commend());
useEvent("note:edit", data); useEvent("note:edit", data);
@ -115,7 +108,7 @@ const unlike = async () => {
} }
const id = toast.loading(m.busy_active_leopard_strive()); 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.dismiss(id);
toast.success(m.fresh_direct_bear_affirm()); toast.success(m.fresh_direct_bear_affirm());
useEvent("note:edit", data); useEvent("note:edit", data);
@ -136,7 +129,7 @@ const reblog = async () => {
} }
const id = toast.loading(m.late_sunny_cobra_scold()); 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.dismiss(id);
toast.success(m.weird_moving_hawk_lift()); toast.success(m.weird_moving_hawk_lift());
useEvent( useEvent(
@ -160,7 +153,7 @@ const unreblog = async () => {
} }
const id = toast.loading(m.white_sharp_gorilla_embrace()); 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.dismiss(id);
toast.success(m.royal_polite_moose_catch()); toast.success(m.royal_polite_moose_catch());
useEvent("note:edit", data); useEvent("note:edit", data);
@ -172,7 +165,7 @@ const react = async (emoji: z.infer<typeof CustomEmoji> | UnicodeEmoji) => {
? (emoji as UnicodeEmoji).unicode ? (emoji as UnicodeEmoji).unicode
: `:${(emoji as z.infer<typeof CustomEmoji>).shortcode}:`; : `:${(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.dismiss(id);
toast.success(m.main_least_turtle_fall()); toast.success(m.main_least_turtle_fall());

View file

@ -1,7 +1,7 @@
<template> <template>
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<p class="text-sm leading-6 wrap-anywhere"> <p class="text-sm leading-6 wrap-anywhere">
{{ contentWarning || m.sour_seemly_bird_hike() }} {{ note.spoiler_text || m.sour_seemly_bird_hike() }}
</p> </p>
<Button <Button
@click="hidden = !hidden" @click="hidden = !hidden"
@ -11,9 +11,7 @@
> >
{{ hidden ? m.bald_direct_turtle_win() : {{ hidden ? m.bald_direct_turtle_win() :
m.known_flaky_cockroach_dash() }} m.known_flaky_cockroach_dash() }}
{{ characterCount > 0 ? ` (${characterCount} characters` : "" }} {{ constructText() }}
{{ attachmentCount > 0 ? `${characterCount > 0 ? " · " : " ("}${attachmentCount} file(s)` : "" }}
{{ (characterCount > 0 || attachmentCount > 0) ? ")" : "" }}
</Button> </Button>
</div> </div>
</template> </template>
@ -21,14 +19,33 @@
<script lang="ts" setup> <script lang="ts" setup>
import * as m from "~~/paraglide/messages.js"; import * as m from "~~/paraglide/messages.js";
import { Button } from "../ui/button"; import { Button } from "../ui/button";
import { key } from "./provider";
const { contentWarning, characterCount, attachmentCount } = defineProps<{ // biome-ignore lint/style/noNonNullAssertion: We want an error if not provided
contentWarning?: string; const { note } = inject(key)!;
characterCount: number;
attachmentCount: number; const attachmentCount = note.media_attachments.length;
}>(); const characterCount = note.text?.length || 0;
const hidden = defineModel<boolean>({ const hidden = defineModel<boolean>({
default: true, 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> </script>

View file

@ -1,51 +1,40 @@
<template> <template>
<ContentWarning <ContentWarning
v-if="(sensitive || contentWarning) && preferences.show_content_warning" v-if="(note.sensitive || note.spoiler_text) && preferences.show_content_warning"
:content-warning="contentWarning"
:character-count="characterCount ?? 0"
:attachment-count="attachments.length"
v-model="hidden" v-model="hidden"
/> />
<OverflowGuard <OverflowGuard
v-if="content" v-if="note.content"
:character-count="characterCount" :character-count="characterCount"
:class="(hidden && preferences.show_content_warning) && 'hidden'" :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> </OverflowGuard>
<Attachments <Attachments
v-if="attachments.length > 0" v-if="note.media_attachments.length > 0"
:attachments="attachments" :attachments="note.media_attachments"
:class="(hidden && preferences.show_content_warning) && 'hidden'" :class="(hidden && preferences.show_content_warning) && 'hidden'"
/> />
<div v-if="quote" class="mt-4 rounded border overflow-hidden"> <div v-if="note.quote" class="mt-4 rounded border overflow-hidden">
<Note :note="quote" :hide-actions="true" :small-layout="true" /> <Note :note="note.quote" :hide-actions="true" :small-layout="true" />
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import type { Attachment, CustomEmoji, Status } from "@versia/client/schemas";
import type { z } from "zod";
import Attachments from "./attachments.vue"; import Attachments from "./attachments.vue";
import ContentWarning from "./content-warning.vue"; import ContentWarning from "./content-warning.vue";
import Note from "./note.vue"; import Note from "./note.vue";
import OverflowGuard from "./overflow-guard.vue"; import OverflowGuard from "./overflow-guard.vue";
import Prose from "./prose.vue"; import Prose from "./prose.vue";
import { key } from "./provider";
const { content, plainContent, sensitive, contentWarning } = defineProps<{ // biome-ignore lint/style/noNonNullAssertion: We want an error if not provided
plainContent?: string; const { note } = inject(key)!;
content: string;
quote?: NonNullable<z.infer<typeof Status.shape.quote>>;
emojis: z.infer<typeof CustomEmoji>[];
attachments: z.infer<typeof Attachment>[];
sensitive: boolean;
contentWarning?: string;
}>();
const hidden = ref(sensitive || !!contentWarning); const hidden = ref(note.sensitive || !!note.spoiler_text);
const characterCount = plainContent?.length; const characterCount = note.text?.length;
</script> </script>

View file

@ -2,14 +2,17 @@
<div class="flex items-start justify-between"> <div class="flex items-start justify-between">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<NuxtLink :href="urlAsPath"> <NuxtLink :href="urlAsPath">
<Avatar :src="author.avatar" :name="author.display_name" /> <Avatar
:src="note.account.avatar"
:name="note.account.display_name"
/>
</NuxtLink> </NuxtLink>
<div class="flex flex-col gap-0.5"> <div class="flex flex-col gap-0.5">
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<span <span
class="text-sm font-semibold" class="text-sm font-semibold"
v-render-emojis="author.emojis" v-render-emojis="note.account.emojis"
>{{ author.display_name }}</span >{{ note.account.display_name }}</span
> >
</div> </div>
<div <div
@ -23,16 +26,7 @@
</div> </div>
</div> </div>
</div> </div>
<Menu <Menu @edit="emit('edit')" @delete="emit('delete')">
: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')"
>
<Button variant="ghost" size="icon"> <Button variant="ghost" size="icon">
<Ellipsis /> <Ellipsis />
</Button> </Button>
@ -41,36 +35,25 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import type { Account, Status } from "@versia/client/schemas";
import type { import type {
UseTimeAgoMessages, UseTimeAgoMessages,
UseTimeAgoUnitNamesDefault, UseTimeAgoUnitNamesDefault,
} from "@vueuse/core"; } from "@vueuse/core";
import { AtSign, Ellipsis, Globe, Lock, LockOpen } from "lucide-vue-next"; import { Ellipsis } from "lucide-vue-next";
import type { z } from "zod";
import { getLocale } from "~~/paraglide/runtime";
import Avatar from "../profiles/avatar.vue"; import Avatar from "../profiles/avatar.vue";
import { Button } from "../ui/button"; import { Button } from "../ui/button";
import Menu from "./menu.vue"; import Menu from "./menu.vue";
import { key } from "./provider";
const { createdAt, noteUrl, author, authorUrl } = defineProps<{ // biome-ignore lint/style/noNonNullAssertion: We want an error if not provided
cornerAvatar?: string; const { note } = inject(key)!;
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;
}>();
const [username, instance] = author.acct.split("@"); const [username, instance] = note.account.acct.split("@");
const digitRegex = /\d/; const digitRegex = /\d/;
const urlAsPath = new URL(authorUrl).pathname; const accountUrl = wrapUrl(`/@${note.account.acct}`);
const noteUrlAsPath = new URL(noteUrl).pathname; const urlAsPath = new URL(accountUrl).pathname;
const timeAgo = useTimeAgo(createdAt, {
const timeAgo = useTimeAgo(note.created_at, {
messages: { messages: {
justNow: "now", justNow: "now",
past: (n) => (n.match(digitRegex) ? `${n}` : n), past: (n) => (n.match(digitRegex) ? `${n}` : n),
@ -85,33 +68,9 @@ const timeAgo = useTimeAgo(createdAt, {
invalid: "", invalid: "",
} as UseTimeAgoMessages<UseTimeAgoUnitNamesDefault>, } as UseTimeAgoMessages<UseTimeAgoUnitNamesDefault>,
}); });
const fullTime = new Intl.DateTimeFormat(getLocale(), {
dateStyle: "medium",
timeStyle: "short",
}).format(createdAt);
const popupOpen = ref(false);
const emit = defineEmits<{ const emit = defineEmits<{
edit: []; edit: [];
delete: []; 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> </script>

View file

@ -21,15 +21,10 @@ import {
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { confirmModalService } from "~/components/modals/composable.ts"; import { confirmModalService } from "~/components/modals/composable.ts";
import * as m from "~~/paraglide/messages.js"; import * as m from "~~/paraglide/messages.js";
import { key } from "./provider";
const { authorId, noteId } = defineProps<{ // biome-ignore lint/style/noNonNullAssertion: We want an error if not provided
apiNoteString: string; const { note, isRemote } = inject(key)!;
isRemote: boolean;
url: string;
remoteUrl?: string;
authorId: string;
noteId: string;
}>();
const emit = defineEmits<{ const emit = defineEmits<{
edit: []; edit: [];
@ -38,7 +33,9 @@ const emit = defineEmits<{
const { copy } = useClipboard(); const { copy } = useClipboard();
const authStore = useAuthStore(); 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) => { const copyText = (text: string) => {
copy(text); copy(text);
@ -68,7 +65,7 @@ const _delete = async () => {
} }
const id = toast.loading(m.new_funny_fox_boil()); const id = toast.loading(m.new_funny_fox_boil());
await authStore.client.deleteStatus(noteId); await authStore.client.deleteStatus(note.id);
toast.dismiss(id); toast.dismiss(id);
toast.success(m.green_tasty_bumblebee_beam()); toast.success(m.green_tasty_bumblebee_beam());
@ -91,11 +88,14 @@ const _delete = async () => {
<Pencil /> <Pencil />
{{ m.front_lime_grizzly_persist() }} {{ m.front_lime_grizzly_persist() }}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem as="button" @click="copyText(apiNoteString)"> <DropdownMenuItem
as="button"
@click="copyText(JSON.stringify(note, null, 4))"
>
<Code /> <Code />
{{ m.yummy_moving_scallop_sail() }} {{ m.yummy_moving_scallop_sail() }}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem as="button" @click="copyText(noteId)"> <DropdownMenuItem as="button" @click="copyText(note.id)">
<Hash /> <Hash />
{{ m.sunny_zany_jellyfish_pop() }} {{ m.sunny_zany_jellyfish_pop() }}
</DropdownMenuItem> </DropdownMenuItem>
@ -108,8 +108,8 @@ const _delete = async () => {
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
as="button" as="button"
v-if="isRemote && remoteUrl" v-if="isRemote && note.url"
@click="copyText(remoteUrl)" @click="copyText(note.url)"
> >
<Link /> <Link />
{{ m.solid_witty_zebra_walk() }} {{ m.solid_witty_zebra_walk() }}
@ -119,7 +119,7 @@ const _delete = async () => {
v-if="isRemote" v-if="isRemote"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
:href="remoteUrl" :href="note.url"
> >
<ExternalLink /> <ExternalLink />
{{ m.active_trite_lark_inspire() }} {{ m.active_trite_lark_inspire() }}
@ -142,7 +142,10 @@ const _delete = async () => {
<Flag /> <Flag />
{{ m.great_few_jaguar_rise() }} {{ m.great_few_jaguar_rise() }}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem as="button" @click="blockUser(authorId)"> <DropdownMenuItem
as="button"
@click="blockUser(note.account.id)"
>
<Ban /> <Ban />
{{ m.misty_soft_sparrow_vent() }} {{ m.misty_soft_sparrow_vent() }}
</DropdownMenuItem> </DropdownMenuItem>

View file

@ -8,63 +8,25 @@
class="absolute left-0 top-0 bottom-0 w-2 bg-border rounded-tl-md" class="absolute left-0 top-0 bottom-0 w-2 bg-border rounded-tl-md"
/> />
<CardHeader as="header" class="space-y-2"> <CardHeader as="header" class="space-y-2">
<ReblogHeader <ReblogHeader v-if="note.reblog" />
v-if="note.reblog"
:avatar="note.account.avatar"
:display-name="note.account.display_name"
:url="reblogAccountUrl"
:emojis="note.account.emojis"
/>
<Header <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)" @edit="useEvent('composer:edit', noteToUse)"
@delete="useEvent('note:delete', noteToUse)" @delete="useEvent('note:delete', noteToUse)"
:note-id="noteToUse.id"
class="z-1" class="z-1"
/> />
</CardHeader> </CardHeader>
<CardContent class="space-y-2"> <CardContent class="space-y-2">
<Content <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"
/>
<Reactions <Reactions
v-if="noteToUse.reactions && noteToUse.reactions.length > 0" v-if="noteToUse.reactions && noteToUse.reactions.length > 0"
:reactions="noteToUse.reactions"
:emojis="noteToUse.emojis"
:status-id="noteToUse.id"
/> />
</CardContent> </CardContent>
<CardFooter v-if="!hideActions"> <CardFooter v-if="!hideActions">
<Actions <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)" @edit="useEvent('composer:edit', noteToUse)"
@reply="useEvent('composer:reply', noteToUse)" @reply="useEvent('composer:reply', noteToUse)"
@quote="useEvent('composer:quote', noteToUse)" @quote="useEvent('composer:quote', noteToUse)"
@delete="useEvent('note:delete', noteToUse)" @delete="useEvent('note:delete', noteToUse)"
:note-id="noteToUse.id"
:liked="noteToUse.favourited ?? false"
:reblogged="noteToUse.reblogged ?? false"
/> />
</CardFooter> </CardFooter>
</Card> </Card>
@ -77,6 +39,7 @@ import { Card, CardContent, CardFooter, CardHeader } from "../ui/card";
import Actions from "./actions.vue"; import Actions from "./actions.vue";
import Content from "./content.vue"; import Content from "./content.vue";
import Header from "./header.vue"; import Header from "./header.vue";
import { key } from "./provider";
import Reactions from "./reactions/index.vue"; import Reactions from "./reactions/index.vue";
import ReblogHeader from "./reblog-header.vue"; import ReblogHeader from "./reblog-header.vue";
@ -99,8 +62,9 @@ const noteToUse = computed(() =>
: (note as z.infer<typeof Status>), : (note as z.infer<typeof Status>),
); );
const url = wrapUrl(`/@${noteToUse.value.account.acct}/${noteToUse.value.id}`); provide(key, {
const accountUrl = wrapUrl(`/@${noteToUse.value.account.acct}`); note: noteToUse.value,
const reblogAccountUrl = wrapUrl(`/@${note.account.acct}`); rebloggerNote: note.reblog ? (note as z.infer<typeof Status>) : undefined,
const isRemote = noteToUse.value.account.acct.includes("@"); isRemote: noteToUse.value.account.acct.includes("@"),
});
</script> </script>

View file

@ -1,7 +1,7 @@
<template> <template>
<div <div
:class="[ :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, $style.content,
]" ]"
> >

View 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>;
}>;

View file

@ -1,23 +1,18 @@
<template> <template>
<div class="flex flex-row gap-1 flex-wrap"> <div class="flex flex-row gap-1 flex-wrap">
<Reaction <Reaction
v-for="reaction in reactions" v-for="reaction in note.reactions"
:key="reaction.name" :key="reaction.name"
:reaction="reaction" :reaction="reaction"
:emoji="emojis.find(e => `:${e.shortcode}:` === reaction.name)" :emoji="note.emojis.find(e => `:${e.shortcode}:` === reaction.name)"
:status-id="statusId"
/> />
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import type { CustomEmoji, NoteReaction } from "@versia/client/schemas"; import { key } from "../provider";
import type { z } from "zod";
import Reaction from "./reaction.vue"; import Reaction from "./reaction.vue";
const { statusId, reactions, emojis } = defineProps<{ // biome-ignore lint/style/noNonNullAssertion: We want an error if not provided
statusId: string; const { note } = inject(key)!;
reactions: z.infer<typeof NoteReaction>[];
emojis: z.infer<typeof CustomEmoji>[];
}>();
</script> </script>

View file

@ -61,14 +61,16 @@ import {
} from "~/components/ui/hover-card"; } from "~/components/ui/hover-card";
import * as m from "~~/paraglide/messages.js"; import * as m from "~~/paraglide/messages.js";
import { getLocale } from "~~/paraglide/runtime.js"; import { getLocale } from "~~/paraglide/runtime.js";
import { key } from "../provider";
const { reaction, emoji, statusId } = defineProps<{ const { reaction, emoji } = defineProps<{
statusId: string;
reaction: z.infer<typeof NoteReaction>; reaction: z.infer<typeof NoteReaction>;
emoji?: z.infer<typeof CustomEmoji>; emoji?: z.infer<typeof CustomEmoji>;
}>(); }>();
const authStore = useAuthStore(); const authStore = useAuthStore();
// biome-ignore lint/style/noNonNullAssertion: We want an error if not provided
const { note } = inject(key)!;
const formatNumber = (number: number) => const formatNumber = (number: number) =>
new Intl.NumberFormat(getLocale(), { new Intl.NumberFormat(getLocale(), {
@ -80,7 +82,7 @@ const formatNumber = (number: number) =>
const accounts = ref<z.infer<typeof Account>[] | null>(null); const accounts = ref<z.infer<typeof Account>[] | null>(null);
const refreshReactions = async () => { const refreshReactions = async () => {
const { data } = await authStore.client.getStatusReactions(statusId); const { data } = await authStore.client.getStatusReactions(note.id);
const accountIds = const accountIds =
data.find((r) => r.name === reaction.name)?.account_ids.slice(0, 10) ?? 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 id = toast.loading(m.gray_stale_antelope_roam());
const { data } = await authStore.client.createEmojiReaction( const { data } = await authStore.client.createEmojiReaction(
statusId, note.id,
reaction.name, reaction.name,
); );
@ -108,7 +110,7 @@ const unreact = async () => {
const id = toast.loading(m.many_weary_bat_intend()); const id = toast.loading(m.many_weary_bat_intend());
const { data } = await authStore.client.deleteEmojiReaction( const { data } = await authStore.client.deleteEmojiReaction(
statusId, note.id,
reaction.name, reaction.name,
); );

View file

@ -4,9 +4,15 @@
class="flex-row px-2 py-1 items-center gap-2 hover:bg-muted duration-100 text-sm" class="flex-row px-2 py-1 items-center gap-2 hover:bg-muted duration-100 text-sm"
> >
<Repeat class="size-4 text-primary" /> <Repeat class="size-4 text-primary" />
<Avatar class="size-6 border" :src="avatar" :name="displayName" /> <Avatar
<span class="font-semibold" v-render-emojis="emojis" class="size-6 border"
>{{ displayName }}</span :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() }} {{ m.large_vivid_horse_catch() }}
</Card> </Card>
@ -14,19 +20,21 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import type { CustomEmoji } from "@versia/client/schemas";
import { Repeat } from "lucide-vue-next"; import { Repeat } from "lucide-vue-next";
import type { z } from "zod";
import * as m from "~~/paraglide/messages.js"; import * as m from "~~/paraglide/messages.js";
import Avatar from "../profiles/avatar.vue"; import Avatar from "../profiles/avatar.vue";
import { Card } from "../ui/card"; import { Card } from "../ui/card";
import { key } from "./provider";
const { url } = defineProps<{ // biome-ignore lint/style/noNonNullAssertion: We want an error if not provided
avatar: string; const { rebloggerNote } = inject(key)!;
displayName: string;
emojis: z.infer<typeof CustomEmoji>[];
url: string;
}>();
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> </script>

View file

@ -60,7 +60,7 @@ const props = defineProps<{
const emit = defineEmits<(e: "update") => void>(); const emit = defineEmits<(e: "update") => void>();
const loadMoreTrigger = ref<HTMLElement | null>(null); const loadMoreTrigger = useTemplateRef("loadMoreTrigger");
useIntersectionObserver(loadMoreTrigger, ([observer]) => { useIntersectionObserver(loadMoreTrigger, ([observer]) => {
if (observer?.isIntersecting && !props.isLoading && !props.hasReachedEnd) { if (observer?.isIntersecting && !props.isLoading && !props.hasReachedEnd) {