Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ A simple Emoji picker that displays all the emojis that GitHub supports. It is a

Use the search field to search for a given emoji. You can click the emoji to get the shortcode on your clipboard or `shift` + click for the Unicode. You can invert the copy behaviour by setting the `copy_type` URL parameter to `unicode` or `shortcode`.

By default only emojis that GitHub supports are shown. Use the switch below the theme and language selectors, or set the `non_github` URL parameter to `true`, to also show newer Unicode emojis that GitHub does not support yet. These have no shortcode, so clicking them always copies the Unicode.

## Contributing

Feel free to open an issue if you have ideas on how to make this repository better or if you want to report a bug! All contributions are welcome. :rocket: Please consult the [contribution guidelines](CONTRIBUTING.md) for more information.
Expand Down
1 change: 1 addition & 0 deletions public/locales/en/translation.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"header.description": "A simple emotion picker that displays all the supported GitHub emojis.",
"header.themeSwitch.description": "Switch to your preferred theme and language.",
"header.nonGithubSwitch.label": "Show emojis GitHub does not support yet",
"footer.description": "Created with",
"localeSelector.translate": "Translate"
}
83 changes: 50 additions & 33 deletions scripts/create_github_emoji_list.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
/**
* @file Fetch the latest emoji data from the GitHub API, compare it with the emojis in
* the 'emoji-datasource' package and generate the `github_emojis.json` and
* `github_custom_emojis.json` data files.
* the 'emoji-datasource' package and generate the `github_emojis.json`,
* `github_custom_emojis.json` and `non_github_emojis.json` data files.
*/
import { mkdir, writeFile } from 'fs';
import inflection from 'inflection'; // Keyword support library.
import emojiLib from 'emojilib' assert { type: 'json' }; // Emoji data search library.
import emojiData from 'emoji-datasource' assert { type: 'json' }; // Multi-OS emoji data.
import unicodeEmoji from 'unicode-emoji-json' assert { type: 'json' }; // Unicode emoji data.
import { Octokit } from '@octokit/core';
import CustomKeyWords from './keywords.json' assert { type: 'json' };
import { mkdir, writeFile } from "fs";
import inflection from "inflection"; // Keyword support library.
import emojiLib from "emojilib" with { type: "json" }; // Emoji data search library.
import emojiData from "emoji-datasource" with { type: "json" }; // Multi-OS emoji data.
import unicodeEmoji from "unicode-emoji-json" with { type: "json" }; // Unicode emoji data.
import { Octokit } from "@octokit/core";
import CustomKeyWords from "./keywords.json" with { type: "json" };
Comment on lines +8 to +12

// Script variables
const DRY_RUN = process.argv.indexOf("--dry") !== -1;
Expand All @@ -25,6 +25,9 @@ const CATEGORIES = [
["Symbols", "symbols"],
["Flags", "flags"],
];
// emoji-mart hides emojis newer than the emoji version it detects in the browser, and
// the newest version it can detect is 15. Clamp so newer emojis are not hidden forever.
const MAX_DETECTABLE_EMOJI_VERSION = 15;
const KEYWORD_SUBSTITUTES = {
highfive: "highfive high-five",
}; // Extend the keyword list with custom keywords.
Expand Down Expand Up @@ -122,42 +125,43 @@ const addGitHubShortName = (emojiObject, githubShortNames) => {
};

/**
* Filter the 'emoji-datasource' package data to only include the GitHub emojis.
* Add the GitHub short names to the 'emoji-datasource' package data. Emojis that
* GitHub does not support keep their 'emoji-datasource' short name and get no
* `github_short_name`.
* @param {Object} githubUnicodeEmojis Object containing the GitHub emoji unicodes.
* @returns {array} Array containing the filtered 'emoji-datasource' package data.
* @returns {array} Array containing the 'emoji-datasource' package data.
* @throws {Error} Throws an error if not all GitHub emojis have a match.
*/
const getFilteredEmojiData = (githubUnicodeEmojis) => {
let filteredEmojis = [];
const getAnnotatedEmojiData = (githubUnicodeEmojis) => {
let notFound = [];

// Loop through GitHub unicodes and try to find a match in the 'emoji-datasource'.
for (const [key, value] of Object.entries(githubUnicodeEmojis)) {
// Try to find match by using unicode.
const unicodeObject = emojiData.find(
(item) => item.unified.toLowerCase() === key
(item) => item.unified.toLowerCase() === key,
);
if (unicodeObject) {
filteredEmojis.push(addGitHubShortName(unicodeObject, value));
addGitHubShortName(unicodeObject, value);
continue;
}

// Try to find match by using non-qualified unicode.
const nonQualifiedObject = emojiData.find(
(item) =>
(item.non_qualified ? item.non_qualified.toLowerCase() : null) === key
(item.non_qualified ? item.non_qualified.toLowerCase() : null) === key,
);
if (nonQualifiedObject) {
filteredEmojis.push(addGitHubShortName(nonQualifiedObject, value));
addGitHubShortName(nonQualifiedObject, value);
continue;
}

// Try to find match by using parsed unicode.
const unicodeObjectParsed = emojiData.find(
(item) => parseEmojiDataUnicode(item.unified.toLowerCase()) === key
(item) => parseEmojiDataUnicode(item.unified.toLowerCase()) === key,
);
if (unicodeObjectParsed) {
filteredEmojis.push(addGitHubShortName(unicodeObjectParsed, value));
addGitHubShortName(unicodeObjectParsed, value);
continue;
}

Expand All @@ -169,11 +173,11 @@ const getFilteredEmojiData = (githubUnicodeEmojis) => {
throw new Error(
`Some GitHub Emojis could not be found in the 'emoji-datasource' package: ${notFound
.flat()
.join(", ")}.`
.join(", ")}.`,
);
}

return filteredEmojis;
return emojiData;
};

/**
Expand All @@ -189,6 +193,7 @@ const buildData = (githubEmojisData) => {
aliases: {},
sheet: { cols: 61, rows: 61 },
};
let nonGithubEmojis = [];

// Add categories.
CATEGORIES.forEach((category, i) => {
Expand All @@ -211,22 +216,22 @@ const buildData = (githubEmojisData) => {
customEmojis: githubCustomEmojis,
} = parseGitHubEmojiData(githubEmojisData);

// Retrieve filtered emoji data from 'emoji-datasource'.
const filteredEmojis = getFilteredEmojiData(githubUnicodeEmojis);
// Annotate the 'emoji-datasource' data with the GitHub short names.
const annotatedEmojis = getAnnotatedEmojiData(githubUnicodeEmojis);

// Make GitHub emojis searchable and create the EmojiMart data source.
filteredEmojis.forEach((datum) => {
// Make emojis searchable and create the EmojiMart data source.
annotatedEmojis.forEach((datum) => {
if (!datum.category)
throw new Error(`“${datum.short_name}” doesn’t have a category.`);

// Retrieve emoji information.
let unified = datum.unified.toLowerCase();
let native = unifiedToNative(unified);
let name = inflection.titleize(
datum.name || datum.short_name.replace(/-/g, " ") || ""
datum.name || datum.short_name.replace(/-/g, " ") || "",
);
let unicodeEmojiName = inflection.titleize(
unicodeEmoji[native]?.name || ""
unicodeEmoji[native]?.name || "",
);
if (
name.indexOf(":") === -1 &&
Expand Down Expand Up @@ -311,10 +316,13 @@ const buildData = (githubEmojisData) => {
// Add version information to emoji.
let addedIn = parseFloat(datum.added_in);
if (addedIn < 1) addedIn = 1;
if (addedIn > MAX_DETECTABLE_EMOJI_VERSION)
addedIn = MAX_DETECTABLE_EMOJI_VERSION;

// Create emoji object.
// Create emoji object. Emojis GitHub does not support keep their datasource id.
const isGithubEmoji = Boolean(datum.github_short_name);
const emoji = {
id: datum.github_short_name,
id: isGithubEmoji ? datum.github_short_name : id,
name,
emoticons,
keywords,
Expand All @@ -329,9 +337,12 @@ const buildData = (githubEmojisData) => {

// Don't add Component emoji category items these are already included as skins.
if (datum.category !== "Component") {
if (data.emojis[emoji.id])
throw new Error(`Duplicate emoji id “${emoji.id}” found.`);
let categoryIndex = categoriesIndex[datum.category];
data.categories[categoryIndex].emojis.push(emoji.id);
data.emojis[emoji.id] = emoji;
if (!isGithubEmoji) nonGithubEmojis.push(emoji.id);
}
});

Expand Down Expand Up @@ -376,18 +387,24 @@ const buildData = (githubEmojisData) => {
JSON.stringify(githubEmojis),
(err) => {
if (err) throw err;
}
},
);
writeFile(
`${folder}/non_github_emojis.json`,
JSON.stringify(nonGithubEmojis),
(err) => {
if (err) throw err;
},
);
});
}
};

/** Main code. */
const run = async () => {
// Retrieve GITHUB_TOKEN from environment variables.
// The emoji endpoint is public; a GITHUB_TOKEN only raises the rate limit.
if (!process.env.GITHUB_TOKEN) {
console.error("No GitHub token found.");
return;
console.warn("No GitHub token found, using unauthenticated requests.");
}

// Get the latest version of the emoji data.
Expand Down
3 changes: 3 additions & 0 deletions scripts/keywords.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@
"bowtie": {
"keywords": ["bowtie", "fabulous", "style", "gentlemen"]
},
"copilot": {
"keywords": ["copilot", "github", "ai", "assistant", "code"]
},
"dependabot": {
"keywords": ["dependabot", "dependency", "security", "ci"]
},
Expand Down
19 changes: 19 additions & 0 deletions src/components/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import App from "./App";
// Mock IntersectionObserver because it is only available in the browser and react lazy
// uses it.
beforeEach(() => {
window.localStorage.clear();
const mockIntersectionObserver = vi.fn();
mockIntersectionObserver.mockReturnValue({
observe: () => null,
Expand Down Expand Up @@ -56,4 +57,22 @@ describe("App", () => {
const textElement = await screen.findByText("GitHub Emoji Picker");
expect(textElement).toBeInTheDocument();
});

it("hides non-GitHub emojis by default", async () => {
render(<App />);
const toggle = await screen.findByRole("checkbox", {
name: "header.nonGithubSwitch.label",
});
expect(toggle).not.toBeChecked();
});

it("shows non-GitHub emojis when the URL parameter is set", async () => {
window.history.replaceState({}, "", "/?non_github=true");
render(<App />);
const toggle = await screen.findByRole("checkbox", {
name: "header.nonGithubSwitch.label",
});
expect(toggle).toBeChecked();
window.history.replaceState({}, "", "/");
});
});
56 changes: 56 additions & 0 deletions src/components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,31 @@ import { Footer } from "@/components/Footer";
import { Header } from "@/components/Header";
import { Snackbar } from "@/components/Snackbar";
import { Loading } from "@/components/Loading";
import nonGithubEmojis from "@/data/non_github_emojis.json";
import { ThemeContext } from "@/store";
import { parseShortCodes, unifiedToUnicodeEmoji } from "@/utils/utils";
import "@/i18n";

const EmojiPicker = lazy(() => import("@/components/EmojiPicker/EmojiPicker"));

/** Ids of the emojis GitHub does not support yet. */
const nonGithubEmojiIds = new Set<string>(nonGithubEmojis);

/**
* Whether emojis GitHub does not support should be shown.
*
* @description The `non_github` URL parameter takes precedence over local storage.
*/
const getShowNonGithub = () => {
const param = new URLSearchParams(window.location.search).get("non_github");
if (param !== null) {
const show = param.toLowerCase() === "true";
window.localStorage.setItem("nonGithub", String(show));
return show;
Comment on lines +33 to +38
}
return window.localStorage.getItem("nonGithub") === "true";
};

/**
* Get the mart locale.
*
Expand Down Expand Up @@ -64,6 +83,11 @@ const App = () => {
undefined,
);
const [copyUnicode, setCopyUnicode] = useState(false); // Whether to copy the unicode instead of the shortcode.
const [showNonGithub] = useState(getShowNonGithub);
const exceptEmojis = useMemo(
() => (showNonGithub ? [] : nonGithubEmojis),
[showNonGithub],
);

/* Store theme mode in local storage. */
useEffect(() => {
Expand Down Expand Up @@ -146,6 +170,22 @@ const App = () => {
* copyUnicode state and whether the shift key is pressed.
*/
const handleEmojiSelect = (selectedEmoji: Emoji, event: PointerEvent) => {
// Emojis GitHub does not support have no shortcode, so always copy the unicode.
if (nonGithubEmojiIds.has(selectedEmoji.id)) {
navigator.clipboard.writeText(
unifiedToUnicodeEmoji(selectedEmoji?.unified),
);
Comment on lines +175 to +177
setSnackPack((prev) => [
...prev,
{
message:
"Emoji 'unicode' copied to clipboard. GitHub does not support this emoji yet, so it has no 'shortcode'.",
key: new Date().getTime(),
},
]);
return;
}

let copyText;
if (event.shiftKey) {
copyText = copyUnicode
Expand Down Expand Up @@ -194,6 +234,19 @@ const App = () => {
setMode(mode === "dark" ? "light" : "dark");
};

/**
* Toggles whether emojis GitHub does not support are shown.
*
* @description Reloads the page because emoji-mart filters its emoji data once,
* when the picker mounts, and cannot add emojis back afterwards.
*/
const toggleNonGithub = () => {
window.localStorage.setItem("nonGithub", String(!showNonGithub));
const url = new URL(window.location.href);
url.searchParams.delete("non_github");
window.location.replace(url);
};

/**
* Changes the UI locale.
*
Expand All @@ -211,6 +264,8 @@ const App = () => {
toggleMode: toggleThemeMode,
locale,
changeLocale,
showNonGithub,
toggleNonGithub,
}}
>
<ThemeProvider theme={themes[mode]}>
Expand All @@ -231,6 +286,7 @@ const App = () => {
<EmojiPicker
onEmojiSelect={handleEmojiSelect}
locale={martLocale}
exceptEmojis={exceptEmojis}
/>
</Grid>
<Grid item>
Expand Down
4 changes: 4 additions & 0 deletions src/components/EmojiPicker/EmojiPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,12 @@ export interface Emoji {
export const EmojiPicker = ({
onEmojiSelect,
locale = "en",
exceptEmojis = [],
}: {
onEmojiSelect: (input: Emoji, event: PointerEvent) => void;
locale?: string;
/** Emoji ids to hide. Only applied when the picker mounts. */
exceptEmojis?: string[];
}) => {
const { mode } = useContext(ThemeContext);
return (
Expand All @@ -42,6 +45,7 @@ export const EmojiPicker = ({
custom={[customGithubEmojis]}
categoryIcons={customEmojiCategories}
onEmojiSelect={onEmojiSelect}
exceptEmojis={exceptEmojis}
theme={mode === "dark" ? "dark" : "light"}
locale={locale}
/>
Expand Down
Loading