Remove emoji from packages

Signed-off-by: Anton Alexeyev <alexeyev.anton@gmail.com>
This commit is contained in:
Anton Alexeyev 2025-05-07 11:43:40 +07:00
parent e34e6250a1
commit cc32979f8a
39 changed files with 85 additions and 1564 deletions

View File

@ -12,4 +12,3 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//

View File

@ -84,7 +84,7 @@
{#if node.attrs?.kind === 'custom'}
{@const src = toString(attrs.url)}
{@const alt = toString(attrs.emoji)}
<img {src} {alt}/>
<img {src} {alt} />
{:else}
{node.attrs?.emoji}
{/if}

View File

@ -84,13 +84,11 @@ export const EmojiNode = Node.create<EmojiNodeOptions>({
),
[
'img',
mergeAttributes(
{
'data-type': this.name,
src: node.attrs.url,
alt: node.attrs.emoji
}
)
mergeAttributes({
'data-type': this.name,
src: node.attrs.url,
alt: node.attrs.emoji
})
]
]
}

View File

@ -1,116 +0,0 @@
<script lang="ts">
//
// © 2025 Hardcore Engineering, Inc. All Rights Reserved.
// Licensed under the Eclipse Public License v2.0 (SPDX: EPL-2.0).
//
import { createEventDispatcher } from 'svelte'
import type { Emoji } from 'emojibase'
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: ExtendedEmoji
export let remove: boolean = false
export let skinTone: number = getSkinTone()
const dispatch = createEventDispatcher()
closeTooltip()
const skins = getEmojiSkins(emoji)
const haveSkins = Array.isArray(skins)
const combinedEmoji = haveSkins && (skins?.length ?? 0) > 5
const clickRemove = (): void => {
dispatch('close', 'remove')
}
const getEmojiParts = (): ExtendedEmoji[] => {
const def = $emojiStore[168]
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()
const combinedTones: number[] = [skinTone, skinTone]
const updateSkinTone = (result: CustomEvent<{ detail: number }>, index: number): void => {
const res = result.detail
if (typeof res === 'number') {
combinedTones[index] = res
const nextIndex = index === 1 ? 0 : 1
if (res === 0 && combinedTones[nextIndex] !== 0) combinedTones[nextIndex] = 0
else if (res !== 0 && combinedTones[nextIndex] === 0) combinedTones[nextIndex] = res
}
}
const getEmojiByTone = (e: ExtendedEmoji, [a, b]: number[]): Emoji | undefined => {
const equal = a === b
const noTone = a === 0
return equal && noTone
? 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: ExtendedEmoji, [a, b]: number[]): string | undefined => {
return getEmojiByTone(e, [a, b])?.emoji
}
</script>
<div class="hulyPopup-container noPadding autoWidth">
{#if haveSkins}
{#if combinedEmoji && emojiParts?.length === 2}
<div class="hulyPopup-row disabled skins-row">
{#each new Array(2) as _, index}
<EmojiButton
emoji={emojiParts[index]}
skinTone={combinedTones[index]}
preview
showTooltip={{
component: SkinToneTooltip,
props: { emoji: emojiParts[index], selected: combinedTones[index] },
onUpdate: (result) => {
updateSkinTone(result, index)
}
}}
/>
{#if index === 0}
<ButtonBase
type={'type-button-icon'}
kind={'tertiary'}
size={'large'}
on:click={() => {
dispatch('close', getEmojiByTone(emoji, combinedTones))
}}
>
<span style:font-size={'2.5rem'}>{getEmojiStringByTone(emoji, combinedTones)}</span>
</ButtonBase>
{/if}
{/each}
</div>
{:else}
<div class="hulyPopup-row disabled skins-row">
{#each new Array((skins?.length ?? 5) + 1) as _, skin}
<EmojiButton {emoji} skinTone={skin} preview on:select={(result) => dispatch('close', result.detail)} />
{/each}
</div>
{/if}
{/if}
{#if remove}
{#if haveSkins}<div class="hulyPopup-divider" />{/if}
<div class="hulyPopup-group">
<button class="hulyPopup-row" on:click={clickRemove}>
<div class="hulyPopup-row__icon red-color"><IconDelete size={'small'} /></div>
<span class="hulyPopup-row__label red-color"><Label label={plugin.string.Remove} /></span>
</button>
</div>
{/if}
</div>
<style lang="scss">
.skins-row {
align-items: center;
}
</style>

View File

@ -1,150 +0,0 @@
<script lang="ts">
//
// © 2025 Hardcore Engineering, Inc. All Rights Reserved.
// Licensed under the Eclipse Public License v2.0 (SPDX: EPL-2.0).
//
import { createEventDispatcher } from 'svelte'
import { getEmbeddedLabel } from '@hcengineering/platform'
import { tooltip, capitalizeFirstLetter, type LabelAndProps, type ExtendedEmoji, getEmojiSkins } from '../../'
import { isCustomEmoji } from './types'
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()
let displayedEmoji: ExtendedEmoji
$: skinIndex = skins?.findIndex((skin) => skin.tone === skinTone) ?? -1
$: displayedEmoji = skinTone > 0 && skins !== undefined && skinIndex > -1 ? skins[skinIndex] : emoji
</script>
{#if emoji}
<button
use:tooltip={showTooltip ?? { label: getEmbeddedLabel(capitalizeFirstLetter(displayedEmoji?.label ?? '')) }}
class="hulyPopupEmoji-button"
class:preview
class:selected
class:skins={skins !== undefined && skins.length === 5}
class:constructor={skins !== undefined && skins.length > 5}
data-skins={skins?.length}
{disabled}
on:touchstart
on:contextmenu
on:click={() => {
if (disabled) return
dispatch('select', displayedEmoji)
}}
>
{#if isCustomEmoji(displayedEmoji)}
<span><img src="{displayedEmoji.url}" alt="{displayedEmoji.shortcode}"></span>
{:else}
<span>{displayedEmoji.emoji}</span>
{/if}
</button>
{/if}
<style lang="scss">
.hulyPopupEmoji-button {
display: flex;
justify-content: center;
align-items: center;
flex-shrink: 0;
line-height: 150%;
border: 1px solid transparent;
&:not(.preview) {
margin: 0.125rem;
padding: 0.25rem;
width: 2.75rem;
height: 2.75rem;
font-size: 2rem;
border-radius: 0.75rem;
}
&.preview {
margin: 0;
padding: 0;
width: 2.25rem;
height: 2.25rem;
font-size: 1.75rem;
border-radius: 0.375rem;
}
span {
transform: translateY(1%);
pointer-events: none;
}
span > img {
height: 1em;
width: auto;
}
&:enabled:hover {
background-color: var(--theme-popup-hover);
}
&.selected {
border-color: var(--button-primary-BorderColor);
background-color: var(--button-primary-BackgroundColor);
&:not(.disabled, :disabled):hover {
background-color: var(--button-primary-hover-BackgroundColor);
}
}
:global(.mobile-theme) & {
width: 2rem;
height: 2rem;
font-size: 1.5rem;
border-radius: 0.25rem;
}
&.skins:not(.preview) {
position: relative;
border: 1px dashed var(--theme-button-border);
&:hover {
&::after {
content: '';
position: absolute;
top: -0.375rem;
right: -0.375rem;
width: 1rem;
height: 1rem;
border-radius: 50%;
background-image: url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='utf-8'%3F%3E%3C!-- Generator: Adobe Illustrator 28.4.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) --%3E%3Csvg version='1.1' id='Layer_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' viewBox='0 0 8 8' style='enable-background:new 0 0 8 8;' xml:space='preserve'%3E%3Cg%3E%3Ccircle fill='%23FFC92C' cx='5.3' cy='1.3' r='1.3' /%3E%3Ccircle fill='%23BF8F68' cx='2.7' cy='6' r='1.3' /%3E%3Ccircle fill='%23E0BB95' cx='5.3' cy='6' r='1.3' /%3E%3Ccircle fill='%239B643D' cx='1.3' cy='3.6' r='1.3' /%3E%3Ccircle fill='%23594539' cx='2.7' cy='1.3' r='1.3' /%3E%3Ccircle fill='%23FADCBC' cx='6.7' cy='3.6' r='1.3' /%3E%3C/g%3E%3C/svg%3E%0A");
}
}
}
&.constructor:not(.preview) {
position: relative;
border: 1px dashed var(--theme-button-border);
&:hover {
&::before {
content: '';
position: absolute;
top: -0.25rem;
right: -0.25rem;
width: 1rem;
height: 1rem;
border-radius: 50%;
background-color: var(--global-focus-BorderColor);
}
&::after {
content: attr(data-skins);
position: absolute;
top: 0.25rem;
right: 0.25rem;
font-size: 0.625rem;
font-weight: 700;
color: #fff;
transform: translate(50%, -50%);
}
}
}
}
</style>

View File

@ -1,105 +0,0 @@
<!--
// Copyright © 2025 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { Label, Lazy } from '../../index'
import EmojiGroupPalette from './EmojiGroupPalette.svelte'
import { resultEmojis, EmojiWithGroup, EmojiCategory } from '.'
import plugin from '../../plugin'
export let group: EmojiCategory
export let lazy: boolean = true
export let searching: boolean = false
export let disabled: boolean = false
export let selected: string | undefined = undefined
export let skinTone: number = 0
export let kind: 'default' | 'fade' = 'fade'
let emojis: EmojiWithGroup[] = []
$: emojis = searching
? $resultEmojis
: Array.isArray(group.emojis)
? group.emojis
: $resultEmojis.filter((re) => re.key === group.id)
</script>
<div class="hulyPopupEmoji-group kind-{kind}" class:lazy>
<div id={group.id} class="hulyPopupEmoji-group__header categoryHeader">
<Label label={searching && $resultEmojis.length === 0 ? plugin.string.NoResults : group.label} />
</div>
{#if lazy}
<Lazy>
<EmojiGroupPalette {emojis} {disabled} {selected} {skinTone} on:select on:touchstart on:contextmenu />
</Lazy>
{:else}
<EmojiGroupPalette {emojis} {disabled} {selected} {skinTone} on:select on:touchstart on:contextmenu />
{/if}
</div>
<style lang="scss">
.hulyPopupEmoji-group {
display: flex;
flex-direction: column;
flex-wrap: nowrap;
min-width: 0;
min-height: 0;
&.lazy {
min-height: 10rem;
}
&__header {
position: sticky;
flex-shrink: 0;
margin: 0.75rem 0.75rem 0.25rem;
padding: 0.25rem 0.375rem;
top: 0;
height: 1.5rem;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
text-shadow: 0 0 0.25rem var(--theme-popup-color);
color: var(--theme-caption-color);
border-radius: 0.25rem;
z-index: 1;
pointer-events: none;
&:first-child {
margin-top: 0;
}
&::before {
content: '';
position: absolute;
top: -1px;
left: 0;
width: 100%;
height: 150%;
background: var(--theme-popup-trans-gradient);
z-index: -1;
}
}
&.kind-default {
.hulyPopupEmoji-group__header {
background: var(--theme-popup-header);
&::before {
content: none;
}
}
}
}
</style>

View File

@ -1,59 +0,0 @@
<!--
// Copyright © 2025 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { createEventDispatcher } from 'svelte'
import type { ExtendedEmoji } from '.'
import EmojiButton from './EmojiButton.svelte'
import { isCustomEmoji } from './types'
export let emojis: ExtendedEmoji[]
export let selected: string | undefined
export let disabled: boolean = false
export let skinTone: number = 0
const dispatch = createEventDispatcher()
</script>
<div class="hulyPopupEmoji-group__palette">
{#each emojis as emoji}
<EmojiButton
{emoji}
selected={isCustomEmoji(emoji) ? emoji.shortcode === selected : emoji.emoji === selected}
{disabled}
{skinTone}
on:select
on:touchstart={(event) => {
dispatch('touchstart', { event, emoji })
}}
on:contextmenu={(event) => {
dispatch('contextmenu', { event, emoji })
}}
/>
{/each}
<div class="clear-mins flex-grow" />
</div>
<style lang="scss">
.hulyPopupEmoji-group__palette {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
flex-shrink: 0;
margin-inline: 0.75rem;
font-size: 1.25rem;
}
</style>

View File

@ -1,428 +0,0 @@
<script lang="ts">
//
// © 2025 Hardcore Engineering, Inc. All Rights Reserved.
// Licensed under the Eclipse Public License v2.0 (SPDX: EPL-2.0).
//
import { createEventDispatcher, onMount, onDestroy } from 'svelte'
import {
Scroller,
SearchInput,
tooltip,
deviceOptionsStore as deviceInfo,
showPopup,
eventToHTMLElement,
ButtonBase,
closeTooltip, getEmojiByShortCode, getEmojiSkins, getUnicodeEmojiByShortCode
} from '../../'
import plugin from '../../plugin'
import {
searchEmoji,
emojiStore,
emojiCategories,
getFrequentlyEmojis,
addFrequentlyEmojis,
removeFrequentlyEmojis,
getSkinTone,
setSkinTone
} from '.'
import type { EmojiWithGroup, EmojiCategory } from '.'
import ActionsPopup from './ActionsPopup.svelte'
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
export let disabled: boolean = false
export let kind: 'default' | 'fade' = 'fade'
const dispatch = createEventDispatcher()
let scrollElement: HTMLDivElement
const touchEvents = ['touchend', 'touchcancel', 'touchmove']
let skinTone: number = getSkinTone()
let shownSTM: boolean = false
let shownContext: boolean = false
$: emojiRowHeightPx = ($deviceInfo.fontSize ?? 16) * 2
$searchEmoji = ''
$: searching = $searchEmoji !== ''
const searchCategory: EmojiCategory[] = [
{
id: 'search-category',
label: plugin.string.SearchResults,
icon: IconSearch
}
]
let timer: any = null
const isMobile = $deviceInfo.isMobile
let emojisCat = emojiCategories
let currentCategory = emojisCat[0]
$: emojiTabs = emojisCat
.map((ec) => {
if (ec.categories !== undefined || ec.emojisString !== undefined || ec.emojis !== undefined) {
return { id: ec.id, label: ec.label, icon: ec.icon }
} else return undefined
})
.filter((f) => f !== undefined) as EmojiCategory[]
$: categoryTabs = searching ? ([...searchCategory, ...emojiTabs] as EmojiCategory[]) : emojiTabs
function handleScrollToCategory (categoryId: string): void {
if (searching && categoryId !== searchCategory[0].id) $searchEmoji = ''
if (isMobile) {
const tempCat = emojiTabs.find((ct) => ct?.id === categoryId)
if (tempCat === undefined) return
currentCategory = tempCat
outputGroups = updateGroups(searching, emojisCat)
} else {
setTimeout(() => {
const labelElement = document.getElementById(categoryId)
if (labelElement) {
const emojisElement = labelElement.nextElementSibling as HTMLElement
scrollElement.scroll(0, emojisElement.offsetTop - $deviceInfo.fontSize * 1.75)
}
})
}
}
function handleCategoryScrolled (): void {
if (isMobile) return
const selectedCategory = emojisCat.find((category) => {
const labelElement = document.getElementById(category.id)
if (labelElement == null) return false
const emojisElement = labelElement.nextElementSibling as HTMLElement
return emojisElement.offsetTop + emojisElement.offsetHeight - emojiRowHeightPx > scrollElement.scrollTop
})
if (selectedCategory !== undefined) currentCategory = selectedCategory
}
const sendEmoji = (emoji: EmojiWithGroup): void => {
selected = isCustomEmoji(emoji) ? emoji.shortcode : emoji.emoji
addFrequentlyEmojis(emoji)
dispatch('close', {
text: selected,
url: isCustomEmoji(emoji) ? emoji.url : undefined
})
}
const selectedEmoji = (event: CustomEvent<EmojiWithGroup>): void => {
if (event.detail === undefined || typeof event.detail !== 'object') return
sendEmoji(event.detail)
}
function openContextMenu (event: TouchEvent | MouseEvent, _emoji: EmojiWithGroup, remove: boolean): void {
event.preventDefault()
const emoji = _emoji
if (emoji === undefined) return
clearTimer()
shownContext = true
showPopup(
ActionsPopup,
{ emoji, remove },
eventToHTMLElement(event),
(result: 'remove' | EmojiWithGroup) => {
if (result === 'remove') {
removeFrequentlyEmojis(emoji)
const index = emojisCat.findIndex((ec) => ec.id === 'frequently-used')
if (index > -1) emojisCat[index].emojis = getFrequentlyEmojis()
emojisCat = emojisCat.filter(
(em) => em.categories !== undefined || (Array.isArray(em.emojis) && em.emojis.length > 0)
)
if (currentCategory.emojis?.length === 0) {
currentCategory = emojisCat.find((ec) => ec.emojis !== undefined && ec.emojis.length > 0) ?? emojisCat[0]
}
} else if (result !== undefined) {
sendEmoji(result)
}
shownContext = false
}
)
}
function handleContextMenu (event: MouseEvent, emoji: EmojiWithGroup, remove: boolean): void {
event.preventDefault()
const skins = getEmojiSkins(emoji)
if (Array.isArray(skins) || remove) openContextMenu(event, emoji, remove)
}
const clearTimer = (): void => {
clearTimeout(timer)
touchObserver(true)
}
const touchObserver = (remove: boolean = false): void => {
touchEvents.forEach((event) => {
if (remove) window.removeEventListener(event, clearTimer)
else window.addEventListener(event, clearTimer)
})
}
function clampedContextMenu (event: TouchEvent, emoji: EmojiWithGroup, remove: boolean): void {
const skins = getEmojiSkins(emoji)
if (timer == null && (Array.isArray(skins) || remove)) {
touchObserver()
timer = setTimeout(function () {
if (!shownContext) openContextMenu(event, emoji, remove)
clearTimer()
}, 1000)
}
}
const showSkinMenu = (event: MouseEvent): void => {
shownSTM = true
showPopup(SkinTonePopup, { emoji: getEmojiByShortCode(':hand:', 0), selected: skinTone }, eventToHTMLElement(event), (result) => {
if (typeof result === 'number') {
skinTone = result
setSkinTone(skinTone)
}
shownSTM = false
})
}
let hidden: boolean = true
const checkScroll = (event: Event): void => {
if (timer != null) clearTimer()
const target = event.target as HTMLElement
if (target == null) return
hidden = target.scrollHeight - target.scrollTop - target.clientHeight < 5
}
const initEmoji = (): void => {
emojiCategories.forEach((em, index) => {
if (em.id === 'frequently-used') {
emojiCategories[index].emojis = getFrequentlyEmojis()
}
if (em.emojisString !== undefined && Array.isArray(em.emojisString) && em.emojis === undefined) {
const tempEmojis: string[] = em.emojisString
const emojis: EmojiWithGroup[] = []
tempEmojis.forEach((te) => {
const e = $emojiStore.find((es) => isCustomEmoji(es) ? es.shortcode === te : es.hexcode === te)
if (e !== undefined) emojis.push(e)
})
emojiCategories[index].emojis = emojis
}
})
emojisCat = emojiCategories.filter(
(em) => em.categories !== undefined || (Array.isArray(em.emojis) && em.emojis.length > 0)
)
currentCategory = emojisCat.find((ec) => ec.emojis !== undefined) ?? emojisCat[0]
}
onMount(() => {
if (scrollElement !== undefined) scrollElement.addEventListener('scroll', checkScroll)
closeTooltip()
setTimeout(initEmoji)
})
onDestroy(() => {
if (scrollElement !== undefined) scrollElement.removeEventListener('scroll', checkScroll)
})
const updateGroups = (s: boolean, ec: EmojiCategory[]): EmojiCategory[] => {
return s ? searchCategory : isMobile ? ec.filter((e) => e.id === currentCategory.id) : ec
}
$: outputGroups = updateGroups(searching, emojisCat)
</script>
<div class="hulyPopupEmoji-container kind-{kind}" class:embedded>
<div class="hulyPopupEmoji-header__tabs-wrapper">
<div class="hulyPopupEmoji-header__tabs">
{#each categoryTabs as category (category.id)}
<button
class="hulyPopupEmoji-header__tab"
class:selected={(searching && searchCategory[0].id === category.id) ||
(!searching && currentCategory.id === category.id)}
data-id={category.id}
use:tooltip={{ label: category.label }}
on:click={() => {
handleScrollToCategory(category.id)
}}
>
<svelte:component this={category.icon} size={isMobile ? 'large' : 'x-large'} />
</button>
{/each}
<div
style:left={`${
(searching ? 0 : emojisCat.findIndex((ec) => ec.id === currentCategory.id)) * (isMobile ? 1.875 : 2.125)
}rem`}
class="hulyPopupEmoji-header__tab-cursor"
/>
</div>
</div>
<div class="hulyPopupEmoji-header__tools">
<SearchInput
value={$searchEmoji}
placeholder={plugin.string.SearchDots}
width={'100%'}
autoFocus
delay={50}
on:change={(result) => {
if (result.detail !== undefined) $searchEmoji = result.detail
else if (result.detail !== '') currentCategory = searchCategory[0]
}}
/>
<ButtonBase
type={'type-button-icon'}
hasMenu
pressed={shownSTM}
kind={'tertiary'}
size={'small'}
tooltip={{ label: plugin.string.DefaultSkinTone }}
on:click={showSkinMenu}
>
<span style:font-size={'1.5rem'}>{getUnicodeEmojiByShortCode(':hand:', skinTone)?.emoji}</span>
</ButtonBase>
</div>
<Scroller
bind:divScroll={scrollElement}
gap="0.5rem"
checkForHeaders
noStretch
on:scrolledCategories={handleCategoryScrolled}
>
{#each outputGroups as group (group.id)}
{@const canRemove = group.id === 'frequently-used'}
<EmojiGroup
{group}
{searching}
{disabled}
{selected}
{skinTone}
{kind}
lazy={group.id !== 'frequently-used'}
on:select={selectedEmoji}
on:touchstart={(ev) => {
const { event, emoji } = ev.detail
clampedContextMenu(event, emoji, canRemove)
}}
on:contextmenu={(ev) => {
const { event, emoji } = ev.detail
handleContextMenu(event, emoji, canRemove)
}}
/>
{/each}
</Scroller>
{#if !hidden && kind === 'fade'}<div class="hulyPopupEmoji-footer" />{/if}
</div>
<style lang="scss">
.hulyPopupEmoji-container {
position: relative;
display: flex;
flex-direction: column;
gap: 0.75rem;
padding-bottom: 0.75rem;
width: 100%;
height: 100%;
min-width: 0;
min-height: 28.5rem;
max-height: 28.5rem;
user-select: none;
:global(.mobile-theme) & {
min-width: 0;
max-width: calc(100vw - 2rem);
min-height: 0;
max-height: calc(100% - 4rem);
}
&:not(.embedded) {
min-width: 25.5rem;
max-width: 25.5rem;
background: var(--theme-popup-color); // var(--global-popover-BackgroundColor);
border: 1px solid var(--theme-popup-divider); // var(--global-popover-BorderColor);
border-radius: var(--small-BorderRadius);
box-shadow: var(--global-popover-ShadowX) var(--global-popover-ShadowY) var(--global-popover-ShadowBlur)
var(--global-popover-ShadowSpread) var(--global-popover-ShadowColor);
:global(.mobile-theme) & {
max-width: 100%;
max-height: 100%;
}
}
.hulyPopupEmoji-header__tools,
.hulyPopupEmoji-header__tabs-wrapper,
.hulyPopupEmoji-header__tabs,
.hulyPopupEmoji-header__tab {
display: flex;
align-items: center;
flex-shrink: 0;
min-width: 0;
min-height: 0;
}
.hulyPopupEmoji-header__tabs-wrapper {
overflow-x: auto;
padding: 0.25rem 0.75rem;
width: 100%;
border-bottom: 1px solid var(--theme-popup-divider);
}
.hulyPopupEmoji-header__tabs {
position: relative;
gap: 0.125rem;
width: 100%;
}
.hulyPopupEmoji-header__tab {
justify-content: center;
width: 2rem;
height: 2rem;
color: var(--theme-halfcontent-color);
transition: color 0.15s ease-in;
transition: left 0.15s ease-in;
&:disabled,
&.disabled {
color: var(--theme-darker-color);
}
:global(.mobile-theme) & {
width: 1.75rem;
height: 1.75rem;
}
:global(svg) {
transform: scale(0.8);
transition: transform 0.15s ease-in-out;
}
&:not(:disabled, .disabled) {
&.selected {
color: var(--theme-caption-color);
:global(svg) {
transform: scale(1);
}
}
&:hover {
color: var(--theme-content-color);
}
}
}
.hulyPopupEmoji-header__tab-cursor {
position: absolute;
bottom: -0.25rem;
width: 2rem;
height: 0.125rem;
background-color: var(--theme-tablist-plain-color);
transition: left 0.15s ease-in;
:global(.mobile-theme) & {
width: 1.75rem;
}
}
.hulyPopupEmoji-header__tools {
gap: 0.5rem;
padding: 0 0.75rem;
}
.hulyPopupEmoji-footer {
position: absolute;
left: 50%;
bottom: 0.75rem;
width: calc(100% - 1.5rem);
height: 1rem;
background: var(--theme-popup-trans-gradient);
transform-origin: center;
transform: translateX(-50%) rotate(180deg);
z-index: 1;
pointer-events: none;
}
}
</style>

View File

@ -1,40 +0,0 @@
<script lang="ts">
//
// © 2025 Hardcore Engineering, Inc. All Rights Reserved.
// Licensed under the Eclipse Public License v2.0 (SPDX: EPL-2.0).
//
import { createEventDispatcher } from 'svelte'
import type { Emoji } from 'emojibase'
import { Label, closeTooltip, ModernCheckbox, getEmojiSkins } from '../../'
import { skinTones } from '.'
export let emoji: Emoji
export let selected: number
const dispatch = createEventDispatcher()
closeTooltip()
const emojiSkins = getEmojiSkins(emoji)
const skins: Emoji[] = emojiSkins !== undefined ? [emoji, ...emojiSkins] : []
</script>
<div class="hulyPopup-container noPadding">
<div class="hulyPopup-group">
{#each skins as skin, index}
{@const disabled = selected === index}
{@const label = skinTones.get(index)}
<button
class="hulyPopup-row withKeys"
class:noHover={disabled}
on:click={() => {
if (disabled) return undefined
dispatch('close', index)
}}
>
<span style:font-size={'1.5rem'}>{skin.emoji}</span>
{#if label}<span class="hulyPopup-row__label"><Label {label} /></span>{/if}
{#if disabled}<span class="hulyPopup-row__keys"><ModernCheckbox checked disabled /></span>{/if}
</button>
{/each}
</div>
</div>

View File

@ -1,37 +0,0 @@
<script lang="ts">
//
// © 2025 Hardcore Engineering, Inc. All Rights Reserved.
// Licensed under the Eclipse Public License v2.0 (SPDX: EPL-2.0).
//
import { createEventDispatcher } from 'svelte'
import { getEmojiSkins } from '.'
import { ButtonBase, closeTooltip } from '../..'
import { Emoji } from 'emojibase'
export let emoji: Emoji
export let selected: number
const dispatch = createEventDispatcher()
const emojiSkins = getEmojiSkins(emoji)
const skins: Emoji[] = emojiSkins !== undefined ? [emoji, ...emojiSkins] : []
</script>
<div class="flex-row-center flex-gap-1">
{#each skins as skin, index}
{@const disabled = selected === index}
<ButtonBase
type={'type-button-icon'}
{disabled}
kind={disabled ? 'secondary' : 'tertiary'}
size={'small'}
on:click={() => {
if (disabled) return undefined
dispatch('update', index)
closeTooltip()
}}
>
<span style:font-size={'1.5rem'}>{skin.emoji}</span>
</ButtonBase>
{/each}
</div>

View File

@ -1,20 +0,0 @@
<script lang="ts">
//
// © 2025 Hardcore Engineering, Inc. All Rights Reserved.
// Licensed under the Eclipse Public License v2.0 (SPDX: EPL-2.0).
//
export let size: 'small' | 'medium' | 'large' | 'x-large'
const fill: string = 'currentColor'
</script>
<svg class="svg-{size}" viewBox="0 0 24 24" {fill} xmlns="http://www.w3.org/2000/svg">
<path
d="M12,7.8c0.4,0,0.8-0.3,0.8-0.8S12.4,6.2,12,6.2c-3.2,0-5.8,2.6-5.8,5.8c0,3.2,2.6,5.8,5.8,5.8c3.2,0,5.8-2.6,5.8-5.8c0-0.4-0.3-0.8-0.8-0.8s-0.8,0.3-0.8,0.8c0,2.3-1.9,4.2-4.2,4.2c-2.3,0-4.2-1.9-4.2-4.2C7.8,9.7,9.7,7.8,12,7.8z"
/>
<path
d="M21.7,10.9c0-0.4-0.4-0.7-0.8-0.7c-0.4,0-0.7,0.4-0.7,0.8c0.1,0.6,0.1,1.2,0,1.8c-0.2,2.2-1.3,4.2-3.1,5.5c-1.7,1.4-3.9,2-6.1,1.8c-2.2-0.2-4.2-1.3-5.5-3.1c-1.4-1.7-2-3.9-1.8-6.1C4,8.9,5.1,6.9,6.8,5.6s3.9-2,6.1-1.8c0.4,0.1,0.8-0.2,0.8-0.7c0-0.4-0.2-0.8-0.7-0.8C10.5,2,7.9,2.8,5.9,4.4c-2,1.6-3.3,3.9-3.6,6.5C2,13.5,2.8,16,4.4,18.1c1.6,2,3.9,3.3,6.5,3.6c0.4,0,0.7,0.1,1.1,0.1c2.2,0,4.3-0.7,6.1-2.1c2-1.6,3.3-3.9,3.6-6.5C21.8,12.4,21.8,11.6,21.7,10.9z"
/>
<path
d="M12.6,10.4c-0.2-0.1-0.4-0.1-0.6-0.1c-1,0-1.8,0.8-1.8,1.8s0.8,1.8,1.8,1.8s1.8-0.8,1.8-1.8c0-0.2,0-0.4-0.1-0.6l1.7-1.7H18c0.2,0,0.4-0.1,0.5-0.2l3-3c0.2-0.2,0.3-0.5,0.2-0.8c-0.1-0.3-0.4-0.5-0.7-0.5h-2.2V3c0-0.3-0.2-0.6-0.5-0.7c-0.3-0.1-0.6-0.1-0.8,0.2l-3,3c-0.1,0.1-0.2,0.3-0.2,0.5v2.7L12.6,10.4z M15.8,6.3l1.5-1.5V6c0,0.4,0.3,0.8,0.8,0.8h1.2l-1.5,1.5h-1.9V6.3z"
/>
</svg>

View File

@ -1,16 +0,0 @@
<script lang="ts">
//
// © 2025 Hardcore Engineering, Inc. All Rights Reserved.
// Licensed under the Eclipse Public License v2.0 (SPDX: EPL-2.0).
//
export let size: 'small' | 'medium' | 'large' | 'x-large'
const fill: string = 'currentColor'
</script>
<svg class="svg-{size}" viewBox="0 0 24 24" {fill} xmlns="http://www.w3.org/2000/svg">
<path d="M10,9.8c-0.7,0-1.2,0.6-1.2,1.3s0.6,1.2,1.2,1.2s1.2-0.6,1.2-1.2v0C11.2,10.3,10.7,9.8,10,9.8z" />
<path d="M14,9.8c-0.7,0-1.2,0.6-1.2,1.3s0.6,1.2,1.2,1.2s1.2-0.6,1.2-1.2v0C15.2,10.3,14.7,9.8,14,9.8z" />
<path
d="M21.7,10.6l-2-6.8c-0.1-0.4-0.5-0.6-0.8-0.5l-6,1c0,0,0,0,0,0h-1.7c0,0,0,0,0,0l-6-1C4.7,3.2,4.4,3.4,4.3,3.8l-2,6.9c-0.1,0.6,0.1,1.3,0.6,1.7c0.3,0.3,0.7,0.4,1.1,0.4c0.1,0,0.2,0,0.3,0c0.8,5.3,2.7,8,5.7,8h4c3,0,4.9-2.7,5.7-8c0.1,0,0.2,0,0.3,0c0.4,0,0.8-0.1,1.1-0.4C21.6,11.9,21.9,11.3,21.7,10.6z M3.8,11.2c-0.1,0-0.1-0.1-0.1-0.2l1.8-6.2l4,0.6l-5.3,5.7C4.1,11.3,3.9,11.3,3.8,11.2z M14,19.2h-1.2V18c0-0.1,0-0.2-0.1-0.4c0.7-0.2,1.1-0.8,1.1-1.6c0-0.4-0.3-0.8-0.8-0.8h-2c-0.4,0-0.8,0.3-0.8,0.8c0,0.8,0.4,1.4,1.1,1.6c-0.1,0.1-0.1,0.2-0.1,0.4v1.2H10c-2.2,0-3.6-2.5-4.3-7.3c0-0.1,0-0.1,0-0.1l5.6-6h1.4l5.6,6c0,0,0,0.1,0,0.1C17.6,16.8,16.2,19.2,14,19.2z M20.2,11.2c-0.1,0.1-0.3,0.1-0.4,0l-5.3-5.7l4-0.6l1.8,6.1C20.3,11.1,20.2,11.1,20.2,11.2z"
/>
</svg>

View File

@ -1,14 +0,0 @@
<script lang="ts">
//
// © 2025 Hardcore Engineering, Inc. All Rights Reserved.
// Licensed under the Eclipse Public License v2.0 (SPDX: EPL-2.0).
//
export let size: 'small' | 'medium' | 'large' | 'x-large'
const fill: string = 'currentColor'
</script>
<svg class="svg-{size}" viewBox="0 0 24 24" {fill} xmlns="http://www.w3.org/2000/svg">
<path
d="M19.3,4.3c-0.3-0.1-0.6-0.1-0.8,0.2c-1.6,1.6-4.3,1.6-5.9,0c-2.2-2.2-5.8-2.2-8,0C4.3,4.6,4.2,4.8,4.2,5v9v7c0,0.4,0.3,0.8,0.8,0.8s0.8-0.3,0.8-0.8v-6.6c1.7-1.4,4.2-1.4,5.7,0.2c1.1,1.1,2.6,1.6,4,1.6s2.9-0.5,4-1.6c0.1-0.1,0.2-0.3,0.2-0.5V5C19.8,4.7,19.6,4.4,19.3,4.3z M18.2,13.7c-1.7,1.4-4.2,1.3-5.7-0.2c-1.1-1.1-2.6-1.6-4-1.6c-0.9,0-1.9,0.2-2.8,0.7V5.3C7.4,3.9,9.9,4,11.5,5.5c1.8,1.8,4.6,2.1,6.8,0.9V13.7z"
/>
</svg>

View File

@ -1,20 +0,0 @@
<script lang="ts">
//
// © 2025 Hardcore Engineering, Inc. All Rights Reserved.
// Licensed under the Eclipse Public License v2.0 (SPDX: EPL-2.0).
//
export let size: 'small' | 'medium' | 'large' | 'x-large'
const fill: string = 'currentColor'
</script>
<svg class="svg-{size}" viewBox="0 0 24 24" {fill} xmlns="http://www.w3.org/2000/svg">
<path
d="M7.6,7.6C7.7,7.7,7.8,7.8,8,7.8c0.2,0,0.5-0.1,0.6-0.3c0.2-0.3,0.2-0.8-0.2-1C8,6.1,7.7,5.6,7.8,5c0-0.5,0.2-1.1,0.7-1.4c0.3-0.2,0.4-0.7,0.2-1c-0.2-0.3-0.7-0.4-1-0.2C6.7,3,6.2,4,6.2,5C6.2,6,6.7,7,7.6,7.6z"
/>
<path
d="M11.6,7.6c0.1,0.1,0.3,0.1,0.4,0.1c0.2,0,0.5-0.1,0.6-0.3c0.2-0.3,0.2-0.8-0.2-1C12,6.1,11.7,5.6,11.8,5c0-0.5,0.2-1.1,0.7-1.4c0.3-0.2,0.4-0.7,0.2-1c-0.2-0.3-0.7-0.4-1-0.2C10.7,3,10.2,4,10.2,5C10.2,6,10.7,7,11.6,7.6z"
/>
<path
d="M19.6,10.6c-0.6-0.3-1.2-0.4-1.8-0.3V10c0-0.4-0.3-0.8-0.8-0.8H3c-0.4,0-0.8,0.3-0.8,0.8v5c0,3.7,3,6.8,6.8,6.8h2c2.8,0,5.2-1.7,6.2-4.1c0.3,0.1,0.5,0.1,0.8,0.1c0.4,0,0.9-0.1,1.3-0.2c0.9-0.3,1.7-1,2.1-2C22.3,13.7,21.4,11.5,19.6,10.6zM16.2,10.8v0.4c0,0,0,0,0,0v1.6c-0.8-0.3-1.7-0.5-2.7-0.5c-1.5,0-3,0.4-4,1.2c-0.7,0.5-1.8,0.8-3.1,0.8c-1.1,0-2-0.2-2.7-0.7v-2.8H16.2z M11,20.2H9c-2.8,0-5.1-2.2-5.2-5c0.8,0.3,1.7,0.5,2.6,0.5c0.1,0,0.1,0,0.2,0c1.5,0,2.9-0.4,3.9-1.2c0.7-0.5,1.8-0.8,3-0.8c0.1,0,0.1,0,0.2,0c1,0,2,0.2,2.7,0.7V15C16.2,17.9,13.9,20.2,11,20.2z M20,14.9c-0.2,0.5-0.7,1-1.3,1.2c-0.4,0.1-0.8,0.2-1.1,0.1c0.1-0.4,0.1-0.8,0.1-1.2v-3.2c0.4,0,0.8,0,1.2,0.2C20.1,12.5,20.6,13.8,20,14.9z"
/>
</svg>

View File

@ -1,17 +0,0 @@
<script lang="ts">
//
// © 2025 Hardcore Engineering, Inc. All Rights Reserved.
// Licensed under the Eclipse Public License v2.0 (SPDX: EPL-2.0).
//
export let size: 'small' | 'medium' | 'large' | 'x-large'
const fill: string = 'currentColor'
</script>
<svg class="svg-{size}" viewBox="0 0 24 24" {fill} xmlns="http://www.w3.org/2000/svg">
<path
d="M12,7.2c-0.4,0-0.8,0.3-0.8,0.8v4c0,0.2,0.1,0.4,0.2,0.5l2,2c0.1,0.1,0.3,0.2,0.5,0.2s0.4-0.1,0.5-0.2c0.3-0.3,0.3-0.8,0-1.1l-1.8-1.8V8C12.8,7.6,12.4,7.2,12,7.2z"
/>
<path
d="M19.6,5.8c-1.6-2-4-3.3-6.6-3.5C7.6,1.7,2.8,5.6,2.3,10.9c0,0.4,0.3,0.8,0.7,0.8c0.4,0,0.8-0.3,0.8-0.7c0.5-4.5,4.5-7.8,9-7.4C15,3.9,17,5,18.4,6.7c1.4,1.7,2,3.9,1.8,6c-0.2,2.2-1.3,4.2-3,5.6c-1.7,1.4-3.9,2-6,1.8c-2.8-0.3-5.2-2-6.5-4.4H8c0.4,0,0.8-0.3,0.8-0.8S8.5,14.2,8,14.2H3.6c0,0,0,0,0,0H3c-0.4,0-0.8,0.3-0.8,0.8v5c0,0.4,0.3,0.8,0.8,0.8s0.8-0.3,0.8-0.8v-2.8c1.6,2.5,4.2,4.1,7.2,4.4c0.3,0,0.7,0.1,1,0.1c2.2,0,4.4-0.8,6.1-2.2c2-1.6,3.3-4,3.5-6.6C22,10.3,21.2,7.8,19.6,5.8z"
/>
</svg>

View File

@ -1,14 +0,0 @@
<script lang="ts">
//
// © 2025 Hardcore Engineering, Inc. All Rights Reserved.
// Licensed under the Eclipse Public License v2.0 (SPDX: EPL-2.0).
//
export let size: 'small' | 'medium' | 'large' | 'x-large'
const fill: string = 'currentColor'
</script>
<svg class="svg-{size}" viewBox="0 0 24 24" {fill} xmlns="http://www.w3.org/2000/svg">
<path
d="M11,21.8c-0.1,0-0.2,0-0.2,0c-0.3-0.1-0.5-0.4-0.5-0.7v-6.2H5c-0.3,0-0.5-0.2-0.7-0.4c-0.1-0.3-0.1-0.6,0.1-0.8l8-11c0.2-0.3,0.5-0.4,0.8-0.3c0.3,0.1,0.5,0.4,0.5,0.7v6.2H19c0.3,0,0.5,0.2,0.7,0.4s0.1,0.6-0.1,0.8l-8,11C11.5,21.6,11.2,21.8,11,21.8z M6.5,13.2H11c0.4,0,0.8,0.3,0.8,0.8v4.7l5.8-7.9H13c-0.4,0-0.8-0.3-0.8-0.8V5.3L6.5,13.2z"
/>
</svg>

View File

@ -1,15 +0,0 @@
<script lang="ts">
//
// © 2025 Hardcore Engineering, Inc. All Rights Reserved.
// Licensed under the Eclipse Public License v2.0 (SPDX: EPL-2.0).
//
export let size: 'small' | 'medium' | 'large' | 'x-large'
const fill: string = 'currentColor'
</script>
<svg class="svg-{size}" viewBox="0 0 24 24" {fill} xmlns="http://www.w3.org/2000/svg">
<path
d="M20.7,6.9l-3.6-3.6c-1.4-1.4-3.7-1.4-5.1,0L9.3,6C8.6,6.6,8.2,7.6,8.2,8.5c0,0.9,0.3,1.7,0.8,2.3l-1.6,1.6c-0.1,0.1-0.2,0.3-0.2,0.5v1.2H6c-0.4,0-0.8,0.3-0.8,0.8v1.2H4c-0.2,0-0.4,0.1-0.5,0.2l-0.6,0.6c-0.4,0.5-0.6,1.1-0.6,1.8l0,1.2l0,0.2c0.1,0.9,0.9,1.5,1.7,1.5l1.2,0l0.2,0c0.6-0.1,1.2-0.3,1.7-0.8l6-6c1.4,1.2,3.6,1.1,4.9-0.2l2.6-2.6C22.1,10.6,22.1,8.3,20.7,6.9z M19.6,11L17,13.6c-0.8,0.8-2.2,0.8-3,0l-0.3-0.3c-0.3-0.3-0.8-0.3-1.1,0l-6.6,6.6c-0.2,0.2-0.5,0.3-0.7,0.4l-1.3,0c-0.1,0-0.2-0.1-0.2-0.2l0-1.2c0-0.3,0.1-0.6,0.3-0.8l0.3-0.3H6c0.4,0,0.8-0.3,0.8-0.8v-1.2H8c0.4,0,0.8-0.3,0.8-0.8v-1.7l1.9-1.9c0.1-0.1,0.2-0.3,0.2-0.5c0-0.2-0.1-0.4-0.2-0.5L10.4,10c-0.8-0.8-0.8-2.2,0-3L13,4.4c0.8-0.8,2.2-0.8,3,0L19.6,8C20.5,8.8,20.5,10.2,19.6,11z"
/>
<path d="M15,7.8L15,7.8c-0.7,0-1.3,0.6-1.3,1.2s0.6,1.2,1.3,1.2s1.2-0.6,1.2-1.2S15.7,7.8,15,7.8z" />
</svg>

View File

@ -1,14 +0,0 @@
<script lang="ts">
//
// © 2025 Hardcore Engineering, Inc. All Rights Reserved.
// Licensed under the Eclipse Public License v2.0 (SPDX: EPL-2.0).
//
export let size: 'small' | 'medium' | 'large' | 'x-large'
const fill: string = 'currentColor'
</script>
<svg class="svg-{size}" viewBox="0 0 24 24" {fill} xmlns="http://www.w3.org/2000/svg">
<path
d="M21.5,20.5L16,14.9c1.1-1.3,1.8-3.1,1.8-4.9c0-4.3-3.5-7.8-7.8-7.8S2.2,5.7,2.2,10s3.5,7.8,7.8,7.8c1.9,0,3.6-0.7,4.9-1.8l5.5,5.5c0.1,0.1,0.3,0.2,0.5,0.2s0.4-0.1,0.5-0.2C21.8,21.2,21.8,20.8,21.5,20.5z M3.8,10c0-3.4,2.8-6.2,6.2-6.2s6.2,2.8,6.2,6.2s-2.8,6.2-6.2,6.2S3.8,13.4,3.8,10z"
/>
</svg>

View File

@ -1,23 +0,0 @@
<script lang="ts">
//
// © 2025 Hardcore Engineering, Inc. All Rights Reserved.
// Licensed under the Eclipse Public License v2.0 (SPDX: EPL-2.0).
//
export let size: 'small' | 'medium' | 'large' | 'x-large'
const fill: string = 'currentColor'
</script>
<svg class="svg-{size}" viewBox="0 0 24 24" {fill} xmlns="http://www.w3.org/2000/svg">
<path
d="M12,2.2c-5.4,0-9.8,4.4-9.8,9.8s4.4,9.8,9.8,9.8s9.8-4.4,9.8-9.8S17.4,2.2,12,2.2z M12,20.2c-4.5,0-8.2-3.7-8.2-8.2S7.5,3.8,12,3.8s8.2,3.7,8.2,8.2S16.5,20.2,12,20.2z"
/>
<path
d="M7.8,9.5c0,0.1,0.1,0.1,0.1,0.2c0,0.1,0.1,0.1,0.2,0.2c0.2,0.2,0.6,0.4,0.9,0.4c0.1,0,0.2,0,0.2,0c0.1,0,0.1,0,0.2-0.1c0.1,0,0.1-0.1,0.2-0.1c0.1,0,0.1-0.1,0.2-0.2C9.9,9.8,10,9.8,10,9.7c0-0.1,0.1-0.1,0.1-0.2c0-0.1,0.1-0.2,0.1-0.2c0-0.1,0-0.2,0-0.2c0-0.3-0.1-0.7-0.4-0.9C9.8,8.1,9.8,8,9.7,8c-0.1,0-0.1-0.1-0.2-0.1c-0.1,0-0.2-0.1-0.2-0.1c-0.4-0.1-0.8,0-1.1,0.3C7.9,8.3,7.8,8.7,7.8,9c0,0.1,0,0.2,0,0.2C7.8,9.3,7.8,9.4,7.8,9.5z"
/>
<path
d="M14.3,10c0.1,0,0.1,0.1,0.2,0.1c0.1,0,0.2,0.1,0.2,0.1c0.1,0,0.2,0,0.2,0s0.2,0,0.2,0c0.1,0,0.2,0,0.2-0.1c0.1,0,0.2-0.1,0.2-0.1c0.1-0.1,0.1-0.1,0.2-0.2c0.2-0.2,0.4-0.6,0.4-0.9c0-0.3-0.1-0.7-0.4-0.9c0-0.1-0.1-0.1-0.2-0.2c-0.1,0-0.1-0.1-0.2-0.1c-0.1,0-0.2-0.1-0.2-0.1c-0.2,0-0.3,0-0.5,0c-0.1,0-0.2,0-0.2,0.1c-0.1,0-0.1,0.1-0.2,0.1c-0.1,0-0.1,0.1-0.2,0.2c-0.2,0.2-0.4,0.6-0.4,0.9c0,0.3,0.1,0.6,0.4,0.9C14.2,9.9,14.2,10,14.3,10z"
/>
<path
d="M16,12.2H8c-0.4,0-0.8,0.3-0.8,0.8c0,2.6,2.1,4.8,4.8,4.8s4.8-2.1,4.8-4.8C16.8,12.6,16.4,12.2,16,12.2z M12,16.2c-1.5,0-2.8-1.1-3.2-2.5h6.3C14.8,15.2,13.5,16.2,12,16.2z"
/>
</svg>

View File

@ -1,23 +0,0 @@
<script lang="ts">
//
// © 2025 Hardcore Engineering, Inc. All Rights Reserved.
// Licensed under the Eclipse Public License v2.0 (SPDX: EPL-2.0).
//
export let size: 'small' | 'medium' | 'large' | 'x-large'
const fill: string = 'currentColor'
</script>
<svg class="svg-{size}" viewBox="0 0 24 24" {fill} xmlns="http://www.w3.org/2000/svg">
<path
d="M6.5,10.8c2.3,0,4.2-1.9,4.2-4.2S8.8,2.2,6.5,2.2S2.2,4.2,2.2,6.5S4.2,10.8,6.5,10.8z M6.5,3.8C8,3.8,9.2,5,9.2,6.5S8,9.2,6.5,9.2S3.8,8,3.8,6.5S5,3.8,6.5,3.8z"
/>
<path
d="M7.2,13.6c-0.3-0.5-1-0.5-1.3,0l-4,7c-0.1,0.2-0.1,0.5,0,0.7c0.1,0.2,0.4,0.4,0.6,0.4h8c0.3,0,0.5-0.1,0.6-0.4c0.1-0.2,0.1-0.5,0-0.7L7.2,13.6z M3.8,20.2l2.7-4.7l2.7,4.7H3.8z"
/>
<path
d="M18.6,6.5l3-3c0.3-0.3,0.3-0.8,0-1.1s-0.8-0.3-1.1,0l-3,3l-3-3c-0.3-0.3-0.8-0.3-1.1,0s-0.3,0.8,0,1.1l3,3l-3,3c-0.3,0.3-0.3,0.8,0,1.1c0.1,0.1,0.3,0.2,0.5,0.2s0.4-0.1,0.5-0.2l3-3l3,3c0.1,0.1,0.3,0.2,0.5,0.2s0.4-0.1,0.5-0.2c0.3-0.3,0.3-0.8,0-1.1L18.6,6.5z"
/>
<path
d="M21,13.2h-7c-0.4,0-0.8,0.3-0.8,0.8v7c0,0.4,0.3,0.8,0.8,0.8h7c0.4,0,0.8-0.3,0.8-0.8v-7C21.8,13.6,21.4,13.2,21,13.2zM20.2,20.2h-5.5v-5.5h5.5V20.2z"
/>
</svg>

View File

@ -1,17 +0,0 @@
<script lang="ts">
//
// © 2025 Hardcore Engineering, Inc. All Rights Reserved.
// Licensed under the Eclipse Public License v2.0 (SPDX: EPL-2.0).
//
export let size: 'small' | 'medium' | 'large' | 'x-large'
const fill: string = 'currentColor'
</script>
<svg class="svg-{size}" viewBox="0 0 24 24" {fill} xmlns="http://www.w3.org/2000/svg">
<path
d="M21.5,11.5l-9-9c-0.3-0.3-0.8-0.3-1.1,0l-9,9c-0.2,0.2-0.3,0.5-0.2,0.8s0.4,0.5,0.7,0.5h1.2V19c0,1.5,1.2,2.8,2.8,2.8h10c1.5,0,2.8-1.2,2.8-2.8v-6.2H21c0.3,0,0.6-0.2,0.7-0.5C21.8,12,21.7,11.7,21.5,11.5z M18.2,19c0,0.7-0.6,1.2-1.2,1.2H7c-0.7,0-1.2-0.6-1.2-1.2v-7c0-0.4-0.3-0.8-0.8-0.8H4.8L12,4.1l7.2,7.2H19c-0.4,0-0.8,0.3-0.8,0.8V19z"
/>
<path
d="M14,11.2h-4c-0.4,0-0.8,0.3-0.8,0.8v4c0,0.4,0.3,0.8,0.8,0.8h4c0.4,0,0.8-0.3,0.8-0.8v-4C14.8,11.6,14.4,11.2,14,11.2zM13.2,15.2h-2.5v-2.5h2.5V15.2z"
/>
</svg>

View File

@ -1,97 +0,0 @@
//
// Copyright © 2025 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import type { IntlString } from '@hcengineering/platform'
import IconFrequentlyUsed from './icons/FrequentlyUsed.svelte'
import IconGettingWorkDone from './icons/GettingWorkDone.svelte'
import IconSmileysAndPeople from './icons/SmileysAndPeople.svelte'
import IconAnimalsAndNature from './icons/AnimalsAndNature.svelte'
import IconFoodAndDrink from './icons/FoodAndDrink.svelte'
import IconTravelAndPlaces from './icons/TravelAndPlaces.svelte'
import IconActivities from './icons/Activities.svelte'
import IconObjects from './icons/Objects.svelte'
import IconSymbols from './icons/Symbols.svelte'
import IconFlags from './icons/Flags.svelte'
import plugin from '../../plugin'
import type { EmojiCategory } from './types'
export * from './types'
export * from './store'
export * from './utils'
export { default as EmojiPopup } from './EmojiPopup.svelte'
export { default as EmojiButton } from './EmojiButton.svelte'
export const emojiCategories: EmojiCategory[] = [
{ id: 'frequently-used', label: plugin.string.FrequentlyUsed, icon: IconFrequentlyUsed },
{
id: 'getting-work-done',
label: plugin.string.GettingWorkDone,
icon: IconGettingWorkDone,
emojisString: [
'2705',
'1F440',
'1F64C',
'1F64F',
'2795',
'2796',
'1F44F',
'1F4A1',
'1F3AF',
'1F44B',
'1F44D',
'1F389',
'0031-FE0F-20E3',
'0032-FE0F-20E3',
'0033-FE0F-20E3',
'1F4E3',
'26AA',
'1F535',
'1F534',
'1F3CE'
]
},
{
id: 'smileys-people',
label: plugin.string.SmileysAndPeople,
icon: IconSmileysAndPeople,
categories: ['smileys-emotion', 'people-body']
},
{
id: 'animals-nature',
label: plugin.string.AnimalsAndNature,
icon: IconAnimalsAndNature,
categories: 'animals-nature'
},
{ id: 'food-drink', label: plugin.string.FoodAndDrink, icon: IconFoodAndDrink, categories: 'food-drink' },
{ id: 'travel-places', label: plugin.string.TravelAndPlaces, icon: IconTravelAndPlaces, categories: 'travel-places' },
{ id: 'activities', label: plugin.string.Activities, icon: IconActivities, categories: 'activities' },
{ id: 'objects', label: plugin.string.Objects, icon: IconObjects, categories: 'objects' },
{ id: 'symbols', label: plugin.string.Symbols, icon: IconSymbols, categories: 'symbols' },
{ id: 'flags', label: plugin.string.Flags, icon: IconFlags, categories: 'flags' }
]
export const skinTonesCodes = [0x1f3fb, 0x1f3fc, 0x1f3fd, 0x1f3fe, 0x1f3ff]
export const skinTones: Map<number, IntlString> = new Map<number, IntlString>(
[
plugin.string.NoTone,
plugin.string.Light,
plugin.string.MediumLight,
plugin.string.Medium,
plugin.string.MediumDark,
plugin.string.Dark
].map((label, index) => [index, label])
)

View File

@ -1,29 +0,0 @@
//
// Copyright © 2025 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import { writable, derived } from 'svelte/store'
import type { EmojiWithGroup } from '.'
export const emojiStore = writable<EmojiWithGroup[]>([])
export const searchEmoji = writable<string>('')
export const resultEmojis = derived([emojiStore, searchEmoji], ([emojis, search]) => {
return search !== ''
? emojis.filter(
(emoji) =>
(emoji.tags?.some((tag: string) => tag.toLowerCase().startsWith(search.toLowerCase())) ?? false) ||
emoji.label.toLowerCase().includes(search.toLowerCase())
)
: emojis
})

View File

@ -1,44 +0,0 @@
//
// Copyright © 2025 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import type { IntlString } from '@hcengineering/platform'
import type { AnySvelteComponent } from '../..'
import type { Emoji } from 'emojibase'
export type ExtendedEmoji = Emoji | CustomEmoji
export type EmojiWithGroup = ExtendedEmoji & { key: string }
export interface CustomEmoji {
shortcode: string
label: string
url: string
tags?: string[]
}
export interface EmojiCategory {
id: string
label: IntlString
icon: AnySvelteComponent
categories?: string[] | string
emojisString?: string[]
emojis?: EmojiWithGroup[]
}
export function isCustomEmoji (emoji: ExtendedEmoji): emoji is CustomEmoji {
return 'url' in emoji
}
/* export function isCustomEmoji (emoji: ExtendedEmoji): boolean {
return false
} */

View File

@ -1,201 +0,0 @@
//
// Copyright © 2025 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import { get } from 'svelte/store'
import type { Emoji, Locale } from 'emojibase'
import { fetchEmojis, fetchMessages } from 'emojibase'
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, isCustomEmoji } from '../..'
import type { EmojiWithGroup } from '.'
import { emojiCategories, emojiStore } from '.'
export const emojiRegex = EMOJI_REGEX
export const emojiGlobalRegex = new RegExp(EMOJI_REGEX.source, EMOJI_REGEX.flags + 'g')
export const emoticonRegex = new RegExp(`(?:^|\\s)(${EMOTICON_REGEX.source})$`)
export const emoticonGlobalRegex = new RegExp(`(?<!\\S)${EMOTICON_REGEX.source}(?!\\S)`, EMOTICON_REGEX.flags + 'g')
export const shortcodeRegex = new RegExp(`(?:^|\\s)(${SHORTCODE_REGEX.source})$`)
export const shortcodeGlobalRegex = new RegExp(`(?<!\\S)${SHORTCODE_REGEX.source}(?!\\S)`, SHORTCODE_REGEX.flags + 'g')
export async function loadEmojis (lang?: string): Promise<EmojiWithGroup[]> {
const local = lang ?? get(deviceInfo).language ?? 'en'
const englishEmojis =
local === 'en'
? await fetchEmojis('en', { version: '15.0', shortcodes: ['iamcal'] })
: await fetchEmojis('en', { compact: true, version: '15.0', shortcodes: ['iamcal'] })
const languageEmojis = local === 'en' ? null : await fetchEmojis(local as Locale, { version: '15.0' })
const messages = await fetchMessages(local as Locale)
const groups = messages.groups
const groupKeys = new Map<number, string>(groups.map((group, index) => [index, group.key]))
const categories = new Map<string, string>()
emojiCategories.forEach((cat) => {
if (Array.isArray(cat.categories)) cat.categories.forEach((c) => categories.set(c, cat.id))
else if (typeof cat.categories === 'string') categories.set(cat.categories, cat.id)
})
const emojis =
languageEmojis !== null
? languageEmojis.map((langEmoji, index) => {
return {
...langEmoji,
tags: [...(englishEmojis[index]?.tags ?? []), ...(langEmoji?.tags ?? [])],
shortcodes: [...(englishEmojis[index]?.shortcodes ?? []), ...(langEmoji?.shortcodes ?? [])]
}
})
: (englishEmojis as Emoji[])
return emojis
.filter((e) => e.group !== 2 && e.group !== undefined)
.map((e) => {
return { ...e, key: categories.get(groupKeys.get(e?.group ?? 0) ?? '') ?? '' }
})
}
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)
}
export function getEmojiByHexcode (hexcode: string): EmojiWithGroup | undefined {
return get(emojiStore).find((e) => !isCustomEmoji(e) && e.hexcode === hexcode)
}
export function getEmojiByEmoticon (emoticon: string | undefined): string | undefined {
if (emoticon === undefined) return undefined
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
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 === pureShortcode
return e.shortcodes?.includes(pureShortcode)
}, 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 = isCustomEmoji(emoji) ? emoji.shortcode : emoji.hexcode
if (hexcode === undefined) return
const frequentlyEmojisKey = getEmojisLocalStorageKey()
const frequentlyEmojis = window.localStorage.getItem(frequentlyEmojisKey)
if (frequentlyEmojis != null) {
const parsedEmojis = JSON.parse(frequentlyEmojis)
if (Array.isArray(parsedEmojis)) {
window.localStorage.setItem(
frequentlyEmojisKey,
JSON.stringify(parsedEmojis.filter((pe) => pe.hexcode !== hexcode))
)
}
}
}
export const addFrequentlyEmojis = (emoji: EmojiWithGroup): void => {
if (emoji === undefined) return
const hexcode = isCustomEmoji(emoji) ? emoji.shortcode : emoji.hexcode
const frequentlyEmojisKey = getEmojisLocalStorageKey()
const frequentlyEmojis = window.localStorage.getItem(frequentlyEmojisKey)
const empty = frequentlyEmojis == null
if (!empty) {
const parsedEmojis = JSON.parse(frequentlyEmojis)
if (Array.isArray(parsedEmojis)) {
const index = parsedEmojis.findIndex((pe) => pe.hexcode === hexcode)
if (index === -1) parsedEmojis.push({ hexcode, count: 1 })
else parsedEmojis[index].count++
parsedEmojis.sort((a, b) => b.count - a.count)
window.localStorage.setItem(frequentlyEmojisKey, JSON.stringify(parsedEmojis))
return undefined
}
}
window.localStorage.setItem(frequentlyEmojisKey, JSON.stringify([{ hexcode, count: 1 }]))
}
export const getFrequentlyEmojis = (): EmojiWithGroup[] | undefined => {
const frequentlyEmojisKey = getEmojisLocalStorageKey()
const frequentlyEmojis = window.localStorage.getItem(frequentlyEmojisKey)
if (frequentlyEmojis == null) return undefined
try {
const parsedEmojis = JSON.parse(frequentlyEmojis)
if (!Array.isArray(parsedEmojis)) return undefined
const emojis = get(emojiStore)
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))
}
export const getSkinTone = (): number => {
const skinToneKey = getEmojisLocalStorageKey('skinTone')
const skinTone = window.localStorage.getItem(skinToneKey)
if (skinTone == null) return 0
try {
return JSON.parse(skinTone)
} catch (e) {
console.error(e)
return 0
}
}
function getEmojisLocalStorageKey (suffix: string = 'frequently'): string {
const me = getCurrentAccount()
return `emojis.${suffix}.${me.uuid}`
}

View File

@ -11,8 +11,7 @@
checkMobile,
deviceOptionsStore as deviceInfo,
checkAdaptiveMatching,
getLocalWeekStart,
updateEmojis
getLocalWeekStart
} from '../../'
import { desktopPlatform, getCurrentLocation, location, locationStorageKeyId, navigate } from '../../location'
import uiPlugin from '../../plugin'
@ -93,8 +92,6 @@
$: document.documentElement.style.setProperty('--app-height', `${docHeight}px`)
$: void updateEmojis($themeStore.language)
let doubleTouchStartTimestamp = 0
document.addEventListener('touchstart', (event) => {
const now = +new Date()

View File

@ -302,7 +302,6 @@ export * from './colors'
export * from './focus'
export * from './resize'
export * from './lazy'
export * from './components/emoji'
export function createApp (target: HTMLElement): SvelteComponent {
return new Root({ target })

View File

@ -6,8 +6,7 @@ import {
getCurrentResolvedLocation,
getEventPositionElement,
showPopup,
type Location,
type EmojiWithGroup, isCustomEmoji
type Location
} from '@hcengineering/ui'
import { type AttributeModel } from '@hcengineering/view'
import emojiPlugin from '@hcengineering/emoji'

View File

@ -5,7 +5,7 @@
//
import { createEventDispatcher } from 'svelte'
import emojiPlugin, { ExtendedEmoji, Emoji } from '@hcengineering/emoji'
import { getEmojiByHexcode, getEmojiSkins } from '../utils'
import { getEmojiByHexcode, getEmojiSkins } from '../utils'
import EmojiButton from './EmojiButton.svelte'
import { getSkinTone, emojiStore } from '../store'
import { Label, IconDelete, closeTooltip, ButtonBase } from '@hcengineering/ui'
@ -50,7 +50,7 @@
const equal = a === b
const noTone = a === 0
return equal && noTone
? e as Emoji.Emoji
? (e as Emoji.Emoji)
: getEmojiSkins(e)?.find((skin) =>
equal ? skin.tone === a : Array.isArray(skin.tone) && skin.tone[0] === a && skin.tone[1] === b
)

View File

@ -5,8 +5,9 @@
//
import { createEventDispatcher } from 'svelte'
import { getEmbeddedLabel } from '@hcengineering/platform'
import { tooltip, capitalizeFirstLetter, type LabelAndProps, type ExtendedEmoji, getEmojiSkins } from '@hcengineering/ui'
import { isCustomEmoji } from '@hcengineering/emoji'
import { tooltip, capitalizeFirstLetter, type LabelAndProps } from '@hcengineering/ui'
import { isCustomEmoji, type ExtendedEmoji } from '@hcengineering/emoji'
import { getEmojiSkins } from '../utils'
export let emoji: ExtendedEmoji
export let selected: boolean = false
@ -42,7 +43,7 @@
}}
>
{#if isCustomEmoji(displayedEmoji)}
<span><img src="{displayedEmoji.url}" alt="{displayedEmoji.shortcode}"></span>
<span><img src={displayedEmoji.url} alt={displayedEmoji.shortcode} /></span>
{:else}
<span>{displayedEmoji.emoji}</span>
{/if}

View File

@ -12,8 +12,10 @@
showPopup,
eventToHTMLElement,
ButtonBase,
closeTooltip, getEmojiByShortCode, getEmojiSkins, getUnicodeEmojiByShortCode, Icon, icon
closeTooltip,
Icon
} from '@hcengineering/ui'
import { getEmojiByShortCode, getEmojiSkins, getUnicodeEmojiByShortCode } from '../utils'
import {
searchEmoji,
emojiStore,
@ -29,7 +31,6 @@
import EmojiGroup from './EmojiGroup.svelte'
import emojiPlugin, { isCustomEmoji } from '@hcengineering/emoji'
import { emojiCategories, EmojiCategory } from '../types'
import { Asset, getMetadata } from '@hcengineering/platform'
export let embedded = false
export let selected: string | undefined
@ -119,27 +120,22 @@
clearTimer()
shownContext = true
showPopup(
ActionsPopup,
{ emoji, remove },
eventToHTMLElement(event),
(result: 'remove' | EmojiWithGroup) => {
if (result === 'remove') {
removeFrequentlyEmojis(emoji)
const index = emojisCat.findIndex((ec) => ec.id === 'frequently-used')
if (index > -1) emojisCat[index].emojis = getFrequentlyEmojis()
emojisCat = emojisCat.filter(
(em) => em.categories !== undefined || (Array.isArray(em.emojis) && em.emojis.length > 0)
)
if (currentCategory.emojis?.length === 0) {
currentCategory = emojisCat.find((ec) => ec.emojis !== undefined && ec.emojis.length > 0) ?? emojisCat[0]
}
} else if (result !== undefined) {
sendEmoji(result)
showPopup(ActionsPopup, { emoji, remove }, eventToHTMLElement(event), (result: 'remove' | EmojiWithGroup) => {
if (result === 'remove') {
removeFrequentlyEmojis(emoji)
const index = emojisCat.findIndex((ec) => ec.id === 'frequently-used')
if (index > -1) emojisCat[index].emojis = getFrequentlyEmojis()
emojisCat = emojisCat.filter(
(em) => em.categories !== undefined || (Array.isArray(em.emojis) && em.emojis.length > 0)
)
if (currentCategory.emojis?.length === 0) {
currentCategory = emojisCat.find((ec) => ec.emojis !== undefined && ec.emojis.length > 0) ?? emojisCat[0]
}
shownContext = false
} else if (result !== undefined) {
sendEmoji(result)
}
)
shownContext = false
})
}
function handleContextMenu (event: MouseEvent, emoji: EmojiWithGroup, remove: boolean): void {
event.preventDefault()
@ -169,13 +165,18 @@
const showSkinMenu = (event: MouseEvent): void => {
shownSTM = true
showPopup(SkinTonePopup, { emoji: getEmojiByShortCode(':hand:', 0), selected: skinTone }, eventToHTMLElement(event), (result) => {
if (typeof result === 'number') {
skinTone = result
setSkinTone(skinTone)
showPopup(
SkinTonePopup,
{ emoji: getEmojiByShortCode(':hand:', 0), selected: skinTone },
eventToHTMLElement(event),
(result) => {
if (typeof result === 'number') {
skinTone = result
setSkinTone(skinTone)
}
shownSTM = false
}
shownSTM = false
})
)
}
let hidden: boolean = true
@ -195,7 +196,7 @@
const tempEmojis: string[] = em.emojisString
const emojis: EmojiWithGroup[] = []
tempEmojis.forEach((te) => {
const e = $emojiStore.find((es) => isCustomEmoji(es) ? es.shortcode === te : 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
@ -236,7 +237,7 @@
handleScrollToCategory(category.id)
}}
>
<Icon icon={category.icon} size={isMobile ? 'large' : 'x-large'}/>
<Icon icon={category.icon} size={isMobile ? 'large' : 'x-large'} />
<!--<svelte:component this={category.icon} size={isMobile ? 'large' : 'x-large'} />-->
</button>
{/each}

View File

@ -2,6 +2,8 @@ import type { Resources } from '@hcengineering/platform'
import EmojiPopup from './components/EmojiPopup.svelte'
import WorkbenchExtension from './components/WorkbenchExtension.svelte'
export * from './utils'
export default async (): Promise<Resources> => ({
component: {
EmojiPopup,

View File

@ -10,7 +10,7 @@ export const resultEmojis = derived([emojiStore, searchEmoji], ([emojis, search]
? emojis.filter(
(emoji) =>
(emoji.tags?.some((tag: string) => tag.toLowerCase().startsWith(search.toLowerCase())) ?? false) ||
emoji.label.toLowerCase().includes(search.toLowerCase())
emoji.label.toLowerCase().includes(search.toLowerCase())
)
: emojis
})
@ -81,14 +81,14 @@ export const getFrequentlyEmojis = (): EmojiWithGroup[] | undefined => {
const result: EmojiWithGroup[] = []
emojis.forEach((emoji: EmojiWithGroup) => {
if (isCustomEmoji(emoji)) {
if (parsedEmojis.find(pe => pe.hexcode === emoji.shortcode) !== undefined) result.push(emoji)
if (parsedEmojis.find((pe) => pe.hexcode === emoji.shortcode) !== undefined) result.push(emoji)
} else {
parsedEmojis.forEach((parsedEmoji: any) => {
if (parsedEmoji.hexcode === emoji.hexcode) {
result.push(emoji)
return
}
const skinEmoji = emoji.skins?.find(s => s.hexcode === parsedEmoji.hexcode)
const skinEmoji = emoji.skins?.find((s) => s.hexcode === parsedEmoji.hexcode)
if (skinEmoji === undefined) return
result.push({ ...skinEmoji, key: '' })
})

View File

@ -52,9 +52,24 @@ export const emojiCategories: EmojiCategory[] = [
icon: emojiPlugin.icon.AnimalsAndNature,
categories: 'animals-nature'
},
{ id: 'food-drink', label: emojiPlugin.string.FoodAndDrink, icon: emojiPlugin.icon.FoodAndDrink, categories: 'food-drink' },
{ id: 'travel-places', label: emojiPlugin.string.TravelAndPlaces, icon: emojiPlugin.icon.TravelAndPlaces, categories: 'travel-places' },
{ id: 'activities', label: emojiPlugin.string.Activities, icon: emojiPlugin.icon.Activities, categories: 'activities' },
{
id: 'food-drink',
label: emojiPlugin.string.FoodAndDrink,
icon: emojiPlugin.icon.FoodAndDrink,
categories: 'food-drink'
},
{
id: 'travel-places',
label: emojiPlugin.string.TravelAndPlaces,
icon: emojiPlugin.icon.TravelAndPlaces,
categories: 'travel-places'
},
{
id: 'activities',
label: emojiPlugin.string.Activities,
icon: emojiPlugin.icon.Activities,
categories: 'activities'
},
{ id: 'objects', label: emojiPlugin.string.Objects, icon: emojiPlugin.icon.Objects, categories: 'objects' },
{ id: 'symbols', label: emojiPlugin.string.Symbols, icon: emojiPlugin.icon.Symbols, categories: 'symbols' },
{ id: 'flags', label: emojiPlugin.string.Flags, icon: emojiPlugin.icon.Flags, categories: 'flags' }

View File

@ -51,7 +51,9 @@ export function getEmojiByHexcode (hexcode: string): EmojiWithGroup | undefined
export function getEmojiByEmoticon (emoticon: string | undefined): string | undefined {
if (emoticon === undefined) return undefined
const matchEmoji = findEmoji(e => !isCustomEmoji(e) && (Array.isArray(e.emoticon) ? e.emoticon.includes(emoticon) : e.emoticon === emoticon))
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
}
@ -71,7 +73,10 @@ export function getEmojiByShortCode (shortcode: string | undefined, skinTone?: n
}, skinTone)
}
function findEmoji (predicate: (e: EmojiWithGroup) => boolean | undefined, skinTone?: number): ExtendedEmoji | undefined {
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

View File

@ -433,7 +433,8 @@
}}
/>
{:else}
<Component is={emojiPlugin.component.EmojiPopup}
<Component
is={emojiPlugin.component.EmojiPopup}
props={{
selected: Array.isArray(color) ? fromCodePoint(...color) : color ? fromCodePoint(color) : undefined,
disabled: readonly,

View File

@ -54,6 +54,7 @@
"@hcengineering/presence": "^0.6.0",
"@hcengineering/text-markdown": "^0.6.0",
"@hcengineering/emoji": "^0.6.0",
"@hcengineering/emoji-resources": "^0.6.0",
"@tiptap/core": "^2.11.7",
"@tiptap/pm": "^2.11.7",
"@tiptap/extension-code-block-lowlight": "^2.11.7",

View File

@ -4,12 +4,12 @@ import {
emoticonGlobalRegex,
shortcodeRegex,
shortcodeGlobalRegex,
getEmojiByEmoticon,
getEmojiByShortCode,
emojiRegex,
emojiGlobalRegex,
type ExtendedEmoji, isCustomEmoji
} from '@hcengineering/ui'
type ExtendedEmoji,
isCustomEmoji
} from '@hcengineering/emoji'
import { getEmojiByEmoticon, getEmojiByShortCode } from '@hcengineering/emoji-resources'
import { type ResolvedPos } from '@tiptap/pm/model'
import { type ExtendedRegExpMatchArray, type SingleCommands, type Range, InputRule, PasteRule } from '@tiptap/core'
import { type EditorState } from '@tiptap/pm/state'

View File

@ -22,7 +22,8 @@
getPlatformColor,
getPlatformColorDef,
themeStore,
Label, Component
Label,
Component
} from '@hcengineering/ui'
import emojiPlugin from '@hcengineering/emoji'
import { createEventDispatcher } from 'svelte'
@ -96,7 +97,8 @@
{/if}
</div>
{:else}
<Component is={emojiPlugin.component.EmojiPopup}
<Component
is={emojiPlugin.component.EmojiPopup}
props={{
selected: Array.isArray(color) ? fromCodePoint(...color) : color ? fromCodePoint(color) : undefined
}}