Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How can I add other utf8 characters as custom emojis? #886

Open
ryancwalsh opened this issue Jan 23, 2024 · 4 comments
Open

How can I add other utf8 characters as custom emojis? #886

ryancwalsh opened this issue Jan 23, 2024 · 4 comments

Comments

@ryancwalsh
Copy link

I love emoji-mart. Thanks for offering it. ❤️

I was wondering if there could be a way for me to add sections of "custom" emojis that are really just other utf8 characters and therefore do not have an image (and so I can't populate the "src" property, as https://github.com/missive/emoji-mart/tree/main?tab=readme-ov-file#custom-emojis seems to require).

For example, it would be great if I could create:

Math:
≈±≡≠∞

Arrows:
►◄▲▼🡸🡹🡺🡻🡼🡽🡾🡿

Vegan:
ⓥⓋ🅥

NEAR:
Ⓝ⋈

And emoji-mart would make them findable via the official utf8 names for each character.

E.g. 🡺 is called "wide-headed rightwards heavy barb arrow". https://graphemica.com/%F0%9F%A1%BA

So, 🡺 should appear in my emoji-mart results whenever I search "right arrow".

What do you think?

I would use this feature every day!

Thanks!

@booni3
Copy link

booni3 commented Aug 11, 2024

Did you ever find a solution to this? I am looking for exactly the same thing.

@ryancwalsh
Copy link
Author

@booni3 Yes, here is a screenshot of the cross-platform Electron app I made for myself.

image

You can see that I got the section identifier icons working (up top) but that I didn't find a way to add a section label above each section, so you can see that the final section label is "Flags" (which is built in to emoji-mart), and there are no labels for my extra custom sections afterward.

Also, I think I've been having trouble with the fonts. You can see that the left and right arrows don't look quite right.

I can look for the code later.

@booni3
Copy link

booni3 commented Aug 11, 2024

Would be awesome to see how you did it. Was it within the standard configuration, or did you have to extend the package?

@ryancwalsh
Copy link
Author

It doesn't look like I forked emoji-mart.

"@emoji-mart/data": "^1.1.2",
"@emoji-mart/react": "^1.1.1",

import emojiMartData, {
  type EmojiMartData as EmojiMartDataOriginal,
  type Emoji as EmojiOriginal,
} from '@emoji-mart/data';

import unicodeMap from '../UnicodeMap.json';

export type EmojiId = string;
export type Emoji = EmojiOriginal & {
  id: EmojiId;
  native: string; // Why is this not in the offical type?
};
export type EmojiMartData = EmojiMartDataOriginal;

type EmojiMartSkin = {
  src: string;
};

type IndexedEmojis = {
  [key: string]: emojiMartData.Emoji;
};

export type EmojiMartCustomEmojis = {
  id: string;
  name: string;
  emojis: {
    id: string;
    name: string;
    keywords: string[];
    skins: EmojiMartSkin[];
  }[];
}[];

type CharactersMap = {
  [category: string]: string;
};

const recent = 'recent';
const frequent = 'frequent';
const arrows = 'arrows';
const math = 'math';
const brands = 'twitter';
const vegan = 'vegan';
const customCharactersMap: CharactersMap = {
  // TODO: For these, change font-family to "Segoe UI Symbol" instead of `EmojiMart, "Segoe UI Emoji", "Segoe UI Symbol", "Segoe UI", "Apple Color Emoji", "Twemoji Mozilla", "Noto Color Emoji", "Android Emoji"`
  [arrows]: '►◄▲▼🡸🡹🡺🡻🡼🡽🡾🡿',
  [math]: '≈±≡≠∞',
  [brands]: '𝕏Ⓝ⋈',
  [vegan]: 'ⓥⓋ🅥🌱',
};
const customCharactersCategories = Object.keys(customCharactersMap);

function getCustomEmoji(category: string, character: string): Emoji {
  const id = `${character}`;
  const charCode = character.codePointAt(0); // Use `codePointAt` instead of `charCodeAt` because some characters in Unicode are represented by a pair of code units known as a surrogate pair.
  const zeroPaddedHexCode = charCode
    ?.toString(16)
    .toUpperCase()
    .padStart(4, '0');
  if (!zeroPaddedHexCode) {
    console.error({ character, charCode, zeroPaddedHexCode });
    throw new Error('unicode not found');
  }
  let name = (unicodeMap as CharactersMap)[zeroPaddedHexCode];
  if (!name) {
    console.error('unicode name not found', {
      character,
      charCode,
      zeroPaddedHexCode,
    });
    name = '?';
  }
  console.log({ charCode, zeroPaddedHexCode, name });

  return {
    id,
    native: id,
    name,
    keywords: [category, id, name],
    skins: [
      {
        unified: id,
        native: id,
      },
    ], // TODO
    version: 1, // TODO
  };
}

function getCustomEmojis(
  category: string,
  characters: string[],
): IndexedEmojis {
  const emojis: IndexedEmojis = {};
  // eslint-disable-next-line no-restricted-syntax
  for (const character of characters) {
    console.log({ character });
    const customEmoji = getCustomEmoji(category, character);
    emojis[customEmoji.id] = customEmoji;
  }

  return emojis;
}

function insertCustomCharacters(
  emojiMartDataInner: EmojiMartData,
): EmojiMartData {
  const result = { ...emojiMartDataInner };

  // eslint-disable-next-line no-restricted-syntax
  for (const category of customCharactersCategories) {
    console.log({ category });
    const customCharacters = Array.from(customCharactersMap[category]);
    console.log({ customCharacters });
    const charactersAsEmojis = getCustomEmojis(category, customCharacters);
    const customCharacterEmojiIds = Object.keys(charactersAsEmojis);
    console.log({ charactersAsEmojis, customCharacterEmojiIds });
    result.emojis = { ...charactersAsEmojis, ...result.emojis };
    result.categories.push({
      id: category,
      emojis: customCharacterEmojiIds,
    });
  }

  return result;
}

/**
 * https://missiveapp.com/open/emoji-mart/example-categories.html
 */
export const categoryIcons = {
  [recent]: {
    // https://react-icons.github.io/react-icons/search/#q=clock: BsFillClockFill
    svg: '<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 16 16" height="200px" width="200px" xmlns="http://www.w3.org/2000/svg"><path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM8 3.5a.5.5 0 0 0-1 0V9a.5.5 0 0 0 .252.434l3.5 2a.5.5 0 0 0 .496-.868L8 8.71V3.5z"></path></svg>',
  },
  [frequent]: {
    // https://react-icons.github.io/react-icons/search/#q=heart: BsFillSuitHeartFill
    svg: '<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 16 16" height="200px" width="200px" xmlns="http://www.w3.org/2000/svg"><path d="M4 1c2.21 0 4 1.755 4 3.92C8 2.755 9.79 1 12 1s4 1.755 4 3.92c0 3.263-3.234 4.414-7.608 9.608a.513.513 0 0 1-.784 0C3.234 9.334 0 8.183 0 4.92 0 2.755 1.79 1 4 1z"></path></svg>',
  },
  [arrows]: {
    // https://react-icons.github.io/react-icons/search/#q=FaRightLeft
    svg: '<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 512 512" height="200px" width="200px" xmlns="http://www.w3.org/2000/svg"><path d="M32 96l320 0V32c0-12.9 7.8-24.6 19.8-29.6s25.7-2.2 34.9 6.9l96 96c6 6 9.4 14.1 9.4 22.6s-3.4 16.6-9.4 22.6l-96 96c-9.2 9.2-22.9 11.9-34.9 6.9s-19.8-16.6-19.8-29.6V160L32 160c-17.7 0-32-14.3-32-32s14.3-32 32-32zM480 352c17.7 0 32 14.3 32 32s-14.3 32-32 32H160v64c0 12.9-7.8 24.6-19.8 29.6s-25.7 2.2-34.9-6.9l-96-96c-6-6-9.4-14.1-9.4-22.6s3.4-16.6 9.4-22.6l96-96c9.2-9.2 22.9-11.9 34.9-6.9s19.8 16.6 19.8 29.6l0 64H480z"></path></svg>',
  },
  [math]: {
    // https://react-icons.github.io/react-icons/search/#q=math
    svg: '<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 24 24" height="200px" width="200px" xmlns="http://www.w3.org/2000/svg"><path d="M7 2H5v3H2v2h3v3h2V7h3V5H7V2zm7 3h8v2h-8zm0 10h8v2h-8zm0 4h8v2h-8zm-5.71-4.71L6 16.59l-2.29-2.3-1.42 1.42L4.59 18l-2.3 2.29 1.42 1.42L6 19.41l2.29 2.3 1.42-1.42L7.41 18l2.3-2.29-1.42-1.42z"></path></svg>',
  },
  [brands]: {
    // https://react-icons.github.io/react-icons/search/#q=twitter
    svg: '<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 512 512" height="200px" width="200px" xmlns="http://www.w3.org/2000/svg"><path d="M389.2 48h70.6L305.6 224.2 487 464H345L233.7 318.6 106.5 464H35.8L200.7 275.5 26.8 48H172.4L272.9 180.9 389.2 48zM364.4 421.8h39.1L151.1 88h-42L364.4 421.8z"></path></svg>',
  },
  [vegan]: {
    // https://react-icons.github.io/react-icons/search/#q=plant
    svg: '<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 256 256" height="200px" width="200px" xmlns="http://www.w3.org/2000/svg"><path d="M205.41,159.07a60.9,60.9,0,0,1-31.83,8.86,71.71,71.71,0,0,1-27.36-5.66A55.55,55.55,0,0,0,136,194.51V224a8,8,0,0,1-8.53,8,8.18,8.18,0,0,1-7.47-8.25V211.31L81.38,172.69A52.5,52.5,0,0,1,63.44,176a45.82,45.82,0,0,1-23.92-6.67C17.73,156.09,6,125.62,8.27,87.79a8,8,0,0,1,7.52-7.52c37.83-2.23,68.3,9.46,81.5,31.25A46,46,0,0,1,103.74,140a4,4,0,0,1-6.89,2.43l-19.2-20.1a8,8,0,0,0-11.31,11.31l53.88,55.25c.06-.78.13-1.56.21-2.33a68.56,68.56,0,0,1,18.64-39.46l50.59-53.46a8,8,0,0,0-11.31-11.32l-49,51.82a4,4,0,0,1-6.78-1.74c-4.74-17.48-2.65-34.88,6.4-49.82,17.86-29.48,59.42-45.26,111.18-42.22a8,8,0,0,1,7.52,7.52C250.67,99.65,234.89,141.21,205.41,159.07Z"></path></svg>',
    // https://react-icons.github.io/react-icons/search/#q=vegan
    // '<svg stroke="currentColor" fill="none" stroke-width="2" viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" height="200px" width="200px" xmlns="http://www.w3.org/2000/svg"><path d="M2 2a26.6 26.6 0 0 1 10 20c.9-6.82 1.5-9.5 4-14"></path><path d="M16 8c4 0 6-2 6-6-4 0-6 2-6 6"></path><path d="M17.41 3.6a10 10 0 1 0 3 3"></path></svg>',
  },
  // people: {
  //   src: document.querySelector('#img-indeed').src,
  // },
};

/**
 * See also data.originalCategories.
 */
export const categories = [
  recent,
  frequent,
  // ...custom.map((item) => item.id),
  'people',
  'nature',
  'foods',
  'activity',
  'places',
  'objects',
  'symbols',
  'flags',
  ...customCharactersCategories,
];

const customRecentCategory = {
  // TODO: How to override `node_modules/@emoji-mart/data/i18n/en.json` to define its label?
  id: recent,
  emojis: [],
};

export function ensureDataHasRecentCategoryPrepended(
  data: EmojiMartData,
): EmojiMartData {
  console.log({ data });

  const customData: EmojiMartData = { ...data } as EmojiMartData;

  if (!customData.categories.find((category) => category.id === recent)) {
    const newCategories = [customRecentCategory, ...data.categories];

    customData.categories = newCategories;

    console.log({ customData });
  }

  return customData;
}

function getCustomData(
  emojiMartDataInner: EmojiMartData,
): emojiMartData.EmojiMartData {
  const withExtraCharacters = insertCustomCharacters(emojiMartDataInner);
  return ensureDataHasRecentCategoryPrepended(withExtraCharacters);
}

export const customData = getCustomData(emojiMartData as EmojiMartData);

export function getTruncatedRecentEmojis(
  emojiId: EmojiId,
  recentEmojis: EmojiId[],
  max: number,
): EmojiId[] {
  const newRecentlyUsedEmojisSet = new Set([emojiId, ...recentEmojis]);
  const newRecentlyUsedEmojisArray = Array.from(newRecentlyUsedEmojisSet);
  newRecentlyUsedEmojisArray.length = Math.min(
    newRecentlyUsedEmojisArray.length,
    max,
  );
  console.log({ newRecentlyUsedEmojisArray });

  return newRecentlyUsedEmojisArray;
}

export function overwriteDataWithRecentlyUsedEmoji(
  previousData: EmojiMartData,
  recentEmojis: EmojiId[],
): EmojiMartData {
  console.log('overwriteDataWithRecentlyUsedEmoji', {
    previousData,
    recentEmojis,
  });
  const indexOfRecentlyUsedEmojis = previousData.categories.findIndex(
    (item) => item.id === recent,
  );
  console.log('overwriteDataWithRecentlyUsedEmoji', {
    indexOfRecentlyUsedEmojis,
  });
  const newData = { ...previousData };
  newData.categories[indexOfRecentlyUsedEmojis].emojis = recentEmojis;

  return newData;
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants