feat: Implement emoji uploads

This commit is contained in:
Jesse Wierzbinski 2024-12-09 13:12:46 +01:00
parent 17441bfd47
commit 93a3d233d0
No known key found for this signature in database
9 changed files with 391 additions and 22 deletions

View file

@ -72,9 +72,15 @@ const { emoji } = defineProps<{
const permissions = usePermissions();
const canEdit =
!emoji.global || permissions.value.includes(RolePermission.ManageEmojis);
(!emoji.global &&
permissions.value.includes(RolePermission.ManageOwnEmojis)) ||
permissions.value.includes(RolePermission.ManageEmojis);
const editName = async () => {
if (!identity.value) {
return;
}
const result = await confirmModalService.confirm({
title: m.slimy_awful_florian_sail(),
defaultValue: emoji.shortcode,
@ -85,12 +91,16 @@ const editName = async () => {
if (result.confirmed) {
const id = toast.loading(m.teary_tame_gull_bless());
try {
await client.value.updateEmoji(emoji.id, {
const { data } = await client.value.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,
);
} catch {
toast.dismiss(id);
}
@ -98,13 +108,29 @@ const editName = async () => {
};
const _delete = async () => {
const id = toast.loading(m.weary_away_liger_zip());
try {
await client.value.deleteEmoji(emoji.id);
toast.dismiss(id);
toast.success(m.crisp_whole_canary_tear());
} catch {
toast.dismiss(id);
if (!identity.value) {
return;
}
const { confirmed } = await confirmModalService.confirm({
title: m.tense_quick_cod_favor(),
message: m.honest_factual_carp_aspire(),
confirmText: m.tense_quick_cod_favor(),
});
if (confirmed) {
const id = toast.loading(m.weary_away_liger_zip());
try {
await client.value.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,
);
} catch {
toast.dismiss(id);
}
}
};
</script>

View file

@ -0,0 +1,245 @@
<template>
<Dialog v-model:open="open">
<DialogTrigger>
<slot />
</DialogTrigger>
<DialogContent>
<DialogTitle>
{{ m.whole_icy_puffin_smile() }}
</DialogTitle>
<DialogDescription class="sr-only">
{{ m.frail_great_marten_pet() }}
</DialogDescription>
<form class="p-4 grid gap-6" @submit="submit">
<div v-if="values.image" class="flex items-center justify-around *:size-20 *:p-2 *:rounded *:border *:shadow">
<div class="bg-background">
<img class="h-full object-cover" :src="createObjectURL(values.image as File)" :alt="values.alt" />
</div>
<div class="bg-zinc-700">
<img class="h-full object-cover" :src="createObjectURL(values.image as File)" :alt="values.alt" />
</div>
<div class="bg-zinc-400">
<img class="h-full object-cover" :src="createObjectURL(values.image as File)" :alt="values.alt" />
</div>
<div class="bg-foreground">
<img class="h-full object-cover" :src="createObjectURL(values.image as File)" :alt="values.alt" />
</div>
</div>
<FormField v-slot="{ handleChange, handleBlur }" name="image">
<FormItem>
<FormLabel>
{{ m.active_direct_bear_compose() }}
</FormLabel>
<FormControl>
<Input type="file" accept="image/*" @change="(e: any) => {
handleChange(e);
if (!values.shortcode) {
setFieldValue('shortcode', e.target.files[0].name.replace(/\.[^/.]+$/, ''));
}
}" @blur="handleBlur" :disabled="isSubmitting" />
</FormControl>
<FormDescription>
{{ m.lime_late_millipede_urge() }}
</FormDescription>
<FormMessage />
</FormItem>
</FormField>
<FormField v-slot="{ componentField }" name="shortcode">
<FormItem>
<FormLabel>
{{ m.happy_mild_fox_gleam() }}
</FormLabel>
<FormControl>
<Input v-bind="componentField" :disabled="isSubmitting" />
</FormControl>
<FormDescription>
{{ m.glad_day_kestrel_amaze() }}
</FormDescription>
<FormMessage />
</FormItem>
</FormField>
<FormField v-slot="{ componentField }" name="category">
<FormItem>
<FormLabel>
{{ m.short_cute_jackdaw_comfort() }}
</FormLabel>
<FormControl>
<Input v-bind="componentField" :disabled="isSubmitting" />
</FormControl>
<FormMessage />
</FormItem>
</FormField>
<FormField v-slot="{ componentField }" name="alt">
<FormItem>
<FormLabel>
{{ m.watery_left_shrimp_bless() }}
</FormLabel>
<FormControl>
<Textarea rows="2" v-bind="componentField" :disabled="isSubmitting" />
</FormControl>
<FormDescription>
{{ m.weird_fun_jurgen_arise() }}
</FormDescription>
<FormMessage />
</FormItem>
</FormField>
<FormField v-slot="{ componentField, value, handleChange }" v-if="hasEmojiAdmin" name="global"
:as="Card">
<FormItem class="grid grid-cols-[1fr,auto] items-center p-6 gap-2">
<CardHeader class="space-y-0.5 p-0">
<FormLabel :as="CardTitle">
{{ m.pink_sharp_carp_work() }}
</FormLabel>
<CardDescription>
{{ m.dark_pretty_hyena_link() }}
</CardDescription>
</CardHeader>
<FormControl>
<Switch :checked="value" @update:checked="handleChange" v-bind="componentField" :disabled="isSubmitting" />
</FormControl>
<FormMessage />
</FormItem>
</FormField>
<DialogFooter>
<DialogClose :as-child="true">
<Button variant="outline" :disabled="isSubmitting">
{{ m.soft_bold_ant_attend() }}
</Button>
</DialogClose>
<Button type="submit" variant="default" :disabled="isSubmitting">
{{ m.flat_safe_haddock_gaze() }}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</template>
<script lang="ts" setup>
import { toTypedSchema } from "@vee-validate/zod";
import { RolePermission } from "@versia/client/types";
import { useForm } from "vee-validate";
import { toast } from "vue-sonner";
import { z } from "zod";
import { Button } from "~/components/ui/button";
import { Card, CardTitle } from "~/components/ui/card";
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogTitle,
DialogTrigger,
} from "~/components/ui/dialog";
import {
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "~/components/ui/form";
import * as m from "~/paraglide/messages.js";
const open = ref(false);
const permissions = usePermissions();
const hasEmojiAdmin = permissions.value.includes(RolePermission.ManageEmojis);
const createObjectURL = URL.createObjectURL;
const formSchema = toTypedSchema(
z.object({
image: z
.instanceof(File, {
message: m.sound_topical_gopher_offer(),
})
.refine(
(v) =>
v.size <=
// @ts-expect-error Types aren't updated with this new value yet
(identity.value?.instance.configuration.emojis
.emoji_size_limit ?? 0),
m.orange_weird_parakeet_hug({
// @ts-expect-error Types aren't updated with this new value yet
count: identity.value?.instance.configuration.emojis
.emoji_size_limit,
}),
),
shortcode: z
.string()
.min(1)
.max(
// @ts-expect-error Types aren't updated with this new value yet
identity.value?.instance.configuration.emojis
.max_emoji_shortcode_characters,
m.solid_inclusive_owl_hug({
// @ts-expect-error Types aren't updated with this new value yet
count: identity.value?.instance.configuration.emojis
.max_emoji_shortcode_characters,
}),
)
.regex(emojiValidator),
global: z.boolean().default(false),
category: z
.string()
.max(
64,
m.home_cool_orangutan_hug({
count: 64,
}),
)
.optional(),
alt: z
.string()
.max(
// @ts-expect-error Types aren't updated with this new value yet
identity.value?.instance.configuration.emojis
.max_emoji_description_characters,
m.key_ago_hound_emerge({
// @ts-expect-error Types aren't updated with this new value yet
count: identity.value?.instance.configuration.emojis
.max_emoji_description_characters,
}),
)
.optional(),
}),
);
const { isSubmitting, handleSubmit, values, setFieldValue } = useForm({
validationSchema: formSchema,
});
const submit = handleSubmit(async (values) => {
if (!identity.value) {
return;
}
const id = toast.loading(m.factual_gray_mouse_believe());
try {
const { data } = await client.value.uploadEmoji(
values.shortcode,
values.image,
{
alt: values.alt,
category: values.category,
global: values.global,
},
);
toast.dismiss(id);
toast.success(m.cool_trite_gull_quiz());
identity.value.emojis = [...identity.value.emojis, data];
open.value = false;
} catch {
toast.dismiss(id);
}
});
</script>

View file

@ -239,7 +239,7 @@
"lucky_suave_myna_adore": "Ask the captain to add some deck decorations.",
"actual_steep_llama_rest": "No decorations in the hold.",
"mild_many_dolphin_mend": "Deck decoration preferences",
"lucky_ago_rat_pinch": "Uncategorized booty",
"lucky_ago_rat_pinch": "Uncategorized",
"empty_awful_lark_dart": "Sailor not found in the ship's log.",
"clean_even_mayfly_tap": "Check for sea monster tracks or try again later.",
"vexed_each_falcon_enjoy": "Blimey!",
@ -317,5 +317,25 @@
"keen_aware_goldfish_thrive": "King's English",
"vivid_mellow_sawfish_approve": "Fancy French",
"gray_clean_shark_comfort": "The following URI parameters be required:",
"grand_spry_goldfish_embrace": "Yer URI parameters be invalid"
"grand_spry_goldfish_embrace": "Yer URI parameters be invalid",
"honest_factual_carp_aspire": "Be yer certain yer want to send this deck decoration to Davy Jones' locker?",
"flat_safe_haddock_gaze": "Return",
"orange_weird_parakeet_hug": "Decoration must be inferior to {count} bytes",
"solid_inclusive_owl_hug": "Code must have less than {count} markings",
"key_ago_hound_emerge": "Map must have less than {count} markings",
"pink_sharp_carp_work": "Global deck decoration",
"dark_pretty_hyena_link": "Can be employed by every sailor, not just yer",
"home_cool_orangutan_hug": "Category must be not greater than {count} markings",
"sound_topical_gopher_offer": "Required",
"watery_left_shrimp_bless": "Description",
"weird_fun_jurgen_arise": "Useful for people with screen readers, or poor network conditions.",
"short_cute_jackdaw_comfort": "Category",
"happy_mild_fox_gleam": "Shortcode",
"active_direct_bear_compose": "Image",
"lime_late_millipede_urge": "Recommended size: 128x128px. Every image type is allowed.",
"factual_gray_mouse_believe": "Uploading emoji...",
"cool_trite_gull_quiz": "Emoji uploaded!",
"kind_deft_myna_hint": "Failed to upload emoji.",
"frail_great_marten_pet": "Upload a new emoji to the server.",
"whole_icy_puffin_smile": "Upload Emoji"
}

View file

@ -318,5 +318,25 @@
"vivid_mellow_sawfish_approve": "French",
"these_awful_ape_reside": "Pirate",
"gray_clean_shark_comfort": "The following rizzy parameters are required:",
"grand_spry_goldfish_embrace": "Invalid URI parameters"
"grand_spry_goldfish_embrace": "Invalid URI parameters",
"honest_factual_carp_aspire": "Are you sure you want to ax this pepe?",
"flat_safe_haddock_gaze": "Upload",
"orange_weird_parakeet_hug": "Image must be less than {count} bytes",
"solid_inclusive_owl_hug": "Shortcode must be less than {count} characters",
"key_ago_hound_emerge": "Description must be less than {count} characters",
"pink_sharp_carp_work": "Global emoji",
"dark_pretty_hyena_link": "Can be used by every NPC, not just you",
"home_cool_orangutan_hug": "Category must be less than {count} characters",
"sound_topical_gopher_offer": "Required",
"watery_left_shrimp_bless": "Description",
"weird_fun_jurgen_arise": "Useful for people with screen readers, or poor network conditions.",
"short_cute_jackdaw_comfort": "Category",
"happy_mild_fox_gleam": "Shortcode",
"active_direct_bear_compose": "Image",
"lime_late_millipede_urge": "Recommended size: 128x128px. Every image type is allowed.",
"factual_gray_mouse_believe": "Uploading emoji...",
"cool_trite_gull_quiz": "Emoji uploaded!",
"kind_deft_myna_hint": "Failed to upload emoji.",
"frail_great_marten_pet": "Upload a new emoji to the server.",
"whole_icy_puffin_smile": "Upload Emoji"
}

View file

@ -318,5 +318,26 @@
"vivid_mellow_sawfish_approve": "French",
"these_awful_ape_reside": "Pirate",
"gray_clean_shark_comfort": "The following URI parameters are required:",
"grand_spry_goldfish_embrace": "Invalid URI parameters"
"grand_spry_goldfish_embrace": "Invalid URI parameters",
"honest_factual_carp_aspire": "Are you sure you want to delete this emoji?",
"flat_safe_haddock_gaze": "Upload",
"orange_weird_parakeet_hug": "Image must be less than {count} bytes",
"solid_inclusive_owl_hug": "Shortcode must be less than {count} characters",
"key_ago_hound_emerge": "Description must be less than {count} characters",
"pink_sharp_carp_work": "Global emoji",
"dark_pretty_hyena_link": "Can be used by every user, not just you",
"home_cool_orangutan_hug": "Category must be less than {count} characters",
"sound_topical_gopher_offer": "Required",
"watery_left_shrimp_bless": "Description",
"weird_fun_jurgen_arise": "Useful for people with screen readers, or poor network conditions.",
"short_cute_jackdaw_comfort": "Category",
"happy_mild_fox_gleam": "Shortcode",
"glad_day_kestrel_amaze": "Should be short.",
"active_direct_bear_compose": "Image",
"lime_late_millipede_urge": "Recommended size: 128x128px. Every image type is allowed.",
"factual_gray_mouse_believe": "Uploading emoji...",
"cool_trite_gull_quiz": "Emoji uploaded!",
"kind_deft_myna_hint": "Failed to upload emoji.",
"frail_great_marten_pet": "Upload a new emoji to the server.",
"whole_icy_puffin_smile": "Upload Emoji"
}

View file

@ -299,7 +299,27 @@
"keen_aware_goldfish_thrive": "Anglais",
"vivid_mellow_sawfish_approve": "Français",
"these_awful_ape_reside": "Pirate",
"dirty_inclusive_meerkat_nudge": "Annuler",
"gray_clean_shark_comfort": "Les paramètres de l'URI suivants sont obligatoires:",
"grand_spry_goldfish_embrace": "Paramètres URI non valides"
"grand_spry_goldfish_embrace": "Paramètres URI non valides",
"honest_factual_carp_aspire": "Etes-vous sûr de vouloir supprimer cet emoji ?",
"flat_safe_haddock_gaze": "Ajouter",
"orange_weird_parakeet_hug": "L'image doit être inférieure à {count} octets",
"solid_inclusive_owl_hug": "Le nom doit contenir moins de {count} caractères",
"key_ago_hound_emerge": "La description doit comporter moins de {count} caractères",
"pink_sharp_carp_work": "Émoji global",
"dark_pretty_hyena_link": "Peut être utilisé par tous les utilisateurs, pas juste vous",
"home_cool_orangutan_hug": "La catégorie doit comporter moins de {count} caractères",
"sound_topical_gopher_offer": "Requis",
"watery_left_shrimp_bless": "Description",
"weird_fun_jurgen_arise": "Utile pour les personnes disposant de lecteurs d'écran ou de mauvaises conditions de réseau.",
"short_cute_jackdaw_comfort": "Catégorie",
"happy_mild_fox_gleam": "Nom",
"active_direct_bear_compose": "Image",
"lime_late_millipede_urge": "Taille recommandée : 128x128px. Tous les types d'images sont autorisés.",
"dirty_inclusive_meerkat_nudge": "Annuler",
"factual_gray_mouse_believe": "Ajout de l'emoji...",
"cool_trite_gull_quiz": "Emoji ajouté!",
"kind_deft_myna_hint": "Échec de l'ajout de l'emoji.",
"frail_great_marten_pet": "Ajoutez un nouvel emoji sur le serveur.",
"whole_icy_puffin_smile": "Ajouter un Emoji"
}

View file

@ -4,7 +4,7 @@
<h1 class="scroll-m-20 text-3xl font-extrabold tracking-tight lg:text-4xl capitalize">
{{ m.tasty_late_termite_sew() }}
</h1>
<Button class="ml-auto" v-if="profileEditor?.dirty" @click="profileEditor.submitForm">Save</Button>
<Button v-if="profileEditor?.dirty" @click="profileEditor.submitForm">Save</Button>
</div>
<div class="grid xl:grid-cols-[1fr,auto] gap-4 *:max-h-[80vh]">
<ProfileEditor ref="profileEditor" />

View file

@ -1,8 +1,15 @@
<template>
<div class="md:px-8 px-4 py-2 max-w-7xl mx-auto w-full space-y-6">
<h1 class="scroll-m-20 text-3xl font-extrabold tracking-tight lg:text-4xl capitalize">
{{ m.suave_smart_mantis_climb() }}
</h1>
<div :class="cn('grid gap-2', canUpload && 'grid-cols-[1fr,auto]')">
<h1 class="scroll-m-20 text-3xl font-extrabold tracking-tight lg:text-4xl capitalize">
{{ m.suave_smart_mantis_climb() }}
</h1>
<Uploader v-if="canUpload">
<Button variant="default">
<Upload /> Upload
</Button>
</Uploader>
</div>
<div v-if="emojis.length > 0" class="max-w-sm w-full relative">
<Input v-model="search" placeholder="Search" class="pl-8" />
<Search class="absolute size-4 top-1/2 left-2.5 transform -translate-y-1/2" />
@ -21,9 +28,11 @@
</template>
<script lang="ts" setup>
import type { Emoji } from "@versia/client/types";
import { Search } from "lucide-vue-next";
import { cn } from "@/lib/utils";
import { type Emoji, RolePermission } from "@versia/client/types";
import { Search, Upload } from "lucide-vue-next";
import Category from "~/components/preferences/emojis/category.vue";
import Uploader from "~/components/preferences/emojis/uploader.vue";
import {
Card,
CardDescription,
@ -47,6 +56,13 @@ definePageMeta({
requiresAuth: true,
});
const permissions = usePermissions();
const canUpload = computed(
() =>
permissions.value.includes(RolePermission.ManageOwnEmojis) ||
permissions.value.includes(RolePermission.ManageEmojis),
);
const emojis = computed(
() =>
identity.value?.emojis?.filter((emoji) =>

View file

@ -1,6 +1,7 @@
import {
caseInsensitive,
char,
charIn,
createRegExp,
digit,
exactly,
@ -12,7 +13,7 @@ import {
export const emojiValidator = createRegExp(
// A-Z a-z 0-9 _ -
oneOrMore(letter.or(digit).or(exactly("_")).or(exactly("-"))),
oneOrMore(letter.or(digit).or(charIn("_-"))),
[caseInsensitive, global],
);