Add custom emojis presentation

Signed-off-by: Anton Alexeyev <alexeyev.anton@gmail.com>
This commit is contained in:
Anton Alexeyev 2025-05-02 22:02:54 +07:00
parent c8f61385ec
commit 14f5252d7f
14 changed files with 121 additions and 45 deletions

View File

@ -80,7 +80,15 @@
{:else if node.type === MarkupNodeType.text}
{node.text}
{:else if node.type === MarkupNodeType.emoji}
<span class="emoji" class:emojiOnly={single}>{node.attrs?.emoji}</span>
<span class="emoji" class:emojiOnly={single}>
{#if node.attrs?.kind === 'custom'}
{@const src = toString(attrs.url)}
{@const alt = toString(attrs.emoji)}
<img {src} {alt}/>
{:else}
{node.attrs?.emoji}
{/if}
</span>
{:else if node.type === MarkupNodeType.paragraph}
<p class="p-inline contrast" class:overflow-label={preview} class:emojiOnly={checkEmoji(nodes)}>
{#if nodes.length > 0}

View File

@ -18,13 +18,15 @@ import { Node, mergeAttributes } from '@tiptap/core'
declare module '@tiptap/core' {
interface Commands<ReturnType> {
emoji: {
insertEmoji: (emoji: string) => ReturnType
insertEmoji: (emoji: string, kind: 'unicode' | 'custom', url?: string) => ReturnType
}
}
}
export interface EmojiNodeOptions {
emoji: string
kind: 'unicode' | 'custom'
url?: string
}
export const EmojiNode = Node.create<EmojiNodeOptions>({
@ -38,6 +40,12 @@ export const EmojiNode = Node.create<EmojiNodeOptions>({
return {
emoji: {
default: ''
},
kind: {
default: 'unicode'
},
url: {
default: null
}
}
},
@ -45,11 +53,11 @@ export const EmojiNode = Node.create<EmojiNodeOptions>({
addCommands () {
return {
insertEmoji:
(emoji: string) =>
(emoji: string, kind: 'unicode' | 'custom', url?: string) =>
({ commands }) => {
return commands.insertContent({
type: this.name,
attrs: { emoji }
attrs: { emoji, kind, url }
})
}
}
@ -64,6 +72,28 @@ export const EmojiNode = Node.create<EmojiNodeOptions>({
},
renderHTML ({ node, HTMLAttributes }) {
if (node.attrs.kind === 'custom') {
return [
'span',
mergeAttributes(
{
'data-type': this.name,
class: 'emoji'
},
HTMLAttributes
),
[
'img',
mergeAttributes(
{
'data-type': this.name,
src: node.attrs.url,
alt: node.attrs.emoji
}
)
]
]
}
return [
'span',
mergeAttributes(

View File

@ -854,3 +854,13 @@
padding-top: 0;
.message.outcoming { border-radius: 0.75rem 0.125rem 0.125rem 0.75rem; }
}
.emoji > img {
display: inline-block;
vertical-align: sub;
height: 1.3em;
width: auto;
margin: 0 0.05em 0 0.1em;
padding: 0;
line-height: 1;
}

View File

@ -77,8 +77,8 @@
insertText: (text) => {
editor?.insertText(text)
},
insertEmoji: (emoji) => {
editor?.insertEmoji(emoji)
insertEmoji: (text: string, url?: string) => {
editor?.insertEmoji(text, url)
},
insertMarkup: (markup) => {
editor?.insertMarkup(markup)

View File

@ -41,7 +41,11 @@
dispatch('select', displayedEmoji)
}}
>
<span>{isCustomEmoji(displayedEmoji) ? displayedEmoji.shortcode : displayedEmoji.emoji}</span>
{#if isCustomEmoji(displayedEmoji)}
<span><img src="{displayedEmoji.url}" alt="{displayedEmoji.shortcode}"></span>
{:else}
<span>{displayedEmoji.emoji}</span>
{/if}
</button>
{/if}
@ -74,6 +78,10 @@
transform: translateY(1%);
pointer-events: none;
}
span > img {
height: 1em;
width: auto;
}
&:enabled:hover {
background-color: var(--theme-popup-hover);
}

View File

@ -102,9 +102,8 @@
selected = isCustomEmoji(emoji) ? emoji.shortcode : emoji.emoji
addFrequentlyEmojis(emoji)
dispatch('close', {
// TODO: send ExtendedEmoji
emoji: selected
// codes: emoji.hexcode.split('-').map((hc) => parseInt(hc, 16))
text: selected,
url: isCustomEmoji(emoji) ? emoji.url : undefined
})
}

View File

@ -69,6 +69,13 @@ export async function loadEmojis (lang?: string): Promise<EmojiWithGroup[]> {
export async function updateEmojis (lang?: string): Promise<void> {
const emojis = await loadEmojis(lang)
emojis.push({
shortcode: 'huly',
label: 'huly',
url: 'https://fonts.gstatic.com/s/e/notoemoji/latest/1f979/512.gif',
tags: ['huly'],
key: 'flags'
})
emojiStore.set(emojis)
}
@ -86,13 +93,15 @@ export function getEmojiByEmoticon (emoticon: string | undefined): string | unde
export function getUnicodeEmojiByShortCode (shortcode: string | undefined, skinTone?: number): Emoji | undefined {
const emoji = getEmojiByShortCode(shortcode, skinTone)
if (emoji === undefined || isCustomEmoji(emoji)) return undefined
return emoji
}
export function getEmojiByShortCode (shortcode: string | undefined, skinTone?: number): ExtendedEmoji | undefined {
if (shortcode === undefined) return undefined
const pureShortcode = shortcode.replaceAll(':', '')
return findEmoji((e) => {
if (isCustomEmoji(e)) return e.shortcode === shortcode
return e.shortcodes?.includes(shortcode.replaceAll(':', ''))
if (isCustomEmoji(e)) return e.shortcode === pureShortcode
return e.shortcodes?.includes(pureShortcode)
}, skinTone)
}

View File

@ -178,8 +178,8 @@
insertText: (text) => {
editor?.commands.insertContent(text)
},
insertEmoji: (emoji) => {
editor?.commands.insertEmoji(emoji)
insertEmoji: (text: string, url?: string) => {
editor?.commands.insertEmoji(text, url === undefined ? 'unicode' : 'custom')
},
insertMarkup: (markup) => {
editor?.commands.insertContent(markupToJSON(markup))

View File

@ -84,8 +84,8 @@
insertText: (text) => {
editor?.insertText(text)
},
insertEmoji: (emoji) => {
editor?.insertEmoji(emoji)
insertEmoji: (text: string, url?: string) => {
editor?.insertEmoji(text, url)
},
insertMarkup: (markup) => {
editor?.insertMarkup(markup)

View File

@ -79,8 +79,8 @@
insertText: (text) => {
editor?.insertText(text)
},
insertEmoji: (emoji) => {
editor?.insertEmoji(emoji)
insertEmoji: (text: string, url?: string) => {
editor?.insertEmoji(text, url)
},
insertMarkup: (markup) => {
editor?.insertMarkup(markup)

View File

@ -96,8 +96,8 @@
return editor
}
export function insertEmoji (emoji: string): void {
editor?.commands.insertEmoji(emoji)
export function insertEmoji (text: string, url?: string): void {
editor?.commands.insertEmoji(text, url === undefined ? 'unicode' : 'custom', url)
}
export function insertText (text: string): void {

View File

@ -27,7 +27,7 @@ export const defaultRefActions: RefAction[] = [
if (emoji === null || emoji === undefined) {
return
}
editorHandler.insertEmoji(emoji.emoji)
editorHandler.insertEmoji(emoji.text, emoji.url)
editorHandler.focus()
},
() => {}

View File

@ -37,16 +37,28 @@ function handleEmoji (
}
const emoji = getEmojiFunction(match.pop())
if (emoji === undefined) return
// TODO: add custom emoji
const unicodeEmoji = typeof emoji === 'string' ? emoji : (isCustomEmoji(emoji) ? emoji.shortcode : emoji.emoji)
if (typeof emoji === 'string' || !isCustomEmoji(emoji)) {
commands.insertContentAt(range, [
{
type: 'emoji',
attrs: {
emoji: unicodeEmoji
emoji: typeof emoji === 'string' ? emoji : emoji.emoji,
kind: 'unicode'
}
}
])
} else {
commands.insertContentAt(range, [
{
type: 'emoji',
attrs: {
emoji: emoji.shortcode,
kind: 'custom',
url: emoji.url
}
}
])
}
}
export const EmojiExtension = EmojiNode.extend<EmojiNodeOptions>({

View File

@ -16,7 +16,7 @@ export type CollaboratorType = 'local' | 'cloud'
*/
export interface TextEditorHandler {
insertText: (html: string) => void
insertEmoji: (emoji: string) => void
insertEmoji: (text: string, url?: string) => void
insertMarkup: (markup: Markup) => void
insertTemplate: (name: string, markup: string) => void
insertTable: (options: { rows?: number, cols?: number, withHeaderRow?: boolean }) => void