mirror of
https://github.com/versia-pub/frontend.git
synced 2026-03-13 03:29:16 +01:00
refactor: ♻️ Rewrite state system to use Pinia for composer and auth
This commit is contained in:
parent
a6db9e059d
commit
b510782a30
80 changed files with 999 additions and 1011 deletions
|
|
@ -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>,
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue