Extend emojis with Custom type

Signed-off-by: Anton Alexeyev <alexeyev.anton@gmail.com>
This commit is contained in:
Anton Alexeyev 2025-05-02 12:25:46 +07:00
parent 7df8a2c779
commit c8f61385ec
10 changed files with 90 additions and 59 deletions

View File

@ -5,34 +5,34 @@
//
import { createEventDispatcher } from 'svelte'
import type { Emoji } from 'emojibase'
import { EmojiWithGroup, getEmojiByHexcode } from '.'
import { EmojiButton, getSkinTone, emojiStore } from '.'
import { ExtendedEmoji, getEmojiByHexcode, EmojiButton, getSkinTone, emojiStore, getEmojiSkins } from '.'
import { Label, IconDelete, closeTooltip, ButtonBase } from '../..'
import plugin from '../../plugin'
import SkinToneTooltip from './SkinToneTooltip.svelte'
export let emoji: EmojiWithGroup
export let emoji: ExtendedEmoji
export let remove: boolean = false
export let skinTone: number = getSkinTone()
const dispatch = createEventDispatcher()
closeTooltip()
const haveSkins = Array.isArray(emoji.skins)
const combinedEmoji = haveSkins && (emoji?.skins?.length ?? 0) > 5
const skins = getEmojiSkins(emoji)
const haveSkins = Array.isArray(skins)
const combinedEmoji = haveSkins && (skins?.length ?? 0) > 5
const clickRemove = (): void => {
dispatch('close', 'remove')
}
const getEmojiParts = (e: EmojiWithGroup): EmojiWithGroup[] => {
const getEmojiParts = (): ExtendedEmoji[] => {
const def = $emojiStore[168]
const temp = e.skins?.find((skin) => Array.isArray(skin.tone) && skin.tone.length > 1)?.hexcode.split('-200D-')
const temp = skins?.find((skin) => Array.isArray(skin.tone) && skin.tone.length > 1)?.hexcode.split('-200D-')
if (temp === undefined || temp.length < 2) return [def, def]
const firstEmoji = getEmojiByHexcode(temp[0].slice(0, -6)) ?? def
const secondEmoji = getEmojiByHexcode(temp[temp.length - 1].slice(0, -6)) ?? def
return [firstEmoji, secondEmoji]
}
const emojiParts = getEmojiParts(emoji)
const emojiParts = getEmojiParts()
const combinedTones: number[] = [skinTone, skinTone]
const updateSkinTone = (result: CustomEvent<{ detail: number }>, index: number): void => {
@ -45,16 +45,16 @@
}
}
const getEmojiByTone = (e: EmojiWithGroup, [a, b]: number[]): Emoji | undefined => {
const getEmojiByTone = (e: ExtendedEmoji, [a, b]: number[]): Emoji | undefined => {
const equal = a === b
const noTone = a === 0
return equal && noTone
? e
: e.skins?.find((skin) =>
? e as Emoji
: getEmojiSkins(e)?.find((skin) =>
equal ? skin.tone === a : Array.isArray(skin.tone) && skin.tone[0] === a && skin.tone[1] === b
)
}
const getEmojiStringByTone = (e: EmojiWithGroup, [a, b]: number[]): string | undefined => {
const getEmojiStringByTone = (e: ExtendedEmoji, [a, b]: number[]): string | undefined => {
return getEmojiByTone(e, [a, b])?.emoji
}
</script>
@ -92,7 +92,7 @@
</div>
{:else}
<div class="hulyPopup-row disabled skins-row">
{#each new Array((emoji.skins?.length ?? 5) + 1) as _, skin}
{#each new Array((skins?.length ?? 5) + 1) as _, skin}
<EmojiButton {emoji} skinTone={skin} preview on:select={(result) => dispatch('close', result.detail)} />
{/each}
</div>

View File

@ -5,25 +5,23 @@
//
import { createEventDispatcher } from 'svelte'
import { getEmbeddedLabel } from '@hcengineering/platform'
import { tooltip, capitalizeFirstLetter, type EmojiWithGroup, type LabelAndProps } from '../../'
import { type Emoji } from 'emojibase'
import { tooltip, capitalizeFirstLetter, type LabelAndProps, type ExtendedEmoji, getEmojiSkins } from '../../'
import { isCustomEmoji } from './types'
export let emoji: EmojiWithGroup
export let emoji: ExtendedEmoji
export let selected: boolean = false
export let disabled: boolean = false
export let preview: boolean = false
export let skinTone: number = 0
export let showTooltip: LabelAndProps | undefined = undefined
const skins = getEmojiSkins(emoji)
const dispatch = createEventDispatcher()
const getSkinsCount = (e: EmojiWithGroup): number | undefined => {
return Array.isArray(e.skins) ? e.skins.length : undefined
}
let displayedEmoji: Emoji | EmojiWithGroup
$: skinIndex = emoji?.skins?.findIndex((skin) => skin.tone === skinTone) ?? -1
$: displayedEmoji = skinTone > 0 && Array.isArray(emoji.skins) && skinIndex > -1 ? emoji.skins[skinIndex] : emoji
let displayedEmoji: ExtendedEmoji
$: skinIndex = skins?.findIndex((skin) => skin.tone === skinTone) ?? -1
$: displayedEmoji = skinTone > 0 && skins !== undefined && skinIndex > -1 ? skins[skinIndex] : emoji
</script>
{#if emoji}
@ -32,9 +30,9 @@
class="hulyPopupEmoji-button"
class:preview
class:selected
class:skins={emoji?.skins !== undefined && emoji.skins.length === 5}
class:constructor={emoji?.skins !== undefined && emoji.skins.length > 5}
data-skins={getSkinsCount(emoji)}
class:skins={skins !== undefined && skins.length === 5}
class:constructor={skins !== undefined && skins.length > 5}
data-skins={skins?.length}
{disabled}
on:touchstart
on:contextmenu
@ -43,7 +41,7 @@
dispatch('select', displayedEmoji)
}}
>
<span>{displayedEmoji.emoji}</span>
<span>{isCustomEmoji(displayedEmoji) ? displayedEmoji.shortcode : displayedEmoji.emoji}</span>
</button>
{/if}

View File

@ -15,11 +15,12 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte'
import type { EmojiWithGroup } from '.'
import type { ExtendedEmoji } from '.'
import EmojiButton from './EmojiButton.svelte'
import { isCustomEmoji } from './types'
export let emojis: EmojiWithGroup[]
export let emojis: ExtendedEmoji[]
export let selected: string | undefined
export let disabled: boolean = false
export let skinTone: number = 0
@ -31,7 +32,7 @@
{#each emojis as emoji}
<EmojiButton
{emoji}
selected={emoji.emoji === selected}
selected={isCustomEmoji(emoji) ? emoji.shortcode === selected : emoji.emoji === selected}
{disabled}
{skinTone}
on:select

View File

@ -12,7 +12,7 @@
showPopup,
eventToHTMLElement,
ButtonBase,
closeTooltip, getEmojiByShortCode
closeTooltip, getEmojiByShortCode, getEmojiSkins, getUnicodeEmojiByShortCode
} from '../../'
import plugin from '../../plugin'
import {
@ -30,6 +30,7 @@
import SkinTonePopup from './SkinTonePopup.svelte'
import IconSearch from './icons/Search.svelte'
import EmojiGroup from './EmojiGroup.svelte'
import { isCustomEmoji } from './types'
export let embedded = false
export let selected: string | undefined
@ -98,11 +99,12 @@
}
const sendEmoji = (emoji: EmojiWithGroup): void => {
selected = emoji.emoji
selected = isCustomEmoji(emoji) ? emoji.shortcode : emoji.emoji
addFrequentlyEmojis(emoji)
dispatch('close', {
emoji: emoji.emoji,
codes: emoji.hexcode.split('-').map((hc) => parseInt(hc, 16))
// TODO: send ExtendedEmoji
emoji: selected
// codes: emoji.hexcode.split('-').map((hc) => parseInt(hc, 16))
})
}
@ -142,7 +144,8 @@
}
function handleContextMenu (event: MouseEvent, emoji: EmojiWithGroup, remove: boolean): void {
event.preventDefault()
if (Array.isArray(emoji.skins) || remove) openContextMenu(event, emoji, remove)
const skins = getEmojiSkins(emoji)
if (Array.isArray(skins) || remove) openContextMenu(event, emoji, remove)
}
const clearTimer = (): void => {
clearTimeout(timer)
@ -155,7 +158,8 @@
})
}
function clampedContextMenu (event: TouchEvent, emoji: EmojiWithGroup, remove: boolean): void {
if (timer == null && (Array.isArray(emoji?.skins) || remove)) {
const skins = getEmojiSkins(emoji)
if (timer == null && (Array.isArray(skins) || remove)) {
touchObserver()
timer = setTimeout(function () {
if (!shownContext) openContextMenu(event, emoji, remove)
@ -192,7 +196,7 @@
const tempEmojis: string[] = em.emojisString
const emojis: EmojiWithGroup[] = []
tempEmojis.forEach((te) => {
const e = $emojiStore.find((es) => es.hexcode === te)
const e = $emojiStore.find((es) => isCustomEmoji(es) ? es.shortcode === te : es.hexcode === te)
if (e !== undefined) emojis.push(e)
})
emojiCategories[index].emojis = emojis
@ -265,7 +269,7 @@
tooltip={{ label: plugin.string.DefaultSkinTone }}
on:click={showSkinMenu}
>
<span style:font-size={'1.5rem'}>{getEmojiByShortCode(':hand:', skinTone)?.emoji}</span>
<span style:font-size={'1.5rem'}>{getUnicodeEmojiByShortCode(':hand:', skinTone)?.emoji}</span>
</ButtonBase>
</div>
<Scroller

View File

@ -5,17 +5,17 @@
//
import { createEventDispatcher } from 'svelte'
import type { Emoji } from 'emojibase'
import { Label, closeTooltip, ModernCheckbox } from '../../'
import { Label, closeTooltip, ModernCheckbox, getEmojiSkins } from '../../'
import { skinTones } from '.'
import type { EmojiWithGroup } from '.'
export let emoji: EmojiWithGroup
export let emoji: Emoji
export let selected: number
const dispatch = createEventDispatcher()
closeTooltip()
const skins: Emoji[] = emoji.skins !== undefined ? [emoji, ...emoji.skins] : []
const emojiSkins = getEmojiSkins(emoji)
const skins: Emoji[] = emojiSkins !== undefined ? [emoji, ...emojiSkins] : []
</script>
<div class="hulyPopup-container noPadding">

View File

@ -4,16 +4,17 @@
// Licensed under the Eclipse Public License v2.0 (SPDX: EPL-2.0).
//
import { createEventDispatcher } from 'svelte'
import { type EmojiWithGroup } from '.'
import { getEmojiSkins } from '.'
import { ButtonBase, closeTooltip } from '../..'
import { Emoji } from 'emojibase'
export let emoji: EmojiWithGroup
export let emoji: Emoji
export let selected: number
const dispatch = createEventDispatcher()
const skins: Emoji[] = emoji.skins !== undefined ? [emoji, ...emoji.skins] : []
const emojiSkins = getEmojiSkins(emoji)
const skins: Emoji[] = emojiSkins !== undefined ? [emoji, ...emojiSkins] : []
</script>
<div class="flex-row-center flex-gap-1">

View File

@ -16,7 +16,7 @@ import type { IntlString } from '@hcengineering/platform'
import type { AnySvelteComponent } from '../..'
import type { Emoji } from 'emojibase'
export type ExtendedEmoji = Emoji // | CustomEmoji
export type ExtendedEmoji = Emoji | CustomEmoji
export type EmojiWithGroup = ExtendedEmoji & { key: string }
export interface CustomEmoji {
@ -35,6 +35,10 @@ export interface EmojiCategory {
emojis?: EmojiWithGroup[]
}
/* export function isCustomEmoji (emoji: EmojiWithGroup): emoji is CustomEmoji & { key: string } {
export function isCustomEmoji (emoji: ExtendedEmoji): emoji is CustomEmoji {
return 'url' in emoji
}*/
}
/* export function isCustomEmoji (emoji: ExtendedEmoji): boolean {
return false
} */

View File

@ -19,7 +19,7 @@ import EMOJI_REGEX from 'emojibase-regex'
import EMOTICON_REGEX from 'emojibase-regex/emoticon'
import SHORTCODE_REGEX from 'emojibase-regex/shortcode'
import { getCurrentAccount } from '@hcengineering/core'
import { deviceOptionsStore as deviceInfo, type ExtendedEmoji } from '../..'
import { deviceOptionsStore as deviceInfo, type ExtendedEmoji, isCustomEmoji } from '../..'
import type { EmojiWithGroup } from '.'
import { emojiCategories, emojiStore } from '.'
@ -73,30 +73,41 @@ export async function updateEmojis (lang?: string): Promise<void> {
}
export function getEmojiByHexcode (hexcode: string): EmojiWithGroup | undefined {
return get(emojiStore).find((e) => e.hexcode === hexcode)
return get(emojiStore).find((e) => !isCustomEmoji(e) && e.hexcode === hexcode)
}
export function getEmojiByEmoticon (emoticon: string | undefined): string | undefined {
if (emoticon === undefined) return undefined
return findEmoji(e => Array.isArray(e.emoticon) ? e.emoticon.includes(emoticon) : e.emoticon === emoticon)?.emoji
const matchEmoji = findEmoji(e => !isCustomEmoji(e) && (Array.isArray(e.emoticon) ? e.emoticon.includes(emoticon) : e.emoticon === emoticon))
if (matchEmoji === undefined) return undefined
return !isCustomEmoji(matchEmoji) ? matchEmoji.emoji : undefined
}
export function getUnicodeEmojiByShortCode (shortcode: string | undefined, skinTone?: number): Emoji | undefined {
const emoji = getEmojiByShortCode(shortcode, skinTone)
if (emoji === undefined || isCustomEmoji(emoji)) return undefined
}
export function getEmojiByShortCode (shortcode: string | undefined, skinTone?: number): ExtendedEmoji | undefined {
if (shortcode === undefined) return undefined
return findEmoji((e) => e.shortcodes?.includes(shortcode.replaceAll(':', '')), skinTone)
return findEmoji((e) => {
if (isCustomEmoji(e)) return e.shortcode === shortcode
return e.shortcodes?.includes(shortcode.replaceAll(':', ''))
}, skinTone)
}
function findEmoji (predicate: (e: EmojiWithGroup) => boolean | undefined, skinTone?: number): ExtendedEmoji | undefined {
const emojis = get(emojiStore)
const matchEmoji = emojis.find(predicate)
if (matchEmoji === undefined) return undefined
if (isCustomEmoji(matchEmoji)) return matchEmoji
if (skinTone === undefined) skinTone = getSkinTone()
if (skinTone === 0 || matchEmoji.skins === undefined) return matchEmoji
return matchEmoji.skins[skinTone - 1]
}
export const removeFrequentlyEmojis = (emoji: EmojiWithGroup): void => {
const hexcode = emoji.hexcode
const hexcode = isCustomEmoji(emoji) ? emoji.shortcode : emoji.hexcode
if (hexcode === undefined) return
const frequentlyEmojisKey = getEmojisLocalStorageKey()
@ -113,7 +124,7 @@ export const removeFrequentlyEmojis = (emoji: EmojiWithGroup): void => {
}
export const addFrequentlyEmojis = (emoji: EmojiWithGroup): void => {
if (emoji === undefined) return
const hexcode = emoji.hexcode
const hexcode = isCustomEmoji(emoji) ? emoji.shortcode : emoji.hexcode
const frequentlyEmojisKey = getEmojisLocalStorageKey()
const frequentlyEmojis = window.localStorage.getItem(frequentlyEmojisKey)
@ -141,13 +152,23 @@ export const getFrequentlyEmojis = (): EmojiWithGroup[] | undefined => {
const parsedEmojis = JSON.parse(frequentlyEmojis)
if (!Array.isArray(parsedEmojis)) return undefined
const emojis = get(emojiStore)
return emojis.filter(e => parsedEmojis.find(pe => pe.hexcode === e.hexcode || e.skins?.find(s => s.hexcode === pe.hexcode) !== undefined) !== undefined)
return emojis.filter((e) => {
if (isCustomEmoji(e)) {
return parsedEmojis.find(pe => pe.hexcode === e.shortcode) !== undefined
}
return parsedEmojis.find(pe => pe.hexcode === e.hexcode || e.skins?.find(s => s.hexcode === pe.hexcode) !== undefined) !== undefined
})
} catch (e) {
console.error(e)
return undefined
}
}
export function getEmojiSkins (emoji: ExtendedEmoji): Emoji[] | undefined {
if (isCustomEmoji(emoji)) return undefined
return emoji.skins
}
export const setSkinTone = (skinTone: number): void => {
const skinToneKey = getEmojisLocalStorageKey('skinTone')
window.localStorage.setItem(skinToneKey, JSON.stringify(skinTone))

View File

@ -8,7 +8,7 @@ import {
getEventPositionElement,
showPopup,
type Location,
type EmojiWithGroup
type EmojiWithGroup, isCustomEmoji
} from '@hcengineering/ui'
import { type AttributeModel } from '@hcengineering/view'
import { get } from 'svelte/store'
@ -62,7 +62,8 @@ export async function addReactionAction (
closePopup()
showPopup(EmojiPopup, {}, element, (emoji: EmojiWithGroup) => {
if (emoji?.emoji !== undefined) void updateDocReactions(reactions, message, emoji.emoji)
// TODO: add custom emoji
if (!isCustomEmoji(emoji) && emoji?.emoji !== undefined) void updateDocReactions(reactions, message, emoji.emoji)
params?.onClose?.()
})
params?.onOpen?.()

View File

@ -8,7 +8,7 @@ import {
getEmojiByShortCode,
emojiRegex,
emojiGlobalRegex,
type ExtendedEmoji
type ExtendedEmoji, isCustomEmoji
} from '@hcengineering/ui'
import { type ResolvedPos } from '@tiptap/pm/model'
import { type ExtendedRegExpMatchArray, type SingleCommands, type Range, InputRule, PasteRule } from '@tiptap/core'
@ -37,7 +37,8 @@ function handleEmoji (
}
const emoji = getEmojiFunction(match.pop())
if (emoji === undefined) return
const unicodeEmoji = typeof emoji === 'string' ? emoji : emoji.emoji
// TODO: add custom emoji
const unicodeEmoji = typeof emoji === 'string' ? emoji : (isCustomEmoji(emoji) ? emoji.shortcode : emoji.emoji)
commands.insertContentAt(range, [
{
type: 'emoji',