mirror of
https://github.com/versia-pub/frontend.git
synced 2025-12-06 08:28:20 +01:00
feat: ✨ Implement custom emojis
This commit is contained in:
parent
bb5de77bb1
commit
348b1ba2b0
|
|
@ -3,7 +3,7 @@
|
|||
maxHeight: collapsed ? '18rem' : `${container?.scrollHeight}px`
|
||||
}">
|
||||
<div :class="['prose prose-sm block relative dark:prose-invert duration-200 !max-w-full break-words prose-a:no-underline prose-a:hover:underline', $style.content]"
|
||||
v-html="content">
|
||||
v-html="content" v-render-emojis="emojis">
|
||||
</div>
|
||||
<div v-if="isOverflowing && collapsed"
|
||||
class="absolute inset-x-0 bottom-0 h-36 bg-gradient-to-t from-black/5 to-transparent rounded-b">
|
||||
|
|
@ -24,7 +24,7 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Attachment, Status } from "@versia/client/types";
|
||||
import type { Attachment, Emoji, Status } from "@versia/client/types";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import Attachments from "./attachments.vue";
|
||||
import Note from "./note.vue";
|
||||
|
|
@ -33,6 +33,7 @@ const { content, plainContent } = defineProps<{
|
|||
plainContent?: string;
|
||||
content: string;
|
||||
quote?: NonNullable<Status["quote"]>;
|
||||
emojis: Emoji[];
|
||||
attachments: Attachment[];
|
||||
}>();
|
||||
const container = ref<HTMLDivElement | null>(null);
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
</Avatar>
|
||||
</NuxtLink>
|
||||
<div :class="cn('flex flex-col gap-0.5 justify-center flex-1 text-left leading-tight', smallLayout && 'text-sm')">
|
||||
<span class="truncate font-semibold">{{
|
||||
<span class="truncate font-semibold" v-render-emojis="emojis">{{
|
||||
displayName
|
||||
}}</span>
|
||||
<span class="truncate text-sm tracking-tight">
|
||||
|
|
@ -36,7 +36,7 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { StatusVisibility } from "@versia/client/types";
|
||||
import type { Emoji, StatusVisibility } from "@versia/client/types";
|
||||
import type {
|
||||
UseTimeAgoMessages,
|
||||
UseTimeAgoUnitNamesDefault,
|
||||
|
|
@ -49,6 +49,7 @@ const { acct, createdAt, url } = defineProps<{
|
|||
cornerAvatar?: string;
|
||||
acct: string;
|
||||
displayName: string;
|
||||
emojis: Emoji[];
|
||||
visibility: StatusVisibility;
|
||||
url: string;
|
||||
createdAt: Date;
|
||||
|
|
|
|||
|
|
@ -2,14 +2,14 @@
|
|||
<Card as="article" class="rounded-none border-0 duration-200 shadow- max-w-full">
|
||||
<CardHeader class="pb-4" as="header">
|
||||
<ReblogHeader v-if="note.reblog" :avatar="note.account.avatar" :display-name="note.account.display_name"
|
||||
:url="reblogAccountUrl" />
|
||||
:url="reblogAccountUrl" :emojis="note.account.emojis" />
|
||||
<Header :avatar="noteToUse.account.avatar" :corner-avatar="note.reblog ? note.account.avatar : undefined"
|
||||
:acct="noteToUse.account.acct" :display-name="noteToUse.account.display_name"
|
||||
:visibility="noteToUse.visibility" :url="accountUrl" :created-at="new Date(noteToUse.created_at)"
|
||||
:small-layout="smallLayout" />
|
||||
:small-layout="smallLayout" :emojis="noteToUse.account.emojis" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Content :content="noteToUse.content" :quote="note.quote ?? undefined" :attachments="noteToUse.media_attachments" :plain-content="noteToUse.plain_content ?? undefined"/>
|
||||
<Content :content="noteToUse.content" :quote="note.quote ?? undefined" :attachments="noteToUse.media_attachments" :plain-content="noteToUse.plain_content ?? undefined" :emojis="noteToUse.emojis" />
|
||||
</CardContent>
|
||||
<CardFooter v-if="!hideActions" class="p-4 pt-0">
|
||||
<Actions :reply-count="noteToUse.replies_count" :like-count="noteToUse.favourites_count" :url="url"
|
||||
|
|
|
|||
|
|
@ -5,18 +5,20 @@
|
|||
<AvatarImage :src="avatar" alt="" />
|
||||
<AvatarFallback class="rounded-lg"> AA </AvatarFallback>
|
||||
</Avatar>
|
||||
<span class="font-semibold">{{ displayName }}</span>
|
||||
<span class="font-semibold" v-render-emojis="emojis">{{ displayName }}</span>
|
||||
reblogged
|
||||
</NuxtLink>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import type { Emoji } from "@versia/client/types";
|
||||
import { Repeat } from "lucide-vue-next";
|
||||
|
||||
const { url } = defineProps<{
|
||||
avatar: string;
|
||||
displayName: string;
|
||||
emojis: Emoji[];
|
||||
url: string;
|
||||
}>();
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
</Avatar>
|
||||
</NuxtLink>
|
||||
<div class="flex flex-col gap-0.5 justify-center flex-1 text-left leading-tight text-sm">
|
||||
<span class="truncate font-semibold">{{
|
||||
<span class="truncate font-semibold" v-render-emojis="follower.emojis">{{
|
||||
follower.display_name
|
||||
}}</span>
|
||||
<span class="truncate tracking-tight">
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
<AvatarImage :src="notification.account.avatar" alt="" />
|
||||
<AvatarFallback> AA </AvatarFallback>
|
||||
</Avatar>
|
||||
<span class="font-semibold">{{
|
||||
<span class="font-semibold" v-render-emojis="notification.account.emojis">{{
|
||||
notification.account.display_name
|
||||
}}</span>
|
||||
<CollapsibleTrigger :as-child="true">
|
||||
|
|
|
|||
|
|
@ -1,11 +1,14 @@
|
|||
<template>
|
||||
<div :class="['prose prose-sm block relative dark:prose-invert duration-200 !max-w-full break-words prose-a:no-underline prose-a:hover:underline', $style.content]" v-html="content">
|
||||
<div :class="['prose prose-sm block relative dark:prose-invert duration-200 !max-w-full break-words prose-a:no-underline prose-a:hover:underline', $style.content]" v-html="content" v-render-emojis="emojis">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Emoji } from "@versia/client/types";
|
||||
|
||||
const { content } = defineProps<{
|
||||
content: string;
|
||||
emojis: Emoji[];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,16 +1,17 @@
|
|||
<template>
|
||||
<div class="flex flex-col gap-y-4">
|
||||
<div v-for="field in fields" :key="field.name" class="flex flex-col gap-1">
|
||||
<h3 class="font-semibold text-sm">{{ field.name }}</h3>
|
||||
<div v-html="field.value" class="prose prose-sm prose-zinc dark:prose-invert"></div>
|
||||
<h3 class="font-semibold text-sm" v-render-emojis="emojis">{{ field.name }}</h3>
|
||||
<div v-html="field.value" class="prose prose-sm prose-zinc dark:prose-invert" v-render-emojis="emojis"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Field } from "@versia/client/types";
|
||||
import type { Emoji, Field } from "@versia/client/types";
|
||||
|
||||
defineProps<{
|
||||
fields: Field[];
|
||||
emojis: Emoji[];
|
||||
}>();
|
||||
</script>
|
||||
|
|
@ -17,7 +17,7 @@
|
|||
</ProfileActions>
|
||||
</div>
|
||||
<div class="flex flex-col -mt-1 gap-2 justify-center">
|
||||
<CardTitle class="">
|
||||
<CardTitle class="" v-render-emojis="account.emojis">
|
||||
{{ account.display_name }}
|
||||
</CardTitle>
|
||||
<CopyableText :text="account.acct">
|
||||
|
|
@ -36,13 +36,13 @@
|
|||
<ProfileBadge v-for="role in roles" :key="role.id" :name="role.name" :description="role.description"
|
||||
:icon="role.icon" />
|
||||
</div>
|
||||
<ProfileContent :content="account.note" />
|
||||
<ProfileContent :content="account.note" :emojis="account.emojis" />
|
||||
</CardContent>
|
||||
<CardFooter class="flex-col items-start gap-4">
|
||||
<ProfileStats :creation-date="new Date(account.created_at || 0)" :follower-count="account.followers_count"
|
||||
:following-count="account.following_count" :note-count="account.statuses_count" />
|
||||
<Separator v-if="account.fields.length > 0" />
|
||||
<ProfileFields v-if="account.fields.length > 0" :fields="account.fields" />
|
||||
<ProfileFields v-if="account.fields.length > 0" :fields="account.fields" :emojis="account.emojis" />
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -199,7 +199,7 @@ const instance = useInstance();
|
|||
<AvatarFallback class="rounded-lg"> AA </AvatarFallback>
|
||||
</Avatar>
|
||||
<div class="grid flex-1 text-left text-sm leading-tight">
|
||||
<span class="truncate font-semibold">{{
|
||||
<span class="truncate font-semibold" v-render-emojis="identity?.account.emojis">{{
|
||||
identity?.account.display_name
|
||||
}}</span>
|
||||
<span class="truncate text-xs">@{{ identity?.account.acct }}</span>
|
||||
|
|
@ -216,7 +216,7 @@ const instance = useInstance();
|
|||
<AvatarFallback class="rounded-lg"> AA </AvatarFallback>
|
||||
</Avatar>
|
||||
<div class="grid flex-1 text-left text-sm leading-tight">
|
||||
<span class="truncate font-semibold">{{
|
||||
<span class="truncate font-semibold" v-render-emojis="identity?.account.emojis">{{
|
||||
identity?.account.display_name
|
||||
}}</span>
|
||||
<span class="truncate text-xs">@{{
|
||||
|
|
|
|||
49
plugins/EmojiRenderer.ts
Normal file
49
plugins/EmojiRenderer.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import type { Emoji } from "@versia/client/types";
|
||||
import { SettingIds } from "~/settings";
|
||||
|
||||
const emojisRegex =
|
||||
/\p{RI}\p{RI}|\p{Emoji}(\p{EMod}|\uFE0F\u20E3?|[\u{E0020}-\u{E007E}]+\u{E007F})?(\u200D(\p{RI}\p{RI}|\p{Emoji}(\p{EMod}|\uFE0F\u20E3?|[\u{E0020}-\u{E007E}]+\u{E007F})?))*/gu;
|
||||
const incorrectEmojisRegex = /^[#*0-9©®]$/;
|
||||
|
||||
export default defineNuxtPlugin((nuxtApp) => {
|
||||
nuxtApp.vueApp.directive<HTMLElement, Emoji[]>("render-emojis", {
|
||||
beforeMount(el, binding) {
|
||||
const shouldRenderEmoji = useSetting(SettingIds.CustomEmojis);
|
||||
const emojiFont = useSetting(SettingIds.EmojiTheme);
|
||||
|
||||
// Replace emoji shortcodes with images
|
||||
if (shouldRenderEmoji.value.value) {
|
||||
el.innerHTML = el.innerHTML.replace(
|
||||
/:([a-zA-Z0-9_-]+):/g,
|
||||
(match, emoji) => {
|
||||
const emojiData = binding.value.find(
|
||||
(e) => e.shortcode === emoji,
|
||||
);
|
||||
|
||||
if (!emojiData) {
|
||||
return match;
|
||||
}
|
||||
|
||||
const image = document.createElement("img");
|
||||
image.src = emojiData.url;
|
||||
image.alt = `:${emoji}:`;
|
||||
image.title = emojiData.shortcode;
|
||||
image.className =
|
||||
"h-[1lh] align-middle inline not-prose hover:scale-110 transition-transform duration-75 ease-in-out";
|
||||
return image.outerHTML;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (emojiFont.value.value !== "native") {
|
||||
el.innerHTML = el.innerHTML.replace(emojisRegex, (match) => {
|
||||
if (incorrectEmojisRegex.test(match)) {
|
||||
return match;
|
||||
}
|
||||
|
||||
return `<img src="/emojis/${emojiFont}/${match}.svg" alt="${match}" class="h-[1em] inline not-prose hover:scale-110 transition-transform duration-75 ease-in-out">`;
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
@ -123,7 +123,7 @@ export const settings: Record<SettingIds, Setting> = {
|
|||
} as EnumSetting,
|
||||
[SettingIds.CustomEmojis]: {
|
||||
title: "Render Custom Emojis",
|
||||
description: "Render custom emojis.",
|
||||
description: "Render custom emojis. Requires a page reload to apply.",
|
||||
type: SettingType.Boolean,
|
||||
value: true,
|
||||
page: SettingPages.Behaviour,
|
||||
|
|
|
|||
Loading…
Reference in a new issue