refactor: ♻️ Rewrite state system to use Pinia for composer and auth

This commit is contained in:
Jesse Wierzbinski 2025-08-28 07:41:51 +02:00
parent a6db9e059d
commit b510782a30
No known key found for this signature in database
80 changed files with 999 additions and 1011 deletions

View file

@ -25,13 +25,13 @@ const copy = (data: string) => {
toast.success("Copied to clipboard");
};
const appData = useAppData();
const authStore = useAuthStore();
const data: [string, string | VNode][] = [
["User ID", identity.value?.account.id ?? ""],
["Instance domain", identity.value?.instance.domain ?? ""],
["Instance version", identity.value?.instance.versia_version ?? ""],
["Client ID", appData.value?.client_id ?? ""],
["User ID", authStore.account?.id ?? ""],
["Instance domain", authStore.instance?.domain ?? ""],
["Instance version", authStore.instance?.versia_version ?? ""],
["Client ID", authStore.application?.client_id ?? ""],
[
"Client secret",
<Button
@ -39,7 +39,7 @@ const data: [string, string | VNode][] = [
class="font-sans"
size="sm"
// @ts-expect-error missing onClick types
onClick={() => copy(appData.value?.client_secret ?? "")}
onClick={() => copy(authStore.application?.client_secret ?? "")}
>
Click to copy
</Button>,
@ -51,7 +51,7 @@ const data: [string, string | VNode][] = [
class="font-sans"
size="sm"
// @ts-expect-error missing onClick types
onClick={() => copy(identity.value?.tokens.access_token ?? "")}
onClick={() => copy(authStore.token?.access_token ?? "")}
>
Click to copy
</Button>,

View file

@ -62,22 +62,11 @@ const categories = Object.fromEntries(
}),
);
const { account: author1 } = useAccountFromAcct(
client,
"jessew@vs.cpluspatch.com",
);
const { account: author2 } = useAccountFromAcct(
client,
"aprl@social.lysand.org",
);
const { account: author3 } = useAccountFromAcct(
client,
"lina@social.lysand.org",
);
const { account: author4 } = useAccountFromAcct(client, "nyx@v.everypizza.im");
const { account: author1 } = useAccountFromAcct("jessew@vs.cpluspatch.com");
const { account: author2 } = useAccountFromAcct("aprl@social.lysand.org");
const { account: author3 } = useAccountFromAcct("lina@social.lysand.org");
const { account: author4 } = useAccountFromAcct("nyx@v.everypizza.im");
const authStore = useAuthStore();
const open = ref(false);
@ -87,14 +76,14 @@ useListen("preferences:open", () => {
</script>
<template>
<Dialog v-model:open="open" v-if="identity">
<Dialog v-model:open="open" v-if="authStore.isSignedIn">
<DialogContent class="md:max-w-5xl w-full h-full p-0 md:max-h-[70dvh] overflow-hidden">
<Tabs class="md:grid-cols-[auto_minmax(0,1fr)] !grid gap-2 *:p-4 overflow-hidden *:overflow-y-auto *:h-full" orientation="vertical"
:default-value="pages[0]">
<DialogHeader class="gap-6 grid grid-rows-[auto_minmax(0,1fr)] border-b md:border-b-0 md:border-r min-w-60 text-left">
<div class="grid gap-3 items-center grid-cols-[auto_minmax(0,1fr)]">
<Avatar :name="identity.account.display_name || identity.account.username"
:src="identity.account.avatar" />
<Avatar :name="authStore.account!.display_name || authStore.account!.username"
:src="authStore.account!.avatar" />
<DialogTitle>Preferences</DialogTitle>
</div>
<DialogDescription class="sr-only">

View file

@ -30,14 +30,14 @@ const { emojis } = defineProps<{
emojis: z.infer<typeof CustomEmoji>[];
}>();
const permissions = usePermissions();
const authStore = useAuthStore();
const canEdit =
(!emojis.some((e) => e.global) &&
permissions.value.includes(RolePermission.ManageOwnEmojis)) ||
permissions.value.includes(RolePermission.ManageEmojis);
authStore.permissions.includes(RolePermission.ManageOwnEmojis)) ||
authStore.permissions.includes(RolePermission.ManageEmojis);
const deleteAll = async () => {
if (!identity.value) {
if (!authStore.isSignedIn) {
return;
}
@ -57,14 +57,16 @@ const deleteAll = async () => {
);
try {
await Promise.all(
emojis.map((emoji) => client.value.deleteEmoji(emoji.id)),
emojis.map((emoji) => authStore.client.deleteEmoji(emoji.id)),
);
toast.dismiss(id);
toast.success("Emojis deleted");
identity.value.emojis = identity.value.emojis.filter(
(e) => !emojis.some((emoji) => e.id === emoji.id),
);
authStore.updateActiveIdentity({
emojis: authStore.emojis.filter(
(e) => !emojis.some((emoji) => e.id === emoji.id),
),
});
} catch {
toast.dismiss(id);
}

View file

@ -42,14 +42,10 @@ const { emoji } = defineProps<{
emoji: z.infer<typeof CustomEmoji>;
}>();
const permissions = usePermissions();
const canEdit =
(!emoji.global &&
permissions.value.includes(RolePermission.ManageOwnEmojis)) ||
permissions.value.includes(RolePermission.ManageEmojis);
const authStore = useAuthStore();
const editName = async () => {
if (!identity.value) {
if (!authStore.isSignedIn) {
return;
}
@ -63,16 +59,18 @@ const editName = async () => {
if (result.confirmed) {
const id = toast.loading(m.teary_tame_gull_bless());
try {
const { data } = await client.value.updateEmoji(emoji.id, {
const { data } = await authStore.client.updateEmoji(emoji.id, {
shortcode: result.value,
});
toast.dismiss(id);
toast.success(m.gaudy_lime_bison_adore());
identity.value.emojis = identity.value.emojis.map((e) =>
e.id === emoji.id ? data : e,
);
authStore.updateActiveIdentity({
emojis: authStore.emojis.map((e) =>
e.id === emoji.id ? data : e,
),
});
} catch {
toast.dismiss(id);
}
@ -80,7 +78,7 @@ const editName = async () => {
};
const _delete = async () => {
if (!identity.value) {
if (!authStore.isSignedIn) {
return;
}
@ -93,13 +91,13 @@ const _delete = async () => {
if (confirmed) {
const id = toast.loading(m.weary_away_liger_zip());
try {
await client.value.deleteEmoji(emoji.id);
await authStore.client.deleteEmoji(emoji.id);
toast.dismiss(id);
toast.success(m.crisp_whole_canary_tear());
identity.value.emojis = identity.value.emojis.filter(
(e) => e.id !== emoji.id,
);
authStore.updateActiveIdentity({
emojis: authStore.emojis.filter((e) => e.id !== emoji.id),
});
} catch {
toast.dismiss(id);
}

View file

@ -1,6 +1,6 @@
<template>
<div v-if="emojis.length > 0" class="grow">
<Table :emojis="emojis" :can-upload="canUpload" />
<div v-if="authStore.emojis.length > 0" class="grow">
<Table :emojis="authStore.emojis" :can-upload="canUpload" />
</div>
</template>
@ -8,12 +8,10 @@
import { RolePermission } from "@versia/client/schemas";
import Table from "./table.vue";
const permissions = usePermissions();
const authStore = useAuthStore();
const canUpload = computed(
() =>
permissions.value.includes(RolePermission.ManageOwnEmojis) ||
permissions.value.includes(RolePermission.ManageEmojis),
authStore.permissions.includes(RolePermission.ManageOwnEmojis) ||
authStore.permissions.includes(RolePermission.ManageEmojis),
);
const emojis = computed(() => identity.value?.emojis ?? []);
</script>

View file

@ -189,8 +189,10 @@ import { Textarea } from "~/components/ui/textarea";
import * as m from "~~/paraglide/messages.js";
const open = ref(false);
const permissions = usePermissions();
const hasEmojiAdmin = permissions.value.includes(RolePermission.ManageEmojis);
const authStore = useAuthStore();
const hasEmojiAdmin = authStore.permissions.includes(
RolePermission.ManageEmojis,
);
const createObjectURL = URL.createObjectURL;
const formSchema = toTypedSchema(
@ -202,11 +204,11 @@ const formSchema = toTypedSchema(
.refine(
(v) =>
v.size <=
(identity.value?.instance.configuration.emojis
(authStore.instance?.configuration.emojis
.emoji_size_limit ?? Number.POSITIVE_INFINITY),
m.orange_weird_parakeet_hug({
count:
identity.value?.instance.configuration.emojis
authStore.instance?.configuration.emojis
.emoji_size_limit ?? Number.POSITIVE_INFINITY,
}),
),
@ -214,11 +216,11 @@ const formSchema = toTypedSchema(
.string()
.min(1)
.max(
identity.value?.instance.configuration.emojis
authStore.instance?.configuration.emojis
.max_shortcode_characters ?? Number.POSITIVE_INFINITY,
m.solid_inclusive_owl_hug({
count:
identity.value?.instance.configuration.emojis
authStore.instance?.configuration.emojis
.max_shortcode_characters ??
Number.POSITIVE_INFINITY,
}),
@ -237,11 +239,11 @@ const formSchema = toTypedSchema(
alt: z
.string()
.max(
identity.value?.instance.configuration.emojis
authStore.instance?.configuration.emojis
.max_description_characters ?? Number.POSITIVE_INFINITY,
m.key_ago_hound_emerge({
count:
identity.value?.instance.configuration.emojis
authStore.instance?.configuration.emojis
.max_description_characters ??
Number.POSITIVE_INFINITY,
}),
@ -254,14 +256,14 @@ const { isSubmitting, handleSubmit, values, setFieldValue } = useForm({
});
const submit = handleSubmit(async (values) => {
if (!identity.value) {
if (!authStore.isSignedIn) {
return;
}
const id = toast.loading(m.factual_gray_mouse_believe());
try {
const { data } = await client.value.uploadEmoji(
const { data } = await authStore.client.uploadEmoji(
values.shortcode,
values.image,
{
@ -274,7 +276,10 @@ const submit = handleSubmit(async (values) => {
toast.dismiss(id);
toast.success(m.cool_trite_gull_quiz());
identity.value.emojis = [...identity.value.emojis, data];
authStore.updateActiveIdentity({
emojis: [...authStore.emojis, data],
});
open.value = false;
} catch {
toast.dismiss(id);

View file

@ -1,10 +1,11 @@
import { toTypedSchema } from "@vee-validate/zod";
import type { Instance } from "@versia/client/schemas";
import { z } from "zod";
import * as m from "~~/paraglide/messages.js";
const characterRegex = new RegExp(/^[a-z0-9_-]+$/);
export const formSchema = (identity: Identity) =>
export const formSchema = (instance: z.infer<typeof Instance>) =>
toTypedSchema(
z.strictObject({
banner: z
@ -12,11 +13,10 @@ export const formSchema = (identity: Identity) =>
.refine(
(v) =>
v.size <=
(identity.instance.configuration.accounts
.header_limit ?? Number.POSITIVE_INFINITY),
(instance.configuration.accounts.header_limit ??
Number.POSITIVE_INFINITY),
m.civil_icy_ant_mend({
size: identity.instance.configuration.accounts
.header_limit,
size: instance.configuration.accounts.header_limit,
}),
)
.optional(),
@ -25,11 +25,10 @@ export const formSchema = (identity: Identity) =>
.refine(
(v) =>
v.size <=
(identity.instance.configuration.accounts
.avatar_limit ?? Number.POSITIVE_INFINITY),
(instance.configuration.accounts.avatar_limit ??
Number.POSITIVE_INFINITY),
m.zippy_caring_raven_edit({
size: identity.instance.configuration.accounts
.avatar_limit,
size: instance.configuration.accounts.avatar_limit,
}),
)
.or(z.string().url())
@ -37,22 +36,15 @@ export const formSchema = (identity: Identity) =>
name: z
.string()
.max(
identity.instance.configuration.accounts
.max_displayname_characters,
instance.configuration.accounts.max_displayname_characters,
),
username: z
.string()
.regex(characterRegex, m.still_upper_otter_dine())
.max(
identity.instance.configuration.accounts
.max_username_characters,
),
.max(instance.configuration.accounts.max_username_characters),
bio: z
.string()
.max(
identity.instance.configuration.accounts
.max_note_characters,
),
.max(instance.configuration.accounts.max_note_characters),
bot: z.boolean().default(false),
locked: z.boolean().default(false),
discoverable: z.boolean().default(true),

View file

@ -1,5 +1,5 @@
<template>
<form v-if="identity" class="grid gap-6" @submit="save">
<form class="grid gap-6" @submit="save">
<Transition name="slide-up">
<Alert v-if="dirty" layout="button" class="absolute bottom-2 z-10 inset-x-2 w-[calc(100%-1rem)]">
<SaveOff class="size-4" />
@ -19,7 +19,7 @@
<FormField v-slot="{ setValue }" name="avatar">
<TextInput :title="m.safe_icy_bulldog_quell()">
<ImageUploader v-model:image="identity.account.avatar" @submit-file="(file) => setValue(file)"
<ImageUploader v-model:image="authStore.account!.avatar" @submit-file="(file) => setValue(file)"
@submit-url="(url) => setValue(url)" />
</TextInput>
</FormField>
@ -85,25 +85,25 @@ import ImageUploader from "./profile/image-uploader.vue";
const dirty = computed(() => form.meta.value.dirty);
const submitting = ref(false);
const authStore = useAuthStore();
if (!identity.value) {
throw new Error("Identity not found.");
if (!(authStore.instance && authStore.account)) {
throw new Error("Not signed in.");
}
const account = computed(() => identity.value?.account as Identity["account"]);
const schema = formSchema(identity.value);
const schema = formSchema(authStore.instance);
const form = useForm({
validationSchema: schema,
initialValues: {
bio: account.value.source?.note ?? "",
bot: account.value.bot ?? false,
locked: account.value.locked ?? false,
discoverable: account.value.discoverable ?? true,
username: account.value.username,
name: account.value.display_name,
bio: authStore.account.source?.note ?? "",
bot: authStore.account.bot ?? false,
locked: authStore.account.locked ?? false,
discoverable: authStore.account.discoverable ?? true,
username: authStore.account.username,
name: authStore.account.display_name,
fields:
account.value.source?.fields.map((f) => ({
authStore.account.source?.fields.map((f) => ({
name: f.name,
value: f.value,
})) ?? [],
@ -111,7 +111,7 @@ const form = useForm({
});
const save = form.handleSubmit(async (values) => {
if (submitting.value) {
if (submitting.value || !authStore.account) {
return;
}
@ -120,25 +120,29 @@ const save = form.handleSubmit(async (values) => {
const changedData = {
display_name:
values.name === account.value.display_name
values.name === authStore.account.display_name
? undefined
: values.name,
username:
values.username === account.value.username
values.username === authStore.account.username
? undefined
: values.username,
note:
values.bio === account.value.source?.note ? undefined : values.bio,
bot: values.bot === account.value.bot ? undefined : values.bot,
values.bio === authStore.account.source?.note
? undefined
: values.bio,
bot: values.bot === authStore.account.bot ? undefined : values.bot,
locked:
values.locked === account.value.locked ? undefined : values.locked,
values.locked === authStore.account.locked
? undefined
: values.locked,
discoverable:
values.discoverable === account.value.discoverable
values.discoverable === authStore.account.discoverable
? undefined
: values.discoverable,
// Can't compare two arrays directly in JS, so we need to check if all fields are the same
fields_attributes: values.fields.every((field) =>
account.value.source?.fields?.some(
authStore.account?.source?.fields?.some(
(f) => f.name === field.name && f.value === field.value,
),
)
@ -157,7 +161,7 @@ const save = form.handleSubmit(async (values) => {
}
try {
const { data } = await client.value.updateCredentials(
const { data } = await authStore.client.updateCredentials(
Object.fromEntries(
Object.entries(changedData).filter(([, v]) => v !== undefined),
),
@ -166,9 +170,9 @@ const save = form.handleSubmit(async (values) => {
toast.dismiss(id);
toast.success(m.spry_honest_kestrel_arrive());
if (identity.value) {
identity.value.account = data;
}
authStore.updateActiveIdentity({
account: data,
});
form.resetForm({
values: {