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()"
|
: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());
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
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>
|
<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>
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue