diff --git a/web/public/config.js b/web/public/config.js index 5909be46..a748dd84 100644 --- a/web/public/config.js +++ b/web/public/config.js @@ -6,14 +6,24 @@ // During web development, you may change values here for rapid testing. var config = { - base_url: window.location.origin, // Change to test against a different server - app_root: "/app", - enable_login: true, - enable_signup: true, - enable_payments: false, - enable_reservations: true, - enable_emails: true, - enable_calls: true, - billing_contact: "", - disallowed_topics: ["docs", "static", "file", "app", "account", "settings", "signup", "login", "v1"] + base_url: window.location.origin, // Change to test against a different server + app_root: "/app", + enable_login: true, + enable_signup: true, + enable_payments: false, + enable_reservations: true, + enable_emails: true, + enable_calls: true, + billing_contact: "", + disallowed_topics: [ + "docs", + "static", + "file", + "app", + "account", + "settings", + "signup", + "login", + "v1", + ], }; diff --git a/web/public/index.html b/web/public/index.html index dfec1666..31dd280e 100644 --- a/web/public/index.html +++ b/web/public/index.html @@ -1,44 +1,64 @@ - - - ntfy web + + + ntfy web - - - - + + + + - - - - + + + + - - + + - - - - - - - - + + + + + + + + - - + + - - - - - - -
- - + + + + + + +
+ + diff --git a/web/public/static/css/app.css b/web/public/static/css/app.css index 12b105a2..213859c0 100644 --- a/web/public/static/css/app.css +++ b/web/public/static/css/app.css @@ -1,10 +1,11 @@ /* web app styling overrides */ -a, a:visited { - color: #338574; +a, +a:visited { + color: #338574; } a:hover { - text-decoration: none; - color: #317f6f; + text-decoration: none; + color: #317f6f; } diff --git a/web/public/static/css/fonts.css b/web/public/static/css/fonts.css index 4245d0f5..2cf00a3c 100644 --- a/web/public/static/css/fonts.css +++ b/web/public/static/css/fonts.css @@ -2,36 +2,32 @@ /* roboto-300 - latin */ @font-face { - font-family: 'Roboto'; - font-style: normal; - font-weight: 300; - src: local(''), - url('../fonts/roboto-v29-latin-300.woff2') format('woff2'); + font-family: "Roboto"; + font-style: normal; + font-weight: 300; + src: local(""), url("../fonts/roboto-v29-latin-300.woff2") format("woff2"); } /* roboto-regular - latin */ @font-face { - font-family: 'Roboto'; - font-style: normal; - font-weight: 400; - src: local(''), - url('../fonts/roboto-v29-latin-regular.woff2') format('woff2'); + font-family: "Roboto"; + font-style: normal; + font-weight: 400; + src: local(""), url("../fonts/roboto-v29-latin-regular.woff2") format("woff2"); } /* roboto-500 - latin */ @font-face { - font-family: 'Roboto'; - font-style: normal; - font-weight: 500; - src: local(''), - url('../fonts/roboto-v29-latin-500.woff2') format('woff2'); + font-family: "Roboto"; + font-style: normal; + font-weight: 500; + src: local(""), url("../fonts/roboto-v29-latin-500.woff2") format("woff2"); } /* roboto-700 - latin */ @font-face { - font-family: 'Roboto'; - font-style: normal; - font-weight: 700; - src: local(''), - url('../fonts/roboto-v29-latin-700.woff2') format('woff2'); + font-family: "Roboto"; + font-style: normal; + font-weight: 700; + src: local(""), url("../fonts/roboto-v29-latin-700.woff2") format("woff2"); } diff --git a/web/src/app/AccountApi.js b/web/src/app/AccountApi.js index 915e3bb8..3f116114 100644 --- a/web/src/app/AccountApi.js +++ b/web/src/app/AccountApi.js @@ -1,429 +1,442 @@ import { - accountBillingPortalUrl, - accountBillingSubscriptionUrl, - accountPasswordUrl, - accountPhoneUrl, - accountPhoneVerifyUrl, - accountReservationSingleUrl, - accountReservationUrl, - accountSettingsUrl, - accountSubscriptionUrl, - accountTokenUrl, - accountUrl, - maybeWithBearerAuth, - tiersUrl, - withBasicAuth, - withBearerAuth + accountBillingPortalUrl, + accountBillingSubscriptionUrl, + accountPasswordUrl, + accountPhoneUrl, + accountPhoneVerifyUrl, + accountReservationSingleUrl, + accountReservationUrl, + accountSettingsUrl, + accountSubscriptionUrl, + accountTokenUrl, + accountUrl, + maybeWithBearerAuth, + tiersUrl, + withBasicAuth, + withBearerAuth, } from "./utils"; import session from "./Session"; import subscriptionManager from "./SubscriptionManager"; import i18n from "i18next"; import prefs from "./Prefs"; import routes from "../components/routes"; -import {fetchOrThrow, UnauthorizedError} from "./errors"; +import { fetchOrThrow, UnauthorizedError } from "./errors"; const delayMillis = 45000; // 45 seconds const intervalMillis = 900000; // 15 minutes class AccountApi { - constructor() { - this.timer = null; - this.listener = null; // Fired when account is fetched from remote - this.tiers = null; // Cached - } + constructor() { + this.timer = null; + this.listener = null; // Fired when account is fetched from remote + this.tiers = null; // Cached + } - registerListener(listener) { - this.listener = listener; - } + registerListener(listener) { + this.listener = listener; + } - resetListener() { - this.listener = null; - } + resetListener() { + this.listener = null; + } - async login(user) { - const url = accountTokenUrl(config.base_url); - console.log(`[AccountApi] Checking auth for ${url}`); - const response = await fetchOrThrow(url, { - method: "POST", - headers: withBasicAuth({}, user.username, user.password) - }); - const json = await response.json(); // May throw SyntaxError - if (!json.token) { - throw new Error(`Unexpected server response: Cannot find token`); + async login(user) { + const url = accountTokenUrl(config.base_url); + console.log(`[AccountApi] Checking auth for ${url}`); + const response = await fetchOrThrow(url, { + method: "POST", + headers: withBasicAuth({}, user.username, user.password), + }); + const json = await response.json(); // May throw SyntaxError + if (!json.token) { + throw new Error(`Unexpected server response: Cannot find token`); + } + return json.token; + } + + async logout() { + const url = accountTokenUrl(config.base_url); + console.log( + `[AccountApi] Logging out from ${url} using token ${session.token()}` + ); + await fetchOrThrow(url, { + method: "DELETE", + headers: withBearerAuth({}, session.token()), + }); + } + + async create(username, password) { + const url = accountUrl(config.base_url); + const body = JSON.stringify({ + username: username, + password: password, + }); + console.log(`[AccountApi] Creating user account ${url}`); + await fetchOrThrow(url, { + method: "POST", + body: body, + }); + } + + async get() { + const url = accountUrl(config.base_url); + console.log(`[AccountApi] Fetching user account ${url}`); + const response = await fetchOrThrow(url, { + headers: maybeWithBearerAuth({}, session.token()), // GET /v1/account endpoint can be called by anonymous + }); + const account = await response.json(); // May throw SyntaxError + console.log(`[AccountApi] Account`, account); + if (this.listener) { + this.listener(account); + } + return account; + } + + async delete(password) { + const url = accountUrl(config.base_url); + console.log(`[AccountApi] Deleting user account ${url}`); + await fetchOrThrow(url, { + method: "DELETE", + headers: withBearerAuth({}, session.token()), + body: JSON.stringify({ + password: password, + }), + }); + } + + async changePassword(currentPassword, newPassword) { + const url = accountPasswordUrl(config.base_url); + console.log(`[AccountApi] Changing account password ${url}`); + await fetchOrThrow(url, { + method: "POST", + headers: withBearerAuth({}, session.token()), + body: JSON.stringify({ + password: currentPassword, + new_password: newPassword, + }), + }); + } + + async createToken(label, expires) { + const url = accountTokenUrl(config.base_url); + const body = { + label: label, + expires: expires > 0 ? Math.floor(Date.now() / 1000) + expires : 0, + }; + console.log(`[AccountApi] Creating user access token ${url}`); + await fetchOrThrow(url, { + method: "POST", + headers: withBearerAuth({}, session.token()), + body: JSON.stringify(body), + }); + } + + async updateToken(token, label, expires) { + const url = accountTokenUrl(config.base_url); + const body = { + token: token, + label: label, + }; + if (expires > 0) { + body.expires = Math.floor(Date.now() / 1000) + expires; + } + console.log(`[AccountApi] Creating user access token ${url}`); + await fetchOrThrow(url, { + method: "PATCH", + headers: withBearerAuth({}, session.token()), + body: JSON.stringify(body), + }); + } + + async extendToken() { + const url = accountTokenUrl(config.base_url); + console.log(`[AccountApi] Extending user access token ${url}`); + await fetchOrThrow(url, { + method: "PATCH", + headers: withBearerAuth({}, session.token()), + }); + } + + async deleteToken(token) { + const url = accountTokenUrl(config.base_url); + console.log(`[AccountApi] Deleting user access token ${url}`); + await fetchOrThrow(url, { + method: "DELETE", + headers: withBearerAuth({ "X-Token": token }, session.token()), + }); + } + + async updateSettings(payload) { + const url = accountSettingsUrl(config.base_url); + const body = JSON.stringify(payload); + console.log(`[AccountApi] Updating user account ${url}: ${body}`); + await fetchOrThrow(url, { + method: "PATCH", + headers: withBearerAuth({}, session.token()), + body: body, + }); + } + + async addSubscription(baseUrl, topic) { + const url = accountSubscriptionUrl(config.base_url); + const body = JSON.stringify({ + base_url: baseUrl, + topic: topic, + }); + console.log(`[AccountApi] Adding user subscription ${url}: ${body}`); + const response = await fetchOrThrow(url, { + method: "POST", + headers: withBearerAuth({}, session.token()), + body: body, + }); + const subscription = await response.json(); // May throw SyntaxError + console.log(`[AccountApi] Subscription`, subscription); + return subscription; + } + + async updateSubscription(baseUrl, topic, payload) { + const url = accountSubscriptionUrl(config.base_url); + const body = JSON.stringify({ + base_url: baseUrl, + topic: topic, + ...payload, + }); + console.log(`[AccountApi] Updating user subscription ${url}: ${body}`); + const response = await fetchOrThrow(url, { + method: "PATCH", + headers: withBearerAuth({}, session.token()), + body: body, + }); + const subscription = await response.json(); // May throw SyntaxError + console.log(`[AccountApi] Subscription`, subscription); + return subscription; + } + + async deleteSubscription(baseUrl, topic) { + const url = accountSubscriptionUrl(config.base_url); + console.log(`[AccountApi] Removing user subscription ${url}`); + const headers = { + "X-BaseURL": baseUrl, + "X-Topic": topic, + }; + await fetchOrThrow(url, { + method: "DELETE", + headers: withBearerAuth(headers, session.token()), + }); + } + + async upsertReservation(topic, everyone) { + const url = accountReservationUrl(config.base_url); + console.log( + `[AccountApi] Upserting user access to topic ${topic}, everyone=${everyone}` + ); + await fetchOrThrow(url, { + method: "POST", + headers: withBearerAuth({}, session.token()), + body: JSON.stringify({ + topic: topic, + everyone: everyone, + }), + }); + } + + async deleteReservation(topic, deleteMessages) { + const url = accountReservationSingleUrl(config.base_url, topic); + console.log(`[AccountApi] Removing topic reservation ${url}`); + const headers = { + "X-Delete-Messages": deleteMessages ? "true" : "false", + }; + await fetchOrThrow(url, { + method: "DELETE", + headers: withBearerAuth(headers, session.token()), + }); + } + + async billingTiers() { + if (this.tiers) { + return this.tiers; + } + const url = tiersUrl(config.base_url); + console.log(`[AccountApi] Fetching billing tiers`); + const response = await fetchOrThrow(url); // No auth needed! + this.tiers = await response.json(); // May throw SyntaxError + return this.tiers; + } + + async createBillingSubscription(tier, interval) { + console.log( + `[AccountApi] Creating billing subscription with ${tier} and interval ${interval}` + ); + return await this.upsertBillingSubscription("POST", tier, interval); + } + + async updateBillingSubscription(tier, interval) { + console.log( + `[AccountApi] Updating billing subscription with ${tier} and interval ${interval}` + ); + return await this.upsertBillingSubscription("PUT", tier, interval); + } + + async upsertBillingSubscription(method, tier, interval) { + const url = accountBillingSubscriptionUrl(config.base_url); + const response = await fetchOrThrow(url, { + method: method, + headers: withBearerAuth({}, session.token()), + body: JSON.stringify({ + tier: tier, + interval: interval, + }), + }); + return await response.json(); // May throw SyntaxError + } + + async deleteBillingSubscription() { + const url = accountBillingSubscriptionUrl(config.base_url); + console.log(`[AccountApi] Cancelling billing subscription`); + await fetchOrThrow(url, { + method: "DELETE", + headers: withBearerAuth({}, session.token()), + }); + } + + async createBillingPortalSession() { + const url = accountBillingPortalUrl(config.base_url); + console.log(`[AccountApi] Creating billing portal session`); + const response = await fetchOrThrow(url, { + method: "POST", + headers: withBearerAuth({}, session.token()), + }); + return await response.json(); // May throw SyntaxError + } + + async verifyPhoneNumber(phoneNumber, channel) { + const url = accountPhoneVerifyUrl(config.base_url); + console.log(`[AccountApi] Sending phone verification ${url}`); + await fetchOrThrow(url, { + method: "PUT", + headers: withBearerAuth({}, session.token()), + body: JSON.stringify({ + number: phoneNumber, + channel: channel, + }), + }); + } + + async addPhoneNumber(phoneNumber, code) { + const url = accountPhoneUrl(config.base_url); + console.log( + `[AccountApi] Adding phone number with verification code ${url}` + ); + await fetchOrThrow(url, { + method: "PUT", + headers: withBearerAuth({}, session.token()), + body: JSON.stringify({ + number: phoneNumber, + code: code, + }), + }); + } + + async deletePhoneNumber(phoneNumber, code) { + const url = accountPhoneUrl(config.base_url); + console.log(`[AccountApi] Deleting phone number ${url}`); + await fetchOrThrow(url, { + method: "DELETE", + headers: withBearerAuth({}, session.token()), + body: JSON.stringify({ + number: phoneNumber, + }), + }); + } + + async sync() { + try { + if (!session.token()) { + return null; + } + console.log(`[AccountApi] Syncing account`); + const account = await this.get(); + if (account.language) { + await i18n.changeLanguage(account.language); + } + if (account.notification) { + if (account.notification.sound) { + await prefs.setSound(account.notification.sound); } - return json.token; - } - - async logout() { - const url = accountTokenUrl(config.base_url); - console.log(`[AccountApi] Logging out from ${url} using token ${session.token()}`); - await fetchOrThrow(url, { - method: "DELETE", - headers: withBearerAuth({}, session.token()) - }); - } - - async create(username, password) { - const url = accountUrl(config.base_url); - const body = JSON.stringify({ - username: username, - password: password - }); - console.log(`[AccountApi] Creating user account ${url}`); - await fetchOrThrow(url, { - method: "POST", - body: body - }); - } - - async get() { - const url = accountUrl(config.base_url); - console.log(`[AccountApi] Fetching user account ${url}`); - const response = await fetchOrThrow(url, { - headers: maybeWithBearerAuth({}, session.token()) // GET /v1/account endpoint can be called by anonymous - }); - const account = await response.json(); // May throw SyntaxError - console.log(`[AccountApi] Account`, account); - if (this.listener) { - this.listener(account); + if (account.notification.delete_after) { + await prefs.setDeleteAfter(account.notification.delete_after); } - return account; - } - - async delete(password) { - const url = accountUrl(config.base_url); - console.log(`[AccountApi] Deleting user account ${url}`); - await fetchOrThrow(url, { - method: "DELETE", - headers: withBearerAuth({}, session.token()), - body: JSON.stringify({ - password: password - }) - }); - } - - async changePassword(currentPassword, newPassword) { - const url = accountPasswordUrl(config.base_url); - console.log(`[AccountApi] Changing account password ${url}`); - await fetchOrThrow(url, { - method: "POST", - headers: withBearerAuth({}, session.token()), - body: JSON.stringify({ - password: currentPassword, - new_password: newPassword - }) - }); - } - - async createToken(label, expires) { - const url = accountTokenUrl(config.base_url); - const body = { - label: label, - expires: (expires > 0) ? Math.floor(Date.now() / 1000) + expires : 0 - }; - console.log(`[AccountApi] Creating user access token ${url}`); - await fetchOrThrow(url, { - method: "POST", - headers: withBearerAuth({}, session.token()), - body: JSON.stringify(body) - }); - } - - async updateToken(token, label, expires) { - const url = accountTokenUrl(config.base_url); - const body = { - token: token, - label: label - }; - if (expires > 0) { - body.expires = Math.floor(Date.now() / 1000) + expires; + if (account.notification.min_priority) { + await prefs.setMinPriority(account.notification.min_priority); } - console.log(`[AccountApi] Creating user access token ${url}`); - await fetchOrThrow(url, { - method: "PATCH", - headers: withBearerAuth({}, session.token()), - body: JSON.stringify(body) - }); + } + if (account.subscriptions) { + await subscriptionManager.syncFromRemote( + account.subscriptions, + account.reservations + ); + } + return account; + } catch (e) { + console.log(`[AccountApi] Error fetching account`, e); + if (e instanceof UnauthorizedError) { + session.resetAndRedirect(routes.login); + } } + } - async extendToken() { - const url = accountTokenUrl(config.base_url); - console.log(`[AccountApi] Extending user access token ${url}`); - await fetchOrThrow(url, { - method: "PATCH", - headers: withBearerAuth({}, session.token()) - }); + startWorker() { + if (this.timer !== null) { + return; } + console.log(`[AccountApi] Starting worker`); + this.timer = setInterval(() => this.runWorker(), intervalMillis); + setTimeout(() => this.runWorker(), delayMillis); + } - async deleteToken(token) { - const url = accountTokenUrl(config.base_url); - console.log(`[AccountApi] Deleting user access token ${url}`); - await fetchOrThrow(url, { - method: "DELETE", - headers: withBearerAuth({"X-Token": token}, session.token()) - }); + async runWorker() { + if (!session.token()) { + return; } - - async updateSettings(payload) { - const url = accountSettingsUrl(config.base_url); - const body = JSON.stringify(payload); - console.log(`[AccountApi] Updating user account ${url}: ${body}`); - await fetchOrThrow(url, { - method: "PATCH", - headers: withBearerAuth({}, session.token()), - body: body - }); - } - - async addSubscription(baseUrl, topic) { - const url = accountSubscriptionUrl(config.base_url); - const body = JSON.stringify({ - base_url: baseUrl, - topic: topic - }); - console.log(`[AccountApi] Adding user subscription ${url}: ${body}`); - const response = await fetchOrThrow(url, { - method: "POST", - headers: withBearerAuth({}, session.token()), - body: body - }); - const subscription = await response.json(); // May throw SyntaxError - console.log(`[AccountApi] Subscription`, subscription); - return subscription; - } - - async updateSubscription(baseUrl, topic, payload) { - const url = accountSubscriptionUrl(config.base_url); - const body = JSON.stringify({ - base_url: baseUrl, - topic: topic, - ...payload - }); - console.log(`[AccountApi] Updating user subscription ${url}: ${body}`); - const response = await fetchOrThrow(url, { - method: "PATCH", - headers: withBearerAuth({}, session.token()), - body: body - }); - const subscription = await response.json(); // May throw SyntaxError - console.log(`[AccountApi] Subscription`, subscription); - return subscription; - } - - async deleteSubscription(baseUrl, topic) { - const url = accountSubscriptionUrl(config.base_url); - console.log(`[AccountApi] Removing user subscription ${url}`); - const headers = { - "X-BaseURL": baseUrl, - "X-Topic": topic, - } - await fetchOrThrow(url, { - method: "DELETE", - headers: withBearerAuth(headers, session.token()), - }); - } - - async upsertReservation(topic, everyone) { - const url = accountReservationUrl(config.base_url); - console.log(`[AccountApi] Upserting user access to topic ${topic}, everyone=${everyone}`); - await fetchOrThrow(url, { - method: "POST", - headers: withBearerAuth({}, session.token()), - body: JSON.stringify({ - topic: topic, - everyone: everyone - }) - }); - } - - async deleteReservation(topic, deleteMessages) { - const url = accountReservationSingleUrl(config.base_url, topic); - console.log(`[AccountApi] Removing topic reservation ${url}`); - const headers = { - "X-Delete-Messages": deleteMessages ? "true" : "false" - } - await fetchOrThrow(url, { - method: "DELETE", - headers: withBearerAuth(headers, session.token()) - }); - } - - async billingTiers() { - if (this.tiers) { - return this.tiers; - } - const url = tiersUrl(config.base_url); - console.log(`[AccountApi] Fetching billing tiers`); - const response = await fetchOrThrow(url); // No auth needed! - this.tiers = await response.json(); // May throw SyntaxError - return this.tiers; - } - - async createBillingSubscription(tier, interval) { - console.log(`[AccountApi] Creating billing subscription with ${tier} and interval ${interval}`); - return await this.upsertBillingSubscription("POST", tier, interval) - } - - async updateBillingSubscription(tier, interval) { - console.log(`[AccountApi] Updating billing subscription with ${tier} and interval ${interval}`); - return await this.upsertBillingSubscription("PUT", tier, interval) - } - - async upsertBillingSubscription(method, tier, interval) { - const url = accountBillingSubscriptionUrl(config.base_url); - const response = await fetchOrThrow(url, { - method: method, - headers: withBearerAuth({}, session.token()), - body: JSON.stringify({ - tier: tier, - interval: interval - }) - }); - return await response.json(); // May throw SyntaxError - } - - async deleteBillingSubscription() { - const url = accountBillingSubscriptionUrl(config.base_url); - console.log(`[AccountApi] Cancelling billing subscription`); - await fetchOrThrow(url, { - method: "DELETE", - headers: withBearerAuth({}, session.token()) - }); - } - - async createBillingPortalSession() { - const url = accountBillingPortalUrl(config.base_url); - console.log(`[AccountApi] Creating billing portal session`); - const response = await fetchOrThrow(url, { - method: "POST", - headers: withBearerAuth({}, session.token()) - }); - return await response.json(); // May throw SyntaxError - } - - async verifyPhoneNumber(phoneNumber, channel) { - const url = accountPhoneVerifyUrl(config.base_url); - console.log(`[AccountApi] Sending phone verification ${url}`); - await fetchOrThrow(url, { - method: "PUT", - headers: withBearerAuth({}, session.token()), - body: JSON.stringify({ - number: phoneNumber, - channel: channel - }) - }); - } - - async addPhoneNumber(phoneNumber, code) { - const url = accountPhoneUrl(config.base_url); - console.log(`[AccountApi] Adding phone number with verification code ${url}`); - await fetchOrThrow(url, { - method: "PUT", - headers: withBearerAuth({}, session.token()), - body: JSON.stringify({ - number: phoneNumber, - code: code - }) - }); - } - - async deletePhoneNumber(phoneNumber, code) { - const url = accountPhoneUrl(config.base_url); - console.log(`[AccountApi] Deleting phone number ${url}`); - await fetchOrThrow(url, { - method: "DELETE", - headers: withBearerAuth({}, session.token()), - body: JSON.stringify({ - number: phoneNumber - }) - }); - } - - async sync() { - try { - if (!session.token()) { - return null; - } - console.log(`[AccountApi] Syncing account`); - const account = await this.get(); - if (account.language) { - await i18n.changeLanguage(account.language); - } - if (account.notification) { - if (account.notification.sound) { - await prefs.setSound(account.notification.sound); - } - if (account.notification.delete_after) { - await prefs.setDeleteAfter(account.notification.delete_after); - } - if (account.notification.min_priority) { - await prefs.setMinPriority(account.notification.min_priority); - } - } - if (account.subscriptions) { - await subscriptionManager.syncFromRemote(account.subscriptions, account.reservations); - } - return account; - } catch (e) { - console.log(`[AccountApi] Error fetching account`, e); - if (e instanceof UnauthorizedError) { - session.resetAndRedirect(routes.login); - } - } - } - - startWorker() { - if (this.timer !== null) { - return; - } - console.log(`[AccountApi] Starting worker`); - this.timer = setInterval(() => this.runWorker(), intervalMillis); - setTimeout(() => this.runWorker(), delayMillis); - } - - async runWorker() { - if (!session.token()) { - return; - } - console.log(`[AccountApi] Extending user access token`); - try { - await this.extendToken(); - } catch (e) { - console.log(`[AccountApi] Error extending user access token`, e); - } + console.log(`[AccountApi] Extending user access token`); + try { + await this.extendToken(); + } catch (e) { + console.log(`[AccountApi] Error extending user access token`, e); } + } } // Maps to user.Role in user/types.go export const Role = { - ADMIN: "admin", - USER: "user" + ADMIN: "admin", + USER: "user", }; // Maps to server.visitorLimitBasis in server/visitor.go export const LimitBasis = { - IP: "ip", - TIER: "tier" + IP: "ip", + TIER: "tier", }; // Maps to stripe.SubscriptionStatus export const SubscriptionStatus = { - ACTIVE: "active", - PAST_DUE: "past_due" + ACTIVE: "active", + PAST_DUE: "past_due", }; // Maps to stripe.PriceRecurringInterval export const SubscriptionInterval = { - MONTH: "month", - YEAR: "year" + MONTH: "month", + YEAR: "year", }; // Maps to user.Permission in user/types.go export const Permission = { - READ_WRITE: "read-write", - READ_ONLY: "read-only", - WRITE_ONLY: "write-only", - DENY_ALL: "deny-all" + READ_WRITE: "read-write", + READ_ONLY: "read-only", + WRITE_ONLY: "write-only", + DENY_ALL: "deny-all", }; const accountApi = new AccountApi(); diff --git a/web/src/app/Api.js b/web/src/app/Api.js index 59bd78b8..345b0f22 100644 --- a/web/src/app/Api.js +++ b/web/src/app/Api.js @@ -1,118 +1,125 @@ import { - fetchLinesIterator, - maybeWithAuth, - topicShortUrl, - topicUrl, - topicUrlAuth, - topicUrlJsonPoll, - topicUrlJsonPollWithSince + fetchLinesIterator, + maybeWithAuth, + topicShortUrl, + topicUrl, + topicUrlAuth, + topicUrlJsonPoll, + topicUrlJsonPollWithSince, } from "./utils"; import userManager from "./UserManager"; -import {fetchOrThrow} from "./errors"; +import { fetchOrThrow } from "./errors"; class Api { - async poll(baseUrl, topic, since) { - const user = await userManager.get(baseUrl); - const shortUrl = topicShortUrl(baseUrl, topic); - const url = (since) - ? topicUrlJsonPollWithSince(baseUrl, topic, since) - : topicUrlJsonPoll(baseUrl, topic); - const messages = []; - const headers = maybeWithAuth({}, user); - console.log(`[Api] Polling ${url}`); - for await (let line of fetchLinesIterator(url, headers)) { - const message = JSON.parse(line); - if (message.id) { - console.log(`[Api, ${shortUrl}] Received message ${line}`); - messages.push(message); - } - } - return messages; + async poll(baseUrl, topic, since) { + const user = await userManager.get(baseUrl); + const shortUrl = topicShortUrl(baseUrl, topic); + const url = since + ? topicUrlJsonPollWithSince(baseUrl, topic, since) + : topicUrlJsonPoll(baseUrl, topic); + const messages = []; + const headers = maybeWithAuth({}, user); + console.log(`[Api] Polling ${url}`); + for await (let line of fetchLinesIterator(url, headers)) { + const message = JSON.parse(line); + if (message.id) { + console.log(`[Api, ${shortUrl}] Received message ${line}`); + messages.push(message); + } } + return messages; + } - async publish(baseUrl, topic, message, options) { - const user = await userManager.get(baseUrl); - console.log(`[Api] Publishing message to ${topicUrl(baseUrl, topic)}`); - const headers = {}; - const body = { - topic: topic, - message: message, - ...options - }; - await fetchOrThrow(baseUrl, { - method: 'PUT', - body: JSON.stringify(body), - headers: maybeWithAuth(headers, user) - }); - } + async publish(baseUrl, topic, message, options) { + const user = await userManager.get(baseUrl); + console.log(`[Api] Publishing message to ${topicUrl(baseUrl, topic)}`); + const headers = {}; + const body = { + topic: topic, + message: message, + ...options, + }; + await fetchOrThrow(baseUrl, { + method: "PUT", + body: JSON.stringify(body), + headers: maybeWithAuth(headers, user), + }); + } - /** - * Publishes to a topic using XMLHttpRequest (XHR), and returns a Promise with the active request. - * Unfortunately, fetch() does not support a progress hook, which is why XHR has to be used. - * - * Firefox XHR bug: - * Firefox has a bug(?), which returns 0 and "" for all fields of the XHR response in the case of an error, - * so we cannot determine the exact error. It also sometimes complains about CORS violations, even when the - * correct headers are clearly set. It's quite the odd behavior. - * - * There is an example, and the bug report here: - * - https://bugzilla.mozilla.org/show_bug.cgi?id=1733755 - * - https://gist.github.com/binwiederhier/627f146d1959799be207ad8c17a8f345 - */ - publishXHR(url, body, headers, onProgress) { - console.log(`[Api] Publishing message to ${url}`); - const xhr = new XMLHttpRequest(); - const send = new Promise(function (resolve, reject) { - xhr.open("PUT", url); - if (body.type) { - xhr.overrideMimeType(body.type); + /** + * Publishes to a topic using XMLHttpRequest (XHR), and returns a Promise with the active request. + * Unfortunately, fetch() does not support a progress hook, which is why XHR has to be used. + * + * Firefox XHR bug: + * Firefox has a bug(?), which returns 0 and "" for all fields of the XHR response in the case of an error, + * so we cannot determine the exact error. It also sometimes complains about CORS violations, even when the + * correct headers are clearly set. It's quite the odd behavior. + * + * There is an example, and the bug report here: + * - https://bugzilla.mozilla.org/show_bug.cgi?id=1733755 + * - https://gist.github.com/binwiederhier/627f146d1959799be207ad8c17a8f345 + */ + publishXHR(url, body, headers, onProgress) { + console.log(`[Api] Publishing message to ${url}`); + const xhr = new XMLHttpRequest(); + const send = new Promise(function (resolve, reject) { + xhr.open("PUT", url); + if (body.type) { + xhr.overrideMimeType(body.type); + } + for (const [key, value] of Object.entries(headers)) { + xhr.setRequestHeader(key, value); + } + xhr.upload.addEventListener("progress", onProgress); + xhr.addEventListener("readystatechange", () => { + if (xhr.readyState === 4 && xhr.status >= 200 && xhr.status <= 299) { + console.log( + `[Api] Publish successful (HTTP ${xhr.status})`, + xhr.response + ); + resolve(xhr.response); + } else if (xhr.readyState === 4) { + // Firefox bug; see description above! + console.log( + `[Api] Publish failed (HTTP ${xhr.status})`, + xhr.responseText + ); + let errorText; + try { + const error = JSON.parse(xhr.responseText); + if (error.code && error.error) { + errorText = `Error ${error.code}: ${error.error}`; } - for (const [key, value] of Object.entries(headers)) { - xhr.setRequestHeader(key, value); - } - xhr.upload.addEventListener("progress", onProgress); - xhr.addEventListener('readystatechange', () => { - if (xhr.readyState === 4 && xhr.status >= 200 && xhr.status <= 299) { - console.log(`[Api] Publish successful (HTTP ${xhr.status})`, xhr.response); - resolve(xhr.response); - } else if (xhr.readyState === 4) { - // Firefox bug; see description above! - console.log(`[Api] Publish failed (HTTP ${xhr.status})`, xhr.responseText); - let errorText; - try { - const error = JSON.parse(xhr.responseText); - if (error.code && error.error) { - errorText = `Error ${error.code}: ${error.error}`; - } - } catch (e) { - // Nothing - } - xhr.abort(); - reject(errorText ?? "An error occurred"); - } - }) - xhr.send(body); - }); - send.abort = () => { - console.log(`[Api] Publish aborted by user`); - xhr.abort(); + } catch (e) { + // Nothing + } + xhr.abort(); + reject(errorText ?? "An error occurred"); } - return send; - } + }); + xhr.send(body); + }); + send.abort = () => { + console.log(`[Api] Publish aborted by user`); + xhr.abort(); + }; + return send; + } - async topicAuth(baseUrl, topic, user) { - const url = topicUrlAuth(baseUrl, topic); - console.log(`[Api] Checking auth for ${url}`); - const response = await fetch(url, { - headers: maybeWithAuth({}, user) - }); - if (response.status >= 200 && response.status <= 299) { - return true; - } else if (response.status === 401 || response.status === 403) { // See server/server.go - return false; - } - throw new Error(`Unexpected server response ${response.status}`); + async topicAuth(baseUrl, topic, user) { + const url = topicUrlAuth(baseUrl, topic); + console.log(`[Api] Checking auth for ${url}`); + const response = await fetch(url, { + headers: maybeWithAuth({}, user), + }); + if (response.status >= 200 && response.status <= 299) { + return true; + } else if (response.status === 401 || response.status === 403) { + // See server/server.go + return false; } + throw new Error(`Unexpected server response ${response.status}`); + } } const api = new Api(); diff --git a/web/src/app/Connection.js b/web/src/app/Connection.js index e86af78a..5dfc41ba 100644 --- a/web/src/app/Connection.js +++ b/web/src/app/Connection.js @@ -1,4 +1,10 @@ -import {basicAuth, bearerAuth, encodeBase64Url, topicShortUrl, topicUrlWs} from "./utils"; +import { + basicAuth, + bearerAuth, + encodeBase64Url, + topicShortUrl, + topicUrlWs, +} from "./utils"; const retryBackoffSeconds = [5, 10, 20, 30, 60, 120]; @@ -9,110 +15,142 @@ const retryBackoffSeconds = [5, 10, 20, 30, 60, 120]; * Incoming messages and state changes are forwarded via listeners. */ class Connection { - constructor(connectionId, subscriptionId, baseUrl, topic, user, since, onNotification, onStateChanged) { - this.connectionId = connectionId; - this.subscriptionId = subscriptionId; - this.baseUrl = baseUrl; - this.topic = topic; - this.user = user; - this.since = since; - this.shortUrl = topicShortUrl(baseUrl, topic); - this.onNotification = onNotification; - this.onStateChanged = onStateChanged; + constructor( + connectionId, + subscriptionId, + baseUrl, + topic, + user, + since, + onNotification, + onStateChanged + ) { + this.connectionId = connectionId; + this.subscriptionId = subscriptionId; + this.baseUrl = baseUrl; + this.topic = topic; + this.user = user; + this.since = since; + this.shortUrl = topicShortUrl(baseUrl, topic); + this.onNotification = onNotification; + this.onStateChanged = onStateChanged; + this.ws = null; + this.retryCount = 0; + this.retryTimeout = null; + } + + start() { + // Don't fetch old messages; we do that as a poll() when adding a subscription; + // we don't want to re-trigger the main view re-render potentially hundreds of times. + + const wsUrl = this.wsUrl(); + console.log( + `[Connection, ${this.shortUrl}, ${this.connectionId}] Opening connection to ${wsUrl}` + ); + + this.ws = new WebSocket(wsUrl); + this.ws.onopen = (event) => { + console.log( + `[Connection, ${this.shortUrl}, ${this.connectionId}] Connection established`, + event + ); + this.retryCount = 0; + this.onStateChanged(this.subscriptionId, ConnectionState.Connected); + }; + this.ws.onmessage = (event) => { + console.log( + `[Connection, ${this.shortUrl}, ${this.connectionId}] Message received from server: ${event.data}` + ); + try { + const data = JSON.parse(event.data); + if (data.event === "open") { + return; + } + const relevantAndValid = + data.event === "message" && + "id" in data && + "time" in data && + "message" in data; + if (!relevantAndValid) { + console.log( + `[Connection, ${this.shortUrl}, ${this.connectionId}] Unexpected message. Ignoring.` + ); + return; + } + this.since = data.id; + this.onNotification(this.subscriptionId, data); + } catch (e) { + console.log( + `[Connection, ${this.shortUrl}, ${this.connectionId}] Error handling message: ${e}` + ); + } + }; + this.ws.onclose = (event) => { + if (event.wasClean) { + console.log( + `[Connection, ${this.shortUrl}, ${this.connectionId}] Connection closed cleanly, code=${event.code} reason=${event.reason}` + ); this.ws = null; - this.retryCount = 0; - this.retryTimeout = null; + } else { + const retrySeconds = + retryBackoffSeconds[ + Math.min(this.retryCount, retryBackoffSeconds.length - 1) + ]; + this.retryCount++; + console.log( + `[Connection, ${this.shortUrl}, ${this.connectionId}] Connection died, retrying in ${retrySeconds} seconds` + ); + this.retryTimeout = setTimeout(() => this.start(), retrySeconds * 1000); + this.onStateChanged(this.subscriptionId, ConnectionState.Connecting); + } + }; + this.ws.onerror = (event) => { + console.log( + `[Connection, ${this.shortUrl}, ${this.connectionId}] Error occurred: ${event}`, + event + ); + }; + } + + close() { + console.log( + `[Connection, ${this.shortUrl}, ${this.connectionId}] Closing connection` + ); + const socket = this.ws; + const retryTimeout = this.retryTimeout; + if (socket !== null) { + socket.close(); } - - start() { - // Don't fetch old messages; we do that as a poll() when adding a subscription; - // we don't want to re-trigger the main view re-render potentially hundreds of times. - - const wsUrl = this.wsUrl(); - console.log(`[Connection, ${this.shortUrl}, ${this.connectionId}] Opening connection to ${wsUrl}`); - - this.ws = new WebSocket(wsUrl); - this.ws.onopen = (event) => { - console.log(`[Connection, ${this.shortUrl}, ${this.connectionId}] Connection established`, event); - this.retryCount = 0; - this.onStateChanged(this.subscriptionId, ConnectionState.Connected); - } - this.ws.onmessage = (event) => { - console.log(`[Connection, ${this.shortUrl}, ${this.connectionId}] Message received from server: ${event.data}`); - try { - const data = JSON.parse(event.data); - if (data.event === 'open') { - return; - } - const relevantAndValid = - data.event === 'message' && - 'id' in data && - 'time' in data && - 'message' in data; - if (!relevantAndValid) { - console.log(`[Connection, ${this.shortUrl}, ${this.connectionId}] Unexpected message. Ignoring.`); - return; - } - this.since = data.id; - this.onNotification(this.subscriptionId, data); - } catch (e) { - console.log(`[Connection, ${this.shortUrl}, ${this.connectionId}] Error handling message: ${e}`); - } - }; - this.ws.onclose = (event) => { - if (event.wasClean) { - console.log(`[Connection, ${this.shortUrl}, ${this.connectionId}] Connection closed cleanly, code=${event.code} reason=${event.reason}`); - this.ws = null; - } else { - const retrySeconds = retryBackoffSeconds[Math.min(this.retryCount, retryBackoffSeconds.length-1)]; - this.retryCount++; - console.log(`[Connection, ${this.shortUrl}, ${this.connectionId}] Connection died, retrying in ${retrySeconds} seconds`); - this.retryTimeout = setTimeout(() => this.start(), retrySeconds * 1000); - this.onStateChanged(this.subscriptionId, ConnectionState.Connecting); - } - }; - this.ws.onerror = (event) => { - console.log(`[Connection, ${this.shortUrl}, ${this.connectionId}] Error occurred: ${event}`, event); - }; + if (retryTimeout !== null) { + clearTimeout(retryTimeout); } + this.retryTimeout = null; + this.ws = null; + } - close() { - console.log(`[Connection, ${this.shortUrl}, ${this.connectionId}] Closing connection`); - const socket = this.ws; - const retryTimeout = this.retryTimeout; - if (socket !== null) { - socket.close(); - } - if (retryTimeout !== null) { - clearTimeout(retryTimeout); - } - this.retryTimeout = null; - this.ws = null; + wsUrl() { + const params = []; + if (this.since) { + params.push(`since=${this.since}`); } + if (this.user) { + params.push(`auth=${this.authParam()}`); + } + const wsUrl = topicUrlWs(this.baseUrl, this.topic); + return params.length === 0 ? wsUrl : `${wsUrl}?${params.join("&")}`; + } - wsUrl() { - const params = []; - if (this.since) { - params.push(`since=${this.since}`); - } - if (this.user) { - params.push(`auth=${this.authParam()}`); - } - const wsUrl = topicUrlWs(this.baseUrl, this.topic); - return (params.length === 0) ? wsUrl : `${wsUrl}?${params.join('&')}`; - } - - authParam() { - if (this.user.password) { - return encodeBase64Url(basicAuth(this.user.username, this.user.password)); - } - return encodeBase64Url(bearerAuth(this.user.token)); + authParam() { + if (this.user.password) { + return encodeBase64Url(basicAuth(this.user.username, this.user.password)); } + return encodeBase64Url(bearerAuth(this.user.token)); + } } export class ConnectionState { - static Connected = "connected"; - static Connecting = "connecting"; + static Connected = "connected"; + static Connecting = "connecting"; } export default Connection; diff --git a/web/src/app/ConnectionManager.js b/web/src/app/ConnectionManager.js index 1e805eb7..ced32d5a 100644 --- a/web/src/app/ConnectionManager.js +++ b/web/src/app/ConnectionManager.js @@ -1,5 +1,5 @@ import Connection from "./Connection"; -import {hashCode} from "./utils"; +import { hashCode } from "./utils"; /** * The connection manager keeps track of active connections (WebSocket connections, see Connection). @@ -8,110 +8,130 @@ import {hashCode} from "./utils"; * as required. This is done pretty much exactly the same way as in the Android app. */ class ConnectionManager { - constructor() { - this.connections = new Map(); // ConnectionId -> Connection (hash, see below) - this.stateListener = null; // Fired when connection state changes - this.messageListener = null; // Fired when new notifications arrive + constructor() { + this.connections = new Map(); // ConnectionId -> Connection (hash, see below) + this.stateListener = null; // Fired when connection state changes + this.messageListener = null; // Fired when new notifications arrive + } + + registerStateListener(listener) { + this.stateListener = listener; + } + + resetStateListener() { + this.stateListener = null; + } + + registerMessageListener(listener) { + this.messageListener = listener; + } + + resetMessageListener() { + this.messageListener = null; + } + + /** + * This function figures out which websocket connections should be running by comparing the + * current state of the world (connections) with the target state (targetIds). + * + * It uses a "connectionId", which is sha256($subscriptionId|$username|$password) to identify + * connections. If any of them change, the connection is closed/replaced. + */ + async refresh(subscriptions, users) { + if (!subscriptions || !users) { + return; } + console.log(`[ConnectionManager] Refreshing connections`); + const subscriptionsWithUsersAndConnectionId = await Promise.all( + subscriptions.map(async (s) => { + const [user] = users.filter((u) => u.baseUrl === s.baseUrl); + const connectionId = await makeConnectionId(s, user); + return { ...s, user, connectionId }; + }) + ); + const targetIds = subscriptionsWithUsersAndConnectionId.map( + (s) => s.connectionId + ); + const deletedIds = Array.from(this.connections.keys()).filter( + (id) => !targetIds.includes(id) + ); - registerStateListener(listener) { - this.stateListener = listener; + // Create and add new connections + subscriptionsWithUsersAndConnectionId.forEach((subscription) => { + const subscriptionId = subscription.id; + const connectionId = subscription.connectionId; + const added = !this.connections.get(connectionId); + if (added) { + const baseUrl = subscription.baseUrl; + const topic = subscription.topic; + const user = subscription.user; + const since = subscription.last; + const connection = new Connection( + connectionId, + subscriptionId, + baseUrl, + topic, + user, + since, + (subscriptionId, notification) => + this.notificationReceived(subscriptionId, notification), + (subscriptionId, state) => this.stateChanged(subscriptionId, state) + ); + this.connections.set(connectionId, connection); + console.log( + `[ConnectionManager] Starting new connection ${connectionId} (subscription ${subscriptionId} with user ${ + user ? user.username : "anonymous" + })` + ); + connection.start(); + } + }); + + // Delete old connections + deletedIds.forEach((id) => { + console.log(`[ConnectionManager] Closing connection ${id}`); + const connection = this.connections.get(id); + this.connections.delete(id); + connection.close(); + }); + } + + stateChanged(subscriptionId, state) { + if (this.stateListener) { + try { + this.stateListener(subscriptionId, state); + } catch (e) { + console.error( + `[ConnectionManager] Error updating state of ${subscriptionId} to ${state}`, + e + ); + } } + } - resetStateListener() { - this.stateListener = null; - } - - registerMessageListener(listener) { - this.messageListener = listener; - } - - resetMessageListener() { - this.messageListener = null; - } - - /** - * This function figures out which websocket connections should be running by comparing the - * current state of the world (connections) with the target state (targetIds). - * - * It uses a "connectionId", which is sha256($subscriptionId|$username|$password) to identify - * connections. If any of them change, the connection is closed/replaced. - */ - async refresh(subscriptions, users) { - if (!subscriptions || !users) { - return; - } - console.log(`[ConnectionManager] Refreshing connections`); - const subscriptionsWithUsersAndConnectionId = await Promise.all(subscriptions - .map(async s => { - const [user] = users.filter(u => u.baseUrl === s.baseUrl); - const connectionId = await makeConnectionId(s, user); - return {...s, user, connectionId}; - })); - const targetIds = subscriptionsWithUsersAndConnectionId.map(s => s.connectionId); - const deletedIds = Array.from(this.connections.keys()).filter(id => !targetIds.includes(id)); - - // Create and add new connections - subscriptionsWithUsersAndConnectionId.forEach(subscription => { - const subscriptionId = subscription.id; - const connectionId = subscription.connectionId; - const added = !this.connections.get(connectionId) - if (added) { - const baseUrl = subscription.baseUrl; - const topic = subscription.topic; - const user = subscription.user; - const since = subscription.last; - const connection = new Connection( - connectionId, - subscriptionId, - baseUrl, - topic, - user, - since, - (subscriptionId, notification) => this.notificationReceived(subscriptionId, notification), - (subscriptionId, state) => this.stateChanged(subscriptionId, state) - ); - this.connections.set(connectionId, connection); - console.log(`[ConnectionManager] Starting new connection ${connectionId} (subscription ${subscriptionId} with user ${user ? user.username : "anonymous"})`); - connection.start(); - } - }); - - // Delete old connections - deletedIds.forEach(id => { - console.log(`[ConnectionManager] Closing connection ${id}`); - const connection = this.connections.get(id); - this.connections.delete(id); - connection.close(); - }); - } - - stateChanged(subscriptionId, state) { - if (this.stateListener) { - try { - this.stateListener(subscriptionId, state); - } catch (e) { - console.error(`[ConnectionManager] Error updating state of ${subscriptionId} to ${state}`, e); - } - } - } - - notificationReceived(subscriptionId, notification) { - if (this.messageListener) { - try { - this.messageListener(subscriptionId, notification); - } catch (e) { - console.error(`[ConnectionManager] Error handling notification for ${subscriptionId}`, e); - } - } + notificationReceived(subscriptionId, notification) { + if (this.messageListener) { + try { + this.messageListener(subscriptionId, notification); + } catch (e) { + console.error( + `[ConnectionManager] Error handling notification for ${subscriptionId}`, + e + ); + } } + } } const makeConnectionId = async (subscription, user) => { - return (user) - ? hashCode(`${subscription.id}|${user.username}|${user.password ?? ""}|${user.token ?? ""}`) - : hashCode(`${subscription.id}`); -} + return user + ? hashCode( + `${subscription.id}|${user.username}|${user.password ?? ""}|${ + user.token ?? "" + }` + ) + : hashCode(`${subscription.id}`); +}; const connectionManager = new ConnectionManager(); export default connectionManager; diff --git a/web/src/app/Notifier.js b/web/src/app/Notifier.js index 613340cb..e4396d25 100644 --- a/web/src/app/Notifier.js +++ b/web/src/app/Notifier.js @@ -1,4 +1,11 @@ -import {formatMessage, formatTitleWithDefault, openUrl, playSound, topicDisplayName, topicShortUrl} from "./utils"; +import { + formatMessage, + formatTitleWithDefault, + openUrl, + playSound, + topicDisplayName, + topicShortUrl, +} from "./utils"; import prefs from "./Prefs"; import subscriptionManager from "./SubscriptionManager"; import logo from "../img/ntfy.png"; @@ -8,89 +15,93 @@ import logo from "../img/ntfy.png"; * support this; most importantly, all iOS browsers do not support window.Notification. */ class Notifier { - async notify(subscriptionId, notification, onClickFallback) { - if (!this.supported()) { - return; - } - const subscription = await subscriptionManager.get(subscriptionId); - const shouldNotify = await this.shouldNotify(subscription, notification); - if (!shouldNotify) { - return; - } - const shortUrl = topicShortUrl(subscription.baseUrl, subscription.topic); - const displayName = topicDisplayName(subscription); - const message = formatMessage(notification); - const title = formatTitleWithDefault(notification, displayName); + async notify(subscriptionId, notification, onClickFallback) { + if (!this.supported()) { + return; + } + const subscription = await subscriptionManager.get(subscriptionId); + const shouldNotify = await this.shouldNotify(subscription, notification); + if (!shouldNotify) { + return; + } + const shortUrl = topicShortUrl(subscription.baseUrl, subscription.topic); + const displayName = topicDisplayName(subscription); + const message = formatMessage(notification); + const title = formatTitleWithDefault(notification, displayName); - // Show notification - console.log(`[Notifier, ${shortUrl}] Displaying notification ${notification.id}: ${message}`); - const n = new Notification(title, { - body: message, - icon: logo - }); - if (notification.click) { - n.onclick = (e) => openUrl(notification.click); - } else { - n.onclick = () => onClickFallback(subscription); - } - - // Play sound - const sound = await prefs.sound(); - if (sound && sound !== "none") { - try { - await playSound(sound); - } catch (e) { - console.log(`[Notifier, ${shortUrl}] Error playing audio`, e); - } - } + // Show notification + console.log( + `[Notifier, ${shortUrl}] Displaying notification ${notification.id}: ${message}` + ); + const n = new Notification(title, { + body: message, + icon: logo, + }); + if (notification.click) { + n.onclick = (e) => openUrl(notification.click); + } else { + n.onclick = () => onClickFallback(subscription); } - granted() { - return this.supported() && Notification.permission === 'granted'; + // Play sound + const sound = await prefs.sound(); + if (sound && sound !== "none") { + try { + await playSound(sound); + } catch (e) { + console.log(`[Notifier, ${shortUrl}] Error playing audio`, e); + } } + } - maybeRequestPermission(cb) { - if (!this.supported()) { - cb(false); - return; - } - if (!this.granted()) { - Notification.requestPermission().then((permission) => { - const granted = permission === 'granted'; - cb(granted); - }); - } - } + granted() { + return this.supported() && Notification.permission === "granted"; + } - async shouldNotify(subscription, notification) { - if (subscription.mutedUntil === 1) { - return false; - } - const priority = (notification.priority) ? notification.priority : 3; - const minPriority = await prefs.minPriority(); - if (priority < minPriority) { - return false; - } - return true; + maybeRequestPermission(cb) { + if (!this.supported()) { + cb(false); + return; } + if (!this.granted()) { + Notification.requestPermission().then((permission) => { + const granted = permission === "granted"; + cb(granted); + }); + } + } - supported() { - return this.browserSupported() && this.contextSupported(); + async shouldNotify(subscription, notification) { + if (subscription.mutedUntil === 1) { + return false; } + const priority = notification.priority ? notification.priority : 3; + const minPriority = await prefs.minPriority(); + if (priority < minPriority) { + return false; + } + return true; + } - browserSupported() { - return 'Notification' in window; - } + supported() { + return this.browserSupported() && this.contextSupported(); + } - /** - * Returns true if this is a HTTPS site, or served over localhost. Otherwise the Notification API - * is not supported, see https://developer.mozilla.org/en-US/docs/Web/API/notification - */ - contextSupported() { - return location.protocol === 'https:' - || location.hostname.match('^127.') - || location.hostname === 'localhost'; - } + browserSupported() { + return "Notification" in window; + } + + /** + * Returns true if this is a HTTPS site, or served over localhost. Otherwise the Notification API + * is not supported, see https://developer.mozilla.org/en-US/docs/Web/API/notification + */ + contextSupported() { + return ( + location.protocol === "https:" || + location.hostname.match("^127.") || + location.hostname === "localhost" + ); + } } const notifier = new Notifier(); diff --git a/web/src/app/Poller.js b/web/src/app/Poller.js index a7eed032..d2bf6965 100644 --- a/web/src/app/Poller.js +++ b/web/src/app/Poller.js @@ -5,54 +5,60 @@ const delayMillis = 2000; // 2 seconds const intervalMillis = 300000; // 5 minutes class Poller { - constructor() { - this.timer = null; - } + constructor() { + this.timer = null; + } - startWorker() { - if (this.timer !== null) { - return; - } - console.log(`[Poller] Starting worker`); - this.timer = setInterval(() => this.pollAll(), intervalMillis); - setTimeout(() => this.pollAll(), delayMillis); + startWorker() { + if (this.timer !== null) { + return; } + console.log(`[Poller] Starting worker`); + this.timer = setInterval(() => this.pollAll(), intervalMillis); + setTimeout(() => this.pollAll(), delayMillis); + } - async pollAll() { - console.log(`[Poller] Polling all subscriptions`); - const subscriptions = await subscriptionManager.all(); - for (const s of subscriptions) { - try { - await this.poll(s); - } catch (e) { - console.log(`[Poller] Error polling ${s.id}`, e); - } - } + async pollAll() { + console.log(`[Poller] Polling all subscriptions`); + const subscriptions = await subscriptionManager.all(); + for (const s of subscriptions) { + try { + await this.poll(s); + } catch (e) { + console.log(`[Poller] Error polling ${s.id}`, e); + } } + } - async poll(subscription) { - console.log(`[Poller] Polling ${subscription.id}`); + async poll(subscription) { + console.log(`[Poller] Polling ${subscription.id}`); - const since = subscription.last; - const notifications = await api.poll(subscription.baseUrl, subscription.topic, since); - if (!notifications || notifications.length === 0) { - console.log(`[Poller] No new notifications found for ${subscription.id}`); - return; - } - console.log(`[Poller] Adding ${notifications.length} notification(s) for ${subscription.id}`); - await subscriptionManager.addNotifications(subscription.id, notifications); + const since = subscription.last; + const notifications = await api.poll( + subscription.baseUrl, + subscription.topic, + since + ); + if (!notifications || notifications.length === 0) { + console.log(`[Poller] No new notifications found for ${subscription.id}`); + return; } + console.log( + `[Poller] Adding ${notifications.length} notification(s) for ${subscription.id}` + ); + await subscriptionManager.addNotifications(subscription.id, notifications); + } - pollInBackground(subscription) { - const fn = async () => { - try { - await this.poll(subscription); - } catch (e) { - console.error(`[App] Error polling subscription ${subscription.id}`, e); - } - }; - setTimeout(() => fn(), 0); - } + pollInBackground(subscription) { + const fn = async () => { + try { + await this.poll(subscription); + } catch (e) { + console.error(`[App] Error polling subscription ${subscription.id}`, e); + } + }; + setTimeout(() => fn(), 0); + } } const poller = new Poller(); diff --git a/web/src/app/Prefs.js b/web/src/app/Prefs.js index b444c6f8..8adc5088 100644 --- a/web/src/app/Prefs.js +++ b/web/src/app/Prefs.js @@ -1,32 +1,32 @@ import db from "./db"; class Prefs { - async setSound(sound) { - db.prefs.put({key: 'sound', value: sound.toString()}); - } + async setSound(sound) { + db.prefs.put({ key: "sound", value: sound.toString() }); + } - async sound() { - const sound = await db.prefs.get('sound'); - return (sound) ? sound.value : "ding"; - } + async sound() { + const sound = await db.prefs.get("sound"); + return sound ? sound.value : "ding"; + } - async setMinPriority(minPriority) { - db.prefs.put({key: 'minPriority', value: minPriority.toString()}); - } + async setMinPriority(minPriority) { + db.prefs.put({ key: "minPriority", value: minPriority.toString() }); + } - async minPriority() { - const minPriority = await db.prefs.get('minPriority'); - return (minPriority) ? Number(minPriority.value) : 1; - } + async minPriority() { + const minPriority = await db.prefs.get("minPriority"); + return minPriority ? Number(minPriority.value) : 1; + } - async setDeleteAfter(deleteAfter) { - db.prefs.put({key:'deleteAfter', value: deleteAfter.toString()}); - } + async setDeleteAfter(deleteAfter) { + db.prefs.put({ key: "deleteAfter", value: deleteAfter.toString() }); + } - async deleteAfter() { - const deleteAfter = await db.prefs.get('deleteAfter'); - return (deleteAfter) ? Number(deleteAfter.value) : 604800; // Default is one week - } + async deleteAfter() { + const deleteAfter = await db.prefs.get("deleteAfter"); + return deleteAfter ? Number(deleteAfter.value) : 604800; // Default is one week + } } const prefs = new Prefs(); diff --git a/web/src/app/Pruner.js b/web/src/app/Pruner.js index 45948057..84853b62 100644 --- a/web/src/app/Pruner.js +++ b/web/src/app/Pruner.js @@ -5,33 +5,36 @@ const delayMillis = 25000; // 25 seconds const intervalMillis = 1800000; // 30 minutes class Pruner { - constructor() { - this.timer = null; - } + constructor() { + this.timer = null; + } - startWorker() { - if (this.timer !== null) { - return; - } - console.log(`[Pruner] Starting worker`); - this.timer = setInterval(() => this.prune(), intervalMillis); - setTimeout(() => this.prune(), delayMillis); + startWorker() { + if (this.timer !== null) { + return; } + console.log(`[Pruner] Starting worker`); + this.timer = setInterval(() => this.prune(), intervalMillis); + setTimeout(() => this.prune(), delayMillis); + } - async prune() { - const deleteAfterSeconds = await prefs.deleteAfter(); - const pruneThresholdTimestamp = Math.round(Date.now()/1000) - deleteAfterSeconds; - if (deleteAfterSeconds === 0) { - console.log(`[Pruner] Pruning is disabled. Skipping.`); - return; - } - console.log(`[Pruner] Pruning notifications older than ${deleteAfterSeconds}s (timestamp ${pruneThresholdTimestamp})`); - try { - await subscriptionManager.pruneNotifications(pruneThresholdTimestamp); - } catch (e) { - console.log(`[Pruner] Error pruning old subscriptions`, e); - } + async prune() { + const deleteAfterSeconds = await prefs.deleteAfter(); + const pruneThresholdTimestamp = + Math.round(Date.now() / 1000) - deleteAfterSeconds; + if (deleteAfterSeconds === 0) { + console.log(`[Pruner] Pruning is disabled. Skipping.`); + return; } + console.log( + `[Pruner] Pruning notifications older than ${deleteAfterSeconds}s (timestamp ${pruneThresholdTimestamp})` + ); + try { + await subscriptionManager.pruneNotifications(pruneThresholdTimestamp); + } catch (e) { + console.log(`[Pruner] Error pruning old subscriptions`, e); + } + } } const pruner = new Pruner(); diff --git a/web/src/app/Session.js b/web/src/app/Session.js index 45f48421..0b47f93a 100644 --- a/web/src/app/Session.js +++ b/web/src/app/Session.js @@ -1,30 +1,30 @@ class Session { - store(username, token) { - localStorage.setItem("user", username); - localStorage.setItem("token", token); - } + store(username, token) { + localStorage.setItem("user", username); + localStorage.setItem("token", token); + } - reset() { - localStorage.removeItem("user"); - localStorage.removeItem("token"); - } + reset() { + localStorage.removeItem("user"); + localStorage.removeItem("token"); + } - resetAndRedirect(url) { - this.reset(); - window.location.href = url; - } + resetAndRedirect(url) { + this.reset(); + window.location.href = url; + } - exists() { - return this.username() && this.token(); - } + exists() { + return this.username() && this.token(); + } - username() { - return localStorage.getItem("user"); - } + username() { + return localStorage.getItem("user"); + } - token() { - return localStorage.getItem("token"); - } + token() { + return localStorage.getItem("token"); + } } const session = new Session(); diff --git a/web/src/app/SubscriptionManager.js b/web/src/app/SubscriptionManager.js index cdfe50e2..25d08309 100644 --- a/web/src/app/SubscriptionManager.js +++ b/web/src/app/SubscriptionManager.js @@ -1,192 +1,193 @@ import db from "./db"; -import {topicUrl} from "./utils"; +import { topicUrl } from "./utils"; class SubscriptionManager { - /** All subscriptions, including "new count"; this is a JOIN, see https://dexie.org/docs/API-Reference#joining */ - async all() { - const subscriptions = await db.subscriptions.toArray(); - await Promise.all(subscriptions.map(async s => { - s.new = await db.notifications - .where({ subscriptionId: s.id, new: 1 }) - .count(); - })); - return subscriptions; + /** All subscriptions, including "new count"; this is a JOIN, see https://dexie.org/docs/API-Reference#joining */ + async all() { + const subscriptions = await db.subscriptions.toArray(); + await Promise.all( + subscriptions.map(async (s) => { + s.new = await db.notifications + .where({ subscriptionId: s.id, new: 1 }) + .count(); + }) + ); + return subscriptions; + } + + async get(subscriptionId) { + return await db.subscriptions.get(subscriptionId); + } + + async add(baseUrl, topic, internal) { + const id = topicUrl(baseUrl, topic); + const existingSubscription = await this.get(id); + if (existingSubscription) { + return existingSubscription; + } + const subscription = { + id: topicUrl(baseUrl, topic), + baseUrl: baseUrl, + topic: topic, + mutedUntil: 0, + last: null, + internal: internal || false, + }; + await db.subscriptions.put(subscription); + return subscription; + } + + async syncFromRemote(remoteSubscriptions, remoteReservations) { + console.log( + `[SubscriptionManager] Syncing subscriptions from remote`, + remoteSubscriptions + ); + + // Add remote subscriptions + let remoteIds = []; // = topicUrl(baseUrl, topic) + for (let i = 0; i < remoteSubscriptions.length; i++) { + const remote = remoteSubscriptions[i]; + const local = await this.add(remote.base_url, remote.topic, false); + const reservation = + remoteReservations?.find( + (r) => remote.base_url === config.base_url && remote.topic === r.topic + ) || null; + await this.update(local.id, { + displayName: remote.display_name, // May be undefined + reservation: reservation, // May be null! + }); + remoteIds.push(local.id); } - async get(subscriptionId) { - return await db.subscriptions.get(subscriptionId) + // Remove local subscriptions that do not exist remotely + const localSubscriptions = await db.subscriptions.toArray(); + for (let i = 0; i < localSubscriptions.length; i++) { + const local = localSubscriptions[i]; + const remoteExists = remoteIds.includes(local.id); + if (!local.internal && !remoteExists) { + await this.remove(local.id); + } } + } - async add(baseUrl, topic, internal) { - const id = topicUrl(baseUrl, topic); - const existingSubscription = await this.get(id); - if (existingSubscription) { - return existingSubscription; - } - const subscription = { - id: topicUrl(baseUrl, topic), - baseUrl: baseUrl, - topic: topic, - mutedUntil: 0, - last: null, - internal: internal || false - }; - await db.subscriptions.put(subscription); - return subscription; + async updateState(subscriptionId, state) { + db.subscriptions.update(subscriptionId, { state: state }); + } + + async remove(subscriptionId) { + await db.subscriptions.delete(subscriptionId); + await db.notifications.where({ subscriptionId: subscriptionId }).delete(); + } + + async first() { + return db.subscriptions.toCollection().first(); // May be undefined + } + + async getNotifications(subscriptionId) { + // This is quite awkward, but it is the recommended approach as per the Dexie docs. + // It's actually fine, because the reading and filtering is quite fast. The rendering is what's + // killing performance. See https://dexie.org/docs/Collection/Collection.offset()#a-better-paging-approach + + return db.notifications + .orderBy("time") // Sort by time first + .filter((n) => n.subscriptionId === subscriptionId) + .reverse() + .toArray(); + } + + async getAllNotifications() { + return db.notifications + .orderBy("time") // Efficient, see docs + .reverse() + .toArray(); + } + + /** Adds notification, or returns false if it already exists */ + async addNotification(subscriptionId, notification) { + const exists = await db.notifications.get(notification.id); + if (exists) { + return false; } - - async syncFromRemote(remoteSubscriptions, remoteReservations) { - console.log(`[SubscriptionManager] Syncing subscriptions from remote`, remoteSubscriptions); - - // Add remote subscriptions - let remoteIds = []; // = topicUrl(baseUrl, topic) - for (let i = 0; i < remoteSubscriptions.length; i++) { - const remote = remoteSubscriptions[i]; - const local = await this.add(remote.base_url, remote.topic, false); - const reservation = remoteReservations?.find(r => remote.base_url === config.base_url && remote.topic === r.topic) || null; - await this.update(local.id, { - displayName: remote.display_name, // May be undefined - reservation: reservation // May be null! - }); - remoteIds.push(local.id); - } - - // Remove local subscriptions that do not exist remotely - const localSubscriptions = await db.subscriptions.toArray(); - for (let i = 0; i < localSubscriptions.length; i++) { - const local = localSubscriptions[i]; - const remoteExists = remoteIds.includes(local.id); - if (!local.internal && !remoteExists) { - await this.remove(local.id); - } - } + try { + notification.new = 1; // New marker (used for bubble indicator); cannot be boolean; Dexie index limitation + await db.notifications.add({ ...notification, subscriptionId }); // FIXME consider put() for double tab + await db.subscriptions.update(subscriptionId, { + last: notification.id, + }); + } catch (e) { + console.error(`[SubscriptionManager] Error adding notification`, e); } + return true; + } - async updateState(subscriptionId, state) { - db.subscriptions.update(subscriptionId, { state: state }); + /** Adds/replaces notifications, will not throw if they exist */ + async addNotifications(subscriptionId, notifications) { + const notificationsWithSubscriptionId = notifications.map( + (notification) => ({ ...notification, subscriptionId }) + ); + const lastNotificationId = notifications.at(-1).id; + await db.notifications.bulkPut(notificationsWithSubscriptionId); + await db.subscriptions.update(subscriptionId, { + last: lastNotificationId, + }); + } + + async updateNotification(notification) { + const exists = await db.notifications.get(notification.id); + if (!exists) { + return false; } - - async remove(subscriptionId) { - await db.subscriptions.delete(subscriptionId); - await db.notifications - .where({subscriptionId: subscriptionId}) - .delete(); + try { + await db.notifications.put({ ...notification }); + } catch (e) { + console.error(`[SubscriptionManager] Error updating notification`, e); } + return true; + } - async first() { - return db.subscriptions.toCollection().first(); // May be undefined - } + async deleteNotification(notificationId) { + await db.notifications.delete(notificationId); + } - async getNotifications(subscriptionId) { - // This is quite awkward, but it is the recommended approach as per the Dexie docs. - // It's actually fine, because the reading and filtering is quite fast. The rendering is what's - // killing performance. See https://dexie.org/docs/Collection/Collection.offset()#a-better-paging-approach + async deleteNotifications(subscriptionId) { + await db.notifications.where({ subscriptionId: subscriptionId }).delete(); + } - return db.notifications - .orderBy("time") // Sort by time first - .filter(n => n.subscriptionId === subscriptionId) - .reverse() - .toArray(); - } + async markNotificationRead(notificationId) { + await db.notifications.where({ id: notificationId }).modify({ new: 0 }); + } - async getAllNotifications() { - return db.notifications - .orderBy("time") // Efficient, see docs - .reverse() - .toArray(); - } + async markNotificationsRead(subscriptionId) { + await db.notifications + .where({ subscriptionId: subscriptionId, new: 1 }) + .modify({ new: 0 }); + } - /** Adds notification, or returns false if it already exists */ - async addNotification(subscriptionId, notification) { - const exists = await db.notifications.get(notification.id); - if (exists) { - return false; - } - try { - notification.new = 1; // New marker (used for bubble indicator); cannot be boolean; Dexie index limitation - await db.notifications.add({ ...notification, subscriptionId }); // FIXME consider put() for double tab - await db.subscriptions.update(subscriptionId, { - last: notification.id - }); - } catch (e) { - console.error(`[SubscriptionManager] Error adding notification`, e); - } - return true; - } + async setMutedUntil(subscriptionId, mutedUntil) { + await db.subscriptions.update(subscriptionId, { + mutedUntil: mutedUntil, + }); + } - /** Adds/replaces notifications, will not throw if they exist */ - async addNotifications(subscriptionId, notifications) { - const notificationsWithSubscriptionId = notifications - .map(notification => ({ ...notification, subscriptionId })); - const lastNotificationId = notifications.at(-1).id; - await db.notifications.bulkPut(notificationsWithSubscriptionId); - await db.subscriptions.update(subscriptionId, { - last: lastNotificationId - }); - } + async setDisplayName(subscriptionId, displayName) { + await db.subscriptions.update(subscriptionId, { + displayName: displayName, + }); + } - async updateNotification(notification) { - const exists = await db.notifications.get(notification.id); - if (!exists) { - return false; - } - try { - await db.notifications.put({ ...notification }); - } catch (e) { - console.error(`[SubscriptionManager] Error updating notification`, e); - } - return true; - } + async setReservation(subscriptionId, reservation) { + await db.subscriptions.update(subscriptionId, { + reservation: reservation, + }); + } - async deleteNotification(notificationId) { - await db.notifications.delete(notificationId); - } + async update(subscriptionId, params) { + await db.subscriptions.update(subscriptionId, params); + } - async deleteNotifications(subscriptionId) { - await db.notifications - .where({subscriptionId: subscriptionId}) - .delete(); - } - - async markNotificationRead(notificationId) { - await db.notifications - .where({id: notificationId}) - .modify({new: 0}); - } - - async markNotificationsRead(subscriptionId) { - await db.notifications - .where({subscriptionId: subscriptionId, new: 1}) - .modify({new: 0}); - } - - async setMutedUntil(subscriptionId, mutedUntil) { - await db.subscriptions.update(subscriptionId, { - mutedUntil: mutedUntil - }); - } - - async setDisplayName(subscriptionId, displayName) { - await db.subscriptions.update(subscriptionId, { - displayName: displayName - }); - } - - async setReservation(subscriptionId, reservation) { - await db.subscriptions.update(subscriptionId, { - reservation: reservation - }); - } - - async update(subscriptionId, params) { - await db.subscriptions.update(subscriptionId, params); - } - - async pruneNotifications(thresholdTimestamp) { - await db.notifications - .where("time").below(thresholdTimestamp) - .delete(); - } + async pruneNotifications(thresholdTimestamp) { + await db.notifications.where("time").below(thresholdTimestamp).delete(); + } } const subscriptionManager = new SubscriptionManager(); diff --git a/web/src/app/UserManager.js b/web/src/app/UserManager.js index 1e54eb0a..2cdd5449 100644 --- a/web/src/app/UserManager.js +++ b/web/src/app/UserManager.js @@ -2,45 +2,45 @@ import db from "./db"; import session from "./Session"; class UserManager { - async all() { - const users = await db.users.toArray(); - if (session.exists()) { - users.unshift(this.localUser()); - } - return users; + async all() { + const users = await db.users.toArray(); + if (session.exists()) { + users.unshift(this.localUser()); } + return users; + } - async get(baseUrl) { - if (session.exists() && baseUrl === config.base_url) { - return this.localUser(); - } - return db.users.get(baseUrl); + async get(baseUrl) { + if (session.exists() && baseUrl === config.base_url) { + return this.localUser(); } + return db.users.get(baseUrl); + } - async save(user) { - if (session.exists() && user.baseUrl === config.base_url) { - return; - } - await db.users.put(user); + async save(user) { + if (session.exists() && user.baseUrl === config.base_url) { + return; } + await db.users.put(user); + } - async delete(baseUrl) { - if (session.exists() && baseUrl === config.base_url) { - return; - } - await db.users.delete(baseUrl); + async delete(baseUrl) { + if (session.exists() && baseUrl === config.base_url) { + return; } + await db.users.delete(baseUrl); + } - localUser() { - if (!session.exists()) { - return null; - } - return { - baseUrl: config.base_url, - username: session.username(), - token: session.token() // Not "password"! - }; + localUser() { + if (!session.exists()) { + return null; } + return { + baseUrl: config.base_url, + username: session.username(), + token: session.token(), // Not "password"! + }; + } } const userManager = new UserManager(); diff --git a/web/src/app/config.js b/web/src/app/config.js index bdec53ed..15225f5b 100644 --- a/web/src/app/config.js +++ b/web/src/app/config.js @@ -3,7 +3,7 @@ const config = window.config; // The backend returns an empty base_url for the config struct, // so the frontend (hey, that's us!) can use the current location. if (!config.base_url || config.base_url === "") { - config.base_url = window.location.origin; + config.base_url = window.location.origin; } export default config; diff --git a/web/src/app/db.js b/web/src/app/db.js index 564ee1ce..0e1a5e71 100644 --- a/web/src/app/db.js +++ b/web/src/app/db.js @@ -1,4 +1,4 @@ -import Dexie from 'dexie'; +import Dexie from "dexie"; import session from "./Session"; // Uses Dexie.js @@ -8,14 +8,14 @@ import session from "./Session"; // - As per docs, we only declare the indexable columns, not all columns // The IndexedDB database name is based on the logged-in user -const dbName = (session.username()) ? `ntfy-${session.username()}` : "ntfy"; +const dbName = session.username() ? `ntfy-${session.username()}` : "ntfy"; const db = new Dexie(dbName); db.version(1).stores({ - subscriptions: '&id,baseUrl', - notifications: '&id,subscriptionId,time,new,[subscriptionId+new]', // compound key for query performance - users: '&baseUrl,username', - prefs: '&key' + subscriptions: "&id,baseUrl", + notifications: "&id,subscriptionId,time,new,[subscriptionId+new]", // compound key for query performance + users: "&baseUrl,username", + prefs: "&key", }); export default db; diff --git a/web/src/app/emojis.js b/web/src/app/emojis.js index f6dac7b1..b7912c35 100644 --- a/web/src/app/emojis.js +++ b/web/src/app/emojis.js @@ -1,3 +1,14500 @@ // This file is generated by scripts/emoji-convert.sh to reduce the size // Original data source: https://github.com/github/gemoji/blob/master/db/emoji.json -export const rawEmojis = [{"emoji":"😀","aliases":["grinning"],"tags":["smile","happy"],"category":"Smileys & Emotion","description":"grinning face","unicode_version":"6.1"},{"emoji":"😃","aliases":["smiley"],"tags":["happy","joy","haha"],"category":"Smileys & Emotion","description":"grinning face with big eyes","unicode_version":"6.0"},{"emoji":"😄","aliases":["smile"],"tags":["happy","joy","laugh","pleased"],"category":"Smileys & Emotion","description":"grinning face with smiling eyes","unicode_version":"6.0"},{"emoji":"😁","aliases":["grin"],"tags":[],"category":"Smileys & Emotion","description":"beaming face with smiling eyes","unicode_version":"6.0"},{"emoji":"😆","aliases":["laughing","satisfied"],"tags":["happy","haha"],"category":"Smileys & Emotion","description":"grinning squinting face","unicode_version":"6.0"},{"emoji":"😅","aliases":["sweat_smile"],"tags":["hot"],"category":"Smileys & Emotion","description":"grinning face with sweat","unicode_version":"6.0"},{"emoji":"🤣","aliases":["rofl"],"tags":["lol","laughing"],"category":"Smileys & Emotion","description":"rolling on the floor laughing","unicode_version":"9.0"},{"emoji":"😂","aliases":["joy"],"tags":["tears"],"category":"Smileys & Emotion","description":"face with tears of joy","unicode_version":"6.0"},{"emoji":"🙂","aliases":["slightly_smiling_face"],"tags":[],"category":"Smileys & Emotion","description":"slightly smiling face","unicode_version":"7.0"},{"emoji":"🙃","aliases":["upside_down_face"],"tags":[],"category":"Smileys & Emotion","description":"upside-down face","unicode_version":"8.0"},{"emoji":"😉","aliases":["wink"],"tags":["flirt"],"category":"Smileys & Emotion","description":"winking face","unicode_version":"6.0"},{"emoji":"😊","aliases":["blush"],"tags":["proud"],"category":"Smileys & Emotion","description":"smiling face with smiling eyes","unicode_version":"6.0"},{"emoji":"😇","aliases":["innocent"],"tags":["angel"],"category":"Smileys & Emotion","description":"smiling face with halo","unicode_version":"6.0"},{"emoji":"🥰","aliases":["smiling_face_with_three_hearts"],"tags":["love"],"category":"Smileys & Emotion","description":"smiling face with hearts","unicode_version":"11.0"},{"emoji":"😍","aliases":["heart_eyes"],"tags":["love","crush"],"category":"Smileys & Emotion","description":"smiling face with heart-eyes","unicode_version":"6.0"},{"emoji":"🤩","aliases":["star_struck"],"tags":["eyes"],"category":"Smileys & Emotion","description":"star-struck","unicode_version":"11.0"},{"emoji":"😘","aliases":["kissing_heart"],"tags":["flirt"],"category":"Smileys & Emotion","description":"face blowing a kiss","unicode_version":"6.0"},{"emoji":"😗","aliases":["kissing"],"tags":[],"category":"Smileys & Emotion","description":"kissing face","unicode_version":"6.1"},{"emoji":"☺️","aliases":["relaxed"],"tags":["blush","pleased"],"category":"Smileys & Emotion","description":"smiling face","unicode_version":""},{"emoji":"😚","aliases":["kissing_closed_eyes"],"tags":[],"category":"Smileys & Emotion","description":"kissing face with closed eyes","unicode_version":"6.0"},{"emoji":"😙","aliases":["kissing_smiling_eyes"],"tags":[],"category":"Smileys & Emotion","description":"kissing face with smiling eyes","unicode_version":"6.1"},{"emoji":"🥲","aliases":["smiling_face_with_tear"],"tags":[],"category":"Smileys & Emotion","description":"smiling face with tear","unicode_version":"13.0"},{"emoji":"😋","aliases":["yum"],"tags":["tongue","lick"],"category":"Smileys & Emotion","description":"face savoring food","unicode_version":"6.0"},{"emoji":"😛","aliases":["stuck_out_tongue"],"tags":[],"category":"Smileys & Emotion","description":"face with tongue","unicode_version":"6.1"},{"emoji":"😜","aliases":["stuck_out_tongue_winking_eye"],"tags":["prank","silly"],"category":"Smileys & Emotion","description":"winking face with tongue","unicode_version":"6.0"},{"emoji":"🤪","aliases":["zany_face"],"tags":["goofy","wacky"],"category":"Smileys & Emotion","description":"zany face","unicode_version":"11.0"},{"emoji":"😝","aliases":["stuck_out_tongue_closed_eyes"],"tags":["prank"],"category":"Smileys & Emotion","description":"squinting face with tongue","unicode_version":"6.0"},{"emoji":"🤑","aliases":["money_mouth_face"],"tags":["rich"],"category":"Smileys & Emotion","description":"money-mouth face","unicode_version":"8.0"},{"emoji":"🤗","aliases":["hugs"],"tags":[],"category":"Smileys & Emotion","description":"hugging face","unicode_version":"8.0"},{"emoji":"🤭","aliases":["hand_over_mouth"],"tags":["quiet","whoops"],"category":"Smileys & Emotion","description":"face with hand over mouth","unicode_version":"11.0"},{"emoji":"🤫","aliases":["shushing_face"],"tags":["silence","quiet"],"category":"Smileys & Emotion","description":"shushing face","unicode_version":"11.0"},{"emoji":"🤔","aliases":["thinking"],"tags":[],"category":"Smileys & Emotion","description":"thinking face","unicode_version":"8.0"},{"emoji":"🤐","aliases":["zipper_mouth_face"],"tags":["silence","hush"],"category":"Smileys & Emotion","description":"zipper-mouth face","unicode_version":"8.0"},{"emoji":"🤨","aliases":["raised_eyebrow"],"tags":["suspicious"],"category":"Smileys & Emotion","description":"face with raised eyebrow","unicode_version":"11.0"},{"emoji":"😐","aliases":["neutral_face"],"tags":["meh"],"category":"Smileys & Emotion","description":"neutral face","unicode_version":"6.0"},{"emoji":"😑","aliases":["expressionless"],"tags":[],"category":"Smileys & Emotion","description":"expressionless face","unicode_version":"6.1"},{"emoji":"😶","aliases":["no_mouth"],"tags":["mute","silence"],"category":"Smileys & Emotion","description":"face without mouth","unicode_version":"6.0"},{"emoji":"😶‍🌫️","aliases":["face_in_clouds"],"tags":[],"category":"Smileys & Emotion","description":"face in clouds","unicode_version":"13.1"},{"emoji":"😏","aliases":["smirk"],"tags":["smug"],"category":"Smileys & Emotion","description":"smirking face","unicode_version":"6.0"},{"emoji":"😒","aliases":["unamused"],"tags":["meh"],"category":"Smileys & Emotion","description":"unamused face","unicode_version":"6.0"},{"emoji":"🙄","aliases":["roll_eyes"],"tags":[],"category":"Smileys & Emotion","description":"face with rolling eyes","unicode_version":"8.0"},{"emoji":"😬","aliases":["grimacing"],"tags":[],"category":"Smileys & Emotion","description":"grimacing face","unicode_version":"6.1"},{"emoji":"😮‍💨","aliases":["face_exhaling"],"tags":[],"category":"Smileys & Emotion","description":"face exhaling","unicode_version":"13.1"},{"emoji":"🤥","aliases":["lying_face"],"tags":["liar"],"category":"Smileys & Emotion","description":"lying face","unicode_version":"9.0"},{"emoji":"😌","aliases":["relieved"],"tags":["whew"],"category":"Smileys & Emotion","description":"relieved face","unicode_version":"6.0"},{"emoji":"😔","aliases":["pensive"],"tags":[],"category":"Smileys & Emotion","description":"pensive face","unicode_version":"6.0"},{"emoji":"😪","aliases":["sleepy"],"tags":["tired"],"category":"Smileys & Emotion","description":"sleepy face","unicode_version":"6.0"},{"emoji":"🤤","aliases":["drooling_face"],"tags":[],"category":"Smileys & Emotion","description":"drooling face","unicode_version":"9.0"},{"emoji":"😴","aliases":["sleeping"],"tags":["zzz"],"category":"Smileys & Emotion","description":"sleeping face","unicode_version":"6.1"},{"emoji":"😷","aliases":["mask"],"tags":["sick","ill"],"category":"Smileys & Emotion","description":"face with medical mask","unicode_version":"6.0"},{"emoji":"🤒","aliases":["face_with_thermometer"],"tags":["sick"],"category":"Smileys & Emotion","description":"face with thermometer","unicode_version":"8.0"},{"emoji":"🤕","aliases":["face_with_head_bandage"],"tags":["hurt"],"category":"Smileys & Emotion","description":"face with head-bandage","unicode_version":"8.0"},{"emoji":"🤢","aliases":["nauseated_face"],"tags":["sick","barf","disgusted"],"category":"Smileys & Emotion","description":"nauseated face","unicode_version":"9.0"},{"emoji":"🤮","aliases":["vomiting_face"],"tags":["barf","sick"],"category":"Smileys & Emotion","description":"face vomiting","unicode_version":"11.0"},{"emoji":"🤧","aliases":["sneezing_face"],"tags":["achoo","sick"],"category":"Smileys & Emotion","description":"sneezing face","unicode_version":"9.0"},{"emoji":"🥵","aliases":["hot_face"],"tags":["heat","sweating"],"category":"Smileys & Emotion","description":"hot face","unicode_version":"11.0"},{"emoji":"🥶","aliases":["cold_face"],"tags":["freezing","ice"],"category":"Smileys & Emotion","description":"cold face","unicode_version":"11.0"},{"emoji":"🥴","aliases":["woozy_face"],"tags":["groggy"],"category":"Smileys & Emotion","description":"woozy face","unicode_version":"11.0"},{"emoji":"😵","aliases":["dizzy_face"],"tags":[],"category":"Smileys & Emotion","description":"knocked-out face","unicode_version":"6.0"},{"emoji":"😵‍💫","aliases":["face_with_spiral_eyes"],"tags":[],"category":"Smileys & Emotion","description":"face with spiral eyes","unicode_version":"13.1"},{"emoji":"🤯","aliases":["exploding_head"],"tags":["mind","blown"],"category":"Smileys & Emotion","description":"exploding head","unicode_version":"11.0"},{"emoji":"🤠","aliases":["cowboy_hat_face"],"tags":[],"category":"Smileys & Emotion","description":"cowboy hat face","unicode_version":"9.0"},{"emoji":"🥳","aliases":["partying_face"],"tags":["celebration","birthday"],"category":"Smileys & Emotion","description":"partying face","unicode_version":"11.0"},{"emoji":"🥸","aliases":["disguised_face"],"tags":[],"category":"Smileys & Emotion","description":"disguised face","unicode_version":"13.0"},{"emoji":"😎","aliases":["sunglasses"],"tags":["cool"],"category":"Smileys & Emotion","description":"smiling face with sunglasses","unicode_version":"6.0"},{"emoji":"🤓","aliases":["nerd_face"],"tags":["geek","glasses"],"category":"Smileys & Emotion","description":"nerd face","unicode_version":"8.0"},{"emoji":"🧐","aliases":["monocle_face"],"tags":[],"category":"Smileys & Emotion","description":"face with monocle","unicode_version":"11.0"},{"emoji":"😕","aliases":["confused"],"tags":[],"category":"Smileys & Emotion","description":"confused face","unicode_version":"6.1"},{"emoji":"😟","aliases":["worried"],"tags":["nervous"],"category":"Smileys & Emotion","description":"worried face","unicode_version":"6.1"},{"emoji":"🙁","aliases":["slightly_frowning_face"],"tags":[],"category":"Smileys & Emotion","description":"slightly frowning face","unicode_version":"7.0"},{"emoji":"☹️","aliases":["frowning_face"],"tags":[],"category":"Smileys & Emotion","description":"frowning face","unicode_version":""},{"emoji":"😮","aliases":["open_mouth"],"tags":["surprise","impressed","wow"],"category":"Smileys & Emotion","description":"face with open mouth","unicode_version":"6.1"},{"emoji":"😯","aliases":["hushed"],"tags":["silence","speechless"],"category":"Smileys & Emotion","description":"hushed face","unicode_version":"6.1"},{"emoji":"😲","aliases":["astonished"],"tags":["amazed","gasp"],"category":"Smileys & Emotion","description":"astonished face","unicode_version":"6.0"},{"emoji":"😳","aliases":["flushed"],"tags":[],"category":"Smileys & Emotion","description":"flushed face","unicode_version":"6.0"},{"emoji":"🥺","aliases":["pleading_face"],"tags":["puppy","eyes"],"category":"Smileys & Emotion","description":"pleading face","unicode_version":"11.0"},{"emoji":"😦","aliases":["frowning"],"tags":[],"category":"Smileys & Emotion","description":"frowning face with open mouth","unicode_version":"6.1"},{"emoji":"😧","aliases":["anguished"],"tags":["stunned"],"category":"Smileys & Emotion","description":"anguished face","unicode_version":"6.1"},{"emoji":"😨","aliases":["fearful"],"tags":["scared","shocked","oops"],"category":"Smileys & Emotion","description":"fearful face","unicode_version":"6.0"},{"emoji":"😰","aliases":["cold_sweat"],"tags":["nervous"],"category":"Smileys & Emotion","description":"anxious face with sweat","unicode_version":"6.0"},{"emoji":"😥","aliases":["disappointed_relieved"],"tags":["phew","sweat","nervous"],"category":"Smileys & Emotion","description":"sad but relieved face","unicode_version":"6.0"},{"emoji":"😢","aliases":["cry"],"tags":["sad","tear"],"category":"Smileys & Emotion","description":"crying face","unicode_version":"6.0"},{"emoji":"😭","aliases":["sob"],"tags":["sad","cry","bawling"],"category":"Smileys & Emotion","description":"loudly crying face","unicode_version":"6.0"},{"emoji":"😱","aliases":["scream"],"tags":["horror","shocked"],"category":"Smileys & Emotion","description":"face screaming in fear","unicode_version":"6.0"},{"emoji":"😖","aliases":["confounded"],"tags":[],"category":"Smileys & Emotion","description":"confounded face","unicode_version":"6.0"},{"emoji":"😣","aliases":["persevere"],"tags":["struggling"],"category":"Smileys & Emotion","description":"persevering face","unicode_version":"6.0"},{"emoji":"😞","aliases":["disappointed"],"tags":["sad"],"category":"Smileys & Emotion","description":"disappointed face","unicode_version":"6.0"},{"emoji":"😓","aliases":["sweat"],"tags":[],"category":"Smileys & Emotion","description":"downcast face with sweat","unicode_version":"6.0"},{"emoji":"😩","aliases":["weary"],"tags":["tired"],"category":"Smileys & Emotion","description":"weary face","unicode_version":"6.0"},{"emoji":"😫","aliases":["tired_face"],"tags":["upset","whine"],"category":"Smileys & Emotion","description":"tired face","unicode_version":"6.0"},{"emoji":"🥱","aliases":["yawning_face"],"tags":[],"category":"Smileys & Emotion","description":"yawning face","unicode_version":"12.0"},{"emoji":"😤","aliases":["triumph"],"tags":["smug"],"category":"Smileys & Emotion","description":"face with steam from nose","unicode_version":"6.0"},{"emoji":"😡","aliases":["rage","pout"],"tags":["angry"],"category":"Smileys & Emotion","description":"pouting face","unicode_version":"6.0"},{"emoji":"😠","aliases":["angry"],"tags":["mad","annoyed"],"category":"Smileys & Emotion","description":"angry face","unicode_version":"6.0"},{"emoji":"🤬","aliases":["cursing_face"],"tags":["foul"],"category":"Smileys & Emotion","description":"face with symbols on mouth","unicode_version":"11.0"},{"emoji":"😈","aliases":["smiling_imp"],"tags":["devil","evil","horns"],"category":"Smileys & Emotion","description":"smiling face with horns","unicode_version":"6.0"},{"emoji":"👿","aliases":["imp"],"tags":["angry","devil","evil","horns"],"category":"Smileys & Emotion","description":"angry face with horns","unicode_version":"6.0"},{"emoji":"💀","aliases":["skull"],"tags":["dead","danger","poison"],"category":"Smileys & Emotion","description":"skull","unicode_version":"6.0"},{"emoji":"☠️","aliases":["skull_and_crossbones"],"tags":["danger","pirate"],"category":"Smileys & Emotion","description":"skull and crossbones","unicode_version":""},{"emoji":"💩","aliases":["hankey","poop","shit"],"tags":["crap"],"category":"Smileys & Emotion","description":"pile of poo","unicode_version":"6.0"},{"emoji":"🤡","aliases":["clown_face"],"tags":[],"category":"Smileys & Emotion","description":"clown face","unicode_version":"9.0"},{"emoji":"👹","aliases":["japanese_ogre"],"tags":["monster"],"category":"Smileys & Emotion","description":"ogre","unicode_version":"6.0"},{"emoji":"👺","aliases":["japanese_goblin"],"tags":[],"category":"Smileys & Emotion","description":"goblin","unicode_version":"6.0"},{"emoji":"👻","aliases":["ghost"],"tags":["halloween"],"category":"Smileys & Emotion","description":"ghost","unicode_version":"6.0"},{"emoji":"👽","aliases":["alien"],"tags":["ufo"],"category":"Smileys & Emotion","description":"alien","unicode_version":"6.0"},{"emoji":"👾","aliases":["space_invader"],"tags":["game","retro"],"category":"Smileys & Emotion","description":"alien monster","unicode_version":"6.0"},{"emoji":"🤖","aliases":["robot"],"tags":[],"category":"Smileys & Emotion","description":"robot","unicode_version":"8.0"},{"emoji":"😺","aliases":["smiley_cat"],"tags":[],"category":"Smileys & Emotion","description":"grinning cat","unicode_version":"6.0"},{"emoji":"😸","aliases":["smile_cat"],"tags":[],"category":"Smileys & Emotion","description":"grinning cat with smiling eyes","unicode_version":"6.0"},{"emoji":"😹","aliases":["joy_cat"],"tags":[],"category":"Smileys & Emotion","description":"cat with tears of joy","unicode_version":"6.0"},{"emoji":"😻","aliases":["heart_eyes_cat"],"tags":[],"category":"Smileys & Emotion","description":"smiling cat with heart-eyes","unicode_version":"6.0"},{"emoji":"😼","aliases":["smirk_cat"],"tags":[],"category":"Smileys & Emotion","description":"cat with wry smile","unicode_version":"6.0"},{"emoji":"😽","aliases":["kissing_cat"],"tags":[],"category":"Smileys & Emotion","description":"kissing cat","unicode_version":"6.0"},{"emoji":"🙀","aliases":["scream_cat"],"tags":["horror"],"category":"Smileys & Emotion","description":"weary cat","unicode_version":"6.0"},{"emoji":"😿","aliases":["crying_cat_face"],"tags":["sad","tear"],"category":"Smileys & Emotion","description":"crying cat","unicode_version":"6.0"},{"emoji":"😾","aliases":["pouting_cat"],"tags":[],"category":"Smileys & Emotion","description":"pouting cat","unicode_version":"6.0"},{"emoji":"🙈","aliases":["see_no_evil"],"tags":["monkey","blind","ignore"],"category":"Smileys & Emotion","description":"see-no-evil monkey","unicode_version":"6.0"},{"emoji":"🙉","aliases":["hear_no_evil"],"tags":["monkey","deaf"],"category":"Smileys & Emotion","description":"hear-no-evil monkey","unicode_version":"6.0"},{"emoji":"🙊","aliases":["speak_no_evil"],"tags":["monkey","mute","hush"],"category":"Smileys & Emotion","description":"speak-no-evil monkey","unicode_version":"6.0"},{"emoji":"💋","aliases":["kiss"],"tags":["lipstick"],"category":"Smileys & Emotion","description":"kiss mark","unicode_version":"6.0"},{"emoji":"💌","aliases":["love_letter"],"tags":["email","envelope"],"category":"Smileys & Emotion","description":"love letter","unicode_version":"6.0"},{"emoji":"💘","aliases":["cupid"],"tags":["love","heart"],"category":"Smileys & Emotion","description":"heart with arrow","unicode_version":"6.0"},{"emoji":"💝","aliases":["gift_heart"],"tags":["chocolates"],"category":"Smileys & Emotion","description":"heart with ribbon","unicode_version":"6.0"},{"emoji":"💖","aliases":["sparkling_heart"],"tags":[],"category":"Smileys & Emotion","description":"sparkling heart","unicode_version":"6.0"},{"emoji":"💗","aliases":["heartpulse"],"tags":[],"category":"Smileys & Emotion","description":"growing heart","unicode_version":"6.0"},{"emoji":"💓","aliases":["heartbeat"],"tags":[],"category":"Smileys & Emotion","description":"beating heart","unicode_version":"6.0"},{"emoji":"💞","aliases":["revolving_hearts"],"tags":[],"category":"Smileys & Emotion","description":"revolving hearts","unicode_version":"6.0"},{"emoji":"💕","aliases":["two_hearts"],"tags":[],"category":"Smileys & Emotion","description":"two hearts","unicode_version":"6.0"},{"emoji":"💟","aliases":["heart_decoration"],"tags":[],"category":"Smileys & Emotion","description":"heart decoration","unicode_version":"6.0"},{"emoji":"❣️","aliases":["heavy_heart_exclamation"],"tags":[],"category":"Smileys & Emotion","description":"heart exclamation","unicode_version":""},{"emoji":"💔","aliases":["broken_heart"],"tags":[],"category":"Smileys & Emotion","description":"broken heart","unicode_version":"6.0"},{"emoji":"❤️‍🔥","aliases":["heart_on_fire"],"tags":[],"category":"Smileys & Emotion","description":"heart on fire","unicode_version":"13.1"},{"emoji":"❤️‍🩹","aliases":["mending_heart"],"tags":[],"category":"Smileys & Emotion","description":"mending heart","unicode_version":"13.1"},{"emoji":"❤️","aliases":["heart"],"tags":["love"],"category":"Smileys & Emotion","description":"red heart","unicode_version":""},{"emoji":"🧡","aliases":["orange_heart"],"tags":[],"category":"Smileys & Emotion","description":"orange heart","unicode_version":"11.0"},{"emoji":"💛","aliases":["yellow_heart"],"tags":[],"category":"Smileys & Emotion","description":"yellow heart","unicode_version":"6.0"},{"emoji":"💚","aliases":["green_heart"],"tags":[],"category":"Smileys & Emotion","description":"green heart","unicode_version":"6.0"},{"emoji":"💙","aliases":["blue_heart"],"tags":[],"category":"Smileys & Emotion","description":"blue heart","unicode_version":"6.0"},{"emoji":"💜","aliases":["purple_heart"],"tags":[],"category":"Smileys & Emotion","description":"purple heart","unicode_version":"6.0"},{"emoji":"🤎","aliases":["brown_heart"],"tags":[],"category":"Smileys & Emotion","description":"brown heart","unicode_version":"12.0"},{"emoji":"🖤","aliases":["black_heart"],"tags":[],"category":"Smileys & Emotion","description":"black heart","unicode_version":"9.0"},{"emoji":"🤍","aliases":["white_heart"],"tags":[],"category":"Smileys & Emotion","description":"white heart","unicode_version":"12.0"},{"emoji":"💯","aliases":["100"],"tags":["score","perfect"],"category":"Smileys & Emotion","description":"hundred points","unicode_version":"6.0"},{"emoji":"💢","aliases":["anger"],"tags":["angry"],"category":"Smileys & Emotion","description":"anger symbol","unicode_version":"6.0"},{"emoji":"💥","aliases":["boom","collision"],"tags":["explode"],"category":"Smileys & Emotion","description":"collision","unicode_version":"6.0"},{"emoji":"💫","aliases":["dizzy"],"tags":["star"],"category":"Smileys & Emotion","description":"dizzy","unicode_version":"6.0"},{"emoji":"💦","aliases":["sweat_drops"],"tags":["water","workout"],"category":"Smileys & Emotion","description":"sweat droplets","unicode_version":"6.0"},{"emoji":"💨","aliases":["dash"],"tags":["wind","blow","fast"],"category":"Smileys & Emotion","description":"dashing away","unicode_version":"6.0"},{"emoji":"🕳️","aliases":["hole"],"tags":[],"category":"Smileys & Emotion","description":"hole","unicode_version":"7.0"},{"emoji":"💣","aliases":["bomb"],"tags":["boom"],"category":"Smileys & Emotion","description":"bomb","unicode_version":"6.0"},{"emoji":"💬","aliases":["speech_balloon"],"tags":["comment"],"category":"Smileys & Emotion","description":"speech balloon","unicode_version":"6.0"},{"emoji":"👁️‍🗨️","aliases":["eye_speech_bubble"],"tags":[],"category":"Smileys & Emotion","description":"eye in speech bubble","unicode_version":"11.0"},{"emoji":"🗨️","aliases":["left_speech_bubble"],"tags":[],"category":"Smileys & Emotion","description":"left speech bubble","unicode_version":"11.0"},{"emoji":"🗯️","aliases":["right_anger_bubble"],"tags":[],"category":"Smileys & Emotion","description":"right anger bubble","unicode_version":"7.0"},{"emoji":"💭","aliases":["thought_balloon"],"tags":["thinking"],"category":"Smileys & Emotion","description":"thought balloon","unicode_version":"6.0"},{"emoji":"💤","aliases":["zzz"],"tags":["sleeping"],"category":"Smileys & Emotion","description":"zzz","unicode_version":"6.0"},{"emoji":"👋","aliases":["wave"],"tags":["goodbye"],"category":"People & Body","description":"waving hand","unicode_version":"6.0"},{"emoji":"🤚","aliases":["raised_back_of_hand"],"tags":[],"category":"People & Body","description":"raised back of hand","unicode_version":"9.0"},{"emoji":"🖐️","aliases":["raised_hand_with_fingers_splayed"],"tags":[],"category":"People & Body","description":"hand with fingers splayed","unicode_version":"7.0"},{"emoji":"✋","aliases":["hand","raised_hand"],"tags":["highfive","stop"],"category":"People & Body","description":"raised hand","unicode_version":"6.0"},{"emoji":"🖖","aliases":["vulcan_salute"],"tags":["prosper","spock"],"category":"People & Body","description":"vulcan salute","unicode_version":"7.0"},{"emoji":"👌","aliases":["ok_hand"],"tags":[],"category":"People & Body","description":"OK hand","unicode_version":"6.0"},{"emoji":"🤌","aliases":["pinched_fingers"],"tags":[],"category":"People & Body","description":"pinched fingers","unicode_version":"13.0"},{"emoji":"🤏","aliases":["pinching_hand"],"tags":[],"category":"People & Body","description":"pinching hand","unicode_version":"12.0"},{"emoji":"✌️","aliases":["v"],"tags":["victory","peace"],"category":"People & Body","description":"victory hand","unicode_version":""},{"emoji":"🤞","aliases":["crossed_fingers"],"tags":["luck","hopeful"],"category":"People & Body","description":"crossed fingers","unicode_version":"9.0"},{"emoji":"🤟","aliases":["love_you_gesture"],"tags":[],"category":"People & Body","description":"love-you gesture","unicode_version":"11.0"},{"emoji":"🤘","aliases":["metal"],"tags":[],"category":"People & Body","description":"sign of the horns","unicode_version":"8.0"},{"emoji":"🤙","aliases":["call_me_hand"],"tags":[],"category":"People & Body","description":"call me hand","unicode_version":"9.0"},{"emoji":"👈","aliases":["point_left"],"tags":[],"category":"People & Body","description":"backhand index pointing left","unicode_version":"6.0"},{"emoji":"👉","aliases":["point_right"],"tags":[],"category":"People & Body","description":"backhand index pointing right","unicode_version":"6.0"},{"emoji":"👆","aliases":["point_up_2"],"tags":[],"category":"People & Body","description":"backhand index pointing up","unicode_version":"6.0"},{"emoji":"🖕","aliases":["middle_finger","fu"],"tags":[],"category":"People & Body","description":"middle finger","unicode_version":"7.0"},{"emoji":"👇","aliases":["point_down"],"tags":[],"category":"People & Body","description":"backhand index pointing down","unicode_version":"6.0"},{"emoji":"☝️","aliases":["point_up"],"tags":[],"category":"People & Body","description":"index pointing up","unicode_version":""},{"emoji":"👍","aliases":["+1","thumbsup"],"tags":["approve","ok"],"category":"People & Body","description":"thumbs up","unicode_version":"6.0"},{"emoji":"👎","aliases":["-1","thumbsdown"],"tags":["disapprove","bury"],"category":"People & Body","description":"thumbs down","unicode_version":"6.0"},{"emoji":"✊","aliases":["fist_raised","fist"],"tags":["power"],"category":"People & Body","description":"raised fist","unicode_version":"6.0"},{"emoji":"👊","aliases":["fist_oncoming","facepunch","punch"],"tags":["attack"],"category":"People & Body","description":"oncoming fist","unicode_version":"6.0"},{"emoji":"🤛","aliases":["fist_left"],"tags":[],"category":"People & Body","description":"left-facing fist","unicode_version":"9.0"},{"emoji":"🤜","aliases":["fist_right"],"tags":[],"category":"People & Body","description":"right-facing fist","unicode_version":"9.0"},{"emoji":"👏","aliases":["clap"],"tags":["praise","applause"],"category":"People & Body","description":"clapping hands","unicode_version":"6.0"},{"emoji":"🙌","aliases":["raised_hands"],"tags":["hooray"],"category":"People & Body","description":"raising hands","unicode_version":"6.0"},{"emoji":"👐","aliases":["open_hands"],"tags":[],"category":"People & Body","description":"open hands","unicode_version":"6.0"},{"emoji":"🤲","aliases":["palms_up_together"],"tags":[],"category":"People & Body","description":"palms up together","unicode_version":"11.0"},{"emoji":"🤝","aliases":["handshake"],"tags":["deal"],"category":"People & Body","description":"handshake","unicode_version":"9.0"},{"emoji":"🙏","aliases":["pray"],"tags":["please","hope","wish"],"category":"People & Body","description":"folded hands","unicode_version":"6.0"},{"emoji":"✍️","aliases":["writing_hand"],"tags":[],"category":"People & Body","description":"writing hand","unicode_version":""},{"emoji":"💅","aliases":["nail_care"],"tags":["beauty","manicure"],"category":"People & Body","description":"nail polish","unicode_version":"6.0"},{"emoji":"🤳","aliases":["selfie"],"tags":[],"category":"People & Body","description":"selfie","unicode_version":"9.0"},{"emoji":"💪","aliases":["muscle"],"tags":["flex","bicep","strong","workout"],"category":"People & Body","description":"flexed biceps","unicode_version":"6.0"},{"emoji":"🦾","aliases":["mechanical_arm"],"tags":[],"category":"People & Body","description":"mechanical arm","unicode_version":"12.0"},{"emoji":"🦿","aliases":["mechanical_leg"],"tags":[],"category":"People & Body","description":"mechanical leg","unicode_version":"12.0"},{"emoji":"🦵","aliases":["leg"],"tags":[],"category":"People & Body","description":"leg","unicode_version":"11.0"},{"emoji":"🦶","aliases":["foot"],"tags":[],"category":"People & Body","description":"foot","unicode_version":"11.0"},{"emoji":"👂","aliases":["ear"],"tags":["hear","sound","listen"],"category":"People & Body","description":"ear","unicode_version":"6.0"},{"emoji":"🦻","aliases":["ear_with_hearing_aid"],"tags":[],"category":"People & Body","description":"ear with hearing aid","unicode_version":"12.0"},{"emoji":"👃","aliases":["nose"],"tags":["smell"],"category":"People & Body","description":"nose","unicode_version":"6.0"},{"emoji":"🧠","aliases":["brain"],"tags":[],"category":"People & Body","description":"brain","unicode_version":"11.0"},{"emoji":"🫀","aliases":["anatomical_heart"],"tags":[],"category":"People & Body","description":"anatomical heart","unicode_version":"13.0"},{"emoji":"🫁","aliases":["lungs"],"tags":[],"category":"People & Body","description":"lungs","unicode_version":"13.0"},{"emoji":"🦷","aliases":["tooth"],"tags":[],"category":"People & Body","description":"tooth","unicode_version":"11.0"},{"emoji":"🦴","aliases":["bone"],"tags":[],"category":"People & Body","description":"bone","unicode_version":"11.0"},{"emoji":"👀","aliases":["eyes"],"tags":["look","see","watch"],"category":"People & Body","description":"eyes","unicode_version":"6.0"},{"emoji":"👁️","aliases":["eye"],"tags":[],"category":"People & Body","description":"eye","unicode_version":"7.0"},{"emoji":"👅","aliases":["tongue"],"tags":["taste"],"category":"People & Body","description":"tongue","unicode_version":"6.0"},{"emoji":"👄","aliases":["lips"],"tags":["kiss"],"category":"People & Body","description":"mouth","unicode_version":"6.0"},{"emoji":"👶","aliases":["baby"],"tags":["child","newborn"],"category":"People & Body","description":"baby","unicode_version":"6.0"},{"emoji":"🧒","aliases":["child"],"tags":[],"category":"People & Body","description":"child","unicode_version":"11.0"},{"emoji":"👦","aliases":["boy"],"tags":["child"],"category":"People & Body","description":"boy","unicode_version":"6.0"},{"emoji":"👧","aliases":["girl"],"tags":["child"],"category":"People & Body","description":"girl","unicode_version":"6.0"},{"emoji":"🧑","aliases":["adult"],"tags":[],"category":"People & Body","description":"person","unicode_version":"11.0"},{"emoji":"👱","aliases":["blond_haired_person"],"tags":[],"category":"People & Body","description":"person: blond hair","unicode_version":"6.0"},{"emoji":"👨","aliases":["man"],"tags":["mustache","father","dad"],"category":"People & Body","description":"man","unicode_version":"6.0"},{"emoji":"🧔","aliases":["bearded_person"],"tags":[],"category":"People & Body","description":"person: beard","unicode_version":"11.0"},{"emoji":"🧔‍♂️","aliases":["man_beard"],"tags":[],"category":"People & Body","description":"man: beard","unicode_version":"13.1"},{"emoji":"🧔‍♀️","aliases":["woman_beard"],"tags":[],"category":"People & Body","description":"woman: beard","unicode_version":"13.1"},{"emoji":"👨‍🦰","aliases":["red_haired_man"],"tags":[],"category":"People & Body","description":"man: red hair","unicode_version":"11.0"},{"emoji":"👨‍🦱","aliases":["curly_haired_man"],"tags":[],"category":"People & Body","description":"man: curly hair","unicode_version":"11.0"},{"emoji":"👨‍🦳","aliases":["white_haired_man"],"tags":[],"category":"People & Body","description":"man: white hair","unicode_version":"11.0"},{"emoji":"👨‍🦲","aliases":["bald_man"],"tags":[],"category":"People & Body","description":"man: bald","unicode_version":"11.0"},{"emoji":"👩","aliases":["woman"],"tags":["girls"],"category":"People & Body","description":"woman","unicode_version":"6.0"},{"emoji":"👩‍🦰","aliases":["red_haired_woman"],"tags":[],"category":"People & Body","description":"woman: red hair","unicode_version":"11.0"},{"emoji":"🧑‍🦰","aliases":["person_red_hair"],"tags":[],"category":"People & Body","description":"person: red hair","unicode_version":"12.1"},{"emoji":"👩‍🦱","aliases":["curly_haired_woman"],"tags":[],"category":"People & Body","description":"woman: curly hair","unicode_version":"11.0"},{"emoji":"🧑‍🦱","aliases":["person_curly_hair"],"tags":[],"category":"People & Body","description":"person: curly hair","unicode_version":"12.1"},{"emoji":"👩‍🦳","aliases":["white_haired_woman"],"tags":[],"category":"People & Body","description":"woman: white hair","unicode_version":"11.0"},{"emoji":"🧑‍🦳","aliases":["person_white_hair"],"tags":[],"category":"People & Body","description":"person: white hair","unicode_version":"12.1"},{"emoji":"👩‍🦲","aliases":["bald_woman"],"tags":[],"category":"People & Body","description":"woman: bald","unicode_version":"11.0"},{"emoji":"🧑‍🦲","aliases":["person_bald"],"tags":[],"category":"People & Body","description":"person: bald","unicode_version":"12.1"},{"emoji":"👱‍♀️","aliases":["blond_haired_woman","blonde_woman"],"tags":[],"category":"People & Body","description":"woman: blond hair","unicode_version":"6.0"},{"emoji":"👱‍♂️","aliases":["blond_haired_man"],"tags":[],"category":"People & Body","description":"man: blond hair","unicode_version":"11.0"},{"emoji":"🧓","aliases":["older_adult"],"tags":[],"category":"People & Body","description":"older person","unicode_version":"11.0"},{"emoji":"👴","aliases":["older_man"],"tags":[],"category":"People & Body","description":"old man","unicode_version":"6.0"},{"emoji":"👵","aliases":["older_woman"],"tags":[],"category":"People & Body","description":"old woman","unicode_version":"6.0"},{"emoji":"🙍","aliases":["frowning_person"],"tags":[],"category":"People & Body","description":"person frowning","unicode_version":"6.0"},{"emoji":"🙍‍♂️","aliases":["frowning_man"],"tags":[],"category":"People & Body","description":"man frowning","unicode_version":"6.0"},{"emoji":"🙍‍♀️","aliases":["frowning_woman"],"tags":[],"category":"People & Body","description":"woman frowning","unicode_version":"11.0"},{"emoji":"🙎","aliases":["pouting_face"],"tags":[],"category":"People & Body","description":"person pouting","unicode_version":"6.0"},{"emoji":"🙎‍♂️","aliases":["pouting_man"],"tags":[],"category":"People & Body","description":"man pouting","unicode_version":"6.0"},{"emoji":"🙎‍♀️","aliases":["pouting_woman"],"tags":[],"category":"People & Body","description":"woman pouting","unicode_version":"11.0"},{"emoji":"🙅","aliases":["no_good"],"tags":["stop","halt","denied"],"category":"People & Body","description":"person gesturing NO","unicode_version":"6.0"},{"emoji":"🙅‍♂️","aliases":["no_good_man","ng_man"],"tags":["stop","halt","denied"],"category":"People & Body","description":"man gesturing NO","unicode_version":"6.0"},{"emoji":"🙅‍♀️","aliases":["no_good_woman","ng_woman"],"tags":["stop","halt","denied"],"category":"People & Body","description":"woman gesturing NO","unicode_version":"11.0"},{"emoji":"🙆","aliases":["ok_person"],"tags":[],"category":"People & Body","description":"person gesturing OK","unicode_version":"6.0"},{"emoji":"🙆‍♂️","aliases":["ok_man"],"tags":[],"category":"People & Body","description":"man gesturing OK","unicode_version":"6.0"},{"emoji":"🙆‍♀️","aliases":["ok_woman"],"tags":[],"category":"People & Body","description":"woman gesturing OK","unicode_version":"11.0"},{"emoji":"💁","aliases":["tipping_hand_person","information_desk_person"],"tags":[],"category":"People & Body","description":"person tipping hand","unicode_version":"6.0"},{"emoji":"💁‍♂️","aliases":["tipping_hand_man","sassy_man"],"tags":["information"],"category":"People & Body","description":"man tipping hand","unicode_version":"6.0"},{"emoji":"💁‍♀️","aliases":["tipping_hand_woman","sassy_woman"],"tags":["information"],"category":"People & Body","description":"woman tipping hand","unicode_version":"11.0"},{"emoji":"🙋","aliases":["raising_hand"],"tags":[],"category":"People & Body","description":"person raising hand","unicode_version":"6.0"},{"emoji":"🙋‍♂️","aliases":["raising_hand_man"],"tags":[],"category":"People & Body","description":"man raising hand","unicode_version":"6.0"},{"emoji":"🙋‍♀️","aliases":["raising_hand_woman"],"tags":[],"category":"People & Body","description":"woman raising hand","unicode_version":"11.0"},{"emoji":"🧏","aliases":["deaf_person"],"tags":[],"category":"People & Body","description":"deaf person","unicode_version":"12.0"},{"emoji":"🧏‍♂️","aliases":["deaf_man"],"tags":[],"category":"People & Body","description":"deaf man","unicode_version":"12.0"},{"emoji":"🧏‍♀️","aliases":["deaf_woman"],"tags":[],"category":"People & Body","description":"deaf woman","unicode_version":"12.0"},{"emoji":"🙇","aliases":["bow"],"tags":["respect","thanks"],"category":"People & Body","description":"person bowing","unicode_version":"6.0"},{"emoji":"🙇‍♂️","aliases":["bowing_man"],"tags":["respect","thanks"],"category":"People & Body","description":"man bowing","unicode_version":"11.0"},{"emoji":"🙇‍♀️","aliases":["bowing_woman"],"tags":["respect","thanks"],"category":"People & Body","description":"woman bowing","unicode_version":"6.0"},{"emoji":"🤦","aliases":["facepalm"],"tags":[],"category":"People & Body","description":"person facepalming","unicode_version":"11.0"},{"emoji":"🤦‍♂️","aliases":["man_facepalming"],"tags":[],"category":"People & Body","description":"man facepalming","unicode_version":"9.0"},{"emoji":"🤦‍♀️","aliases":["woman_facepalming"],"tags":[],"category":"People & Body","description":"woman facepalming","unicode_version":"9.0"},{"emoji":"🤷","aliases":["shrug"],"tags":[],"category":"People & Body","description":"person shrugging","unicode_version":"11.0"},{"emoji":"🤷‍♂️","aliases":["man_shrugging"],"tags":[],"category":"People & Body","description":"man shrugging","unicode_version":"9.0"},{"emoji":"🤷‍♀️","aliases":["woman_shrugging"],"tags":[],"category":"People & Body","description":"woman shrugging","unicode_version":"9.0"},{"emoji":"🧑‍⚕️","aliases":["health_worker"],"tags":[],"category":"People & Body","description":"health worker","unicode_version":"12.1"},{"emoji":"👨‍⚕️","aliases":["man_health_worker"],"tags":["doctor","nurse"],"category":"People & Body","description":"man health worker","unicode_version":""},{"emoji":"👩‍⚕️","aliases":["woman_health_worker"],"tags":["doctor","nurse"],"category":"People & Body","description":"woman health worker","unicode_version":""},{"emoji":"🧑‍🎓","aliases":["student"],"tags":[],"category":"People & Body","description":"student","unicode_version":"12.1"},{"emoji":"👨‍🎓","aliases":["man_student"],"tags":["graduation"],"category":"People & Body","description":"man student","unicode_version":""},{"emoji":"👩‍🎓","aliases":["woman_student"],"tags":["graduation"],"category":"People & Body","description":"woman student","unicode_version":""},{"emoji":"🧑‍🏫","aliases":["teacher"],"tags":[],"category":"People & Body","description":"teacher","unicode_version":"12.1"},{"emoji":"👨‍🏫","aliases":["man_teacher"],"tags":["school","professor"],"category":"People & Body","description":"man teacher","unicode_version":""},{"emoji":"👩‍🏫","aliases":["woman_teacher"],"tags":["school","professor"],"category":"People & Body","description":"woman teacher","unicode_version":""},{"emoji":"🧑‍⚖️","aliases":["judge"],"tags":[],"category":"People & Body","description":"judge","unicode_version":"12.1"},{"emoji":"👨‍⚖️","aliases":["man_judge"],"tags":["justice"],"category":"People & Body","description":"man judge","unicode_version":""},{"emoji":"👩‍⚖️","aliases":["woman_judge"],"tags":["justice"],"category":"People & Body","description":"woman judge","unicode_version":""},{"emoji":"🧑‍🌾","aliases":["farmer"],"tags":[],"category":"People & Body","description":"farmer","unicode_version":"12.1"},{"emoji":"👨‍🌾","aliases":["man_farmer"],"tags":[],"category":"People & Body","description":"man farmer","unicode_version":""},{"emoji":"👩‍🌾","aliases":["woman_farmer"],"tags":[],"category":"People & Body","description":"woman farmer","unicode_version":""},{"emoji":"🧑‍🍳","aliases":["cook"],"tags":[],"category":"People & Body","description":"cook","unicode_version":"12.1"},{"emoji":"👨‍🍳","aliases":["man_cook"],"tags":["chef"],"category":"People & Body","description":"man cook","unicode_version":""},{"emoji":"👩‍🍳","aliases":["woman_cook"],"tags":["chef"],"category":"People & Body","description":"woman cook","unicode_version":""},{"emoji":"🧑‍🔧","aliases":["mechanic"],"tags":[],"category":"People & Body","description":"mechanic","unicode_version":"12.1"},{"emoji":"👨‍🔧","aliases":["man_mechanic"],"tags":[],"category":"People & Body","description":"man mechanic","unicode_version":""},{"emoji":"👩‍🔧","aliases":["woman_mechanic"],"tags":[],"category":"People & Body","description":"woman mechanic","unicode_version":""},{"emoji":"🧑‍🏭","aliases":["factory_worker"],"tags":[],"category":"People & Body","description":"factory worker","unicode_version":"12.1"},{"emoji":"👨‍🏭","aliases":["man_factory_worker"],"tags":[],"category":"People & Body","description":"man factory worker","unicode_version":""},{"emoji":"👩‍🏭","aliases":["woman_factory_worker"],"tags":[],"category":"People & Body","description":"woman factory worker","unicode_version":""},{"emoji":"🧑‍💼","aliases":["office_worker"],"tags":[],"category":"People & Body","description":"office worker","unicode_version":"12.1"},{"emoji":"👨‍💼","aliases":["man_office_worker"],"tags":["business"],"category":"People & Body","description":"man office worker","unicode_version":""},{"emoji":"👩‍💼","aliases":["woman_office_worker"],"tags":["business"],"category":"People & Body","description":"woman office worker","unicode_version":""},{"emoji":"🧑‍🔬","aliases":["scientist"],"tags":[],"category":"People & Body","description":"scientist","unicode_version":"12.1"},{"emoji":"👨‍🔬","aliases":["man_scientist"],"tags":["research"],"category":"People & Body","description":"man scientist","unicode_version":""},{"emoji":"👩‍🔬","aliases":["woman_scientist"],"tags":["research"],"category":"People & Body","description":"woman scientist","unicode_version":""},{"emoji":"🧑‍💻","aliases":["technologist"],"tags":[],"category":"People & Body","description":"technologist","unicode_version":"12.1"},{"emoji":"👨‍💻","aliases":["man_technologist"],"tags":["coder"],"category":"People & Body","description":"man technologist","unicode_version":""},{"emoji":"👩‍💻","aliases":["woman_technologist"],"tags":["coder"],"category":"People & Body","description":"woman technologist","unicode_version":""},{"emoji":"🧑‍🎤","aliases":["singer"],"tags":[],"category":"People & Body","description":"singer","unicode_version":"12.1"},{"emoji":"👨‍🎤","aliases":["man_singer"],"tags":["rockstar"],"category":"People & Body","description":"man singer","unicode_version":""},{"emoji":"👩‍🎤","aliases":["woman_singer"],"tags":["rockstar"],"category":"People & Body","description":"woman singer","unicode_version":""},{"emoji":"🧑‍🎨","aliases":["artist"],"tags":[],"category":"People & Body","description":"artist","unicode_version":"12.1"},{"emoji":"👨‍🎨","aliases":["man_artist"],"tags":["painter"],"category":"People & Body","description":"man artist","unicode_version":""},{"emoji":"👩‍🎨","aliases":["woman_artist"],"tags":["painter"],"category":"People & Body","description":"woman artist","unicode_version":""},{"emoji":"🧑‍✈️","aliases":["pilot"],"tags":[],"category":"People & Body","description":"pilot","unicode_version":"12.1"},{"emoji":"👨‍✈️","aliases":["man_pilot"],"tags":[],"category":"People & Body","description":"man pilot","unicode_version":""},{"emoji":"👩‍✈️","aliases":["woman_pilot"],"tags":[],"category":"People & Body","description":"woman pilot","unicode_version":""},{"emoji":"🧑‍🚀","aliases":["astronaut"],"tags":[],"category":"People & Body","description":"astronaut","unicode_version":"12.1"},{"emoji":"👨‍🚀","aliases":["man_astronaut"],"tags":["space"],"category":"People & Body","description":"man astronaut","unicode_version":""},{"emoji":"👩‍🚀","aliases":["woman_astronaut"],"tags":["space"],"category":"People & Body","description":"woman astronaut","unicode_version":""},{"emoji":"🧑‍🚒","aliases":["firefighter"],"tags":[],"category":"People & Body","description":"firefighter","unicode_version":"12.1"},{"emoji":"👨‍🚒","aliases":["man_firefighter"],"tags":[],"category":"People & Body","description":"man firefighter","unicode_version":""},{"emoji":"👩‍🚒","aliases":["woman_firefighter"],"tags":[],"category":"People & Body","description":"woman firefighter","unicode_version":""},{"emoji":"👮","aliases":["police_officer","cop"],"tags":["law"],"category":"People & Body","description":"police officer","unicode_version":"6.0"},{"emoji":"👮‍♂️","aliases":["policeman"],"tags":["law","cop"],"category":"People & Body","description":"man police officer","unicode_version":"11.0"},{"emoji":"👮‍♀️","aliases":["policewoman"],"tags":["law","cop"],"category":"People & Body","description":"woman police officer","unicode_version":"6.0"},{"emoji":"🕵️","aliases":["detective"],"tags":["sleuth"],"category":"People & Body","description":"detective","unicode_version":"7.0"},{"emoji":"🕵️‍♂️","aliases":["male_detective"],"tags":["sleuth"],"category":"People & Body","description":"man detective","unicode_version":"11.0"},{"emoji":"🕵️‍♀️","aliases":["female_detective"],"tags":["sleuth"],"category":"People & Body","description":"woman detective","unicode_version":"6.0"},{"emoji":"💂","aliases":["guard"],"tags":[],"category":"People & Body","description":"guard","unicode_version":"6.0"},{"emoji":"💂‍♂️","aliases":["guardsman"],"tags":[],"category":"People & Body","description":"man guard","unicode_version":"11.0"},{"emoji":"💂‍♀️","aliases":["guardswoman"],"tags":[],"category":"People & Body","description":"woman guard","unicode_version":"6.0"},{"emoji":"🥷","aliases":["ninja"],"tags":[],"category":"People & Body","description":"ninja","unicode_version":"13.0"},{"emoji":"👷","aliases":["construction_worker"],"tags":["helmet"],"category":"People & Body","description":"construction worker","unicode_version":"6.0"},{"emoji":"👷‍♂️","aliases":["construction_worker_man"],"tags":["helmet"],"category":"People & Body","description":"man construction worker","unicode_version":"11.0"},{"emoji":"👷‍♀️","aliases":["construction_worker_woman"],"tags":["helmet"],"category":"People & Body","description":"woman construction worker","unicode_version":"6.0"},{"emoji":"🤴","aliases":["prince"],"tags":["crown","royal"],"category":"People & Body","description":"prince","unicode_version":"9.0"},{"emoji":"👸","aliases":["princess"],"tags":["crown","royal"],"category":"People & Body","description":"princess","unicode_version":"6.0"},{"emoji":"👳","aliases":["person_with_turban"],"tags":[],"category":"People & Body","description":"person wearing turban","unicode_version":"6.0"},{"emoji":"👳‍♂️","aliases":["man_with_turban"],"tags":[],"category":"People & Body","description":"man wearing turban","unicode_version":"11.0"},{"emoji":"👳‍♀️","aliases":["woman_with_turban"],"tags":[],"category":"People & Body","description":"woman wearing turban","unicode_version":"6.0"},{"emoji":"👲","aliases":["man_with_gua_pi_mao"],"tags":[],"category":"People & Body","description":"person with skullcap","unicode_version":"6.0"},{"emoji":"🧕","aliases":["woman_with_headscarf"],"tags":["hijab"],"category":"People & Body","description":"woman with headscarf","unicode_version":"11.0"},{"emoji":"🤵","aliases":["person_in_tuxedo"],"tags":["groom","marriage","wedding"],"category":"People & Body","description":"person in tuxedo","unicode_version":"9.0"},{"emoji":"🤵‍♂️","aliases":["man_in_tuxedo"],"tags":[],"category":"People & Body","description":"man in tuxedo","unicode_version":"13.0"},{"emoji":"🤵‍♀️","aliases":["woman_in_tuxedo"],"tags":[],"category":"People & Body","description":"woman in tuxedo","unicode_version":"13.0"},{"emoji":"👰","aliases":["person_with_veil"],"tags":["marriage","wedding"],"category":"People & Body","description":"person with veil","unicode_version":"6.0"},{"emoji":"👰‍♂️","aliases":["man_with_veil"],"tags":[],"category":"People & Body","description":"man with veil","unicode_version":"13.0"},{"emoji":"👰‍♀️","aliases":["woman_with_veil","bride_with_veil"],"tags":[],"category":"People & Body","description":"woman with veil","unicode_version":"13.0"},{"emoji":"🤰","aliases":["pregnant_woman"],"tags":[],"category":"People & Body","description":"pregnant woman","unicode_version":"9.0"},{"emoji":"🤱","aliases":["breast_feeding"],"tags":["nursing"],"category":"People & Body","description":"breast-feeding","unicode_version":"11.0"},{"emoji":"👩‍🍼","aliases":["woman_feeding_baby"],"tags":[],"category":"People & Body","description":"woman feeding baby","unicode_version":"13.0"},{"emoji":"👨‍🍼","aliases":["man_feeding_baby"],"tags":[],"category":"People & Body","description":"man feeding baby","unicode_version":"13.0"},{"emoji":"🧑‍🍼","aliases":["person_feeding_baby"],"tags":[],"category":"People & Body","description":"person feeding baby","unicode_version":"13.0"},{"emoji":"👼","aliases":["angel"],"tags":[],"category":"People & Body","description":"baby angel","unicode_version":"6.0"},{"emoji":"🎅","aliases":["santa"],"tags":["christmas"],"category":"People & Body","description":"Santa Claus","unicode_version":"6.0"},{"emoji":"🤶","aliases":["mrs_claus"],"tags":["santa"],"category":"People & Body","description":"Mrs. Claus","unicode_version":"9.0"},{"emoji":"🧑‍🎄","aliases":["mx_claus"],"tags":[],"category":"People & Body","description":"mx claus","unicode_version":"13.0"},{"emoji":"🦸","aliases":["superhero"],"tags":[],"category":"People & Body","description":"superhero","unicode_version":"11.0"},{"emoji":"🦸‍♂️","aliases":["superhero_man"],"tags":[],"category":"People & Body","description":"man superhero","unicode_version":"11.0"},{"emoji":"🦸‍♀️","aliases":["superhero_woman"],"tags":[],"category":"People & Body","description":"woman superhero","unicode_version":"11.0"},{"emoji":"🦹","aliases":["supervillain"],"tags":[],"category":"People & Body","description":"supervillain","unicode_version":"11.0"},{"emoji":"🦹‍♂️","aliases":["supervillain_man"],"tags":[],"category":"People & Body","description":"man supervillain","unicode_version":"11.0"},{"emoji":"🦹‍♀️","aliases":["supervillain_woman"],"tags":[],"category":"People & Body","description":"woman supervillain","unicode_version":"11.0"},{"emoji":"🧙","aliases":["mage"],"tags":["wizard"],"category":"People & Body","description":"mage","unicode_version":"11.0"},{"emoji":"🧙‍♂️","aliases":["mage_man"],"tags":["wizard"],"category":"People & Body","description":"man mage","unicode_version":"11.0"},{"emoji":"🧙‍♀️","aliases":["mage_woman"],"tags":["wizard"],"category":"People & Body","description":"woman mage","unicode_version":"11.0"},{"emoji":"🧚","aliases":["fairy"],"tags":[],"category":"People & Body","description":"fairy","unicode_version":"11.0"},{"emoji":"🧚‍♂️","aliases":["fairy_man"],"tags":[],"category":"People & Body","description":"man fairy","unicode_version":"11.0"},{"emoji":"🧚‍♀️","aliases":["fairy_woman"],"tags":[],"category":"People & Body","description":"woman fairy","unicode_version":"11.0"},{"emoji":"🧛","aliases":["vampire"],"tags":[],"category":"People & Body","description":"vampire","unicode_version":"11.0"},{"emoji":"🧛‍♂️","aliases":["vampire_man"],"tags":[],"category":"People & Body","description":"man vampire","unicode_version":"11.0"},{"emoji":"🧛‍♀️","aliases":["vampire_woman"],"tags":[],"category":"People & Body","description":"woman vampire","unicode_version":"11.0"},{"emoji":"🧜","aliases":["merperson"],"tags":[],"category":"People & Body","description":"merperson","unicode_version":"11.0"},{"emoji":"🧜‍♂️","aliases":["merman"],"tags":[],"category":"People & Body","description":"merman","unicode_version":"11.0"},{"emoji":"🧜‍♀️","aliases":["mermaid"],"tags":[],"category":"People & Body","description":"mermaid","unicode_version":"11.0"},{"emoji":"🧝","aliases":["elf"],"tags":[],"category":"People & Body","description":"elf","unicode_version":"11.0"},{"emoji":"🧝‍♂️","aliases":["elf_man"],"tags":[],"category":"People & Body","description":"man elf","unicode_version":"11.0"},{"emoji":"🧝‍♀️","aliases":["elf_woman"],"tags":[],"category":"People & Body","description":"woman elf","unicode_version":"11.0"},{"emoji":"🧞","aliases":["genie"],"tags":[],"category":"People & Body","description":"genie","unicode_version":"11.0"},{"emoji":"🧞‍♂️","aliases":["genie_man"],"tags":[],"category":"People & Body","description":"man genie","unicode_version":"11.0"},{"emoji":"🧞‍♀️","aliases":["genie_woman"],"tags":[],"category":"People & Body","description":"woman genie","unicode_version":"11.0"},{"emoji":"🧟","aliases":["zombie"],"tags":[],"category":"People & Body","description":"zombie","unicode_version":"11.0"},{"emoji":"🧟‍♂️","aliases":["zombie_man"],"tags":[],"category":"People & Body","description":"man zombie","unicode_version":"11.0"},{"emoji":"🧟‍♀️","aliases":["zombie_woman"],"tags":[],"category":"People & Body","description":"woman zombie","unicode_version":"11.0"},{"emoji":"💆","aliases":["massage"],"tags":["spa"],"category":"People & Body","description":"person getting massage","unicode_version":"6.0"},{"emoji":"💆‍♂️","aliases":["massage_man"],"tags":["spa"],"category":"People & Body","description":"man getting massage","unicode_version":"6.0"},{"emoji":"💆‍♀️","aliases":["massage_woman"],"tags":["spa"],"category":"People & Body","description":"woman getting massage","unicode_version":"11.0"},{"emoji":"💇","aliases":["haircut"],"tags":["beauty"],"category":"People & Body","description":"person getting haircut","unicode_version":"6.0"},{"emoji":"💇‍♂️","aliases":["haircut_man"],"tags":[],"category":"People & Body","description":"man getting haircut","unicode_version":"6.0"},{"emoji":"💇‍♀️","aliases":["haircut_woman"],"tags":[],"category":"People & Body","description":"woman getting haircut","unicode_version":"11.0"},{"emoji":"🚶","aliases":["walking"],"tags":[],"category":"People & Body","description":"person walking","unicode_version":"6.0"},{"emoji":"🚶‍♂️","aliases":["walking_man"],"tags":[],"category":"People & Body","description":"man walking","unicode_version":"11.0"},{"emoji":"🚶‍♀️","aliases":["walking_woman"],"tags":[],"category":"People & Body","description":"woman walking","unicode_version":"6.0"},{"emoji":"🧍","aliases":["standing_person"],"tags":[],"category":"People & Body","description":"person standing","unicode_version":"12.0"},{"emoji":"🧍‍♂️","aliases":["standing_man"],"tags":[],"category":"People & Body","description":"man standing","unicode_version":"12.0"},{"emoji":"🧍‍♀️","aliases":["standing_woman"],"tags":[],"category":"People & Body","description":"woman standing","unicode_version":"12.0"},{"emoji":"🧎","aliases":["kneeling_person"],"tags":[],"category":"People & Body","description":"person kneeling","unicode_version":"12.0"},{"emoji":"🧎‍♂️","aliases":["kneeling_man"],"tags":[],"category":"People & Body","description":"man kneeling","unicode_version":"12.0"},{"emoji":"🧎‍♀️","aliases":["kneeling_woman"],"tags":[],"category":"People & Body","description":"woman kneeling","unicode_version":"12.0"},{"emoji":"🧑‍🦯","aliases":["person_with_probing_cane"],"tags":[],"category":"People & Body","description":"person with white cane","unicode_version":"12.1"},{"emoji":"👨‍🦯","aliases":["man_with_probing_cane"],"tags":[],"category":"People & Body","description":"man with white cane","unicode_version":"12.0"},{"emoji":"👩‍🦯","aliases":["woman_with_probing_cane"],"tags":[],"category":"People & Body","description":"woman with white cane","unicode_version":"12.0"},{"emoji":"🧑‍🦼","aliases":["person_in_motorized_wheelchair"],"tags":[],"category":"People & Body","description":"person in motorized wheelchair","unicode_version":"12.1"},{"emoji":"👨‍🦼","aliases":["man_in_motorized_wheelchair"],"tags":[],"category":"People & Body","description":"man in motorized wheelchair","unicode_version":"12.0"},{"emoji":"👩‍🦼","aliases":["woman_in_motorized_wheelchair"],"tags":[],"category":"People & Body","description":"woman in motorized wheelchair","unicode_version":"12.0"},{"emoji":"🧑‍🦽","aliases":["person_in_manual_wheelchair"],"tags":[],"category":"People & Body","description":"person in manual wheelchair","unicode_version":"12.1"},{"emoji":"👨‍🦽","aliases":["man_in_manual_wheelchair"],"tags":[],"category":"People & Body","description":"man in manual wheelchair","unicode_version":"12.0"},{"emoji":"👩‍🦽","aliases":["woman_in_manual_wheelchair"],"tags":[],"category":"People & Body","description":"woman in manual wheelchair","unicode_version":"12.0"},{"emoji":"🏃","aliases":["runner","running"],"tags":["exercise","workout","marathon"],"category":"People & Body","description":"person running","unicode_version":"6.0"},{"emoji":"🏃‍♂️","aliases":["running_man"],"tags":["exercise","workout","marathon"],"category":"People & Body","description":"man running","unicode_version":"11.0"},{"emoji":"🏃‍♀️","aliases":["running_woman"],"tags":["exercise","workout","marathon"],"category":"People & Body","description":"woman running","unicode_version":"6.0"},{"emoji":"💃","aliases":["woman_dancing","dancer"],"tags":["dress"],"category":"People & Body","description":"woman dancing","unicode_version":"6.0"},{"emoji":"🕺","aliases":["man_dancing"],"tags":["dancer"],"category":"People & Body","description":"man dancing","unicode_version":"9.0"},{"emoji":"🕴️","aliases":["business_suit_levitating"],"tags":[],"category":"People & Body","description":"person in suit levitating","unicode_version":"7.0"},{"emoji":"👯","aliases":["dancers"],"tags":["bunny"],"category":"People & Body","description":"people with bunny ears","unicode_version":"6.0"},{"emoji":"👯‍♂️","aliases":["dancing_men"],"tags":["bunny"],"category":"People & Body","description":"men with bunny ears","unicode_version":"6.0"},{"emoji":"👯‍♀️","aliases":["dancing_women"],"tags":["bunny"],"category":"People & Body","description":"women with bunny ears","unicode_version":"11.0"},{"emoji":"🧖","aliases":["sauna_person"],"tags":["steamy"],"category":"People & Body","description":"person in steamy room","unicode_version":"11.0"},{"emoji":"🧖‍♂️","aliases":["sauna_man"],"tags":["steamy"],"category":"People & Body","description":"man in steamy room","unicode_version":"11.0"},{"emoji":"🧖‍♀️","aliases":["sauna_woman"],"tags":["steamy"],"category":"People & Body","description":"woman in steamy room","unicode_version":"11.0"},{"emoji":"🧗","aliases":["climbing"],"tags":["bouldering"],"category":"People & Body","description":"person climbing","unicode_version":"11.0"},{"emoji":"🧗‍♂️","aliases":["climbing_man"],"tags":["bouldering"],"category":"People & Body","description":"man climbing","unicode_version":"11.0"},{"emoji":"🧗‍♀️","aliases":["climbing_woman"],"tags":["bouldering"],"category":"People & Body","description":"woman climbing","unicode_version":"11.0"},{"emoji":"🤺","aliases":["person_fencing"],"tags":[],"category":"People & Body","description":"person fencing","unicode_version":"9.0"},{"emoji":"🏇","aliases":["horse_racing"],"tags":[],"category":"People & Body","description":"horse racing","unicode_version":"6.0"},{"emoji":"⛷️","aliases":["skier"],"tags":[],"category":"People & Body","description":"skier","unicode_version":"5.2"},{"emoji":"🏂","aliases":["snowboarder"],"tags":[],"category":"People & Body","description":"snowboarder","unicode_version":"6.0"},{"emoji":"🏌️","aliases":["golfing"],"tags":[],"category":"People & Body","description":"person golfing","unicode_version":"7.0"},{"emoji":"🏌️‍♂️","aliases":["golfing_man"],"tags":[],"category":"People & Body","description":"man golfing","unicode_version":"11.0"},{"emoji":"🏌️‍♀️","aliases":["golfing_woman"],"tags":[],"category":"People & Body","description":"woman golfing","unicode_version":""},{"emoji":"🏄","aliases":["surfer"],"tags":[],"category":"People & Body","description":"person surfing","unicode_version":"6.0"},{"emoji":"🏄‍♂️","aliases":["surfing_man"],"tags":[],"category":"People & Body","description":"man surfing","unicode_version":"11.0"},{"emoji":"🏄‍♀️","aliases":["surfing_woman"],"tags":[],"category":"People & Body","description":"woman surfing","unicode_version":"7.0"},{"emoji":"🚣","aliases":["rowboat"],"tags":[],"category":"People & Body","description":"person rowing boat","unicode_version":"6.0"},{"emoji":"🚣‍♂️","aliases":["rowing_man"],"tags":[],"category":"People & Body","description":"man rowing boat","unicode_version":"11.0"},{"emoji":"🚣‍♀️","aliases":["rowing_woman"],"tags":[],"category":"People & Body","description":"woman rowing boat","unicode_version":"6.0"},{"emoji":"🏊","aliases":["swimmer"],"tags":[],"category":"People & Body","description":"person swimming","unicode_version":"6.0"},{"emoji":"🏊‍♂️","aliases":["swimming_man"],"tags":[],"category":"People & Body","description":"man swimming","unicode_version":"11.0"},{"emoji":"🏊‍♀️","aliases":["swimming_woman"],"tags":[],"category":"People & Body","description":"woman swimming","unicode_version":"6.0"},{"emoji":"⛹️","aliases":["bouncing_ball_person"],"tags":["basketball"],"category":"People & Body","description":"person bouncing ball","unicode_version":"5.2"},{"emoji":"⛹️‍♂️","aliases":["bouncing_ball_man","basketball_man"],"tags":[],"category":"People & Body","description":"man bouncing ball","unicode_version":"11.0"},{"emoji":"⛹️‍♀️","aliases":["bouncing_ball_woman","basketball_woman"],"tags":[],"category":"People & Body","description":"woman bouncing ball","unicode_version":"7.0"},{"emoji":"🏋️","aliases":["weight_lifting"],"tags":["gym","workout"],"category":"People & Body","description":"person lifting weights","unicode_version":"7.0"},{"emoji":"🏋️‍♂️","aliases":["weight_lifting_man"],"tags":["gym","workout"],"category":"People & Body","description":"man lifting weights","unicode_version":"11.0"},{"emoji":"🏋️‍♀️","aliases":["weight_lifting_woman"],"tags":["gym","workout"],"category":"People & Body","description":"woman lifting weights","unicode_version":"6.0"},{"emoji":"🚴","aliases":["bicyclist"],"tags":[],"category":"People & Body","description":"person biking","unicode_version":"6.0"},{"emoji":"🚴‍♂️","aliases":["biking_man"],"tags":[],"category":"People & Body","description":"man biking","unicode_version":"11.0"},{"emoji":"🚴‍♀️","aliases":["biking_woman"],"tags":[],"category":"People & Body","description":"woman biking","unicode_version":"6.0"},{"emoji":"🚵","aliases":["mountain_bicyclist"],"tags":[],"category":"People & Body","description":"person mountain biking","unicode_version":"6.0"},{"emoji":"🚵‍♂️","aliases":["mountain_biking_man"],"tags":[],"category":"People & Body","description":"man mountain biking","unicode_version":"11.0"},{"emoji":"🚵‍♀️","aliases":["mountain_biking_woman"],"tags":[],"category":"People & Body","description":"woman mountain biking","unicode_version":"6.0"},{"emoji":"🤸","aliases":["cartwheeling"],"tags":[],"category":"People & Body","description":"person cartwheeling","unicode_version":"11.0"},{"emoji":"🤸‍♂️","aliases":["man_cartwheeling"],"tags":[],"category":"People & Body","description":"man cartwheeling","unicode_version":""},{"emoji":"🤸‍♀️","aliases":["woman_cartwheeling"],"tags":[],"category":"People & Body","description":"woman cartwheeling","unicode_version":""},{"emoji":"🤼","aliases":["wrestling"],"tags":[],"category":"People & Body","description":"people wrestling","unicode_version":"11.0"},{"emoji":"🤼‍♂️","aliases":["men_wrestling"],"tags":[],"category":"People & Body","description":"men wrestling","unicode_version":"9.0"},{"emoji":"🤼‍♀️","aliases":["women_wrestling"],"tags":[],"category":"People & Body","description":"women wrestling","unicode_version":"9.0"},{"emoji":"🤽","aliases":["water_polo"],"tags":[],"category":"People & Body","description":"person playing water polo","unicode_version":"11.0"},{"emoji":"🤽‍♂️","aliases":["man_playing_water_polo"],"tags":[],"category":"People & Body","description":"man playing water polo","unicode_version":"9.0"},{"emoji":"🤽‍♀️","aliases":["woman_playing_water_polo"],"tags":[],"category":"People & Body","description":"woman playing water polo","unicode_version":"9.0"},{"emoji":"🤾","aliases":["handball_person"],"tags":[],"category":"People & Body","description":"person playing handball","unicode_version":"11.0"},{"emoji":"🤾‍♂️","aliases":["man_playing_handball"],"tags":[],"category":"People & Body","description":"man playing handball","unicode_version":"9.0"},{"emoji":"🤾‍♀️","aliases":["woman_playing_handball"],"tags":[],"category":"People & Body","description":"woman playing handball","unicode_version":"9.0"},{"emoji":"🤹","aliases":["juggling_person"],"tags":[],"category":"People & Body","description":"person juggling","unicode_version":"11.0"},{"emoji":"🤹‍♂️","aliases":["man_juggling"],"tags":[],"category":"People & Body","description":"man juggling","unicode_version":"9.0"},{"emoji":"🤹‍♀️","aliases":["woman_juggling"],"tags":[],"category":"People & Body","description":"woman juggling","unicode_version":"9.0"},{"emoji":"🧘","aliases":["lotus_position"],"tags":["meditation"],"category":"People & Body","description":"person in lotus position","unicode_version":"11.0"},{"emoji":"🧘‍♂️","aliases":["lotus_position_man"],"tags":["meditation"],"category":"People & Body","description":"man in lotus position","unicode_version":"11.0"},{"emoji":"🧘‍♀️","aliases":["lotus_position_woman"],"tags":["meditation"],"category":"People & Body","description":"woman in lotus position","unicode_version":"11.0"},{"emoji":"🛀","aliases":["bath"],"tags":["shower"],"category":"People & Body","description":"person taking bath","unicode_version":"6.0"},{"emoji":"🛌","aliases":["sleeping_bed"],"tags":[],"category":"People & Body","description":"person in bed","unicode_version":"7.0"},{"emoji":"🧑‍🤝‍🧑","aliases":["people_holding_hands"],"tags":["couple","date"],"category":"People & Body","description":"people holding hands","unicode_version":"12.0"},{"emoji":"👭","aliases":["two_women_holding_hands"],"tags":["couple","date"],"category":"People & Body","description":"women holding hands","unicode_version":"6.0"},{"emoji":"👫","aliases":["couple"],"tags":["date"],"category":"People & Body","description":"woman and man holding hands","unicode_version":"6.0"},{"emoji":"👬","aliases":["two_men_holding_hands"],"tags":["couple","date"],"category":"People & Body","description":"men holding hands","unicode_version":"6.0"},{"emoji":"💏","aliases":["couplekiss"],"tags":[],"category":"People & Body","description":"kiss","unicode_version":"6.0"},{"emoji":"👩‍❤️‍💋‍👨","aliases":["couplekiss_man_woman"],"tags":[],"category":"People & Body","description":"kiss: woman, man","unicode_version":"11.0"},{"emoji":"👨‍❤️‍💋‍👨","aliases":["couplekiss_man_man"],"tags":[],"category":"People & Body","description":"kiss: man, man","unicode_version":"6.0"},{"emoji":"👩‍❤️‍💋‍👩","aliases":["couplekiss_woman_woman"],"tags":[],"category":"People & Body","description":"kiss: woman, woman","unicode_version":"6.0"},{"emoji":"💑","aliases":["couple_with_heart"],"tags":[],"category":"People & Body","description":"couple with heart","unicode_version":"6.0"},{"emoji":"👩‍❤️‍👨","aliases":["couple_with_heart_woman_man"],"tags":[],"category":"People & Body","description":"couple with heart: woman, man","unicode_version":"11.0"},{"emoji":"👨‍❤️‍👨","aliases":["couple_with_heart_man_man"],"tags":[],"category":"People & Body","description":"couple with heart: man, man","unicode_version":"6.0"},{"emoji":"👩‍❤️‍👩","aliases":["couple_with_heart_woman_woman"],"tags":[],"category":"People & Body","description":"couple with heart: woman, woman","unicode_version":"6.0"},{"emoji":"👪","aliases":["family"],"tags":["home","parents","child"],"category":"People & Body","description":"family","unicode_version":"6.0"},{"emoji":"👨‍👩‍👦","aliases":["family_man_woman_boy"],"tags":[],"category":"People & Body","description":"family: man, woman, boy","unicode_version":"11.0"},{"emoji":"👨‍👩‍👧","aliases":["family_man_woman_girl"],"tags":[],"category":"People & Body","description":"family: man, woman, girl","unicode_version":"6.0"},{"emoji":"👨‍👩‍👧‍👦","aliases":["family_man_woman_girl_boy"],"tags":[],"category":"People & Body","description":"family: man, woman, girl, boy","unicode_version":"6.0"},{"emoji":"👨‍👩‍👦‍👦","aliases":["family_man_woman_boy_boy"],"tags":[],"category":"People & Body","description":"family: man, woman, boy, boy","unicode_version":"6.0"},{"emoji":"👨‍👩‍👧‍👧","aliases":["family_man_woman_girl_girl"],"tags":[],"category":"People & Body","description":"family: man, woman, girl, girl","unicode_version":"6.0"},{"emoji":"👨‍👨‍👦","aliases":["family_man_man_boy"],"tags":[],"category":"People & Body","description":"family: man, man, boy","unicode_version":"6.0"},{"emoji":"👨‍👨‍👧","aliases":["family_man_man_girl"],"tags":[],"category":"People & Body","description":"family: man, man, girl","unicode_version":"6.0"},{"emoji":"👨‍👨‍👧‍👦","aliases":["family_man_man_girl_boy"],"tags":[],"category":"People & Body","description":"family: man, man, girl, boy","unicode_version":"6.0"},{"emoji":"👨‍👨‍👦‍👦","aliases":["family_man_man_boy_boy"],"tags":[],"category":"People & Body","description":"family: man, man, boy, boy","unicode_version":"6.0"},{"emoji":"👨‍👨‍👧‍👧","aliases":["family_man_man_girl_girl"],"tags":[],"category":"People & Body","description":"family: man, man, girl, girl","unicode_version":"6.0"},{"emoji":"👩‍👩‍👦","aliases":["family_woman_woman_boy"],"tags":[],"category":"People & Body","description":"family: woman, woman, boy","unicode_version":"6.0"},{"emoji":"👩‍👩‍👧","aliases":["family_woman_woman_girl"],"tags":[],"category":"People & Body","description":"family: woman, woman, girl","unicode_version":"6.0"},{"emoji":"👩‍👩‍👧‍👦","aliases":["family_woman_woman_girl_boy"],"tags":[],"category":"People & Body","description":"family: woman, woman, girl, boy","unicode_version":"6.0"},{"emoji":"👩‍👩‍👦‍👦","aliases":["family_woman_woman_boy_boy"],"tags":[],"category":"People & Body","description":"family: woman, woman, boy, boy","unicode_version":"6.0"},{"emoji":"👩‍👩‍👧‍👧","aliases":["family_woman_woman_girl_girl"],"tags":[],"category":"People & Body","description":"family: woman, woman, girl, girl","unicode_version":"6.0"},{"emoji":"👨‍👦","aliases":["family_man_boy"],"tags":[],"category":"People & Body","description":"family: man, boy","unicode_version":"6.0"},{"emoji":"👨‍👦‍👦","aliases":["family_man_boy_boy"],"tags":[],"category":"People & Body","description":"family: man, boy, boy","unicode_version":"6.0"},{"emoji":"👨‍👧","aliases":["family_man_girl"],"tags":[],"category":"People & Body","description":"family: man, girl","unicode_version":"6.0"},{"emoji":"👨‍👧‍👦","aliases":["family_man_girl_boy"],"tags":[],"category":"People & Body","description":"family: man, girl, boy","unicode_version":"6.0"},{"emoji":"👨‍👧‍👧","aliases":["family_man_girl_girl"],"tags":[],"category":"People & Body","description":"family: man, girl, girl","unicode_version":"6.0"},{"emoji":"👩‍👦","aliases":["family_woman_boy"],"tags":[],"category":"People & Body","description":"family: woman, boy","unicode_version":"6.0"},{"emoji":"👩‍👦‍👦","aliases":["family_woman_boy_boy"],"tags":[],"category":"People & Body","description":"family: woman, boy, boy","unicode_version":"6.0"},{"emoji":"👩‍👧","aliases":["family_woman_girl"],"tags":[],"category":"People & Body","description":"family: woman, girl","unicode_version":"6.0"},{"emoji":"👩‍👧‍👦","aliases":["family_woman_girl_boy"],"tags":[],"category":"People & Body","description":"family: woman, girl, boy","unicode_version":"6.0"},{"emoji":"👩‍👧‍👧","aliases":["family_woman_girl_girl"],"tags":[],"category":"People & Body","description":"family: woman, girl, girl","unicode_version":"6.0"},{"emoji":"🗣️","aliases":["speaking_head"],"tags":[],"category":"People & Body","description":"speaking head","unicode_version":"7.0"},{"emoji":"👤","aliases":["bust_in_silhouette"],"tags":["user"],"category":"People & Body","description":"bust in silhouette","unicode_version":"6.0"},{"emoji":"👥","aliases":["busts_in_silhouette"],"tags":["users","group","team"],"category":"People & Body","description":"busts in silhouette","unicode_version":"6.0"},{"emoji":"🫂","aliases":["people_hugging"],"tags":[],"category":"People & Body","description":"people hugging","unicode_version":"13.0"},{"emoji":"👣","aliases":["footprints"],"tags":["feet","tracks"],"category":"People & Body","description":"footprints","unicode_version":"6.0"},{"emoji":"🐵","aliases":["monkey_face"],"tags":[],"category":"Animals & Nature","description":"monkey face","unicode_version":"6.0"},{"emoji":"🐒","aliases":["monkey"],"tags":[],"category":"Animals & Nature","description":"monkey","unicode_version":"6.0"},{"emoji":"🦍","aliases":["gorilla"],"tags":[],"category":"Animals & Nature","description":"gorilla","unicode_version":"9.0"},{"emoji":"🦧","aliases":["orangutan"],"tags":[],"category":"Animals & Nature","description":"orangutan","unicode_version":"12.0"},{"emoji":"🐶","aliases":["dog"],"tags":["pet"],"category":"Animals & Nature","description":"dog face","unicode_version":"6.0"},{"emoji":"🐕","aliases":["dog2"],"tags":[],"category":"Animals & Nature","description":"dog","unicode_version":"6.0"},{"emoji":"🦮","aliases":["guide_dog"],"tags":[],"category":"Animals & Nature","description":"guide dog","unicode_version":"12.0"},{"emoji":"🐕‍🦺","aliases":["service_dog"],"tags":[],"category":"Animals & Nature","description":"service dog","unicode_version":"12.0"},{"emoji":"🐩","aliases":["poodle"],"tags":["dog"],"category":"Animals & Nature","description":"poodle","unicode_version":"6.0"},{"emoji":"🐺","aliases":["wolf"],"tags":[],"category":"Animals & Nature","description":"wolf","unicode_version":"6.0"},{"emoji":"🦊","aliases":["fox_face"],"tags":[],"category":"Animals & Nature","description":"fox","unicode_version":"9.0"},{"emoji":"🦝","aliases":["raccoon"],"tags":[],"category":"Animals & Nature","description":"raccoon","unicode_version":"11.0"},{"emoji":"🐱","aliases":["cat"],"tags":["pet"],"category":"Animals & Nature","description":"cat face","unicode_version":"6.0"},{"emoji":"🐈","aliases":["cat2"],"tags":[],"category":"Animals & Nature","description":"cat","unicode_version":"6.0"},{"emoji":"🐈‍⬛","aliases":["black_cat"],"tags":[],"category":"Animals & Nature","description":"black cat","unicode_version":"13.0"},{"emoji":"🦁","aliases":["lion"],"tags":[],"category":"Animals & Nature","description":"lion","unicode_version":"8.0"},{"emoji":"🐯","aliases":["tiger"],"tags":[],"category":"Animals & Nature","description":"tiger face","unicode_version":"6.0"},{"emoji":"🐅","aliases":["tiger2"],"tags":[],"category":"Animals & Nature","description":"tiger","unicode_version":"6.0"},{"emoji":"🐆","aliases":["leopard"],"tags":[],"category":"Animals & Nature","description":"leopard","unicode_version":"6.0"},{"emoji":"🐴","aliases":["horse"],"tags":[],"category":"Animals & Nature","description":"horse face","unicode_version":"6.0"},{"emoji":"🐎","aliases":["racehorse"],"tags":["speed"],"category":"Animals & Nature","description":"horse","unicode_version":"6.0"},{"emoji":"🦄","aliases":["unicorn"],"tags":[],"category":"Animals & Nature","description":"unicorn","unicode_version":"8.0"},{"emoji":"🦓","aliases":["zebra"],"tags":[],"category":"Animals & Nature","description":"zebra","unicode_version":"11.0"},{"emoji":"🦌","aliases":["deer"],"tags":[],"category":"Animals & Nature","description":"deer","unicode_version":"9.0"},{"emoji":"🦬","aliases":["bison"],"tags":[],"category":"Animals & Nature","description":"bison","unicode_version":"13.0"},{"emoji":"🐮","aliases":["cow"],"tags":[],"category":"Animals & Nature","description":"cow face","unicode_version":"6.0"},{"emoji":"🐂","aliases":["ox"],"tags":[],"category":"Animals & Nature","description":"ox","unicode_version":"6.0"},{"emoji":"🐃","aliases":["water_buffalo"],"tags":[],"category":"Animals & Nature","description":"water buffalo","unicode_version":"6.0"},{"emoji":"🐄","aliases":["cow2"],"tags":[],"category":"Animals & Nature","description":"cow","unicode_version":"6.0"},{"emoji":"🐷","aliases":["pig"],"tags":[],"category":"Animals & Nature","description":"pig face","unicode_version":"6.0"},{"emoji":"🐖","aliases":["pig2"],"tags":[],"category":"Animals & Nature","description":"pig","unicode_version":"6.0"},{"emoji":"🐗","aliases":["boar"],"tags":[],"category":"Animals & Nature","description":"boar","unicode_version":"6.0"},{"emoji":"🐽","aliases":["pig_nose"],"tags":[],"category":"Animals & Nature","description":"pig nose","unicode_version":"6.0"},{"emoji":"🐏","aliases":["ram"],"tags":[],"category":"Animals & Nature","description":"ram","unicode_version":"6.0"},{"emoji":"🐑","aliases":["sheep"],"tags":[],"category":"Animals & Nature","description":"ewe","unicode_version":"6.0"},{"emoji":"🐐","aliases":["goat"],"tags":[],"category":"Animals & Nature","description":"goat","unicode_version":"6.0"},{"emoji":"🐪","aliases":["dromedary_camel"],"tags":["desert"],"category":"Animals & Nature","description":"camel","unicode_version":"6.0"},{"emoji":"🐫","aliases":["camel"],"tags":[],"category":"Animals & Nature","description":"two-hump camel","unicode_version":"6.0"},{"emoji":"🦙","aliases":["llama"],"tags":[],"category":"Animals & Nature","description":"llama","unicode_version":"11.0"},{"emoji":"🦒","aliases":["giraffe"],"tags":[],"category":"Animals & Nature","description":"giraffe","unicode_version":"11.0"},{"emoji":"🐘","aliases":["elephant"],"tags":[],"category":"Animals & Nature","description":"elephant","unicode_version":"6.0"},{"emoji":"🦣","aliases":["mammoth"],"tags":[],"category":"Animals & Nature","description":"mammoth","unicode_version":"13.0"},{"emoji":"🦏","aliases":["rhinoceros"],"tags":[],"category":"Animals & Nature","description":"rhinoceros","unicode_version":"9.0"},{"emoji":"🦛","aliases":["hippopotamus"],"tags":[],"category":"Animals & Nature","description":"hippopotamus","unicode_version":"11.0"},{"emoji":"🐭","aliases":["mouse"],"tags":[],"category":"Animals & Nature","description":"mouse face","unicode_version":"6.0"},{"emoji":"🐁","aliases":["mouse2"],"tags":[],"category":"Animals & Nature","description":"mouse","unicode_version":"6.0"},{"emoji":"🐀","aliases":["rat"],"tags":[],"category":"Animals & Nature","description":"rat","unicode_version":"6.0"},{"emoji":"🐹","aliases":["hamster"],"tags":["pet"],"category":"Animals & Nature","description":"hamster","unicode_version":"6.0"},{"emoji":"🐰","aliases":["rabbit"],"tags":["bunny"],"category":"Animals & Nature","description":"rabbit face","unicode_version":"6.0"},{"emoji":"🐇","aliases":["rabbit2"],"tags":[],"category":"Animals & Nature","description":"rabbit","unicode_version":"6.0"},{"emoji":"🐿️","aliases":["chipmunk"],"tags":[],"category":"Animals & Nature","description":"chipmunk","unicode_version":"7.0"},{"emoji":"🦫","aliases":["beaver"],"tags":[],"category":"Animals & Nature","description":"beaver","unicode_version":"13.0"},{"emoji":"🦔","aliases":["hedgehog"],"tags":[],"category":"Animals & Nature","description":"hedgehog","unicode_version":"11.0"},{"emoji":"🦇","aliases":["bat"],"tags":[],"category":"Animals & Nature","description":"bat","unicode_version":"9.0"},{"emoji":"🐻","aliases":["bear"],"tags":[],"category":"Animals & Nature","description":"bear","unicode_version":"6.0"},{"emoji":"🐻‍❄️","aliases":["polar_bear"],"tags":[],"category":"Animals & Nature","description":"polar bear","unicode_version":"13.0"},{"emoji":"🐨","aliases":["koala"],"tags":[],"category":"Animals & Nature","description":"koala","unicode_version":"6.0"},{"emoji":"🐼","aliases":["panda_face"],"tags":[],"category":"Animals & Nature","description":"panda","unicode_version":"6.0"},{"emoji":"🦥","aliases":["sloth"],"tags":[],"category":"Animals & Nature","description":"sloth","unicode_version":"12.0"},{"emoji":"🦦","aliases":["otter"],"tags":[],"category":"Animals & Nature","description":"otter","unicode_version":"12.0"},{"emoji":"🦨","aliases":["skunk"],"tags":[],"category":"Animals & Nature","description":"skunk","unicode_version":"12.0"},{"emoji":"🦘","aliases":["kangaroo"],"tags":[],"category":"Animals & Nature","description":"kangaroo","unicode_version":"11.0"},{"emoji":"🦡","aliases":["badger"],"tags":[],"category":"Animals & Nature","description":"badger","unicode_version":"11.0"},{"emoji":"🐾","aliases":["feet","paw_prints"],"tags":[],"category":"Animals & Nature","description":"paw prints","unicode_version":"6.0"},{"emoji":"🦃","aliases":["turkey"],"tags":["thanksgiving"],"category":"Animals & Nature","description":"turkey","unicode_version":"8.0"},{"emoji":"🐔","aliases":["chicken"],"tags":[],"category":"Animals & Nature","description":"chicken","unicode_version":"6.0"},{"emoji":"🐓","aliases":["rooster"],"tags":[],"category":"Animals & Nature","description":"rooster","unicode_version":"6.0"},{"emoji":"🐣","aliases":["hatching_chick"],"tags":[],"category":"Animals & Nature","description":"hatching chick","unicode_version":"6.0"},{"emoji":"🐤","aliases":["baby_chick"],"tags":[],"category":"Animals & Nature","description":"baby chick","unicode_version":"6.0"},{"emoji":"🐥","aliases":["hatched_chick"],"tags":[],"category":"Animals & Nature","description":"front-facing baby chick","unicode_version":"6.0"},{"emoji":"🐦","aliases":["bird"],"tags":[],"category":"Animals & Nature","description":"bird","unicode_version":"6.0"},{"emoji":"🐧","aliases":["penguin"],"tags":[],"category":"Animals & Nature","description":"penguin","unicode_version":"6.0"},{"emoji":"🕊️","aliases":["dove"],"tags":["peace"],"category":"Animals & Nature","description":"dove","unicode_version":"7.0"},{"emoji":"🦅","aliases":["eagle"],"tags":[],"category":"Animals & Nature","description":"eagle","unicode_version":"9.0"},{"emoji":"🦆","aliases":["duck"],"tags":[],"category":"Animals & Nature","description":"duck","unicode_version":"9.0"},{"emoji":"🦢","aliases":["swan"],"tags":[],"category":"Animals & Nature","description":"swan","unicode_version":"11.0"},{"emoji":"🦉","aliases":["owl"],"tags":[],"category":"Animals & Nature","description":"owl","unicode_version":"9.0"},{"emoji":"🦤","aliases":["dodo"],"tags":[],"category":"Animals & Nature","description":"dodo","unicode_version":"13.0"},{"emoji":"🪶","aliases":["feather"],"tags":[],"category":"Animals & Nature","description":"feather","unicode_version":"13.0"},{"emoji":"🦩","aliases":["flamingo"],"tags":[],"category":"Animals & Nature","description":"flamingo","unicode_version":"12.0"},{"emoji":"🦚","aliases":["peacock"],"tags":[],"category":"Animals & Nature","description":"peacock","unicode_version":"11.0"},{"emoji":"🦜","aliases":["parrot"],"tags":[],"category":"Animals & Nature","description":"parrot","unicode_version":"11.0"},{"emoji":"🐸","aliases":["frog"],"tags":[],"category":"Animals & Nature","description":"frog","unicode_version":"6.0"},{"emoji":"🐊","aliases":["crocodile"],"tags":[],"category":"Animals & Nature","description":"crocodile","unicode_version":"6.0"},{"emoji":"🐢","aliases":["turtle"],"tags":["slow"],"category":"Animals & Nature","description":"turtle","unicode_version":"6.0"},{"emoji":"🦎","aliases":["lizard"],"tags":[],"category":"Animals & Nature","description":"lizard","unicode_version":"9.0"},{"emoji":"🐍","aliases":["snake"],"tags":[],"category":"Animals & Nature","description":"snake","unicode_version":"6.0"},{"emoji":"🐲","aliases":["dragon_face"],"tags":[],"category":"Animals & Nature","description":"dragon face","unicode_version":"6.0"},{"emoji":"🐉","aliases":["dragon"],"tags":[],"category":"Animals & Nature","description":"dragon","unicode_version":"6.0"},{"emoji":"🦕","aliases":["sauropod"],"tags":["dinosaur"],"category":"Animals & Nature","description":"sauropod","unicode_version":"11.0"},{"emoji":"🦖","aliases":["t-rex"],"tags":["dinosaur"],"category":"Animals & Nature","description":"T-Rex","unicode_version":"11.0"},{"emoji":"🐳","aliases":["whale"],"tags":["sea"],"category":"Animals & Nature","description":"spouting whale","unicode_version":"6.0"},{"emoji":"🐋","aliases":["whale2"],"tags":[],"category":"Animals & Nature","description":"whale","unicode_version":"6.0"},{"emoji":"🐬","aliases":["dolphin","flipper"],"tags":[],"category":"Animals & Nature","description":"dolphin","unicode_version":"6.0"},{"emoji":"🦭","aliases":["seal"],"tags":[],"category":"Animals & Nature","description":"seal","unicode_version":"13.0"},{"emoji":"🐟","aliases":["fish"],"tags":[],"category":"Animals & Nature","description":"fish","unicode_version":"6.0"},{"emoji":"🐠","aliases":["tropical_fish"],"tags":[],"category":"Animals & Nature","description":"tropical fish","unicode_version":"6.0"},{"emoji":"🐡","aliases":["blowfish"],"tags":[],"category":"Animals & Nature","description":"blowfish","unicode_version":"6.0"},{"emoji":"🦈","aliases":["shark"],"tags":[],"category":"Animals & Nature","description":"shark","unicode_version":"9.0"},{"emoji":"🐙","aliases":["octopus"],"tags":[],"category":"Animals & Nature","description":"octopus","unicode_version":"6.0"},{"emoji":"🐚","aliases":["shell"],"tags":["sea","beach"],"category":"Animals & Nature","description":"spiral shell","unicode_version":"6.0"},{"emoji":"🐌","aliases":["snail"],"tags":["slow"],"category":"Animals & Nature","description":"snail","unicode_version":"6.0"},{"emoji":"🦋","aliases":["butterfly"],"tags":[],"category":"Animals & Nature","description":"butterfly","unicode_version":"9.0"},{"emoji":"🐛","aliases":["bug"],"tags":[],"category":"Animals & Nature","description":"bug","unicode_version":"6.0"},{"emoji":"🐜","aliases":["ant"],"tags":[],"category":"Animals & Nature","description":"ant","unicode_version":"6.0"},{"emoji":"🐝","aliases":["bee","honeybee"],"tags":[],"category":"Animals & Nature","description":"honeybee","unicode_version":"6.0"},{"emoji":"🪲","aliases":["beetle"],"tags":[],"category":"Animals & Nature","description":"beetle","unicode_version":"13.0"},{"emoji":"🐞","aliases":["lady_beetle"],"tags":["bug"],"category":"Animals & Nature","description":"lady beetle","unicode_version":"6.0"},{"emoji":"🦗","aliases":["cricket"],"tags":[],"category":"Animals & Nature","description":"cricket","unicode_version":"11.0"},{"emoji":"🪳","aliases":["cockroach"],"tags":[],"category":"Animals & Nature","description":"cockroach","unicode_version":"13.0"},{"emoji":"🕷️","aliases":["spider"],"tags":[],"category":"Animals & Nature","description":"spider","unicode_version":"7.0"},{"emoji":"🕸️","aliases":["spider_web"],"tags":[],"category":"Animals & Nature","description":"spider web","unicode_version":"7.0"},{"emoji":"🦂","aliases":["scorpion"],"tags":[],"category":"Animals & Nature","description":"scorpion","unicode_version":"8.0"},{"emoji":"🦟","aliases":["mosquito"],"tags":[],"category":"Animals & Nature","description":"mosquito","unicode_version":"11.0"},{"emoji":"🪰","aliases":["fly"],"tags":[],"category":"Animals & Nature","description":"fly","unicode_version":"13.0"},{"emoji":"🪱","aliases":["worm"],"tags":[],"category":"Animals & Nature","description":"worm","unicode_version":"13.0"},{"emoji":"🦠","aliases":["microbe"],"tags":["germ"],"category":"Animals & Nature","description":"microbe","unicode_version":"11.0"},{"emoji":"💐","aliases":["bouquet"],"tags":["flowers"],"category":"Animals & Nature","description":"bouquet","unicode_version":"6.0"},{"emoji":"🌸","aliases":["cherry_blossom"],"tags":["flower","spring"],"category":"Animals & Nature","description":"cherry blossom","unicode_version":"6.0"},{"emoji":"💮","aliases":["white_flower"],"tags":[],"category":"Animals & Nature","description":"white flower","unicode_version":"6.0"},{"emoji":"🏵️","aliases":["rosette"],"tags":[],"category":"Animals & Nature","description":"rosette","unicode_version":"7.0"},{"emoji":"🌹","aliases":["rose"],"tags":["flower"],"category":"Animals & Nature","description":"rose","unicode_version":"6.0"},{"emoji":"🥀","aliases":["wilted_flower"],"tags":[],"category":"Animals & Nature","description":"wilted flower","unicode_version":"9.0"},{"emoji":"🌺","aliases":["hibiscus"],"tags":[],"category":"Animals & Nature","description":"hibiscus","unicode_version":"6.0"},{"emoji":"🌻","aliases":["sunflower"],"tags":[],"category":"Animals & Nature","description":"sunflower","unicode_version":"6.0"},{"emoji":"🌼","aliases":["blossom"],"tags":[],"category":"Animals & Nature","description":"blossom","unicode_version":"6.0"},{"emoji":"🌷","aliases":["tulip"],"tags":["flower"],"category":"Animals & Nature","description":"tulip","unicode_version":"6.0"},{"emoji":"🌱","aliases":["seedling"],"tags":["plant"],"category":"Animals & Nature","description":"seedling","unicode_version":"6.0"},{"emoji":"🪴","aliases":["potted_plant"],"tags":[],"category":"Animals & Nature","description":"potted plant","unicode_version":"13.0"},{"emoji":"🌲","aliases":["evergreen_tree"],"tags":["wood"],"category":"Animals & Nature","description":"evergreen tree","unicode_version":"6.0"},{"emoji":"🌳","aliases":["deciduous_tree"],"tags":["wood"],"category":"Animals & Nature","description":"deciduous tree","unicode_version":"6.0"},{"emoji":"🌴","aliases":["palm_tree"],"tags":[],"category":"Animals & Nature","description":"palm tree","unicode_version":"6.0"},{"emoji":"🌵","aliases":["cactus"],"tags":[],"category":"Animals & Nature","description":"cactus","unicode_version":"6.0"},{"emoji":"🌾","aliases":["ear_of_rice"],"tags":[],"category":"Animals & Nature","description":"sheaf of rice","unicode_version":"6.0"},{"emoji":"🌿","aliases":["herb"],"tags":[],"category":"Animals & Nature","description":"herb","unicode_version":"6.0"},{"emoji":"☘️","aliases":["shamrock"],"tags":[],"category":"Animals & Nature","description":"shamrock","unicode_version":"4.1"},{"emoji":"🍀","aliases":["four_leaf_clover"],"tags":["luck"],"category":"Animals & Nature","description":"four leaf clover","unicode_version":"6.0"},{"emoji":"🍁","aliases":["maple_leaf"],"tags":["canada"],"category":"Animals & Nature","description":"maple leaf","unicode_version":"6.0"},{"emoji":"🍂","aliases":["fallen_leaf"],"tags":["autumn"],"category":"Animals & Nature","description":"fallen leaf","unicode_version":"6.0"},{"emoji":"🍃","aliases":["leaves"],"tags":["leaf"],"category":"Animals & Nature","description":"leaf fluttering in wind","unicode_version":"6.0"},{"emoji":"🍇","aliases":["grapes"],"tags":[],"category":"Food & Drink","description":"grapes","unicode_version":"6.0"},{"emoji":"🍈","aliases":["melon"],"tags":[],"category":"Food & Drink","description":"melon","unicode_version":"6.0"},{"emoji":"🍉","aliases":["watermelon"],"tags":[],"category":"Food & Drink","description":"watermelon","unicode_version":"6.0"},{"emoji":"🍊","aliases":["tangerine","orange","mandarin"],"tags":[],"category":"Food & Drink","description":"tangerine","unicode_version":"6.0"},{"emoji":"🍋","aliases":["lemon"],"tags":[],"category":"Food & Drink","description":"lemon","unicode_version":"6.0"},{"emoji":"🍌","aliases":["banana"],"tags":["fruit"],"category":"Food & Drink","description":"banana","unicode_version":"6.0"},{"emoji":"🍍","aliases":["pineapple"],"tags":[],"category":"Food & Drink","description":"pineapple","unicode_version":"6.0"},{"emoji":"🥭","aliases":["mango"],"tags":[],"category":"Food & Drink","description":"mango","unicode_version":"11.0"},{"emoji":"🍎","aliases":["apple"],"tags":[],"category":"Food & Drink","description":"red apple","unicode_version":"6.0"},{"emoji":"🍏","aliases":["green_apple"],"tags":["fruit"],"category":"Food & Drink","description":"green apple","unicode_version":"6.0"},{"emoji":"🍐","aliases":["pear"],"tags":[],"category":"Food & Drink","description":"pear","unicode_version":"6.0"},{"emoji":"🍑","aliases":["peach"],"tags":[],"category":"Food & Drink","description":"peach","unicode_version":"6.0"},{"emoji":"🍒","aliases":["cherries"],"tags":["fruit"],"category":"Food & Drink","description":"cherries","unicode_version":"6.0"},{"emoji":"🍓","aliases":["strawberry"],"tags":["fruit"],"category":"Food & Drink","description":"strawberry","unicode_version":"6.0"},{"emoji":"🫐","aliases":["blueberries"],"tags":[],"category":"Food & Drink","description":"blueberries","unicode_version":"13.0"},{"emoji":"🥝","aliases":["kiwi_fruit"],"tags":[],"category":"Food & Drink","description":"kiwi fruit","unicode_version":"9.0"},{"emoji":"🍅","aliases":["tomato"],"tags":[],"category":"Food & Drink","description":"tomato","unicode_version":"6.0"},{"emoji":"🫒","aliases":["olive"],"tags":[],"category":"Food & Drink","description":"olive","unicode_version":"13.0"},{"emoji":"🥥","aliases":["coconut"],"tags":[],"category":"Food & Drink","description":"coconut","unicode_version":"11.0"},{"emoji":"🥑","aliases":["avocado"],"tags":[],"category":"Food & Drink","description":"avocado","unicode_version":"9.0"},{"emoji":"🍆","aliases":["eggplant"],"tags":["aubergine"],"category":"Food & Drink","description":"eggplant","unicode_version":"6.0"},{"emoji":"🥔","aliases":["potato"],"tags":[],"category":"Food & Drink","description":"potato","unicode_version":"9.0"},{"emoji":"🥕","aliases":["carrot"],"tags":[],"category":"Food & Drink","description":"carrot","unicode_version":"9.0"},{"emoji":"🌽","aliases":["corn"],"tags":[],"category":"Food & Drink","description":"ear of corn","unicode_version":"6.0"},{"emoji":"🌶️","aliases":["hot_pepper"],"tags":["spicy"],"category":"Food & Drink","description":"hot pepper","unicode_version":"7.0"},{"emoji":"🫑","aliases":["bell_pepper"],"tags":[],"category":"Food & Drink","description":"bell pepper","unicode_version":"13.0"},{"emoji":"🥒","aliases":["cucumber"],"tags":[],"category":"Food & Drink","description":"cucumber","unicode_version":"9.0"},{"emoji":"🥬","aliases":["leafy_green"],"tags":[],"category":"Food & Drink","description":"leafy green","unicode_version":"11.0"},{"emoji":"🥦","aliases":["broccoli"],"tags":[],"category":"Food & Drink","description":"broccoli","unicode_version":"11.0"},{"emoji":"🧄","aliases":["garlic"],"tags":[],"category":"Food & Drink","description":"garlic","unicode_version":"12.0"},{"emoji":"🧅","aliases":["onion"],"tags":[],"category":"Food & Drink","description":"onion","unicode_version":"12.0"},{"emoji":"🍄","aliases":["mushroom"],"tags":[],"category":"Food & Drink","description":"mushroom","unicode_version":"6.0"},{"emoji":"🥜","aliases":["peanuts"],"tags":[],"category":"Food & Drink","description":"peanuts","unicode_version":"9.0"},{"emoji":"🌰","aliases":["chestnut"],"tags":[],"category":"Food & Drink","description":"chestnut","unicode_version":"6.0"},{"emoji":"🍞","aliases":["bread"],"tags":["toast"],"category":"Food & Drink","description":"bread","unicode_version":"6.0"},{"emoji":"🥐","aliases":["croissant"],"tags":[],"category":"Food & Drink","description":"croissant","unicode_version":"9.0"},{"emoji":"🥖","aliases":["baguette_bread"],"tags":[],"category":"Food & Drink","description":"baguette bread","unicode_version":"9.0"},{"emoji":"🫓","aliases":["flatbread"],"tags":[],"category":"Food & Drink","description":"flatbread","unicode_version":"13.0"},{"emoji":"🥨","aliases":["pretzel"],"tags":[],"category":"Food & Drink","description":"pretzel","unicode_version":"11.0"},{"emoji":"🥯","aliases":["bagel"],"tags":[],"category":"Food & Drink","description":"bagel","unicode_version":"11.0"},{"emoji":"🥞","aliases":["pancakes"],"tags":[],"category":"Food & Drink","description":"pancakes","unicode_version":"9.0"},{"emoji":"🧇","aliases":["waffle"],"tags":[],"category":"Food & Drink","description":"waffle","unicode_version":"12.0"},{"emoji":"🧀","aliases":["cheese"],"tags":[],"category":"Food & Drink","description":"cheese wedge","unicode_version":"8.0"},{"emoji":"🍖","aliases":["meat_on_bone"],"tags":[],"category":"Food & Drink","description":"meat on bone","unicode_version":"6.0"},{"emoji":"🍗","aliases":["poultry_leg"],"tags":["meat","chicken"],"category":"Food & Drink","description":"poultry leg","unicode_version":"6.0"},{"emoji":"🥩","aliases":["cut_of_meat"],"tags":[],"category":"Food & Drink","description":"cut of meat","unicode_version":"11.0"},{"emoji":"🥓","aliases":["bacon"],"tags":[],"category":"Food & Drink","description":"bacon","unicode_version":"9.0"},{"emoji":"🍔","aliases":["hamburger"],"tags":["burger"],"category":"Food & Drink","description":"hamburger","unicode_version":"6.0"},{"emoji":"🍟","aliases":["fries"],"tags":[],"category":"Food & Drink","description":"french fries","unicode_version":"6.0"},{"emoji":"🍕","aliases":["pizza"],"tags":[],"category":"Food & Drink","description":"pizza","unicode_version":"6.0"},{"emoji":"🌭","aliases":["hotdog"],"tags":[],"category":"Food & Drink","description":"hot dog","unicode_version":"8.0"},{"emoji":"🥪","aliases":["sandwich"],"tags":[],"category":"Food & Drink","description":"sandwich","unicode_version":"11.0"},{"emoji":"🌮","aliases":["taco"],"tags":[],"category":"Food & Drink","description":"taco","unicode_version":"8.0"},{"emoji":"🌯","aliases":["burrito"],"tags":[],"category":"Food & Drink","description":"burrito","unicode_version":"8.0"},{"emoji":"🫔","aliases":["tamale"],"tags":[],"category":"Food & Drink","description":"tamale","unicode_version":"13.0"},{"emoji":"🥙","aliases":["stuffed_flatbread"],"tags":[],"category":"Food & Drink","description":"stuffed flatbread","unicode_version":"9.0"},{"emoji":"🧆","aliases":["falafel"],"tags":[],"category":"Food & Drink","description":"falafel","unicode_version":"12.0"},{"emoji":"🥚","aliases":["egg"],"tags":[],"category":"Food & Drink","description":"egg","unicode_version":"9.0"},{"emoji":"🍳","aliases":["fried_egg"],"tags":["breakfast"],"category":"Food & Drink","description":"cooking","unicode_version":"6.0"},{"emoji":"🥘","aliases":["shallow_pan_of_food"],"tags":["paella","curry"],"category":"Food & Drink","description":"shallow pan of food","unicode_version":""},{"emoji":"🍲","aliases":["stew"],"tags":[],"category":"Food & Drink","description":"pot of food","unicode_version":"6.0"},{"emoji":"🫕","aliases":["fondue"],"tags":[],"category":"Food & Drink","description":"fondue","unicode_version":"13.0"},{"emoji":"🥣","aliases":["bowl_with_spoon"],"tags":[],"category":"Food & Drink","description":"bowl with spoon","unicode_version":"11.0"},{"emoji":"🥗","aliases":["green_salad"],"tags":[],"category":"Food & Drink","description":"green salad","unicode_version":"9.0"},{"emoji":"🍿","aliases":["popcorn"],"tags":[],"category":"Food & Drink","description":"popcorn","unicode_version":"8.0"},{"emoji":"🧈","aliases":["butter"],"tags":[],"category":"Food & Drink","description":"butter","unicode_version":"12.0"},{"emoji":"🧂","aliases":["salt"],"tags":[],"category":"Food & Drink","description":"salt","unicode_version":"11.0"},{"emoji":"🥫","aliases":["canned_food"],"tags":[],"category":"Food & Drink","description":"canned food","unicode_version":"11.0"},{"emoji":"🍱","aliases":["bento"],"tags":[],"category":"Food & Drink","description":"bento box","unicode_version":"6.0"},{"emoji":"🍘","aliases":["rice_cracker"],"tags":[],"category":"Food & Drink","description":"rice cracker","unicode_version":"6.0"},{"emoji":"🍙","aliases":["rice_ball"],"tags":[],"category":"Food & Drink","description":"rice ball","unicode_version":"6.0"},{"emoji":"🍚","aliases":["rice"],"tags":[],"category":"Food & Drink","description":"cooked rice","unicode_version":"6.0"},{"emoji":"🍛","aliases":["curry"],"tags":[],"category":"Food & Drink","description":"curry rice","unicode_version":"6.0"},{"emoji":"🍜","aliases":["ramen"],"tags":["noodle"],"category":"Food & Drink","description":"steaming bowl","unicode_version":"6.0"},{"emoji":"🍝","aliases":["spaghetti"],"tags":["pasta"],"category":"Food & Drink","description":"spaghetti","unicode_version":"6.0"},{"emoji":"🍠","aliases":["sweet_potato"],"tags":[],"category":"Food & Drink","description":"roasted sweet potato","unicode_version":"6.0"},{"emoji":"🍢","aliases":["oden"],"tags":[],"category":"Food & Drink","description":"oden","unicode_version":"6.0"},{"emoji":"🍣","aliases":["sushi"],"tags":[],"category":"Food & Drink","description":"sushi","unicode_version":"6.0"},{"emoji":"🍤","aliases":["fried_shrimp"],"tags":["tempura"],"category":"Food & Drink","description":"fried shrimp","unicode_version":"6.0"},{"emoji":"🍥","aliases":["fish_cake"],"tags":[],"category":"Food & Drink","description":"fish cake with swirl","unicode_version":"6.0"},{"emoji":"🥮","aliases":["moon_cake"],"tags":[],"category":"Food & Drink","description":"moon cake","unicode_version":"11.0"},{"emoji":"🍡","aliases":["dango"],"tags":[],"category":"Food & Drink","description":"dango","unicode_version":"6.0"},{"emoji":"🥟","aliases":["dumpling"],"tags":[],"category":"Food & Drink","description":"dumpling","unicode_version":"11.0"},{"emoji":"🥠","aliases":["fortune_cookie"],"tags":[],"category":"Food & Drink","description":"fortune cookie","unicode_version":"11.0"},{"emoji":"🥡","aliases":["takeout_box"],"tags":[],"category":"Food & Drink","description":"takeout box","unicode_version":"11.0"},{"emoji":"🦀","aliases":["crab"],"tags":[],"category":"Food & Drink","description":"crab","unicode_version":"8.0"},{"emoji":"🦞","aliases":["lobster"],"tags":[],"category":"Food & Drink","description":"lobster","unicode_version":"11.0"},{"emoji":"🦐","aliases":["shrimp"],"tags":[],"category":"Food & Drink","description":"shrimp","unicode_version":"9.0"},{"emoji":"🦑","aliases":["squid"],"tags":[],"category":"Food & Drink","description":"squid","unicode_version":"9.0"},{"emoji":"🦪","aliases":["oyster"],"tags":[],"category":"Food & Drink","description":"oyster","unicode_version":"12.0"},{"emoji":"🍦","aliases":["icecream"],"tags":[],"category":"Food & Drink","description":"soft ice cream","unicode_version":"6.0"},{"emoji":"🍧","aliases":["shaved_ice"],"tags":[],"category":"Food & Drink","description":"shaved ice","unicode_version":"6.0"},{"emoji":"🍨","aliases":["ice_cream"],"tags":[],"category":"Food & Drink","description":"ice cream","unicode_version":"6.0"},{"emoji":"🍩","aliases":["doughnut"],"tags":[],"category":"Food & Drink","description":"doughnut","unicode_version":"6.0"},{"emoji":"🍪","aliases":["cookie"],"tags":[],"category":"Food & Drink","description":"cookie","unicode_version":"6.0"},{"emoji":"🎂","aliases":["birthday"],"tags":["party"],"category":"Food & Drink","description":"birthday cake","unicode_version":"6.0"},{"emoji":"🍰","aliases":["cake"],"tags":["dessert"],"category":"Food & Drink","description":"shortcake","unicode_version":"6.0"},{"emoji":"🧁","aliases":["cupcake"],"tags":[],"category":"Food & Drink","description":"cupcake","unicode_version":"11.0"},{"emoji":"🥧","aliases":["pie"],"tags":[],"category":"Food & Drink","description":"pie","unicode_version":"11.0"},{"emoji":"🍫","aliases":["chocolate_bar"],"tags":[],"category":"Food & Drink","description":"chocolate bar","unicode_version":"6.0"},{"emoji":"🍬","aliases":["candy"],"tags":["sweet"],"category":"Food & Drink","description":"candy","unicode_version":"6.0"},{"emoji":"🍭","aliases":["lollipop"],"tags":[],"category":"Food & Drink","description":"lollipop","unicode_version":"6.0"},{"emoji":"🍮","aliases":["custard"],"tags":[],"category":"Food & Drink","description":"custard","unicode_version":"6.0"},{"emoji":"🍯","aliases":["honey_pot"],"tags":[],"category":"Food & Drink","description":"honey pot","unicode_version":"6.0"},{"emoji":"🍼","aliases":["baby_bottle"],"tags":["milk"],"category":"Food & Drink","description":"baby bottle","unicode_version":"6.0"},{"emoji":"🥛","aliases":["milk_glass"],"tags":[],"category":"Food & Drink","description":"glass of milk","unicode_version":"9.0"},{"emoji":"☕","aliases":["coffee"],"tags":["cafe","espresso"],"category":"Food & Drink","description":"hot beverage","unicode_version":"4.0"},{"emoji":"🫖","aliases":["teapot"],"tags":[],"category":"Food & Drink","description":"teapot","unicode_version":"13.0"},{"emoji":"🍵","aliases":["tea"],"tags":["green","breakfast"],"category":"Food & Drink","description":"teacup without handle","unicode_version":"6.0"},{"emoji":"🍶","aliases":["sake"],"tags":[],"category":"Food & Drink","description":"sake","unicode_version":"6.0"},{"emoji":"🍾","aliases":["champagne"],"tags":["bottle","bubbly","celebration"],"category":"Food & Drink","description":"bottle with popping cork","unicode_version":"8.0"},{"emoji":"🍷","aliases":["wine_glass"],"tags":[],"category":"Food & Drink","description":"wine glass","unicode_version":"6.0"},{"emoji":"🍸","aliases":["cocktail"],"tags":["drink"],"category":"Food & Drink","description":"cocktail glass","unicode_version":"6.0"},{"emoji":"🍹","aliases":["tropical_drink"],"tags":["summer","vacation"],"category":"Food & Drink","description":"tropical drink","unicode_version":"6.0"},{"emoji":"🍺","aliases":["beer"],"tags":["drink"],"category":"Food & Drink","description":"beer mug","unicode_version":"6.0"},{"emoji":"🍻","aliases":["beers"],"tags":["drinks"],"category":"Food & Drink","description":"clinking beer mugs","unicode_version":"6.0"},{"emoji":"🥂","aliases":["clinking_glasses"],"tags":["cheers","toast"],"category":"Food & Drink","description":"clinking glasses","unicode_version":"9.0"},{"emoji":"🥃","aliases":["tumbler_glass"],"tags":["whisky"],"category":"Food & Drink","description":"tumbler glass","unicode_version":"9.0"},{"emoji":"🥤","aliases":["cup_with_straw"],"tags":[],"category":"Food & Drink","description":"cup with straw","unicode_version":"11.0"},{"emoji":"🧋","aliases":["bubble_tea"],"tags":[],"category":"Food & Drink","description":"bubble tea","unicode_version":"13.0"},{"emoji":"🧃","aliases":["beverage_box"],"tags":[],"category":"Food & Drink","description":"beverage box","unicode_version":"12.0"},{"emoji":"🧉","aliases":["mate"],"tags":[],"category":"Food & Drink","description":"mate","unicode_version":"12.0"},{"emoji":"🧊","aliases":["ice_cube"],"tags":[],"category":"Food & Drink","description":"ice","unicode_version":"12.0"},{"emoji":"🥢","aliases":["chopsticks"],"tags":[],"category":"Food & Drink","description":"chopsticks","unicode_version":"11.0"},{"emoji":"🍽️","aliases":["plate_with_cutlery"],"tags":["dining","dinner"],"category":"Food & Drink","description":"fork and knife with plate","unicode_version":"7.0"},{"emoji":"🍴","aliases":["fork_and_knife"],"tags":["cutlery"],"category":"Food & Drink","description":"fork and knife","unicode_version":"6.0"},{"emoji":"🥄","aliases":["spoon"],"tags":[],"category":"Food & Drink","description":"spoon","unicode_version":"9.0"},{"emoji":"🔪","aliases":["hocho","knife"],"tags":["cut","chop"],"category":"Food & Drink","description":"kitchen knife","unicode_version":"6.0"},{"emoji":"🏺","aliases":["amphora"],"tags":[],"category":"Food & Drink","description":"amphora","unicode_version":"8.0"},{"emoji":"🌍","aliases":["earth_africa"],"tags":["globe","world","international"],"category":"Travel & Places","description":"globe showing Europe-Africa","unicode_version":"6.0"},{"emoji":"🌎","aliases":["earth_americas"],"tags":["globe","world","international"],"category":"Travel & Places","description":"globe showing Americas","unicode_version":"6.0"},{"emoji":"🌏","aliases":["earth_asia"],"tags":["globe","world","international"],"category":"Travel & Places","description":"globe showing Asia-Australia","unicode_version":"6.0"},{"emoji":"🌐","aliases":["globe_with_meridians"],"tags":["world","global","international"],"category":"Travel & Places","description":"globe with meridians","unicode_version":"6.0"},{"emoji":"🗺️","aliases":["world_map"],"tags":["travel"],"category":"Travel & Places","description":"world map","unicode_version":"7.0"},{"emoji":"🗾","aliases":["japan"],"tags":[],"category":"Travel & Places","description":"map of Japan","unicode_version":"6.0"},{"emoji":"🧭","aliases":["compass"],"tags":[],"category":"Travel & Places","description":"compass","unicode_version":"11.0"},{"emoji":"🏔️","aliases":["mountain_snow"],"tags":[],"category":"Travel & Places","description":"snow-capped mountain","unicode_version":"7.0"},{"emoji":"⛰️","aliases":["mountain"],"tags":[],"category":"Travel & Places","description":"mountain","unicode_version":"5.2"},{"emoji":"🌋","aliases":["volcano"],"tags":[],"category":"Travel & Places","description":"volcano","unicode_version":"6.0"},{"emoji":"🗻","aliases":["mount_fuji"],"tags":[],"category":"Travel & Places","description":"mount fuji","unicode_version":"6.0"},{"emoji":"🏕️","aliases":["camping"],"tags":[],"category":"Travel & Places","description":"camping","unicode_version":"7.0"},{"emoji":"🏖️","aliases":["beach_umbrella"],"tags":[],"category":"Travel & Places","description":"beach with umbrella","unicode_version":"7.0"},{"emoji":"🏜️","aliases":["desert"],"tags":[],"category":"Travel & Places","description":"desert","unicode_version":"7.0"},{"emoji":"🏝️","aliases":["desert_island"],"tags":[],"category":"Travel & Places","description":"desert island","unicode_version":"7.0"},{"emoji":"🏞️","aliases":["national_park"],"tags":[],"category":"Travel & Places","description":"national park","unicode_version":"7.0"},{"emoji":"🏟️","aliases":["stadium"],"tags":[],"category":"Travel & Places","description":"stadium","unicode_version":"7.0"},{"emoji":"🏛️","aliases":["classical_building"],"tags":[],"category":"Travel & Places","description":"classical building","unicode_version":"7.0"},{"emoji":"🏗️","aliases":["building_construction"],"tags":[],"category":"Travel & Places","description":"building construction","unicode_version":"7.0"},{"emoji":"🧱","aliases":["bricks"],"tags":[],"category":"Travel & Places","description":"brick","unicode_version":"11.0"},{"emoji":"🪨","aliases":["rock"],"tags":[],"category":"Travel & Places","description":"rock","unicode_version":"13.0"},{"emoji":"🪵","aliases":["wood"],"tags":[],"category":"Travel & Places","description":"wood","unicode_version":"13.0"},{"emoji":"🛖","aliases":["hut"],"tags":[],"category":"Travel & Places","description":"hut","unicode_version":"13.0"},{"emoji":"🏘️","aliases":["houses"],"tags":[],"category":"Travel & Places","description":"houses","unicode_version":"7.0"},{"emoji":"🏚️","aliases":["derelict_house"],"tags":[],"category":"Travel & Places","description":"derelict house","unicode_version":"7.0"},{"emoji":"🏠","aliases":["house"],"tags":[],"category":"Travel & Places","description":"house","unicode_version":"6.0"},{"emoji":"🏡","aliases":["house_with_garden"],"tags":[],"category":"Travel & Places","description":"house with garden","unicode_version":"6.0"},{"emoji":"🏢","aliases":["office"],"tags":[],"category":"Travel & Places","description":"office building","unicode_version":"6.0"},{"emoji":"🏣","aliases":["post_office"],"tags":[],"category":"Travel & Places","description":"Japanese post office","unicode_version":"6.0"},{"emoji":"🏤","aliases":["european_post_office"],"tags":[],"category":"Travel & Places","description":"post office","unicode_version":"6.0"},{"emoji":"🏥","aliases":["hospital"],"tags":[],"category":"Travel & Places","description":"hospital","unicode_version":"6.0"},{"emoji":"🏦","aliases":["bank"],"tags":[],"category":"Travel & Places","description":"bank","unicode_version":"6.0"},{"emoji":"🏨","aliases":["hotel"],"tags":[],"category":"Travel & Places","description":"hotel","unicode_version":"6.0"},{"emoji":"🏩","aliases":["love_hotel"],"tags":[],"category":"Travel & Places","description":"love hotel","unicode_version":"6.0"},{"emoji":"🏪","aliases":["convenience_store"],"tags":[],"category":"Travel & Places","description":"convenience store","unicode_version":"6.0"},{"emoji":"🏫","aliases":["school"],"tags":[],"category":"Travel & Places","description":"school","unicode_version":"6.0"},{"emoji":"🏬","aliases":["department_store"],"tags":[],"category":"Travel & Places","description":"department store","unicode_version":"6.0"},{"emoji":"🏭","aliases":["factory"],"tags":[],"category":"Travel & Places","description":"factory","unicode_version":"6.0"},{"emoji":"🏯","aliases":["japanese_castle"],"tags":[],"category":"Travel & Places","description":"Japanese castle","unicode_version":"6.0"},{"emoji":"🏰","aliases":["european_castle"],"tags":[],"category":"Travel & Places","description":"castle","unicode_version":"6.0"},{"emoji":"💒","aliases":["wedding"],"tags":["marriage"],"category":"Travel & Places","description":"wedding","unicode_version":"6.0"},{"emoji":"🗼","aliases":["tokyo_tower"],"tags":[],"category":"Travel & Places","description":"Tokyo tower","unicode_version":"6.0"},{"emoji":"🗽","aliases":["statue_of_liberty"],"tags":[],"category":"Travel & Places","description":"Statue of Liberty","unicode_version":"6.0"},{"emoji":"⛪","aliases":["church"],"tags":[],"category":"Travel & Places","description":"church","unicode_version":"5.2"},{"emoji":"🕌","aliases":["mosque"],"tags":[],"category":"Travel & Places","description":"mosque","unicode_version":"8.0"},{"emoji":"🛕","aliases":["hindu_temple"],"tags":[],"category":"Travel & Places","description":"hindu temple","unicode_version":"12.0"},{"emoji":"🕍","aliases":["synagogue"],"tags":[],"category":"Travel & Places","description":"synagogue","unicode_version":"8.0"},{"emoji":"⛩️","aliases":["shinto_shrine"],"tags":[],"category":"Travel & Places","description":"shinto shrine","unicode_version":"5.2"},{"emoji":"🕋","aliases":["kaaba"],"tags":[],"category":"Travel & Places","description":"kaaba","unicode_version":"8.0"},{"emoji":"⛲","aliases":["fountain"],"tags":[],"category":"Travel & Places","description":"fountain","unicode_version":"5.2"},{"emoji":"⛺","aliases":["tent"],"tags":["camping"],"category":"Travel & Places","description":"tent","unicode_version":"5.2"},{"emoji":"🌁","aliases":["foggy"],"tags":["karl"],"category":"Travel & Places","description":"foggy","unicode_version":"6.0"},{"emoji":"🌃","aliases":["night_with_stars"],"tags":[],"category":"Travel & Places","description":"night with stars","unicode_version":"6.0"},{"emoji":"🏙️","aliases":["cityscape"],"tags":["skyline"],"category":"Travel & Places","description":"cityscape","unicode_version":"7.0"},{"emoji":"🌄","aliases":["sunrise_over_mountains"],"tags":[],"category":"Travel & Places","description":"sunrise over mountains","unicode_version":"6.0"},{"emoji":"🌅","aliases":["sunrise"],"tags":[],"category":"Travel & Places","description":"sunrise","unicode_version":"6.0"},{"emoji":"🌆","aliases":["city_sunset"],"tags":[],"category":"Travel & Places","description":"cityscape at dusk","unicode_version":"6.0"},{"emoji":"🌇","aliases":["city_sunrise"],"tags":[],"category":"Travel & Places","description":"sunset","unicode_version":"6.0"},{"emoji":"🌉","aliases":["bridge_at_night"],"tags":[],"category":"Travel & Places","description":"bridge at night","unicode_version":"6.0"},{"emoji":"♨️","aliases":["hotsprings"],"tags":[],"category":"Travel & Places","description":"hot springs","unicode_version":""},{"emoji":"🎠","aliases":["carousel_horse"],"tags":[],"category":"Travel & Places","description":"carousel horse","unicode_version":"6.0"},{"emoji":"🎡","aliases":["ferris_wheel"],"tags":[],"category":"Travel & Places","description":"ferris wheel","unicode_version":"6.0"},{"emoji":"🎢","aliases":["roller_coaster"],"tags":[],"category":"Travel & Places","description":"roller coaster","unicode_version":"6.0"},{"emoji":"💈","aliases":["barber"],"tags":[],"category":"Travel & Places","description":"barber pole","unicode_version":"6.0"},{"emoji":"🎪","aliases":["circus_tent"],"tags":[],"category":"Travel & Places","description":"circus tent","unicode_version":"6.0"},{"emoji":"🚂","aliases":["steam_locomotive"],"tags":["train"],"category":"Travel & Places","description":"locomotive","unicode_version":"6.0"},{"emoji":"🚃","aliases":["railway_car"],"tags":[],"category":"Travel & Places","description":"railway car","unicode_version":"6.0"},{"emoji":"🚄","aliases":["bullettrain_side"],"tags":["train"],"category":"Travel & Places","description":"high-speed train","unicode_version":"6.0"},{"emoji":"🚅","aliases":["bullettrain_front"],"tags":["train"],"category":"Travel & Places","description":"bullet train","unicode_version":"6.0"},{"emoji":"🚆","aliases":["train2"],"tags":[],"category":"Travel & Places","description":"train","unicode_version":"6.0"},{"emoji":"🚇","aliases":["metro"],"tags":[],"category":"Travel & Places","description":"metro","unicode_version":"6.0"},{"emoji":"🚈","aliases":["light_rail"],"tags":[],"category":"Travel & Places","description":"light rail","unicode_version":"6.0"},{"emoji":"🚉","aliases":["station"],"tags":[],"category":"Travel & Places","description":"station","unicode_version":"6.0"},{"emoji":"🚊","aliases":["tram"],"tags":[],"category":"Travel & Places","description":"tram","unicode_version":"6.0"},{"emoji":"🚝","aliases":["monorail"],"tags":[],"category":"Travel & Places","description":"monorail","unicode_version":"6.0"},{"emoji":"🚞","aliases":["mountain_railway"],"tags":[],"category":"Travel & Places","description":"mountain railway","unicode_version":"6.0"},{"emoji":"🚋","aliases":["train"],"tags":[],"category":"Travel & Places","description":"tram car","unicode_version":"6.0"},{"emoji":"🚌","aliases":["bus"],"tags":[],"category":"Travel & Places","description":"bus","unicode_version":"6.0"},{"emoji":"🚍","aliases":["oncoming_bus"],"tags":[],"category":"Travel & Places","description":"oncoming bus","unicode_version":"6.0"},{"emoji":"🚎","aliases":["trolleybus"],"tags":[],"category":"Travel & Places","description":"trolleybus","unicode_version":"6.0"},{"emoji":"🚐","aliases":["minibus"],"tags":[],"category":"Travel & Places","description":"minibus","unicode_version":"6.0"},{"emoji":"🚑","aliases":["ambulance"],"tags":[],"category":"Travel & Places","description":"ambulance","unicode_version":"6.0"},{"emoji":"🚒","aliases":["fire_engine"],"tags":[],"category":"Travel & Places","description":"fire engine","unicode_version":"6.0"},{"emoji":"🚓","aliases":["police_car"],"tags":[],"category":"Travel & Places","description":"police car","unicode_version":"6.0"},{"emoji":"🚔","aliases":["oncoming_police_car"],"tags":[],"category":"Travel & Places","description":"oncoming police car","unicode_version":"6.0"},{"emoji":"🚕","aliases":["taxi"],"tags":[],"category":"Travel & Places","description":"taxi","unicode_version":"6.0"},{"emoji":"🚖","aliases":["oncoming_taxi"],"tags":[],"category":"Travel & Places","description":"oncoming taxi","unicode_version":"6.0"},{"emoji":"🚗","aliases":["car","red_car"],"tags":[],"category":"Travel & Places","description":"automobile","unicode_version":"6.0"},{"emoji":"🚘","aliases":["oncoming_automobile"],"tags":[],"category":"Travel & Places","description":"oncoming automobile","unicode_version":"6.0"},{"emoji":"🚙","aliases":["blue_car"],"tags":[],"category":"Travel & Places","description":"sport utility vehicle","unicode_version":"6.0"},{"emoji":"🛻","aliases":["pickup_truck"],"tags":[],"category":"Travel & Places","description":"pickup truck","unicode_version":"13.0"},{"emoji":"🚚","aliases":["truck"],"tags":[],"category":"Travel & Places","description":"delivery truck","unicode_version":"6.0"},{"emoji":"🚛","aliases":["articulated_lorry"],"tags":[],"category":"Travel & Places","description":"articulated lorry","unicode_version":"6.0"},{"emoji":"🚜","aliases":["tractor"],"tags":[],"category":"Travel & Places","description":"tractor","unicode_version":"6.0"},{"emoji":"🏎️","aliases":["racing_car"],"tags":[],"category":"Travel & Places","description":"racing car","unicode_version":"7.0"},{"emoji":"🏍️","aliases":["motorcycle"],"tags":[],"category":"Travel & Places","description":"motorcycle","unicode_version":"7.0"},{"emoji":"🛵","aliases":["motor_scooter"],"tags":[],"category":"Travel & Places","description":"motor scooter","unicode_version":"9.0"},{"emoji":"🦽","aliases":["manual_wheelchair"],"tags":[],"category":"Travel & Places","description":"manual wheelchair","unicode_version":"12.0"},{"emoji":"🦼","aliases":["motorized_wheelchair"],"tags":[],"category":"Travel & Places","description":"motorized wheelchair","unicode_version":"12.0"},{"emoji":"🛺","aliases":["auto_rickshaw"],"tags":[],"category":"Travel & Places","description":"auto rickshaw","unicode_version":"12.0"},{"emoji":"🚲","aliases":["bike"],"tags":["bicycle"],"category":"Travel & Places","description":"bicycle","unicode_version":"6.0"},{"emoji":"🛴","aliases":["kick_scooter"],"tags":[],"category":"Travel & Places","description":"kick scooter","unicode_version":"9.0"},{"emoji":"🛹","aliases":["skateboard"],"tags":[],"category":"Travel & Places","description":"skateboard","unicode_version":"11.0"},{"emoji":"🛼","aliases":["roller_skate"],"tags":[],"category":"Travel & Places","description":"roller skate","unicode_version":"13.0"},{"emoji":"🚏","aliases":["busstop"],"tags":[],"category":"Travel & Places","description":"bus stop","unicode_version":"6.0"},{"emoji":"🛣️","aliases":["motorway"],"tags":[],"category":"Travel & Places","description":"motorway","unicode_version":"7.0"},{"emoji":"🛤️","aliases":["railway_track"],"tags":[],"category":"Travel & Places","description":"railway track","unicode_version":"7.0"},{"emoji":"🛢️","aliases":["oil_drum"],"tags":[],"category":"Travel & Places","description":"oil drum","unicode_version":"7.0"},{"emoji":"⛽","aliases":["fuelpump"],"tags":[],"category":"Travel & Places","description":"fuel pump","unicode_version":"5.2"},{"emoji":"🚨","aliases":["rotating_light"],"tags":["911","emergency"],"category":"Travel & Places","description":"police car light","unicode_version":"6.0"},{"emoji":"🚥","aliases":["traffic_light"],"tags":[],"category":"Travel & Places","description":"horizontal traffic light","unicode_version":"6.0"},{"emoji":"🚦","aliases":["vertical_traffic_light"],"tags":["semaphore"],"category":"Travel & Places","description":"vertical traffic light","unicode_version":"6.0"},{"emoji":"🛑","aliases":["stop_sign"],"tags":[],"category":"Travel & Places","description":"stop sign","unicode_version":"9.0"},{"emoji":"🚧","aliases":["construction"],"tags":["wip"],"category":"Travel & Places","description":"construction","unicode_version":"6.0"},{"emoji":"⚓","aliases":["anchor"],"tags":["ship"],"category":"Travel & Places","description":"anchor","unicode_version":"4.1"},{"emoji":"⛵","aliases":["boat","sailboat"],"tags":[],"category":"Travel & Places","description":"sailboat","unicode_version":"5.2"},{"emoji":"🛶","aliases":["canoe"],"tags":[],"category":"Travel & Places","description":"canoe","unicode_version":"9.0"},{"emoji":"🚤","aliases":["speedboat"],"tags":["ship"],"category":"Travel & Places","description":"speedboat","unicode_version":"6.0"},{"emoji":"🛳️","aliases":["passenger_ship"],"tags":["cruise"],"category":"Travel & Places","description":"passenger ship","unicode_version":"7.0"},{"emoji":"⛴️","aliases":["ferry"],"tags":[],"category":"Travel & Places","description":"ferry","unicode_version":"5.2"},{"emoji":"🛥️","aliases":["motor_boat"],"tags":[],"category":"Travel & Places","description":"motor boat","unicode_version":"7.0"},{"emoji":"🚢","aliases":["ship"],"tags":[],"category":"Travel & Places","description":"ship","unicode_version":"6.0"},{"emoji":"✈️","aliases":["airplane"],"tags":["flight"],"category":"Travel & Places","description":"airplane","unicode_version":""},{"emoji":"🛩️","aliases":["small_airplane"],"tags":["flight"],"category":"Travel & Places","description":"small airplane","unicode_version":"7.0"},{"emoji":"🛫","aliases":["flight_departure"],"tags":[],"category":"Travel & Places","description":"airplane departure","unicode_version":"7.0"},{"emoji":"🛬","aliases":["flight_arrival"],"tags":[],"category":"Travel & Places","description":"airplane arrival","unicode_version":"7.0"},{"emoji":"🪂","aliases":["parachute"],"tags":[],"category":"Travel & Places","description":"parachute","unicode_version":"12.0"},{"emoji":"💺","aliases":["seat"],"tags":[],"category":"Travel & Places","description":"seat","unicode_version":"6.0"},{"emoji":"🚁","aliases":["helicopter"],"tags":[],"category":"Travel & Places","description":"helicopter","unicode_version":"6.0"},{"emoji":"🚟","aliases":["suspension_railway"],"tags":[],"category":"Travel & Places","description":"suspension railway","unicode_version":"6.0"},{"emoji":"🚠","aliases":["mountain_cableway"],"tags":[],"category":"Travel & Places","description":"mountain cableway","unicode_version":"6.0"},{"emoji":"🚡","aliases":["aerial_tramway"],"tags":[],"category":"Travel & Places","description":"aerial tramway","unicode_version":"6.0"},{"emoji":"🛰️","aliases":["artificial_satellite"],"tags":["orbit","space"],"category":"Travel & Places","description":"satellite","unicode_version":"7.0"},{"emoji":"🚀","aliases":["rocket"],"tags":["ship","launch"],"category":"Travel & Places","description":"rocket","unicode_version":"6.0"},{"emoji":"🛸","aliases":["flying_saucer"],"tags":["ufo"],"category":"Travel & Places","description":"flying saucer","unicode_version":"11.0"},{"emoji":"🛎️","aliases":["bellhop_bell"],"tags":[],"category":"Travel & Places","description":"bellhop bell","unicode_version":"7.0"},{"emoji":"🧳","aliases":["luggage"],"tags":[],"category":"Travel & Places","description":"luggage","unicode_version":"11.0"},{"emoji":"⌛","aliases":["hourglass"],"tags":["time"],"category":"Travel & Places","description":"hourglass done","unicode_version":""},{"emoji":"⏳","aliases":["hourglass_flowing_sand"],"tags":["time"],"category":"Travel & Places","description":"hourglass not done","unicode_version":"6.0"},{"emoji":"⌚","aliases":["watch"],"tags":["time"],"category":"Travel & Places","description":"watch","unicode_version":""},{"emoji":"⏰","aliases":["alarm_clock"],"tags":["morning"],"category":"Travel & Places","description":"alarm clock","unicode_version":"6.0"},{"emoji":"⏱️","aliases":["stopwatch"],"tags":[],"category":"Travel & Places","description":"stopwatch","unicode_version":"6.0"},{"emoji":"⏲️","aliases":["timer_clock"],"tags":[],"category":"Travel & Places","description":"timer clock","unicode_version":"6.0"},{"emoji":"🕰️","aliases":["mantelpiece_clock"],"tags":[],"category":"Travel & Places","description":"mantelpiece clock","unicode_version":"7.0"},{"emoji":"🕛","aliases":["clock12"],"tags":[],"category":"Travel & Places","description":"twelve o’clock","unicode_version":"6.0"},{"emoji":"🕧","aliases":["clock1230"],"tags":[],"category":"Travel & Places","description":"twelve-thirty","unicode_version":"6.0"},{"emoji":"🕐","aliases":["clock1"],"tags":[],"category":"Travel & Places","description":"one o’clock","unicode_version":"6.0"},{"emoji":"🕜","aliases":["clock130"],"tags":[],"category":"Travel & Places","description":"one-thirty","unicode_version":"6.0"},{"emoji":"🕑","aliases":["clock2"],"tags":[],"category":"Travel & Places","description":"two o’clock","unicode_version":"6.0"},{"emoji":"🕝","aliases":["clock230"],"tags":[],"category":"Travel & Places","description":"two-thirty","unicode_version":"6.0"},{"emoji":"🕒","aliases":["clock3"],"tags":[],"category":"Travel & Places","description":"three o’clock","unicode_version":"6.0"},{"emoji":"🕞","aliases":["clock330"],"tags":[],"category":"Travel & Places","description":"three-thirty","unicode_version":"6.0"},{"emoji":"🕓","aliases":["clock4"],"tags":[],"category":"Travel & Places","description":"four o’clock","unicode_version":"6.0"},{"emoji":"🕟","aliases":["clock430"],"tags":[],"category":"Travel & Places","description":"four-thirty","unicode_version":"6.0"},{"emoji":"🕔","aliases":["clock5"],"tags":[],"category":"Travel & Places","description":"five o’clock","unicode_version":"6.0"},{"emoji":"🕠","aliases":["clock530"],"tags":[],"category":"Travel & Places","description":"five-thirty","unicode_version":"6.0"},{"emoji":"🕕","aliases":["clock6"],"tags":[],"category":"Travel & Places","description":"six o’clock","unicode_version":"6.0"},{"emoji":"🕡","aliases":["clock630"],"tags":[],"category":"Travel & Places","description":"six-thirty","unicode_version":"6.0"},{"emoji":"🕖","aliases":["clock7"],"tags":[],"category":"Travel & Places","description":"seven o’clock","unicode_version":"6.0"},{"emoji":"🕢","aliases":["clock730"],"tags":[],"category":"Travel & Places","description":"seven-thirty","unicode_version":"6.0"},{"emoji":"🕗","aliases":["clock8"],"tags":[],"category":"Travel & Places","description":"eight o’clock","unicode_version":"6.0"},{"emoji":"🕣","aliases":["clock830"],"tags":[],"category":"Travel & Places","description":"eight-thirty","unicode_version":"6.0"},{"emoji":"🕘","aliases":["clock9"],"tags":[],"category":"Travel & Places","description":"nine o’clock","unicode_version":"6.0"},{"emoji":"🕤","aliases":["clock930"],"tags":[],"category":"Travel & Places","description":"nine-thirty","unicode_version":"6.0"},{"emoji":"🕙","aliases":["clock10"],"tags":[],"category":"Travel & Places","description":"ten o’clock","unicode_version":"6.0"},{"emoji":"🕥","aliases":["clock1030"],"tags":[],"category":"Travel & Places","description":"ten-thirty","unicode_version":"6.0"},{"emoji":"🕚","aliases":["clock11"],"tags":[],"category":"Travel & Places","description":"eleven o’clock","unicode_version":"6.0"},{"emoji":"🕦","aliases":["clock1130"],"tags":[],"category":"Travel & Places","description":"eleven-thirty","unicode_version":"6.0"},{"emoji":"🌑","aliases":["new_moon"],"tags":[],"category":"Travel & Places","description":"new moon","unicode_version":"6.0"},{"emoji":"🌒","aliases":["waxing_crescent_moon"],"tags":[],"category":"Travel & Places","description":"waxing crescent moon","unicode_version":"6.0"},{"emoji":"🌓","aliases":["first_quarter_moon"],"tags":[],"category":"Travel & Places","description":"first quarter moon","unicode_version":"6.0"},{"emoji":"🌔","aliases":["moon","waxing_gibbous_moon"],"tags":[],"category":"Travel & Places","description":"waxing gibbous moon","unicode_version":"6.0"},{"emoji":"🌕","aliases":["full_moon"],"tags":[],"category":"Travel & Places","description":"full moon","unicode_version":"6.0"},{"emoji":"🌖","aliases":["waning_gibbous_moon"],"tags":[],"category":"Travel & Places","description":"waning gibbous moon","unicode_version":"6.0"},{"emoji":"🌗","aliases":["last_quarter_moon"],"tags":[],"category":"Travel & Places","description":"last quarter moon","unicode_version":"6.0"},{"emoji":"🌘","aliases":["waning_crescent_moon"],"tags":[],"category":"Travel & Places","description":"waning crescent moon","unicode_version":"6.0"},{"emoji":"🌙","aliases":["crescent_moon"],"tags":["night"],"category":"Travel & Places","description":"crescent moon","unicode_version":"6.0"},{"emoji":"🌚","aliases":["new_moon_with_face"],"tags":[],"category":"Travel & Places","description":"new moon face","unicode_version":"6.0"},{"emoji":"🌛","aliases":["first_quarter_moon_with_face"],"tags":[],"category":"Travel & Places","description":"first quarter moon face","unicode_version":"6.0"},{"emoji":"🌜","aliases":["last_quarter_moon_with_face"],"tags":[],"category":"Travel & Places","description":"last quarter moon face","unicode_version":"6.0"},{"emoji":"🌡️","aliases":["thermometer"],"tags":[],"category":"Travel & Places","description":"thermometer","unicode_version":"7.0"},{"emoji":"☀️","aliases":["sunny"],"tags":["weather"],"category":"Travel & Places","description":"sun","unicode_version":""},{"emoji":"🌝","aliases":["full_moon_with_face"],"tags":[],"category":"Travel & Places","description":"full moon face","unicode_version":"6.0"},{"emoji":"🌞","aliases":["sun_with_face"],"tags":["summer"],"category":"Travel & Places","description":"sun with face","unicode_version":"6.0"},{"emoji":"🪐","aliases":["ringed_planet"],"tags":[],"category":"Travel & Places","description":"ringed planet","unicode_version":"12.0"},{"emoji":"⭐","aliases":["star"],"tags":[],"category":"Travel & Places","description":"star","unicode_version":"5.1"},{"emoji":"🌟","aliases":["star2"],"tags":[],"category":"Travel & Places","description":"glowing star","unicode_version":"6.0"},{"emoji":"🌠","aliases":["stars"],"tags":[],"category":"Travel & Places","description":"shooting star","unicode_version":"6.0"},{"emoji":"🌌","aliases":["milky_way"],"tags":[],"category":"Travel & Places","description":"milky way","unicode_version":"6.0"},{"emoji":"☁️","aliases":["cloud"],"tags":[],"category":"Travel & Places","description":"cloud","unicode_version":""},{"emoji":"⛅","aliases":["partly_sunny"],"tags":["weather","cloud"],"category":"Travel & Places","description":"sun behind cloud","unicode_version":"5.2"},{"emoji":"⛈️","aliases":["cloud_with_lightning_and_rain"],"tags":[],"category":"Travel & Places","description":"cloud with lightning and rain","unicode_version":"5.2"},{"emoji":"🌤️","aliases":["sun_behind_small_cloud"],"tags":[],"category":"Travel & Places","description":"sun behind small cloud","unicode_version":"7.0"},{"emoji":"🌥️","aliases":["sun_behind_large_cloud"],"tags":[],"category":"Travel & Places","description":"sun behind large cloud","unicode_version":"7.0"},{"emoji":"🌦️","aliases":["sun_behind_rain_cloud"],"tags":[],"category":"Travel & Places","description":"sun behind rain cloud","unicode_version":"7.0"},{"emoji":"🌧️","aliases":["cloud_with_rain"],"tags":[],"category":"Travel & Places","description":"cloud with rain","unicode_version":"7.0"},{"emoji":"🌨️","aliases":["cloud_with_snow"],"tags":[],"category":"Travel & Places","description":"cloud with snow","unicode_version":"7.0"},{"emoji":"🌩️","aliases":["cloud_with_lightning"],"tags":[],"category":"Travel & Places","description":"cloud with lightning","unicode_version":"7.0"},{"emoji":"🌪️","aliases":["tornado"],"tags":[],"category":"Travel & Places","description":"tornado","unicode_version":"7.0"},{"emoji":"🌫️","aliases":["fog"],"tags":[],"category":"Travel & Places","description":"fog","unicode_version":"7.0"},{"emoji":"🌬️","aliases":["wind_face"],"tags":[],"category":"Travel & Places","description":"wind face","unicode_version":"7.0"},{"emoji":"🌀","aliases":["cyclone"],"tags":["swirl"],"category":"Travel & Places","description":"cyclone","unicode_version":"6.0"},{"emoji":"🌈","aliases":["rainbow"],"tags":[],"category":"Travel & Places","description":"rainbow","unicode_version":"6.0"},{"emoji":"🌂","aliases":["closed_umbrella"],"tags":["weather","rain"],"category":"Travel & Places","description":"closed umbrella","unicode_version":"6.0"},{"emoji":"☂️","aliases":["open_umbrella"],"tags":[],"category":"Travel & Places","description":"umbrella","unicode_version":""},{"emoji":"☔","aliases":["umbrella"],"tags":["rain","weather"],"category":"Travel & Places","description":"umbrella with rain drops","unicode_version":"4.0"},{"emoji":"⛱️","aliases":["parasol_on_ground"],"tags":["beach_umbrella"],"category":"Travel & Places","description":"umbrella on ground","unicode_version":"5.2"},{"emoji":"⚡","aliases":["zap"],"tags":["lightning","thunder"],"category":"Travel & Places","description":"high voltage","unicode_version":"4.0"},{"emoji":"❄️","aliases":["snowflake"],"tags":["winter","cold","weather"],"category":"Travel & Places","description":"snowflake","unicode_version":""},{"emoji":"☃️","aliases":["snowman_with_snow"],"tags":["winter","christmas"],"category":"Travel & Places","description":"snowman","unicode_version":""},{"emoji":"⛄","aliases":["snowman"],"tags":["winter"],"category":"Travel & Places","description":"snowman without snow","unicode_version":"5.2"},{"emoji":"☄️","aliases":["comet"],"tags":[],"category":"Travel & Places","description":"comet","unicode_version":""},{"emoji":"🔥","aliases":["fire"],"tags":["burn"],"category":"Travel & Places","description":"fire","unicode_version":"6.0"},{"emoji":"💧","aliases":["droplet"],"tags":["water"],"category":"Travel & Places","description":"droplet","unicode_version":"6.0"},{"emoji":"🌊","aliases":["ocean"],"tags":["sea"],"category":"Travel & Places","description":"water wave","unicode_version":"6.0"},{"emoji":"🎃","aliases":["jack_o_lantern"],"tags":["halloween"],"category":"Activities","description":"jack-o-lantern","unicode_version":"6.0"},{"emoji":"🎄","aliases":["christmas_tree"],"tags":[],"category":"Activities","description":"Christmas tree","unicode_version":"6.0"},{"emoji":"🎆","aliases":["fireworks"],"tags":["festival","celebration"],"category":"Activities","description":"fireworks","unicode_version":"6.0"},{"emoji":"🎇","aliases":["sparkler"],"tags":[],"category":"Activities","description":"sparkler","unicode_version":"6.0"},{"emoji":"🧨","aliases":["firecracker"],"tags":[],"category":"Activities","description":"firecracker","unicode_version":"11.0"},{"emoji":"✨","aliases":["sparkles"],"tags":["shiny"],"category":"Activities","description":"sparkles","unicode_version":"6.0"},{"emoji":"🎈","aliases":["balloon"],"tags":["party","birthday"],"category":"Activities","description":"balloon","unicode_version":"6.0"},{"emoji":"🎉","aliases":["tada"],"tags":["hooray","party"],"category":"Activities","description":"party popper","unicode_version":"6.0"},{"emoji":"🎊","aliases":["confetti_ball"],"tags":[],"category":"Activities","description":"confetti ball","unicode_version":"6.0"},{"emoji":"🎋","aliases":["tanabata_tree"],"tags":[],"category":"Activities","description":"tanabata tree","unicode_version":"6.0"},{"emoji":"🎍","aliases":["bamboo"],"tags":[],"category":"Activities","description":"pine decoration","unicode_version":"6.0"},{"emoji":"🎎","aliases":["dolls"],"tags":[],"category":"Activities","description":"Japanese dolls","unicode_version":"6.0"},{"emoji":"🎏","aliases":["flags"],"tags":[],"category":"Activities","description":"carp streamer","unicode_version":"6.0"},{"emoji":"🎐","aliases":["wind_chime"],"tags":[],"category":"Activities","description":"wind chime","unicode_version":"6.0"},{"emoji":"🎑","aliases":["rice_scene"],"tags":[],"category":"Activities","description":"moon viewing ceremony","unicode_version":"6.0"},{"emoji":"🧧","aliases":["red_envelope"],"tags":[],"category":"Activities","description":"red envelope","unicode_version":"11.0"},{"emoji":"🎀","aliases":["ribbon"],"tags":[],"category":"Activities","description":"ribbon","unicode_version":"6.0"},{"emoji":"🎁","aliases":["gift"],"tags":["present","birthday","christmas"],"category":"Activities","description":"wrapped gift","unicode_version":"6.0"},{"emoji":"🎗️","aliases":["reminder_ribbon"],"tags":[],"category":"Activities","description":"reminder ribbon","unicode_version":"7.0"},{"emoji":"🎟️","aliases":["tickets"],"tags":[],"category":"Activities","description":"admission tickets","unicode_version":"7.0"},{"emoji":"🎫","aliases":["ticket"],"tags":[],"category":"Activities","description":"ticket","unicode_version":"6.0"},{"emoji":"🎖️","aliases":["medal_military"],"tags":[],"category":"Activities","description":"military medal","unicode_version":"7.0"},{"emoji":"🏆","aliases":["trophy"],"tags":["award","contest","winner"],"category":"Activities","description":"trophy","unicode_version":"6.0"},{"emoji":"🏅","aliases":["medal_sports"],"tags":["gold","winner"],"category":"Activities","description":"sports medal","unicode_version":"7.0"},{"emoji":"🥇","aliases":["1st_place_medal"],"tags":["gold"],"category":"Activities","description":"1st place medal","unicode_version":"9.0"},{"emoji":"🥈","aliases":["2nd_place_medal"],"tags":["silver"],"category":"Activities","description":"2nd place medal","unicode_version":"9.0"},{"emoji":"🥉","aliases":["3rd_place_medal"],"tags":["bronze"],"category":"Activities","description":"3rd place medal","unicode_version":"9.0"},{"emoji":"⚽","aliases":["soccer"],"tags":["sports"],"category":"Activities","description":"soccer ball","unicode_version":"5.2"},{"emoji":"⚾","aliases":["baseball"],"tags":["sports"],"category":"Activities","description":"baseball","unicode_version":"5.2"},{"emoji":"🥎","aliases":["softball"],"tags":[],"category":"Activities","description":"softball","unicode_version":"11.0"},{"emoji":"🏀","aliases":["basketball"],"tags":["sports"],"category":"Activities","description":"basketball","unicode_version":"6.0"},{"emoji":"🏐","aliases":["volleyball"],"tags":[],"category":"Activities","description":"volleyball","unicode_version":"8.0"},{"emoji":"🏈","aliases":["football"],"tags":["sports"],"category":"Activities","description":"american football","unicode_version":"6.0"},{"emoji":"🏉","aliases":["rugby_football"],"tags":[],"category":"Activities","description":"rugby football","unicode_version":"6.0"},{"emoji":"🎾","aliases":["tennis"],"tags":["sports"],"category":"Activities","description":"tennis","unicode_version":"6.0"},{"emoji":"🥏","aliases":["flying_disc"],"tags":[],"category":"Activities","description":"flying disc","unicode_version":"11.0"},{"emoji":"🎳","aliases":["bowling"],"tags":[],"category":"Activities","description":"bowling","unicode_version":"6.0"},{"emoji":"🏏","aliases":["cricket_game"],"tags":[],"category":"Activities","description":"cricket game","unicode_version":"8.0"},{"emoji":"🏑","aliases":["field_hockey"],"tags":[],"category":"Activities","description":"field hockey","unicode_version":"8.0"},{"emoji":"🏒","aliases":["ice_hockey"],"tags":[],"category":"Activities","description":"ice hockey","unicode_version":"8.0"},{"emoji":"🥍","aliases":["lacrosse"],"tags":[],"category":"Activities","description":"lacrosse","unicode_version":"11.0"},{"emoji":"🏓","aliases":["ping_pong"],"tags":[],"category":"Activities","description":"ping pong","unicode_version":"8.0"},{"emoji":"🏸","aliases":["badminton"],"tags":[],"category":"Activities","description":"badminton","unicode_version":"8.0"},{"emoji":"🥊","aliases":["boxing_glove"],"tags":[],"category":"Activities","description":"boxing glove","unicode_version":"9.0"},{"emoji":"🥋","aliases":["martial_arts_uniform"],"tags":[],"category":"Activities","description":"martial arts uniform","unicode_version":"9.0"},{"emoji":"🥅","aliases":["goal_net"],"tags":[],"category":"Activities","description":"goal net","unicode_version":"9.0"},{"emoji":"⛳","aliases":["golf"],"tags":[],"category":"Activities","description":"flag in hole","unicode_version":"5.2"},{"emoji":"⛸️","aliases":["ice_skate"],"tags":["skating"],"category":"Activities","description":"ice skate","unicode_version":"5.2"},{"emoji":"🎣","aliases":["fishing_pole_and_fish"],"tags":[],"category":"Activities","description":"fishing pole","unicode_version":"6.0"},{"emoji":"🤿","aliases":["diving_mask"],"tags":[],"category":"Activities","description":"diving mask","unicode_version":"12.0"},{"emoji":"🎽","aliases":["running_shirt_with_sash"],"tags":["marathon"],"category":"Activities","description":"running shirt","unicode_version":"6.0"},{"emoji":"🎿","aliases":["ski"],"tags":[],"category":"Activities","description":"skis","unicode_version":"6.0"},{"emoji":"🛷","aliases":["sled"],"tags":[],"category":"Activities","description":"sled","unicode_version":"11.0"},{"emoji":"🥌","aliases":["curling_stone"],"tags":[],"category":"Activities","description":"curling stone","unicode_version":"11.0"},{"emoji":"🎯","aliases":["dart"],"tags":["target"],"category":"Activities","description":"bullseye","unicode_version":"6.0"},{"emoji":"🪀","aliases":["yo_yo"],"tags":[],"category":"Activities","description":"yo-yo","unicode_version":"12.0"},{"emoji":"🪁","aliases":["kite"],"tags":[],"category":"Activities","description":"kite","unicode_version":"12.0"},{"emoji":"🎱","aliases":["8ball"],"tags":["pool","billiards"],"category":"Activities","description":"pool 8 ball","unicode_version":"6.0"},{"emoji":"🔮","aliases":["crystal_ball"],"tags":["fortune"],"category":"Activities","description":"crystal ball","unicode_version":"6.0"},{"emoji":"🪄","aliases":["magic_wand"],"tags":[],"category":"Activities","description":"magic wand","unicode_version":"13.0"},{"emoji":"🧿","aliases":["nazar_amulet"],"tags":[],"category":"Activities","description":"nazar amulet","unicode_version":"11.0"},{"emoji":"🎮","aliases":["video_game"],"tags":["play","controller","console"],"category":"Activities","description":"video game","unicode_version":"6.0"},{"emoji":"🕹️","aliases":["joystick"],"tags":[],"category":"Activities","description":"joystick","unicode_version":"7.0"},{"emoji":"🎰","aliases":["slot_machine"],"tags":[],"category":"Activities","description":"slot machine","unicode_version":"6.0"},{"emoji":"🎲","aliases":["game_die"],"tags":["dice","gambling"],"category":"Activities","description":"game die","unicode_version":"6.0"},{"emoji":"🧩","aliases":["jigsaw"],"tags":[],"category":"Activities","description":"puzzle piece","unicode_version":"11.0"},{"emoji":"🧸","aliases":["teddy_bear"],"tags":[],"category":"Activities","description":"teddy bear","unicode_version":"11.0"},{"emoji":"🪅","aliases":["pinata"],"tags":[],"category":"Activities","description":"piñata","unicode_version":"13.0"},{"emoji":"🪆","aliases":["nesting_dolls"],"tags":[],"category":"Activities","description":"nesting dolls","unicode_version":"13.0"},{"emoji":"♠️","aliases":["spades"],"tags":[],"category":"Activities","description":"spade suit","unicode_version":""},{"emoji":"♥️","aliases":["hearts"],"tags":[],"category":"Activities","description":"heart suit","unicode_version":""},{"emoji":"♦️","aliases":["diamonds"],"tags":[],"category":"Activities","description":"diamond suit","unicode_version":""},{"emoji":"♣️","aliases":["clubs"],"tags":[],"category":"Activities","description":"club suit","unicode_version":""},{"emoji":"♟️","aliases":["chess_pawn"],"tags":[],"category":"Activities","description":"chess pawn","unicode_version":"11.0"},{"emoji":"🃏","aliases":["black_joker"],"tags":[],"category":"Activities","description":"joker","unicode_version":"6.0"},{"emoji":"🀄","aliases":["mahjong"],"tags":[],"category":"Activities","description":"mahjong red dragon","unicode_version":""},{"emoji":"🎴","aliases":["flower_playing_cards"],"tags":[],"category":"Activities","description":"flower playing cards","unicode_version":"6.0"},{"emoji":"🎭","aliases":["performing_arts"],"tags":["theater","drama"],"category":"Activities","description":"performing arts","unicode_version":"6.0"},{"emoji":"🖼️","aliases":["framed_picture"],"tags":[],"category":"Activities","description":"framed picture","unicode_version":"7.0"},{"emoji":"🎨","aliases":["art"],"tags":["design","paint"],"category":"Activities","description":"artist palette","unicode_version":"6.0"},{"emoji":"🧵","aliases":["thread"],"tags":[],"category":"Activities","description":"thread","unicode_version":"11.0"},{"emoji":"🪡","aliases":["sewing_needle"],"tags":[],"category":"Activities","description":"sewing needle","unicode_version":"13.0"},{"emoji":"🧶","aliases":["yarn"],"tags":[],"category":"Activities","description":"yarn","unicode_version":"11.0"},{"emoji":"🪢","aliases":["knot"],"tags":[],"category":"Activities","description":"knot","unicode_version":"13.0"},{"emoji":"👓","aliases":["eyeglasses"],"tags":["glasses"],"category":"Objects","description":"glasses","unicode_version":"6.0"},{"emoji":"🕶️","aliases":["dark_sunglasses"],"tags":[],"category":"Objects","description":"sunglasses","unicode_version":"7.0"},{"emoji":"🥽","aliases":["goggles"],"tags":[],"category":"Objects","description":"goggles","unicode_version":"11.0"},{"emoji":"🥼","aliases":["lab_coat"],"tags":[],"category":"Objects","description":"lab coat","unicode_version":"11.0"},{"emoji":"🦺","aliases":["safety_vest"],"tags":[],"category":"Objects","description":"safety vest","unicode_version":"12.0"},{"emoji":"👔","aliases":["necktie"],"tags":["shirt","formal"],"category":"Objects","description":"necktie","unicode_version":"6.0"},{"emoji":"👕","aliases":["shirt","tshirt"],"tags":[],"category":"Objects","description":"t-shirt","unicode_version":"6.0"},{"emoji":"👖","aliases":["jeans"],"tags":["pants"],"category":"Objects","description":"jeans","unicode_version":"6.0"},{"emoji":"🧣","aliases":["scarf"],"tags":[],"category":"Objects","description":"scarf","unicode_version":"11.0"},{"emoji":"🧤","aliases":["gloves"],"tags":[],"category":"Objects","description":"gloves","unicode_version":"11.0"},{"emoji":"🧥","aliases":["coat"],"tags":[],"category":"Objects","description":"coat","unicode_version":"11.0"},{"emoji":"🧦","aliases":["socks"],"tags":[],"category":"Objects","description":"socks","unicode_version":"11.0"},{"emoji":"👗","aliases":["dress"],"tags":[],"category":"Objects","description":"dress","unicode_version":"6.0"},{"emoji":"👘","aliases":["kimono"],"tags":[],"category":"Objects","description":"kimono","unicode_version":"6.0"},{"emoji":"🥻","aliases":["sari"],"tags":[],"category":"Objects","description":"sari","unicode_version":"12.0"},{"emoji":"🩱","aliases":["one_piece_swimsuit"],"tags":[],"category":"Objects","description":"one-piece swimsuit","unicode_version":"12.0"},{"emoji":"🩲","aliases":["swim_brief"],"tags":[],"category":"Objects","description":"briefs","unicode_version":"12.0"},{"emoji":"🩳","aliases":["shorts"],"tags":[],"category":"Objects","description":"shorts","unicode_version":"12.0"},{"emoji":"👙","aliases":["bikini"],"tags":["beach"],"category":"Objects","description":"bikini","unicode_version":"6.0"},{"emoji":"👚","aliases":["womans_clothes"],"tags":[],"category":"Objects","description":"woman’s clothes","unicode_version":"6.0"},{"emoji":"👛","aliases":["purse"],"tags":[],"category":"Objects","description":"purse","unicode_version":"6.0"},{"emoji":"👜","aliases":["handbag"],"tags":["bag"],"category":"Objects","description":"handbag","unicode_version":"6.0"},{"emoji":"👝","aliases":["pouch"],"tags":["bag"],"category":"Objects","description":"clutch bag","unicode_version":"6.0"},{"emoji":"🛍️","aliases":["shopping"],"tags":["bags"],"category":"Objects","description":"shopping bags","unicode_version":"7.0"},{"emoji":"🎒","aliases":["school_satchel"],"tags":[],"category":"Objects","description":"backpack","unicode_version":"6.0"},{"emoji":"🩴","aliases":["thong_sandal"],"tags":[],"category":"Objects","description":"thong sandal","unicode_version":"13.0"},{"emoji":"👞","aliases":["mans_shoe","shoe"],"tags":[],"category":"Objects","description":"man’s shoe","unicode_version":"6.0"},{"emoji":"👟","aliases":["athletic_shoe"],"tags":["sneaker","sport","running"],"category":"Objects","description":"running shoe","unicode_version":"6.0"},{"emoji":"🥾","aliases":["hiking_boot"],"tags":[],"category":"Objects","description":"hiking boot","unicode_version":"11.0"},{"emoji":"🥿","aliases":["flat_shoe"],"tags":[],"category":"Objects","description":"flat shoe","unicode_version":"11.0"},{"emoji":"👠","aliases":["high_heel"],"tags":["shoe"],"category":"Objects","description":"high-heeled shoe","unicode_version":"6.0"},{"emoji":"👡","aliases":["sandal"],"tags":["shoe"],"category":"Objects","description":"woman’s sandal","unicode_version":"6.0"},{"emoji":"🩰","aliases":["ballet_shoes"],"tags":[],"category":"Objects","description":"ballet shoes","unicode_version":"12.0"},{"emoji":"👢","aliases":["boot"],"tags":[],"category":"Objects","description":"woman’s boot","unicode_version":"6.0"},{"emoji":"👑","aliases":["crown"],"tags":["king","queen","royal"],"category":"Objects","description":"crown","unicode_version":"6.0"},{"emoji":"👒","aliases":["womans_hat"],"tags":[],"category":"Objects","description":"woman’s hat","unicode_version":"6.0"},{"emoji":"🎩","aliases":["tophat"],"tags":["hat","classy"],"category":"Objects","description":"top hat","unicode_version":"6.0"},{"emoji":"🎓","aliases":["mortar_board"],"tags":["education","college","university","graduation"],"category":"Objects","description":"graduation cap","unicode_version":"6.0"},{"emoji":"🧢","aliases":["billed_cap"],"tags":[],"category":"Objects","description":"billed cap","unicode_version":"11.0"},{"emoji":"🪖","aliases":["military_helmet"],"tags":[],"category":"Objects","description":"military helmet","unicode_version":"13.0"},{"emoji":"⛑️","aliases":["rescue_worker_helmet"],"tags":[],"category":"Objects","description":"rescue worker’s helmet","unicode_version":"5.2"},{"emoji":"📿","aliases":["prayer_beads"],"tags":[],"category":"Objects","description":"prayer beads","unicode_version":"8.0"},{"emoji":"💄","aliases":["lipstick"],"tags":["makeup"],"category":"Objects","description":"lipstick","unicode_version":"6.0"},{"emoji":"💍","aliases":["ring"],"tags":["wedding","marriage","engaged"],"category":"Objects","description":"ring","unicode_version":"6.0"},{"emoji":"💎","aliases":["gem"],"tags":["diamond"],"category":"Objects","description":"gem stone","unicode_version":"6.0"},{"emoji":"🔇","aliases":["mute"],"tags":["sound","volume"],"category":"Objects","description":"muted speaker","unicode_version":"6.0"},{"emoji":"🔈","aliases":["speaker"],"tags":[],"category":"Objects","description":"speaker low volume","unicode_version":"6.0"},{"emoji":"🔉","aliases":["sound"],"tags":["volume"],"category":"Objects","description":"speaker medium volume","unicode_version":"6.0"},{"emoji":"🔊","aliases":["loud_sound"],"tags":["volume"],"category":"Objects","description":"speaker high volume","unicode_version":"6.0"},{"emoji":"📢","aliases":["loudspeaker"],"tags":["announcement"],"category":"Objects","description":"loudspeaker","unicode_version":"6.0"},{"emoji":"📣","aliases":["mega"],"tags":[],"category":"Objects","description":"megaphone","unicode_version":"6.0"},{"emoji":"📯","aliases":["postal_horn"],"tags":[],"category":"Objects","description":"postal horn","unicode_version":"6.0"},{"emoji":"🔔","aliases":["bell"],"tags":["sound","notification"],"category":"Objects","description":"bell","unicode_version":"6.0"},{"emoji":"🔕","aliases":["no_bell"],"tags":["volume","off"],"category":"Objects","description":"bell with slash","unicode_version":"6.0"},{"emoji":"🎼","aliases":["musical_score"],"tags":[],"category":"Objects","description":"musical score","unicode_version":"6.0"},{"emoji":"🎵","aliases":["musical_note"],"tags":[],"category":"Objects","description":"musical note","unicode_version":"6.0"},{"emoji":"🎶","aliases":["notes"],"tags":["music"],"category":"Objects","description":"musical notes","unicode_version":"6.0"},{"emoji":"🎙️","aliases":["studio_microphone"],"tags":["podcast"],"category":"Objects","description":"studio microphone","unicode_version":"7.0"},{"emoji":"🎚️","aliases":["level_slider"],"tags":[],"category":"Objects","description":"level slider","unicode_version":"7.0"},{"emoji":"🎛️","aliases":["control_knobs"],"tags":[],"category":"Objects","description":"control knobs","unicode_version":"7.0"},{"emoji":"🎤","aliases":["microphone"],"tags":["sing"],"category":"Objects","description":"microphone","unicode_version":"6.0"},{"emoji":"🎧","aliases":["headphones"],"tags":["music","earphones"],"category":"Objects","description":"headphone","unicode_version":"6.0"},{"emoji":"📻","aliases":["radio"],"tags":["podcast"],"category":"Objects","description":"radio","unicode_version":"6.0"},{"emoji":"🎷","aliases":["saxophone"],"tags":[],"category":"Objects","description":"saxophone","unicode_version":"6.0"},{"emoji":"🪗","aliases":["accordion"],"tags":[],"category":"Objects","description":"accordion","unicode_version":"13.0"},{"emoji":"🎸","aliases":["guitar"],"tags":["rock"],"category":"Objects","description":"guitar","unicode_version":"6.0"},{"emoji":"🎹","aliases":["musical_keyboard"],"tags":["piano"],"category":"Objects","description":"musical keyboard","unicode_version":"6.0"},{"emoji":"🎺","aliases":["trumpet"],"tags":[],"category":"Objects","description":"trumpet","unicode_version":"6.0"},{"emoji":"🎻","aliases":["violin"],"tags":[],"category":"Objects","description":"violin","unicode_version":"6.0"},{"emoji":"🪕","aliases":["banjo"],"tags":[],"category":"Objects","description":"banjo","unicode_version":"12.0"},{"emoji":"🥁","aliases":["drum"],"tags":[],"category":"Objects","description":"drum","unicode_version":""},{"emoji":"🪘","aliases":["long_drum"],"tags":[],"category":"Objects","description":"long drum","unicode_version":"13.0"},{"emoji":"📱","aliases":["iphone"],"tags":["smartphone","mobile"],"category":"Objects","description":"mobile phone","unicode_version":"6.0"},{"emoji":"📲","aliases":["calling"],"tags":["call","incoming"],"category":"Objects","description":"mobile phone with arrow","unicode_version":"6.0"},{"emoji":"☎️","aliases":["phone","telephone"],"tags":[],"category":"Objects","description":"telephone","unicode_version":""},{"emoji":"📞","aliases":["telephone_receiver"],"tags":["phone","call"],"category":"Objects","description":"telephone receiver","unicode_version":"6.0"},{"emoji":"📟","aliases":["pager"],"tags":[],"category":"Objects","description":"pager","unicode_version":"6.0"},{"emoji":"📠","aliases":["fax"],"tags":[],"category":"Objects","description":"fax machine","unicode_version":"6.0"},{"emoji":"🔋","aliases":["battery"],"tags":["power"],"category":"Objects","description":"battery","unicode_version":"6.0"},{"emoji":"🔌","aliases":["electric_plug"],"tags":[],"category":"Objects","description":"electric plug","unicode_version":"6.0"},{"emoji":"💻","aliases":["computer"],"tags":["desktop","screen"],"category":"Objects","description":"laptop","unicode_version":"6.0"},{"emoji":"🖥️","aliases":["desktop_computer"],"tags":[],"category":"Objects","description":"desktop computer","unicode_version":"7.0"},{"emoji":"🖨️","aliases":["printer"],"tags":[],"category":"Objects","description":"printer","unicode_version":"7.0"},{"emoji":"⌨️","aliases":["keyboard"],"tags":[],"category":"Objects","description":"keyboard","unicode_version":""},{"emoji":"🖱️","aliases":["computer_mouse"],"tags":[],"category":"Objects","description":"computer mouse","unicode_version":"7.0"},{"emoji":"🖲️","aliases":["trackball"],"tags":[],"category":"Objects","description":"trackball","unicode_version":"7.0"},{"emoji":"💽","aliases":["minidisc"],"tags":[],"category":"Objects","description":"computer disk","unicode_version":"6.0"},{"emoji":"💾","aliases":["floppy_disk"],"tags":["save"],"category":"Objects","description":"floppy disk","unicode_version":"6.0"},{"emoji":"💿","aliases":["cd"],"tags":[],"category":"Objects","description":"optical disk","unicode_version":"6.0"},{"emoji":"📀","aliases":["dvd"],"tags":[],"category":"Objects","description":"dvd","unicode_version":"6.0"},{"emoji":"🧮","aliases":["abacus"],"tags":[],"category":"Objects","description":"abacus","unicode_version":"11.0"},{"emoji":"🎥","aliases":["movie_camera"],"tags":["film","video"],"category":"Objects","description":"movie camera","unicode_version":"6.0"},{"emoji":"🎞️","aliases":["film_strip"],"tags":[],"category":"Objects","description":"film frames","unicode_version":"7.0"},{"emoji":"📽️","aliases":["film_projector"],"tags":[],"category":"Objects","description":"film projector","unicode_version":"7.0"},{"emoji":"🎬","aliases":["clapper"],"tags":["film"],"category":"Objects","description":"clapper board","unicode_version":"6.0"},{"emoji":"📺","aliases":["tv"],"tags":[],"category":"Objects","description":"television","unicode_version":"6.0"},{"emoji":"📷","aliases":["camera"],"tags":["photo"],"category":"Objects","description":"camera","unicode_version":"6.0"},{"emoji":"📸","aliases":["camera_flash"],"tags":["photo"],"category":"Objects","description":"camera with flash","unicode_version":"7.0"},{"emoji":"📹","aliases":["video_camera"],"tags":[],"category":"Objects","description":"video camera","unicode_version":"6.0"},{"emoji":"📼","aliases":["vhs"],"tags":[],"category":"Objects","description":"videocassette","unicode_version":"6.0"},{"emoji":"🔍","aliases":["mag"],"tags":["search","zoom"],"category":"Objects","description":"magnifying glass tilted left","unicode_version":"6.0"},{"emoji":"🔎","aliases":["mag_right"],"tags":[],"category":"Objects","description":"magnifying glass tilted right","unicode_version":"6.0"},{"emoji":"🕯️","aliases":["candle"],"tags":[],"category":"Objects","description":"candle","unicode_version":"7.0"},{"emoji":"💡","aliases":["bulb"],"tags":["idea","light"],"category":"Objects","description":"light bulb","unicode_version":"6.0"},{"emoji":"🔦","aliases":["flashlight"],"tags":[],"category":"Objects","description":"flashlight","unicode_version":"6.0"},{"emoji":"🏮","aliases":["izakaya_lantern","lantern"],"tags":[],"category":"Objects","description":"red paper lantern","unicode_version":"6.0"},{"emoji":"🪔","aliases":["diya_lamp"],"tags":[],"category":"Objects","description":"diya lamp","unicode_version":"12.0"},{"emoji":"📔","aliases":["notebook_with_decorative_cover"],"tags":[],"category":"Objects","description":"notebook with decorative cover","unicode_version":"6.0"},{"emoji":"📕","aliases":["closed_book"],"tags":[],"category":"Objects","description":"closed book","unicode_version":"6.0"},{"emoji":"📖","aliases":["book","open_book"],"tags":[],"category":"Objects","description":"open book","unicode_version":"6.0"},{"emoji":"📗","aliases":["green_book"],"tags":[],"category":"Objects","description":"green book","unicode_version":"6.0"},{"emoji":"📘","aliases":["blue_book"],"tags":[],"category":"Objects","description":"blue book","unicode_version":"6.0"},{"emoji":"📙","aliases":["orange_book"],"tags":[],"category":"Objects","description":"orange book","unicode_version":"6.0"},{"emoji":"📚","aliases":["books"],"tags":["library"],"category":"Objects","description":"books","unicode_version":"6.0"},{"emoji":"📓","aliases":["notebook"],"tags":[],"category":"Objects","description":"notebook","unicode_version":"6.0"},{"emoji":"📒","aliases":["ledger"],"tags":[],"category":"Objects","description":"ledger","unicode_version":"6.0"},{"emoji":"📃","aliases":["page_with_curl"],"tags":[],"category":"Objects","description":"page with curl","unicode_version":"6.0"},{"emoji":"📜","aliases":["scroll"],"tags":["document"],"category":"Objects","description":"scroll","unicode_version":"6.0"},{"emoji":"📄","aliases":["page_facing_up"],"tags":["document"],"category":"Objects","description":"page facing up","unicode_version":"6.0"},{"emoji":"📰","aliases":["newspaper"],"tags":["press"],"category":"Objects","description":"newspaper","unicode_version":"6.0"},{"emoji":"🗞️","aliases":["newspaper_roll"],"tags":["press"],"category":"Objects","description":"rolled-up newspaper","unicode_version":"7.0"},{"emoji":"📑","aliases":["bookmark_tabs"],"tags":[],"category":"Objects","description":"bookmark tabs","unicode_version":"6.0"},{"emoji":"🔖","aliases":["bookmark"],"tags":[],"category":"Objects","description":"bookmark","unicode_version":"6.0"},{"emoji":"🏷️","aliases":["label"],"tags":["tag"],"category":"Objects","description":"label","unicode_version":"7.0"},{"emoji":"💰","aliases":["moneybag"],"tags":["dollar","cream"],"category":"Objects","description":"money bag","unicode_version":"6.0"},{"emoji":"🪙","aliases":["coin"],"tags":[],"category":"Objects","description":"coin","unicode_version":"13.0"},{"emoji":"💴","aliases":["yen"],"tags":[],"category":"Objects","description":"yen banknote","unicode_version":"6.0"},{"emoji":"💵","aliases":["dollar"],"tags":["money"],"category":"Objects","description":"dollar banknote","unicode_version":"6.0"},{"emoji":"💶","aliases":["euro"],"tags":[],"category":"Objects","description":"euro banknote","unicode_version":"6.0"},{"emoji":"💷","aliases":["pound"],"tags":[],"category":"Objects","description":"pound banknote","unicode_version":"6.0"},{"emoji":"💸","aliases":["money_with_wings"],"tags":["dollar"],"category":"Objects","description":"money with wings","unicode_version":"6.0"},{"emoji":"💳","aliases":["credit_card"],"tags":["subscription"],"category":"Objects","description":"credit card","unicode_version":"6.0"},{"emoji":"🧾","aliases":["receipt"],"tags":[],"category":"Objects","description":"receipt","unicode_version":"11.0"},{"emoji":"💹","aliases":["chart"],"tags":[],"category":"Objects","description":"chart increasing with yen","unicode_version":"6.0"},{"emoji":"✉️","aliases":["envelope"],"tags":["letter","email"],"category":"Objects","description":"envelope","unicode_version":""},{"emoji":"📧","aliases":["email","e-mail"],"tags":[],"category":"Objects","description":"e-mail","unicode_version":"6.0"},{"emoji":"📨","aliases":["incoming_envelope"],"tags":[],"category":"Objects","description":"incoming envelope","unicode_version":"6.0"},{"emoji":"📩","aliases":["envelope_with_arrow"],"tags":[],"category":"Objects","description":"envelope with arrow","unicode_version":"6.0"},{"emoji":"📤","aliases":["outbox_tray"],"tags":[],"category":"Objects","description":"outbox tray","unicode_version":"6.0"},{"emoji":"📥","aliases":["inbox_tray"],"tags":[],"category":"Objects","description":"inbox tray","unicode_version":"6.0"},{"emoji":"📦","aliases":["package"],"tags":["shipping"],"category":"Objects","description":"package","unicode_version":"6.0"},{"emoji":"📫","aliases":["mailbox"],"tags":[],"category":"Objects","description":"closed mailbox with raised flag","unicode_version":"6.0"},{"emoji":"📪","aliases":["mailbox_closed"],"tags":[],"category":"Objects","description":"closed mailbox with lowered flag","unicode_version":"6.0"},{"emoji":"📬","aliases":["mailbox_with_mail"],"tags":[],"category":"Objects","description":"open mailbox with raised flag","unicode_version":"6.0"},{"emoji":"📭","aliases":["mailbox_with_no_mail"],"tags":[],"category":"Objects","description":"open mailbox with lowered flag","unicode_version":"6.0"},{"emoji":"📮","aliases":["postbox"],"tags":[],"category":"Objects","description":"postbox","unicode_version":"6.0"},{"emoji":"🗳️","aliases":["ballot_box"],"tags":[],"category":"Objects","description":"ballot box with ballot","unicode_version":"7.0"},{"emoji":"✏️","aliases":["pencil2"],"tags":[],"category":"Objects","description":"pencil","unicode_version":""},{"emoji":"✒️","aliases":["black_nib"],"tags":[],"category":"Objects","description":"black nib","unicode_version":""},{"emoji":"🖋️","aliases":["fountain_pen"],"tags":[],"category":"Objects","description":"fountain pen","unicode_version":"7.0"},{"emoji":"🖊️","aliases":["pen"],"tags":[],"category":"Objects","description":"pen","unicode_version":"7.0"},{"emoji":"🖌️","aliases":["paintbrush"],"tags":[],"category":"Objects","description":"paintbrush","unicode_version":"7.0"},{"emoji":"🖍️","aliases":["crayon"],"tags":[],"category":"Objects","description":"crayon","unicode_version":"7.0"},{"emoji":"📝","aliases":["memo","pencil"],"tags":["document","note"],"category":"Objects","description":"memo","unicode_version":"6.0"},{"emoji":"💼","aliases":["briefcase"],"tags":["business"],"category":"Objects","description":"briefcase","unicode_version":"6.0"},{"emoji":"📁","aliases":["file_folder"],"tags":["directory"],"category":"Objects","description":"file folder","unicode_version":"6.0"},{"emoji":"📂","aliases":["open_file_folder"],"tags":[],"category":"Objects","description":"open file folder","unicode_version":"6.0"},{"emoji":"🗂️","aliases":["card_index_dividers"],"tags":[],"category":"Objects","description":"card index dividers","unicode_version":"7.0"},{"emoji":"📅","aliases":["date"],"tags":["calendar","schedule"],"category":"Objects","description":"calendar","unicode_version":"6.0"},{"emoji":"📆","aliases":["calendar"],"tags":["schedule"],"category":"Objects","description":"tear-off calendar","unicode_version":"6.0"},{"emoji":"🗒️","aliases":["spiral_notepad"],"tags":[],"category":"Objects","description":"spiral notepad","unicode_version":"7.0"},{"emoji":"🗓️","aliases":["spiral_calendar"],"tags":[],"category":"Objects","description":"spiral calendar","unicode_version":"7.0"},{"emoji":"📇","aliases":["card_index"],"tags":[],"category":"Objects","description":"card index","unicode_version":"6.0"},{"emoji":"📈","aliases":["chart_with_upwards_trend"],"tags":["graph","metrics"],"category":"Objects","description":"chart increasing","unicode_version":"6.0"},{"emoji":"📉","aliases":["chart_with_downwards_trend"],"tags":["graph","metrics"],"category":"Objects","description":"chart decreasing","unicode_version":"6.0"},{"emoji":"📊","aliases":["bar_chart"],"tags":["stats","metrics"],"category":"Objects","description":"bar chart","unicode_version":"6.0"},{"emoji":"📋","aliases":["clipboard"],"tags":[],"category":"Objects","description":"clipboard","unicode_version":"6.0"},{"emoji":"📌","aliases":["pushpin"],"tags":["location"],"category":"Objects","description":"pushpin","unicode_version":"6.0"},{"emoji":"📍","aliases":["round_pushpin"],"tags":["location"],"category":"Objects","description":"round pushpin","unicode_version":"6.0"},{"emoji":"📎","aliases":["paperclip"],"tags":[],"category":"Objects","description":"paperclip","unicode_version":"6.0"},{"emoji":"🖇️","aliases":["paperclips"],"tags":[],"category":"Objects","description":"linked paperclips","unicode_version":"7.0"},{"emoji":"📏","aliases":["straight_ruler"],"tags":[],"category":"Objects","description":"straight ruler","unicode_version":"6.0"},{"emoji":"📐","aliases":["triangular_ruler"],"tags":[],"category":"Objects","description":"triangular ruler","unicode_version":"6.0"},{"emoji":"✂️","aliases":["scissors"],"tags":["cut"],"category":"Objects","description":"scissors","unicode_version":""},{"emoji":"🗃️","aliases":["card_file_box"],"tags":[],"category":"Objects","description":"card file box","unicode_version":"7.0"},{"emoji":"🗄️","aliases":["file_cabinet"],"tags":[],"category":"Objects","description":"file cabinet","unicode_version":"7.0"},{"emoji":"🗑️","aliases":["wastebasket"],"tags":["trash"],"category":"Objects","description":"wastebasket","unicode_version":"7.0"},{"emoji":"🔒","aliases":["lock"],"tags":["security","private"],"category":"Objects","description":"locked","unicode_version":"6.0"},{"emoji":"🔓","aliases":["unlock"],"tags":["security"],"category":"Objects","description":"unlocked","unicode_version":"6.0"},{"emoji":"🔏","aliases":["lock_with_ink_pen"],"tags":[],"category":"Objects","description":"locked with pen","unicode_version":"6.0"},{"emoji":"🔐","aliases":["closed_lock_with_key"],"tags":["security"],"category":"Objects","description":"locked with key","unicode_version":"6.0"},{"emoji":"🔑","aliases":["key"],"tags":["lock","password"],"category":"Objects","description":"key","unicode_version":"6.0"},{"emoji":"🗝️","aliases":["old_key"],"tags":[],"category":"Objects","description":"old key","unicode_version":"7.0"},{"emoji":"🔨","aliases":["hammer"],"tags":["tool"],"category":"Objects","description":"hammer","unicode_version":"6.0"},{"emoji":"🪓","aliases":["axe"],"tags":[],"category":"Objects","description":"axe","unicode_version":"12.0"},{"emoji":"⛏️","aliases":["pick"],"tags":[],"category":"Objects","description":"pick","unicode_version":"5.2"},{"emoji":"⚒️","aliases":["hammer_and_pick"],"tags":[],"category":"Objects","description":"hammer and pick","unicode_version":"4.1"},{"emoji":"🛠️","aliases":["hammer_and_wrench"],"tags":[],"category":"Objects","description":"hammer and wrench","unicode_version":"7.0"},{"emoji":"🗡️","aliases":["dagger"],"tags":[],"category":"Objects","description":"dagger","unicode_version":"7.0"},{"emoji":"⚔️","aliases":["crossed_swords"],"tags":[],"category":"Objects","description":"crossed swords","unicode_version":"4.1"},{"emoji":"🔫","aliases":["gun"],"tags":["shoot","weapon"],"category":"Objects","description":"water pistol","unicode_version":"6.0"},{"emoji":"🪃","aliases":["boomerang"],"tags":[],"category":"Objects","description":"boomerang","unicode_version":"13.0"},{"emoji":"🏹","aliases":["bow_and_arrow"],"tags":["archery"],"category":"Objects","description":"bow and arrow","unicode_version":"8.0"},{"emoji":"🛡️","aliases":["shield"],"tags":[],"category":"Objects","description":"shield","unicode_version":"7.0"},{"emoji":"🪚","aliases":["carpentry_saw"],"tags":[],"category":"Objects","description":"carpentry saw","unicode_version":"13.0"},{"emoji":"🔧","aliases":["wrench"],"tags":["tool"],"category":"Objects","description":"wrench","unicode_version":"6.0"},{"emoji":"🪛","aliases":["screwdriver"],"tags":[],"category":"Objects","description":"screwdriver","unicode_version":"13.0"},{"emoji":"🔩","aliases":["nut_and_bolt"],"tags":[],"category":"Objects","description":"nut and bolt","unicode_version":"6.0"},{"emoji":"⚙️","aliases":["gear"],"tags":[],"category":"Objects","description":"gear","unicode_version":"4.1"},{"emoji":"🗜️","aliases":["clamp"],"tags":[],"category":"Objects","description":"clamp","unicode_version":"7.0"},{"emoji":"⚖️","aliases":["balance_scale"],"tags":[],"category":"Objects","description":"balance scale","unicode_version":"4.1"},{"emoji":"🦯","aliases":["probing_cane"],"tags":[],"category":"Objects","description":"white cane","unicode_version":"12.0"},{"emoji":"🔗","aliases":["link"],"tags":[],"category":"Objects","description":"link","unicode_version":"6.0"},{"emoji":"⛓️","aliases":["chains"],"tags":[],"category":"Objects","description":"chains","unicode_version":"5.2"},{"emoji":"🪝","aliases":["hook"],"tags":[],"category":"Objects","description":"hook","unicode_version":"13.0"},{"emoji":"🧰","aliases":["toolbox"],"tags":[],"category":"Objects","description":"toolbox","unicode_version":"11.0"},{"emoji":"🧲","aliases":["magnet"],"tags":[],"category":"Objects","description":"magnet","unicode_version":"11.0"},{"emoji":"🪜","aliases":["ladder"],"tags":[],"category":"Objects","description":"ladder","unicode_version":"13.0"},{"emoji":"⚗️","aliases":["alembic"],"tags":[],"category":"Objects","description":"alembic","unicode_version":"4.1"},{"emoji":"🧪","aliases":["test_tube"],"tags":[],"category":"Objects","description":"test tube","unicode_version":"11.0"},{"emoji":"🧫","aliases":["petri_dish"],"tags":[],"category":"Objects","description":"petri dish","unicode_version":"11.0"},{"emoji":"🧬","aliases":["dna"],"tags":[],"category":"Objects","description":"dna","unicode_version":"11.0"},{"emoji":"🔬","aliases":["microscope"],"tags":["science","laboratory","investigate"],"category":"Objects","description":"microscope","unicode_version":"6.0"},{"emoji":"🔭","aliases":["telescope"],"tags":[],"category":"Objects","description":"telescope","unicode_version":"6.0"},{"emoji":"📡","aliases":["satellite"],"tags":["signal"],"category":"Objects","description":"satellite antenna","unicode_version":"6.0"},{"emoji":"💉","aliases":["syringe"],"tags":["health","hospital","needle"],"category":"Objects","description":"syringe","unicode_version":"6.0"},{"emoji":"🩸","aliases":["drop_of_blood"],"tags":[],"category":"Objects","description":"drop of blood","unicode_version":"12.0"},{"emoji":"💊","aliases":["pill"],"tags":["health","medicine"],"category":"Objects","description":"pill","unicode_version":"6.0"},{"emoji":"🩹","aliases":["adhesive_bandage"],"tags":[],"category":"Objects","description":"adhesive bandage","unicode_version":"12.0"},{"emoji":"🩺","aliases":["stethoscope"],"tags":[],"category":"Objects","description":"stethoscope","unicode_version":"12.0"},{"emoji":"🚪","aliases":["door"],"tags":[],"category":"Objects","description":"door","unicode_version":"6.0"},{"emoji":"🛗","aliases":["elevator"],"tags":[],"category":"Objects","description":"elevator","unicode_version":"13.0"},{"emoji":"🪞","aliases":["mirror"],"tags":[],"category":"Objects","description":"mirror","unicode_version":"13.0"},{"emoji":"🪟","aliases":["window"],"tags":[],"category":"Objects","description":"window","unicode_version":"13.0"},{"emoji":"🛏️","aliases":["bed"],"tags":[],"category":"Objects","description":"bed","unicode_version":"7.0"},{"emoji":"🛋️","aliases":["couch_and_lamp"],"tags":[],"category":"Objects","description":"couch and lamp","unicode_version":"7.0"},{"emoji":"🪑","aliases":["chair"],"tags":[],"category":"Objects","description":"chair","unicode_version":"12.0"},{"emoji":"🚽","aliases":["toilet"],"tags":["wc"],"category":"Objects","description":"toilet","unicode_version":"6.0"},{"emoji":"🪠","aliases":["plunger"],"tags":[],"category":"Objects","description":"plunger","unicode_version":"13.0"},{"emoji":"🚿","aliases":["shower"],"tags":["bath"],"category":"Objects","description":"shower","unicode_version":"6.0"},{"emoji":"🛁","aliases":["bathtub"],"tags":[],"category":"Objects","description":"bathtub","unicode_version":"6.0"},{"emoji":"🪤","aliases":["mouse_trap"],"tags":[],"category":"Objects","description":"mouse trap","unicode_version":"13.0"},{"emoji":"🪒","aliases":["razor"],"tags":[],"category":"Objects","description":"razor","unicode_version":"12.0"},{"emoji":"🧴","aliases":["lotion_bottle"],"tags":[],"category":"Objects","description":"lotion bottle","unicode_version":"11.0"},{"emoji":"🧷","aliases":["safety_pin"],"tags":[],"category":"Objects","description":"safety pin","unicode_version":"11.0"},{"emoji":"🧹","aliases":["broom"],"tags":[],"category":"Objects","description":"broom","unicode_version":"11.0"},{"emoji":"🧺","aliases":["basket"],"tags":[],"category":"Objects","description":"basket","unicode_version":"11.0"},{"emoji":"🧻","aliases":["roll_of_paper"],"tags":["toilet"],"category":"Objects","description":"roll of paper","unicode_version":"11.0"},{"emoji":"🪣","aliases":["bucket"],"tags":[],"category":"Objects","description":"bucket","unicode_version":"13.0"},{"emoji":"🧼","aliases":["soap"],"tags":[],"category":"Objects","description":"soap","unicode_version":"11.0"},{"emoji":"🪥","aliases":["toothbrush"],"tags":[],"category":"Objects","description":"toothbrush","unicode_version":"13.0"},{"emoji":"🧽","aliases":["sponge"],"tags":[],"category":"Objects","description":"sponge","unicode_version":"11.0"},{"emoji":"🧯","aliases":["fire_extinguisher"],"tags":[],"category":"Objects","description":"fire extinguisher","unicode_version":"11.0"},{"emoji":"🛒","aliases":["shopping_cart"],"tags":[],"category":"Objects","description":"shopping cart","unicode_version":"9.0"},{"emoji":"🚬","aliases":["smoking"],"tags":["cigarette"],"category":"Objects","description":"cigarette","unicode_version":"6.0"},{"emoji":"⚰️","aliases":["coffin"],"tags":["funeral"],"category":"Objects","description":"coffin","unicode_version":"4.1"},{"emoji":"🪦","aliases":["headstone"],"tags":[],"category":"Objects","description":"headstone","unicode_version":"13.0"},{"emoji":"⚱️","aliases":["funeral_urn"],"tags":[],"category":"Objects","description":"funeral urn","unicode_version":"4.1"},{"emoji":"🗿","aliases":["moyai"],"tags":["stone"],"category":"Objects","description":"moai","unicode_version":"6.0"},{"emoji":"🪧","aliases":["placard"],"tags":[],"category":"Objects","description":"placard","unicode_version":"13.0"},{"emoji":"🏧","aliases":["atm"],"tags":[],"category":"Symbols","description":"ATM sign","unicode_version":"6.0"},{"emoji":"🚮","aliases":["put_litter_in_its_place"],"tags":[],"category":"Symbols","description":"litter in bin sign","unicode_version":"6.0"},{"emoji":"🚰","aliases":["potable_water"],"tags":[],"category":"Symbols","description":"potable water","unicode_version":"6.0"},{"emoji":"♿","aliases":["wheelchair"],"tags":["accessibility"],"category":"Symbols","description":"wheelchair symbol","unicode_version":"4.1"},{"emoji":"🚹","aliases":["mens"],"tags":[],"category":"Symbols","description":"men’s room","unicode_version":"6.0"},{"emoji":"🚺","aliases":["womens"],"tags":[],"category":"Symbols","description":"women’s room","unicode_version":"6.0"},{"emoji":"🚻","aliases":["restroom"],"tags":["toilet"],"category":"Symbols","description":"restroom","unicode_version":"6.0"},{"emoji":"🚼","aliases":["baby_symbol"],"tags":[],"category":"Symbols","description":"baby symbol","unicode_version":"6.0"},{"emoji":"🚾","aliases":["wc"],"tags":["toilet","restroom"],"category":"Symbols","description":"water closet","unicode_version":"6.0"},{"emoji":"🛂","aliases":["passport_control"],"tags":[],"category":"Symbols","description":"passport control","unicode_version":"6.0"},{"emoji":"🛃","aliases":["customs"],"tags":[],"category":"Symbols","description":"customs","unicode_version":"6.0"},{"emoji":"🛄","aliases":["baggage_claim"],"tags":["airport"],"category":"Symbols","description":"baggage claim","unicode_version":"6.0"},{"emoji":"🛅","aliases":["left_luggage"],"tags":[],"category":"Symbols","description":"left luggage","unicode_version":"6.0"},{"emoji":"⚠️","aliases":["warning"],"tags":["wip"],"category":"Symbols","description":"warning","unicode_version":"4.0"},{"emoji":"🚸","aliases":["children_crossing"],"tags":[],"category":"Symbols","description":"children crossing","unicode_version":"6.0"},{"emoji":"⛔","aliases":["no_entry"],"tags":["limit"],"category":"Symbols","description":"no entry","unicode_version":"5.2"},{"emoji":"🚫","aliases":["no_entry_sign"],"tags":["block","forbidden"],"category":"Symbols","description":"prohibited","unicode_version":"6.0"},{"emoji":"🚳","aliases":["no_bicycles"],"tags":[],"category":"Symbols","description":"no bicycles","unicode_version":"6.0"},{"emoji":"🚭","aliases":["no_smoking"],"tags":[],"category":"Symbols","description":"no smoking","unicode_version":"6.0"},{"emoji":"🚯","aliases":["do_not_litter"],"tags":[],"category":"Symbols","description":"no littering","unicode_version":"6.0"},{"emoji":"🚱","aliases":["non-potable_water"],"tags":[],"category":"Symbols","description":"non-potable water","unicode_version":"6.0"},{"emoji":"🚷","aliases":["no_pedestrians"],"tags":[],"category":"Symbols","description":"no pedestrians","unicode_version":"6.0"},{"emoji":"📵","aliases":["no_mobile_phones"],"tags":[],"category":"Symbols","description":"no mobile phones","unicode_version":"6.0"},{"emoji":"🔞","aliases":["underage"],"tags":[],"category":"Symbols","description":"no one under eighteen","unicode_version":"6.0"},{"emoji":"☢️","aliases":["radioactive"],"tags":[],"category":"Symbols","description":"radioactive","unicode_version":""},{"emoji":"☣️","aliases":["biohazard"],"tags":[],"category":"Symbols","description":"biohazard","unicode_version":""},{"emoji":"⬆️","aliases":["arrow_up"],"tags":[],"category":"Symbols","description":"up arrow","unicode_version":"4.0"},{"emoji":"↗️","aliases":["arrow_upper_right"],"tags":[],"category":"Symbols","description":"up-right arrow","unicode_version":""},{"emoji":"➡️","aliases":["arrow_right"],"tags":[],"category":"Symbols","description":"right arrow","unicode_version":""},{"emoji":"↘️","aliases":["arrow_lower_right"],"tags":[],"category":"Symbols","description":"down-right arrow","unicode_version":""},{"emoji":"⬇️","aliases":["arrow_down"],"tags":[],"category":"Symbols","description":"down arrow","unicode_version":"4.0"},{"emoji":"↙️","aliases":["arrow_lower_left"],"tags":[],"category":"Symbols","description":"down-left arrow","unicode_version":""},{"emoji":"⬅️","aliases":["arrow_left"],"tags":[],"category":"Symbols","description":"left arrow","unicode_version":"4.0"},{"emoji":"↖️","aliases":["arrow_upper_left"],"tags":[],"category":"Symbols","description":"up-left arrow","unicode_version":""},{"emoji":"↕️","aliases":["arrow_up_down"],"tags":[],"category":"Symbols","description":"up-down arrow","unicode_version":""},{"emoji":"↔️","aliases":["left_right_arrow"],"tags":[],"category":"Symbols","description":"left-right arrow","unicode_version":""},{"emoji":"↩️","aliases":["leftwards_arrow_with_hook"],"tags":["return"],"category":"Symbols","description":"right arrow curving left","unicode_version":""},{"emoji":"↪️","aliases":["arrow_right_hook"],"tags":[],"category":"Symbols","description":"left arrow curving right","unicode_version":""},{"emoji":"⤴️","aliases":["arrow_heading_up"],"tags":[],"category":"Symbols","description":"right arrow curving up","unicode_version":""},{"emoji":"⤵️","aliases":["arrow_heading_down"],"tags":[],"category":"Symbols","description":"right arrow curving down","unicode_version":""},{"emoji":"🔃","aliases":["arrows_clockwise"],"tags":[],"category":"Symbols","description":"clockwise vertical arrows","unicode_version":"6.0"},{"emoji":"🔄","aliases":["arrows_counterclockwise"],"tags":["sync"],"category":"Symbols","description":"counterclockwise arrows button","unicode_version":"6.0"},{"emoji":"🔙","aliases":["back"],"tags":[],"category":"Symbols","description":"BACK arrow","unicode_version":"6.0"},{"emoji":"🔚","aliases":["end"],"tags":[],"category":"Symbols","description":"END arrow","unicode_version":"6.0"},{"emoji":"🔛","aliases":["on"],"tags":[],"category":"Symbols","description":"ON! arrow","unicode_version":"6.0"},{"emoji":"🔜","aliases":["soon"],"tags":[],"category":"Symbols","description":"SOON arrow","unicode_version":"6.0"},{"emoji":"🔝","aliases":["top"],"tags":[],"category":"Symbols","description":"TOP arrow","unicode_version":"6.0"},{"emoji":"🛐","aliases":["place_of_worship"],"tags":[],"category":"Symbols","description":"place of worship","unicode_version":"8.0"},{"emoji":"⚛️","aliases":["atom_symbol"],"tags":[],"category":"Symbols","description":"atom symbol","unicode_version":"4.1"},{"emoji":"🕉️","aliases":["om"],"tags":[],"category":"Symbols","description":"om","unicode_version":"7.0"},{"emoji":"✡️","aliases":["star_of_david"],"tags":[],"category":"Symbols","description":"star of David","unicode_version":""},{"emoji":"☸️","aliases":["wheel_of_dharma"],"tags":[],"category":"Symbols","description":"wheel of dharma","unicode_version":""},{"emoji":"☯️","aliases":["yin_yang"],"tags":[],"category":"Symbols","description":"yin yang","unicode_version":""},{"emoji":"✝️","aliases":["latin_cross"],"tags":[],"category":"Symbols","description":"latin cross","unicode_version":""},{"emoji":"☦️","aliases":["orthodox_cross"],"tags":[],"category":"Symbols","description":"orthodox cross","unicode_version":""},{"emoji":"☪️","aliases":["star_and_crescent"],"tags":[],"category":"Symbols","description":"star and crescent","unicode_version":""},{"emoji":"☮️","aliases":["peace_symbol"],"tags":[],"category":"Symbols","description":"peace symbol","unicode_version":""},{"emoji":"🕎","aliases":["menorah"],"tags":[],"category":"Symbols","description":"menorah","unicode_version":"8.0"},{"emoji":"🔯","aliases":["six_pointed_star"],"tags":[],"category":"Symbols","description":"dotted six-pointed star","unicode_version":"6.0"},{"emoji":"♈","aliases":["aries"],"tags":[],"category":"Symbols","description":"Aries","unicode_version":""},{"emoji":"♉","aliases":["taurus"],"tags":[],"category":"Symbols","description":"Taurus","unicode_version":""},{"emoji":"♊","aliases":["gemini"],"tags":[],"category":"Symbols","description":"Gemini","unicode_version":""},{"emoji":"♋","aliases":["cancer"],"tags":[],"category":"Symbols","description":"Cancer","unicode_version":""},{"emoji":"♌","aliases":["leo"],"tags":[],"category":"Symbols","description":"Leo","unicode_version":""},{"emoji":"♍","aliases":["virgo"],"tags":[],"category":"Symbols","description":"Virgo","unicode_version":""},{"emoji":"♎","aliases":["libra"],"tags":[],"category":"Symbols","description":"Libra","unicode_version":""},{"emoji":"♏","aliases":["scorpius"],"tags":[],"category":"Symbols","description":"Scorpio","unicode_version":""},{"emoji":"♐","aliases":["sagittarius"],"tags":[],"category":"Symbols","description":"Sagittarius","unicode_version":""},{"emoji":"♑","aliases":["capricorn"],"tags":[],"category":"Symbols","description":"Capricorn","unicode_version":""},{"emoji":"♒","aliases":["aquarius"],"tags":[],"category":"Symbols","description":"Aquarius","unicode_version":""},{"emoji":"♓","aliases":["pisces"],"tags":[],"category":"Symbols","description":"Pisces","unicode_version":""},{"emoji":"⛎","aliases":["ophiuchus"],"tags":[],"category":"Symbols","description":"Ophiuchus","unicode_version":"6.0"},{"emoji":"🔀","aliases":["twisted_rightwards_arrows"],"tags":["shuffle"],"category":"Symbols","description":"shuffle tracks button","unicode_version":"6.0"},{"emoji":"🔁","aliases":["repeat"],"tags":["loop"],"category":"Symbols","description":"repeat button","unicode_version":"6.0"},{"emoji":"🔂","aliases":["repeat_one"],"tags":[],"category":"Symbols","description":"repeat single button","unicode_version":"6.0"},{"emoji":"▶️","aliases":["arrow_forward"],"tags":[],"category":"Symbols","description":"play button","unicode_version":""},{"emoji":"⏩","aliases":["fast_forward"],"tags":[],"category":"Symbols","description":"fast-forward button","unicode_version":"6.0"},{"emoji":"⏭️","aliases":["next_track_button"],"tags":[],"category":"Symbols","description":"next track button","unicode_version":"6.0"},{"emoji":"⏯️","aliases":["play_or_pause_button"],"tags":[],"category":"Symbols","description":"play or pause button","unicode_version":"6.0"},{"emoji":"◀️","aliases":["arrow_backward"],"tags":[],"category":"Symbols","description":"reverse button","unicode_version":""},{"emoji":"⏪","aliases":["rewind"],"tags":[],"category":"Symbols","description":"fast reverse button","unicode_version":"6.0"},{"emoji":"⏮️","aliases":["previous_track_button"],"tags":[],"category":"Symbols","description":"last track button","unicode_version":"6.0"},{"emoji":"🔼","aliases":["arrow_up_small"],"tags":[],"category":"Symbols","description":"upwards button","unicode_version":"6.0"},{"emoji":"⏫","aliases":["arrow_double_up"],"tags":[],"category":"Symbols","description":"fast up button","unicode_version":"6.0"},{"emoji":"🔽","aliases":["arrow_down_small"],"tags":[],"category":"Symbols","description":"downwards button","unicode_version":"6.0"},{"emoji":"⏬","aliases":["arrow_double_down"],"tags":[],"category":"Symbols","description":"fast down button","unicode_version":"6.0"},{"emoji":"⏸️","aliases":["pause_button"],"tags":[],"category":"Symbols","description":"pause button","unicode_version":"7.0"},{"emoji":"⏹️","aliases":["stop_button"],"tags":[],"category":"Symbols","description":"stop button","unicode_version":"7.0"},{"emoji":"⏺️","aliases":["record_button"],"tags":[],"category":"Symbols","description":"record button","unicode_version":"7.0"},{"emoji":"⏏️","aliases":["eject_button"],"tags":[],"category":"Symbols","description":"eject button","unicode_version":"11.0"},{"emoji":"🎦","aliases":["cinema"],"tags":["film","movie"],"category":"Symbols","description":"cinema","unicode_version":"6.0"},{"emoji":"🔅","aliases":["low_brightness"],"tags":[],"category":"Symbols","description":"dim button","unicode_version":"6.0"},{"emoji":"🔆","aliases":["high_brightness"],"tags":[],"category":"Symbols","description":"bright button","unicode_version":"6.0"},{"emoji":"📶","aliases":["signal_strength"],"tags":["wifi"],"category":"Symbols","description":"antenna bars","unicode_version":"6.0"},{"emoji":"📳","aliases":["vibration_mode"],"tags":[],"category":"Symbols","description":"vibration mode","unicode_version":"6.0"},{"emoji":"📴","aliases":["mobile_phone_off"],"tags":["mute","off"],"category":"Symbols","description":"mobile phone off","unicode_version":"6.0"},{"emoji":"♀️","aliases":["female_sign"],"tags":[],"category":"Symbols","description":"female sign","unicode_version":"11.0"},{"emoji":"♂️","aliases":["male_sign"],"tags":[],"category":"Symbols","description":"male sign","unicode_version":"11.0"},{"emoji":"⚧️","aliases":["transgender_symbol"],"tags":[],"category":"Symbols","description":"transgender symbol","unicode_version":"13.0"},{"emoji":"✖️","aliases":["heavy_multiplication_x"],"tags":[],"category":"Symbols","description":"multiply","unicode_version":""},{"emoji":"➕","aliases":["heavy_plus_sign"],"tags":[],"category":"Symbols","description":"plus","unicode_version":"6.0"},{"emoji":"➖","aliases":["heavy_minus_sign"],"tags":[],"category":"Symbols","description":"minus","unicode_version":"6.0"},{"emoji":"➗","aliases":["heavy_division_sign"],"tags":[],"category":"Symbols","description":"divide","unicode_version":"6.0"},{"emoji":"♾️","aliases":["infinity"],"tags":[],"category":"Symbols","description":"infinity","unicode_version":"11.0"},{"emoji":"‼️","aliases":["bangbang"],"tags":[],"category":"Symbols","description":"double exclamation mark","unicode_version":""},{"emoji":"⁉️","aliases":["interrobang"],"tags":[],"category":"Symbols","description":"exclamation question mark","unicode_version":"3.0"},{"emoji":"❓","aliases":["question"],"tags":["confused"],"category":"Symbols","description":"red question mark","unicode_version":"6.0"},{"emoji":"❔","aliases":["grey_question"],"tags":[],"category":"Symbols","description":"white question mark","unicode_version":"6.0"},{"emoji":"❕","aliases":["grey_exclamation"],"tags":[],"category":"Symbols","description":"white exclamation mark","unicode_version":"6.0"},{"emoji":"❗","aliases":["exclamation","heavy_exclamation_mark"],"tags":["bang"],"category":"Symbols","description":"red exclamation mark","unicode_version":"5.2"},{"emoji":"〰️","aliases":["wavy_dash"],"tags":[],"category":"Symbols","description":"wavy dash","unicode_version":""},{"emoji":"💱","aliases":["currency_exchange"],"tags":[],"category":"Symbols","description":"currency exchange","unicode_version":"6.0"},{"emoji":"💲","aliases":["heavy_dollar_sign"],"tags":[],"category":"Symbols","description":"heavy dollar sign","unicode_version":"6.0"},{"emoji":"⚕️","aliases":["medical_symbol"],"tags":[],"category":"Symbols","description":"medical symbol","unicode_version":"11.0"},{"emoji":"♻️","aliases":["recycle"],"tags":["environment","green"],"category":"Symbols","description":"recycling symbol","unicode_version":"3.2"},{"emoji":"⚜️","aliases":["fleur_de_lis"],"tags":[],"category":"Symbols","description":"fleur-de-lis","unicode_version":"4.1"},{"emoji":"🔱","aliases":["trident"],"tags":[],"category":"Symbols","description":"trident emblem","unicode_version":"6.0"},{"emoji":"📛","aliases":["name_badge"],"tags":[],"category":"Symbols","description":"name badge","unicode_version":"6.0"},{"emoji":"🔰","aliases":["beginner"],"tags":[],"category":"Symbols","description":"Japanese symbol for beginner","unicode_version":"6.0"},{"emoji":"⭕","aliases":["o"],"tags":[],"category":"Symbols","description":"hollow red circle","unicode_version":"5.2"},{"emoji":"✅","aliases":["white_check_mark"],"tags":[],"category":"Symbols","description":"check mark button","unicode_version":"6.0"},{"emoji":"☑️","aliases":["ballot_box_with_check"],"tags":[],"category":"Symbols","description":"check box with check","unicode_version":""},{"emoji":"✔️","aliases":["heavy_check_mark"],"tags":[],"category":"Symbols","description":"check mark","unicode_version":""},{"emoji":"❌","aliases":["x"],"tags":[],"category":"Symbols","description":"cross mark","unicode_version":"6.0"},{"emoji":"❎","aliases":["negative_squared_cross_mark"],"tags":[],"category":"Symbols","description":"cross mark button","unicode_version":"6.0"},{"emoji":"➰","aliases":["curly_loop"],"tags":[],"category":"Symbols","description":"curly loop","unicode_version":"6.0"},{"emoji":"➿","aliases":["loop"],"tags":[],"category":"Symbols","description":"double curly loop","unicode_version":"6.0"},{"emoji":"〽️","aliases":["part_alternation_mark"],"tags":[],"category":"Symbols","description":"part alternation mark","unicode_version":"3.2"},{"emoji":"✳️","aliases":["eight_spoked_asterisk"],"tags":[],"category":"Symbols","description":"eight-spoked asterisk","unicode_version":""},{"emoji":"✴️","aliases":["eight_pointed_black_star"],"tags":[],"category":"Symbols","description":"eight-pointed star","unicode_version":""},{"emoji":"❇️","aliases":["sparkle"],"tags":[],"category":"Symbols","description":"sparkle","unicode_version":""},{"emoji":"©️","aliases":["copyright"],"tags":[],"category":"Symbols","description":"copyright","unicode_version":""},{"emoji":"®️","aliases":["registered"],"tags":[],"category":"Symbols","description":"registered","unicode_version":""},{"emoji":"™️","aliases":["tm"],"tags":["trademark"],"category":"Symbols","description":"trade mark","unicode_version":""},{"emoji":"#️⃣","aliases":["hash"],"tags":["number"],"category":"Symbols","description":"keycap: #","unicode_version":""},{"emoji":"*️⃣","aliases":["asterisk"],"tags":[],"category":"Symbols","description":"keycap: *","unicode_version":""},{"emoji":"0️⃣","aliases":["zero"],"tags":[],"category":"Symbols","description":"keycap: 0","unicode_version":""},{"emoji":"1️⃣","aliases":["one"],"tags":[],"category":"Symbols","description":"keycap: 1","unicode_version":""},{"emoji":"2️⃣","aliases":["two"],"tags":[],"category":"Symbols","description":"keycap: 2","unicode_version":""},{"emoji":"3️⃣","aliases":["three"],"tags":[],"category":"Symbols","description":"keycap: 3","unicode_version":""},{"emoji":"4️⃣","aliases":["four"],"tags":[],"category":"Symbols","description":"keycap: 4","unicode_version":""},{"emoji":"5️⃣","aliases":["five"],"tags":[],"category":"Symbols","description":"keycap: 5","unicode_version":""},{"emoji":"6️⃣","aliases":["six"],"tags":[],"category":"Symbols","description":"keycap: 6","unicode_version":""},{"emoji":"7️⃣","aliases":["seven"],"tags":[],"category":"Symbols","description":"keycap: 7","unicode_version":""},{"emoji":"8️⃣","aliases":["eight"],"tags":[],"category":"Symbols","description":"keycap: 8","unicode_version":""},{"emoji":"9️⃣","aliases":["nine"],"tags":[],"category":"Symbols","description":"keycap: 9","unicode_version":""},{"emoji":"🔟","aliases":["keycap_ten"],"tags":[],"category":"Symbols","description":"keycap: 10","unicode_version":"6.0"},{"emoji":"🔠","aliases":["capital_abcd"],"tags":["letters"],"category":"Symbols","description":"input latin uppercase","unicode_version":"6.0"},{"emoji":"🔡","aliases":["abcd"],"tags":[],"category":"Symbols","description":"input latin lowercase","unicode_version":"6.0"},{"emoji":"🔢","aliases":["1234"],"tags":["numbers"],"category":"Symbols","description":"input numbers","unicode_version":"6.0"},{"emoji":"🔣","aliases":["symbols"],"tags":[],"category":"Symbols","description":"input symbols","unicode_version":"6.0"},{"emoji":"🔤","aliases":["abc"],"tags":["alphabet"],"category":"Symbols","description":"input latin letters","unicode_version":"6.0"},{"emoji":"🅰️","aliases":["a"],"tags":[],"category":"Symbols","description":"A button (blood type)","unicode_version":"6.0"},{"emoji":"🆎","aliases":["ab"],"tags":[],"category":"Symbols","description":"AB button (blood type)","unicode_version":"6.0"},{"emoji":"🅱️","aliases":["b"],"tags":[],"category":"Symbols","description":"B button (blood type)","unicode_version":"6.0"},{"emoji":"🆑","aliases":["cl"],"tags":[],"category":"Symbols","description":"CL button","unicode_version":"6.0"},{"emoji":"🆒","aliases":["cool"],"tags":[],"category":"Symbols","description":"COOL button","unicode_version":"6.0"},{"emoji":"🆓","aliases":["free"],"tags":[],"category":"Symbols","description":"FREE button","unicode_version":"6.0"},{"emoji":"ℹ️","aliases":["information_source"],"tags":[],"category":"Symbols","description":"information","unicode_version":"3.0"},{"emoji":"🆔","aliases":["id"],"tags":[],"category":"Symbols","description":"ID button","unicode_version":"6.0"},{"emoji":"Ⓜ️","aliases":["m"],"tags":[],"category":"Symbols","description":"circled M","unicode_version":""},{"emoji":"🆕","aliases":["new"],"tags":["fresh"],"category":"Symbols","description":"NEW button","unicode_version":"6.0"},{"emoji":"🆖","aliases":["ng"],"tags":[],"category":"Symbols","description":"NG button","unicode_version":"6.0"},{"emoji":"🅾️","aliases":["o2"],"tags":[],"category":"Symbols","description":"O button (blood type)","unicode_version":"6.0"},{"emoji":"🆗","aliases":["ok"],"tags":["yes"],"category":"Symbols","description":"OK button","unicode_version":"6.0"},{"emoji":"🅿️","aliases":["parking"],"tags":[],"category":"Symbols","description":"P button","unicode_version":"5.2"},{"emoji":"🆘","aliases":["sos"],"tags":["help","emergency"],"category":"Symbols","description":"SOS button","unicode_version":"6.0"},{"emoji":"🆙","aliases":["up"],"tags":[],"category":"Symbols","description":"UP! button","unicode_version":"6.0"},{"emoji":"🆚","aliases":["vs"],"tags":[],"category":"Symbols","description":"VS button","unicode_version":"6.0"},{"emoji":"🈁","aliases":["koko"],"tags":[],"category":"Symbols","description":"Japanese “here” button","unicode_version":"6.0"},{"emoji":"🈂️","aliases":["sa"],"tags":[],"category":"Symbols","description":"Japanese “service charge” button","unicode_version":"6.0"},{"emoji":"🈷️","aliases":["u6708"],"tags":[],"category":"Symbols","description":"Japanese “monthly amount” button","unicode_version":"6.0"},{"emoji":"🈶","aliases":["u6709"],"tags":[],"category":"Symbols","description":"Japanese “not free of charge” button","unicode_version":"6.0"},{"emoji":"🈯","aliases":["u6307"],"tags":[],"category":"Symbols","description":"Japanese “reserved” button","unicode_version":""},{"emoji":"🉐","aliases":["ideograph_advantage"],"tags":[],"category":"Symbols","description":"Japanese “bargain” button","unicode_version":"6.0"},{"emoji":"🈹","aliases":["u5272"],"tags":[],"category":"Symbols","description":"Japanese “discount” button","unicode_version":"6.0"},{"emoji":"🈚","aliases":["u7121"],"tags":[],"category":"Symbols","description":"Japanese “free of charge” button","unicode_version":""},{"emoji":"🈲","aliases":["u7981"],"tags":[],"category":"Symbols","description":"Japanese “prohibited” button","unicode_version":"6.0"},{"emoji":"🉑","aliases":["accept"],"tags":[],"category":"Symbols","description":"Japanese “acceptable” button","unicode_version":"6.0"},{"emoji":"🈸","aliases":["u7533"],"tags":[],"category":"Symbols","description":"Japanese “application” button","unicode_version":"6.0"},{"emoji":"🈴","aliases":["u5408"],"tags":[],"category":"Symbols","description":"Japanese “passing grade” button","unicode_version":"6.0"},{"emoji":"🈳","aliases":["u7a7a"],"tags":[],"category":"Symbols","description":"Japanese “vacancy” button","unicode_version":"6.0"},{"emoji":"㊗️","aliases":["congratulations"],"tags":[],"category":"Symbols","description":"Japanese “congratulations” button","unicode_version":""},{"emoji":"㊙️","aliases":["secret"],"tags":[],"category":"Symbols","description":"Japanese “secret” button","unicode_version":""},{"emoji":"🈺","aliases":["u55b6"],"tags":[],"category":"Symbols","description":"Japanese “open for business” button","unicode_version":"6.0"},{"emoji":"🈵","aliases":["u6e80"],"tags":[],"category":"Symbols","description":"Japanese “no vacancy” button","unicode_version":"6.0"},{"emoji":"🔴","aliases":["red_circle"],"tags":[],"category":"Symbols","description":"red circle","unicode_version":"6.0"},{"emoji":"🟠","aliases":["orange_circle"],"tags":[],"category":"Symbols","description":"orange circle","unicode_version":"12.0"},{"emoji":"🟡","aliases":["yellow_circle"],"tags":[],"category":"Symbols","description":"yellow circle","unicode_version":"12.0"},{"emoji":"🟢","aliases":["green_circle"],"tags":[],"category":"Symbols","description":"green circle","unicode_version":"12.0"},{"emoji":"🔵","aliases":["large_blue_circle"],"tags":[],"category":"Symbols","description":"blue circle","unicode_version":"6.0"},{"emoji":"🟣","aliases":["purple_circle"],"tags":[],"category":"Symbols","description":"purple circle","unicode_version":"12.0"},{"emoji":"🟤","aliases":["brown_circle"],"tags":[],"category":"Symbols","description":"brown circle","unicode_version":"12.0"},{"emoji":"⚫","aliases":["black_circle"],"tags":[],"category":"Symbols","description":"black circle","unicode_version":"4.1"},{"emoji":"⚪","aliases":["white_circle"],"tags":[],"category":"Symbols","description":"white circle","unicode_version":"4.1"},{"emoji":"🟥","aliases":["red_square"],"tags":[],"category":"Symbols","description":"red square","unicode_version":"12.0"},{"emoji":"🟧","aliases":["orange_square"],"tags":[],"category":"Symbols","description":"orange square","unicode_version":"12.0"},{"emoji":"🟨","aliases":["yellow_square"],"tags":[],"category":"Symbols","description":"yellow square","unicode_version":"12.0"},{"emoji":"🟩","aliases":["green_square"],"tags":[],"category":"Symbols","description":"green square","unicode_version":"12.0"},{"emoji":"🟦","aliases":["blue_square"],"tags":[],"category":"Symbols","description":"blue square","unicode_version":"12.0"},{"emoji":"🟪","aliases":["purple_square"],"tags":[],"category":"Symbols","description":"purple square","unicode_version":"12.0"},{"emoji":"🟫","aliases":["brown_square"],"tags":[],"category":"Symbols","description":"brown square","unicode_version":"12.0"},{"emoji":"⬛","aliases":["black_large_square"],"tags":[],"category":"Symbols","description":"black large square","unicode_version":"5.1"},{"emoji":"⬜","aliases":["white_large_square"],"tags":[],"category":"Symbols","description":"white large square","unicode_version":"5.1"},{"emoji":"◼️","aliases":["black_medium_square"],"tags":[],"category":"Symbols","description":"black medium square","unicode_version":"3.2"},{"emoji":"◻️","aliases":["white_medium_square"],"tags":[],"category":"Symbols","description":"white medium square","unicode_version":"3.2"},{"emoji":"◾","aliases":["black_medium_small_square"],"tags":[],"category":"Symbols","description":"black medium-small square","unicode_version":"3.2"},{"emoji":"◽","aliases":["white_medium_small_square"],"tags":[],"category":"Symbols","description":"white medium-small square","unicode_version":"3.2"},{"emoji":"▪️","aliases":["black_small_square"],"tags":[],"category":"Symbols","description":"black small square","unicode_version":""},{"emoji":"▫️","aliases":["white_small_square"],"tags":[],"category":"Symbols","description":"white small square","unicode_version":""},{"emoji":"🔶","aliases":["large_orange_diamond"],"tags":[],"category":"Symbols","description":"large orange diamond","unicode_version":"6.0"},{"emoji":"🔷","aliases":["large_blue_diamond"],"tags":[],"category":"Symbols","description":"large blue diamond","unicode_version":"6.0"},{"emoji":"🔸","aliases":["small_orange_diamond"],"tags":[],"category":"Symbols","description":"small orange diamond","unicode_version":"6.0"},{"emoji":"🔹","aliases":["small_blue_diamond"],"tags":[],"category":"Symbols","description":"small blue diamond","unicode_version":"6.0"},{"emoji":"🔺","aliases":["small_red_triangle"],"tags":[],"category":"Symbols","description":"red triangle pointed up","unicode_version":"6.0"},{"emoji":"🔻","aliases":["small_red_triangle_down"],"tags":[],"category":"Symbols","description":"red triangle pointed down","unicode_version":"6.0"},{"emoji":"💠","aliases":["diamond_shape_with_a_dot_inside"],"tags":[],"category":"Symbols","description":"diamond with a dot","unicode_version":"6.0"},{"emoji":"🔘","aliases":["radio_button"],"tags":[],"category":"Symbols","description":"radio button","unicode_version":"6.0"},{"emoji":"🔳","aliases":["white_square_button"],"tags":[],"category":"Symbols","description":"white square button","unicode_version":"6.0"},{"emoji":"🔲","aliases":["black_square_button"],"tags":[],"category":"Symbols","description":"black square button","unicode_version":"6.0"},{"emoji":"🏁","aliases":["checkered_flag"],"tags":["milestone","finish"],"category":"Flags","description":"chequered flag","unicode_version":"6.0"},{"emoji":"🚩","aliases":["triangular_flag_on_post"],"tags":[],"category":"Flags","description":"triangular flag","unicode_version":"6.0"},{"emoji":"🎌","aliases":["crossed_flags"],"tags":[],"category":"Flags","description":"crossed flags","unicode_version":"6.0"},{"emoji":"🏴","aliases":["black_flag"],"tags":[],"category":"Flags","description":"black flag","unicode_version":"7.0"},{"emoji":"🏳️","aliases":["white_flag"],"tags":[],"category":"Flags","description":"white flag","unicode_version":"7.0"},{"emoji":"🏳️‍🌈","aliases":["rainbow_flag"],"tags":["pride"],"category":"Flags","description":"rainbow flag","unicode_version":"6.0"},{"emoji":"🏳️‍⚧️","aliases":["transgender_flag"],"tags":[],"category":"Flags","description":"transgender flag","unicode_version":"13.0"},{"emoji":"🏴‍☠️","aliases":["pirate_flag"],"tags":[],"category":"Flags","description":"pirate flag","unicode_version":"11.0"},{"emoji":"🇦🇨","aliases":["ascension_island"],"tags":[],"category":"Flags","description":"flag: Ascension Island","unicode_version":"11.0"},{"emoji":"🇦🇩","aliases":["andorra"],"tags":[],"category":"Flags","description":"flag: Andorra","unicode_version":"6.0"},{"emoji":"🇦🇪","aliases":["united_arab_emirates"],"tags":[],"category":"Flags","description":"flag: United Arab Emirates","unicode_version":"6.0"},{"emoji":"🇦🇫","aliases":["afghanistan"],"tags":[],"category":"Flags","description":"flag: Afghanistan","unicode_version":"6.0"},{"emoji":"🇦🇬","aliases":["antigua_barbuda"],"tags":[],"category":"Flags","description":"flag: Antigua & Barbuda","unicode_version":"6.0"},{"emoji":"🇦🇮","aliases":["anguilla"],"tags":[],"category":"Flags","description":"flag: Anguilla","unicode_version":"6.0"},{"emoji":"🇦🇱","aliases":["albania"],"tags":[],"category":"Flags","description":"flag: Albania","unicode_version":"6.0"},{"emoji":"🇦🇲","aliases":["armenia"],"tags":[],"category":"Flags","description":"flag: Armenia","unicode_version":"6.0"},{"emoji":"🇦🇴","aliases":["angola"],"tags":[],"category":"Flags","description":"flag: Angola","unicode_version":"6.0"},{"emoji":"🇦🇶","aliases":["antarctica"],"tags":[],"category":"Flags","description":"flag: Antarctica","unicode_version":"6.0"},{"emoji":"🇦🇷","aliases":["argentina"],"tags":[],"category":"Flags","description":"flag: Argentina","unicode_version":"6.0"},{"emoji":"🇦🇸","aliases":["american_samoa"],"tags":[],"category":"Flags","description":"flag: American Samoa","unicode_version":"6.0"},{"emoji":"🇦🇹","aliases":["austria"],"tags":[],"category":"Flags","description":"flag: Austria","unicode_version":"6.0"},{"emoji":"🇦🇺","aliases":["australia"],"tags":[],"category":"Flags","description":"flag: Australia","unicode_version":"6.0"},{"emoji":"🇦🇼","aliases":["aruba"],"tags":[],"category":"Flags","description":"flag: Aruba","unicode_version":"6.0"},{"emoji":"🇦🇽","aliases":["aland_islands"],"tags":[],"category":"Flags","description":"flag: Åland Islands","unicode_version":"6.0"},{"emoji":"🇦🇿","aliases":["azerbaijan"],"tags":[],"category":"Flags","description":"flag: Azerbaijan","unicode_version":"6.0"},{"emoji":"🇧🇦","aliases":["bosnia_herzegovina"],"tags":[],"category":"Flags","description":"flag: Bosnia & Herzegovina","unicode_version":"6.0"},{"emoji":"🇧🇧","aliases":["barbados"],"tags":[],"category":"Flags","description":"flag: Barbados","unicode_version":"6.0"},{"emoji":"🇧🇩","aliases":["bangladesh"],"tags":[],"category":"Flags","description":"flag: Bangladesh","unicode_version":"6.0"},{"emoji":"🇧🇪","aliases":["belgium"],"tags":[],"category":"Flags","description":"flag: Belgium","unicode_version":"6.0"},{"emoji":"🇧🇫","aliases":["burkina_faso"],"tags":[],"category":"Flags","description":"flag: Burkina Faso","unicode_version":"6.0"},{"emoji":"🇧🇬","aliases":["bulgaria"],"tags":[],"category":"Flags","description":"flag: Bulgaria","unicode_version":"6.0"},{"emoji":"🇧🇭","aliases":["bahrain"],"tags":[],"category":"Flags","description":"flag: Bahrain","unicode_version":"6.0"},{"emoji":"🇧🇮","aliases":["burundi"],"tags":[],"category":"Flags","description":"flag: Burundi","unicode_version":"6.0"},{"emoji":"🇧🇯","aliases":["benin"],"tags":[],"category":"Flags","description":"flag: Benin","unicode_version":"6.0"},{"emoji":"🇧🇱","aliases":["st_barthelemy"],"tags":[],"category":"Flags","description":"flag: St. Barthélemy","unicode_version":"6.0"},{"emoji":"🇧🇲","aliases":["bermuda"],"tags":[],"category":"Flags","description":"flag: Bermuda","unicode_version":"6.0"},{"emoji":"🇧🇳","aliases":["brunei"],"tags":[],"category":"Flags","description":"flag: Brunei","unicode_version":"6.0"},{"emoji":"🇧🇴","aliases":["bolivia"],"tags":[],"category":"Flags","description":"flag: Bolivia","unicode_version":"6.0"},{"emoji":"🇧🇶","aliases":["caribbean_netherlands"],"tags":[],"category":"Flags","description":"flag: Caribbean Netherlands","unicode_version":"6.0"},{"emoji":"🇧🇷","aliases":["brazil"],"tags":[],"category":"Flags","description":"flag: Brazil","unicode_version":"6.0"},{"emoji":"🇧🇸","aliases":["bahamas"],"tags":[],"category":"Flags","description":"flag: Bahamas","unicode_version":"6.0"},{"emoji":"🇧🇹","aliases":["bhutan"],"tags":[],"category":"Flags","description":"flag: Bhutan","unicode_version":"6.0"},{"emoji":"🇧🇻","aliases":["bouvet_island"],"tags":[],"category":"Flags","description":"flag: Bouvet Island","unicode_version":"11.0"},{"emoji":"🇧🇼","aliases":["botswana"],"tags":[],"category":"Flags","description":"flag: Botswana","unicode_version":"6.0"},{"emoji":"🇧🇾","aliases":["belarus"],"tags":[],"category":"Flags","description":"flag: Belarus","unicode_version":"6.0"},{"emoji":"🇧🇿","aliases":["belize"],"tags":[],"category":"Flags","description":"flag: Belize","unicode_version":"6.0"},{"emoji":"🇨🇦","aliases":["canada"],"tags":[],"category":"Flags","description":"flag: Canada","unicode_version":"6.0"},{"emoji":"🇨🇨","aliases":["cocos_islands"],"tags":["keeling"],"category":"Flags","description":"flag: Cocos (Keeling) Islands","unicode_version":"6.0"},{"emoji":"🇨🇩","aliases":["congo_kinshasa"],"tags":[],"category":"Flags","description":"flag: Congo - Kinshasa","unicode_version":"6.0"},{"emoji":"🇨🇫","aliases":["central_african_republic"],"tags":[],"category":"Flags","description":"flag: Central African Republic","unicode_version":"6.0"},{"emoji":"🇨🇬","aliases":["congo_brazzaville"],"tags":[],"category":"Flags","description":"flag: Congo - Brazzaville","unicode_version":"6.0"},{"emoji":"🇨🇭","aliases":["switzerland"],"tags":[],"category":"Flags","description":"flag: Switzerland","unicode_version":"6.0"},{"emoji":"🇨🇮","aliases":["cote_divoire"],"tags":["ivory"],"category":"Flags","description":"flag: Côte d’Ivoire","unicode_version":"6.0"},{"emoji":"🇨🇰","aliases":["cook_islands"],"tags":[],"category":"Flags","description":"flag: Cook Islands","unicode_version":"6.0"},{"emoji":"🇨🇱","aliases":["chile"],"tags":[],"category":"Flags","description":"flag: Chile","unicode_version":"6.0"},{"emoji":"🇨🇲","aliases":["cameroon"],"tags":[],"category":"Flags","description":"flag: Cameroon","unicode_version":"6.0"},{"emoji":"🇨🇳","aliases":["cn"],"tags":["china"],"category":"Flags","description":"flag: China","unicode_version":"6.0"},{"emoji":"🇨🇴","aliases":["colombia"],"tags":[],"category":"Flags","description":"flag: Colombia","unicode_version":"6.0"},{"emoji":"🇨🇵","aliases":["clipperton_island"],"tags":[],"category":"Flags","description":"flag: Clipperton Island","unicode_version":"11.0"},{"emoji":"🇨🇷","aliases":["costa_rica"],"tags":[],"category":"Flags","description":"flag: Costa Rica","unicode_version":"6.0"},{"emoji":"🇨🇺","aliases":["cuba"],"tags":[],"category":"Flags","description":"flag: Cuba","unicode_version":"6.0"},{"emoji":"🇨🇻","aliases":["cape_verde"],"tags":[],"category":"Flags","description":"flag: Cape Verde","unicode_version":"6.0"},{"emoji":"🇨🇼","aliases":["curacao"],"tags":[],"category":"Flags","description":"flag: Curaçao","unicode_version":"6.0"},{"emoji":"🇨🇽","aliases":["christmas_island"],"tags":[],"category":"Flags","description":"flag: Christmas Island","unicode_version":"6.0"},{"emoji":"🇨🇾","aliases":["cyprus"],"tags":[],"category":"Flags","description":"flag: Cyprus","unicode_version":"6.0"},{"emoji":"🇨🇿","aliases":["czech_republic"],"tags":[],"category":"Flags","description":"flag: Czechia","unicode_version":"6.0"},{"emoji":"🇩🇪","aliases":["de"],"tags":["flag","germany"],"category":"Flags","description":"flag: Germany","unicode_version":"6.0"},{"emoji":"🇩🇬","aliases":["diego_garcia"],"tags":[],"category":"Flags","description":"flag: Diego Garcia","unicode_version":"11.0"},{"emoji":"🇩🇯","aliases":["djibouti"],"tags":[],"category":"Flags","description":"flag: Djibouti","unicode_version":"6.0"},{"emoji":"🇩🇰","aliases":["denmark"],"tags":[],"category":"Flags","description":"flag: Denmark","unicode_version":"6.0"},{"emoji":"🇩🇲","aliases":["dominica"],"tags":[],"category":"Flags","description":"flag: Dominica","unicode_version":"6.0"},{"emoji":"🇩🇴","aliases":["dominican_republic"],"tags":[],"category":"Flags","description":"flag: Dominican Republic","unicode_version":"6.0"},{"emoji":"🇩🇿","aliases":["algeria"],"tags":[],"category":"Flags","description":"flag: Algeria","unicode_version":"6.0"},{"emoji":"🇪🇦","aliases":["ceuta_melilla"],"tags":[],"category":"Flags","description":"flag: Ceuta & Melilla","unicode_version":"11.0"},{"emoji":"🇪🇨","aliases":["ecuador"],"tags":[],"category":"Flags","description":"flag: Ecuador","unicode_version":"6.0"},{"emoji":"🇪🇪","aliases":["estonia"],"tags":[],"category":"Flags","description":"flag: Estonia","unicode_version":"6.0"},{"emoji":"🇪🇬","aliases":["egypt"],"tags":[],"category":"Flags","description":"flag: Egypt","unicode_version":"6.0"},{"emoji":"🇪🇭","aliases":["western_sahara"],"tags":[],"category":"Flags","description":"flag: Western Sahara","unicode_version":"6.0"},{"emoji":"🇪🇷","aliases":["eritrea"],"tags":[],"category":"Flags","description":"flag: Eritrea","unicode_version":"6.0"},{"emoji":"🇪🇸","aliases":["es"],"tags":["spain"],"category":"Flags","description":"flag: Spain","unicode_version":"6.0"},{"emoji":"🇪🇹","aliases":["ethiopia"],"tags":[],"category":"Flags","description":"flag: Ethiopia","unicode_version":"6.0"},{"emoji":"🇪🇺","aliases":["eu","european_union"],"tags":[],"category":"Flags","description":"flag: European Union","unicode_version":"6.0"},{"emoji":"🇫🇮","aliases":["finland"],"tags":[],"category":"Flags","description":"flag: Finland","unicode_version":"6.0"},{"emoji":"🇫🇯","aliases":["fiji"],"tags":[],"category":"Flags","description":"flag: Fiji","unicode_version":"6.0"},{"emoji":"🇫🇰","aliases":["falkland_islands"],"tags":[],"category":"Flags","description":"flag: Falkland Islands","unicode_version":"6.0"},{"emoji":"🇫🇲","aliases":["micronesia"],"tags":[],"category":"Flags","description":"flag: Micronesia","unicode_version":"6.0"},{"emoji":"🇫🇴","aliases":["faroe_islands"],"tags":[],"category":"Flags","description":"flag: Faroe Islands","unicode_version":"6.0"},{"emoji":"🇫🇷","aliases":["fr"],"tags":["france","french"],"category":"Flags","description":"flag: France","unicode_version":"6.0"},{"emoji":"🇬🇦","aliases":["gabon"],"tags":[],"category":"Flags","description":"flag: Gabon","unicode_version":"6.0"},{"emoji":"🇬🇧","aliases":["gb","uk"],"tags":["flag","british"],"category":"Flags","description":"flag: United Kingdom","unicode_version":"6.0"},{"emoji":"🇬🇩","aliases":["grenada"],"tags":[],"category":"Flags","description":"flag: Grenada","unicode_version":"6.0"},{"emoji":"🇬🇪","aliases":["georgia"],"tags":[],"category":"Flags","description":"flag: Georgia","unicode_version":"6.0"},{"emoji":"🇬🇫","aliases":["french_guiana"],"tags":[],"category":"Flags","description":"flag: French Guiana","unicode_version":"6.0"},{"emoji":"🇬🇬","aliases":["guernsey"],"tags":[],"category":"Flags","description":"flag: Guernsey","unicode_version":"6.0"},{"emoji":"🇬🇭","aliases":["ghana"],"tags":[],"category":"Flags","description":"flag: Ghana","unicode_version":"6.0"},{"emoji":"🇬🇮","aliases":["gibraltar"],"tags":[],"category":"Flags","description":"flag: Gibraltar","unicode_version":"6.0"},{"emoji":"🇬🇱","aliases":["greenland"],"tags":[],"category":"Flags","description":"flag: Greenland","unicode_version":"6.0"},{"emoji":"🇬🇲","aliases":["gambia"],"tags":[],"category":"Flags","description":"flag: Gambia","unicode_version":"6.0"},{"emoji":"🇬🇳","aliases":["guinea"],"tags":[],"category":"Flags","description":"flag: Guinea","unicode_version":"6.0"},{"emoji":"🇬🇵","aliases":["guadeloupe"],"tags":[],"category":"Flags","description":"flag: Guadeloupe","unicode_version":"6.0"},{"emoji":"🇬🇶","aliases":["equatorial_guinea"],"tags":[],"category":"Flags","description":"flag: Equatorial Guinea","unicode_version":"6.0"},{"emoji":"🇬🇷","aliases":["greece"],"tags":[],"category":"Flags","description":"flag: Greece","unicode_version":"6.0"},{"emoji":"🇬🇸","aliases":["south_georgia_south_sandwich_islands"],"tags":[],"category":"Flags","description":"flag: South Georgia & South Sandwich Islands","unicode_version":"6.0"},{"emoji":"🇬🇹","aliases":["guatemala"],"tags":[],"category":"Flags","description":"flag: Guatemala","unicode_version":"6.0"},{"emoji":"🇬🇺","aliases":["guam"],"tags":[],"category":"Flags","description":"flag: Guam","unicode_version":"6.0"},{"emoji":"🇬🇼","aliases":["guinea_bissau"],"tags":[],"category":"Flags","description":"flag: Guinea-Bissau","unicode_version":"6.0"},{"emoji":"🇬🇾","aliases":["guyana"],"tags":[],"category":"Flags","description":"flag: Guyana","unicode_version":"6.0"},{"emoji":"🇭🇰","aliases":["hong_kong"],"tags":[],"category":"Flags","description":"flag: Hong Kong SAR China","unicode_version":"6.0"},{"emoji":"🇭🇲","aliases":["heard_mcdonald_islands"],"tags":[],"category":"Flags","description":"flag: Heard & McDonald Islands","unicode_version":"11.0"},{"emoji":"🇭🇳","aliases":["honduras"],"tags":[],"category":"Flags","description":"flag: Honduras","unicode_version":"6.0"},{"emoji":"🇭🇷","aliases":["croatia"],"tags":[],"category":"Flags","description":"flag: Croatia","unicode_version":"6.0"},{"emoji":"🇭🇹","aliases":["haiti"],"tags":[],"category":"Flags","description":"flag: Haiti","unicode_version":"6.0"},{"emoji":"🇭🇺","aliases":["hungary"],"tags":[],"category":"Flags","description":"flag: Hungary","unicode_version":"6.0"},{"emoji":"🇮🇨","aliases":["canary_islands"],"tags":[],"category":"Flags","description":"flag: Canary Islands","unicode_version":"6.0"},{"emoji":"🇮🇩","aliases":["indonesia"],"tags":[],"category":"Flags","description":"flag: Indonesia","unicode_version":"6.0"},{"emoji":"🇮🇪","aliases":["ireland"],"tags":[],"category":"Flags","description":"flag: Ireland","unicode_version":"6.0"},{"emoji":"🇮🇱","aliases":["israel"],"tags":[],"category":"Flags","description":"flag: Israel","unicode_version":"6.0"},{"emoji":"🇮🇲","aliases":["isle_of_man"],"tags":[],"category":"Flags","description":"flag: Isle of Man","unicode_version":"6.0"},{"emoji":"🇮🇳","aliases":["india"],"tags":[],"category":"Flags","description":"flag: India","unicode_version":"6.0"},{"emoji":"🇮🇴","aliases":["british_indian_ocean_territory"],"tags":[],"category":"Flags","description":"flag: British Indian Ocean Territory","unicode_version":"6.0"},{"emoji":"🇮🇶","aliases":["iraq"],"tags":[],"category":"Flags","description":"flag: Iraq","unicode_version":"6.0"},{"emoji":"🇮🇷","aliases":["iran"],"tags":[],"category":"Flags","description":"flag: Iran","unicode_version":"6.0"},{"emoji":"🇮🇸","aliases":["iceland"],"tags":[],"category":"Flags","description":"flag: Iceland","unicode_version":"6.0"},{"emoji":"🇮🇹","aliases":["it"],"tags":["italy"],"category":"Flags","description":"flag: Italy","unicode_version":"6.0"},{"emoji":"🇯🇪","aliases":["jersey"],"tags":[],"category":"Flags","description":"flag: Jersey","unicode_version":"6.0"},{"emoji":"🇯🇲","aliases":["jamaica"],"tags":[],"category":"Flags","description":"flag: Jamaica","unicode_version":"6.0"},{"emoji":"🇯🇴","aliases":["jordan"],"tags":[],"category":"Flags","description":"flag: Jordan","unicode_version":"6.0"},{"emoji":"🇯🇵","aliases":["jp"],"tags":["japan"],"category":"Flags","description":"flag: Japan","unicode_version":"6.0"},{"emoji":"🇰🇪","aliases":["kenya"],"tags":[],"category":"Flags","description":"flag: Kenya","unicode_version":"6.0"},{"emoji":"🇰🇬","aliases":["kyrgyzstan"],"tags":[],"category":"Flags","description":"flag: Kyrgyzstan","unicode_version":"6.0"},{"emoji":"🇰🇭","aliases":["cambodia"],"tags":[],"category":"Flags","description":"flag: Cambodia","unicode_version":"6.0"},{"emoji":"🇰🇮","aliases":["kiribati"],"tags":[],"category":"Flags","description":"flag: Kiribati","unicode_version":"6.0"},{"emoji":"🇰🇲","aliases":["comoros"],"tags":[],"category":"Flags","description":"flag: Comoros","unicode_version":"6.0"},{"emoji":"🇰🇳","aliases":["st_kitts_nevis"],"tags":[],"category":"Flags","description":"flag: St. Kitts & Nevis","unicode_version":"6.0"},{"emoji":"🇰🇵","aliases":["north_korea"],"tags":[],"category":"Flags","description":"flag: North Korea","unicode_version":"6.0"},{"emoji":"🇰🇷","aliases":["kr"],"tags":["korea"],"category":"Flags","description":"flag: South Korea","unicode_version":"6.0"},{"emoji":"🇰🇼","aliases":["kuwait"],"tags":[],"category":"Flags","description":"flag: Kuwait","unicode_version":"6.0"},{"emoji":"🇰🇾","aliases":["cayman_islands"],"tags":[],"category":"Flags","description":"flag: Cayman Islands","unicode_version":"6.0"},{"emoji":"🇰🇿","aliases":["kazakhstan"],"tags":[],"category":"Flags","description":"flag: Kazakhstan","unicode_version":"6.0"},{"emoji":"🇱🇦","aliases":["laos"],"tags":[],"category":"Flags","description":"flag: Laos","unicode_version":"6.0"},{"emoji":"🇱🇧","aliases":["lebanon"],"tags":[],"category":"Flags","description":"flag: Lebanon","unicode_version":"6.0"},{"emoji":"🇱🇨","aliases":["st_lucia"],"tags":[],"category":"Flags","description":"flag: St. Lucia","unicode_version":"6.0"},{"emoji":"🇱🇮","aliases":["liechtenstein"],"tags":[],"category":"Flags","description":"flag: Liechtenstein","unicode_version":"6.0"},{"emoji":"🇱🇰","aliases":["sri_lanka"],"tags":[],"category":"Flags","description":"flag: Sri Lanka","unicode_version":"6.0"},{"emoji":"🇱🇷","aliases":["liberia"],"tags":[],"category":"Flags","description":"flag: Liberia","unicode_version":"6.0"},{"emoji":"🇱🇸","aliases":["lesotho"],"tags":[],"category":"Flags","description":"flag: Lesotho","unicode_version":"6.0"},{"emoji":"🇱🇹","aliases":["lithuania"],"tags":[],"category":"Flags","description":"flag: Lithuania","unicode_version":"6.0"},{"emoji":"🇱🇺","aliases":["luxembourg"],"tags":[],"category":"Flags","description":"flag: Luxembourg","unicode_version":"6.0"},{"emoji":"🇱🇻","aliases":["latvia"],"tags":[],"category":"Flags","description":"flag: Latvia","unicode_version":"6.0"},{"emoji":"🇱🇾","aliases":["libya"],"tags":[],"category":"Flags","description":"flag: Libya","unicode_version":"6.0"},{"emoji":"🇲🇦","aliases":["morocco"],"tags":[],"category":"Flags","description":"flag: Morocco","unicode_version":"6.0"},{"emoji":"🇲🇨","aliases":["monaco"],"tags":[],"category":"Flags","description":"flag: Monaco","unicode_version":"6.0"},{"emoji":"🇲🇩","aliases":["moldova"],"tags":[],"category":"Flags","description":"flag: Moldova","unicode_version":"6.0"},{"emoji":"🇲🇪","aliases":["montenegro"],"tags":[],"category":"Flags","description":"flag: Montenegro","unicode_version":"6.0"},{"emoji":"🇲🇫","aliases":["st_martin"],"tags":[],"category":"Flags","description":"flag: St. Martin","unicode_version":"11.0"},{"emoji":"🇲🇬","aliases":["madagascar"],"tags":[],"category":"Flags","description":"flag: Madagascar","unicode_version":"6.0"},{"emoji":"🇲🇭","aliases":["marshall_islands"],"tags":[],"category":"Flags","description":"flag: Marshall Islands","unicode_version":"6.0"},{"emoji":"🇲🇰","aliases":["macedonia"],"tags":[],"category":"Flags","description":"flag: North Macedonia","unicode_version":"6.0"},{"emoji":"🇲🇱","aliases":["mali"],"tags":[],"category":"Flags","description":"flag: Mali","unicode_version":"6.0"},{"emoji":"🇲🇲","aliases":["myanmar"],"tags":["burma"],"category":"Flags","description":"flag: Myanmar (Burma)","unicode_version":"6.0"},{"emoji":"🇲🇳","aliases":["mongolia"],"tags":[],"category":"Flags","description":"flag: Mongolia","unicode_version":"6.0"},{"emoji":"🇲🇴","aliases":["macau"],"tags":[],"category":"Flags","description":"flag: Macao SAR China","unicode_version":"6.0"},{"emoji":"🇲🇵","aliases":["northern_mariana_islands"],"tags":[],"category":"Flags","description":"flag: Northern Mariana Islands","unicode_version":"6.0"},{"emoji":"🇲🇶","aliases":["martinique"],"tags":[],"category":"Flags","description":"flag: Martinique","unicode_version":"6.0"},{"emoji":"🇲🇷","aliases":["mauritania"],"tags":[],"category":"Flags","description":"flag: Mauritania","unicode_version":"6.0"},{"emoji":"🇲🇸","aliases":["montserrat"],"tags":[],"category":"Flags","description":"flag: Montserrat","unicode_version":"6.0"},{"emoji":"🇲🇹","aliases":["malta"],"tags":[],"category":"Flags","description":"flag: Malta","unicode_version":"6.0"},{"emoji":"🇲🇺","aliases":["mauritius"],"tags":[],"category":"Flags","description":"flag: Mauritius","unicode_version":"6.0"},{"emoji":"🇲🇻","aliases":["maldives"],"tags":[],"category":"Flags","description":"flag: Maldives","unicode_version":"6.0"},{"emoji":"🇲🇼","aliases":["malawi"],"tags":[],"category":"Flags","description":"flag: Malawi","unicode_version":"6.0"},{"emoji":"🇲🇽","aliases":["mexico"],"tags":[],"category":"Flags","description":"flag: Mexico","unicode_version":"6.0"},{"emoji":"🇲🇾","aliases":["malaysia"],"tags":[],"category":"Flags","description":"flag: Malaysia","unicode_version":"6.0"},{"emoji":"🇲🇿","aliases":["mozambique"],"tags":[],"category":"Flags","description":"flag: Mozambique","unicode_version":"6.0"},{"emoji":"🇳🇦","aliases":["namibia"],"tags":[],"category":"Flags","description":"flag: Namibia","unicode_version":"6.0"},{"emoji":"🇳🇨","aliases":["new_caledonia"],"tags":[],"category":"Flags","description":"flag: New Caledonia","unicode_version":"6.0"},{"emoji":"🇳🇪","aliases":["niger"],"tags":[],"category":"Flags","description":"flag: Niger","unicode_version":"6.0"},{"emoji":"🇳🇫","aliases":["norfolk_island"],"tags":[],"category":"Flags","description":"flag: Norfolk Island","unicode_version":"6.0"},{"emoji":"🇳🇬","aliases":["nigeria"],"tags":[],"category":"Flags","description":"flag: Nigeria","unicode_version":"6.0"},{"emoji":"🇳🇮","aliases":["nicaragua"],"tags":[],"category":"Flags","description":"flag: Nicaragua","unicode_version":"6.0"},{"emoji":"🇳🇱","aliases":["netherlands"],"tags":[],"category":"Flags","description":"flag: Netherlands","unicode_version":"6.0"},{"emoji":"🇳🇴","aliases":["norway"],"tags":[],"category":"Flags","description":"flag: Norway","unicode_version":"6.0"},{"emoji":"🇳🇵","aliases":["nepal"],"tags":[],"category":"Flags","description":"flag: Nepal","unicode_version":"6.0"},{"emoji":"🇳🇷","aliases":["nauru"],"tags":[],"category":"Flags","description":"flag: Nauru","unicode_version":"6.0"},{"emoji":"🇳🇺","aliases":["niue"],"tags":[],"category":"Flags","description":"flag: Niue","unicode_version":"6.0"},{"emoji":"🇳🇿","aliases":["new_zealand"],"tags":[],"category":"Flags","description":"flag: New Zealand","unicode_version":"6.0"},{"emoji":"🇴🇲","aliases":["oman"],"tags":[],"category":"Flags","description":"flag: Oman","unicode_version":"6.0"},{"emoji":"🇵🇦","aliases":["panama"],"tags":[],"category":"Flags","description":"flag: Panama","unicode_version":"6.0"},{"emoji":"🇵🇪","aliases":["peru"],"tags":[],"category":"Flags","description":"flag: Peru","unicode_version":"6.0"},{"emoji":"🇵🇫","aliases":["french_polynesia"],"tags":[],"category":"Flags","description":"flag: French Polynesia","unicode_version":"6.0"},{"emoji":"🇵🇬","aliases":["papua_new_guinea"],"tags":[],"category":"Flags","description":"flag: Papua New Guinea","unicode_version":"6.0"},{"emoji":"🇵🇭","aliases":["philippines"],"tags":[],"category":"Flags","description":"flag: Philippines","unicode_version":"6.0"},{"emoji":"🇵🇰","aliases":["pakistan"],"tags":[],"category":"Flags","description":"flag: Pakistan","unicode_version":"6.0"},{"emoji":"🇵🇱","aliases":["poland"],"tags":[],"category":"Flags","description":"flag: Poland","unicode_version":"6.0"},{"emoji":"🇵🇲","aliases":["st_pierre_miquelon"],"tags":[],"category":"Flags","description":"flag: St. Pierre & Miquelon","unicode_version":"6.0"},{"emoji":"🇵🇳","aliases":["pitcairn_islands"],"tags":[],"category":"Flags","description":"flag: Pitcairn Islands","unicode_version":"6.0"},{"emoji":"🇵🇷","aliases":["puerto_rico"],"tags":[],"category":"Flags","description":"flag: Puerto Rico","unicode_version":"6.0"},{"emoji":"🇵🇸","aliases":["palestinian_territories"],"tags":[],"category":"Flags","description":"flag: Palestinian Territories","unicode_version":"6.0"},{"emoji":"🇵🇹","aliases":["portugal"],"tags":[],"category":"Flags","description":"flag: Portugal","unicode_version":"6.0"},{"emoji":"🇵🇼","aliases":["palau"],"tags":[],"category":"Flags","description":"flag: Palau","unicode_version":"6.0"},{"emoji":"🇵🇾","aliases":["paraguay"],"tags":[],"category":"Flags","description":"flag: Paraguay","unicode_version":"6.0"},{"emoji":"🇶🇦","aliases":["qatar"],"tags":[],"category":"Flags","description":"flag: Qatar","unicode_version":"6.0"},{"emoji":"🇷🇪","aliases":["reunion"],"tags":[],"category":"Flags","description":"flag: Réunion","unicode_version":"6.0"},{"emoji":"🇷🇴","aliases":["romania"],"tags":[],"category":"Flags","description":"flag: Romania","unicode_version":"6.0"},{"emoji":"🇷🇸","aliases":["serbia"],"tags":[],"category":"Flags","description":"flag: Serbia","unicode_version":"6.0"},{"emoji":"🇷🇺","aliases":["ru"],"tags":["russia"],"category":"Flags","description":"flag: Russia","unicode_version":"6.0"},{"emoji":"🇷🇼","aliases":["rwanda"],"tags":[],"category":"Flags","description":"flag: Rwanda","unicode_version":"6.0"},{"emoji":"🇸🇦","aliases":["saudi_arabia"],"tags":[],"category":"Flags","description":"flag: Saudi Arabia","unicode_version":"6.0"},{"emoji":"🇸🇧","aliases":["solomon_islands"],"tags":[],"category":"Flags","description":"flag: Solomon Islands","unicode_version":"6.0"},{"emoji":"🇸🇨","aliases":["seychelles"],"tags":[],"category":"Flags","description":"flag: Seychelles","unicode_version":"6.0"},{"emoji":"🇸🇩","aliases":["sudan"],"tags":[],"category":"Flags","description":"flag: Sudan","unicode_version":"6.0"},{"emoji":"🇸🇪","aliases":["sweden"],"tags":[],"category":"Flags","description":"flag: Sweden","unicode_version":"6.0"},{"emoji":"🇸🇬","aliases":["singapore"],"tags":[],"category":"Flags","description":"flag: Singapore","unicode_version":"6.0"},{"emoji":"🇸🇭","aliases":["st_helena"],"tags":[],"category":"Flags","description":"flag: St. Helena","unicode_version":"6.0"},{"emoji":"🇸🇮","aliases":["slovenia"],"tags":[],"category":"Flags","description":"flag: Slovenia","unicode_version":"6.0"},{"emoji":"🇸🇯","aliases":["svalbard_jan_mayen"],"tags":[],"category":"Flags","description":"flag: Svalbard & Jan Mayen","unicode_version":"11.0"},{"emoji":"🇸🇰","aliases":["slovakia"],"tags":[],"category":"Flags","description":"flag: Slovakia","unicode_version":"6.0"},{"emoji":"🇸🇱","aliases":["sierra_leone"],"tags":[],"category":"Flags","description":"flag: Sierra Leone","unicode_version":"6.0"},{"emoji":"🇸🇲","aliases":["san_marino"],"tags":[],"category":"Flags","description":"flag: San Marino","unicode_version":"6.0"},{"emoji":"🇸🇳","aliases":["senegal"],"tags":[],"category":"Flags","description":"flag: Senegal","unicode_version":"6.0"},{"emoji":"🇸🇴","aliases":["somalia"],"tags":[],"category":"Flags","description":"flag: Somalia","unicode_version":"6.0"},{"emoji":"🇸🇷","aliases":["suriname"],"tags":[],"category":"Flags","description":"flag: Suriname","unicode_version":"6.0"},{"emoji":"🇸🇸","aliases":["south_sudan"],"tags":[],"category":"Flags","description":"flag: South Sudan","unicode_version":"6.0"},{"emoji":"🇸🇹","aliases":["sao_tome_principe"],"tags":[],"category":"Flags","description":"flag: São Tomé & Príncipe","unicode_version":"6.0"},{"emoji":"🇸🇻","aliases":["el_salvador"],"tags":[],"category":"Flags","description":"flag: El Salvador","unicode_version":"6.0"},{"emoji":"🇸🇽","aliases":["sint_maarten"],"tags":[],"category":"Flags","description":"flag: Sint Maarten","unicode_version":"6.0"},{"emoji":"🇸🇾","aliases":["syria"],"tags":[],"category":"Flags","description":"flag: Syria","unicode_version":"6.0"},{"emoji":"🇸🇿","aliases":["swaziland"],"tags":[],"category":"Flags","description":"flag: Eswatini","unicode_version":"6.0"},{"emoji":"🇹🇦","aliases":["tristan_da_cunha"],"tags":[],"category":"Flags","description":"flag: Tristan da Cunha","unicode_version":"11.0"},{"emoji":"🇹🇨","aliases":["turks_caicos_islands"],"tags":[],"category":"Flags","description":"flag: Turks & Caicos Islands","unicode_version":"6.0"},{"emoji":"🇹🇩","aliases":["chad"],"tags":[],"category":"Flags","description":"flag: Chad","unicode_version":"6.0"},{"emoji":"🇹🇫","aliases":["french_southern_territories"],"tags":[],"category":"Flags","description":"flag: French Southern Territories","unicode_version":"6.0"},{"emoji":"🇹🇬","aliases":["togo"],"tags":[],"category":"Flags","description":"flag: Togo","unicode_version":"6.0"},{"emoji":"🇹🇭","aliases":["thailand"],"tags":[],"category":"Flags","description":"flag: Thailand","unicode_version":"6.0"},{"emoji":"🇹🇯","aliases":["tajikistan"],"tags":[],"category":"Flags","description":"flag: Tajikistan","unicode_version":"6.0"},{"emoji":"🇹🇰","aliases":["tokelau"],"tags":[],"category":"Flags","description":"flag: Tokelau","unicode_version":"6.0"},{"emoji":"🇹🇱","aliases":["timor_leste"],"tags":[],"category":"Flags","description":"flag: Timor-Leste","unicode_version":"6.0"},{"emoji":"🇹🇲","aliases":["turkmenistan"],"tags":[],"category":"Flags","description":"flag: Turkmenistan","unicode_version":"6.0"},{"emoji":"🇹🇳","aliases":["tunisia"],"tags":[],"category":"Flags","description":"flag: Tunisia","unicode_version":"6.0"},{"emoji":"🇹🇴","aliases":["tonga"],"tags":[],"category":"Flags","description":"flag: Tonga","unicode_version":"6.0"},{"emoji":"🇹🇷","aliases":["tr"],"tags":["turkey"],"category":"Flags","description":"flag: Turkey","unicode_version":"8.0"},{"emoji":"🇹🇹","aliases":["trinidad_tobago"],"tags":[],"category":"Flags","description":"flag: Trinidad & Tobago","unicode_version":"6.0"},{"emoji":"🇹🇻","aliases":["tuvalu"],"tags":[],"category":"Flags","description":"flag: Tuvalu","unicode_version":"6.0"},{"emoji":"🇹🇼","aliases":["taiwan"],"tags":[],"category":"Flags","description":"flag: Taiwan","unicode_version":"6.0"},{"emoji":"🇹🇿","aliases":["tanzania"],"tags":[],"category":"Flags","description":"flag: Tanzania","unicode_version":"6.0"},{"emoji":"🇺🇦","aliases":["ukraine"],"tags":[],"category":"Flags","description":"flag: Ukraine","unicode_version":"6.0"},{"emoji":"🇺🇬","aliases":["uganda"],"tags":[],"category":"Flags","description":"flag: Uganda","unicode_version":"6.0"},{"emoji":"🇺🇲","aliases":["us_outlying_islands"],"tags":[],"category":"Flags","description":"flag: U.S. Outlying Islands","unicode_version":"11.0"},{"emoji":"🇺🇳","aliases":["united_nations"],"tags":[],"category":"Flags","description":"flag: United Nations","unicode_version":"11.0"},{"emoji":"🇺🇸","aliases":["us"],"tags":["flag","united","america"],"category":"Flags","description":"flag: United States","unicode_version":"6.0"},{"emoji":"🇺🇾","aliases":["uruguay"],"tags":[],"category":"Flags","description":"flag: Uruguay","unicode_version":"6.0"},{"emoji":"🇺🇿","aliases":["uzbekistan"],"tags":[],"category":"Flags","description":"flag: Uzbekistan","unicode_version":"6.0"},{"emoji":"🇻🇦","aliases":["vatican_city"],"tags":[],"category":"Flags","description":"flag: Vatican City","unicode_version":"6.0"},{"emoji":"🇻🇨","aliases":["st_vincent_grenadines"],"tags":[],"category":"Flags","description":"flag: St. Vincent & Grenadines","unicode_version":"6.0"},{"emoji":"🇻🇪","aliases":["venezuela"],"tags":[],"category":"Flags","description":"flag: Venezuela","unicode_version":"6.0"},{"emoji":"🇻🇬","aliases":["british_virgin_islands"],"tags":[],"category":"Flags","description":"flag: British Virgin Islands","unicode_version":"6.0"},{"emoji":"🇻🇮","aliases":["us_virgin_islands"],"tags":[],"category":"Flags","description":"flag: U.S. Virgin Islands","unicode_version":"6.0"},{"emoji":"🇻🇳","aliases":["vietnam"],"tags":[],"category":"Flags","description":"flag: Vietnam","unicode_version":"6.0"},{"emoji":"🇻🇺","aliases":["vanuatu"],"tags":[],"category":"Flags","description":"flag: Vanuatu","unicode_version":"6.0"},{"emoji":"🇼🇫","aliases":["wallis_futuna"],"tags":[],"category":"Flags","description":"flag: Wallis & Futuna","unicode_version":"6.0"},{"emoji":"🇼🇸","aliases":["samoa"],"tags":[],"category":"Flags","description":"flag: Samoa","unicode_version":"6.0"},{"emoji":"🇽🇰","aliases":["kosovo"],"tags":[],"category":"Flags","description":"flag: Kosovo","unicode_version":"6.0"},{"emoji":"🇾🇪","aliases":["yemen"],"tags":[],"category":"Flags","description":"flag: Yemen","unicode_version":"6.0"},{"emoji":"🇾🇹","aliases":["mayotte"],"tags":[],"category":"Flags","description":"flag: Mayotte","unicode_version":"6.0"},{"emoji":"🇿🇦","aliases":["south_africa"],"tags":[],"category":"Flags","description":"flag: South Africa","unicode_version":"6.0"},{"emoji":"🇿🇲","aliases":["zambia"],"tags":[],"category":"Flags","description":"flag: Zambia","unicode_version":"6.0"},{"emoji":"🇿🇼","aliases":["zimbabwe"],"tags":[],"category":"Flags","description":"flag: Zimbabwe","unicode_version":"6.0"},{"emoji":"🏴󠁧󠁢󠁥󠁮󠁧󠁿","aliases":["england"],"tags":[],"category":"Flags","description":"flag: England","unicode_version":"11.0"},{"emoji":"🏴󠁧󠁢󠁳󠁣󠁴󠁿","aliases":["scotland"],"tags":[],"category":"Flags","description":"flag: Scotland","unicode_version":"11.0"},{"emoji":"🏴󠁧󠁢󠁷󠁬󠁳󠁿","aliases":["wales"],"tags":[],"category":"Flags","description":"flag: Wales","unicode_version":"11.0"}] +export const rawEmojis = [ + { + emoji: "😀", + aliases: ["grinning"], + tags: ["smile", "happy"], + category: "Smileys & Emotion", + description: "grinning face", + unicode_version: "6.1", + }, + { + emoji: "😃", + aliases: ["smiley"], + tags: ["happy", "joy", "haha"], + category: "Smileys & Emotion", + description: "grinning face with big eyes", + unicode_version: "6.0", + }, + { + emoji: "😄", + aliases: ["smile"], + tags: ["happy", "joy", "laugh", "pleased"], + category: "Smileys & Emotion", + description: "grinning face with smiling eyes", + unicode_version: "6.0", + }, + { + emoji: "😁", + aliases: ["grin"], + tags: [], + category: "Smileys & Emotion", + description: "beaming face with smiling eyes", + unicode_version: "6.0", + }, + { + emoji: "😆", + aliases: ["laughing", "satisfied"], + tags: ["happy", "haha"], + category: "Smileys & Emotion", + description: "grinning squinting face", + unicode_version: "6.0", + }, + { + emoji: "😅", + aliases: ["sweat_smile"], + tags: ["hot"], + category: "Smileys & Emotion", + description: "grinning face with sweat", + unicode_version: "6.0", + }, + { + emoji: "🤣", + aliases: ["rofl"], + tags: ["lol", "laughing"], + category: "Smileys & Emotion", + description: "rolling on the floor laughing", + unicode_version: "9.0", + }, + { + emoji: "😂", + aliases: ["joy"], + tags: ["tears"], + category: "Smileys & Emotion", + description: "face with tears of joy", + unicode_version: "6.0", + }, + { + emoji: "🙂", + aliases: ["slightly_smiling_face"], + tags: [], + category: "Smileys & Emotion", + description: "slightly smiling face", + unicode_version: "7.0", + }, + { + emoji: "🙃", + aliases: ["upside_down_face"], + tags: [], + category: "Smileys & Emotion", + description: "upside-down face", + unicode_version: "8.0", + }, + { + emoji: "😉", + aliases: ["wink"], + tags: ["flirt"], + category: "Smileys & Emotion", + description: "winking face", + unicode_version: "6.0", + }, + { + emoji: "😊", + aliases: ["blush"], + tags: ["proud"], + category: "Smileys & Emotion", + description: "smiling face with smiling eyes", + unicode_version: "6.0", + }, + { + emoji: "😇", + aliases: ["innocent"], + tags: ["angel"], + category: "Smileys & Emotion", + description: "smiling face with halo", + unicode_version: "6.0", + }, + { + emoji: "🥰", + aliases: ["smiling_face_with_three_hearts"], + tags: ["love"], + category: "Smileys & Emotion", + description: "smiling face with hearts", + unicode_version: "11.0", + }, + { + emoji: "😍", + aliases: ["heart_eyes"], + tags: ["love", "crush"], + category: "Smileys & Emotion", + description: "smiling face with heart-eyes", + unicode_version: "6.0", + }, + { + emoji: "🤩", + aliases: ["star_struck"], + tags: ["eyes"], + category: "Smileys & Emotion", + description: "star-struck", + unicode_version: "11.0", + }, + { + emoji: "😘", + aliases: ["kissing_heart"], + tags: ["flirt"], + category: "Smileys & Emotion", + description: "face blowing a kiss", + unicode_version: "6.0", + }, + { + emoji: "😗", + aliases: ["kissing"], + tags: [], + category: "Smileys & Emotion", + description: "kissing face", + unicode_version: "6.1", + }, + { + emoji: "☺️", + aliases: ["relaxed"], + tags: ["blush", "pleased"], + category: "Smileys & Emotion", + description: "smiling face", + unicode_version: "", + }, + { + emoji: "😚", + aliases: ["kissing_closed_eyes"], + tags: [], + category: "Smileys & Emotion", + description: "kissing face with closed eyes", + unicode_version: "6.0", + }, + { + emoji: "😙", + aliases: ["kissing_smiling_eyes"], + tags: [], + category: "Smileys & Emotion", + description: "kissing face with smiling eyes", + unicode_version: "6.1", + }, + { + emoji: "🥲", + aliases: ["smiling_face_with_tear"], + tags: [], + category: "Smileys & Emotion", + description: "smiling face with tear", + unicode_version: "13.0", + }, + { + emoji: "😋", + aliases: ["yum"], + tags: ["tongue", "lick"], + category: "Smileys & Emotion", + description: "face savoring food", + unicode_version: "6.0", + }, + { + emoji: "😛", + aliases: ["stuck_out_tongue"], + tags: [], + category: "Smileys & Emotion", + description: "face with tongue", + unicode_version: "6.1", + }, + { + emoji: "😜", + aliases: ["stuck_out_tongue_winking_eye"], + tags: ["prank", "silly"], + category: "Smileys & Emotion", + description: "winking face with tongue", + unicode_version: "6.0", + }, + { + emoji: "🤪", + aliases: ["zany_face"], + tags: ["goofy", "wacky"], + category: "Smileys & Emotion", + description: "zany face", + unicode_version: "11.0", + }, + { + emoji: "😝", + aliases: ["stuck_out_tongue_closed_eyes"], + tags: ["prank"], + category: "Smileys & Emotion", + description: "squinting face with tongue", + unicode_version: "6.0", + }, + { + emoji: "🤑", + aliases: ["money_mouth_face"], + tags: ["rich"], + category: "Smileys & Emotion", + description: "money-mouth face", + unicode_version: "8.0", + }, + { + emoji: "🤗", + aliases: ["hugs"], + tags: [], + category: "Smileys & Emotion", + description: "hugging face", + unicode_version: "8.0", + }, + { + emoji: "🤭", + aliases: ["hand_over_mouth"], + tags: ["quiet", "whoops"], + category: "Smileys & Emotion", + description: "face with hand over mouth", + unicode_version: "11.0", + }, + { + emoji: "🤫", + aliases: ["shushing_face"], + tags: ["silence", "quiet"], + category: "Smileys & Emotion", + description: "shushing face", + unicode_version: "11.0", + }, + { + emoji: "🤔", + aliases: ["thinking"], + tags: [], + category: "Smileys & Emotion", + description: "thinking face", + unicode_version: "8.0", + }, + { + emoji: "🤐", + aliases: ["zipper_mouth_face"], + tags: ["silence", "hush"], + category: "Smileys & Emotion", + description: "zipper-mouth face", + unicode_version: "8.0", + }, + { + emoji: "🤨", + aliases: ["raised_eyebrow"], + tags: ["suspicious"], + category: "Smileys & Emotion", + description: "face with raised eyebrow", + unicode_version: "11.0", + }, + { + emoji: "😐", + aliases: ["neutral_face"], + tags: ["meh"], + category: "Smileys & Emotion", + description: "neutral face", + unicode_version: "6.0", + }, + { + emoji: "😑", + aliases: ["expressionless"], + tags: [], + category: "Smileys & Emotion", + description: "expressionless face", + unicode_version: "6.1", + }, + { + emoji: "😶", + aliases: ["no_mouth"], + tags: ["mute", "silence"], + category: "Smileys & Emotion", + description: "face without mouth", + unicode_version: "6.0", + }, + { + emoji: "😶‍🌫️", + aliases: ["face_in_clouds"], + tags: [], + category: "Smileys & Emotion", + description: "face in clouds", + unicode_version: "13.1", + }, + { + emoji: "😏", + aliases: ["smirk"], + tags: ["smug"], + category: "Smileys & Emotion", + description: "smirking face", + unicode_version: "6.0", + }, + { + emoji: "😒", + aliases: ["unamused"], + tags: ["meh"], + category: "Smileys & Emotion", + description: "unamused face", + unicode_version: "6.0", + }, + { + emoji: "🙄", + aliases: ["roll_eyes"], + tags: [], + category: "Smileys & Emotion", + description: "face with rolling eyes", + unicode_version: "8.0", + }, + { + emoji: "😬", + aliases: ["grimacing"], + tags: [], + category: "Smileys & Emotion", + description: "grimacing face", + unicode_version: "6.1", + }, + { + emoji: "😮‍💨", + aliases: ["face_exhaling"], + tags: [], + category: "Smileys & Emotion", + description: "face exhaling", + unicode_version: "13.1", + }, + { + emoji: "🤥", + aliases: ["lying_face"], + tags: ["liar"], + category: "Smileys & Emotion", + description: "lying face", + unicode_version: "9.0", + }, + { + emoji: "😌", + aliases: ["relieved"], + tags: ["whew"], + category: "Smileys & Emotion", + description: "relieved face", + unicode_version: "6.0", + }, + { + emoji: "😔", + aliases: ["pensive"], + tags: [], + category: "Smileys & Emotion", + description: "pensive face", + unicode_version: "6.0", + }, + { + emoji: "😪", + aliases: ["sleepy"], + tags: ["tired"], + category: "Smileys & Emotion", + description: "sleepy face", + unicode_version: "6.0", + }, + { + emoji: "🤤", + aliases: ["drooling_face"], + tags: [], + category: "Smileys & Emotion", + description: "drooling face", + unicode_version: "9.0", + }, + { + emoji: "😴", + aliases: ["sleeping"], + tags: ["zzz"], + category: "Smileys & Emotion", + description: "sleeping face", + unicode_version: "6.1", + }, + { + emoji: "😷", + aliases: ["mask"], + tags: ["sick", "ill"], + category: "Smileys & Emotion", + description: "face with medical mask", + unicode_version: "6.0", + }, + { + emoji: "🤒", + aliases: ["face_with_thermometer"], + tags: ["sick"], + category: "Smileys & Emotion", + description: "face with thermometer", + unicode_version: "8.0", + }, + { + emoji: "🤕", + aliases: ["face_with_head_bandage"], + tags: ["hurt"], + category: "Smileys & Emotion", + description: "face with head-bandage", + unicode_version: "8.0", + }, + { + emoji: "🤢", + aliases: ["nauseated_face"], + tags: ["sick", "barf", "disgusted"], + category: "Smileys & Emotion", + description: "nauseated face", + unicode_version: "9.0", + }, + { + emoji: "🤮", + aliases: ["vomiting_face"], + tags: ["barf", "sick"], + category: "Smileys & Emotion", + description: "face vomiting", + unicode_version: "11.0", + }, + { + emoji: "🤧", + aliases: ["sneezing_face"], + tags: ["achoo", "sick"], + category: "Smileys & Emotion", + description: "sneezing face", + unicode_version: "9.0", + }, + { + emoji: "🥵", + aliases: ["hot_face"], + tags: ["heat", "sweating"], + category: "Smileys & Emotion", + description: "hot face", + unicode_version: "11.0", + }, + { + emoji: "🥶", + aliases: ["cold_face"], + tags: ["freezing", "ice"], + category: "Smileys & Emotion", + description: "cold face", + unicode_version: "11.0", + }, + { + emoji: "🥴", + aliases: ["woozy_face"], + tags: ["groggy"], + category: "Smileys & Emotion", + description: "woozy face", + unicode_version: "11.0", + }, + { + emoji: "😵", + aliases: ["dizzy_face"], + tags: [], + category: "Smileys & Emotion", + description: "knocked-out face", + unicode_version: "6.0", + }, + { + emoji: "😵‍💫", + aliases: ["face_with_spiral_eyes"], + tags: [], + category: "Smileys & Emotion", + description: "face with spiral eyes", + unicode_version: "13.1", + }, + { + emoji: "🤯", + aliases: ["exploding_head"], + tags: ["mind", "blown"], + category: "Smileys & Emotion", + description: "exploding head", + unicode_version: "11.0", + }, + { + emoji: "🤠", + aliases: ["cowboy_hat_face"], + tags: [], + category: "Smileys & Emotion", + description: "cowboy hat face", + unicode_version: "9.0", + }, + { + emoji: "🥳", + aliases: ["partying_face"], + tags: ["celebration", "birthday"], + category: "Smileys & Emotion", + description: "partying face", + unicode_version: "11.0", + }, + { + emoji: "🥸", + aliases: ["disguised_face"], + tags: [], + category: "Smileys & Emotion", + description: "disguised face", + unicode_version: "13.0", + }, + { + emoji: "😎", + aliases: ["sunglasses"], + tags: ["cool"], + category: "Smileys & Emotion", + description: "smiling face with sunglasses", + unicode_version: "6.0", + }, + { + emoji: "🤓", + aliases: ["nerd_face"], + tags: ["geek", "glasses"], + category: "Smileys & Emotion", + description: "nerd face", + unicode_version: "8.0", + }, + { + emoji: "🧐", + aliases: ["monocle_face"], + tags: [], + category: "Smileys & Emotion", + description: "face with monocle", + unicode_version: "11.0", + }, + { + emoji: "😕", + aliases: ["confused"], + tags: [], + category: "Smileys & Emotion", + description: "confused face", + unicode_version: "6.1", + }, + { + emoji: "😟", + aliases: ["worried"], + tags: ["nervous"], + category: "Smileys & Emotion", + description: "worried face", + unicode_version: "6.1", + }, + { + emoji: "🙁", + aliases: ["slightly_frowning_face"], + tags: [], + category: "Smileys & Emotion", + description: "slightly frowning face", + unicode_version: "7.0", + }, + { + emoji: "☹️", + aliases: ["frowning_face"], + tags: [], + category: "Smileys & Emotion", + description: "frowning face", + unicode_version: "", + }, + { + emoji: "😮", + aliases: ["open_mouth"], + tags: ["surprise", "impressed", "wow"], + category: "Smileys & Emotion", + description: "face with open mouth", + unicode_version: "6.1", + }, + { + emoji: "😯", + aliases: ["hushed"], + tags: ["silence", "speechless"], + category: "Smileys & Emotion", + description: "hushed face", + unicode_version: "6.1", + }, + { + emoji: "😲", + aliases: ["astonished"], + tags: ["amazed", "gasp"], + category: "Smileys & Emotion", + description: "astonished face", + unicode_version: "6.0", + }, + { + emoji: "😳", + aliases: ["flushed"], + tags: [], + category: "Smileys & Emotion", + description: "flushed face", + unicode_version: "6.0", + }, + { + emoji: "🥺", + aliases: ["pleading_face"], + tags: ["puppy", "eyes"], + category: "Smileys & Emotion", + description: "pleading face", + unicode_version: "11.0", + }, + { + emoji: "😦", + aliases: ["frowning"], + tags: [], + category: "Smileys & Emotion", + description: "frowning face with open mouth", + unicode_version: "6.1", + }, + { + emoji: "😧", + aliases: ["anguished"], + tags: ["stunned"], + category: "Smileys & Emotion", + description: "anguished face", + unicode_version: "6.1", + }, + { + emoji: "😨", + aliases: ["fearful"], + tags: ["scared", "shocked", "oops"], + category: "Smileys & Emotion", + description: "fearful face", + unicode_version: "6.0", + }, + { + emoji: "😰", + aliases: ["cold_sweat"], + tags: ["nervous"], + category: "Smileys & Emotion", + description: "anxious face with sweat", + unicode_version: "6.0", + }, + { + emoji: "😥", + aliases: ["disappointed_relieved"], + tags: ["phew", "sweat", "nervous"], + category: "Smileys & Emotion", + description: "sad but relieved face", + unicode_version: "6.0", + }, + { + emoji: "😢", + aliases: ["cry"], + tags: ["sad", "tear"], + category: "Smileys & Emotion", + description: "crying face", + unicode_version: "6.0", + }, + { + emoji: "😭", + aliases: ["sob"], + tags: ["sad", "cry", "bawling"], + category: "Smileys & Emotion", + description: "loudly crying face", + unicode_version: "6.0", + }, + { + emoji: "😱", + aliases: ["scream"], + tags: ["horror", "shocked"], + category: "Smileys & Emotion", + description: "face screaming in fear", + unicode_version: "6.0", + }, + { + emoji: "😖", + aliases: ["confounded"], + tags: [], + category: "Smileys & Emotion", + description: "confounded face", + unicode_version: "6.0", + }, + { + emoji: "😣", + aliases: ["persevere"], + tags: ["struggling"], + category: "Smileys & Emotion", + description: "persevering face", + unicode_version: "6.0", + }, + { + emoji: "😞", + aliases: ["disappointed"], + tags: ["sad"], + category: "Smileys & Emotion", + description: "disappointed face", + unicode_version: "6.0", + }, + { + emoji: "😓", + aliases: ["sweat"], + tags: [], + category: "Smileys & Emotion", + description: "downcast face with sweat", + unicode_version: "6.0", + }, + { + emoji: "😩", + aliases: ["weary"], + tags: ["tired"], + category: "Smileys & Emotion", + description: "weary face", + unicode_version: "6.0", + }, + { + emoji: "😫", + aliases: ["tired_face"], + tags: ["upset", "whine"], + category: "Smileys & Emotion", + description: "tired face", + unicode_version: "6.0", + }, + { + emoji: "🥱", + aliases: ["yawning_face"], + tags: [], + category: "Smileys & Emotion", + description: "yawning face", + unicode_version: "12.0", + }, + { + emoji: "😤", + aliases: ["triumph"], + tags: ["smug"], + category: "Smileys & Emotion", + description: "face with steam from nose", + unicode_version: "6.0", + }, + { + emoji: "😡", + aliases: ["rage", "pout"], + tags: ["angry"], + category: "Smileys & Emotion", + description: "pouting face", + unicode_version: "6.0", + }, + { + emoji: "😠", + aliases: ["angry"], + tags: ["mad", "annoyed"], + category: "Smileys & Emotion", + description: "angry face", + unicode_version: "6.0", + }, + { + emoji: "🤬", + aliases: ["cursing_face"], + tags: ["foul"], + category: "Smileys & Emotion", + description: "face with symbols on mouth", + unicode_version: "11.0", + }, + { + emoji: "😈", + aliases: ["smiling_imp"], + tags: ["devil", "evil", "horns"], + category: "Smileys & Emotion", + description: "smiling face with horns", + unicode_version: "6.0", + }, + { + emoji: "👿", + aliases: ["imp"], + tags: ["angry", "devil", "evil", "horns"], + category: "Smileys & Emotion", + description: "angry face with horns", + unicode_version: "6.0", + }, + { + emoji: "💀", + aliases: ["skull"], + tags: ["dead", "danger", "poison"], + category: "Smileys & Emotion", + description: "skull", + unicode_version: "6.0", + }, + { + emoji: "☠️", + aliases: ["skull_and_crossbones"], + tags: ["danger", "pirate"], + category: "Smileys & Emotion", + description: "skull and crossbones", + unicode_version: "", + }, + { + emoji: "💩", + aliases: ["hankey", "poop", "shit"], + tags: ["crap"], + category: "Smileys & Emotion", + description: "pile of poo", + unicode_version: "6.0", + }, + { + emoji: "🤡", + aliases: ["clown_face"], + tags: [], + category: "Smileys & Emotion", + description: "clown face", + unicode_version: "9.0", + }, + { + emoji: "👹", + aliases: ["japanese_ogre"], + tags: ["monster"], + category: "Smileys & Emotion", + description: "ogre", + unicode_version: "6.0", + }, + { + emoji: "👺", + aliases: ["japanese_goblin"], + tags: [], + category: "Smileys & Emotion", + description: "goblin", + unicode_version: "6.0", + }, + { + emoji: "👻", + aliases: ["ghost"], + tags: ["halloween"], + category: "Smileys & Emotion", + description: "ghost", + unicode_version: "6.0", + }, + { + emoji: "👽", + aliases: ["alien"], + tags: ["ufo"], + category: "Smileys & Emotion", + description: "alien", + unicode_version: "6.0", + }, + { + emoji: "👾", + aliases: ["space_invader"], + tags: ["game", "retro"], + category: "Smileys & Emotion", + description: "alien monster", + unicode_version: "6.0", + }, + { + emoji: "🤖", + aliases: ["robot"], + tags: [], + category: "Smileys & Emotion", + description: "robot", + unicode_version: "8.0", + }, + { + emoji: "😺", + aliases: ["smiley_cat"], + tags: [], + category: "Smileys & Emotion", + description: "grinning cat", + unicode_version: "6.0", + }, + { + emoji: "😸", + aliases: ["smile_cat"], + tags: [], + category: "Smileys & Emotion", + description: "grinning cat with smiling eyes", + unicode_version: "6.0", + }, + { + emoji: "😹", + aliases: ["joy_cat"], + tags: [], + category: "Smileys & Emotion", + description: "cat with tears of joy", + unicode_version: "6.0", + }, + { + emoji: "😻", + aliases: ["heart_eyes_cat"], + tags: [], + category: "Smileys & Emotion", + description: "smiling cat with heart-eyes", + unicode_version: "6.0", + }, + { + emoji: "😼", + aliases: ["smirk_cat"], + tags: [], + category: "Smileys & Emotion", + description: "cat with wry smile", + unicode_version: "6.0", + }, + { + emoji: "😽", + aliases: ["kissing_cat"], + tags: [], + category: "Smileys & Emotion", + description: "kissing cat", + unicode_version: "6.0", + }, + { + emoji: "🙀", + aliases: ["scream_cat"], + tags: ["horror"], + category: "Smileys & Emotion", + description: "weary cat", + unicode_version: "6.0", + }, + { + emoji: "😿", + aliases: ["crying_cat_face"], + tags: ["sad", "tear"], + category: "Smileys & Emotion", + description: "crying cat", + unicode_version: "6.0", + }, + { + emoji: "😾", + aliases: ["pouting_cat"], + tags: [], + category: "Smileys & Emotion", + description: "pouting cat", + unicode_version: "6.0", + }, + { + emoji: "🙈", + aliases: ["see_no_evil"], + tags: ["monkey", "blind", "ignore"], + category: "Smileys & Emotion", + description: "see-no-evil monkey", + unicode_version: "6.0", + }, + { + emoji: "🙉", + aliases: ["hear_no_evil"], + tags: ["monkey", "deaf"], + category: "Smileys & Emotion", + description: "hear-no-evil monkey", + unicode_version: "6.0", + }, + { + emoji: "🙊", + aliases: ["speak_no_evil"], + tags: ["monkey", "mute", "hush"], + category: "Smileys & Emotion", + description: "speak-no-evil monkey", + unicode_version: "6.0", + }, + { + emoji: "💋", + aliases: ["kiss"], + tags: ["lipstick"], + category: "Smileys & Emotion", + description: "kiss mark", + unicode_version: "6.0", + }, + { + emoji: "💌", + aliases: ["love_letter"], + tags: ["email", "envelope"], + category: "Smileys & Emotion", + description: "love letter", + unicode_version: "6.0", + }, + { + emoji: "💘", + aliases: ["cupid"], + tags: ["love", "heart"], + category: "Smileys & Emotion", + description: "heart with arrow", + unicode_version: "6.0", + }, + { + emoji: "💝", + aliases: ["gift_heart"], + tags: ["chocolates"], + category: "Smileys & Emotion", + description: "heart with ribbon", + unicode_version: "6.0", + }, + { + emoji: "💖", + aliases: ["sparkling_heart"], + tags: [], + category: "Smileys & Emotion", + description: "sparkling heart", + unicode_version: "6.0", + }, + { + emoji: "💗", + aliases: ["heartpulse"], + tags: [], + category: "Smileys & Emotion", + description: "growing heart", + unicode_version: "6.0", + }, + { + emoji: "💓", + aliases: ["heartbeat"], + tags: [], + category: "Smileys & Emotion", + description: "beating heart", + unicode_version: "6.0", + }, + { + emoji: "💞", + aliases: ["revolving_hearts"], + tags: [], + category: "Smileys & Emotion", + description: "revolving hearts", + unicode_version: "6.0", + }, + { + emoji: "💕", + aliases: ["two_hearts"], + tags: [], + category: "Smileys & Emotion", + description: "two hearts", + unicode_version: "6.0", + }, + { + emoji: "💟", + aliases: ["heart_decoration"], + tags: [], + category: "Smileys & Emotion", + description: "heart decoration", + unicode_version: "6.0", + }, + { + emoji: "❣️", + aliases: ["heavy_heart_exclamation"], + tags: [], + category: "Smileys & Emotion", + description: "heart exclamation", + unicode_version: "", + }, + { + emoji: "💔", + aliases: ["broken_heart"], + tags: [], + category: "Smileys & Emotion", + description: "broken heart", + unicode_version: "6.0", + }, + { + emoji: "❤️‍🔥", + aliases: ["heart_on_fire"], + tags: [], + category: "Smileys & Emotion", + description: "heart on fire", + unicode_version: "13.1", + }, + { + emoji: "❤️‍🩹", + aliases: ["mending_heart"], + tags: [], + category: "Smileys & Emotion", + description: "mending heart", + unicode_version: "13.1", + }, + { + emoji: "❤️", + aliases: ["heart"], + tags: ["love"], + category: "Smileys & Emotion", + description: "red heart", + unicode_version: "", + }, + { + emoji: "🧡", + aliases: ["orange_heart"], + tags: [], + category: "Smileys & Emotion", + description: "orange heart", + unicode_version: "11.0", + }, + { + emoji: "💛", + aliases: ["yellow_heart"], + tags: [], + category: "Smileys & Emotion", + description: "yellow heart", + unicode_version: "6.0", + }, + { + emoji: "💚", + aliases: ["green_heart"], + tags: [], + category: "Smileys & Emotion", + description: "green heart", + unicode_version: "6.0", + }, + { + emoji: "💙", + aliases: ["blue_heart"], + tags: [], + category: "Smileys & Emotion", + description: "blue heart", + unicode_version: "6.0", + }, + { + emoji: "💜", + aliases: ["purple_heart"], + tags: [], + category: "Smileys & Emotion", + description: "purple heart", + unicode_version: "6.0", + }, + { + emoji: "🤎", + aliases: ["brown_heart"], + tags: [], + category: "Smileys & Emotion", + description: "brown heart", + unicode_version: "12.0", + }, + { + emoji: "🖤", + aliases: ["black_heart"], + tags: [], + category: "Smileys & Emotion", + description: "black heart", + unicode_version: "9.0", + }, + { + emoji: "🤍", + aliases: ["white_heart"], + tags: [], + category: "Smileys & Emotion", + description: "white heart", + unicode_version: "12.0", + }, + { + emoji: "💯", + aliases: ["100"], + tags: ["score", "perfect"], + category: "Smileys & Emotion", + description: "hundred points", + unicode_version: "6.0", + }, + { + emoji: "💢", + aliases: ["anger"], + tags: ["angry"], + category: "Smileys & Emotion", + description: "anger symbol", + unicode_version: "6.0", + }, + { + emoji: "💥", + aliases: ["boom", "collision"], + tags: ["explode"], + category: "Smileys & Emotion", + description: "collision", + unicode_version: "6.0", + }, + { + emoji: "💫", + aliases: ["dizzy"], + tags: ["star"], + category: "Smileys & Emotion", + description: "dizzy", + unicode_version: "6.0", + }, + { + emoji: "💦", + aliases: ["sweat_drops"], + tags: ["water", "workout"], + category: "Smileys & Emotion", + description: "sweat droplets", + unicode_version: "6.0", + }, + { + emoji: "💨", + aliases: ["dash"], + tags: ["wind", "blow", "fast"], + category: "Smileys & Emotion", + description: "dashing away", + unicode_version: "6.0", + }, + { + emoji: "🕳️", + aliases: ["hole"], + tags: [], + category: "Smileys & Emotion", + description: "hole", + unicode_version: "7.0", + }, + { + emoji: "💣", + aliases: ["bomb"], + tags: ["boom"], + category: "Smileys & Emotion", + description: "bomb", + unicode_version: "6.0", + }, + { + emoji: "💬", + aliases: ["speech_balloon"], + tags: ["comment"], + category: "Smileys & Emotion", + description: "speech balloon", + unicode_version: "6.0", + }, + { + emoji: "👁️‍🗨️", + aliases: ["eye_speech_bubble"], + tags: [], + category: "Smileys & Emotion", + description: "eye in speech bubble", + unicode_version: "11.0", + }, + { + emoji: "🗨️", + aliases: ["left_speech_bubble"], + tags: [], + category: "Smileys & Emotion", + description: "left speech bubble", + unicode_version: "11.0", + }, + { + emoji: "🗯️", + aliases: ["right_anger_bubble"], + tags: [], + category: "Smileys & Emotion", + description: "right anger bubble", + unicode_version: "7.0", + }, + { + emoji: "💭", + aliases: ["thought_balloon"], + tags: ["thinking"], + category: "Smileys & Emotion", + description: "thought balloon", + unicode_version: "6.0", + }, + { + emoji: "💤", + aliases: ["zzz"], + tags: ["sleeping"], + category: "Smileys & Emotion", + description: "zzz", + unicode_version: "6.0", + }, + { + emoji: "👋", + aliases: ["wave"], + tags: ["goodbye"], + category: "People & Body", + description: "waving hand", + unicode_version: "6.0", + }, + { + emoji: "🤚", + aliases: ["raised_back_of_hand"], + tags: [], + category: "People & Body", + description: "raised back of hand", + unicode_version: "9.0", + }, + { + emoji: "🖐️", + aliases: ["raised_hand_with_fingers_splayed"], + tags: [], + category: "People & Body", + description: "hand with fingers splayed", + unicode_version: "7.0", + }, + { + emoji: "✋", + aliases: ["hand", "raised_hand"], + tags: ["highfive", "stop"], + category: "People & Body", + description: "raised hand", + unicode_version: "6.0", + }, + { + emoji: "🖖", + aliases: ["vulcan_salute"], + tags: ["prosper", "spock"], + category: "People & Body", + description: "vulcan salute", + unicode_version: "7.0", + }, + { + emoji: "👌", + aliases: ["ok_hand"], + tags: [], + category: "People & Body", + description: "OK hand", + unicode_version: "6.0", + }, + { + emoji: "🤌", + aliases: ["pinched_fingers"], + tags: [], + category: "People & Body", + description: "pinched fingers", + unicode_version: "13.0", + }, + { + emoji: "🤏", + aliases: ["pinching_hand"], + tags: [], + category: "People & Body", + description: "pinching hand", + unicode_version: "12.0", + }, + { + emoji: "✌️", + aliases: ["v"], + tags: ["victory", "peace"], + category: "People & Body", + description: "victory hand", + unicode_version: "", + }, + { + emoji: "🤞", + aliases: ["crossed_fingers"], + tags: ["luck", "hopeful"], + category: "People & Body", + description: "crossed fingers", + unicode_version: "9.0", + }, + { + emoji: "🤟", + aliases: ["love_you_gesture"], + tags: [], + category: "People & Body", + description: "love-you gesture", + unicode_version: "11.0", + }, + { + emoji: "🤘", + aliases: ["metal"], + tags: [], + category: "People & Body", + description: "sign of the horns", + unicode_version: "8.0", + }, + { + emoji: "🤙", + aliases: ["call_me_hand"], + tags: [], + category: "People & Body", + description: "call me hand", + unicode_version: "9.0", + }, + { + emoji: "👈", + aliases: ["point_left"], + tags: [], + category: "People & Body", + description: "backhand index pointing left", + unicode_version: "6.0", + }, + { + emoji: "👉", + aliases: ["point_right"], + tags: [], + category: "People & Body", + description: "backhand index pointing right", + unicode_version: "6.0", + }, + { + emoji: "👆", + aliases: ["point_up_2"], + tags: [], + category: "People & Body", + description: "backhand index pointing up", + unicode_version: "6.0", + }, + { + emoji: "🖕", + aliases: ["middle_finger", "fu"], + tags: [], + category: "People & Body", + description: "middle finger", + unicode_version: "7.0", + }, + { + emoji: "👇", + aliases: ["point_down"], + tags: [], + category: "People & Body", + description: "backhand index pointing down", + unicode_version: "6.0", + }, + { + emoji: "☝️", + aliases: ["point_up"], + tags: [], + category: "People & Body", + description: "index pointing up", + unicode_version: "", + }, + { + emoji: "👍", + aliases: ["+1", "thumbsup"], + tags: ["approve", "ok"], + category: "People & Body", + description: "thumbs up", + unicode_version: "6.0", + }, + { + emoji: "👎", + aliases: ["-1", "thumbsdown"], + tags: ["disapprove", "bury"], + category: "People & Body", + description: "thumbs down", + unicode_version: "6.0", + }, + { + emoji: "✊", + aliases: ["fist_raised", "fist"], + tags: ["power"], + category: "People & Body", + description: "raised fist", + unicode_version: "6.0", + }, + { + emoji: "👊", + aliases: ["fist_oncoming", "facepunch", "punch"], + tags: ["attack"], + category: "People & Body", + description: "oncoming fist", + unicode_version: "6.0", + }, + { + emoji: "🤛", + aliases: ["fist_left"], + tags: [], + category: "People & Body", + description: "left-facing fist", + unicode_version: "9.0", + }, + { + emoji: "🤜", + aliases: ["fist_right"], + tags: [], + category: "People & Body", + description: "right-facing fist", + unicode_version: "9.0", + }, + { + emoji: "👏", + aliases: ["clap"], + tags: ["praise", "applause"], + category: "People & Body", + description: "clapping hands", + unicode_version: "6.0", + }, + { + emoji: "🙌", + aliases: ["raised_hands"], + tags: ["hooray"], + category: "People & Body", + description: "raising hands", + unicode_version: "6.0", + }, + { + emoji: "👐", + aliases: ["open_hands"], + tags: [], + category: "People & Body", + description: "open hands", + unicode_version: "6.0", + }, + { + emoji: "🤲", + aliases: ["palms_up_together"], + tags: [], + category: "People & Body", + description: "palms up together", + unicode_version: "11.0", + }, + { + emoji: "🤝", + aliases: ["handshake"], + tags: ["deal"], + category: "People & Body", + description: "handshake", + unicode_version: "9.0", + }, + { + emoji: "🙏", + aliases: ["pray"], + tags: ["please", "hope", "wish"], + category: "People & Body", + description: "folded hands", + unicode_version: "6.0", + }, + { + emoji: "✍️", + aliases: ["writing_hand"], + tags: [], + category: "People & Body", + description: "writing hand", + unicode_version: "", + }, + { + emoji: "💅", + aliases: ["nail_care"], + tags: ["beauty", "manicure"], + category: "People & Body", + description: "nail polish", + unicode_version: "6.0", + }, + { + emoji: "🤳", + aliases: ["selfie"], + tags: [], + category: "People & Body", + description: "selfie", + unicode_version: "9.0", + }, + { + emoji: "💪", + aliases: ["muscle"], + tags: ["flex", "bicep", "strong", "workout"], + category: "People & Body", + description: "flexed biceps", + unicode_version: "6.0", + }, + { + emoji: "🦾", + aliases: ["mechanical_arm"], + tags: [], + category: "People & Body", + description: "mechanical arm", + unicode_version: "12.0", + }, + { + emoji: "🦿", + aliases: ["mechanical_leg"], + tags: [], + category: "People & Body", + description: "mechanical leg", + unicode_version: "12.0", + }, + { + emoji: "🦵", + aliases: ["leg"], + tags: [], + category: "People & Body", + description: "leg", + unicode_version: "11.0", + }, + { + emoji: "🦶", + aliases: ["foot"], + tags: [], + category: "People & Body", + description: "foot", + unicode_version: "11.0", + }, + { + emoji: "👂", + aliases: ["ear"], + tags: ["hear", "sound", "listen"], + category: "People & Body", + description: "ear", + unicode_version: "6.0", + }, + { + emoji: "🦻", + aliases: ["ear_with_hearing_aid"], + tags: [], + category: "People & Body", + description: "ear with hearing aid", + unicode_version: "12.0", + }, + { + emoji: "👃", + aliases: ["nose"], + tags: ["smell"], + category: "People & Body", + description: "nose", + unicode_version: "6.0", + }, + { + emoji: "🧠", + aliases: ["brain"], + tags: [], + category: "People & Body", + description: "brain", + unicode_version: "11.0", + }, + { + emoji: "🫀", + aliases: ["anatomical_heart"], + tags: [], + category: "People & Body", + description: "anatomical heart", + unicode_version: "13.0", + }, + { + emoji: "🫁", + aliases: ["lungs"], + tags: [], + category: "People & Body", + description: "lungs", + unicode_version: "13.0", + }, + { + emoji: "🦷", + aliases: ["tooth"], + tags: [], + category: "People & Body", + description: "tooth", + unicode_version: "11.0", + }, + { + emoji: "🦴", + aliases: ["bone"], + tags: [], + category: "People & Body", + description: "bone", + unicode_version: "11.0", + }, + { + emoji: "👀", + aliases: ["eyes"], + tags: ["look", "see", "watch"], + category: "People & Body", + description: "eyes", + unicode_version: "6.0", + }, + { + emoji: "👁️", + aliases: ["eye"], + tags: [], + category: "People & Body", + description: "eye", + unicode_version: "7.0", + }, + { + emoji: "👅", + aliases: ["tongue"], + tags: ["taste"], + category: "People & Body", + description: "tongue", + unicode_version: "6.0", + }, + { + emoji: "👄", + aliases: ["lips"], + tags: ["kiss"], + category: "People & Body", + description: "mouth", + unicode_version: "6.0", + }, + { + emoji: "👶", + aliases: ["baby"], + tags: ["child", "newborn"], + category: "People & Body", + description: "baby", + unicode_version: "6.0", + }, + { + emoji: "🧒", + aliases: ["child"], + tags: [], + category: "People & Body", + description: "child", + unicode_version: "11.0", + }, + { + emoji: "👦", + aliases: ["boy"], + tags: ["child"], + category: "People & Body", + description: "boy", + unicode_version: "6.0", + }, + { + emoji: "👧", + aliases: ["girl"], + tags: ["child"], + category: "People & Body", + description: "girl", + unicode_version: "6.0", + }, + { + emoji: "🧑", + aliases: ["adult"], + tags: [], + category: "People & Body", + description: "person", + unicode_version: "11.0", + }, + { + emoji: "👱", + aliases: ["blond_haired_person"], + tags: [], + category: "People & Body", + description: "person: blond hair", + unicode_version: "6.0", + }, + { + emoji: "👨", + aliases: ["man"], + tags: ["mustache", "father", "dad"], + category: "People & Body", + description: "man", + unicode_version: "6.0", + }, + { + emoji: "🧔", + aliases: ["bearded_person"], + tags: [], + category: "People & Body", + description: "person: beard", + unicode_version: "11.0", + }, + { + emoji: "🧔‍♂️", + aliases: ["man_beard"], + tags: [], + category: "People & Body", + description: "man: beard", + unicode_version: "13.1", + }, + { + emoji: "🧔‍♀️", + aliases: ["woman_beard"], + tags: [], + category: "People & Body", + description: "woman: beard", + unicode_version: "13.1", + }, + { + emoji: "👨‍🦰", + aliases: ["red_haired_man"], + tags: [], + category: "People & Body", + description: "man: red hair", + unicode_version: "11.0", + }, + { + emoji: "👨‍🦱", + aliases: ["curly_haired_man"], + tags: [], + category: "People & Body", + description: "man: curly hair", + unicode_version: "11.0", + }, + { + emoji: "👨‍🦳", + aliases: ["white_haired_man"], + tags: [], + category: "People & Body", + description: "man: white hair", + unicode_version: "11.0", + }, + { + emoji: "👨‍🦲", + aliases: ["bald_man"], + tags: [], + category: "People & Body", + description: "man: bald", + unicode_version: "11.0", + }, + { + emoji: "👩", + aliases: ["woman"], + tags: ["girls"], + category: "People & Body", + description: "woman", + unicode_version: "6.0", + }, + { + emoji: "👩‍🦰", + aliases: ["red_haired_woman"], + tags: [], + category: "People & Body", + description: "woman: red hair", + unicode_version: "11.0", + }, + { + emoji: "🧑‍🦰", + aliases: ["person_red_hair"], + tags: [], + category: "People & Body", + description: "person: red hair", + unicode_version: "12.1", + }, + { + emoji: "👩‍🦱", + aliases: ["curly_haired_woman"], + tags: [], + category: "People & Body", + description: "woman: curly hair", + unicode_version: "11.0", + }, + { + emoji: "🧑‍🦱", + aliases: ["person_curly_hair"], + tags: [], + category: "People & Body", + description: "person: curly hair", + unicode_version: "12.1", + }, + { + emoji: "👩‍🦳", + aliases: ["white_haired_woman"], + tags: [], + category: "People & Body", + description: "woman: white hair", + unicode_version: "11.0", + }, + { + emoji: "🧑‍🦳", + aliases: ["person_white_hair"], + tags: [], + category: "People & Body", + description: "person: white hair", + unicode_version: "12.1", + }, + { + emoji: "👩‍🦲", + aliases: ["bald_woman"], + tags: [], + category: "People & Body", + description: "woman: bald", + unicode_version: "11.0", + }, + { + emoji: "🧑‍🦲", + aliases: ["person_bald"], + tags: [], + category: "People & Body", + description: "person: bald", + unicode_version: "12.1", + }, + { + emoji: "👱‍♀️", + aliases: ["blond_haired_woman", "blonde_woman"], + tags: [], + category: "People & Body", + description: "woman: blond hair", + unicode_version: "6.0", + }, + { + emoji: "👱‍♂️", + aliases: ["blond_haired_man"], + tags: [], + category: "People & Body", + description: "man: blond hair", + unicode_version: "11.0", + }, + { + emoji: "🧓", + aliases: ["older_adult"], + tags: [], + category: "People & Body", + description: "older person", + unicode_version: "11.0", + }, + { + emoji: "👴", + aliases: ["older_man"], + tags: [], + category: "People & Body", + description: "old man", + unicode_version: "6.0", + }, + { + emoji: "👵", + aliases: ["older_woman"], + tags: [], + category: "People & Body", + description: "old woman", + unicode_version: "6.0", + }, + { + emoji: "🙍", + aliases: ["frowning_person"], + tags: [], + category: "People & Body", + description: "person frowning", + unicode_version: "6.0", + }, + { + emoji: "🙍‍♂️", + aliases: ["frowning_man"], + tags: [], + category: "People & Body", + description: "man frowning", + unicode_version: "6.0", + }, + { + emoji: "🙍‍♀️", + aliases: ["frowning_woman"], + tags: [], + category: "People & Body", + description: "woman frowning", + unicode_version: "11.0", + }, + { + emoji: "🙎", + aliases: ["pouting_face"], + tags: [], + category: "People & Body", + description: "person pouting", + unicode_version: "6.0", + }, + { + emoji: "🙎‍♂️", + aliases: ["pouting_man"], + tags: [], + category: "People & Body", + description: "man pouting", + unicode_version: "6.0", + }, + { + emoji: "🙎‍♀️", + aliases: ["pouting_woman"], + tags: [], + category: "People & Body", + description: "woman pouting", + unicode_version: "11.0", + }, + { + emoji: "🙅", + aliases: ["no_good"], + tags: ["stop", "halt", "denied"], + category: "People & Body", + description: "person gesturing NO", + unicode_version: "6.0", + }, + { + emoji: "🙅‍♂️", + aliases: ["no_good_man", "ng_man"], + tags: ["stop", "halt", "denied"], + category: "People & Body", + description: "man gesturing NO", + unicode_version: "6.0", + }, + { + emoji: "🙅‍♀️", + aliases: ["no_good_woman", "ng_woman"], + tags: ["stop", "halt", "denied"], + category: "People & Body", + description: "woman gesturing NO", + unicode_version: "11.0", + }, + { + emoji: "🙆", + aliases: ["ok_person"], + tags: [], + category: "People & Body", + description: "person gesturing OK", + unicode_version: "6.0", + }, + { + emoji: "🙆‍♂️", + aliases: ["ok_man"], + tags: [], + category: "People & Body", + description: "man gesturing OK", + unicode_version: "6.0", + }, + { + emoji: "🙆‍♀️", + aliases: ["ok_woman"], + tags: [], + category: "People & Body", + description: "woman gesturing OK", + unicode_version: "11.0", + }, + { + emoji: "💁", + aliases: ["tipping_hand_person", "information_desk_person"], + tags: [], + category: "People & Body", + description: "person tipping hand", + unicode_version: "6.0", + }, + { + emoji: "💁‍♂️", + aliases: ["tipping_hand_man", "sassy_man"], + tags: ["information"], + category: "People & Body", + description: "man tipping hand", + unicode_version: "6.0", + }, + { + emoji: "💁‍♀️", + aliases: ["tipping_hand_woman", "sassy_woman"], + tags: ["information"], + category: "People & Body", + description: "woman tipping hand", + unicode_version: "11.0", + }, + { + emoji: "🙋", + aliases: ["raising_hand"], + tags: [], + category: "People & Body", + description: "person raising hand", + unicode_version: "6.0", + }, + { + emoji: "🙋‍♂️", + aliases: ["raising_hand_man"], + tags: [], + category: "People & Body", + description: "man raising hand", + unicode_version: "6.0", + }, + { + emoji: "🙋‍♀️", + aliases: ["raising_hand_woman"], + tags: [], + category: "People & Body", + description: "woman raising hand", + unicode_version: "11.0", + }, + { + emoji: "🧏", + aliases: ["deaf_person"], + tags: [], + category: "People & Body", + description: "deaf person", + unicode_version: "12.0", + }, + { + emoji: "🧏‍♂️", + aliases: ["deaf_man"], + tags: [], + category: "People & Body", + description: "deaf man", + unicode_version: "12.0", + }, + { + emoji: "🧏‍♀️", + aliases: ["deaf_woman"], + tags: [], + category: "People & Body", + description: "deaf woman", + unicode_version: "12.0", + }, + { + emoji: "🙇", + aliases: ["bow"], + tags: ["respect", "thanks"], + category: "People & Body", + description: "person bowing", + unicode_version: "6.0", + }, + { + emoji: "🙇‍♂️", + aliases: ["bowing_man"], + tags: ["respect", "thanks"], + category: "People & Body", + description: "man bowing", + unicode_version: "11.0", + }, + { + emoji: "🙇‍♀️", + aliases: ["bowing_woman"], + tags: ["respect", "thanks"], + category: "People & Body", + description: "woman bowing", + unicode_version: "6.0", + }, + { + emoji: "🤦", + aliases: ["facepalm"], + tags: [], + category: "People & Body", + description: "person facepalming", + unicode_version: "11.0", + }, + { + emoji: "🤦‍♂️", + aliases: ["man_facepalming"], + tags: [], + category: "People & Body", + description: "man facepalming", + unicode_version: "9.0", + }, + { + emoji: "🤦‍♀️", + aliases: ["woman_facepalming"], + tags: [], + category: "People & Body", + description: "woman facepalming", + unicode_version: "9.0", + }, + { + emoji: "🤷", + aliases: ["shrug"], + tags: [], + category: "People & Body", + description: "person shrugging", + unicode_version: "11.0", + }, + { + emoji: "🤷‍♂️", + aliases: ["man_shrugging"], + tags: [], + category: "People & Body", + description: "man shrugging", + unicode_version: "9.0", + }, + { + emoji: "🤷‍♀️", + aliases: ["woman_shrugging"], + tags: [], + category: "People & Body", + description: "woman shrugging", + unicode_version: "9.0", + }, + { + emoji: "🧑‍⚕️", + aliases: ["health_worker"], + tags: [], + category: "People & Body", + description: "health worker", + unicode_version: "12.1", + }, + { + emoji: "👨‍⚕️", + aliases: ["man_health_worker"], + tags: ["doctor", "nurse"], + category: "People & Body", + description: "man health worker", + unicode_version: "", + }, + { + emoji: "👩‍⚕️", + aliases: ["woman_health_worker"], + tags: ["doctor", "nurse"], + category: "People & Body", + description: "woman health worker", + unicode_version: "", + }, + { + emoji: "🧑‍🎓", + aliases: ["student"], + tags: [], + category: "People & Body", + description: "student", + unicode_version: "12.1", + }, + { + emoji: "👨‍🎓", + aliases: ["man_student"], + tags: ["graduation"], + category: "People & Body", + description: "man student", + unicode_version: "", + }, + { + emoji: "👩‍🎓", + aliases: ["woman_student"], + tags: ["graduation"], + category: "People & Body", + description: "woman student", + unicode_version: "", + }, + { + emoji: "🧑‍🏫", + aliases: ["teacher"], + tags: [], + category: "People & Body", + description: "teacher", + unicode_version: "12.1", + }, + { + emoji: "👨‍🏫", + aliases: ["man_teacher"], + tags: ["school", "professor"], + category: "People & Body", + description: "man teacher", + unicode_version: "", + }, + { + emoji: "👩‍🏫", + aliases: ["woman_teacher"], + tags: ["school", "professor"], + category: "People & Body", + description: "woman teacher", + unicode_version: "", + }, + { + emoji: "🧑‍⚖️", + aliases: ["judge"], + tags: [], + category: "People & Body", + description: "judge", + unicode_version: "12.1", + }, + { + emoji: "👨‍⚖️", + aliases: ["man_judge"], + tags: ["justice"], + category: "People & Body", + description: "man judge", + unicode_version: "", + }, + { + emoji: "👩‍⚖️", + aliases: ["woman_judge"], + tags: ["justice"], + category: "People & Body", + description: "woman judge", + unicode_version: "", + }, + { + emoji: "🧑‍🌾", + aliases: ["farmer"], + tags: [], + category: "People & Body", + description: "farmer", + unicode_version: "12.1", + }, + { + emoji: "👨‍🌾", + aliases: ["man_farmer"], + tags: [], + category: "People & Body", + description: "man farmer", + unicode_version: "", + }, + { + emoji: "👩‍🌾", + aliases: ["woman_farmer"], + tags: [], + category: "People & Body", + description: "woman farmer", + unicode_version: "", + }, + { + emoji: "🧑‍🍳", + aliases: ["cook"], + tags: [], + category: "People & Body", + description: "cook", + unicode_version: "12.1", + }, + { + emoji: "👨‍🍳", + aliases: ["man_cook"], + tags: ["chef"], + category: "People & Body", + description: "man cook", + unicode_version: "", + }, + { + emoji: "👩‍🍳", + aliases: ["woman_cook"], + tags: ["chef"], + category: "People & Body", + description: "woman cook", + unicode_version: "", + }, + { + emoji: "🧑‍🔧", + aliases: ["mechanic"], + tags: [], + category: "People & Body", + description: "mechanic", + unicode_version: "12.1", + }, + { + emoji: "👨‍🔧", + aliases: ["man_mechanic"], + tags: [], + category: "People & Body", + description: "man mechanic", + unicode_version: "", + }, + { + emoji: "👩‍🔧", + aliases: ["woman_mechanic"], + tags: [], + category: "People & Body", + description: "woman mechanic", + unicode_version: "", + }, + { + emoji: "🧑‍🏭", + aliases: ["factory_worker"], + tags: [], + category: "People & Body", + description: "factory worker", + unicode_version: "12.1", + }, + { + emoji: "👨‍🏭", + aliases: ["man_factory_worker"], + tags: [], + category: "People & Body", + description: "man factory worker", + unicode_version: "", + }, + { + emoji: "👩‍🏭", + aliases: ["woman_factory_worker"], + tags: [], + category: "People & Body", + description: "woman factory worker", + unicode_version: "", + }, + { + emoji: "🧑‍💼", + aliases: ["office_worker"], + tags: [], + category: "People & Body", + description: "office worker", + unicode_version: "12.1", + }, + { + emoji: "👨‍💼", + aliases: ["man_office_worker"], + tags: ["business"], + category: "People & Body", + description: "man office worker", + unicode_version: "", + }, + { + emoji: "👩‍💼", + aliases: ["woman_office_worker"], + tags: ["business"], + category: "People & Body", + description: "woman office worker", + unicode_version: "", + }, + { + emoji: "🧑‍🔬", + aliases: ["scientist"], + tags: [], + category: "People & Body", + description: "scientist", + unicode_version: "12.1", + }, + { + emoji: "👨‍🔬", + aliases: ["man_scientist"], + tags: ["research"], + category: "People & Body", + description: "man scientist", + unicode_version: "", + }, + { + emoji: "👩‍🔬", + aliases: ["woman_scientist"], + tags: ["research"], + category: "People & Body", + description: "woman scientist", + unicode_version: "", + }, + { + emoji: "🧑‍💻", + aliases: ["technologist"], + tags: [], + category: "People & Body", + description: "technologist", + unicode_version: "12.1", + }, + { + emoji: "👨‍💻", + aliases: ["man_technologist"], + tags: ["coder"], + category: "People & Body", + description: "man technologist", + unicode_version: "", + }, + { + emoji: "👩‍💻", + aliases: ["woman_technologist"], + tags: ["coder"], + category: "People & Body", + description: "woman technologist", + unicode_version: "", + }, + { + emoji: "🧑‍🎤", + aliases: ["singer"], + tags: [], + category: "People & Body", + description: "singer", + unicode_version: "12.1", + }, + { + emoji: "👨‍🎤", + aliases: ["man_singer"], + tags: ["rockstar"], + category: "People & Body", + description: "man singer", + unicode_version: "", + }, + { + emoji: "👩‍🎤", + aliases: ["woman_singer"], + tags: ["rockstar"], + category: "People & Body", + description: "woman singer", + unicode_version: "", + }, + { + emoji: "🧑‍🎨", + aliases: ["artist"], + tags: [], + category: "People & Body", + description: "artist", + unicode_version: "12.1", + }, + { + emoji: "👨‍🎨", + aliases: ["man_artist"], + tags: ["painter"], + category: "People & Body", + description: "man artist", + unicode_version: "", + }, + { + emoji: "👩‍🎨", + aliases: ["woman_artist"], + tags: ["painter"], + category: "People & Body", + description: "woman artist", + unicode_version: "", + }, + { + emoji: "🧑‍✈️", + aliases: ["pilot"], + tags: [], + category: "People & Body", + description: "pilot", + unicode_version: "12.1", + }, + { + emoji: "👨‍✈️", + aliases: ["man_pilot"], + tags: [], + category: "People & Body", + description: "man pilot", + unicode_version: "", + }, + { + emoji: "👩‍✈️", + aliases: ["woman_pilot"], + tags: [], + category: "People & Body", + description: "woman pilot", + unicode_version: "", + }, + { + emoji: "🧑‍🚀", + aliases: ["astronaut"], + tags: [], + category: "People & Body", + description: "astronaut", + unicode_version: "12.1", + }, + { + emoji: "👨‍🚀", + aliases: ["man_astronaut"], + tags: ["space"], + category: "People & Body", + description: "man astronaut", + unicode_version: "", + }, + { + emoji: "👩‍🚀", + aliases: ["woman_astronaut"], + tags: ["space"], + category: "People & Body", + description: "woman astronaut", + unicode_version: "", + }, + { + emoji: "🧑‍🚒", + aliases: ["firefighter"], + tags: [], + category: "People & Body", + description: "firefighter", + unicode_version: "12.1", + }, + { + emoji: "👨‍🚒", + aliases: ["man_firefighter"], + tags: [], + category: "People & Body", + description: "man firefighter", + unicode_version: "", + }, + { + emoji: "👩‍🚒", + aliases: ["woman_firefighter"], + tags: [], + category: "People & Body", + description: "woman firefighter", + unicode_version: "", + }, + { + emoji: "👮", + aliases: ["police_officer", "cop"], + tags: ["law"], + category: "People & Body", + description: "police officer", + unicode_version: "6.0", + }, + { + emoji: "👮‍♂️", + aliases: ["policeman"], + tags: ["law", "cop"], + category: "People & Body", + description: "man police officer", + unicode_version: "11.0", + }, + { + emoji: "👮‍♀️", + aliases: ["policewoman"], + tags: ["law", "cop"], + category: "People & Body", + description: "woman police officer", + unicode_version: "6.0", + }, + { + emoji: "🕵️", + aliases: ["detective"], + tags: ["sleuth"], + category: "People & Body", + description: "detective", + unicode_version: "7.0", + }, + { + emoji: "🕵️‍♂️", + aliases: ["male_detective"], + tags: ["sleuth"], + category: "People & Body", + description: "man detective", + unicode_version: "11.0", + }, + { + emoji: "🕵️‍♀️", + aliases: ["female_detective"], + tags: ["sleuth"], + category: "People & Body", + description: "woman detective", + unicode_version: "6.0", + }, + { + emoji: "💂", + aliases: ["guard"], + tags: [], + category: "People & Body", + description: "guard", + unicode_version: "6.0", + }, + { + emoji: "💂‍♂️", + aliases: ["guardsman"], + tags: [], + category: "People & Body", + description: "man guard", + unicode_version: "11.0", + }, + { + emoji: "💂‍♀️", + aliases: ["guardswoman"], + tags: [], + category: "People & Body", + description: "woman guard", + unicode_version: "6.0", + }, + { + emoji: "🥷", + aliases: ["ninja"], + tags: [], + category: "People & Body", + description: "ninja", + unicode_version: "13.0", + }, + { + emoji: "👷", + aliases: ["construction_worker"], + tags: ["helmet"], + category: "People & Body", + description: "construction worker", + unicode_version: "6.0", + }, + { + emoji: "👷‍♂️", + aliases: ["construction_worker_man"], + tags: ["helmet"], + category: "People & Body", + description: "man construction worker", + unicode_version: "11.0", + }, + { + emoji: "👷‍♀️", + aliases: ["construction_worker_woman"], + tags: ["helmet"], + category: "People & Body", + description: "woman construction worker", + unicode_version: "6.0", + }, + { + emoji: "🤴", + aliases: ["prince"], + tags: ["crown", "royal"], + category: "People & Body", + description: "prince", + unicode_version: "9.0", + }, + { + emoji: "👸", + aliases: ["princess"], + tags: ["crown", "royal"], + category: "People & Body", + description: "princess", + unicode_version: "6.0", + }, + { + emoji: "👳", + aliases: ["person_with_turban"], + tags: [], + category: "People & Body", + description: "person wearing turban", + unicode_version: "6.0", + }, + { + emoji: "👳‍♂️", + aliases: ["man_with_turban"], + tags: [], + category: "People & Body", + description: "man wearing turban", + unicode_version: "11.0", + }, + { + emoji: "👳‍♀️", + aliases: ["woman_with_turban"], + tags: [], + category: "People & Body", + description: "woman wearing turban", + unicode_version: "6.0", + }, + { + emoji: "👲", + aliases: ["man_with_gua_pi_mao"], + tags: [], + category: "People & Body", + description: "person with skullcap", + unicode_version: "6.0", + }, + { + emoji: "🧕", + aliases: ["woman_with_headscarf"], + tags: ["hijab"], + category: "People & Body", + description: "woman with headscarf", + unicode_version: "11.0", + }, + { + emoji: "🤵", + aliases: ["person_in_tuxedo"], + tags: ["groom", "marriage", "wedding"], + category: "People & Body", + description: "person in tuxedo", + unicode_version: "9.0", + }, + { + emoji: "🤵‍♂️", + aliases: ["man_in_tuxedo"], + tags: [], + category: "People & Body", + description: "man in tuxedo", + unicode_version: "13.0", + }, + { + emoji: "🤵‍♀️", + aliases: ["woman_in_tuxedo"], + tags: [], + category: "People & Body", + description: "woman in tuxedo", + unicode_version: "13.0", + }, + { + emoji: "👰", + aliases: ["person_with_veil"], + tags: ["marriage", "wedding"], + category: "People & Body", + description: "person with veil", + unicode_version: "6.0", + }, + { + emoji: "👰‍♂️", + aliases: ["man_with_veil"], + tags: [], + category: "People & Body", + description: "man with veil", + unicode_version: "13.0", + }, + { + emoji: "👰‍♀️", + aliases: ["woman_with_veil", "bride_with_veil"], + tags: [], + category: "People & Body", + description: "woman with veil", + unicode_version: "13.0", + }, + { + emoji: "🤰", + aliases: ["pregnant_woman"], + tags: [], + category: "People & Body", + description: "pregnant woman", + unicode_version: "9.0", + }, + { + emoji: "🤱", + aliases: ["breast_feeding"], + tags: ["nursing"], + category: "People & Body", + description: "breast-feeding", + unicode_version: "11.0", + }, + { + emoji: "👩‍🍼", + aliases: ["woman_feeding_baby"], + tags: [], + category: "People & Body", + description: "woman feeding baby", + unicode_version: "13.0", + }, + { + emoji: "👨‍🍼", + aliases: ["man_feeding_baby"], + tags: [], + category: "People & Body", + description: "man feeding baby", + unicode_version: "13.0", + }, + { + emoji: "🧑‍🍼", + aliases: ["person_feeding_baby"], + tags: [], + category: "People & Body", + description: "person feeding baby", + unicode_version: "13.0", + }, + { + emoji: "👼", + aliases: ["angel"], + tags: [], + category: "People & Body", + description: "baby angel", + unicode_version: "6.0", + }, + { + emoji: "🎅", + aliases: ["santa"], + tags: ["christmas"], + category: "People & Body", + description: "Santa Claus", + unicode_version: "6.0", + }, + { + emoji: "🤶", + aliases: ["mrs_claus"], + tags: ["santa"], + category: "People & Body", + description: "Mrs. Claus", + unicode_version: "9.0", + }, + { + emoji: "🧑‍🎄", + aliases: ["mx_claus"], + tags: [], + category: "People & Body", + description: "mx claus", + unicode_version: "13.0", + }, + { + emoji: "🦸", + aliases: ["superhero"], + tags: [], + category: "People & Body", + description: "superhero", + unicode_version: "11.0", + }, + { + emoji: "🦸‍♂️", + aliases: ["superhero_man"], + tags: [], + category: "People & Body", + description: "man superhero", + unicode_version: "11.0", + }, + { + emoji: "🦸‍♀️", + aliases: ["superhero_woman"], + tags: [], + category: "People & Body", + description: "woman superhero", + unicode_version: "11.0", + }, + { + emoji: "🦹", + aliases: ["supervillain"], + tags: [], + category: "People & Body", + description: "supervillain", + unicode_version: "11.0", + }, + { + emoji: "🦹‍♂️", + aliases: ["supervillain_man"], + tags: [], + category: "People & Body", + description: "man supervillain", + unicode_version: "11.0", + }, + { + emoji: "🦹‍♀️", + aliases: ["supervillain_woman"], + tags: [], + category: "People & Body", + description: "woman supervillain", + unicode_version: "11.0", + }, + { + emoji: "🧙", + aliases: ["mage"], + tags: ["wizard"], + category: "People & Body", + description: "mage", + unicode_version: "11.0", + }, + { + emoji: "🧙‍♂️", + aliases: ["mage_man"], + tags: ["wizard"], + category: "People & Body", + description: "man mage", + unicode_version: "11.0", + }, + { + emoji: "🧙‍♀️", + aliases: ["mage_woman"], + tags: ["wizard"], + category: "People & Body", + description: "woman mage", + unicode_version: "11.0", + }, + { + emoji: "🧚", + aliases: ["fairy"], + tags: [], + category: "People & Body", + description: "fairy", + unicode_version: "11.0", + }, + { + emoji: "🧚‍♂️", + aliases: ["fairy_man"], + tags: [], + category: "People & Body", + description: "man fairy", + unicode_version: "11.0", + }, + { + emoji: "🧚‍♀️", + aliases: ["fairy_woman"], + tags: [], + category: "People & Body", + description: "woman fairy", + unicode_version: "11.0", + }, + { + emoji: "🧛", + aliases: ["vampire"], + tags: [], + category: "People & Body", + description: "vampire", + unicode_version: "11.0", + }, + { + emoji: "🧛‍♂️", + aliases: ["vampire_man"], + tags: [], + category: "People & Body", + description: "man vampire", + unicode_version: "11.0", + }, + { + emoji: "🧛‍♀️", + aliases: ["vampire_woman"], + tags: [], + category: "People & Body", + description: "woman vampire", + unicode_version: "11.0", + }, + { + emoji: "🧜", + aliases: ["merperson"], + tags: [], + category: "People & Body", + description: "merperson", + unicode_version: "11.0", + }, + { + emoji: "🧜‍♂️", + aliases: ["merman"], + tags: [], + category: "People & Body", + description: "merman", + unicode_version: "11.0", + }, + { + emoji: "🧜‍♀️", + aliases: ["mermaid"], + tags: [], + category: "People & Body", + description: "mermaid", + unicode_version: "11.0", + }, + { + emoji: "🧝", + aliases: ["elf"], + tags: [], + category: "People & Body", + description: "elf", + unicode_version: "11.0", + }, + { + emoji: "🧝‍♂️", + aliases: ["elf_man"], + tags: [], + category: "People & Body", + description: "man elf", + unicode_version: "11.0", + }, + { + emoji: "🧝‍♀️", + aliases: ["elf_woman"], + tags: [], + category: "People & Body", + description: "woman elf", + unicode_version: "11.0", + }, + { + emoji: "🧞", + aliases: ["genie"], + tags: [], + category: "People & Body", + description: "genie", + unicode_version: "11.0", + }, + { + emoji: "🧞‍♂️", + aliases: ["genie_man"], + tags: [], + category: "People & Body", + description: "man genie", + unicode_version: "11.0", + }, + { + emoji: "🧞‍♀️", + aliases: ["genie_woman"], + tags: [], + category: "People & Body", + description: "woman genie", + unicode_version: "11.0", + }, + { + emoji: "🧟", + aliases: ["zombie"], + tags: [], + category: "People & Body", + description: "zombie", + unicode_version: "11.0", + }, + { + emoji: "🧟‍♂️", + aliases: ["zombie_man"], + tags: [], + category: "People & Body", + description: "man zombie", + unicode_version: "11.0", + }, + { + emoji: "🧟‍♀️", + aliases: ["zombie_woman"], + tags: [], + category: "People & Body", + description: "woman zombie", + unicode_version: "11.0", + }, + { + emoji: "💆", + aliases: ["massage"], + tags: ["spa"], + category: "People & Body", + description: "person getting massage", + unicode_version: "6.0", + }, + { + emoji: "💆‍♂️", + aliases: ["massage_man"], + tags: ["spa"], + category: "People & Body", + description: "man getting massage", + unicode_version: "6.0", + }, + { + emoji: "💆‍♀️", + aliases: ["massage_woman"], + tags: ["spa"], + category: "People & Body", + description: "woman getting massage", + unicode_version: "11.0", + }, + { + emoji: "💇", + aliases: ["haircut"], + tags: ["beauty"], + category: "People & Body", + description: "person getting haircut", + unicode_version: "6.0", + }, + { + emoji: "💇‍♂️", + aliases: ["haircut_man"], + tags: [], + category: "People & Body", + description: "man getting haircut", + unicode_version: "6.0", + }, + { + emoji: "💇‍♀️", + aliases: ["haircut_woman"], + tags: [], + category: "People & Body", + description: "woman getting haircut", + unicode_version: "11.0", + }, + { + emoji: "🚶", + aliases: ["walking"], + tags: [], + category: "People & Body", + description: "person walking", + unicode_version: "6.0", + }, + { + emoji: "🚶‍♂️", + aliases: ["walking_man"], + tags: [], + category: "People & Body", + description: "man walking", + unicode_version: "11.0", + }, + { + emoji: "🚶‍♀️", + aliases: ["walking_woman"], + tags: [], + category: "People & Body", + description: "woman walking", + unicode_version: "6.0", + }, + { + emoji: "🧍", + aliases: ["standing_person"], + tags: [], + category: "People & Body", + description: "person standing", + unicode_version: "12.0", + }, + { + emoji: "🧍‍♂️", + aliases: ["standing_man"], + tags: [], + category: "People & Body", + description: "man standing", + unicode_version: "12.0", + }, + { + emoji: "🧍‍♀️", + aliases: ["standing_woman"], + tags: [], + category: "People & Body", + description: "woman standing", + unicode_version: "12.0", + }, + { + emoji: "🧎", + aliases: ["kneeling_person"], + tags: [], + category: "People & Body", + description: "person kneeling", + unicode_version: "12.0", + }, + { + emoji: "🧎‍♂️", + aliases: ["kneeling_man"], + tags: [], + category: "People & Body", + description: "man kneeling", + unicode_version: "12.0", + }, + { + emoji: "🧎‍♀️", + aliases: ["kneeling_woman"], + tags: [], + category: "People & Body", + description: "woman kneeling", + unicode_version: "12.0", + }, + { + emoji: "🧑‍🦯", + aliases: ["person_with_probing_cane"], + tags: [], + category: "People & Body", + description: "person with white cane", + unicode_version: "12.1", + }, + { + emoji: "👨‍🦯", + aliases: ["man_with_probing_cane"], + tags: [], + category: "People & Body", + description: "man with white cane", + unicode_version: "12.0", + }, + { + emoji: "👩‍🦯", + aliases: ["woman_with_probing_cane"], + tags: [], + category: "People & Body", + description: "woman with white cane", + unicode_version: "12.0", + }, + { + emoji: "🧑‍🦼", + aliases: ["person_in_motorized_wheelchair"], + tags: [], + category: "People & Body", + description: "person in motorized wheelchair", + unicode_version: "12.1", + }, + { + emoji: "👨‍🦼", + aliases: ["man_in_motorized_wheelchair"], + tags: [], + category: "People & Body", + description: "man in motorized wheelchair", + unicode_version: "12.0", + }, + { + emoji: "👩‍🦼", + aliases: ["woman_in_motorized_wheelchair"], + tags: [], + category: "People & Body", + description: "woman in motorized wheelchair", + unicode_version: "12.0", + }, + { + emoji: "🧑‍🦽", + aliases: ["person_in_manual_wheelchair"], + tags: [], + category: "People & Body", + description: "person in manual wheelchair", + unicode_version: "12.1", + }, + { + emoji: "👨‍🦽", + aliases: ["man_in_manual_wheelchair"], + tags: [], + category: "People & Body", + description: "man in manual wheelchair", + unicode_version: "12.0", + }, + { + emoji: "👩‍🦽", + aliases: ["woman_in_manual_wheelchair"], + tags: [], + category: "People & Body", + description: "woman in manual wheelchair", + unicode_version: "12.0", + }, + { + emoji: "🏃", + aliases: ["runner", "running"], + tags: ["exercise", "workout", "marathon"], + category: "People & Body", + description: "person running", + unicode_version: "6.0", + }, + { + emoji: "🏃‍♂️", + aliases: ["running_man"], + tags: ["exercise", "workout", "marathon"], + category: "People & Body", + description: "man running", + unicode_version: "11.0", + }, + { + emoji: "🏃‍♀️", + aliases: ["running_woman"], + tags: ["exercise", "workout", "marathon"], + category: "People & Body", + description: "woman running", + unicode_version: "6.0", + }, + { + emoji: "💃", + aliases: ["woman_dancing", "dancer"], + tags: ["dress"], + category: "People & Body", + description: "woman dancing", + unicode_version: "6.0", + }, + { + emoji: "🕺", + aliases: ["man_dancing"], + tags: ["dancer"], + category: "People & Body", + description: "man dancing", + unicode_version: "9.0", + }, + { + emoji: "🕴️", + aliases: ["business_suit_levitating"], + tags: [], + category: "People & Body", + description: "person in suit levitating", + unicode_version: "7.0", + }, + { + emoji: "👯", + aliases: ["dancers"], + tags: ["bunny"], + category: "People & Body", + description: "people with bunny ears", + unicode_version: "6.0", + }, + { + emoji: "👯‍♂️", + aliases: ["dancing_men"], + tags: ["bunny"], + category: "People & Body", + description: "men with bunny ears", + unicode_version: "6.0", + }, + { + emoji: "👯‍♀️", + aliases: ["dancing_women"], + tags: ["bunny"], + category: "People & Body", + description: "women with bunny ears", + unicode_version: "11.0", + }, + { + emoji: "🧖", + aliases: ["sauna_person"], + tags: ["steamy"], + category: "People & Body", + description: "person in steamy room", + unicode_version: "11.0", + }, + { + emoji: "🧖‍♂️", + aliases: ["sauna_man"], + tags: ["steamy"], + category: "People & Body", + description: "man in steamy room", + unicode_version: "11.0", + }, + { + emoji: "🧖‍♀️", + aliases: ["sauna_woman"], + tags: ["steamy"], + category: "People & Body", + description: "woman in steamy room", + unicode_version: "11.0", + }, + { + emoji: "🧗", + aliases: ["climbing"], + tags: ["bouldering"], + category: "People & Body", + description: "person climbing", + unicode_version: "11.0", + }, + { + emoji: "🧗‍♂️", + aliases: ["climbing_man"], + tags: ["bouldering"], + category: "People & Body", + description: "man climbing", + unicode_version: "11.0", + }, + { + emoji: "🧗‍♀️", + aliases: ["climbing_woman"], + tags: ["bouldering"], + category: "People & Body", + description: "woman climbing", + unicode_version: "11.0", + }, + { + emoji: "🤺", + aliases: ["person_fencing"], + tags: [], + category: "People & Body", + description: "person fencing", + unicode_version: "9.0", + }, + { + emoji: "🏇", + aliases: ["horse_racing"], + tags: [], + category: "People & Body", + description: "horse racing", + unicode_version: "6.0", + }, + { + emoji: "⛷️", + aliases: ["skier"], + tags: [], + category: "People & Body", + description: "skier", + unicode_version: "5.2", + }, + { + emoji: "🏂", + aliases: ["snowboarder"], + tags: [], + category: "People & Body", + description: "snowboarder", + unicode_version: "6.0", + }, + { + emoji: "🏌️", + aliases: ["golfing"], + tags: [], + category: "People & Body", + description: "person golfing", + unicode_version: "7.0", + }, + { + emoji: "🏌️‍♂️", + aliases: ["golfing_man"], + tags: [], + category: "People & Body", + description: "man golfing", + unicode_version: "11.0", + }, + { + emoji: "🏌️‍♀️", + aliases: ["golfing_woman"], + tags: [], + category: "People & Body", + description: "woman golfing", + unicode_version: "", + }, + { + emoji: "🏄", + aliases: ["surfer"], + tags: [], + category: "People & Body", + description: "person surfing", + unicode_version: "6.0", + }, + { + emoji: "🏄‍♂️", + aliases: ["surfing_man"], + tags: [], + category: "People & Body", + description: "man surfing", + unicode_version: "11.0", + }, + { + emoji: "🏄‍♀️", + aliases: ["surfing_woman"], + tags: [], + category: "People & Body", + description: "woman surfing", + unicode_version: "7.0", + }, + { + emoji: "🚣", + aliases: ["rowboat"], + tags: [], + category: "People & Body", + description: "person rowing boat", + unicode_version: "6.0", + }, + { + emoji: "🚣‍♂️", + aliases: ["rowing_man"], + tags: [], + category: "People & Body", + description: "man rowing boat", + unicode_version: "11.0", + }, + { + emoji: "🚣‍♀️", + aliases: ["rowing_woman"], + tags: [], + category: "People & Body", + description: "woman rowing boat", + unicode_version: "6.0", + }, + { + emoji: "🏊", + aliases: ["swimmer"], + tags: [], + category: "People & Body", + description: "person swimming", + unicode_version: "6.0", + }, + { + emoji: "🏊‍♂️", + aliases: ["swimming_man"], + tags: [], + category: "People & Body", + description: "man swimming", + unicode_version: "11.0", + }, + { + emoji: "🏊‍♀️", + aliases: ["swimming_woman"], + tags: [], + category: "People & Body", + description: "woman swimming", + unicode_version: "6.0", + }, + { + emoji: "⛹️", + aliases: ["bouncing_ball_person"], + tags: ["basketball"], + category: "People & Body", + description: "person bouncing ball", + unicode_version: "5.2", + }, + { + emoji: "⛹️‍♂️", + aliases: ["bouncing_ball_man", "basketball_man"], + tags: [], + category: "People & Body", + description: "man bouncing ball", + unicode_version: "11.0", + }, + { + emoji: "⛹️‍♀️", + aliases: ["bouncing_ball_woman", "basketball_woman"], + tags: [], + category: "People & Body", + description: "woman bouncing ball", + unicode_version: "7.0", + }, + { + emoji: "🏋️", + aliases: ["weight_lifting"], + tags: ["gym", "workout"], + category: "People & Body", + description: "person lifting weights", + unicode_version: "7.0", + }, + { + emoji: "🏋️‍♂️", + aliases: ["weight_lifting_man"], + tags: ["gym", "workout"], + category: "People & Body", + description: "man lifting weights", + unicode_version: "11.0", + }, + { + emoji: "🏋️‍♀️", + aliases: ["weight_lifting_woman"], + tags: ["gym", "workout"], + category: "People & Body", + description: "woman lifting weights", + unicode_version: "6.0", + }, + { + emoji: "🚴", + aliases: ["bicyclist"], + tags: [], + category: "People & Body", + description: "person biking", + unicode_version: "6.0", + }, + { + emoji: "🚴‍♂️", + aliases: ["biking_man"], + tags: [], + category: "People & Body", + description: "man biking", + unicode_version: "11.0", + }, + { + emoji: "🚴‍♀️", + aliases: ["biking_woman"], + tags: [], + category: "People & Body", + description: "woman biking", + unicode_version: "6.0", + }, + { + emoji: "🚵", + aliases: ["mountain_bicyclist"], + tags: [], + category: "People & Body", + description: "person mountain biking", + unicode_version: "6.0", + }, + { + emoji: "🚵‍♂️", + aliases: ["mountain_biking_man"], + tags: [], + category: "People & Body", + description: "man mountain biking", + unicode_version: "11.0", + }, + { + emoji: "🚵‍♀️", + aliases: ["mountain_biking_woman"], + tags: [], + category: "People & Body", + description: "woman mountain biking", + unicode_version: "6.0", + }, + { + emoji: "🤸", + aliases: ["cartwheeling"], + tags: [], + category: "People & Body", + description: "person cartwheeling", + unicode_version: "11.0", + }, + { + emoji: "🤸‍♂️", + aliases: ["man_cartwheeling"], + tags: [], + category: "People & Body", + description: "man cartwheeling", + unicode_version: "", + }, + { + emoji: "🤸‍♀️", + aliases: ["woman_cartwheeling"], + tags: [], + category: "People & Body", + description: "woman cartwheeling", + unicode_version: "", + }, + { + emoji: "🤼", + aliases: ["wrestling"], + tags: [], + category: "People & Body", + description: "people wrestling", + unicode_version: "11.0", + }, + { + emoji: "🤼‍♂️", + aliases: ["men_wrestling"], + tags: [], + category: "People & Body", + description: "men wrestling", + unicode_version: "9.0", + }, + { + emoji: "🤼‍♀️", + aliases: ["women_wrestling"], + tags: [], + category: "People & Body", + description: "women wrestling", + unicode_version: "9.0", + }, + { + emoji: "🤽", + aliases: ["water_polo"], + tags: [], + category: "People & Body", + description: "person playing water polo", + unicode_version: "11.0", + }, + { + emoji: "🤽‍♂️", + aliases: ["man_playing_water_polo"], + tags: [], + category: "People & Body", + description: "man playing water polo", + unicode_version: "9.0", + }, + { + emoji: "🤽‍♀️", + aliases: ["woman_playing_water_polo"], + tags: [], + category: "People & Body", + description: "woman playing water polo", + unicode_version: "9.0", + }, + { + emoji: "🤾", + aliases: ["handball_person"], + tags: [], + category: "People & Body", + description: "person playing handball", + unicode_version: "11.0", + }, + { + emoji: "🤾‍♂️", + aliases: ["man_playing_handball"], + tags: [], + category: "People & Body", + description: "man playing handball", + unicode_version: "9.0", + }, + { + emoji: "🤾‍♀️", + aliases: ["woman_playing_handball"], + tags: [], + category: "People & Body", + description: "woman playing handball", + unicode_version: "9.0", + }, + { + emoji: "🤹", + aliases: ["juggling_person"], + tags: [], + category: "People & Body", + description: "person juggling", + unicode_version: "11.0", + }, + { + emoji: "🤹‍♂️", + aliases: ["man_juggling"], + tags: [], + category: "People & Body", + description: "man juggling", + unicode_version: "9.0", + }, + { + emoji: "🤹‍♀️", + aliases: ["woman_juggling"], + tags: [], + category: "People & Body", + description: "woman juggling", + unicode_version: "9.0", + }, + { + emoji: "🧘", + aliases: ["lotus_position"], + tags: ["meditation"], + category: "People & Body", + description: "person in lotus position", + unicode_version: "11.0", + }, + { + emoji: "🧘‍♂️", + aliases: ["lotus_position_man"], + tags: ["meditation"], + category: "People & Body", + description: "man in lotus position", + unicode_version: "11.0", + }, + { + emoji: "🧘‍♀️", + aliases: ["lotus_position_woman"], + tags: ["meditation"], + category: "People & Body", + description: "woman in lotus position", + unicode_version: "11.0", + }, + { + emoji: "🛀", + aliases: ["bath"], + tags: ["shower"], + category: "People & Body", + description: "person taking bath", + unicode_version: "6.0", + }, + { + emoji: "🛌", + aliases: ["sleeping_bed"], + tags: [], + category: "People & Body", + description: "person in bed", + unicode_version: "7.0", + }, + { + emoji: "🧑‍🤝‍🧑", + aliases: ["people_holding_hands"], + tags: ["couple", "date"], + category: "People & Body", + description: "people holding hands", + unicode_version: "12.0", + }, + { + emoji: "👭", + aliases: ["two_women_holding_hands"], + tags: ["couple", "date"], + category: "People & Body", + description: "women holding hands", + unicode_version: "6.0", + }, + { + emoji: "👫", + aliases: ["couple"], + tags: ["date"], + category: "People & Body", + description: "woman and man holding hands", + unicode_version: "6.0", + }, + { + emoji: "👬", + aliases: ["two_men_holding_hands"], + tags: ["couple", "date"], + category: "People & Body", + description: "men holding hands", + unicode_version: "6.0", + }, + { + emoji: "💏", + aliases: ["couplekiss"], + tags: [], + category: "People & Body", + description: "kiss", + unicode_version: "6.0", + }, + { + emoji: "👩‍❤️‍💋‍👨", + aliases: ["couplekiss_man_woman"], + tags: [], + category: "People & Body", + description: "kiss: woman, man", + unicode_version: "11.0", + }, + { + emoji: "👨‍❤️‍💋‍👨", + aliases: ["couplekiss_man_man"], + tags: [], + category: "People & Body", + description: "kiss: man, man", + unicode_version: "6.0", + }, + { + emoji: "👩‍❤️‍💋‍👩", + aliases: ["couplekiss_woman_woman"], + tags: [], + category: "People & Body", + description: "kiss: woman, woman", + unicode_version: "6.0", + }, + { + emoji: "💑", + aliases: ["couple_with_heart"], + tags: [], + category: "People & Body", + description: "couple with heart", + unicode_version: "6.0", + }, + { + emoji: "👩‍❤️‍👨", + aliases: ["couple_with_heart_woman_man"], + tags: [], + category: "People & Body", + description: "couple with heart: woman, man", + unicode_version: "11.0", + }, + { + emoji: "👨‍❤️‍👨", + aliases: ["couple_with_heart_man_man"], + tags: [], + category: "People & Body", + description: "couple with heart: man, man", + unicode_version: "6.0", + }, + { + emoji: "👩‍❤️‍👩", + aliases: ["couple_with_heart_woman_woman"], + tags: [], + category: "People & Body", + description: "couple with heart: woman, woman", + unicode_version: "6.0", + }, + { + emoji: "👪", + aliases: ["family"], + tags: ["home", "parents", "child"], + category: "People & Body", + description: "family", + unicode_version: "6.0", + }, + { + emoji: "👨‍👩‍👦", + aliases: ["family_man_woman_boy"], + tags: [], + category: "People & Body", + description: "family: man, woman, boy", + unicode_version: "11.0", + }, + { + emoji: "👨‍👩‍👧", + aliases: ["family_man_woman_girl"], + tags: [], + category: "People & Body", + description: "family: man, woman, girl", + unicode_version: "6.0", + }, + { + emoji: "👨‍👩‍👧‍👦", + aliases: ["family_man_woman_girl_boy"], + tags: [], + category: "People & Body", + description: "family: man, woman, girl, boy", + unicode_version: "6.0", + }, + { + emoji: "👨‍👩‍👦‍👦", + aliases: ["family_man_woman_boy_boy"], + tags: [], + category: "People & Body", + description: "family: man, woman, boy, boy", + unicode_version: "6.0", + }, + { + emoji: "👨‍👩‍👧‍👧", + aliases: ["family_man_woman_girl_girl"], + tags: [], + category: "People & Body", + description: "family: man, woman, girl, girl", + unicode_version: "6.0", + }, + { + emoji: "👨‍👨‍👦", + aliases: ["family_man_man_boy"], + tags: [], + category: "People & Body", + description: "family: man, man, boy", + unicode_version: "6.0", + }, + { + emoji: "👨‍👨‍👧", + aliases: ["family_man_man_girl"], + tags: [], + category: "People & Body", + description: "family: man, man, girl", + unicode_version: "6.0", + }, + { + emoji: "👨‍👨‍👧‍👦", + aliases: ["family_man_man_girl_boy"], + tags: [], + category: "People & Body", + description: "family: man, man, girl, boy", + unicode_version: "6.0", + }, + { + emoji: "👨‍👨‍👦‍👦", + aliases: ["family_man_man_boy_boy"], + tags: [], + category: "People & Body", + description: "family: man, man, boy, boy", + unicode_version: "6.0", + }, + { + emoji: "👨‍👨‍👧‍👧", + aliases: ["family_man_man_girl_girl"], + tags: [], + category: "People & Body", + description: "family: man, man, girl, girl", + unicode_version: "6.0", + }, + { + emoji: "👩‍👩‍👦", + aliases: ["family_woman_woman_boy"], + tags: [], + category: "People & Body", + description: "family: woman, woman, boy", + unicode_version: "6.0", + }, + { + emoji: "👩‍👩‍👧", + aliases: ["family_woman_woman_girl"], + tags: [], + category: "People & Body", + description: "family: woman, woman, girl", + unicode_version: "6.0", + }, + { + emoji: "👩‍👩‍👧‍👦", + aliases: ["family_woman_woman_girl_boy"], + tags: [], + category: "People & Body", + description: "family: woman, woman, girl, boy", + unicode_version: "6.0", + }, + { + emoji: "👩‍👩‍👦‍👦", + aliases: ["family_woman_woman_boy_boy"], + tags: [], + category: "People & Body", + description: "family: woman, woman, boy, boy", + unicode_version: "6.0", + }, + { + emoji: "👩‍👩‍👧‍👧", + aliases: ["family_woman_woman_girl_girl"], + tags: [], + category: "People & Body", + description: "family: woman, woman, girl, girl", + unicode_version: "6.0", + }, + { + emoji: "👨‍👦", + aliases: ["family_man_boy"], + tags: [], + category: "People & Body", + description: "family: man, boy", + unicode_version: "6.0", + }, + { + emoji: "👨‍👦‍👦", + aliases: ["family_man_boy_boy"], + tags: [], + category: "People & Body", + description: "family: man, boy, boy", + unicode_version: "6.0", + }, + { + emoji: "👨‍👧", + aliases: ["family_man_girl"], + tags: [], + category: "People & Body", + description: "family: man, girl", + unicode_version: "6.0", + }, + { + emoji: "👨‍👧‍👦", + aliases: ["family_man_girl_boy"], + tags: [], + category: "People & Body", + description: "family: man, girl, boy", + unicode_version: "6.0", + }, + { + emoji: "👨‍👧‍👧", + aliases: ["family_man_girl_girl"], + tags: [], + category: "People & Body", + description: "family: man, girl, girl", + unicode_version: "6.0", + }, + { + emoji: "👩‍👦", + aliases: ["family_woman_boy"], + tags: [], + category: "People & Body", + description: "family: woman, boy", + unicode_version: "6.0", + }, + { + emoji: "👩‍👦‍👦", + aliases: ["family_woman_boy_boy"], + tags: [], + category: "People & Body", + description: "family: woman, boy, boy", + unicode_version: "6.0", + }, + { + emoji: "👩‍👧", + aliases: ["family_woman_girl"], + tags: [], + category: "People & Body", + description: "family: woman, girl", + unicode_version: "6.0", + }, + { + emoji: "👩‍👧‍👦", + aliases: ["family_woman_girl_boy"], + tags: [], + category: "People & Body", + description: "family: woman, girl, boy", + unicode_version: "6.0", + }, + { + emoji: "👩‍👧‍👧", + aliases: ["family_woman_girl_girl"], + tags: [], + category: "People & Body", + description: "family: woman, girl, girl", + unicode_version: "6.0", + }, + { + emoji: "🗣️", + aliases: ["speaking_head"], + tags: [], + category: "People & Body", + description: "speaking head", + unicode_version: "7.0", + }, + { + emoji: "👤", + aliases: ["bust_in_silhouette"], + tags: ["user"], + category: "People & Body", + description: "bust in silhouette", + unicode_version: "6.0", + }, + { + emoji: "👥", + aliases: ["busts_in_silhouette"], + tags: ["users", "group", "team"], + category: "People & Body", + description: "busts in silhouette", + unicode_version: "6.0", + }, + { + emoji: "🫂", + aliases: ["people_hugging"], + tags: [], + category: "People & Body", + description: "people hugging", + unicode_version: "13.0", + }, + { + emoji: "👣", + aliases: ["footprints"], + tags: ["feet", "tracks"], + category: "People & Body", + description: "footprints", + unicode_version: "6.0", + }, + { + emoji: "🐵", + aliases: ["monkey_face"], + tags: [], + category: "Animals & Nature", + description: "monkey face", + unicode_version: "6.0", + }, + { + emoji: "🐒", + aliases: ["monkey"], + tags: [], + category: "Animals & Nature", + description: "monkey", + unicode_version: "6.0", + }, + { + emoji: "🦍", + aliases: ["gorilla"], + tags: [], + category: "Animals & Nature", + description: "gorilla", + unicode_version: "9.0", + }, + { + emoji: "🦧", + aliases: ["orangutan"], + tags: [], + category: "Animals & Nature", + description: "orangutan", + unicode_version: "12.0", + }, + { + emoji: "🐶", + aliases: ["dog"], + tags: ["pet"], + category: "Animals & Nature", + description: "dog face", + unicode_version: "6.0", + }, + { + emoji: "🐕", + aliases: ["dog2"], + tags: [], + category: "Animals & Nature", + description: "dog", + unicode_version: "6.0", + }, + { + emoji: "🦮", + aliases: ["guide_dog"], + tags: [], + category: "Animals & Nature", + description: "guide dog", + unicode_version: "12.0", + }, + { + emoji: "🐕‍🦺", + aliases: ["service_dog"], + tags: [], + category: "Animals & Nature", + description: "service dog", + unicode_version: "12.0", + }, + { + emoji: "🐩", + aliases: ["poodle"], + tags: ["dog"], + category: "Animals & Nature", + description: "poodle", + unicode_version: "6.0", + }, + { + emoji: "🐺", + aliases: ["wolf"], + tags: [], + category: "Animals & Nature", + description: "wolf", + unicode_version: "6.0", + }, + { + emoji: "🦊", + aliases: ["fox_face"], + tags: [], + category: "Animals & Nature", + description: "fox", + unicode_version: "9.0", + }, + { + emoji: "🦝", + aliases: ["raccoon"], + tags: [], + category: "Animals & Nature", + description: "raccoon", + unicode_version: "11.0", + }, + { + emoji: "🐱", + aliases: ["cat"], + tags: ["pet"], + category: "Animals & Nature", + description: "cat face", + unicode_version: "6.0", + }, + { + emoji: "🐈", + aliases: ["cat2"], + tags: [], + category: "Animals & Nature", + description: "cat", + unicode_version: "6.0", + }, + { + emoji: "🐈‍⬛", + aliases: ["black_cat"], + tags: [], + category: "Animals & Nature", + description: "black cat", + unicode_version: "13.0", + }, + { + emoji: "🦁", + aliases: ["lion"], + tags: [], + category: "Animals & Nature", + description: "lion", + unicode_version: "8.0", + }, + { + emoji: "🐯", + aliases: ["tiger"], + tags: [], + category: "Animals & Nature", + description: "tiger face", + unicode_version: "6.0", + }, + { + emoji: "🐅", + aliases: ["tiger2"], + tags: [], + category: "Animals & Nature", + description: "tiger", + unicode_version: "6.0", + }, + { + emoji: "🐆", + aliases: ["leopard"], + tags: [], + category: "Animals & Nature", + description: "leopard", + unicode_version: "6.0", + }, + { + emoji: "🐴", + aliases: ["horse"], + tags: [], + category: "Animals & Nature", + description: "horse face", + unicode_version: "6.0", + }, + { + emoji: "🐎", + aliases: ["racehorse"], + tags: ["speed"], + category: "Animals & Nature", + description: "horse", + unicode_version: "6.0", + }, + { + emoji: "🦄", + aliases: ["unicorn"], + tags: [], + category: "Animals & Nature", + description: "unicorn", + unicode_version: "8.0", + }, + { + emoji: "🦓", + aliases: ["zebra"], + tags: [], + category: "Animals & Nature", + description: "zebra", + unicode_version: "11.0", + }, + { + emoji: "🦌", + aliases: ["deer"], + tags: [], + category: "Animals & Nature", + description: "deer", + unicode_version: "9.0", + }, + { + emoji: "🦬", + aliases: ["bison"], + tags: [], + category: "Animals & Nature", + description: "bison", + unicode_version: "13.0", + }, + { + emoji: "🐮", + aliases: ["cow"], + tags: [], + category: "Animals & Nature", + description: "cow face", + unicode_version: "6.0", + }, + { + emoji: "🐂", + aliases: ["ox"], + tags: [], + category: "Animals & Nature", + description: "ox", + unicode_version: "6.0", + }, + { + emoji: "🐃", + aliases: ["water_buffalo"], + tags: [], + category: "Animals & Nature", + description: "water buffalo", + unicode_version: "6.0", + }, + { + emoji: "🐄", + aliases: ["cow2"], + tags: [], + category: "Animals & Nature", + description: "cow", + unicode_version: "6.0", + }, + { + emoji: "🐷", + aliases: ["pig"], + tags: [], + category: "Animals & Nature", + description: "pig face", + unicode_version: "6.0", + }, + { + emoji: "🐖", + aliases: ["pig2"], + tags: [], + category: "Animals & Nature", + description: "pig", + unicode_version: "6.0", + }, + { + emoji: "🐗", + aliases: ["boar"], + tags: [], + category: "Animals & Nature", + description: "boar", + unicode_version: "6.0", + }, + { + emoji: "🐽", + aliases: ["pig_nose"], + tags: [], + category: "Animals & Nature", + description: "pig nose", + unicode_version: "6.0", + }, + { + emoji: "🐏", + aliases: ["ram"], + tags: [], + category: "Animals & Nature", + description: "ram", + unicode_version: "6.0", + }, + { + emoji: "🐑", + aliases: ["sheep"], + tags: [], + category: "Animals & Nature", + description: "ewe", + unicode_version: "6.0", + }, + { + emoji: "🐐", + aliases: ["goat"], + tags: [], + category: "Animals & Nature", + description: "goat", + unicode_version: "6.0", + }, + { + emoji: "🐪", + aliases: ["dromedary_camel"], + tags: ["desert"], + category: "Animals & Nature", + description: "camel", + unicode_version: "6.0", + }, + { + emoji: "🐫", + aliases: ["camel"], + tags: [], + category: "Animals & Nature", + description: "two-hump camel", + unicode_version: "6.0", + }, + { + emoji: "🦙", + aliases: ["llama"], + tags: [], + category: "Animals & Nature", + description: "llama", + unicode_version: "11.0", + }, + { + emoji: "🦒", + aliases: ["giraffe"], + tags: [], + category: "Animals & Nature", + description: "giraffe", + unicode_version: "11.0", + }, + { + emoji: "🐘", + aliases: ["elephant"], + tags: [], + category: "Animals & Nature", + description: "elephant", + unicode_version: "6.0", + }, + { + emoji: "🦣", + aliases: ["mammoth"], + tags: [], + category: "Animals & Nature", + description: "mammoth", + unicode_version: "13.0", + }, + { + emoji: "🦏", + aliases: ["rhinoceros"], + tags: [], + category: "Animals & Nature", + description: "rhinoceros", + unicode_version: "9.0", + }, + { + emoji: "🦛", + aliases: ["hippopotamus"], + tags: [], + category: "Animals & Nature", + description: "hippopotamus", + unicode_version: "11.0", + }, + { + emoji: "🐭", + aliases: ["mouse"], + tags: [], + category: "Animals & Nature", + description: "mouse face", + unicode_version: "6.0", + }, + { + emoji: "🐁", + aliases: ["mouse2"], + tags: [], + category: "Animals & Nature", + description: "mouse", + unicode_version: "6.0", + }, + { + emoji: "🐀", + aliases: ["rat"], + tags: [], + category: "Animals & Nature", + description: "rat", + unicode_version: "6.0", + }, + { + emoji: "🐹", + aliases: ["hamster"], + tags: ["pet"], + category: "Animals & Nature", + description: "hamster", + unicode_version: "6.0", + }, + { + emoji: "🐰", + aliases: ["rabbit"], + tags: ["bunny"], + category: "Animals & Nature", + description: "rabbit face", + unicode_version: "6.0", + }, + { + emoji: "🐇", + aliases: ["rabbit2"], + tags: [], + category: "Animals & Nature", + description: "rabbit", + unicode_version: "6.0", + }, + { + emoji: "🐿️", + aliases: ["chipmunk"], + tags: [], + category: "Animals & Nature", + description: "chipmunk", + unicode_version: "7.0", + }, + { + emoji: "🦫", + aliases: ["beaver"], + tags: [], + category: "Animals & Nature", + description: "beaver", + unicode_version: "13.0", + }, + { + emoji: "🦔", + aliases: ["hedgehog"], + tags: [], + category: "Animals & Nature", + description: "hedgehog", + unicode_version: "11.0", + }, + { + emoji: "🦇", + aliases: ["bat"], + tags: [], + category: "Animals & Nature", + description: "bat", + unicode_version: "9.0", + }, + { + emoji: "🐻", + aliases: ["bear"], + tags: [], + category: "Animals & Nature", + description: "bear", + unicode_version: "6.0", + }, + { + emoji: "🐻‍❄️", + aliases: ["polar_bear"], + tags: [], + category: "Animals & Nature", + description: "polar bear", + unicode_version: "13.0", + }, + { + emoji: "🐨", + aliases: ["koala"], + tags: [], + category: "Animals & Nature", + description: "koala", + unicode_version: "6.0", + }, + { + emoji: "🐼", + aliases: ["panda_face"], + tags: [], + category: "Animals & Nature", + description: "panda", + unicode_version: "6.0", + }, + { + emoji: "🦥", + aliases: ["sloth"], + tags: [], + category: "Animals & Nature", + description: "sloth", + unicode_version: "12.0", + }, + { + emoji: "🦦", + aliases: ["otter"], + tags: [], + category: "Animals & Nature", + description: "otter", + unicode_version: "12.0", + }, + { + emoji: "🦨", + aliases: ["skunk"], + tags: [], + category: "Animals & Nature", + description: "skunk", + unicode_version: "12.0", + }, + { + emoji: "🦘", + aliases: ["kangaroo"], + tags: [], + category: "Animals & Nature", + description: "kangaroo", + unicode_version: "11.0", + }, + { + emoji: "🦡", + aliases: ["badger"], + tags: [], + category: "Animals & Nature", + description: "badger", + unicode_version: "11.0", + }, + { + emoji: "🐾", + aliases: ["feet", "paw_prints"], + tags: [], + category: "Animals & Nature", + description: "paw prints", + unicode_version: "6.0", + }, + { + emoji: "🦃", + aliases: ["turkey"], + tags: ["thanksgiving"], + category: "Animals & Nature", + description: "turkey", + unicode_version: "8.0", + }, + { + emoji: "🐔", + aliases: ["chicken"], + tags: [], + category: "Animals & Nature", + description: "chicken", + unicode_version: "6.0", + }, + { + emoji: "🐓", + aliases: ["rooster"], + tags: [], + category: "Animals & Nature", + description: "rooster", + unicode_version: "6.0", + }, + { + emoji: "🐣", + aliases: ["hatching_chick"], + tags: [], + category: "Animals & Nature", + description: "hatching chick", + unicode_version: "6.0", + }, + { + emoji: "🐤", + aliases: ["baby_chick"], + tags: [], + category: "Animals & Nature", + description: "baby chick", + unicode_version: "6.0", + }, + { + emoji: "🐥", + aliases: ["hatched_chick"], + tags: [], + category: "Animals & Nature", + description: "front-facing baby chick", + unicode_version: "6.0", + }, + { + emoji: "🐦", + aliases: ["bird"], + tags: [], + category: "Animals & Nature", + description: "bird", + unicode_version: "6.0", + }, + { + emoji: "🐧", + aliases: ["penguin"], + tags: [], + category: "Animals & Nature", + description: "penguin", + unicode_version: "6.0", + }, + { + emoji: "🕊️", + aliases: ["dove"], + tags: ["peace"], + category: "Animals & Nature", + description: "dove", + unicode_version: "7.0", + }, + { + emoji: "🦅", + aliases: ["eagle"], + tags: [], + category: "Animals & Nature", + description: "eagle", + unicode_version: "9.0", + }, + { + emoji: "🦆", + aliases: ["duck"], + tags: [], + category: "Animals & Nature", + description: "duck", + unicode_version: "9.0", + }, + { + emoji: "🦢", + aliases: ["swan"], + tags: [], + category: "Animals & Nature", + description: "swan", + unicode_version: "11.0", + }, + { + emoji: "🦉", + aliases: ["owl"], + tags: [], + category: "Animals & Nature", + description: "owl", + unicode_version: "9.0", + }, + { + emoji: "🦤", + aliases: ["dodo"], + tags: [], + category: "Animals & Nature", + description: "dodo", + unicode_version: "13.0", + }, + { + emoji: "🪶", + aliases: ["feather"], + tags: [], + category: "Animals & Nature", + description: "feather", + unicode_version: "13.0", + }, + { + emoji: "🦩", + aliases: ["flamingo"], + tags: [], + category: "Animals & Nature", + description: "flamingo", + unicode_version: "12.0", + }, + { + emoji: "🦚", + aliases: ["peacock"], + tags: [], + category: "Animals & Nature", + description: "peacock", + unicode_version: "11.0", + }, + { + emoji: "🦜", + aliases: ["parrot"], + tags: [], + category: "Animals & Nature", + description: "parrot", + unicode_version: "11.0", + }, + { + emoji: "🐸", + aliases: ["frog"], + tags: [], + category: "Animals & Nature", + description: "frog", + unicode_version: "6.0", + }, + { + emoji: "🐊", + aliases: ["crocodile"], + tags: [], + category: "Animals & Nature", + description: "crocodile", + unicode_version: "6.0", + }, + { + emoji: "🐢", + aliases: ["turtle"], + tags: ["slow"], + category: "Animals & Nature", + description: "turtle", + unicode_version: "6.0", + }, + { + emoji: "🦎", + aliases: ["lizard"], + tags: [], + category: "Animals & Nature", + description: "lizard", + unicode_version: "9.0", + }, + { + emoji: "🐍", + aliases: ["snake"], + tags: [], + category: "Animals & Nature", + description: "snake", + unicode_version: "6.0", + }, + { + emoji: "🐲", + aliases: ["dragon_face"], + tags: [], + category: "Animals & Nature", + description: "dragon face", + unicode_version: "6.0", + }, + { + emoji: "🐉", + aliases: ["dragon"], + tags: [], + category: "Animals & Nature", + description: "dragon", + unicode_version: "6.0", + }, + { + emoji: "🦕", + aliases: ["sauropod"], + tags: ["dinosaur"], + category: "Animals & Nature", + description: "sauropod", + unicode_version: "11.0", + }, + { + emoji: "🦖", + aliases: ["t-rex"], + tags: ["dinosaur"], + category: "Animals & Nature", + description: "T-Rex", + unicode_version: "11.0", + }, + { + emoji: "🐳", + aliases: ["whale"], + tags: ["sea"], + category: "Animals & Nature", + description: "spouting whale", + unicode_version: "6.0", + }, + { + emoji: "🐋", + aliases: ["whale2"], + tags: [], + category: "Animals & Nature", + description: "whale", + unicode_version: "6.0", + }, + { + emoji: "🐬", + aliases: ["dolphin", "flipper"], + tags: [], + category: "Animals & Nature", + description: "dolphin", + unicode_version: "6.0", + }, + { + emoji: "🦭", + aliases: ["seal"], + tags: [], + category: "Animals & Nature", + description: "seal", + unicode_version: "13.0", + }, + { + emoji: "🐟", + aliases: ["fish"], + tags: [], + category: "Animals & Nature", + description: "fish", + unicode_version: "6.0", + }, + { + emoji: "🐠", + aliases: ["tropical_fish"], + tags: [], + category: "Animals & Nature", + description: "tropical fish", + unicode_version: "6.0", + }, + { + emoji: "🐡", + aliases: ["blowfish"], + tags: [], + category: "Animals & Nature", + description: "blowfish", + unicode_version: "6.0", + }, + { + emoji: "🦈", + aliases: ["shark"], + tags: [], + category: "Animals & Nature", + description: "shark", + unicode_version: "9.0", + }, + { + emoji: "🐙", + aliases: ["octopus"], + tags: [], + category: "Animals & Nature", + description: "octopus", + unicode_version: "6.0", + }, + { + emoji: "🐚", + aliases: ["shell"], + tags: ["sea", "beach"], + category: "Animals & Nature", + description: "spiral shell", + unicode_version: "6.0", + }, + { + emoji: "🐌", + aliases: ["snail"], + tags: ["slow"], + category: "Animals & Nature", + description: "snail", + unicode_version: "6.0", + }, + { + emoji: "🦋", + aliases: ["butterfly"], + tags: [], + category: "Animals & Nature", + description: "butterfly", + unicode_version: "9.0", + }, + { + emoji: "🐛", + aliases: ["bug"], + tags: [], + category: "Animals & Nature", + description: "bug", + unicode_version: "6.0", + }, + { + emoji: "🐜", + aliases: ["ant"], + tags: [], + category: "Animals & Nature", + description: "ant", + unicode_version: "6.0", + }, + { + emoji: "🐝", + aliases: ["bee", "honeybee"], + tags: [], + category: "Animals & Nature", + description: "honeybee", + unicode_version: "6.0", + }, + { + emoji: "🪲", + aliases: ["beetle"], + tags: [], + category: "Animals & Nature", + description: "beetle", + unicode_version: "13.0", + }, + { + emoji: "🐞", + aliases: ["lady_beetle"], + tags: ["bug"], + category: "Animals & Nature", + description: "lady beetle", + unicode_version: "6.0", + }, + { + emoji: "🦗", + aliases: ["cricket"], + tags: [], + category: "Animals & Nature", + description: "cricket", + unicode_version: "11.0", + }, + { + emoji: "🪳", + aliases: ["cockroach"], + tags: [], + category: "Animals & Nature", + description: "cockroach", + unicode_version: "13.0", + }, + { + emoji: "🕷️", + aliases: ["spider"], + tags: [], + category: "Animals & Nature", + description: "spider", + unicode_version: "7.0", + }, + { + emoji: "🕸️", + aliases: ["spider_web"], + tags: [], + category: "Animals & Nature", + description: "spider web", + unicode_version: "7.0", + }, + { + emoji: "🦂", + aliases: ["scorpion"], + tags: [], + category: "Animals & Nature", + description: "scorpion", + unicode_version: "8.0", + }, + { + emoji: "🦟", + aliases: ["mosquito"], + tags: [], + category: "Animals & Nature", + description: "mosquito", + unicode_version: "11.0", + }, + { + emoji: "🪰", + aliases: ["fly"], + tags: [], + category: "Animals & Nature", + description: "fly", + unicode_version: "13.0", + }, + { + emoji: "🪱", + aliases: ["worm"], + tags: [], + category: "Animals & Nature", + description: "worm", + unicode_version: "13.0", + }, + { + emoji: "🦠", + aliases: ["microbe"], + tags: ["germ"], + category: "Animals & Nature", + description: "microbe", + unicode_version: "11.0", + }, + { + emoji: "💐", + aliases: ["bouquet"], + tags: ["flowers"], + category: "Animals & Nature", + description: "bouquet", + unicode_version: "6.0", + }, + { + emoji: "🌸", + aliases: ["cherry_blossom"], + tags: ["flower", "spring"], + category: "Animals & Nature", + description: "cherry blossom", + unicode_version: "6.0", + }, + { + emoji: "💮", + aliases: ["white_flower"], + tags: [], + category: "Animals & Nature", + description: "white flower", + unicode_version: "6.0", + }, + { + emoji: "🏵️", + aliases: ["rosette"], + tags: [], + category: "Animals & Nature", + description: "rosette", + unicode_version: "7.0", + }, + { + emoji: "🌹", + aliases: ["rose"], + tags: ["flower"], + category: "Animals & Nature", + description: "rose", + unicode_version: "6.0", + }, + { + emoji: "🥀", + aliases: ["wilted_flower"], + tags: [], + category: "Animals & Nature", + description: "wilted flower", + unicode_version: "9.0", + }, + { + emoji: "🌺", + aliases: ["hibiscus"], + tags: [], + category: "Animals & Nature", + description: "hibiscus", + unicode_version: "6.0", + }, + { + emoji: "🌻", + aliases: ["sunflower"], + tags: [], + category: "Animals & Nature", + description: "sunflower", + unicode_version: "6.0", + }, + { + emoji: "🌼", + aliases: ["blossom"], + tags: [], + category: "Animals & Nature", + description: "blossom", + unicode_version: "6.0", + }, + { + emoji: "🌷", + aliases: ["tulip"], + tags: ["flower"], + category: "Animals & Nature", + description: "tulip", + unicode_version: "6.0", + }, + { + emoji: "🌱", + aliases: ["seedling"], + tags: ["plant"], + category: "Animals & Nature", + description: "seedling", + unicode_version: "6.0", + }, + { + emoji: "🪴", + aliases: ["potted_plant"], + tags: [], + category: "Animals & Nature", + description: "potted plant", + unicode_version: "13.0", + }, + { + emoji: "🌲", + aliases: ["evergreen_tree"], + tags: ["wood"], + category: "Animals & Nature", + description: "evergreen tree", + unicode_version: "6.0", + }, + { + emoji: "🌳", + aliases: ["deciduous_tree"], + tags: ["wood"], + category: "Animals & Nature", + description: "deciduous tree", + unicode_version: "6.0", + }, + { + emoji: "🌴", + aliases: ["palm_tree"], + tags: [], + category: "Animals & Nature", + description: "palm tree", + unicode_version: "6.0", + }, + { + emoji: "🌵", + aliases: ["cactus"], + tags: [], + category: "Animals & Nature", + description: "cactus", + unicode_version: "6.0", + }, + { + emoji: "🌾", + aliases: ["ear_of_rice"], + tags: [], + category: "Animals & Nature", + description: "sheaf of rice", + unicode_version: "6.0", + }, + { + emoji: "🌿", + aliases: ["herb"], + tags: [], + category: "Animals & Nature", + description: "herb", + unicode_version: "6.0", + }, + { + emoji: "☘️", + aliases: ["shamrock"], + tags: [], + category: "Animals & Nature", + description: "shamrock", + unicode_version: "4.1", + }, + { + emoji: "🍀", + aliases: ["four_leaf_clover"], + tags: ["luck"], + category: "Animals & Nature", + description: "four leaf clover", + unicode_version: "6.0", + }, + { + emoji: "🍁", + aliases: ["maple_leaf"], + tags: ["canada"], + category: "Animals & Nature", + description: "maple leaf", + unicode_version: "6.0", + }, + { + emoji: "🍂", + aliases: ["fallen_leaf"], + tags: ["autumn"], + category: "Animals & Nature", + description: "fallen leaf", + unicode_version: "6.0", + }, + { + emoji: "🍃", + aliases: ["leaves"], + tags: ["leaf"], + category: "Animals & Nature", + description: "leaf fluttering in wind", + unicode_version: "6.0", + }, + { + emoji: "🍇", + aliases: ["grapes"], + tags: [], + category: "Food & Drink", + description: "grapes", + unicode_version: "6.0", + }, + { + emoji: "🍈", + aliases: ["melon"], + tags: [], + category: "Food & Drink", + description: "melon", + unicode_version: "6.0", + }, + { + emoji: "🍉", + aliases: ["watermelon"], + tags: [], + category: "Food & Drink", + description: "watermelon", + unicode_version: "6.0", + }, + { + emoji: "🍊", + aliases: ["tangerine", "orange", "mandarin"], + tags: [], + category: "Food & Drink", + description: "tangerine", + unicode_version: "6.0", + }, + { + emoji: "🍋", + aliases: ["lemon"], + tags: [], + category: "Food & Drink", + description: "lemon", + unicode_version: "6.0", + }, + { + emoji: "🍌", + aliases: ["banana"], + tags: ["fruit"], + category: "Food & Drink", + description: "banana", + unicode_version: "6.0", + }, + { + emoji: "🍍", + aliases: ["pineapple"], + tags: [], + category: "Food & Drink", + description: "pineapple", + unicode_version: "6.0", + }, + { + emoji: "🥭", + aliases: ["mango"], + tags: [], + category: "Food & Drink", + description: "mango", + unicode_version: "11.0", + }, + { + emoji: "🍎", + aliases: ["apple"], + tags: [], + category: "Food & Drink", + description: "red apple", + unicode_version: "6.0", + }, + { + emoji: "🍏", + aliases: ["green_apple"], + tags: ["fruit"], + category: "Food & Drink", + description: "green apple", + unicode_version: "6.0", + }, + { + emoji: "🍐", + aliases: ["pear"], + tags: [], + category: "Food & Drink", + description: "pear", + unicode_version: "6.0", + }, + { + emoji: "🍑", + aliases: ["peach"], + tags: [], + category: "Food & Drink", + description: "peach", + unicode_version: "6.0", + }, + { + emoji: "🍒", + aliases: ["cherries"], + tags: ["fruit"], + category: "Food & Drink", + description: "cherries", + unicode_version: "6.0", + }, + { + emoji: "🍓", + aliases: ["strawberry"], + tags: ["fruit"], + category: "Food & Drink", + description: "strawberry", + unicode_version: "6.0", + }, + { + emoji: "🫐", + aliases: ["blueberries"], + tags: [], + category: "Food & Drink", + description: "blueberries", + unicode_version: "13.0", + }, + { + emoji: "🥝", + aliases: ["kiwi_fruit"], + tags: [], + category: "Food & Drink", + description: "kiwi fruit", + unicode_version: "9.0", + }, + { + emoji: "🍅", + aliases: ["tomato"], + tags: [], + category: "Food & Drink", + description: "tomato", + unicode_version: "6.0", + }, + { + emoji: "🫒", + aliases: ["olive"], + tags: [], + category: "Food & Drink", + description: "olive", + unicode_version: "13.0", + }, + { + emoji: "🥥", + aliases: ["coconut"], + tags: [], + category: "Food & Drink", + description: "coconut", + unicode_version: "11.0", + }, + { + emoji: "🥑", + aliases: ["avocado"], + tags: [], + category: "Food & Drink", + description: "avocado", + unicode_version: "9.0", + }, + { + emoji: "🍆", + aliases: ["eggplant"], + tags: ["aubergine"], + category: "Food & Drink", + description: "eggplant", + unicode_version: "6.0", + }, + { + emoji: "🥔", + aliases: ["potato"], + tags: [], + category: "Food & Drink", + description: "potato", + unicode_version: "9.0", + }, + { + emoji: "🥕", + aliases: ["carrot"], + tags: [], + category: "Food & Drink", + description: "carrot", + unicode_version: "9.0", + }, + { + emoji: "🌽", + aliases: ["corn"], + tags: [], + category: "Food & Drink", + description: "ear of corn", + unicode_version: "6.0", + }, + { + emoji: "🌶️", + aliases: ["hot_pepper"], + tags: ["spicy"], + category: "Food & Drink", + description: "hot pepper", + unicode_version: "7.0", + }, + { + emoji: "🫑", + aliases: ["bell_pepper"], + tags: [], + category: "Food & Drink", + description: "bell pepper", + unicode_version: "13.0", + }, + { + emoji: "🥒", + aliases: ["cucumber"], + tags: [], + category: "Food & Drink", + description: "cucumber", + unicode_version: "9.0", + }, + { + emoji: "🥬", + aliases: ["leafy_green"], + tags: [], + category: "Food & Drink", + description: "leafy green", + unicode_version: "11.0", + }, + { + emoji: "🥦", + aliases: ["broccoli"], + tags: [], + category: "Food & Drink", + description: "broccoli", + unicode_version: "11.0", + }, + { + emoji: "🧄", + aliases: ["garlic"], + tags: [], + category: "Food & Drink", + description: "garlic", + unicode_version: "12.0", + }, + { + emoji: "🧅", + aliases: ["onion"], + tags: [], + category: "Food & Drink", + description: "onion", + unicode_version: "12.0", + }, + { + emoji: "🍄", + aliases: ["mushroom"], + tags: [], + category: "Food & Drink", + description: "mushroom", + unicode_version: "6.0", + }, + { + emoji: "🥜", + aliases: ["peanuts"], + tags: [], + category: "Food & Drink", + description: "peanuts", + unicode_version: "9.0", + }, + { + emoji: "🌰", + aliases: ["chestnut"], + tags: [], + category: "Food & Drink", + description: "chestnut", + unicode_version: "6.0", + }, + { + emoji: "🍞", + aliases: ["bread"], + tags: ["toast"], + category: "Food & Drink", + description: "bread", + unicode_version: "6.0", + }, + { + emoji: "🥐", + aliases: ["croissant"], + tags: [], + category: "Food & Drink", + description: "croissant", + unicode_version: "9.0", + }, + { + emoji: "🥖", + aliases: ["baguette_bread"], + tags: [], + category: "Food & Drink", + description: "baguette bread", + unicode_version: "9.0", + }, + { + emoji: "🫓", + aliases: ["flatbread"], + tags: [], + category: "Food & Drink", + description: "flatbread", + unicode_version: "13.0", + }, + { + emoji: "🥨", + aliases: ["pretzel"], + tags: [], + category: "Food & Drink", + description: "pretzel", + unicode_version: "11.0", + }, + { + emoji: "🥯", + aliases: ["bagel"], + tags: [], + category: "Food & Drink", + description: "bagel", + unicode_version: "11.0", + }, + { + emoji: "🥞", + aliases: ["pancakes"], + tags: [], + category: "Food & Drink", + description: "pancakes", + unicode_version: "9.0", + }, + { + emoji: "🧇", + aliases: ["waffle"], + tags: [], + category: "Food & Drink", + description: "waffle", + unicode_version: "12.0", + }, + { + emoji: "🧀", + aliases: ["cheese"], + tags: [], + category: "Food & Drink", + description: "cheese wedge", + unicode_version: "8.0", + }, + { + emoji: "🍖", + aliases: ["meat_on_bone"], + tags: [], + category: "Food & Drink", + description: "meat on bone", + unicode_version: "6.0", + }, + { + emoji: "🍗", + aliases: ["poultry_leg"], + tags: ["meat", "chicken"], + category: "Food & Drink", + description: "poultry leg", + unicode_version: "6.0", + }, + { + emoji: "🥩", + aliases: ["cut_of_meat"], + tags: [], + category: "Food & Drink", + description: "cut of meat", + unicode_version: "11.0", + }, + { + emoji: "🥓", + aliases: ["bacon"], + tags: [], + category: "Food & Drink", + description: "bacon", + unicode_version: "9.0", + }, + { + emoji: "🍔", + aliases: ["hamburger"], + tags: ["burger"], + category: "Food & Drink", + description: "hamburger", + unicode_version: "6.0", + }, + { + emoji: "🍟", + aliases: ["fries"], + tags: [], + category: "Food & Drink", + description: "french fries", + unicode_version: "6.0", + }, + { + emoji: "🍕", + aliases: ["pizza"], + tags: [], + category: "Food & Drink", + description: "pizza", + unicode_version: "6.0", + }, + { + emoji: "🌭", + aliases: ["hotdog"], + tags: [], + category: "Food & Drink", + description: "hot dog", + unicode_version: "8.0", + }, + { + emoji: "🥪", + aliases: ["sandwich"], + tags: [], + category: "Food & Drink", + description: "sandwich", + unicode_version: "11.0", + }, + { + emoji: "🌮", + aliases: ["taco"], + tags: [], + category: "Food & Drink", + description: "taco", + unicode_version: "8.0", + }, + { + emoji: "🌯", + aliases: ["burrito"], + tags: [], + category: "Food & Drink", + description: "burrito", + unicode_version: "8.0", + }, + { + emoji: "🫔", + aliases: ["tamale"], + tags: [], + category: "Food & Drink", + description: "tamale", + unicode_version: "13.0", + }, + { + emoji: "🥙", + aliases: ["stuffed_flatbread"], + tags: [], + category: "Food & Drink", + description: "stuffed flatbread", + unicode_version: "9.0", + }, + { + emoji: "🧆", + aliases: ["falafel"], + tags: [], + category: "Food & Drink", + description: "falafel", + unicode_version: "12.0", + }, + { + emoji: "🥚", + aliases: ["egg"], + tags: [], + category: "Food & Drink", + description: "egg", + unicode_version: "9.0", + }, + { + emoji: "🍳", + aliases: ["fried_egg"], + tags: ["breakfast"], + category: "Food & Drink", + description: "cooking", + unicode_version: "6.0", + }, + { + emoji: "🥘", + aliases: ["shallow_pan_of_food"], + tags: ["paella", "curry"], + category: "Food & Drink", + description: "shallow pan of food", + unicode_version: "", + }, + { + emoji: "🍲", + aliases: ["stew"], + tags: [], + category: "Food & Drink", + description: "pot of food", + unicode_version: "6.0", + }, + { + emoji: "🫕", + aliases: ["fondue"], + tags: [], + category: "Food & Drink", + description: "fondue", + unicode_version: "13.0", + }, + { + emoji: "🥣", + aliases: ["bowl_with_spoon"], + tags: [], + category: "Food & Drink", + description: "bowl with spoon", + unicode_version: "11.0", + }, + { + emoji: "🥗", + aliases: ["green_salad"], + tags: [], + category: "Food & Drink", + description: "green salad", + unicode_version: "9.0", + }, + { + emoji: "🍿", + aliases: ["popcorn"], + tags: [], + category: "Food & Drink", + description: "popcorn", + unicode_version: "8.0", + }, + { + emoji: "🧈", + aliases: ["butter"], + tags: [], + category: "Food & Drink", + description: "butter", + unicode_version: "12.0", + }, + { + emoji: "🧂", + aliases: ["salt"], + tags: [], + category: "Food & Drink", + description: "salt", + unicode_version: "11.0", + }, + { + emoji: "🥫", + aliases: ["canned_food"], + tags: [], + category: "Food & Drink", + description: "canned food", + unicode_version: "11.0", + }, + { + emoji: "🍱", + aliases: ["bento"], + tags: [], + category: "Food & Drink", + description: "bento box", + unicode_version: "6.0", + }, + { + emoji: "🍘", + aliases: ["rice_cracker"], + tags: [], + category: "Food & Drink", + description: "rice cracker", + unicode_version: "6.0", + }, + { + emoji: "🍙", + aliases: ["rice_ball"], + tags: [], + category: "Food & Drink", + description: "rice ball", + unicode_version: "6.0", + }, + { + emoji: "🍚", + aliases: ["rice"], + tags: [], + category: "Food & Drink", + description: "cooked rice", + unicode_version: "6.0", + }, + { + emoji: "🍛", + aliases: ["curry"], + tags: [], + category: "Food & Drink", + description: "curry rice", + unicode_version: "6.0", + }, + { + emoji: "🍜", + aliases: ["ramen"], + tags: ["noodle"], + category: "Food & Drink", + description: "steaming bowl", + unicode_version: "6.0", + }, + { + emoji: "🍝", + aliases: ["spaghetti"], + tags: ["pasta"], + category: "Food & Drink", + description: "spaghetti", + unicode_version: "6.0", + }, + { + emoji: "🍠", + aliases: ["sweet_potato"], + tags: [], + category: "Food & Drink", + description: "roasted sweet potato", + unicode_version: "6.0", + }, + { + emoji: "🍢", + aliases: ["oden"], + tags: [], + category: "Food & Drink", + description: "oden", + unicode_version: "6.0", + }, + { + emoji: "🍣", + aliases: ["sushi"], + tags: [], + category: "Food & Drink", + description: "sushi", + unicode_version: "6.0", + }, + { + emoji: "🍤", + aliases: ["fried_shrimp"], + tags: ["tempura"], + category: "Food & Drink", + description: "fried shrimp", + unicode_version: "6.0", + }, + { + emoji: "🍥", + aliases: ["fish_cake"], + tags: [], + category: "Food & Drink", + description: "fish cake with swirl", + unicode_version: "6.0", + }, + { + emoji: "🥮", + aliases: ["moon_cake"], + tags: [], + category: "Food & Drink", + description: "moon cake", + unicode_version: "11.0", + }, + { + emoji: "🍡", + aliases: ["dango"], + tags: [], + category: "Food & Drink", + description: "dango", + unicode_version: "6.0", + }, + { + emoji: "🥟", + aliases: ["dumpling"], + tags: [], + category: "Food & Drink", + description: "dumpling", + unicode_version: "11.0", + }, + { + emoji: "🥠", + aliases: ["fortune_cookie"], + tags: [], + category: "Food & Drink", + description: "fortune cookie", + unicode_version: "11.0", + }, + { + emoji: "🥡", + aliases: ["takeout_box"], + tags: [], + category: "Food & Drink", + description: "takeout box", + unicode_version: "11.0", + }, + { + emoji: "🦀", + aliases: ["crab"], + tags: [], + category: "Food & Drink", + description: "crab", + unicode_version: "8.0", + }, + { + emoji: "🦞", + aliases: ["lobster"], + tags: [], + category: "Food & Drink", + description: "lobster", + unicode_version: "11.0", + }, + { + emoji: "🦐", + aliases: ["shrimp"], + tags: [], + category: "Food & Drink", + description: "shrimp", + unicode_version: "9.0", + }, + { + emoji: "🦑", + aliases: ["squid"], + tags: [], + category: "Food & Drink", + description: "squid", + unicode_version: "9.0", + }, + { + emoji: "🦪", + aliases: ["oyster"], + tags: [], + category: "Food & Drink", + description: "oyster", + unicode_version: "12.0", + }, + { + emoji: "🍦", + aliases: ["icecream"], + tags: [], + category: "Food & Drink", + description: "soft ice cream", + unicode_version: "6.0", + }, + { + emoji: "🍧", + aliases: ["shaved_ice"], + tags: [], + category: "Food & Drink", + description: "shaved ice", + unicode_version: "6.0", + }, + { + emoji: "🍨", + aliases: ["ice_cream"], + tags: [], + category: "Food & Drink", + description: "ice cream", + unicode_version: "6.0", + }, + { + emoji: "🍩", + aliases: ["doughnut"], + tags: [], + category: "Food & Drink", + description: "doughnut", + unicode_version: "6.0", + }, + { + emoji: "🍪", + aliases: ["cookie"], + tags: [], + category: "Food & Drink", + description: "cookie", + unicode_version: "6.0", + }, + { + emoji: "🎂", + aliases: ["birthday"], + tags: ["party"], + category: "Food & Drink", + description: "birthday cake", + unicode_version: "6.0", + }, + { + emoji: "🍰", + aliases: ["cake"], + tags: ["dessert"], + category: "Food & Drink", + description: "shortcake", + unicode_version: "6.0", + }, + { + emoji: "🧁", + aliases: ["cupcake"], + tags: [], + category: "Food & Drink", + description: "cupcake", + unicode_version: "11.0", + }, + { + emoji: "🥧", + aliases: ["pie"], + tags: [], + category: "Food & Drink", + description: "pie", + unicode_version: "11.0", + }, + { + emoji: "🍫", + aliases: ["chocolate_bar"], + tags: [], + category: "Food & Drink", + description: "chocolate bar", + unicode_version: "6.0", + }, + { + emoji: "🍬", + aliases: ["candy"], + tags: ["sweet"], + category: "Food & Drink", + description: "candy", + unicode_version: "6.0", + }, + { + emoji: "🍭", + aliases: ["lollipop"], + tags: [], + category: "Food & Drink", + description: "lollipop", + unicode_version: "6.0", + }, + { + emoji: "🍮", + aliases: ["custard"], + tags: [], + category: "Food & Drink", + description: "custard", + unicode_version: "6.0", + }, + { + emoji: "🍯", + aliases: ["honey_pot"], + tags: [], + category: "Food & Drink", + description: "honey pot", + unicode_version: "6.0", + }, + { + emoji: "🍼", + aliases: ["baby_bottle"], + tags: ["milk"], + category: "Food & Drink", + description: "baby bottle", + unicode_version: "6.0", + }, + { + emoji: "🥛", + aliases: ["milk_glass"], + tags: [], + category: "Food & Drink", + description: "glass of milk", + unicode_version: "9.0", + }, + { + emoji: "☕", + aliases: ["coffee"], + tags: ["cafe", "espresso"], + category: "Food & Drink", + description: "hot beverage", + unicode_version: "4.0", + }, + { + emoji: "🫖", + aliases: ["teapot"], + tags: [], + category: "Food & Drink", + description: "teapot", + unicode_version: "13.0", + }, + { + emoji: "🍵", + aliases: ["tea"], + tags: ["green", "breakfast"], + category: "Food & Drink", + description: "teacup without handle", + unicode_version: "6.0", + }, + { + emoji: "🍶", + aliases: ["sake"], + tags: [], + category: "Food & Drink", + description: "sake", + unicode_version: "6.0", + }, + { + emoji: "🍾", + aliases: ["champagne"], + tags: ["bottle", "bubbly", "celebration"], + category: "Food & Drink", + description: "bottle with popping cork", + unicode_version: "8.0", + }, + { + emoji: "🍷", + aliases: ["wine_glass"], + tags: [], + category: "Food & Drink", + description: "wine glass", + unicode_version: "6.0", + }, + { + emoji: "🍸", + aliases: ["cocktail"], + tags: ["drink"], + category: "Food & Drink", + description: "cocktail glass", + unicode_version: "6.0", + }, + { + emoji: "🍹", + aliases: ["tropical_drink"], + tags: ["summer", "vacation"], + category: "Food & Drink", + description: "tropical drink", + unicode_version: "6.0", + }, + { + emoji: "🍺", + aliases: ["beer"], + tags: ["drink"], + category: "Food & Drink", + description: "beer mug", + unicode_version: "6.0", + }, + { + emoji: "🍻", + aliases: ["beers"], + tags: ["drinks"], + category: "Food & Drink", + description: "clinking beer mugs", + unicode_version: "6.0", + }, + { + emoji: "🥂", + aliases: ["clinking_glasses"], + tags: ["cheers", "toast"], + category: "Food & Drink", + description: "clinking glasses", + unicode_version: "9.0", + }, + { + emoji: "🥃", + aliases: ["tumbler_glass"], + tags: ["whisky"], + category: "Food & Drink", + description: "tumbler glass", + unicode_version: "9.0", + }, + { + emoji: "🥤", + aliases: ["cup_with_straw"], + tags: [], + category: "Food & Drink", + description: "cup with straw", + unicode_version: "11.0", + }, + { + emoji: "🧋", + aliases: ["bubble_tea"], + tags: [], + category: "Food & Drink", + description: "bubble tea", + unicode_version: "13.0", + }, + { + emoji: "🧃", + aliases: ["beverage_box"], + tags: [], + category: "Food & Drink", + description: "beverage box", + unicode_version: "12.0", + }, + { + emoji: "🧉", + aliases: ["mate"], + tags: [], + category: "Food & Drink", + description: "mate", + unicode_version: "12.0", + }, + { + emoji: "🧊", + aliases: ["ice_cube"], + tags: [], + category: "Food & Drink", + description: "ice", + unicode_version: "12.0", + }, + { + emoji: "🥢", + aliases: ["chopsticks"], + tags: [], + category: "Food & Drink", + description: "chopsticks", + unicode_version: "11.0", + }, + { + emoji: "🍽️", + aliases: ["plate_with_cutlery"], + tags: ["dining", "dinner"], + category: "Food & Drink", + description: "fork and knife with plate", + unicode_version: "7.0", + }, + { + emoji: "🍴", + aliases: ["fork_and_knife"], + tags: ["cutlery"], + category: "Food & Drink", + description: "fork and knife", + unicode_version: "6.0", + }, + { + emoji: "🥄", + aliases: ["spoon"], + tags: [], + category: "Food & Drink", + description: "spoon", + unicode_version: "9.0", + }, + { + emoji: "🔪", + aliases: ["hocho", "knife"], + tags: ["cut", "chop"], + category: "Food & Drink", + description: "kitchen knife", + unicode_version: "6.0", + }, + { + emoji: "🏺", + aliases: ["amphora"], + tags: [], + category: "Food & Drink", + description: "amphora", + unicode_version: "8.0", + }, + { + emoji: "🌍", + aliases: ["earth_africa"], + tags: ["globe", "world", "international"], + category: "Travel & Places", + description: "globe showing Europe-Africa", + unicode_version: "6.0", + }, + { + emoji: "🌎", + aliases: ["earth_americas"], + tags: ["globe", "world", "international"], + category: "Travel & Places", + description: "globe showing Americas", + unicode_version: "6.0", + }, + { + emoji: "🌏", + aliases: ["earth_asia"], + tags: ["globe", "world", "international"], + category: "Travel & Places", + description: "globe showing Asia-Australia", + unicode_version: "6.0", + }, + { + emoji: "🌐", + aliases: ["globe_with_meridians"], + tags: ["world", "global", "international"], + category: "Travel & Places", + description: "globe with meridians", + unicode_version: "6.0", + }, + { + emoji: "🗺️", + aliases: ["world_map"], + tags: ["travel"], + category: "Travel & Places", + description: "world map", + unicode_version: "7.0", + }, + { + emoji: "🗾", + aliases: ["japan"], + tags: [], + category: "Travel & Places", + description: "map of Japan", + unicode_version: "6.0", + }, + { + emoji: "🧭", + aliases: ["compass"], + tags: [], + category: "Travel & Places", + description: "compass", + unicode_version: "11.0", + }, + { + emoji: "🏔️", + aliases: ["mountain_snow"], + tags: [], + category: "Travel & Places", + description: "snow-capped mountain", + unicode_version: "7.0", + }, + { + emoji: "⛰️", + aliases: ["mountain"], + tags: [], + category: "Travel & Places", + description: "mountain", + unicode_version: "5.2", + }, + { + emoji: "🌋", + aliases: ["volcano"], + tags: [], + category: "Travel & Places", + description: "volcano", + unicode_version: "6.0", + }, + { + emoji: "🗻", + aliases: ["mount_fuji"], + tags: [], + category: "Travel & Places", + description: "mount fuji", + unicode_version: "6.0", + }, + { + emoji: "🏕️", + aliases: ["camping"], + tags: [], + category: "Travel & Places", + description: "camping", + unicode_version: "7.0", + }, + { + emoji: "🏖️", + aliases: ["beach_umbrella"], + tags: [], + category: "Travel & Places", + description: "beach with umbrella", + unicode_version: "7.0", + }, + { + emoji: "🏜️", + aliases: ["desert"], + tags: [], + category: "Travel & Places", + description: "desert", + unicode_version: "7.0", + }, + { + emoji: "🏝️", + aliases: ["desert_island"], + tags: [], + category: "Travel & Places", + description: "desert island", + unicode_version: "7.0", + }, + { + emoji: "🏞️", + aliases: ["national_park"], + tags: [], + category: "Travel & Places", + description: "national park", + unicode_version: "7.0", + }, + { + emoji: "🏟️", + aliases: ["stadium"], + tags: [], + category: "Travel & Places", + description: "stadium", + unicode_version: "7.0", + }, + { + emoji: "🏛️", + aliases: ["classical_building"], + tags: [], + category: "Travel & Places", + description: "classical building", + unicode_version: "7.0", + }, + { + emoji: "🏗️", + aliases: ["building_construction"], + tags: [], + category: "Travel & Places", + description: "building construction", + unicode_version: "7.0", + }, + { + emoji: "🧱", + aliases: ["bricks"], + tags: [], + category: "Travel & Places", + description: "brick", + unicode_version: "11.0", + }, + { + emoji: "🪨", + aliases: ["rock"], + tags: [], + category: "Travel & Places", + description: "rock", + unicode_version: "13.0", + }, + { + emoji: "🪵", + aliases: ["wood"], + tags: [], + category: "Travel & Places", + description: "wood", + unicode_version: "13.0", + }, + { + emoji: "🛖", + aliases: ["hut"], + tags: [], + category: "Travel & Places", + description: "hut", + unicode_version: "13.0", + }, + { + emoji: "🏘️", + aliases: ["houses"], + tags: [], + category: "Travel & Places", + description: "houses", + unicode_version: "7.0", + }, + { + emoji: "🏚️", + aliases: ["derelict_house"], + tags: [], + category: "Travel & Places", + description: "derelict house", + unicode_version: "7.0", + }, + { + emoji: "🏠", + aliases: ["house"], + tags: [], + category: "Travel & Places", + description: "house", + unicode_version: "6.0", + }, + { + emoji: "🏡", + aliases: ["house_with_garden"], + tags: [], + category: "Travel & Places", + description: "house with garden", + unicode_version: "6.0", + }, + { + emoji: "🏢", + aliases: ["office"], + tags: [], + category: "Travel & Places", + description: "office building", + unicode_version: "6.0", + }, + { + emoji: "🏣", + aliases: ["post_office"], + tags: [], + category: "Travel & Places", + description: "Japanese post office", + unicode_version: "6.0", + }, + { + emoji: "🏤", + aliases: ["european_post_office"], + tags: [], + category: "Travel & Places", + description: "post office", + unicode_version: "6.0", + }, + { + emoji: "🏥", + aliases: ["hospital"], + tags: [], + category: "Travel & Places", + description: "hospital", + unicode_version: "6.0", + }, + { + emoji: "🏦", + aliases: ["bank"], + tags: [], + category: "Travel & Places", + description: "bank", + unicode_version: "6.0", + }, + { + emoji: "🏨", + aliases: ["hotel"], + tags: [], + category: "Travel & Places", + description: "hotel", + unicode_version: "6.0", + }, + { + emoji: "🏩", + aliases: ["love_hotel"], + tags: [], + category: "Travel & Places", + description: "love hotel", + unicode_version: "6.0", + }, + { + emoji: "🏪", + aliases: ["convenience_store"], + tags: [], + category: "Travel & Places", + description: "convenience store", + unicode_version: "6.0", + }, + { + emoji: "🏫", + aliases: ["school"], + tags: [], + category: "Travel & Places", + description: "school", + unicode_version: "6.0", + }, + { + emoji: "🏬", + aliases: ["department_store"], + tags: [], + category: "Travel & Places", + description: "department store", + unicode_version: "6.0", + }, + { + emoji: "🏭", + aliases: ["factory"], + tags: [], + category: "Travel & Places", + description: "factory", + unicode_version: "6.0", + }, + { + emoji: "🏯", + aliases: ["japanese_castle"], + tags: [], + category: "Travel & Places", + description: "Japanese castle", + unicode_version: "6.0", + }, + { + emoji: "🏰", + aliases: ["european_castle"], + tags: [], + category: "Travel & Places", + description: "castle", + unicode_version: "6.0", + }, + { + emoji: "💒", + aliases: ["wedding"], + tags: ["marriage"], + category: "Travel & Places", + description: "wedding", + unicode_version: "6.0", + }, + { + emoji: "🗼", + aliases: ["tokyo_tower"], + tags: [], + category: "Travel & Places", + description: "Tokyo tower", + unicode_version: "6.0", + }, + { + emoji: "🗽", + aliases: ["statue_of_liberty"], + tags: [], + category: "Travel & Places", + description: "Statue of Liberty", + unicode_version: "6.0", + }, + { + emoji: "⛪", + aliases: ["church"], + tags: [], + category: "Travel & Places", + description: "church", + unicode_version: "5.2", + }, + { + emoji: "🕌", + aliases: ["mosque"], + tags: [], + category: "Travel & Places", + description: "mosque", + unicode_version: "8.0", + }, + { + emoji: "🛕", + aliases: ["hindu_temple"], + tags: [], + category: "Travel & Places", + description: "hindu temple", + unicode_version: "12.0", + }, + { + emoji: "🕍", + aliases: ["synagogue"], + tags: [], + category: "Travel & Places", + description: "synagogue", + unicode_version: "8.0", + }, + { + emoji: "⛩️", + aliases: ["shinto_shrine"], + tags: [], + category: "Travel & Places", + description: "shinto shrine", + unicode_version: "5.2", + }, + { + emoji: "🕋", + aliases: ["kaaba"], + tags: [], + category: "Travel & Places", + description: "kaaba", + unicode_version: "8.0", + }, + { + emoji: "⛲", + aliases: ["fountain"], + tags: [], + category: "Travel & Places", + description: "fountain", + unicode_version: "5.2", + }, + { + emoji: "⛺", + aliases: ["tent"], + tags: ["camping"], + category: "Travel & Places", + description: "tent", + unicode_version: "5.2", + }, + { + emoji: "🌁", + aliases: ["foggy"], + tags: ["karl"], + category: "Travel & Places", + description: "foggy", + unicode_version: "6.0", + }, + { + emoji: "🌃", + aliases: ["night_with_stars"], + tags: [], + category: "Travel & Places", + description: "night with stars", + unicode_version: "6.0", + }, + { + emoji: "🏙️", + aliases: ["cityscape"], + tags: ["skyline"], + category: "Travel & Places", + description: "cityscape", + unicode_version: "7.0", + }, + { + emoji: "🌄", + aliases: ["sunrise_over_mountains"], + tags: [], + category: "Travel & Places", + description: "sunrise over mountains", + unicode_version: "6.0", + }, + { + emoji: "🌅", + aliases: ["sunrise"], + tags: [], + category: "Travel & Places", + description: "sunrise", + unicode_version: "6.0", + }, + { + emoji: "🌆", + aliases: ["city_sunset"], + tags: [], + category: "Travel & Places", + description: "cityscape at dusk", + unicode_version: "6.0", + }, + { + emoji: "🌇", + aliases: ["city_sunrise"], + tags: [], + category: "Travel & Places", + description: "sunset", + unicode_version: "6.0", + }, + { + emoji: "🌉", + aliases: ["bridge_at_night"], + tags: [], + category: "Travel & Places", + description: "bridge at night", + unicode_version: "6.0", + }, + { + emoji: "♨️", + aliases: ["hotsprings"], + tags: [], + category: "Travel & Places", + description: "hot springs", + unicode_version: "", + }, + { + emoji: "🎠", + aliases: ["carousel_horse"], + tags: [], + category: "Travel & Places", + description: "carousel horse", + unicode_version: "6.0", + }, + { + emoji: "🎡", + aliases: ["ferris_wheel"], + tags: [], + category: "Travel & Places", + description: "ferris wheel", + unicode_version: "6.0", + }, + { + emoji: "🎢", + aliases: ["roller_coaster"], + tags: [], + category: "Travel & Places", + description: "roller coaster", + unicode_version: "6.0", + }, + { + emoji: "💈", + aliases: ["barber"], + tags: [], + category: "Travel & Places", + description: "barber pole", + unicode_version: "6.0", + }, + { + emoji: "🎪", + aliases: ["circus_tent"], + tags: [], + category: "Travel & Places", + description: "circus tent", + unicode_version: "6.0", + }, + { + emoji: "🚂", + aliases: ["steam_locomotive"], + tags: ["train"], + category: "Travel & Places", + description: "locomotive", + unicode_version: "6.0", + }, + { + emoji: "🚃", + aliases: ["railway_car"], + tags: [], + category: "Travel & Places", + description: "railway car", + unicode_version: "6.0", + }, + { + emoji: "🚄", + aliases: ["bullettrain_side"], + tags: ["train"], + category: "Travel & Places", + description: "high-speed train", + unicode_version: "6.0", + }, + { + emoji: "🚅", + aliases: ["bullettrain_front"], + tags: ["train"], + category: "Travel & Places", + description: "bullet train", + unicode_version: "6.0", + }, + { + emoji: "🚆", + aliases: ["train2"], + tags: [], + category: "Travel & Places", + description: "train", + unicode_version: "6.0", + }, + { + emoji: "🚇", + aliases: ["metro"], + tags: [], + category: "Travel & Places", + description: "metro", + unicode_version: "6.0", + }, + { + emoji: "🚈", + aliases: ["light_rail"], + tags: [], + category: "Travel & Places", + description: "light rail", + unicode_version: "6.0", + }, + { + emoji: "🚉", + aliases: ["station"], + tags: [], + category: "Travel & Places", + description: "station", + unicode_version: "6.0", + }, + { + emoji: "🚊", + aliases: ["tram"], + tags: [], + category: "Travel & Places", + description: "tram", + unicode_version: "6.0", + }, + { + emoji: "🚝", + aliases: ["monorail"], + tags: [], + category: "Travel & Places", + description: "monorail", + unicode_version: "6.0", + }, + { + emoji: "🚞", + aliases: ["mountain_railway"], + tags: [], + category: "Travel & Places", + description: "mountain railway", + unicode_version: "6.0", + }, + { + emoji: "🚋", + aliases: ["train"], + tags: [], + category: "Travel & Places", + description: "tram car", + unicode_version: "6.0", + }, + { + emoji: "🚌", + aliases: ["bus"], + tags: [], + category: "Travel & Places", + description: "bus", + unicode_version: "6.0", + }, + { + emoji: "🚍", + aliases: ["oncoming_bus"], + tags: [], + category: "Travel & Places", + description: "oncoming bus", + unicode_version: "6.0", + }, + { + emoji: "🚎", + aliases: ["trolleybus"], + tags: [], + category: "Travel & Places", + description: "trolleybus", + unicode_version: "6.0", + }, + { + emoji: "🚐", + aliases: ["minibus"], + tags: [], + category: "Travel & Places", + description: "minibus", + unicode_version: "6.0", + }, + { + emoji: "🚑", + aliases: ["ambulance"], + tags: [], + category: "Travel & Places", + description: "ambulance", + unicode_version: "6.0", + }, + { + emoji: "🚒", + aliases: ["fire_engine"], + tags: [], + category: "Travel & Places", + description: "fire engine", + unicode_version: "6.0", + }, + { + emoji: "🚓", + aliases: ["police_car"], + tags: [], + category: "Travel & Places", + description: "police car", + unicode_version: "6.0", + }, + { + emoji: "🚔", + aliases: ["oncoming_police_car"], + tags: [], + category: "Travel & Places", + description: "oncoming police car", + unicode_version: "6.0", + }, + { + emoji: "🚕", + aliases: ["taxi"], + tags: [], + category: "Travel & Places", + description: "taxi", + unicode_version: "6.0", + }, + { + emoji: "🚖", + aliases: ["oncoming_taxi"], + tags: [], + category: "Travel & Places", + description: "oncoming taxi", + unicode_version: "6.0", + }, + { + emoji: "🚗", + aliases: ["car", "red_car"], + tags: [], + category: "Travel & Places", + description: "automobile", + unicode_version: "6.0", + }, + { + emoji: "🚘", + aliases: ["oncoming_automobile"], + tags: [], + category: "Travel & Places", + description: "oncoming automobile", + unicode_version: "6.0", + }, + { + emoji: "🚙", + aliases: ["blue_car"], + tags: [], + category: "Travel & Places", + description: "sport utility vehicle", + unicode_version: "6.0", + }, + { + emoji: "🛻", + aliases: ["pickup_truck"], + tags: [], + category: "Travel & Places", + description: "pickup truck", + unicode_version: "13.0", + }, + { + emoji: "🚚", + aliases: ["truck"], + tags: [], + category: "Travel & Places", + description: "delivery truck", + unicode_version: "6.0", + }, + { + emoji: "🚛", + aliases: ["articulated_lorry"], + tags: [], + category: "Travel & Places", + description: "articulated lorry", + unicode_version: "6.0", + }, + { + emoji: "🚜", + aliases: ["tractor"], + tags: [], + category: "Travel & Places", + description: "tractor", + unicode_version: "6.0", + }, + { + emoji: "🏎️", + aliases: ["racing_car"], + tags: [], + category: "Travel & Places", + description: "racing car", + unicode_version: "7.0", + }, + { + emoji: "🏍️", + aliases: ["motorcycle"], + tags: [], + category: "Travel & Places", + description: "motorcycle", + unicode_version: "7.0", + }, + { + emoji: "🛵", + aliases: ["motor_scooter"], + tags: [], + category: "Travel & Places", + description: "motor scooter", + unicode_version: "9.0", + }, + { + emoji: "🦽", + aliases: ["manual_wheelchair"], + tags: [], + category: "Travel & Places", + description: "manual wheelchair", + unicode_version: "12.0", + }, + { + emoji: "🦼", + aliases: ["motorized_wheelchair"], + tags: [], + category: "Travel & Places", + description: "motorized wheelchair", + unicode_version: "12.0", + }, + { + emoji: "🛺", + aliases: ["auto_rickshaw"], + tags: [], + category: "Travel & Places", + description: "auto rickshaw", + unicode_version: "12.0", + }, + { + emoji: "🚲", + aliases: ["bike"], + tags: ["bicycle"], + category: "Travel & Places", + description: "bicycle", + unicode_version: "6.0", + }, + { + emoji: "🛴", + aliases: ["kick_scooter"], + tags: [], + category: "Travel & Places", + description: "kick scooter", + unicode_version: "9.0", + }, + { + emoji: "🛹", + aliases: ["skateboard"], + tags: [], + category: "Travel & Places", + description: "skateboard", + unicode_version: "11.0", + }, + { + emoji: "🛼", + aliases: ["roller_skate"], + tags: [], + category: "Travel & Places", + description: "roller skate", + unicode_version: "13.0", + }, + { + emoji: "🚏", + aliases: ["busstop"], + tags: [], + category: "Travel & Places", + description: "bus stop", + unicode_version: "6.0", + }, + { + emoji: "🛣️", + aliases: ["motorway"], + tags: [], + category: "Travel & Places", + description: "motorway", + unicode_version: "7.0", + }, + { + emoji: "🛤️", + aliases: ["railway_track"], + tags: [], + category: "Travel & Places", + description: "railway track", + unicode_version: "7.0", + }, + { + emoji: "🛢️", + aliases: ["oil_drum"], + tags: [], + category: "Travel & Places", + description: "oil drum", + unicode_version: "7.0", + }, + { + emoji: "⛽", + aliases: ["fuelpump"], + tags: [], + category: "Travel & Places", + description: "fuel pump", + unicode_version: "5.2", + }, + { + emoji: "🚨", + aliases: ["rotating_light"], + tags: ["911", "emergency"], + category: "Travel & Places", + description: "police car light", + unicode_version: "6.0", + }, + { + emoji: "🚥", + aliases: ["traffic_light"], + tags: [], + category: "Travel & Places", + description: "horizontal traffic light", + unicode_version: "6.0", + }, + { + emoji: "🚦", + aliases: ["vertical_traffic_light"], + tags: ["semaphore"], + category: "Travel & Places", + description: "vertical traffic light", + unicode_version: "6.0", + }, + { + emoji: "🛑", + aliases: ["stop_sign"], + tags: [], + category: "Travel & Places", + description: "stop sign", + unicode_version: "9.0", + }, + { + emoji: "🚧", + aliases: ["construction"], + tags: ["wip"], + category: "Travel & Places", + description: "construction", + unicode_version: "6.0", + }, + { + emoji: "⚓", + aliases: ["anchor"], + tags: ["ship"], + category: "Travel & Places", + description: "anchor", + unicode_version: "4.1", + }, + { + emoji: "⛵", + aliases: ["boat", "sailboat"], + tags: [], + category: "Travel & Places", + description: "sailboat", + unicode_version: "5.2", + }, + { + emoji: "🛶", + aliases: ["canoe"], + tags: [], + category: "Travel & Places", + description: "canoe", + unicode_version: "9.0", + }, + { + emoji: "🚤", + aliases: ["speedboat"], + tags: ["ship"], + category: "Travel & Places", + description: "speedboat", + unicode_version: "6.0", + }, + { + emoji: "🛳️", + aliases: ["passenger_ship"], + tags: ["cruise"], + category: "Travel & Places", + description: "passenger ship", + unicode_version: "7.0", + }, + { + emoji: "⛴️", + aliases: ["ferry"], + tags: [], + category: "Travel & Places", + description: "ferry", + unicode_version: "5.2", + }, + { + emoji: "🛥️", + aliases: ["motor_boat"], + tags: [], + category: "Travel & Places", + description: "motor boat", + unicode_version: "7.0", + }, + { + emoji: "🚢", + aliases: ["ship"], + tags: [], + category: "Travel & Places", + description: "ship", + unicode_version: "6.0", + }, + { + emoji: "✈️", + aliases: ["airplane"], + tags: ["flight"], + category: "Travel & Places", + description: "airplane", + unicode_version: "", + }, + { + emoji: "🛩️", + aliases: ["small_airplane"], + tags: ["flight"], + category: "Travel & Places", + description: "small airplane", + unicode_version: "7.0", + }, + { + emoji: "🛫", + aliases: ["flight_departure"], + tags: [], + category: "Travel & Places", + description: "airplane departure", + unicode_version: "7.0", + }, + { + emoji: "🛬", + aliases: ["flight_arrival"], + tags: [], + category: "Travel & Places", + description: "airplane arrival", + unicode_version: "7.0", + }, + { + emoji: "🪂", + aliases: ["parachute"], + tags: [], + category: "Travel & Places", + description: "parachute", + unicode_version: "12.0", + }, + { + emoji: "💺", + aliases: ["seat"], + tags: [], + category: "Travel & Places", + description: "seat", + unicode_version: "6.0", + }, + { + emoji: "🚁", + aliases: ["helicopter"], + tags: [], + category: "Travel & Places", + description: "helicopter", + unicode_version: "6.0", + }, + { + emoji: "🚟", + aliases: ["suspension_railway"], + tags: [], + category: "Travel & Places", + description: "suspension railway", + unicode_version: "6.0", + }, + { + emoji: "🚠", + aliases: ["mountain_cableway"], + tags: [], + category: "Travel & Places", + description: "mountain cableway", + unicode_version: "6.0", + }, + { + emoji: "🚡", + aliases: ["aerial_tramway"], + tags: [], + category: "Travel & Places", + description: "aerial tramway", + unicode_version: "6.0", + }, + { + emoji: "🛰️", + aliases: ["artificial_satellite"], + tags: ["orbit", "space"], + category: "Travel & Places", + description: "satellite", + unicode_version: "7.0", + }, + { + emoji: "🚀", + aliases: ["rocket"], + tags: ["ship", "launch"], + category: "Travel & Places", + description: "rocket", + unicode_version: "6.0", + }, + { + emoji: "🛸", + aliases: ["flying_saucer"], + tags: ["ufo"], + category: "Travel & Places", + description: "flying saucer", + unicode_version: "11.0", + }, + { + emoji: "🛎️", + aliases: ["bellhop_bell"], + tags: [], + category: "Travel & Places", + description: "bellhop bell", + unicode_version: "7.0", + }, + { + emoji: "🧳", + aliases: ["luggage"], + tags: [], + category: "Travel & Places", + description: "luggage", + unicode_version: "11.0", + }, + { + emoji: "⌛", + aliases: ["hourglass"], + tags: ["time"], + category: "Travel & Places", + description: "hourglass done", + unicode_version: "", + }, + { + emoji: "⏳", + aliases: ["hourglass_flowing_sand"], + tags: ["time"], + category: "Travel & Places", + description: "hourglass not done", + unicode_version: "6.0", + }, + { + emoji: "⌚", + aliases: ["watch"], + tags: ["time"], + category: "Travel & Places", + description: "watch", + unicode_version: "", + }, + { + emoji: "⏰", + aliases: ["alarm_clock"], + tags: ["morning"], + category: "Travel & Places", + description: "alarm clock", + unicode_version: "6.0", + }, + { + emoji: "⏱️", + aliases: ["stopwatch"], + tags: [], + category: "Travel & Places", + description: "stopwatch", + unicode_version: "6.0", + }, + { + emoji: "⏲️", + aliases: ["timer_clock"], + tags: [], + category: "Travel & Places", + description: "timer clock", + unicode_version: "6.0", + }, + { + emoji: "🕰️", + aliases: ["mantelpiece_clock"], + tags: [], + category: "Travel & Places", + description: "mantelpiece clock", + unicode_version: "7.0", + }, + { + emoji: "🕛", + aliases: ["clock12"], + tags: [], + category: "Travel & Places", + description: "twelve o’clock", + unicode_version: "6.0", + }, + { + emoji: "🕧", + aliases: ["clock1230"], + tags: [], + category: "Travel & Places", + description: "twelve-thirty", + unicode_version: "6.0", + }, + { + emoji: "🕐", + aliases: ["clock1"], + tags: [], + category: "Travel & Places", + description: "one o’clock", + unicode_version: "6.0", + }, + { + emoji: "🕜", + aliases: ["clock130"], + tags: [], + category: "Travel & Places", + description: "one-thirty", + unicode_version: "6.0", + }, + { + emoji: "🕑", + aliases: ["clock2"], + tags: [], + category: "Travel & Places", + description: "two o’clock", + unicode_version: "6.0", + }, + { + emoji: "🕝", + aliases: ["clock230"], + tags: [], + category: "Travel & Places", + description: "two-thirty", + unicode_version: "6.0", + }, + { + emoji: "🕒", + aliases: ["clock3"], + tags: [], + category: "Travel & Places", + description: "three o’clock", + unicode_version: "6.0", + }, + { + emoji: "🕞", + aliases: ["clock330"], + tags: [], + category: "Travel & Places", + description: "three-thirty", + unicode_version: "6.0", + }, + { + emoji: "🕓", + aliases: ["clock4"], + tags: [], + category: "Travel & Places", + description: "four o’clock", + unicode_version: "6.0", + }, + { + emoji: "🕟", + aliases: ["clock430"], + tags: [], + category: "Travel & Places", + description: "four-thirty", + unicode_version: "6.0", + }, + { + emoji: "🕔", + aliases: ["clock5"], + tags: [], + category: "Travel & Places", + description: "five o’clock", + unicode_version: "6.0", + }, + { + emoji: "🕠", + aliases: ["clock530"], + tags: [], + category: "Travel & Places", + description: "five-thirty", + unicode_version: "6.0", + }, + { + emoji: "🕕", + aliases: ["clock6"], + tags: [], + category: "Travel & Places", + description: "six o’clock", + unicode_version: "6.0", + }, + { + emoji: "🕡", + aliases: ["clock630"], + tags: [], + category: "Travel & Places", + description: "six-thirty", + unicode_version: "6.0", + }, + { + emoji: "🕖", + aliases: ["clock7"], + tags: [], + category: "Travel & Places", + description: "seven o’clock", + unicode_version: "6.0", + }, + { + emoji: "🕢", + aliases: ["clock730"], + tags: [], + category: "Travel & Places", + description: "seven-thirty", + unicode_version: "6.0", + }, + { + emoji: "🕗", + aliases: ["clock8"], + tags: [], + category: "Travel & Places", + description: "eight o’clock", + unicode_version: "6.0", + }, + { + emoji: "🕣", + aliases: ["clock830"], + tags: [], + category: "Travel & Places", + description: "eight-thirty", + unicode_version: "6.0", + }, + { + emoji: "🕘", + aliases: ["clock9"], + tags: [], + category: "Travel & Places", + description: "nine o’clock", + unicode_version: "6.0", + }, + { + emoji: "🕤", + aliases: ["clock930"], + tags: [], + category: "Travel & Places", + description: "nine-thirty", + unicode_version: "6.0", + }, + { + emoji: "🕙", + aliases: ["clock10"], + tags: [], + category: "Travel & Places", + description: "ten o’clock", + unicode_version: "6.0", + }, + { + emoji: "🕥", + aliases: ["clock1030"], + tags: [], + category: "Travel & Places", + description: "ten-thirty", + unicode_version: "6.0", + }, + { + emoji: "🕚", + aliases: ["clock11"], + tags: [], + category: "Travel & Places", + description: "eleven o’clock", + unicode_version: "6.0", + }, + { + emoji: "🕦", + aliases: ["clock1130"], + tags: [], + category: "Travel & Places", + description: "eleven-thirty", + unicode_version: "6.0", + }, + { + emoji: "🌑", + aliases: ["new_moon"], + tags: [], + category: "Travel & Places", + description: "new moon", + unicode_version: "6.0", + }, + { + emoji: "🌒", + aliases: ["waxing_crescent_moon"], + tags: [], + category: "Travel & Places", + description: "waxing crescent moon", + unicode_version: "6.0", + }, + { + emoji: "🌓", + aliases: ["first_quarter_moon"], + tags: [], + category: "Travel & Places", + description: "first quarter moon", + unicode_version: "6.0", + }, + { + emoji: "🌔", + aliases: ["moon", "waxing_gibbous_moon"], + tags: [], + category: "Travel & Places", + description: "waxing gibbous moon", + unicode_version: "6.0", + }, + { + emoji: "🌕", + aliases: ["full_moon"], + tags: [], + category: "Travel & Places", + description: "full moon", + unicode_version: "6.0", + }, + { + emoji: "🌖", + aliases: ["waning_gibbous_moon"], + tags: [], + category: "Travel & Places", + description: "waning gibbous moon", + unicode_version: "6.0", + }, + { + emoji: "🌗", + aliases: ["last_quarter_moon"], + tags: [], + category: "Travel & Places", + description: "last quarter moon", + unicode_version: "6.0", + }, + { + emoji: "🌘", + aliases: ["waning_crescent_moon"], + tags: [], + category: "Travel & Places", + description: "waning crescent moon", + unicode_version: "6.0", + }, + { + emoji: "🌙", + aliases: ["crescent_moon"], + tags: ["night"], + category: "Travel & Places", + description: "crescent moon", + unicode_version: "6.0", + }, + { + emoji: "🌚", + aliases: ["new_moon_with_face"], + tags: [], + category: "Travel & Places", + description: "new moon face", + unicode_version: "6.0", + }, + { + emoji: "🌛", + aliases: ["first_quarter_moon_with_face"], + tags: [], + category: "Travel & Places", + description: "first quarter moon face", + unicode_version: "6.0", + }, + { + emoji: "🌜", + aliases: ["last_quarter_moon_with_face"], + tags: [], + category: "Travel & Places", + description: "last quarter moon face", + unicode_version: "6.0", + }, + { + emoji: "🌡️", + aliases: ["thermometer"], + tags: [], + category: "Travel & Places", + description: "thermometer", + unicode_version: "7.0", + }, + { + emoji: "☀️", + aliases: ["sunny"], + tags: ["weather"], + category: "Travel & Places", + description: "sun", + unicode_version: "", + }, + { + emoji: "🌝", + aliases: ["full_moon_with_face"], + tags: [], + category: "Travel & Places", + description: "full moon face", + unicode_version: "6.0", + }, + { + emoji: "🌞", + aliases: ["sun_with_face"], + tags: ["summer"], + category: "Travel & Places", + description: "sun with face", + unicode_version: "6.0", + }, + { + emoji: "🪐", + aliases: ["ringed_planet"], + tags: [], + category: "Travel & Places", + description: "ringed planet", + unicode_version: "12.0", + }, + { + emoji: "⭐", + aliases: ["star"], + tags: [], + category: "Travel & Places", + description: "star", + unicode_version: "5.1", + }, + { + emoji: "🌟", + aliases: ["star2"], + tags: [], + category: "Travel & Places", + description: "glowing star", + unicode_version: "6.0", + }, + { + emoji: "🌠", + aliases: ["stars"], + tags: [], + category: "Travel & Places", + description: "shooting star", + unicode_version: "6.0", + }, + { + emoji: "🌌", + aliases: ["milky_way"], + tags: [], + category: "Travel & Places", + description: "milky way", + unicode_version: "6.0", + }, + { + emoji: "☁️", + aliases: ["cloud"], + tags: [], + category: "Travel & Places", + description: "cloud", + unicode_version: "", + }, + { + emoji: "⛅", + aliases: ["partly_sunny"], + tags: ["weather", "cloud"], + category: "Travel & Places", + description: "sun behind cloud", + unicode_version: "5.2", + }, + { + emoji: "⛈️", + aliases: ["cloud_with_lightning_and_rain"], + tags: [], + category: "Travel & Places", + description: "cloud with lightning and rain", + unicode_version: "5.2", + }, + { + emoji: "🌤️", + aliases: ["sun_behind_small_cloud"], + tags: [], + category: "Travel & Places", + description: "sun behind small cloud", + unicode_version: "7.0", + }, + { + emoji: "🌥️", + aliases: ["sun_behind_large_cloud"], + tags: [], + category: "Travel & Places", + description: "sun behind large cloud", + unicode_version: "7.0", + }, + { + emoji: "🌦️", + aliases: ["sun_behind_rain_cloud"], + tags: [], + category: "Travel & Places", + description: "sun behind rain cloud", + unicode_version: "7.0", + }, + { + emoji: "🌧️", + aliases: ["cloud_with_rain"], + tags: [], + category: "Travel & Places", + description: "cloud with rain", + unicode_version: "7.0", + }, + { + emoji: "🌨️", + aliases: ["cloud_with_snow"], + tags: [], + category: "Travel & Places", + description: "cloud with snow", + unicode_version: "7.0", + }, + { + emoji: "🌩️", + aliases: ["cloud_with_lightning"], + tags: [], + category: "Travel & Places", + description: "cloud with lightning", + unicode_version: "7.0", + }, + { + emoji: "🌪️", + aliases: ["tornado"], + tags: [], + category: "Travel & Places", + description: "tornado", + unicode_version: "7.0", + }, + { + emoji: "🌫️", + aliases: ["fog"], + tags: [], + category: "Travel & Places", + description: "fog", + unicode_version: "7.0", + }, + { + emoji: "🌬️", + aliases: ["wind_face"], + tags: [], + category: "Travel & Places", + description: "wind face", + unicode_version: "7.0", + }, + { + emoji: "🌀", + aliases: ["cyclone"], + tags: ["swirl"], + category: "Travel & Places", + description: "cyclone", + unicode_version: "6.0", + }, + { + emoji: "🌈", + aliases: ["rainbow"], + tags: [], + category: "Travel & Places", + description: "rainbow", + unicode_version: "6.0", + }, + { + emoji: "🌂", + aliases: ["closed_umbrella"], + tags: ["weather", "rain"], + category: "Travel & Places", + description: "closed umbrella", + unicode_version: "6.0", + }, + { + emoji: "☂️", + aliases: ["open_umbrella"], + tags: [], + category: "Travel & Places", + description: "umbrella", + unicode_version: "", + }, + { + emoji: "☔", + aliases: ["umbrella"], + tags: ["rain", "weather"], + category: "Travel & Places", + description: "umbrella with rain drops", + unicode_version: "4.0", + }, + { + emoji: "⛱️", + aliases: ["parasol_on_ground"], + tags: ["beach_umbrella"], + category: "Travel & Places", + description: "umbrella on ground", + unicode_version: "5.2", + }, + { + emoji: "⚡", + aliases: ["zap"], + tags: ["lightning", "thunder"], + category: "Travel & Places", + description: "high voltage", + unicode_version: "4.0", + }, + { + emoji: "❄️", + aliases: ["snowflake"], + tags: ["winter", "cold", "weather"], + category: "Travel & Places", + description: "snowflake", + unicode_version: "", + }, + { + emoji: "☃️", + aliases: ["snowman_with_snow"], + tags: ["winter", "christmas"], + category: "Travel & Places", + description: "snowman", + unicode_version: "", + }, + { + emoji: "⛄", + aliases: ["snowman"], + tags: ["winter"], + category: "Travel & Places", + description: "snowman without snow", + unicode_version: "5.2", + }, + { + emoji: "☄️", + aliases: ["comet"], + tags: [], + category: "Travel & Places", + description: "comet", + unicode_version: "", + }, + { + emoji: "🔥", + aliases: ["fire"], + tags: ["burn"], + category: "Travel & Places", + description: "fire", + unicode_version: "6.0", + }, + { + emoji: "💧", + aliases: ["droplet"], + tags: ["water"], + category: "Travel & Places", + description: "droplet", + unicode_version: "6.0", + }, + { + emoji: "🌊", + aliases: ["ocean"], + tags: ["sea"], + category: "Travel & Places", + description: "water wave", + unicode_version: "6.0", + }, + { + emoji: "🎃", + aliases: ["jack_o_lantern"], + tags: ["halloween"], + category: "Activities", + description: "jack-o-lantern", + unicode_version: "6.0", + }, + { + emoji: "🎄", + aliases: ["christmas_tree"], + tags: [], + category: "Activities", + description: "Christmas tree", + unicode_version: "6.0", + }, + { + emoji: "🎆", + aliases: ["fireworks"], + tags: ["festival", "celebration"], + category: "Activities", + description: "fireworks", + unicode_version: "6.0", + }, + { + emoji: "🎇", + aliases: ["sparkler"], + tags: [], + category: "Activities", + description: "sparkler", + unicode_version: "6.0", + }, + { + emoji: "🧨", + aliases: ["firecracker"], + tags: [], + category: "Activities", + description: "firecracker", + unicode_version: "11.0", + }, + { + emoji: "✨", + aliases: ["sparkles"], + tags: ["shiny"], + category: "Activities", + description: "sparkles", + unicode_version: "6.0", + }, + { + emoji: "🎈", + aliases: ["balloon"], + tags: ["party", "birthday"], + category: "Activities", + description: "balloon", + unicode_version: "6.0", + }, + { + emoji: "🎉", + aliases: ["tada"], + tags: ["hooray", "party"], + category: "Activities", + description: "party popper", + unicode_version: "6.0", + }, + { + emoji: "🎊", + aliases: ["confetti_ball"], + tags: [], + category: "Activities", + description: "confetti ball", + unicode_version: "6.0", + }, + { + emoji: "🎋", + aliases: ["tanabata_tree"], + tags: [], + category: "Activities", + description: "tanabata tree", + unicode_version: "6.0", + }, + { + emoji: "🎍", + aliases: ["bamboo"], + tags: [], + category: "Activities", + description: "pine decoration", + unicode_version: "6.0", + }, + { + emoji: "🎎", + aliases: ["dolls"], + tags: [], + category: "Activities", + description: "Japanese dolls", + unicode_version: "6.0", + }, + { + emoji: "🎏", + aliases: ["flags"], + tags: [], + category: "Activities", + description: "carp streamer", + unicode_version: "6.0", + }, + { + emoji: "🎐", + aliases: ["wind_chime"], + tags: [], + category: "Activities", + description: "wind chime", + unicode_version: "6.0", + }, + { + emoji: "🎑", + aliases: ["rice_scene"], + tags: [], + category: "Activities", + description: "moon viewing ceremony", + unicode_version: "6.0", + }, + { + emoji: "🧧", + aliases: ["red_envelope"], + tags: [], + category: "Activities", + description: "red envelope", + unicode_version: "11.0", + }, + { + emoji: "🎀", + aliases: ["ribbon"], + tags: [], + category: "Activities", + description: "ribbon", + unicode_version: "6.0", + }, + { + emoji: "🎁", + aliases: ["gift"], + tags: ["present", "birthday", "christmas"], + category: "Activities", + description: "wrapped gift", + unicode_version: "6.0", + }, + { + emoji: "🎗️", + aliases: ["reminder_ribbon"], + tags: [], + category: "Activities", + description: "reminder ribbon", + unicode_version: "7.0", + }, + { + emoji: "🎟️", + aliases: ["tickets"], + tags: [], + category: "Activities", + description: "admission tickets", + unicode_version: "7.0", + }, + { + emoji: "🎫", + aliases: ["ticket"], + tags: [], + category: "Activities", + description: "ticket", + unicode_version: "6.0", + }, + { + emoji: "🎖️", + aliases: ["medal_military"], + tags: [], + category: "Activities", + description: "military medal", + unicode_version: "7.0", + }, + { + emoji: "🏆", + aliases: ["trophy"], + tags: ["award", "contest", "winner"], + category: "Activities", + description: "trophy", + unicode_version: "6.0", + }, + { + emoji: "🏅", + aliases: ["medal_sports"], + tags: ["gold", "winner"], + category: "Activities", + description: "sports medal", + unicode_version: "7.0", + }, + { + emoji: "🥇", + aliases: ["1st_place_medal"], + tags: ["gold"], + category: "Activities", + description: "1st place medal", + unicode_version: "9.0", + }, + { + emoji: "🥈", + aliases: ["2nd_place_medal"], + tags: ["silver"], + category: "Activities", + description: "2nd place medal", + unicode_version: "9.0", + }, + { + emoji: "🥉", + aliases: ["3rd_place_medal"], + tags: ["bronze"], + category: "Activities", + description: "3rd place medal", + unicode_version: "9.0", + }, + { + emoji: "⚽", + aliases: ["soccer"], + tags: ["sports"], + category: "Activities", + description: "soccer ball", + unicode_version: "5.2", + }, + { + emoji: "⚾", + aliases: ["baseball"], + tags: ["sports"], + category: "Activities", + description: "baseball", + unicode_version: "5.2", + }, + { + emoji: "🥎", + aliases: ["softball"], + tags: [], + category: "Activities", + description: "softball", + unicode_version: "11.0", + }, + { + emoji: "🏀", + aliases: ["basketball"], + tags: ["sports"], + category: "Activities", + description: "basketball", + unicode_version: "6.0", + }, + { + emoji: "🏐", + aliases: ["volleyball"], + tags: [], + category: "Activities", + description: "volleyball", + unicode_version: "8.0", + }, + { + emoji: "🏈", + aliases: ["football"], + tags: ["sports"], + category: "Activities", + description: "american football", + unicode_version: "6.0", + }, + { + emoji: "🏉", + aliases: ["rugby_football"], + tags: [], + category: "Activities", + description: "rugby football", + unicode_version: "6.0", + }, + { + emoji: "🎾", + aliases: ["tennis"], + tags: ["sports"], + category: "Activities", + description: "tennis", + unicode_version: "6.0", + }, + { + emoji: "🥏", + aliases: ["flying_disc"], + tags: [], + category: "Activities", + description: "flying disc", + unicode_version: "11.0", + }, + { + emoji: "🎳", + aliases: ["bowling"], + tags: [], + category: "Activities", + description: "bowling", + unicode_version: "6.0", + }, + { + emoji: "🏏", + aliases: ["cricket_game"], + tags: [], + category: "Activities", + description: "cricket game", + unicode_version: "8.0", + }, + { + emoji: "🏑", + aliases: ["field_hockey"], + tags: [], + category: "Activities", + description: "field hockey", + unicode_version: "8.0", + }, + { + emoji: "🏒", + aliases: ["ice_hockey"], + tags: [], + category: "Activities", + description: "ice hockey", + unicode_version: "8.0", + }, + { + emoji: "🥍", + aliases: ["lacrosse"], + tags: [], + category: "Activities", + description: "lacrosse", + unicode_version: "11.0", + }, + { + emoji: "🏓", + aliases: ["ping_pong"], + tags: [], + category: "Activities", + description: "ping pong", + unicode_version: "8.0", + }, + { + emoji: "🏸", + aliases: ["badminton"], + tags: [], + category: "Activities", + description: "badminton", + unicode_version: "8.0", + }, + { + emoji: "🥊", + aliases: ["boxing_glove"], + tags: [], + category: "Activities", + description: "boxing glove", + unicode_version: "9.0", + }, + { + emoji: "🥋", + aliases: ["martial_arts_uniform"], + tags: [], + category: "Activities", + description: "martial arts uniform", + unicode_version: "9.0", + }, + { + emoji: "🥅", + aliases: ["goal_net"], + tags: [], + category: "Activities", + description: "goal net", + unicode_version: "9.0", + }, + { + emoji: "⛳", + aliases: ["golf"], + tags: [], + category: "Activities", + description: "flag in hole", + unicode_version: "5.2", + }, + { + emoji: "⛸️", + aliases: ["ice_skate"], + tags: ["skating"], + category: "Activities", + description: "ice skate", + unicode_version: "5.2", + }, + { + emoji: "🎣", + aliases: ["fishing_pole_and_fish"], + tags: [], + category: "Activities", + description: "fishing pole", + unicode_version: "6.0", + }, + { + emoji: "🤿", + aliases: ["diving_mask"], + tags: [], + category: "Activities", + description: "diving mask", + unicode_version: "12.0", + }, + { + emoji: "🎽", + aliases: ["running_shirt_with_sash"], + tags: ["marathon"], + category: "Activities", + description: "running shirt", + unicode_version: "6.0", + }, + { + emoji: "🎿", + aliases: ["ski"], + tags: [], + category: "Activities", + description: "skis", + unicode_version: "6.0", + }, + { + emoji: "🛷", + aliases: ["sled"], + tags: [], + category: "Activities", + description: "sled", + unicode_version: "11.0", + }, + { + emoji: "🥌", + aliases: ["curling_stone"], + tags: [], + category: "Activities", + description: "curling stone", + unicode_version: "11.0", + }, + { + emoji: "🎯", + aliases: ["dart"], + tags: ["target"], + category: "Activities", + description: "bullseye", + unicode_version: "6.0", + }, + { + emoji: "🪀", + aliases: ["yo_yo"], + tags: [], + category: "Activities", + description: "yo-yo", + unicode_version: "12.0", + }, + { + emoji: "🪁", + aliases: ["kite"], + tags: [], + category: "Activities", + description: "kite", + unicode_version: "12.0", + }, + { + emoji: "🎱", + aliases: ["8ball"], + tags: ["pool", "billiards"], + category: "Activities", + description: "pool 8 ball", + unicode_version: "6.0", + }, + { + emoji: "🔮", + aliases: ["crystal_ball"], + tags: ["fortune"], + category: "Activities", + description: "crystal ball", + unicode_version: "6.0", + }, + { + emoji: "🪄", + aliases: ["magic_wand"], + tags: [], + category: "Activities", + description: "magic wand", + unicode_version: "13.0", + }, + { + emoji: "🧿", + aliases: ["nazar_amulet"], + tags: [], + category: "Activities", + description: "nazar amulet", + unicode_version: "11.0", + }, + { + emoji: "🎮", + aliases: ["video_game"], + tags: ["play", "controller", "console"], + category: "Activities", + description: "video game", + unicode_version: "6.0", + }, + { + emoji: "🕹️", + aliases: ["joystick"], + tags: [], + category: "Activities", + description: "joystick", + unicode_version: "7.0", + }, + { + emoji: "🎰", + aliases: ["slot_machine"], + tags: [], + category: "Activities", + description: "slot machine", + unicode_version: "6.0", + }, + { + emoji: "🎲", + aliases: ["game_die"], + tags: ["dice", "gambling"], + category: "Activities", + description: "game die", + unicode_version: "6.0", + }, + { + emoji: "🧩", + aliases: ["jigsaw"], + tags: [], + category: "Activities", + description: "puzzle piece", + unicode_version: "11.0", + }, + { + emoji: "🧸", + aliases: ["teddy_bear"], + tags: [], + category: "Activities", + description: "teddy bear", + unicode_version: "11.0", + }, + { + emoji: "🪅", + aliases: ["pinata"], + tags: [], + category: "Activities", + description: "piñata", + unicode_version: "13.0", + }, + { + emoji: "🪆", + aliases: ["nesting_dolls"], + tags: [], + category: "Activities", + description: "nesting dolls", + unicode_version: "13.0", + }, + { + emoji: "♠️", + aliases: ["spades"], + tags: [], + category: "Activities", + description: "spade suit", + unicode_version: "", + }, + { + emoji: "♥️", + aliases: ["hearts"], + tags: [], + category: "Activities", + description: "heart suit", + unicode_version: "", + }, + { + emoji: "♦️", + aliases: ["diamonds"], + tags: [], + category: "Activities", + description: "diamond suit", + unicode_version: "", + }, + { + emoji: "♣️", + aliases: ["clubs"], + tags: [], + category: "Activities", + description: "club suit", + unicode_version: "", + }, + { + emoji: "♟️", + aliases: ["chess_pawn"], + tags: [], + category: "Activities", + description: "chess pawn", + unicode_version: "11.0", + }, + { + emoji: "🃏", + aliases: ["black_joker"], + tags: [], + category: "Activities", + description: "joker", + unicode_version: "6.0", + }, + { + emoji: "🀄", + aliases: ["mahjong"], + tags: [], + category: "Activities", + description: "mahjong red dragon", + unicode_version: "", + }, + { + emoji: "🎴", + aliases: ["flower_playing_cards"], + tags: [], + category: "Activities", + description: "flower playing cards", + unicode_version: "6.0", + }, + { + emoji: "🎭", + aliases: ["performing_arts"], + tags: ["theater", "drama"], + category: "Activities", + description: "performing arts", + unicode_version: "6.0", + }, + { + emoji: "🖼️", + aliases: ["framed_picture"], + tags: [], + category: "Activities", + description: "framed picture", + unicode_version: "7.0", + }, + { + emoji: "🎨", + aliases: ["art"], + tags: ["design", "paint"], + category: "Activities", + description: "artist palette", + unicode_version: "6.0", + }, + { + emoji: "🧵", + aliases: ["thread"], + tags: [], + category: "Activities", + description: "thread", + unicode_version: "11.0", + }, + { + emoji: "🪡", + aliases: ["sewing_needle"], + tags: [], + category: "Activities", + description: "sewing needle", + unicode_version: "13.0", + }, + { + emoji: "🧶", + aliases: ["yarn"], + tags: [], + category: "Activities", + description: "yarn", + unicode_version: "11.0", + }, + { + emoji: "🪢", + aliases: ["knot"], + tags: [], + category: "Activities", + description: "knot", + unicode_version: "13.0", + }, + { + emoji: "👓", + aliases: ["eyeglasses"], + tags: ["glasses"], + category: "Objects", + description: "glasses", + unicode_version: "6.0", + }, + { + emoji: "🕶️", + aliases: ["dark_sunglasses"], + tags: [], + category: "Objects", + description: "sunglasses", + unicode_version: "7.0", + }, + { + emoji: "🥽", + aliases: ["goggles"], + tags: [], + category: "Objects", + description: "goggles", + unicode_version: "11.0", + }, + { + emoji: "🥼", + aliases: ["lab_coat"], + tags: [], + category: "Objects", + description: "lab coat", + unicode_version: "11.0", + }, + { + emoji: "🦺", + aliases: ["safety_vest"], + tags: [], + category: "Objects", + description: "safety vest", + unicode_version: "12.0", + }, + { + emoji: "👔", + aliases: ["necktie"], + tags: ["shirt", "formal"], + category: "Objects", + description: "necktie", + unicode_version: "6.0", + }, + { + emoji: "👕", + aliases: ["shirt", "tshirt"], + tags: [], + category: "Objects", + description: "t-shirt", + unicode_version: "6.0", + }, + { + emoji: "👖", + aliases: ["jeans"], + tags: ["pants"], + category: "Objects", + description: "jeans", + unicode_version: "6.0", + }, + { + emoji: "🧣", + aliases: ["scarf"], + tags: [], + category: "Objects", + description: "scarf", + unicode_version: "11.0", + }, + { + emoji: "🧤", + aliases: ["gloves"], + tags: [], + category: "Objects", + description: "gloves", + unicode_version: "11.0", + }, + { + emoji: "🧥", + aliases: ["coat"], + tags: [], + category: "Objects", + description: "coat", + unicode_version: "11.0", + }, + { + emoji: "🧦", + aliases: ["socks"], + tags: [], + category: "Objects", + description: "socks", + unicode_version: "11.0", + }, + { + emoji: "👗", + aliases: ["dress"], + tags: [], + category: "Objects", + description: "dress", + unicode_version: "6.0", + }, + { + emoji: "👘", + aliases: ["kimono"], + tags: [], + category: "Objects", + description: "kimono", + unicode_version: "6.0", + }, + { + emoji: "🥻", + aliases: ["sari"], + tags: [], + category: "Objects", + description: "sari", + unicode_version: "12.0", + }, + { + emoji: "🩱", + aliases: ["one_piece_swimsuit"], + tags: [], + category: "Objects", + description: "one-piece swimsuit", + unicode_version: "12.0", + }, + { + emoji: "🩲", + aliases: ["swim_brief"], + tags: [], + category: "Objects", + description: "briefs", + unicode_version: "12.0", + }, + { + emoji: "🩳", + aliases: ["shorts"], + tags: [], + category: "Objects", + description: "shorts", + unicode_version: "12.0", + }, + { + emoji: "👙", + aliases: ["bikini"], + tags: ["beach"], + category: "Objects", + description: "bikini", + unicode_version: "6.0", + }, + { + emoji: "👚", + aliases: ["womans_clothes"], + tags: [], + category: "Objects", + description: "woman’s clothes", + unicode_version: "6.0", + }, + { + emoji: "👛", + aliases: ["purse"], + tags: [], + category: "Objects", + description: "purse", + unicode_version: "6.0", + }, + { + emoji: "👜", + aliases: ["handbag"], + tags: ["bag"], + category: "Objects", + description: "handbag", + unicode_version: "6.0", + }, + { + emoji: "👝", + aliases: ["pouch"], + tags: ["bag"], + category: "Objects", + description: "clutch bag", + unicode_version: "6.0", + }, + { + emoji: "🛍️", + aliases: ["shopping"], + tags: ["bags"], + category: "Objects", + description: "shopping bags", + unicode_version: "7.0", + }, + { + emoji: "🎒", + aliases: ["school_satchel"], + tags: [], + category: "Objects", + description: "backpack", + unicode_version: "6.0", + }, + { + emoji: "🩴", + aliases: ["thong_sandal"], + tags: [], + category: "Objects", + description: "thong sandal", + unicode_version: "13.0", + }, + { + emoji: "👞", + aliases: ["mans_shoe", "shoe"], + tags: [], + category: "Objects", + description: "man’s shoe", + unicode_version: "6.0", + }, + { + emoji: "👟", + aliases: ["athletic_shoe"], + tags: ["sneaker", "sport", "running"], + category: "Objects", + description: "running shoe", + unicode_version: "6.0", + }, + { + emoji: "🥾", + aliases: ["hiking_boot"], + tags: [], + category: "Objects", + description: "hiking boot", + unicode_version: "11.0", + }, + { + emoji: "🥿", + aliases: ["flat_shoe"], + tags: [], + category: "Objects", + description: "flat shoe", + unicode_version: "11.0", + }, + { + emoji: "👠", + aliases: ["high_heel"], + tags: ["shoe"], + category: "Objects", + description: "high-heeled shoe", + unicode_version: "6.0", + }, + { + emoji: "👡", + aliases: ["sandal"], + tags: ["shoe"], + category: "Objects", + description: "woman’s sandal", + unicode_version: "6.0", + }, + { + emoji: "🩰", + aliases: ["ballet_shoes"], + tags: [], + category: "Objects", + description: "ballet shoes", + unicode_version: "12.0", + }, + { + emoji: "👢", + aliases: ["boot"], + tags: [], + category: "Objects", + description: "woman’s boot", + unicode_version: "6.0", + }, + { + emoji: "👑", + aliases: ["crown"], + tags: ["king", "queen", "royal"], + category: "Objects", + description: "crown", + unicode_version: "6.0", + }, + { + emoji: "👒", + aliases: ["womans_hat"], + tags: [], + category: "Objects", + description: "woman’s hat", + unicode_version: "6.0", + }, + { + emoji: "🎩", + aliases: ["tophat"], + tags: ["hat", "classy"], + category: "Objects", + description: "top hat", + unicode_version: "6.0", + }, + { + emoji: "🎓", + aliases: ["mortar_board"], + tags: ["education", "college", "university", "graduation"], + category: "Objects", + description: "graduation cap", + unicode_version: "6.0", + }, + { + emoji: "🧢", + aliases: ["billed_cap"], + tags: [], + category: "Objects", + description: "billed cap", + unicode_version: "11.0", + }, + { + emoji: "🪖", + aliases: ["military_helmet"], + tags: [], + category: "Objects", + description: "military helmet", + unicode_version: "13.0", + }, + { + emoji: "⛑️", + aliases: ["rescue_worker_helmet"], + tags: [], + category: "Objects", + description: "rescue worker’s helmet", + unicode_version: "5.2", + }, + { + emoji: "📿", + aliases: ["prayer_beads"], + tags: [], + category: "Objects", + description: "prayer beads", + unicode_version: "8.0", + }, + { + emoji: "💄", + aliases: ["lipstick"], + tags: ["makeup"], + category: "Objects", + description: "lipstick", + unicode_version: "6.0", + }, + { + emoji: "💍", + aliases: ["ring"], + tags: ["wedding", "marriage", "engaged"], + category: "Objects", + description: "ring", + unicode_version: "6.0", + }, + { + emoji: "💎", + aliases: ["gem"], + tags: ["diamond"], + category: "Objects", + description: "gem stone", + unicode_version: "6.0", + }, + { + emoji: "🔇", + aliases: ["mute"], + tags: ["sound", "volume"], + category: "Objects", + description: "muted speaker", + unicode_version: "6.0", + }, + { + emoji: "🔈", + aliases: ["speaker"], + tags: [], + category: "Objects", + description: "speaker low volume", + unicode_version: "6.0", + }, + { + emoji: "🔉", + aliases: ["sound"], + tags: ["volume"], + category: "Objects", + description: "speaker medium volume", + unicode_version: "6.0", + }, + { + emoji: "🔊", + aliases: ["loud_sound"], + tags: ["volume"], + category: "Objects", + description: "speaker high volume", + unicode_version: "6.0", + }, + { + emoji: "📢", + aliases: ["loudspeaker"], + tags: ["announcement"], + category: "Objects", + description: "loudspeaker", + unicode_version: "6.0", + }, + { + emoji: "📣", + aliases: ["mega"], + tags: [], + category: "Objects", + description: "megaphone", + unicode_version: "6.0", + }, + { + emoji: "📯", + aliases: ["postal_horn"], + tags: [], + category: "Objects", + description: "postal horn", + unicode_version: "6.0", + }, + { + emoji: "🔔", + aliases: ["bell"], + tags: ["sound", "notification"], + category: "Objects", + description: "bell", + unicode_version: "6.0", + }, + { + emoji: "🔕", + aliases: ["no_bell"], + tags: ["volume", "off"], + category: "Objects", + description: "bell with slash", + unicode_version: "6.0", + }, + { + emoji: "🎼", + aliases: ["musical_score"], + tags: [], + category: "Objects", + description: "musical score", + unicode_version: "6.0", + }, + { + emoji: "🎵", + aliases: ["musical_note"], + tags: [], + category: "Objects", + description: "musical note", + unicode_version: "6.0", + }, + { + emoji: "🎶", + aliases: ["notes"], + tags: ["music"], + category: "Objects", + description: "musical notes", + unicode_version: "6.0", + }, + { + emoji: "🎙️", + aliases: ["studio_microphone"], + tags: ["podcast"], + category: "Objects", + description: "studio microphone", + unicode_version: "7.0", + }, + { + emoji: "🎚️", + aliases: ["level_slider"], + tags: [], + category: "Objects", + description: "level slider", + unicode_version: "7.0", + }, + { + emoji: "🎛️", + aliases: ["control_knobs"], + tags: [], + category: "Objects", + description: "control knobs", + unicode_version: "7.0", + }, + { + emoji: "🎤", + aliases: ["microphone"], + tags: ["sing"], + category: "Objects", + description: "microphone", + unicode_version: "6.0", + }, + { + emoji: "🎧", + aliases: ["headphones"], + tags: ["music", "earphones"], + category: "Objects", + description: "headphone", + unicode_version: "6.0", + }, + { + emoji: "📻", + aliases: ["radio"], + tags: ["podcast"], + category: "Objects", + description: "radio", + unicode_version: "6.0", + }, + { + emoji: "🎷", + aliases: ["saxophone"], + tags: [], + category: "Objects", + description: "saxophone", + unicode_version: "6.0", + }, + { + emoji: "🪗", + aliases: ["accordion"], + tags: [], + category: "Objects", + description: "accordion", + unicode_version: "13.0", + }, + { + emoji: "🎸", + aliases: ["guitar"], + tags: ["rock"], + category: "Objects", + description: "guitar", + unicode_version: "6.0", + }, + { + emoji: "🎹", + aliases: ["musical_keyboard"], + tags: ["piano"], + category: "Objects", + description: "musical keyboard", + unicode_version: "6.0", + }, + { + emoji: "🎺", + aliases: ["trumpet"], + tags: [], + category: "Objects", + description: "trumpet", + unicode_version: "6.0", + }, + { + emoji: "🎻", + aliases: ["violin"], + tags: [], + category: "Objects", + description: "violin", + unicode_version: "6.0", + }, + { + emoji: "🪕", + aliases: ["banjo"], + tags: [], + category: "Objects", + description: "banjo", + unicode_version: "12.0", + }, + { + emoji: "🥁", + aliases: ["drum"], + tags: [], + category: "Objects", + description: "drum", + unicode_version: "", + }, + { + emoji: "🪘", + aliases: ["long_drum"], + tags: [], + category: "Objects", + description: "long drum", + unicode_version: "13.0", + }, + { + emoji: "📱", + aliases: ["iphone"], + tags: ["smartphone", "mobile"], + category: "Objects", + description: "mobile phone", + unicode_version: "6.0", + }, + { + emoji: "📲", + aliases: ["calling"], + tags: ["call", "incoming"], + category: "Objects", + description: "mobile phone with arrow", + unicode_version: "6.0", + }, + { + emoji: "☎️", + aliases: ["phone", "telephone"], + tags: [], + category: "Objects", + description: "telephone", + unicode_version: "", + }, + { + emoji: "📞", + aliases: ["telephone_receiver"], + tags: ["phone", "call"], + category: "Objects", + description: "telephone receiver", + unicode_version: "6.0", + }, + { + emoji: "📟", + aliases: ["pager"], + tags: [], + category: "Objects", + description: "pager", + unicode_version: "6.0", + }, + { + emoji: "📠", + aliases: ["fax"], + tags: [], + category: "Objects", + description: "fax machine", + unicode_version: "6.0", + }, + { + emoji: "🔋", + aliases: ["battery"], + tags: ["power"], + category: "Objects", + description: "battery", + unicode_version: "6.0", + }, + { + emoji: "🔌", + aliases: ["electric_plug"], + tags: [], + category: "Objects", + description: "electric plug", + unicode_version: "6.0", + }, + { + emoji: "💻", + aliases: ["computer"], + tags: ["desktop", "screen"], + category: "Objects", + description: "laptop", + unicode_version: "6.0", + }, + { + emoji: "🖥️", + aliases: ["desktop_computer"], + tags: [], + category: "Objects", + description: "desktop computer", + unicode_version: "7.0", + }, + { + emoji: "🖨️", + aliases: ["printer"], + tags: [], + category: "Objects", + description: "printer", + unicode_version: "7.0", + }, + { + emoji: "⌨️", + aliases: ["keyboard"], + tags: [], + category: "Objects", + description: "keyboard", + unicode_version: "", + }, + { + emoji: "🖱️", + aliases: ["computer_mouse"], + tags: [], + category: "Objects", + description: "computer mouse", + unicode_version: "7.0", + }, + { + emoji: "🖲️", + aliases: ["trackball"], + tags: [], + category: "Objects", + description: "trackball", + unicode_version: "7.0", + }, + { + emoji: "💽", + aliases: ["minidisc"], + tags: [], + category: "Objects", + description: "computer disk", + unicode_version: "6.0", + }, + { + emoji: "💾", + aliases: ["floppy_disk"], + tags: ["save"], + category: "Objects", + description: "floppy disk", + unicode_version: "6.0", + }, + { + emoji: "💿", + aliases: ["cd"], + tags: [], + category: "Objects", + description: "optical disk", + unicode_version: "6.0", + }, + { + emoji: "📀", + aliases: ["dvd"], + tags: [], + category: "Objects", + description: "dvd", + unicode_version: "6.0", + }, + { + emoji: "🧮", + aliases: ["abacus"], + tags: [], + category: "Objects", + description: "abacus", + unicode_version: "11.0", + }, + { + emoji: "🎥", + aliases: ["movie_camera"], + tags: ["film", "video"], + category: "Objects", + description: "movie camera", + unicode_version: "6.0", + }, + { + emoji: "🎞️", + aliases: ["film_strip"], + tags: [], + category: "Objects", + description: "film frames", + unicode_version: "7.0", + }, + { + emoji: "📽️", + aliases: ["film_projector"], + tags: [], + category: "Objects", + description: "film projector", + unicode_version: "7.0", + }, + { + emoji: "🎬", + aliases: ["clapper"], + tags: ["film"], + category: "Objects", + description: "clapper board", + unicode_version: "6.0", + }, + { + emoji: "📺", + aliases: ["tv"], + tags: [], + category: "Objects", + description: "television", + unicode_version: "6.0", + }, + { + emoji: "📷", + aliases: ["camera"], + tags: ["photo"], + category: "Objects", + description: "camera", + unicode_version: "6.0", + }, + { + emoji: "📸", + aliases: ["camera_flash"], + tags: ["photo"], + category: "Objects", + description: "camera with flash", + unicode_version: "7.0", + }, + { + emoji: "📹", + aliases: ["video_camera"], + tags: [], + category: "Objects", + description: "video camera", + unicode_version: "6.0", + }, + { + emoji: "📼", + aliases: ["vhs"], + tags: [], + category: "Objects", + description: "videocassette", + unicode_version: "6.0", + }, + { + emoji: "🔍", + aliases: ["mag"], + tags: ["search", "zoom"], + category: "Objects", + description: "magnifying glass tilted left", + unicode_version: "6.0", + }, + { + emoji: "🔎", + aliases: ["mag_right"], + tags: [], + category: "Objects", + description: "magnifying glass tilted right", + unicode_version: "6.0", + }, + { + emoji: "🕯️", + aliases: ["candle"], + tags: [], + category: "Objects", + description: "candle", + unicode_version: "7.0", + }, + { + emoji: "💡", + aliases: ["bulb"], + tags: ["idea", "light"], + category: "Objects", + description: "light bulb", + unicode_version: "6.0", + }, + { + emoji: "🔦", + aliases: ["flashlight"], + tags: [], + category: "Objects", + description: "flashlight", + unicode_version: "6.0", + }, + { + emoji: "🏮", + aliases: ["izakaya_lantern", "lantern"], + tags: [], + category: "Objects", + description: "red paper lantern", + unicode_version: "6.0", + }, + { + emoji: "🪔", + aliases: ["diya_lamp"], + tags: [], + category: "Objects", + description: "diya lamp", + unicode_version: "12.0", + }, + { + emoji: "📔", + aliases: ["notebook_with_decorative_cover"], + tags: [], + category: "Objects", + description: "notebook with decorative cover", + unicode_version: "6.0", + }, + { + emoji: "📕", + aliases: ["closed_book"], + tags: [], + category: "Objects", + description: "closed book", + unicode_version: "6.0", + }, + { + emoji: "📖", + aliases: ["book", "open_book"], + tags: [], + category: "Objects", + description: "open book", + unicode_version: "6.0", + }, + { + emoji: "📗", + aliases: ["green_book"], + tags: [], + category: "Objects", + description: "green book", + unicode_version: "6.0", + }, + { + emoji: "📘", + aliases: ["blue_book"], + tags: [], + category: "Objects", + description: "blue book", + unicode_version: "6.0", + }, + { + emoji: "📙", + aliases: ["orange_book"], + tags: [], + category: "Objects", + description: "orange book", + unicode_version: "6.0", + }, + { + emoji: "📚", + aliases: ["books"], + tags: ["library"], + category: "Objects", + description: "books", + unicode_version: "6.0", + }, + { + emoji: "📓", + aliases: ["notebook"], + tags: [], + category: "Objects", + description: "notebook", + unicode_version: "6.0", + }, + { + emoji: "📒", + aliases: ["ledger"], + tags: [], + category: "Objects", + description: "ledger", + unicode_version: "6.0", + }, + { + emoji: "📃", + aliases: ["page_with_curl"], + tags: [], + category: "Objects", + description: "page with curl", + unicode_version: "6.0", + }, + { + emoji: "📜", + aliases: ["scroll"], + tags: ["document"], + category: "Objects", + description: "scroll", + unicode_version: "6.0", + }, + { + emoji: "📄", + aliases: ["page_facing_up"], + tags: ["document"], + category: "Objects", + description: "page facing up", + unicode_version: "6.0", + }, + { + emoji: "📰", + aliases: ["newspaper"], + tags: ["press"], + category: "Objects", + description: "newspaper", + unicode_version: "6.0", + }, + { + emoji: "🗞️", + aliases: ["newspaper_roll"], + tags: ["press"], + category: "Objects", + description: "rolled-up newspaper", + unicode_version: "7.0", + }, + { + emoji: "📑", + aliases: ["bookmark_tabs"], + tags: [], + category: "Objects", + description: "bookmark tabs", + unicode_version: "6.0", + }, + { + emoji: "🔖", + aliases: ["bookmark"], + tags: [], + category: "Objects", + description: "bookmark", + unicode_version: "6.0", + }, + { + emoji: "🏷️", + aliases: ["label"], + tags: ["tag"], + category: "Objects", + description: "label", + unicode_version: "7.0", + }, + { + emoji: "💰", + aliases: ["moneybag"], + tags: ["dollar", "cream"], + category: "Objects", + description: "money bag", + unicode_version: "6.0", + }, + { + emoji: "🪙", + aliases: ["coin"], + tags: [], + category: "Objects", + description: "coin", + unicode_version: "13.0", + }, + { + emoji: "💴", + aliases: ["yen"], + tags: [], + category: "Objects", + description: "yen banknote", + unicode_version: "6.0", + }, + { + emoji: "💵", + aliases: ["dollar"], + tags: ["money"], + category: "Objects", + description: "dollar banknote", + unicode_version: "6.0", + }, + { + emoji: "💶", + aliases: ["euro"], + tags: [], + category: "Objects", + description: "euro banknote", + unicode_version: "6.0", + }, + { + emoji: "💷", + aliases: ["pound"], + tags: [], + category: "Objects", + description: "pound banknote", + unicode_version: "6.0", + }, + { + emoji: "💸", + aliases: ["money_with_wings"], + tags: ["dollar"], + category: "Objects", + description: "money with wings", + unicode_version: "6.0", + }, + { + emoji: "💳", + aliases: ["credit_card"], + tags: ["subscription"], + category: "Objects", + description: "credit card", + unicode_version: "6.0", + }, + { + emoji: "🧾", + aliases: ["receipt"], + tags: [], + category: "Objects", + description: "receipt", + unicode_version: "11.0", + }, + { + emoji: "💹", + aliases: ["chart"], + tags: [], + category: "Objects", + description: "chart increasing with yen", + unicode_version: "6.0", + }, + { + emoji: "✉️", + aliases: ["envelope"], + tags: ["letter", "email"], + category: "Objects", + description: "envelope", + unicode_version: "", + }, + { + emoji: "📧", + aliases: ["email", "e-mail"], + tags: [], + category: "Objects", + description: "e-mail", + unicode_version: "6.0", + }, + { + emoji: "📨", + aliases: ["incoming_envelope"], + tags: [], + category: "Objects", + description: "incoming envelope", + unicode_version: "6.0", + }, + { + emoji: "📩", + aliases: ["envelope_with_arrow"], + tags: [], + category: "Objects", + description: "envelope with arrow", + unicode_version: "6.0", + }, + { + emoji: "📤", + aliases: ["outbox_tray"], + tags: [], + category: "Objects", + description: "outbox tray", + unicode_version: "6.0", + }, + { + emoji: "📥", + aliases: ["inbox_tray"], + tags: [], + category: "Objects", + description: "inbox tray", + unicode_version: "6.0", + }, + { + emoji: "📦", + aliases: ["package"], + tags: ["shipping"], + category: "Objects", + description: "package", + unicode_version: "6.0", + }, + { + emoji: "📫", + aliases: ["mailbox"], + tags: [], + category: "Objects", + description: "closed mailbox with raised flag", + unicode_version: "6.0", + }, + { + emoji: "📪", + aliases: ["mailbox_closed"], + tags: [], + category: "Objects", + description: "closed mailbox with lowered flag", + unicode_version: "6.0", + }, + { + emoji: "📬", + aliases: ["mailbox_with_mail"], + tags: [], + category: "Objects", + description: "open mailbox with raised flag", + unicode_version: "6.0", + }, + { + emoji: "📭", + aliases: ["mailbox_with_no_mail"], + tags: [], + category: "Objects", + description: "open mailbox with lowered flag", + unicode_version: "6.0", + }, + { + emoji: "📮", + aliases: ["postbox"], + tags: [], + category: "Objects", + description: "postbox", + unicode_version: "6.0", + }, + { + emoji: "🗳️", + aliases: ["ballot_box"], + tags: [], + category: "Objects", + description: "ballot box with ballot", + unicode_version: "7.0", + }, + { + emoji: "✏️", + aliases: ["pencil2"], + tags: [], + category: "Objects", + description: "pencil", + unicode_version: "", + }, + { + emoji: "✒️", + aliases: ["black_nib"], + tags: [], + category: "Objects", + description: "black nib", + unicode_version: "", + }, + { + emoji: "🖋️", + aliases: ["fountain_pen"], + tags: [], + category: "Objects", + description: "fountain pen", + unicode_version: "7.0", + }, + { + emoji: "🖊️", + aliases: ["pen"], + tags: [], + category: "Objects", + description: "pen", + unicode_version: "7.0", + }, + { + emoji: "🖌️", + aliases: ["paintbrush"], + tags: [], + category: "Objects", + description: "paintbrush", + unicode_version: "7.0", + }, + { + emoji: "🖍️", + aliases: ["crayon"], + tags: [], + category: "Objects", + description: "crayon", + unicode_version: "7.0", + }, + { + emoji: "📝", + aliases: ["memo", "pencil"], + tags: ["document", "note"], + category: "Objects", + description: "memo", + unicode_version: "6.0", + }, + { + emoji: "💼", + aliases: ["briefcase"], + tags: ["business"], + category: "Objects", + description: "briefcase", + unicode_version: "6.0", + }, + { + emoji: "📁", + aliases: ["file_folder"], + tags: ["directory"], + category: "Objects", + description: "file folder", + unicode_version: "6.0", + }, + { + emoji: "📂", + aliases: ["open_file_folder"], + tags: [], + category: "Objects", + description: "open file folder", + unicode_version: "6.0", + }, + { + emoji: "🗂️", + aliases: ["card_index_dividers"], + tags: [], + category: "Objects", + description: "card index dividers", + unicode_version: "7.0", + }, + { + emoji: "📅", + aliases: ["date"], + tags: ["calendar", "schedule"], + category: "Objects", + description: "calendar", + unicode_version: "6.0", + }, + { + emoji: "📆", + aliases: ["calendar"], + tags: ["schedule"], + category: "Objects", + description: "tear-off calendar", + unicode_version: "6.0", + }, + { + emoji: "🗒️", + aliases: ["spiral_notepad"], + tags: [], + category: "Objects", + description: "spiral notepad", + unicode_version: "7.0", + }, + { + emoji: "🗓️", + aliases: ["spiral_calendar"], + tags: [], + category: "Objects", + description: "spiral calendar", + unicode_version: "7.0", + }, + { + emoji: "📇", + aliases: ["card_index"], + tags: [], + category: "Objects", + description: "card index", + unicode_version: "6.0", + }, + { + emoji: "📈", + aliases: ["chart_with_upwards_trend"], + tags: ["graph", "metrics"], + category: "Objects", + description: "chart increasing", + unicode_version: "6.0", + }, + { + emoji: "📉", + aliases: ["chart_with_downwards_trend"], + tags: ["graph", "metrics"], + category: "Objects", + description: "chart decreasing", + unicode_version: "6.0", + }, + { + emoji: "📊", + aliases: ["bar_chart"], + tags: ["stats", "metrics"], + category: "Objects", + description: "bar chart", + unicode_version: "6.0", + }, + { + emoji: "📋", + aliases: ["clipboard"], + tags: [], + category: "Objects", + description: "clipboard", + unicode_version: "6.0", + }, + { + emoji: "📌", + aliases: ["pushpin"], + tags: ["location"], + category: "Objects", + description: "pushpin", + unicode_version: "6.0", + }, + { + emoji: "📍", + aliases: ["round_pushpin"], + tags: ["location"], + category: "Objects", + description: "round pushpin", + unicode_version: "6.0", + }, + { + emoji: "📎", + aliases: ["paperclip"], + tags: [], + category: "Objects", + description: "paperclip", + unicode_version: "6.0", + }, + { + emoji: "🖇️", + aliases: ["paperclips"], + tags: [], + category: "Objects", + description: "linked paperclips", + unicode_version: "7.0", + }, + { + emoji: "📏", + aliases: ["straight_ruler"], + tags: [], + category: "Objects", + description: "straight ruler", + unicode_version: "6.0", + }, + { + emoji: "📐", + aliases: ["triangular_ruler"], + tags: [], + category: "Objects", + description: "triangular ruler", + unicode_version: "6.0", + }, + { + emoji: "✂️", + aliases: ["scissors"], + tags: ["cut"], + category: "Objects", + description: "scissors", + unicode_version: "", + }, + { + emoji: "🗃️", + aliases: ["card_file_box"], + tags: [], + category: "Objects", + description: "card file box", + unicode_version: "7.0", + }, + { + emoji: "🗄️", + aliases: ["file_cabinet"], + tags: [], + category: "Objects", + description: "file cabinet", + unicode_version: "7.0", + }, + { + emoji: "🗑️", + aliases: ["wastebasket"], + tags: ["trash"], + category: "Objects", + description: "wastebasket", + unicode_version: "7.0", + }, + { + emoji: "🔒", + aliases: ["lock"], + tags: ["security", "private"], + category: "Objects", + description: "locked", + unicode_version: "6.0", + }, + { + emoji: "🔓", + aliases: ["unlock"], + tags: ["security"], + category: "Objects", + description: "unlocked", + unicode_version: "6.0", + }, + { + emoji: "🔏", + aliases: ["lock_with_ink_pen"], + tags: [], + category: "Objects", + description: "locked with pen", + unicode_version: "6.0", + }, + { + emoji: "🔐", + aliases: ["closed_lock_with_key"], + tags: ["security"], + category: "Objects", + description: "locked with key", + unicode_version: "6.0", + }, + { + emoji: "🔑", + aliases: ["key"], + tags: ["lock", "password"], + category: "Objects", + description: "key", + unicode_version: "6.0", + }, + { + emoji: "🗝️", + aliases: ["old_key"], + tags: [], + category: "Objects", + description: "old key", + unicode_version: "7.0", + }, + { + emoji: "🔨", + aliases: ["hammer"], + tags: ["tool"], + category: "Objects", + description: "hammer", + unicode_version: "6.0", + }, + { + emoji: "🪓", + aliases: ["axe"], + tags: [], + category: "Objects", + description: "axe", + unicode_version: "12.0", + }, + { + emoji: "⛏️", + aliases: ["pick"], + tags: [], + category: "Objects", + description: "pick", + unicode_version: "5.2", + }, + { + emoji: "⚒️", + aliases: ["hammer_and_pick"], + tags: [], + category: "Objects", + description: "hammer and pick", + unicode_version: "4.1", + }, + { + emoji: "🛠️", + aliases: ["hammer_and_wrench"], + tags: [], + category: "Objects", + description: "hammer and wrench", + unicode_version: "7.0", + }, + { + emoji: "🗡️", + aliases: ["dagger"], + tags: [], + category: "Objects", + description: "dagger", + unicode_version: "7.0", + }, + { + emoji: "⚔️", + aliases: ["crossed_swords"], + tags: [], + category: "Objects", + description: "crossed swords", + unicode_version: "4.1", + }, + { + emoji: "🔫", + aliases: ["gun"], + tags: ["shoot", "weapon"], + category: "Objects", + description: "water pistol", + unicode_version: "6.0", + }, + { + emoji: "🪃", + aliases: ["boomerang"], + tags: [], + category: "Objects", + description: "boomerang", + unicode_version: "13.0", + }, + { + emoji: "🏹", + aliases: ["bow_and_arrow"], + tags: ["archery"], + category: "Objects", + description: "bow and arrow", + unicode_version: "8.0", + }, + { + emoji: "🛡️", + aliases: ["shield"], + tags: [], + category: "Objects", + description: "shield", + unicode_version: "7.0", + }, + { + emoji: "🪚", + aliases: ["carpentry_saw"], + tags: [], + category: "Objects", + description: "carpentry saw", + unicode_version: "13.0", + }, + { + emoji: "🔧", + aliases: ["wrench"], + tags: ["tool"], + category: "Objects", + description: "wrench", + unicode_version: "6.0", + }, + { + emoji: "🪛", + aliases: ["screwdriver"], + tags: [], + category: "Objects", + description: "screwdriver", + unicode_version: "13.0", + }, + { + emoji: "🔩", + aliases: ["nut_and_bolt"], + tags: [], + category: "Objects", + description: "nut and bolt", + unicode_version: "6.0", + }, + { + emoji: "⚙️", + aliases: ["gear"], + tags: [], + category: "Objects", + description: "gear", + unicode_version: "4.1", + }, + { + emoji: "🗜️", + aliases: ["clamp"], + tags: [], + category: "Objects", + description: "clamp", + unicode_version: "7.0", + }, + { + emoji: "⚖️", + aliases: ["balance_scale"], + tags: [], + category: "Objects", + description: "balance scale", + unicode_version: "4.1", + }, + { + emoji: "🦯", + aliases: ["probing_cane"], + tags: [], + category: "Objects", + description: "white cane", + unicode_version: "12.0", + }, + { + emoji: "🔗", + aliases: ["link"], + tags: [], + category: "Objects", + description: "link", + unicode_version: "6.0", + }, + { + emoji: "⛓️", + aliases: ["chains"], + tags: [], + category: "Objects", + description: "chains", + unicode_version: "5.2", + }, + { + emoji: "🪝", + aliases: ["hook"], + tags: [], + category: "Objects", + description: "hook", + unicode_version: "13.0", + }, + { + emoji: "🧰", + aliases: ["toolbox"], + tags: [], + category: "Objects", + description: "toolbox", + unicode_version: "11.0", + }, + { + emoji: "🧲", + aliases: ["magnet"], + tags: [], + category: "Objects", + description: "magnet", + unicode_version: "11.0", + }, + { + emoji: "🪜", + aliases: ["ladder"], + tags: [], + category: "Objects", + description: "ladder", + unicode_version: "13.0", + }, + { + emoji: "⚗️", + aliases: ["alembic"], + tags: [], + category: "Objects", + description: "alembic", + unicode_version: "4.1", + }, + { + emoji: "🧪", + aliases: ["test_tube"], + tags: [], + category: "Objects", + description: "test tube", + unicode_version: "11.0", + }, + { + emoji: "🧫", + aliases: ["petri_dish"], + tags: [], + category: "Objects", + description: "petri dish", + unicode_version: "11.0", + }, + { + emoji: "🧬", + aliases: ["dna"], + tags: [], + category: "Objects", + description: "dna", + unicode_version: "11.0", + }, + { + emoji: "🔬", + aliases: ["microscope"], + tags: ["science", "laboratory", "investigate"], + category: "Objects", + description: "microscope", + unicode_version: "6.0", + }, + { + emoji: "🔭", + aliases: ["telescope"], + tags: [], + category: "Objects", + description: "telescope", + unicode_version: "6.0", + }, + { + emoji: "📡", + aliases: ["satellite"], + tags: ["signal"], + category: "Objects", + description: "satellite antenna", + unicode_version: "6.0", + }, + { + emoji: "💉", + aliases: ["syringe"], + tags: ["health", "hospital", "needle"], + category: "Objects", + description: "syringe", + unicode_version: "6.0", + }, + { + emoji: "🩸", + aliases: ["drop_of_blood"], + tags: [], + category: "Objects", + description: "drop of blood", + unicode_version: "12.0", + }, + { + emoji: "💊", + aliases: ["pill"], + tags: ["health", "medicine"], + category: "Objects", + description: "pill", + unicode_version: "6.0", + }, + { + emoji: "🩹", + aliases: ["adhesive_bandage"], + tags: [], + category: "Objects", + description: "adhesive bandage", + unicode_version: "12.0", + }, + { + emoji: "🩺", + aliases: ["stethoscope"], + tags: [], + category: "Objects", + description: "stethoscope", + unicode_version: "12.0", + }, + { + emoji: "🚪", + aliases: ["door"], + tags: [], + category: "Objects", + description: "door", + unicode_version: "6.0", + }, + { + emoji: "🛗", + aliases: ["elevator"], + tags: [], + category: "Objects", + description: "elevator", + unicode_version: "13.0", + }, + { + emoji: "🪞", + aliases: ["mirror"], + tags: [], + category: "Objects", + description: "mirror", + unicode_version: "13.0", + }, + { + emoji: "🪟", + aliases: ["window"], + tags: [], + category: "Objects", + description: "window", + unicode_version: "13.0", + }, + { + emoji: "🛏️", + aliases: ["bed"], + tags: [], + category: "Objects", + description: "bed", + unicode_version: "7.0", + }, + { + emoji: "🛋️", + aliases: ["couch_and_lamp"], + tags: [], + category: "Objects", + description: "couch and lamp", + unicode_version: "7.0", + }, + { + emoji: "🪑", + aliases: ["chair"], + tags: [], + category: "Objects", + description: "chair", + unicode_version: "12.0", + }, + { + emoji: "🚽", + aliases: ["toilet"], + tags: ["wc"], + category: "Objects", + description: "toilet", + unicode_version: "6.0", + }, + { + emoji: "🪠", + aliases: ["plunger"], + tags: [], + category: "Objects", + description: "plunger", + unicode_version: "13.0", + }, + { + emoji: "🚿", + aliases: ["shower"], + tags: ["bath"], + category: "Objects", + description: "shower", + unicode_version: "6.0", + }, + { + emoji: "🛁", + aliases: ["bathtub"], + tags: [], + category: "Objects", + description: "bathtub", + unicode_version: "6.0", + }, + { + emoji: "🪤", + aliases: ["mouse_trap"], + tags: [], + category: "Objects", + description: "mouse trap", + unicode_version: "13.0", + }, + { + emoji: "🪒", + aliases: ["razor"], + tags: [], + category: "Objects", + description: "razor", + unicode_version: "12.0", + }, + { + emoji: "🧴", + aliases: ["lotion_bottle"], + tags: [], + category: "Objects", + description: "lotion bottle", + unicode_version: "11.0", + }, + { + emoji: "🧷", + aliases: ["safety_pin"], + tags: [], + category: "Objects", + description: "safety pin", + unicode_version: "11.0", + }, + { + emoji: "🧹", + aliases: ["broom"], + tags: [], + category: "Objects", + description: "broom", + unicode_version: "11.0", + }, + { + emoji: "🧺", + aliases: ["basket"], + tags: [], + category: "Objects", + description: "basket", + unicode_version: "11.0", + }, + { + emoji: "🧻", + aliases: ["roll_of_paper"], + tags: ["toilet"], + category: "Objects", + description: "roll of paper", + unicode_version: "11.0", + }, + { + emoji: "🪣", + aliases: ["bucket"], + tags: [], + category: "Objects", + description: "bucket", + unicode_version: "13.0", + }, + { + emoji: "🧼", + aliases: ["soap"], + tags: [], + category: "Objects", + description: "soap", + unicode_version: "11.0", + }, + { + emoji: "🪥", + aliases: ["toothbrush"], + tags: [], + category: "Objects", + description: "toothbrush", + unicode_version: "13.0", + }, + { + emoji: "🧽", + aliases: ["sponge"], + tags: [], + category: "Objects", + description: "sponge", + unicode_version: "11.0", + }, + { + emoji: "🧯", + aliases: ["fire_extinguisher"], + tags: [], + category: "Objects", + description: "fire extinguisher", + unicode_version: "11.0", + }, + { + emoji: "🛒", + aliases: ["shopping_cart"], + tags: [], + category: "Objects", + description: "shopping cart", + unicode_version: "9.0", + }, + { + emoji: "🚬", + aliases: ["smoking"], + tags: ["cigarette"], + category: "Objects", + description: "cigarette", + unicode_version: "6.0", + }, + { + emoji: "⚰️", + aliases: ["coffin"], + tags: ["funeral"], + category: "Objects", + description: "coffin", + unicode_version: "4.1", + }, + { + emoji: "🪦", + aliases: ["headstone"], + tags: [], + category: "Objects", + description: "headstone", + unicode_version: "13.0", + }, + { + emoji: "⚱️", + aliases: ["funeral_urn"], + tags: [], + category: "Objects", + description: "funeral urn", + unicode_version: "4.1", + }, + { + emoji: "🗿", + aliases: ["moyai"], + tags: ["stone"], + category: "Objects", + description: "moai", + unicode_version: "6.0", + }, + { + emoji: "🪧", + aliases: ["placard"], + tags: [], + category: "Objects", + description: "placard", + unicode_version: "13.0", + }, + { + emoji: "🏧", + aliases: ["atm"], + tags: [], + category: "Symbols", + description: "ATM sign", + unicode_version: "6.0", + }, + { + emoji: "🚮", + aliases: ["put_litter_in_its_place"], + tags: [], + category: "Symbols", + description: "litter in bin sign", + unicode_version: "6.0", + }, + { + emoji: "🚰", + aliases: ["potable_water"], + tags: [], + category: "Symbols", + description: "potable water", + unicode_version: "6.0", + }, + { + emoji: "♿", + aliases: ["wheelchair"], + tags: ["accessibility"], + category: "Symbols", + description: "wheelchair symbol", + unicode_version: "4.1", + }, + { + emoji: "🚹", + aliases: ["mens"], + tags: [], + category: "Symbols", + description: "men’s room", + unicode_version: "6.0", + }, + { + emoji: "🚺", + aliases: ["womens"], + tags: [], + category: "Symbols", + description: "women’s room", + unicode_version: "6.0", + }, + { + emoji: "🚻", + aliases: ["restroom"], + tags: ["toilet"], + category: "Symbols", + description: "restroom", + unicode_version: "6.0", + }, + { + emoji: "🚼", + aliases: ["baby_symbol"], + tags: [], + category: "Symbols", + description: "baby symbol", + unicode_version: "6.0", + }, + { + emoji: "🚾", + aliases: ["wc"], + tags: ["toilet", "restroom"], + category: "Symbols", + description: "water closet", + unicode_version: "6.0", + }, + { + emoji: "🛂", + aliases: ["passport_control"], + tags: [], + category: "Symbols", + description: "passport control", + unicode_version: "6.0", + }, + { + emoji: "🛃", + aliases: ["customs"], + tags: [], + category: "Symbols", + description: "customs", + unicode_version: "6.0", + }, + { + emoji: "🛄", + aliases: ["baggage_claim"], + tags: ["airport"], + category: "Symbols", + description: "baggage claim", + unicode_version: "6.0", + }, + { + emoji: "🛅", + aliases: ["left_luggage"], + tags: [], + category: "Symbols", + description: "left luggage", + unicode_version: "6.0", + }, + { + emoji: "⚠️", + aliases: ["warning"], + tags: ["wip"], + category: "Symbols", + description: "warning", + unicode_version: "4.0", + }, + { + emoji: "🚸", + aliases: ["children_crossing"], + tags: [], + category: "Symbols", + description: "children crossing", + unicode_version: "6.0", + }, + { + emoji: "⛔", + aliases: ["no_entry"], + tags: ["limit"], + category: "Symbols", + description: "no entry", + unicode_version: "5.2", + }, + { + emoji: "🚫", + aliases: ["no_entry_sign"], + tags: ["block", "forbidden"], + category: "Symbols", + description: "prohibited", + unicode_version: "6.0", + }, + { + emoji: "🚳", + aliases: ["no_bicycles"], + tags: [], + category: "Symbols", + description: "no bicycles", + unicode_version: "6.0", + }, + { + emoji: "🚭", + aliases: ["no_smoking"], + tags: [], + category: "Symbols", + description: "no smoking", + unicode_version: "6.0", + }, + { + emoji: "🚯", + aliases: ["do_not_litter"], + tags: [], + category: "Symbols", + description: "no littering", + unicode_version: "6.0", + }, + { + emoji: "🚱", + aliases: ["non-potable_water"], + tags: [], + category: "Symbols", + description: "non-potable water", + unicode_version: "6.0", + }, + { + emoji: "🚷", + aliases: ["no_pedestrians"], + tags: [], + category: "Symbols", + description: "no pedestrians", + unicode_version: "6.0", + }, + { + emoji: "📵", + aliases: ["no_mobile_phones"], + tags: [], + category: "Symbols", + description: "no mobile phones", + unicode_version: "6.0", + }, + { + emoji: "🔞", + aliases: ["underage"], + tags: [], + category: "Symbols", + description: "no one under eighteen", + unicode_version: "6.0", + }, + { + emoji: "☢️", + aliases: ["radioactive"], + tags: [], + category: "Symbols", + description: "radioactive", + unicode_version: "", + }, + { + emoji: "☣️", + aliases: ["biohazard"], + tags: [], + category: "Symbols", + description: "biohazard", + unicode_version: "", + }, + { + emoji: "⬆️", + aliases: ["arrow_up"], + tags: [], + category: "Symbols", + description: "up arrow", + unicode_version: "4.0", + }, + { + emoji: "↗️", + aliases: ["arrow_upper_right"], + tags: [], + category: "Symbols", + description: "up-right arrow", + unicode_version: "", + }, + { + emoji: "➡️", + aliases: ["arrow_right"], + tags: [], + category: "Symbols", + description: "right arrow", + unicode_version: "", + }, + { + emoji: "↘️", + aliases: ["arrow_lower_right"], + tags: [], + category: "Symbols", + description: "down-right arrow", + unicode_version: "", + }, + { + emoji: "⬇️", + aliases: ["arrow_down"], + tags: [], + category: "Symbols", + description: "down arrow", + unicode_version: "4.0", + }, + { + emoji: "↙️", + aliases: ["arrow_lower_left"], + tags: [], + category: "Symbols", + description: "down-left arrow", + unicode_version: "", + }, + { + emoji: "⬅️", + aliases: ["arrow_left"], + tags: [], + category: "Symbols", + description: "left arrow", + unicode_version: "4.0", + }, + { + emoji: "↖️", + aliases: ["arrow_upper_left"], + tags: [], + category: "Symbols", + description: "up-left arrow", + unicode_version: "", + }, + { + emoji: "↕️", + aliases: ["arrow_up_down"], + tags: [], + category: "Symbols", + description: "up-down arrow", + unicode_version: "", + }, + { + emoji: "↔️", + aliases: ["left_right_arrow"], + tags: [], + category: "Symbols", + description: "left-right arrow", + unicode_version: "", + }, + { + emoji: "↩️", + aliases: ["leftwards_arrow_with_hook"], + tags: ["return"], + category: "Symbols", + description: "right arrow curving left", + unicode_version: "", + }, + { + emoji: "↪️", + aliases: ["arrow_right_hook"], + tags: [], + category: "Symbols", + description: "left arrow curving right", + unicode_version: "", + }, + { + emoji: "⤴️", + aliases: ["arrow_heading_up"], + tags: [], + category: "Symbols", + description: "right arrow curving up", + unicode_version: "", + }, + { + emoji: "⤵️", + aliases: ["arrow_heading_down"], + tags: [], + category: "Symbols", + description: "right arrow curving down", + unicode_version: "", + }, + { + emoji: "🔃", + aliases: ["arrows_clockwise"], + tags: [], + category: "Symbols", + description: "clockwise vertical arrows", + unicode_version: "6.0", + }, + { + emoji: "🔄", + aliases: ["arrows_counterclockwise"], + tags: ["sync"], + category: "Symbols", + description: "counterclockwise arrows button", + unicode_version: "6.0", + }, + { + emoji: "🔙", + aliases: ["back"], + tags: [], + category: "Symbols", + description: "BACK arrow", + unicode_version: "6.0", + }, + { + emoji: "🔚", + aliases: ["end"], + tags: [], + category: "Symbols", + description: "END arrow", + unicode_version: "6.0", + }, + { + emoji: "🔛", + aliases: ["on"], + tags: [], + category: "Symbols", + description: "ON! arrow", + unicode_version: "6.0", + }, + { + emoji: "🔜", + aliases: ["soon"], + tags: [], + category: "Symbols", + description: "SOON arrow", + unicode_version: "6.0", + }, + { + emoji: "🔝", + aliases: ["top"], + tags: [], + category: "Symbols", + description: "TOP arrow", + unicode_version: "6.0", + }, + { + emoji: "🛐", + aliases: ["place_of_worship"], + tags: [], + category: "Symbols", + description: "place of worship", + unicode_version: "8.0", + }, + { + emoji: "⚛️", + aliases: ["atom_symbol"], + tags: [], + category: "Symbols", + description: "atom symbol", + unicode_version: "4.1", + }, + { + emoji: "🕉️", + aliases: ["om"], + tags: [], + category: "Symbols", + description: "om", + unicode_version: "7.0", + }, + { + emoji: "✡️", + aliases: ["star_of_david"], + tags: [], + category: "Symbols", + description: "star of David", + unicode_version: "", + }, + { + emoji: "☸️", + aliases: ["wheel_of_dharma"], + tags: [], + category: "Symbols", + description: "wheel of dharma", + unicode_version: "", + }, + { + emoji: "☯️", + aliases: ["yin_yang"], + tags: [], + category: "Symbols", + description: "yin yang", + unicode_version: "", + }, + { + emoji: "✝️", + aliases: ["latin_cross"], + tags: [], + category: "Symbols", + description: "latin cross", + unicode_version: "", + }, + { + emoji: "☦️", + aliases: ["orthodox_cross"], + tags: [], + category: "Symbols", + description: "orthodox cross", + unicode_version: "", + }, + { + emoji: "☪️", + aliases: ["star_and_crescent"], + tags: [], + category: "Symbols", + description: "star and crescent", + unicode_version: "", + }, + { + emoji: "☮️", + aliases: ["peace_symbol"], + tags: [], + category: "Symbols", + description: "peace symbol", + unicode_version: "", + }, + { + emoji: "🕎", + aliases: ["menorah"], + tags: [], + category: "Symbols", + description: "menorah", + unicode_version: "8.0", + }, + { + emoji: "🔯", + aliases: ["six_pointed_star"], + tags: [], + category: "Symbols", + description: "dotted six-pointed star", + unicode_version: "6.0", + }, + { + emoji: "♈", + aliases: ["aries"], + tags: [], + category: "Symbols", + description: "Aries", + unicode_version: "", + }, + { + emoji: "♉", + aliases: ["taurus"], + tags: [], + category: "Symbols", + description: "Taurus", + unicode_version: "", + }, + { + emoji: "♊", + aliases: ["gemini"], + tags: [], + category: "Symbols", + description: "Gemini", + unicode_version: "", + }, + { + emoji: "♋", + aliases: ["cancer"], + tags: [], + category: "Symbols", + description: "Cancer", + unicode_version: "", + }, + { + emoji: "♌", + aliases: ["leo"], + tags: [], + category: "Symbols", + description: "Leo", + unicode_version: "", + }, + { + emoji: "♍", + aliases: ["virgo"], + tags: [], + category: "Symbols", + description: "Virgo", + unicode_version: "", + }, + { + emoji: "♎", + aliases: ["libra"], + tags: [], + category: "Symbols", + description: "Libra", + unicode_version: "", + }, + { + emoji: "♏", + aliases: ["scorpius"], + tags: [], + category: "Symbols", + description: "Scorpio", + unicode_version: "", + }, + { + emoji: "♐", + aliases: ["sagittarius"], + tags: [], + category: "Symbols", + description: "Sagittarius", + unicode_version: "", + }, + { + emoji: "♑", + aliases: ["capricorn"], + tags: [], + category: "Symbols", + description: "Capricorn", + unicode_version: "", + }, + { + emoji: "♒", + aliases: ["aquarius"], + tags: [], + category: "Symbols", + description: "Aquarius", + unicode_version: "", + }, + { + emoji: "♓", + aliases: ["pisces"], + tags: [], + category: "Symbols", + description: "Pisces", + unicode_version: "", + }, + { + emoji: "⛎", + aliases: ["ophiuchus"], + tags: [], + category: "Symbols", + description: "Ophiuchus", + unicode_version: "6.0", + }, + { + emoji: "🔀", + aliases: ["twisted_rightwards_arrows"], + tags: ["shuffle"], + category: "Symbols", + description: "shuffle tracks button", + unicode_version: "6.0", + }, + { + emoji: "🔁", + aliases: ["repeat"], + tags: ["loop"], + category: "Symbols", + description: "repeat button", + unicode_version: "6.0", + }, + { + emoji: "🔂", + aliases: ["repeat_one"], + tags: [], + category: "Symbols", + description: "repeat single button", + unicode_version: "6.0", + }, + { + emoji: "▶️", + aliases: ["arrow_forward"], + tags: [], + category: "Symbols", + description: "play button", + unicode_version: "", + }, + { + emoji: "⏩", + aliases: ["fast_forward"], + tags: [], + category: "Symbols", + description: "fast-forward button", + unicode_version: "6.0", + }, + { + emoji: "⏭️", + aliases: ["next_track_button"], + tags: [], + category: "Symbols", + description: "next track button", + unicode_version: "6.0", + }, + { + emoji: "⏯️", + aliases: ["play_or_pause_button"], + tags: [], + category: "Symbols", + description: "play or pause button", + unicode_version: "6.0", + }, + { + emoji: "◀️", + aliases: ["arrow_backward"], + tags: [], + category: "Symbols", + description: "reverse button", + unicode_version: "", + }, + { + emoji: "⏪", + aliases: ["rewind"], + tags: [], + category: "Symbols", + description: "fast reverse button", + unicode_version: "6.0", + }, + { + emoji: "⏮️", + aliases: ["previous_track_button"], + tags: [], + category: "Symbols", + description: "last track button", + unicode_version: "6.0", + }, + { + emoji: "🔼", + aliases: ["arrow_up_small"], + tags: [], + category: "Symbols", + description: "upwards button", + unicode_version: "6.0", + }, + { + emoji: "⏫", + aliases: ["arrow_double_up"], + tags: [], + category: "Symbols", + description: "fast up button", + unicode_version: "6.0", + }, + { + emoji: "🔽", + aliases: ["arrow_down_small"], + tags: [], + category: "Symbols", + description: "downwards button", + unicode_version: "6.0", + }, + { + emoji: "⏬", + aliases: ["arrow_double_down"], + tags: [], + category: "Symbols", + description: "fast down button", + unicode_version: "6.0", + }, + { + emoji: "⏸️", + aliases: ["pause_button"], + tags: [], + category: "Symbols", + description: "pause button", + unicode_version: "7.0", + }, + { + emoji: "⏹️", + aliases: ["stop_button"], + tags: [], + category: "Symbols", + description: "stop button", + unicode_version: "7.0", + }, + { + emoji: "⏺️", + aliases: ["record_button"], + tags: [], + category: "Symbols", + description: "record button", + unicode_version: "7.0", + }, + { + emoji: "⏏️", + aliases: ["eject_button"], + tags: [], + category: "Symbols", + description: "eject button", + unicode_version: "11.0", + }, + { + emoji: "🎦", + aliases: ["cinema"], + tags: ["film", "movie"], + category: "Symbols", + description: "cinema", + unicode_version: "6.0", + }, + { + emoji: "🔅", + aliases: ["low_brightness"], + tags: [], + category: "Symbols", + description: "dim button", + unicode_version: "6.0", + }, + { + emoji: "🔆", + aliases: ["high_brightness"], + tags: [], + category: "Symbols", + description: "bright button", + unicode_version: "6.0", + }, + { + emoji: "📶", + aliases: ["signal_strength"], + tags: ["wifi"], + category: "Symbols", + description: "antenna bars", + unicode_version: "6.0", + }, + { + emoji: "📳", + aliases: ["vibration_mode"], + tags: [], + category: "Symbols", + description: "vibration mode", + unicode_version: "6.0", + }, + { + emoji: "📴", + aliases: ["mobile_phone_off"], + tags: ["mute", "off"], + category: "Symbols", + description: "mobile phone off", + unicode_version: "6.0", + }, + { + emoji: "♀️", + aliases: ["female_sign"], + tags: [], + category: "Symbols", + description: "female sign", + unicode_version: "11.0", + }, + { + emoji: "♂️", + aliases: ["male_sign"], + tags: [], + category: "Symbols", + description: "male sign", + unicode_version: "11.0", + }, + { + emoji: "⚧️", + aliases: ["transgender_symbol"], + tags: [], + category: "Symbols", + description: "transgender symbol", + unicode_version: "13.0", + }, + { + emoji: "✖️", + aliases: ["heavy_multiplication_x"], + tags: [], + category: "Symbols", + description: "multiply", + unicode_version: "", + }, + { + emoji: "➕", + aliases: ["heavy_plus_sign"], + tags: [], + category: "Symbols", + description: "plus", + unicode_version: "6.0", + }, + { + emoji: "➖", + aliases: ["heavy_minus_sign"], + tags: [], + category: "Symbols", + description: "minus", + unicode_version: "6.0", + }, + { + emoji: "➗", + aliases: ["heavy_division_sign"], + tags: [], + category: "Symbols", + description: "divide", + unicode_version: "6.0", + }, + { + emoji: "♾️", + aliases: ["infinity"], + tags: [], + category: "Symbols", + description: "infinity", + unicode_version: "11.0", + }, + { + emoji: "‼️", + aliases: ["bangbang"], + tags: [], + category: "Symbols", + description: "double exclamation mark", + unicode_version: "", + }, + { + emoji: "⁉️", + aliases: ["interrobang"], + tags: [], + category: "Symbols", + description: "exclamation question mark", + unicode_version: "3.0", + }, + { + emoji: "❓", + aliases: ["question"], + tags: ["confused"], + category: "Symbols", + description: "red question mark", + unicode_version: "6.0", + }, + { + emoji: "❔", + aliases: ["grey_question"], + tags: [], + category: "Symbols", + description: "white question mark", + unicode_version: "6.0", + }, + { + emoji: "❕", + aliases: ["grey_exclamation"], + tags: [], + category: "Symbols", + description: "white exclamation mark", + unicode_version: "6.0", + }, + { + emoji: "❗", + aliases: ["exclamation", "heavy_exclamation_mark"], + tags: ["bang"], + category: "Symbols", + description: "red exclamation mark", + unicode_version: "5.2", + }, + { + emoji: "〰️", + aliases: ["wavy_dash"], + tags: [], + category: "Symbols", + description: "wavy dash", + unicode_version: "", + }, + { + emoji: "💱", + aliases: ["currency_exchange"], + tags: [], + category: "Symbols", + description: "currency exchange", + unicode_version: "6.0", + }, + { + emoji: "💲", + aliases: ["heavy_dollar_sign"], + tags: [], + category: "Symbols", + description: "heavy dollar sign", + unicode_version: "6.0", + }, + { + emoji: "⚕️", + aliases: ["medical_symbol"], + tags: [], + category: "Symbols", + description: "medical symbol", + unicode_version: "11.0", + }, + { + emoji: "♻️", + aliases: ["recycle"], + tags: ["environment", "green"], + category: "Symbols", + description: "recycling symbol", + unicode_version: "3.2", + }, + { + emoji: "⚜️", + aliases: ["fleur_de_lis"], + tags: [], + category: "Symbols", + description: "fleur-de-lis", + unicode_version: "4.1", + }, + { + emoji: "🔱", + aliases: ["trident"], + tags: [], + category: "Symbols", + description: "trident emblem", + unicode_version: "6.0", + }, + { + emoji: "📛", + aliases: ["name_badge"], + tags: [], + category: "Symbols", + description: "name badge", + unicode_version: "6.0", + }, + { + emoji: "🔰", + aliases: ["beginner"], + tags: [], + category: "Symbols", + description: "Japanese symbol for beginner", + unicode_version: "6.0", + }, + { + emoji: "⭕", + aliases: ["o"], + tags: [], + category: "Symbols", + description: "hollow red circle", + unicode_version: "5.2", + }, + { + emoji: "✅", + aliases: ["white_check_mark"], + tags: [], + category: "Symbols", + description: "check mark button", + unicode_version: "6.0", + }, + { + emoji: "☑️", + aliases: ["ballot_box_with_check"], + tags: [], + category: "Symbols", + description: "check box with check", + unicode_version: "", + }, + { + emoji: "✔️", + aliases: ["heavy_check_mark"], + tags: [], + category: "Symbols", + description: "check mark", + unicode_version: "", + }, + { + emoji: "❌", + aliases: ["x"], + tags: [], + category: "Symbols", + description: "cross mark", + unicode_version: "6.0", + }, + { + emoji: "❎", + aliases: ["negative_squared_cross_mark"], + tags: [], + category: "Symbols", + description: "cross mark button", + unicode_version: "6.0", + }, + { + emoji: "➰", + aliases: ["curly_loop"], + tags: [], + category: "Symbols", + description: "curly loop", + unicode_version: "6.0", + }, + { + emoji: "➿", + aliases: ["loop"], + tags: [], + category: "Symbols", + description: "double curly loop", + unicode_version: "6.0", + }, + { + emoji: "〽️", + aliases: ["part_alternation_mark"], + tags: [], + category: "Symbols", + description: "part alternation mark", + unicode_version: "3.2", + }, + { + emoji: "✳️", + aliases: ["eight_spoked_asterisk"], + tags: [], + category: "Symbols", + description: "eight-spoked asterisk", + unicode_version: "", + }, + { + emoji: "✴️", + aliases: ["eight_pointed_black_star"], + tags: [], + category: "Symbols", + description: "eight-pointed star", + unicode_version: "", + }, + { + emoji: "❇️", + aliases: ["sparkle"], + tags: [], + category: "Symbols", + description: "sparkle", + unicode_version: "", + }, + { + emoji: "©️", + aliases: ["copyright"], + tags: [], + category: "Symbols", + description: "copyright", + unicode_version: "", + }, + { + emoji: "®️", + aliases: ["registered"], + tags: [], + category: "Symbols", + description: "registered", + unicode_version: "", + }, + { + emoji: "™️", + aliases: ["tm"], + tags: ["trademark"], + category: "Symbols", + description: "trade mark", + unicode_version: "", + }, + { + emoji: "#️⃣", + aliases: ["hash"], + tags: ["number"], + category: "Symbols", + description: "keycap: #", + unicode_version: "", + }, + { + emoji: "*️⃣", + aliases: ["asterisk"], + tags: [], + category: "Symbols", + description: "keycap: *", + unicode_version: "", + }, + { + emoji: "0️⃣", + aliases: ["zero"], + tags: [], + category: "Symbols", + description: "keycap: 0", + unicode_version: "", + }, + { + emoji: "1️⃣", + aliases: ["one"], + tags: [], + category: "Symbols", + description: "keycap: 1", + unicode_version: "", + }, + { + emoji: "2️⃣", + aliases: ["two"], + tags: [], + category: "Symbols", + description: "keycap: 2", + unicode_version: "", + }, + { + emoji: "3️⃣", + aliases: ["three"], + tags: [], + category: "Symbols", + description: "keycap: 3", + unicode_version: "", + }, + { + emoji: "4️⃣", + aliases: ["four"], + tags: [], + category: "Symbols", + description: "keycap: 4", + unicode_version: "", + }, + { + emoji: "5️⃣", + aliases: ["five"], + tags: [], + category: "Symbols", + description: "keycap: 5", + unicode_version: "", + }, + { + emoji: "6️⃣", + aliases: ["six"], + tags: [], + category: "Symbols", + description: "keycap: 6", + unicode_version: "", + }, + { + emoji: "7️⃣", + aliases: ["seven"], + tags: [], + category: "Symbols", + description: "keycap: 7", + unicode_version: "", + }, + { + emoji: "8️⃣", + aliases: ["eight"], + tags: [], + category: "Symbols", + description: "keycap: 8", + unicode_version: "", + }, + { + emoji: "9️⃣", + aliases: ["nine"], + tags: [], + category: "Symbols", + description: "keycap: 9", + unicode_version: "", + }, + { + emoji: "🔟", + aliases: ["keycap_ten"], + tags: [], + category: "Symbols", + description: "keycap: 10", + unicode_version: "6.0", + }, + { + emoji: "🔠", + aliases: ["capital_abcd"], + tags: ["letters"], + category: "Symbols", + description: "input latin uppercase", + unicode_version: "6.0", + }, + { + emoji: "🔡", + aliases: ["abcd"], + tags: [], + category: "Symbols", + description: "input latin lowercase", + unicode_version: "6.0", + }, + { + emoji: "🔢", + aliases: ["1234"], + tags: ["numbers"], + category: "Symbols", + description: "input numbers", + unicode_version: "6.0", + }, + { + emoji: "🔣", + aliases: ["symbols"], + tags: [], + category: "Symbols", + description: "input symbols", + unicode_version: "6.0", + }, + { + emoji: "🔤", + aliases: ["abc"], + tags: ["alphabet"], + category: "Symbols", + description: "input latin letters", + unicode_version: "6.0", + }, + { + emoji: "🅰️", + aliases: ["a"], + tags: [], + category: "Symbols", + description: "A button (blood type)", + unicode_version: "6.0", + }, + { + emoji: "🆎", + aliases: ["ab"], + tags: [], + category: "Symbols", + description: "AB button (blood type)", + unicode_version: "6.0", + }, + { + emoji: "🅱️", + aliases: ["b"], + tags: [], + category: "Symbols", + description: "B button (blood type)", + unicode_version: "6.0", + }, + { + emoji: "🆑", + aliases: ["cl"], + tags: [], + category: "Symbols", + description: "CL button", + unicode_version: "6.0", + }, + { + emoji: "🆒", + aliases: ["cool"], + tags: [], + category: "Symbols", + description: "COOL button", + unicode_version: "6.0", + }, + { + emoji: "🆓", + aliases: ["free"], + tags: [], + category: "Symbols", + description: "FREE button", + unicode_version: "6.0", + }, + { + emoji: "ℹ️", + aliases: ["information_source"], + tags: [], + category: "Symbols", + description: "information", + unicode_version: "3.0", + }, + { + emoji: "🆔", + aliases: ["id"], + tags: [], + category: "Symbols", + description: "ID button", + unicode_version: "6.0", + }, + { + emoji: "Ⓜ️", + aliases: ["m"], + tags: [], + category: "Symbols", + description: "circled M", + unicode_version: "", + }, + { + emoji: "🆕", + aliases: ["new"], + tags: ["fresh"], + category: "Symbols", + description: "NEW button", + unicode_version: "6.0", + }, + { + emoji: "🆖", + aliases: ["ng"], + tags: [], + category: "Symbols", + description: "NG button", + unicode_version: "6.0", + }, + { + emoji: "🅾️", + aliases: ["o2"], + tags: [], + category: "Symbols", + description: "O button (blood type)", + unicode_version: "6.0", + }, + { + emoji: "🆗", + aliases: ["ok"], + tags: ["yes"], + category: "Symbols", + description: "OK button", + unicode_version: "6.0", + }, + { + emoji: "🅿️", + aliases: ["parking"], + tags: [], + category: "Symbols", + description: "P button", + unicode_version: "5.2", + }, + { + emoji: "🆘", + aliases: ["sos"], + tags: ["help", "emergency"], + category: "Symbols", + description: "SOS button", + unicode_version: "6.0", + }, + { + emoji: "🆙", + aliases: ["up"], + tags: [], + category: "Symbols", + description: "UP! button", + unicode_version: "6.0", + }, + { + emoji: "🆚", + aliases: ["vs"], + tags: [], + category: "Symbols", + description: "VS button", + unicode_version: "6.0", + }, + { + emoji: "🈁", + aliases: ["koko"], + tags: [], + category: "Symbols", + description: "Japanese “here” button", + unicode_version: "6.0", + }, + { + emoji: "🈂️", + aliases: ["sa"], + tags: [], + category: "Symbols", + description: "Japanese “service charge” button", + unicode_version: "6.0", + }, + { + emoji: "🈷️", + aliases: ["u6708"], + tags: [], + category: "Symbols", + description: "Japanese “monthly amount” button", + unicode_version: "6.0", + }, + { + emoji: "🈶", + aliases: ["u6709"], + tags: [], + category: "Symbols", + description: "Japanese “not free of charge” button", + unicode_version: "6.0", + }, + { + emoji: "🈯", + aliases: ["u6307"], + tags: [], + category: "Symbols", + description: "Japanese “reserved” button", + unicode_version: "", + }, + { + emoji: "🉐", + aliases: ["ideograph_advantage"], + tags: [], + category: "Symbols", + description: "Japanese “bargain” button", + unicode_version: "6.0", + }, + { + emoji: "🈹", + aliases: ["u5272"], + tags: [], + category: "Symbols", + description: "Japanese “discount” button", + unicode_version: "6.0", + }, + { + emoji: "🈚", + aliases: ["u7121"], + tags: [], + category: "Symbols", + description: "Japanese “free of charge” button", + unicode_version: "", + }, + { + emoji: "🈲", + aliases: ["u7981"], + tags: [], + category: "Symbols", + description: "Japanese “prohibited” button", + unicode_version: "6.0", + }, + { + emoji: "🉑", + aliases: ["accept"], + tags: [], + category: "Symbols", + description: "Japanese “acceptable” button", + unicode_version: "6.0", + }, + { + emoji: "🈸", + aliases: ["u7533"], + tags: [], + category: "Symbols", + description: "Japanese “application” button", + unicode_version: "6.0", + }, + { + emoji: "🈴", + aliases: ["u5408"], + tags: [], + category: "Symbols", + description: "Japanese “passing grade” button", + unicode_version: "6.0", + }, + { + emoji: "🈳", + aliases: ["u7a7a"], + tags: [], + category: "Symbols", + description: "Japanese “vacancy” button", + unicode_version: "6.0", + }, + { + emoji: "㊗️", + aliases: ["congratulations"], + tags: [], + category: "Symbols", + description: "Japanese “congratulations” button", + unicode_version: "", + }, + { + emoji: "㊙️", + aliases: ["secret"], + tags: [], + category: "Symbols", + description: "Japanese “secret” button", + unicode_version: "", + }, + { + emoji: "🈺", + aliases: ["u55b6"], + tags: [], + category: "Symbols", + description: "Japanese “open for business” button", + unicode_version: "6.0", + }, + { + emoji: "🈵", + aliases: ["u6e80"], + tags: [], + category: "Symbols", + description: "Japanese “no vacancy” button", + unicode_version: "6.0", + }, + { + emoji: "🔴", + aliases: ["red_circle"], + tags: [], + category: "Symbols", + description: "red circle", + unicode_version: "6.0", + }, + { + emoji: "🟠", + aliases: ["orange_circle"], + tags: [], + category: "Symbols", + description: "orange circle", + unicode_version: "12.0", + }, + { + emoji: "🟡", + aliases: ["yellow_circle"], + tags: [], + category: "Symbols", + description: "yellow circle", + unicode_version: "12.0", + }, + { + emoji: "🟢", + aliases: ["green_circle"], + tags: [], + category: "Symbols", + description: "green circle", + unicode_version: "12.0", + }, + { + emoji: "🔵", + aliases: ["large_blue_circle"], + tags: [], + category: "Symbols", + description: "blue circle", + unicode_version: "6.0", + }, + { + emoji: "🟣", + aliases: ["purple_circle"], + tags: [], + category: "Symbols", + description: "purple circle", + unicode_version: "12.0", + }, + { + emoji: "🟤", + aliases: ["brown_circle"], + tags: [], + category: "Symbols", + description: "brown circle", + unicode_version: "12.0", + }, + { + emoji: "⚫", + aliases: ["black_circle"], + tags: [], + category: "Symbols", + description: "black circle", + unicode_version: "4.1", + }, + { + emoji: "⚪", + aliases: ["white_circle"], + tags: [], + category: "Symbols", + description: "white circle", + unicode_version: "4.1", + }, + { + emoji: "🟥", + aliases: ["red_square"], + tags: [], + category: "Symbols", + description: "red square", + unicode_version: "12.0", + }, + { + emoji: "🟧", + aliases: ["orange_square"], + tags: [], + category: "Symbols", + description: "orange square", + unicode_version: "12.0", + }, + { + emoji: "🟨", + aliases: ["yellow_square"], + tags: [], + category: "Symbols", + description: "yellow square", + unicode_version: "12.0", + }, + { + emoji: "🟩", + aliases: ["green_square"], + tags: [], + category: "Symbols", + description: "green square", + unicode_version: "12.0", + }, + { + emoji: "🟦", + aliases: ["blue_square"], + tags: [], + category: "Symbols", + description: "blue square", + unicode_version: "12.0", + }, + { + emoji: "🟪", + aliases: ["purple_square"], + tags: [], + category: "Symbols", + description: "purple square", + unicode_version: "12.0", + }, + { + emoji: "🟫", + aliases: ["brown_square"], + tags: [], + category: "Symbols", + description: "brown square", + unicode_version: "12.0", + }, + { + emoji: "⬛", + aliases: ["black_large_square"], + tags: [], + category: "Symbols", + description: "black large square", + unicode_version: "5.1", + }, + { + emoji: "⬜", + aliases: ["white_large_square"], + tags: [], + category: "Symbols", + description: "white large square", + unicode_version: "5.1", + }, + { + emoji: "◼️", + aliases: ["black_medium_square"], + tags: [], + category: "Symbols", + description: "black medium square", + unicode_version: "3.2", + }, + { + emoji: "◻️", + aliases: ["white_medium_square"], + tags: [], + category: "Symbols", + description: "white medium square", + unicode_version: "3.2", + }, + { + emoji: "◾", + aliases: ["black_medium_small_square"], + tags: [], + category: "Symbols", + description: "black medium-small square", + unicode_version: "3.2", + }, + { + emoji: "◽", + aliases: ["white_medium_small_square"], + tags: [], + category: "Symbols", + description: "white medium-small square", + unicode_version: "3.2", + }, + { + emoji: "▪️", + aliases: ["black_small_square"], + tags: [], + category: "Symbols", + description: "black small square", + unicode_version: "", + }, + { + emoji: "▫️", + aliases: ["white_small_square"], + tags: [], + category: "Symbols", + description: "white small square", + unicode_version: "", + }, + { + emoji: "🔶", + aliases: ["large_orange_diamond"], + tags: [], + category: "Symbols", + description: "large orange diamond", + unicode_version: "6.0", + }, + { + emoji: "🔷", + aliases: ["large_blue_diamond"], + tags: [], + category: "Symbols", + description: "large blue diamond", + unicode_version: "6.0", + }, + { + emoji: "🔸", + aliases: ["small_orange_diamond"], + tags: [], + category: "Symbols", + description: "small orange diamond", + unicode_version: "6.0", + }, + { + emoji: "🔹", + aliases: ["small_blue_diamond"], + tags: [], + category: "Symbols", + description: "small blue diamond", + unicode_version: "6.0", + }, + { + emoji: "🔺", + aliases: ["small_red_triangle"], + tags: [], + category: "Symbols", + description: "red triangle pointed up", + unicode_version: "6.0", + }, + { + emoji: "🔻", + aliases: ["small_red_triangle_down"], + tags: [], + category: "Symbols", + description: "red triangle pointed down", + unicode_version: "6.0", + }, + { + emoji: "💠", + aliases: ["diamond_shape_with_a_dot_inside"], + tags: [], + category: "Symbols", + description: "diamond with a dot", + unicode_version: "6.0", + }, + { + emoji: "🔘", + aliases: ["radio_button"], + tags: [], + category: "Symbols", + description: "radio button", + unicode_version: "6.0", + }, + { + emoji: "🔳", + aliases: ["white_square_button"], + tags: [], + category: "Symbols", + description: "white square button", + unicode_version: "6.0", + }, + { + emoji: "🔲", + aliases: ["black_square_button"], + tags: [], + category: "Symbols", + description: "black square button", + unicode_version: "6.0", + }, + { + emoji: "🏁", + aliases: ["checkered_flag"], + tags: ["milestone", "finish"], + category: "Flags", + description: "chequered flag", + unicode_version: "6.0", + }, + { + emoji: "🚩", + aliases: ["triangular_flag_on_post"], + tags: [], + category: "Flags", + description: "triangular flag", + unicode_version: "6.0", + }, + { + emoji: "🎌", + aliases: ["crossed_flags"], + tags: [], + category: "Flags", + description: "crossed flags", + unicode_version: "6.0", + }, + { + emoji: "🏴", + aliases: ["black_flag"], + tags: [], + category: "Flags", + description: "black flag", + unicode_version: "7.0", + }, + { + emoji: "🏳️", + aliases: ["white_flag"], + tags: [], + category: "Flags", + description: "white flag", + unicode_version: "7.0", + }, + { + emoji: "🏳️‍🌈", + aliases: ["rainbow_flag"], + tags: ["pride"], + category: "Flags", + description: "rainbow flag", + unicode_version: "6.0", + }, + { + emoji: "🏳️‍⚧️", + aliases: ["transgender_flag"], + tags: [], + category: "Flags", + description: "transgender flag", + unicode_version: "13.0", + }, + { + emoji: "🏴‍☠️", + aliases: ["pirate_flag"], + tags: [], + category: "Flags", + description: "pirate flag", + unicode_version: "11.0", + }, + { + emoji: "🇦🇨", + aliases: ["ascension_island"], + tags: [], + category: "Flags", + description: "flag: Ascension Island", + unicode_version: "11.0", + }, + { + emoji: "🇦🇩", + aliases: ["andorra"], + tags: [], + category: "Flags", + description: "flag: Andorra", + unicode_version: "6.0", + }, + { + emoji: "🇦🇪", + aliases: ["united_arab_emirates"], + tags: [], + category: "Flags", + description: "flag: United Arab Emirates", + unicode_version: "6.0", + }, + { + emoji: "🇦🇫", + aliases: ["afghanistan"], + tags: [], + category: "Flags", + description: "flag: Afghanistan", + unicode_version: "6.0", + }, + { + emoji: "🇦🇬", + aliases: ["antigua_barbuda"], + tags: [], + category: "Flags", + description: "flag: Antigua & Barbuda", + unicode_version: "6.0", + }, + { + emoji: "🇦🇮", + aliases: ["anguilla"], + tags: [], + category: "Flags", + description: "flag: Anguilla", + unicode_version: "6.0", + }, + { + emoji: "🇦🇱", + aliases: ["albania"], + tags: [], + category: "Flags", + description: "flag: Albania", + unicode_version: "6.0", + }, + { + emoji: "🇦🇲", + aliases: ["armenia"], + tags: [], + category: "Flags", + description: "flag: Armenia", + unicode_version: "6.0", + }, + { + emoji: "🇦🇴", + aliases: ["angola"], + tags: [], + category: "Flags", + description: "flag: Angola", + unicode_version: "6.0", + }, + { + emoji: "🇦🇶", + aliases: ["antarctica"], + tags: [], + category: "Flags", + description: "flag: Antarctica", + unicode_version: "6.0", + }, + { + emoji: "🇦🇷", + aliases: ["argentina"], + tags: [], + category: "Flags", + description: "flag: Argentina", + unicode_version: "6.0", + }, + { + emoji: "🇦🇸", + aliases: ["american_samoa"], + tags: [], + category: "Flags", + description: "flag: American Samoa", + unicode_version: "6.0", + }, + { + emoji: "🇦🇹", + aliases: ["austria"], + tags: [], + category: "Flags", + description: "flag: Austria", + unicode_version: "6.0", + }, + { + emoji: "🇦🇺", + aliases: ["australia"], + tags: [], + category: "Flags", + description: "flag: Australia", + unicode_version: "6.0", + }, + { + emoji: "🇦🇼", + aliases: ["aruba"], + tags: [], + category: "Flags", + description: "flag: Aruba", + unicode_version: "6.0", + }, + { + emoji: "🇦🇽", + aliases: ["aland_islands"], + tags: [], + category: "Flags", + description: "flag: Åland Islands", + unicode_version: "6.0", + }, + { + emoji: "🇦🇿", + aliases: ["azerbaijan"], + tags: [], + category: "Flags", + description: "flag: Azerbaijan", + unicode_version: "6.0", + }, + { + emoji: "🇧🇦", + aliases: ["bosnia_herzegovina"], + tags: [], + category: "Flags", + description: "flag: Bosnia & Herzegovina", + unicode_version: "6.0", + }, + { + emoji: "🇧🇧", + aliases: ["barbados"], + tags: [], + category: "Flags", + description: "flag: Barbados", + unicode_version: "6.0", + }, + { + emoji: "🇧🇩", + aliases: ["bangladesh"], + tags: [], + category: "Flags", + description: "flag: Bangladesh", + unicode_version: "6.0", + }, + { + emoji: "🇧🇪", + aliases: ["belgium"], + tags: [], + category: "Flags", + description: "flag: Belgium", + unicode_version: "6.0", + }, + { + emoji: "🇧🇫", + aliases: ["burkina_faso"], + tags: [], + category: "Flags", + description: "flag: Burkina Faso", + unicode_version: "6.0", + }, + { + emoji: "🇧🇬", + aliases: ["bulgaria"], + tags: [], + category: "Flags", + description: "flag: Bulgaria", + unicode_version: "6.0", + }, + { + emoji: "🇧🇭", + aliases: ["bahrain"], + tags: [], + category: "Flags", + description: "flag: Bahrain", + unicode_version: "6.0", + }, + { + emoji: "🇧🇮", + aliases: ["burundi"], + tags: [], + category: "Flags", + description: "flag: Burundi", + unicode_version: "6.0", + }, + { + emoji: "🇧🇯", + aliases: ["benin"], + tags: [], + category: "Flags", + description: "flag: Benin", + unicode_version: "6.0", + }, + { + emoji: "🇧🇱", + aliases: ["st_barthelemy"], + tags: [], + category: "Flags", + description: "flag: St. Barthélemy", + unicode_version: "6.0", + }, + { + emoji: "🇧🇲", + aliases: ["bermuda"], + tags: [], + category: "Flags", + description: "flag: Bermuda", + unicode_version: "6.0", + }, + { + emoji: "🇧🇳", + aliases: ["brunei"], + tags: [], + category: "Flags", + description: "flag: Brunei", + unicode_version: "6.0", + }, + { + emoji: "🇧🇴", + aliases: ["bolivia"], + tags: [], + category: "Flags", + description: "flag: Bolivia", + unicode_version: "6.0", + }, + { + emoji: "🇧🇶", + aliases: ["caribbean_netherlands"], + tags: [], + category: "Flags", + description: "flag: Caribbean Netherlands", + unicode_version: "6.0", + }, + { + emoji: "🇧🇷", + aliases: ["brazil"], + tags: [], + category: "Flags", + description: "flag: Brazil", + unicode_version: "6.0", + }, + { + emoji: "🇧🇸", + aliases: ["bahamas"], + tags: [], + category: "Flags", + description: "flag: Bahamas", + unicode_version: "6.0", + }, + { + emoji: "🇧🇹", + aliases: ["bhutan"], + tags: [], + category: "Flags", + description: "flag: Bhutan", + unicode_version: "6.0", + }, + { + emoji: "🇧🇻", + aliases: ["bouvet_island"], + tags: [], + category: "Flags", + description: "flag: Bouvet Island", + unicode_version: "11.0", + }, + { + emoji: "🇧🇼", + aliases: ["botswana"], + tags: [], + category: "Flags", + description: "flag: Botswana", + unicode_version: "6.0", + }, + { + emoji: "🇧🇾", + aliases: ["belarus"], + tags: [], + category: "Flags", + description: "flag: Belarus", + unicode_version: "6.0", + }, + { + emoji: "🇧🇿", + aliases: ["belize"], + tags: [], + category: "Flags", + description: "flag: Belize", + unicode_version: "6.0", + }, + { + emoji: "🇨🇦", + aliases: ["canada"], + tags: [], + category: "Flags", + description: "flag: Canada", + unicode_version: "6.0", + }, + { + emoji: "🇨🇨", + aliases: ["cocos_islands"], + tags: ["keeling"], + category: "Flags", + description: "flag: Cocos (Keeling) Islands", + unicode_version: "6.0", + }, + { + emoji: "🇨🇩", + aliases: ["congo_kinshasa"], + tags: [], + category: "Flags", + description: "flag: Congo - Kinshasa", + unicode_version: "6.0", + }, + { + emoji: "🇨🇫", + aliases: ["central_african_republic"], + tags: [], + category: "Flags", + description: "flag: Central African Republic", + unicode_version: "6.0", + }, + { + emoji: "🇨🇬", + aliases: ["congo_brazzaville"], + tags: [], + category: "Flags", + description: "flag: Congo - Brazzaville", + unicode_version: "6.0", + }, + { + emoji: "🇨🇭", + aliases: ["switzerland"], + tags: [], + category: "Flags", + description: "flag: Switzerland", + unicode_version: "6.0", + }, + { + emoji: "🇨🇮", + aliases: ["cote_divoire"], + tags: ["ivory"], + category: "Flags", + description: "flag: Côte d’Ivoire", + unicode_version: "6.0", + }, + { + emoji: "🇨🇰", + aliases: ["cook_islands"], + tags: [], + category: "Flags", + description: "flag: Cook Islands", + unicode_version: "6.0", + }, + { + emoji: "🇨🇱", + aliases: ["chile"], + tags: [], + category: "Flags", + description: "flag: Chile", + unicode_version: "6.0", + }, + { + emoji: "🇨🇲", + aliases: ["cameroon"], + tags: [], + category: "Flags", + description: "flag: Cameroon", + unicode_version: "6.0", + }, + { + emoji: "🇨🇳", + aliases: ["cn"], + tags: ["china"], + category: "Flags", + description: "flag: China", + unicode_version: "6.0", + }, + { + emoji: "🇨🇴", + aliases: ["colombia"], + tags: [], + category: "Flags", + description: "flag: Colombia", + unicode_version: "6.0", + }, + { + emoji: "🇨🇵", + aliases: ["clipperton_island"], + tags: [], + category: "Flags", + description: "flag: Clipperton Island", + unicode_version: "11.0", + }, + { + emoji: "🇨🇷", + aliases: ["costa_rica"], + tags: [], + category: "Flags", + description: "flag: Costa Rica", + unicode_version: "6.0", + }, + { + emoji: "🇨🇺", + aliases: ["cuba"], + tags: [], + category: "Flags", + description: "flag: Cuba", + unicode_version: "6.0", + }, + { + emoji: "🇨🇻", + aliases: ["cape_verde"], + tags: [], + category: "Flags", + description: "flag: Cape Verde", + unicode_version: "6.0", + }, + { + emoji: "🇨🇼", + aliases: ["curacao"], + tags: [], + category: "Flags", + description: "flag: Curaçao", + unicode_version: "6.0", + }, + { + emoji: "🇨🇽", + aliases: ["christmas_island"], + tags: [], + category: "Flags", + description: "flag: Christmas Island", + unicode_version: "6.0", + }, + { + emoji: "🇨🇾", + aliases: ["cyprus"], + tags: [], + category: "Flags", + description: "flag: Cyprus", + unicode_version: "6.0", + }, + { + emoji: "🇨🇿", + aliases: ["czech_republic"], + tags: [], + category: "Flags", + description: "flag: Czechia", + unicode_version: "6.0", + }, + { + emoji: "🇩🇪", + aliases: ["de"], + tags: ["flag", "germany"], + category: "Flags", + description: "flag: Germany", + unicode_version: "6.0", + }, + { + emoji: "🇩🇬", + aliases: ["diego_garcia"], + tags: [], + category: "Flags", + description: "flag: Diego Garcia", + unicode_version: "11.0", + }, + { + emoji: "🇩🇯", + aliases: ["djibouti"], + tags: [], + category: "Flags", + description: "flag: Djibouti", + unicode_version: "6.0", + }, + { + emoji: "🇩🇰", + aliases: ["denmark"], + tags: [], + category: "Flags", + description: "flag: Denmark", + unicode_version: "6.0", + }, + { + emoji: "🇩🇲", + aliases: ["dominica"], + tags: [], + category: "Flags", + description: "flag: Dominica", + unicode_version: "6.0", + }, + { + emoji: "🇩🇴", + aliases: ["dominican_republic"], + tags: [], + category: "Flags", + description: "flag: Dominican Republic", + unicode_version: "6.0", + }, + { + emoji: "🇩🇿", + aliases: ["algeria"], + tags: [], + category: "Flags", + description: "flag: Algeria", + unicode_version: "6.0", + }, + { + emoji: "🇪🇦", + aliases: ["ceuta_melilla"], + tags: [], + category: "Flags", + description: "flag: Ceuta & Melilla", + unicode_version: "11.0", + }, + { + emoji: "🇪🇨", + aliases: ["ecuador"], + tags: [], + category: "Flags", + description: "flag: Ecuador", + unicode_version: "6.0", + }, + { + emoji: "🇪🇪", + aliases: ["estonia"], + tags: [], + category: "Flags", + description: "flag: Estonia", + unicode_version: "6.0", + }, + { + emoji: "🇪🇬", + aliases: ["egypt"], + tags: [], + category: "Flags", + description: "flag: Egypt", + unicode_version: "6.0", + }, + { + emoji: "🇪🇭", + aliases: ["western_sahara"], + tags: [], + category: "Flags", + description: "flag: Western Sahara", + unicode_version: "6.0", + }, + { + emoji: "🇪🇷", + aliases: ["eritrea"], + tags: [], + category: "Flags", + description: "flag: Eritrea", + unicode_version: "6.0", + }, + { + emoji: "🇪🇸", + aliases: ["es"], + tags: ["spain"], + category: "Flags", + description: "flag: Spain", + unicode_version: "6.0", + }, + { + emoji: "🇪🇹", + aliases: ["ethiopia"], + tags: [], + category: "Flags", + description: "flag: Ethiopia", + unicode_version: "6.0", + }, + { + emoji: "🇪🇺", + aliases: ["eu", "european_union"], + tags: [], + category: "Flags", + description: "flag: European Union", + unicode_version: "6.0", + }, + { + emoji: "🇫🇮", + aliases: ["finland"], + tags: [], + category: "Flags", + description: "flag: Finland", + unicode_version: "6.0", + }, + { + emoji: "🇫🇯", + aliases: ["fiji"], + tags: [], + category: "Flags", + description: "flag: Fiji", + unicode_version: "6.0", + }, + { + emoji: "🇫🇰", + aliases: ["falkland_islands"], + tags: [], + category: "Flags", + description: "flag: Falkland Islands", + unicode_version: "6.0", + }, + { + emoji: "🇫🇲", + aliases: ["micronesia"], + tags: [], + category: "Flags", + description: "flag: Micronesia", + unicode_version: "6.0", + }, + { + emoji: "🇫🇴", + aliases: ["faroe_islands"], + tags: [], + category: "Flags", + description: "flag: Faroe Islands", + unicode_version: "6.0", + }, + { + emoji: "🇫🇷", + aliases: ["fr"], + tags: ["france", "french"], + category: "Flags", + description: "flag: France", + unicode_version: "6.0", + }, + { + emoji: "🇬🇦", + aliases: ["gabon"], + tags: [], + category: "Flags", + description: "flag: Gabon", + unicode_version: "6.0", + }, + { + emoji: "🇬🇧", + aliases: ["gb", "uk"], + tags: ["flag", "british"], + category: "Flags", + description: "flag: United Kingdom", + unicode_version: "6.0", + }, + { + emoji: "🇬🇩", + aliases: ["grenada"], + tags: [], + category: "Flags", + description: "flag: Grenada", + unicode_version: "6.0", + }, + { + emoji: "🇬🇪", + aliases: ["georgia"], + tags: [], + category: "Flags", + description: "flag: Georgia", + unicode_version: "6.0", + }, + { + emoji: "🇬🇫", + aliases: ["french_guiana"], + tags: [], + category: "Flags", + description: "flag: French Guiana", + unicode_version: "6.0", + }, + { + emoji: "🇬🇬", + aliases: ["guernsey"], + tags: [], + category: "Flags", + description: "flag: Guernsey", + unicode_version: "6.0", + }, + { + emoji: "🇬🇭", + aliases: ["ghana"], + tags: [], + category: "Flags", + description: "flag: Ghana", + unicode_version: "6.0", + }, + { + emoji: "🇬🇮", + aliases: ["gibraltar"], + tags: [], + category: "Flags", + description: "flag: Gibraltar", + unicode_version: "6.0", + }, + { + emoji: "🇬🇱", + aliases: ["greenland"], + tags: [], + category: "Flags", + description: "flag: Greenland", + unicode_version: "6.0", + }, + { + emoji: "🇬🇲", + aliases: ["gambia"], + tags: [], + category: "Flags", + description: "flag: Gambia", + unicode_version: "6.0", + }, + { + emoji: "🇬🇳", + aliases: ["guinea"], + tags: [], + category: "Flags", + description: "flag: Guinea", + unicode_version: "6.0", + }, + { + emoji: "🇬🇵", + aliases: ["guadeloupe"], + tags: [], + category: "Flags", + description: "flag: Guadeloupe", + unicode_version: "6.0", + }, + { + emoji: "🇬🇶", + aliases: ["equatorial_guinea"], + tags: [], + category: "Flags", + description: "flag: Equatorial Guinea", + unicode_version: "6.0", + }, + { + emoji: "🇬🇷", + aliases: ["greece"], + tags: [], + category: "Flags", + description: "flag: Greece", + unicode_version: "6.0", + }, + { + emoji: "🇬🇸", + aliases: ["south_georgia_south_sandwich_islands"], + tags: [], + category: "Flags", + description: "flag: South Georgia & South Sandwich Islands", + unicode_version: "6.0", + }, + { + emoji: "🇬🇹", + aliases: ["guatemala"], + tags: [], + category: "Flags", + description: "flag: Guatemala", + unicode_version: "6.0", + }, + { + emoji: "🇬🇺", + aliases: ["guam"], + tags: [], + category: "Flags", + description: "flag: Guam", + unicode_version: "6.0", + }, + { + emoji: "🇬🇼", + aliases: ["guinea_bissau"], + tags: [], + category: "Flags", + description: "flag: Guinea-Bissau", + unicode_version: "6.0", + }, + { + emoji: "🇬🇾", + aliases: ["guyana"], + tags: [], + category: "Flags", + description: "flag: Guyana", + unicode_version: "6.0", + }, + { + emoji: "🇭🇰", + aliases: ["hong_kong"], + tags: [], + category: "Flags", + description: "flag: Hong Kong SAR China", + unicode_version: "6.0", + }, + { + emoji: "🇭🇲", + aliases: ["heard_mcdonald_islands"], + tags: [], + category: "Flags", + description: "flag: Heard & McDonald Islands", + unicode_version: "11.0", + }, + { + emoji: "🇭🇳", + aliases: ["honduras"], + tags: [], + category: "Flags", + description: "flag: Honduras", + unicode_version: "6.0", + }, + { + emoji: "🇭🇷", + aliases: ["croatia"], + tags: [], + category: "Flags", + description: "flag: Croatia", + unicode_version: "6.0", + }, + { + emoji: "🇭🇹", + aliases: ["haiti"], + tags: [], + category: "Flags", + description: "flag: Haiti", + unicode_version: "6.0", + }, + { + emoji: "🇭🇺", + aliases: ["hungary"], + tags: [], + category: "Flags", + description: "flag: Hungary", + unicode_version: "6.0", + }, + { + emoji: "🇮🇨", + aliases: ["canary_islands"], + tags: [], + category: "Flags", + description: "flag: Canary Islands", + unicode_version: "6.0", + }, + { + emoji: "🇮🇩", + aliases: ["indonesia"], + tags: [], + category: "Flags", + description: "flag: Indonesia", + unicode_version: "6.0", + }, + { + emoji: "🇮🇪", + aliases: ["ireland"], + tags: [], + category: "Flags", + description: "flag: Ireland", + unicode_version: "6.0", + }, + { + emoji: "🇮🇱", + aliases: ["israel"], + tags: [], + category: "Flags", + description: "flag: Israel", + unicode_version: "6.0", + }, + { + emoji: "🇮🇲", + aliases: ["isle_of_man"], + tags: [], + category: "Flags", + description: "flag: Isle of Man", + unicode_version: "6.0", + }, + { + emoji: "🇮🇳", + aliases: ["india"], + tags: [], + category: "Flags", + description: "flag: India", + unicode_version: "6.0", + }, + { + emoji: "🇮🇴", + aliases: ["british_indian_ocean_territory"], + tags: [], + category: "Flags", + description: "flag: British Indian Ocean Territory", + unicode_version: "6.0", + }, + { + emoji: "🇮🇶", + aliases: ["iraq"], + tags: [], + category: "Flags", + description: "flag: Iraq", + unicode_version: "6.0", + }, + { + emoji: "🇮🇷", + aliases: ["iran"], + tags: [], + category: "Flags", + description: "flag: Iran", + unicode_version: "6.0", + }, + { + emoji: "🇮🇸", + aliases: ["iceland"], + tags: [], + category: "Flags", + description: "flag: Iceland", + unicode_version: "6.0", + }, + { + emoji: "🇮🇹", + aliases: ["it"], + tags: ["italy"], + category: "Flags", + description: "flag: Italy", + unicode_version: "6.0", + }, + { + emoji: "🇯🇪", + aliases: ["jersey"], + tags: [], + category: "Flags", + description: "flag: Jersey", + unicode_version: "6.0", + }, + { + emoji: "🇯🇲", + aliases: ["jamaica"], + tags: [], + category: "Flags", + description: "flag: Jamaica", + unicode_version: "6.0", + }, + { + emoji: "🇯🇴", + aliases: ["jordan"], + tags: [], + category: "Flags", + description: "flag: Jordan", + unicode_version: "6.0", + }, + { + emoji: "🇯🇵", + aliases: ["jp"], + tags: ["japan"], + category: "Flags", + description: "flag: Japan", + unicode_version: "6.0", + }, + { + emoji: "🇰🇪", + aliases: ["kenya"], + tags: [], + category: "Flags", + description: "flag: Kenya", + unicode_version: "6.0", + }, + { + emoji: "🇰🇬", + aliases: ["kyrgyzstan"], + tags: [], + category: "Flags", + description: "flag: Kyrgyzstan", + unicode_version: "6.0", + }, + { + emoji: "🇰🇭", + aliases: ["cambodia"], + tags: [], + category: "Flags", + description: "flag: Cambodia", + unicode_version: "6.0", + }, + { + emoji: "🇰🇮", + aliases: ["kiribati"], + tags: [], + category: "Flags", + description: "flag: Kiribati", + unicode_version: "6.0", + }, + { + emoji: "🇰🇲", + aliases: ["comoros"], + tags: [], + category: "Flags", + description: "flag: Comoros", + unicode_version: "6.0", + }, + { + emoji: "🇰🇳", + aliases: ["st_kitts_nevis"], + tags: [], + category: "Flags", + description: "flag: St. Kitts & Nevis", + unicode_version: "6.0", + }, + { + emoji: "🇰🇵", + aliases: ["north_korea"], + tags: [], + category: "Flags", + description: "flag: North Korea", + unicode_version: "6.0", + }, + { + emoji: "🇰🇷", + aliases: ["kr"], + tags: ["korea"], + category: "Flags", + description: "flag: South Korea", + unicode_version: "6.0", + }, + { + emoji: "🇰🇼", + aliases: ["kuwait"], + tags: [], + category: "Flags", + description: "flag: Kuwait", + unicode_version: "6.0", + }, + { + emoji: "🇰🇾", + aliases: ["cayman_islands"], + tags: [], + category: "Flags", + description: "flag: Cayman Islands", + unicode_version: "6.0", + }, + { + emoji: "🇰🇿", + aliases: ["kazakhstan"], + tags: [], + category: "Flags", + description: "flag: Kazakhstan", + unicode_version: "6.0", + }, + { + emoji: "🇱🇦", + aliases: ["laos"], + tags: [], + category: "Flags", + description: "flag: Laos", + unicode_version: "6.0", + }, + { + emoji: "🇱🇧", + aliases: ["lebanon"], + tags: [], + category: "Flags", + description: "flag: Lebanon", + unicode_version: "6.0", + }, + { + emoji: "🇱🇨", + aliases: ["st_lucia"], + tags: [], + category: "Flags", + description: "flag: St. Lucia", + unicode_version: "6.0", + }, + { + emoji: "🇱🇮", + aliases: ["liechtenstein"], + tags: [], + category: "Flags", + description: "flag: Liechtenstein", + unicode_version: "6.0", + }, + { + emoji: "🇱🇰", + aliases: ["sri_lanka"], + tags: [], + category: "Flags", + description: "flag: Sri Lanka", + unicode_version: "6.0", + }, + { + emoji: "🇱🇷", + aliases: ["liberia"], + tags: [], + category: "Flags", + description: "flag: Liberia", + unicode_version: "6.0", + }, + { + emoji: "🇱🇸", + aliases: ["lesotho"], + tags: [], + category: "Flags", + description: "flag: Lesotho", + unicode_version: "6.0", + }, + { + emoji: "🇱🇹", + aliases: ["lithuania"], + tags: [], + category: "Flags", + description: "flag: Lithuania", + unicode_version: "6.0", + }, + { + emoji: "🇱🇺", + aliases: ["luxembourg"], + tags: [], + category: "Flags", + description: "flag: Luxembourg", + unicode_version: "6.0", + }, + { + emoji: "🇱🇻", + aliases: ["latvia"], + tags: [], + category: "Flags", + description: "flag: Latvia", + unicode_version: "6.0", + }, + { + emoji: "🇱🇾", + aliases: ["libya"], + tags: [], + category: "Flags", + description: "flag: Libya", + unicode_version: "6.0", + }, + { + emoji: "🇲🇦", + aliases: ["morocco"], + tags: [], + category: "Flags", + description: "flag: Morocco", + unicode_version: "6.0", + }, + { + emoji: "🇲🇨", + aliases: ["monaco"], + tags: [], + category: "Flags", + description: "flag: Monaco", + unicode_version: "6.0", + }, + { + emoji: "🇲🇩", + aliases: ["moldova"], + tags: [], + category: "Flags", + description: "flag: Moldova", + unicode_version: "6.0", + }, + { + emoji: "🇲🇪", + aliases: ["montenegro"], + tags: [], + category: "Flags", + description: "flag: Montenegro", + unicode_version: "6.0", + }, + { + emoji: "🇲🇫", + aliases: ["st_martin"], + tags: [], + category: "Flags", + description: "flag: St. Martin", + unicode_version: "11.0", + }, + { + emoji: "🇲🇬", + aliases: ["madagascar"], + tags: [], + category: "Flags", + description: "flag: Madagascar", + unicode_version: "6.0", + }, + { + emoji: "🇲🇭", + aliases: ["marshall_islands"], + tags: [], + category: "Flags", + description: "flag: Marshall Islands", + unicode_version: "6.0", + }, + { + emoji: "🇲🇰", + aliases: ["macedonia"], + tags: [], + category: "Flags", + description: "flag: North Macedonia", + unicode_version: "6.0", + }, + { + emoji: "🇲🇱", + aliases: ["mali"], + tags: [], + category: "Flags", + description: "flag: Mali", + unicode_version: "6.0", + }, + { + emoji: "🇲🇲", + aliases: ["myanmar"], + tags: ["burma"], + category: "Flags", + description: "flag: Myanmar (Burma)", + unicode_version: "6.0", + }, + { + emoji: "🇲🇳", + aliases: ["mongolia"], + tags: [], + category: "Flags", + description: "flag: Mongolia", + unicode_version: "6.0", + }, + { + emoji: "🇲🇴", + aliases: ["macau"], + tags: [], + category: "Flags", + description: "flag: Macao SAR China", + unicode_version: "6.0", + }, + { + emoji: "🇲🇵", + aliases: ["northern_mariana_islands"], + tags: [], + category: "Flags", + description: "flag: Northern Mariana Islands", + unicode_version: "6.0", + }, + { + emoji: "🇲🇶", + aliases: ["martinique"], + tags: [], + category: "Flags", + description: "flag: Martinique", + unicode_version: "6.0", + }, + { + emoji: "🇲🇷", + aliases: ["mauritania"], + tags: [], + category: "Flags", + description: "flag: Mauritania", + unicode_version: "6.0", + }, + { + emoji: "🇲🇸", + aliases: ["montserrat"], + tags: [], + category: "Flags", + description: "flag: Montserrat", + unicode_version: "6.0", + }, + { + emoji: "🇲🇹", + aliases: ["malta"], + tags: [], + category: "Flags", + description: "flag: Malta", + unicode_version: "6.0", + }, + { + emoji: "🇲🇺", + aliases: ["mauritius"], + tags: [], + category: "Flags", + description: "flag: Mauritius", + unicode_version: "6.0", + }, + { + emoji: "🇲🇻", + aliases: ["maldives"], + tags: [], + category: "Flags", + description: "flag: Maldives", + unicode_version: "6.0", + }, + { + emoji: "🇲🇼", + aliases: ["malawi"], + tags: [], + category: "Flags", + description: "flag: Malawi", + unicode_version: "6.0", + }, + { + emoji: "🇲🇽", + aliases: ["mexico"], + tags: [], + category: "Flags", + description: "flag: Mexico", + unicode_version: "6.0", + }, + { + emoji: "🇲🇾", + aliases: ["malaysia"], + tags: [], + category: "Flags", + description: "flag: Malaysia", + unicode_version: "6.0", + }, + { + emoji: "🇲🇿", + aliases: ["mozambique"], + tags: [], + category: "Flags", + description: "flag: Mozambique", + unicode_version: "6.0", + }, + { + emoji: "🇳🇦", + aliases: ["namibia"], + tags: [], + category: "Flags", + description: "flag: Namibia", + unicode_version: "6.0", + }, + { + emoji: "🇳🇨", + aliases: ["new_caledonia"], + tags: [], + category: "Flags", + description: "flag: New Caledonia", + unicode_version: "6.0", + }, + { + emoji: "🇳🇪", + aliases: ["niger"], + tags: [], + category: "Flags", + description: "flag: Niger", + unicode_version: "6.0", + }, + { + emoji: "🇳🇫", + aliases: ["norfolk_island"], + tags: [], + category: "Flags", + description: "flag: Norfolk Island", + unicode_version: "6.0", + }, + { + emoji: "🇳🇬", + aliases: ["nigeria"], + tags: [], + category: "Flags", + description: "flag: Nigeria", + unicode_version: "6.0", + }, + { + emoji: "🇳🇮", + aliases: ["nicaragua"], + tags: [], + category: "Flags", + description: "flag: Nicaragua", + unicode_version: "6.0", + }, + { + emoji: "🇳🇱", + aliases: ["netherlands"], + tags: [], + category: "Flags", + description: "flag: Netherlands", + unicode_version: "6.0", + }, + { + emoji: "🇳🇴", + aliases: ["norway"], + tags: [], + category: "Flags", + description: "flag: Norway", + unicode_version: "6.0", + }, + { + emoji: "🇳🇵", + aliases: ["nepal"], + tags: [], + category: "Flags", + description: "flag: Nepal", + unicode_version: "6.0", + }, + { + emoji: "🇳🇷", + aliases: ["nauru"], + tags: [], + category: "Flags", + description: "flag: Nauru", + unicode_version: "6.0", + }, + { + emoji: "🇳🇺", + aliases: ["niue"], + tags: [], + category: "Flags", + description: "flag: Niue", + unicode_version: "6.0", + }, + { + emoji: "🇳🇿", + aliases: ["new_zealand"], + tags: [], + category: "Flags", + description: "flag: New Zealand", + unicode_version: "6.0", + }, + { + emoji: "🇴🇲", + aliases: ["oman"], + tags: [], + category: "Flags", + description: "flag: Oman", + unicode_version: "6.0", + }, + { + emoji: "🇵🇦", + aliases: ["panama"], + tags: [], + category: "Flags", + description: "flag: Panama", + unicode_version: "6.0", + }, + { + emoji: "🇵🇪", + aliases: ["peru"], + tags: [], + category: "Flags", + description: "flag: Peru", + unicode_version: "6.0", + }, + { + emoji: "🇵🇫", + aliases: ["french_polynesia"], + tags: [], + category: "Flags", + description: "flag: French Polynesia", + unicode_version: "6.0", + }, + { + emoji: "🇵🇬", + aliases: ["papua_new_guinea"], + tags: [], + category: "Flags", + description: "flag: Papua New Guinea", + unicode_version: "6.0", + }, + { + emoji: "🇵🇭", + aliases: ["philippines"], + tags: [], + category: "Flags", + description: "flag: Philippines", + unicode_version: "6.0", + }, + { + emoji: "🇵🇰", + aliases: ["pakistan"], + tags: [], + category: "Flags", + description: "flag: Pakistan", + unicode_version: "6.0", + }, + { + emoji: "🇵🇱", + aliases: ["poland"], + tags: [], + category: "Flags", + description: "flag: Poland", + unicode_version: "6.0", + }, + { + emoji: "🇵🇲", + aliases: ["st_pierre_miquelon"], + tags: [], + category: "Flags", + description: "flag: St. Pierre & Miquelon", + unicode_version: "6.0", + }, + { + emoji: "🇵🇳", + aliases: ["pitcairn_islands"], + tags: [], + category: "Flags", + description: "flag: Pitcairn Islands", + unicode_version: "6.0", + }, + { + emoji: "🇵🇷", + aliases: ["puerto_rico"], + tags: [], + category: "Flags", + description: "flag: Puerto Rico", + unicode_version: "6.0", + }, + { + emoji: "🇵🇸", + aliases: ["palestinian_territories"], + tags: [], + category: "Flags", + description: "flag: Palestinian Territories", + unicode_version: "6.0", + }, + { + emoji: "🇵🇹", + aliases: ["portugal"], + tags: [], + category: "Flags", + description: "flag: Portugal", + unicode_version: "6.0", + }, + { + emoji: "🇵🇼", + aliases: ["palau"], + tags: [], + category: "Flags", + description: "flag: Palau", + unicode_version: "6.0", + }, + { + emoji: "🇵🇾", + aliases: ["paraguay"], + tags: [], + category: "Flags", + description: "flag: Paraguay", + unicode_version: "6.0", + }, + { + emoji: "🇶🇦", + aliases: ["qatar"], + tags: [], + category: "Flags", + description: "flag: Qatar", + unicode_version: "6.0", + }, + { + emoji: "🇷🇪", + aliases: ["reunion"], + tags: [], + category: "Flags", + description: "flag: Réunion", + unicode_version: "6.0", + }, + { + emoji: "🇷🇴", + aliases: ["romania"], + tags: [], + category: "Flags", + description: "flag: Romania", + unicode_version: "6.0", + }, + { + emoji: "🇷🇸", + aliases: ["serbia"], + tags: [], + category: "Flags", + description: "flag: Serbia", + unicode_version: "6.0", + }, + { + emoji: "🇷🇺", + aliases: ["ru"], + tags: ["russia"], + category: "Flags", + description: "flag: Russia", + unicode_version: "6.0", + }, + { + emoji: "🇷🇼", + aliases: ["rwanda"], + tags: [], + category: "Flags", + description: "flag: Rwanda", + unicode_version: "6.0", + }, + { + emoji: "🇸🇦", + aliases: ["saudi_arabia"], + tags: [], + category: "Flags", + description: "flag: Saudi Arabia", + unicode_version: "6.0", + }, + { + emoji: "🇸🇧", + aliases: ["solomon_islands"], + tags: [], + category: "Flags", + description: "flag: Solomon Islands", + unicode_version: "6.0", + }, + { + emoji: "🇸🇨", + aliases: ["seychelles"], + tags: [], + category: "Flags", + description: "flag: Seychelles", + unicode_version: "6.0", + }, + { + emoji: "🇸🇩", + aliases: ["sudan"], + tags: [], + category: "Flags", + description: "flag: Sudan", + unicode_version: "6.0", + }, + { + emoji: "🇸🇪", + aliases: ["sweden"], + tags: [], + category: "Flags", + description: "flag: Sweden", + unicode_version: "6.0", + }, + { + emoji: "🇸🇬", + aliases: ["singapore"], + tags: [], + category: "Flags", + description: "flag: Singapore", + unicode_version: "6.0", + }, + { + emoji: "🇸🇭", + aliases: ["st_helena"], + tags: [], + category: "Flags", + description: "flag: St. Helena", + unicode_version: "6.0", + }, + { + emoji: "🇸🇮", + aliases: ["slovenia"], + tags: [], + category: "Flags", + description: "flag: Slovenia", + unicode_version: "6.0", + }, + { + emoji: "🇸🇯", + aliases: ["svalbard_jan_mayen"], + tags: [], + category: "Flags", + description: "flag: Svalbard & Jan Mayen", + unicode_version: "11.0", + }, + { + emoji: "🇸🇰", + aliases: ["slovakia"], + tags: [], + category: "Flags", + description: "flag: Slovakia", + unicode_version: "6.0", + }, + { + emoji: "🇸🇱", + aliases: ["sierra_leone"], + tags: [], + category: "Flags", + description: "flag: Sierra Leone", + unicode_version: "6.0", + }, + { + emoji: "🇸🇲", + aliases: ["san_marino"], + tags: [], + category: "Flags", + description: "flag: San Marino", + unicode_version: "6.0", + }, + { + emoji: "🇸🇳", + aliases: ["senegal"], + tags: [], + category: "Flags", + description: "flag: Senegal", + unicode_version: "6.0", + }, + { + emoji: "🇸🇴", + aliases: ["somalia"], + tags: [], + category: "Flags", + description: "flag: Somalia", + unicode_version: "6.0", + }, + { + emoji: "🇸🇷", + aliases: ["suriname"], + tags: [], + category: "Flags", + description: "flag: Suriname", + unicode_version: "6.0", + }, + { + emoji: "🇸🇸", + aliases: ["south_sudan"], + tags: [], + category: "Flags", + description: "flag: South Sudan", + unicode_version: "6.0", + }, + { + emoji: "🇸🇹", + aliases: ["sao_tome_principe"], + tags: [], + category: "Flags", + description: "flag: São Tomé & Príncipe", + unicode_version: "6.0", + }, + { + emoji: "🇸🇻", + aliases: ["el_salvador"], + tags: [], + category: "Flags", + description: "flag: El Salvador", + unicode_version: "6.0", + }, + { + emoji: "🇸🇽", + aliases: ["sint_maarten"], + tags: [], + category: "Flags", + description: "flag: Sint Maarten", + unicode_version: "6.0", + }, + { + emoji: "🇸🇾", + aliases: ["syria"], + tags: [], + category: "Flags", + description: "flag: Syria", + unicode_version: "6.0", + }, + { + emoji: "🇸🇿", + aliases: ["swaziland"], + tags: [], + category: "Flags", + description: "flag: Eswatini", + unicode_version: "6.0", + }, + { + emoji: "🇹🇦", + aliases: ["tristan_da_cunha"], + tags: [], + category: "Flags", + description: "flag: Tristan da Cunha", + unicode_version: "11.0", + }, + { + emoji: "🇹🇨", + aliases: ["turks_caicos_islands"], + tags: [], + category: "Flags", + description: "flag: Turks & Caicos Islands", + unicode_version: "6.0", + }, + { + emoji: "🇹🇩", + aliases: ["chad"], + tags: [], + category: "Flags", + description: "flag: Chad", + unicode_version: "6.0", + }, + { + emoji: "🇹🇫", + aliases: ["french_southern_territories"], + tags: [], + category: "Flags", + description: "flag: French Southern Territories", + unicode_version: "6.0", + }, + { + emoji: "🇹🇬", + aliases: ["togo"], + tags: [], + category: "Flags", + description: "flag: Togo", + unicode_version: "6.0", + }, + { + emoji: "🇹🇭", + aliases: ["thailand"], + tags: [], + category: "Flags", + description: "flag: Thailand", + unicode_version: "6.0", + }, + { + emoji: "🇹🇯", + aliases: ["tajikistan"], + tags: [], + category: "Flags", + description: "flag: Tajikistan", + unicode_version: "6.0", + }, + { + emoji: "🇹🇰", + aliases: ["tokelau"], + tags: [], + category: "Flags", + description: "flag: Tokelau", + unicode_version: "6.0", + }, + { + emoji: "🇹🇱", + aliases: ["timor_leste"], + tags: [], + category: "Flags", + description: "flag: Timor-Leste", + unicode_version: "6.0", + }, + { + emoji: "🇹🇲", + aliases: ["turkmenistan"], + tags: [], + category: "Flags", + description: "flag: Turkmenistan", + unicode_version: "6.0", + }, + { + emoji: "🇹🇳", + aliases: ["tunisia"], + tags: [], + category: "Flags", + description: "flag: Tunisia", + unicode_version: "6.0", + }, + { + emoji: "🇹🇴", + aliases: ["tonga"], + tags: [], + category: "Flags", + description: "flag: Tonga", + unicode_version: "6.0", + }, + { + emoji: "🇹🇷", + aliases: ["tr"], + tags: ["turkey"], + category: "Flags", + description: "flag: Turkey", + unicode_version: "8.0", + }, + { + emoji: "🇹🇹", + aliases: ["trinidad_tobago"], + tags: [], + category: "Flags", + description: "flag: Trinidad & Tobago", + unicode_version: "6.0", + }, + { + emoji: "🇹🇻", + aliases: ["tuvalu"], + tags: [], + category: "Flags", + description: "flag: Tuvalu", + unicode_version: "6.0", + }, + { + emoji: "🇹🇼", + aliases: ["taiwan"], + tags: [], + category: "Flags", + description: "flag: Taiwan", + unicode_version: "6.0", + }, + { + emoji: "🇹🇿", + aliases: ["tanzania"], + tags: [], + category: "Flags", + description: "flag: Tanzania", + unicode_version: "6.0", + }, + { + emoji: "🇺🇦", + aliases: ["ukraine"], + tags: [], + category: "Flags", + description: "flag: Ukraine", + unicode_version: "6.0", + }, + { + emoji: "🇺🇬", + aliases: ["uganda"], + tags: [], + category: "Flags", + description: "flag: Uganda", + unicode_version: "6.0", + }, + { + emoji: "🇺🇲", + aliases: ["us_outlying_islands"], + tags: [], + category: "Flags", + description: "flag: U.S. Outlying Islands", + unicode_version: "11.0", + }, + { + emoji: "🇺🇳", + aliases: ["united_nations"], + tags: [], + category: "Flags", + description: "flag: United Nations", + unicode_version: "11.0", + }, + { + emoji: "🇺🇸", + aliases: ["us"], + tags: ["flag", "united", "america"], + category: "Flags", + description: "flag: United States", + unicode_version: "6.0", + }, + { + emoji: "🇺🇾", + aliases: ["uruguay"], + tags: [], + category: "Flags", + description: "flag: Uruguay", + unicode_version: "6.0", + }, + { + emoji: "🇺🇿", + aliases: ["uzbekistan"], + tags: [], + category: "Flags", + description: "flag: Uzbekistan", + unicode_version: "6.0", + }, + { + emoji: "🇻🇦", + aliases: ["vatican_city"], + tags: [], + category: "Flags", + description: "flag: Vatican City", + unicode_version: "6.0", + }, + { + emoji: "🇻🇨", + aliases: ["st_vincent_grenadines"], + tags: [], + category: "Flags", + description: "flag: St. Vincent & Grenadines", + unicode_version: "6.0", + }, + { + emoji: "🇻🇪", + aliases: ["venezuela"], + tags: [], + category: "Flags", + description: "flag: Venezuela", + unicode_version: "6.0", + }, + { + emoji: "🇻🇬", + aliases: ["british_virgin_islands"], + tags: [], + category: "Flags", + description: "flag: British Virgin Islands", + unicode_version: "6.0", + }, + { + emoji: "🇻🇮", + aliases: ["us_virgin_islands"], + tags: [], + category: "Flags", + description: "flag: U.S. Virgin Islands", + unicode_version: "6.0", + }, + { + emoji: "🇻🇳", + aliases: ["vietnam"], + tags: [], + category: "Flags", + description: "flag: Vietnam", + unicode_version: "6.0", + }, + { + emoji: "🇻🇺", + aliases: ["vanuatu"], + tags: [], + category: "Flags", + description: "flag: Vanuatu", + unicode_version: "6.0", + }, + { + emoji: "🇼🇫", + aliases: ["wallis_futuna"], + tags: [], + category: "Flags", + description: "flag: Wallis & Futuna", + unicode_version: "6.0", + }, + { + emoji: "🇼🇸", + aliases: ["samoa"], + tags: [], + category: "Flags", + description: "flag: Samoa", + unicode_version: "6.0", + }, + { + emoji: "🇽🇰", + aliases: ["kosovo"], + tags: [], + category: "Flags", + description: "flag: Kosovo", + unicode_version: "6.0", + }, + { + emoji: "🇾🇪", + aliases: ["yemen"], + tags: [], + category: "Flags", + description: "flag: Yemen", + unicode_version: "6.0", + }, + { + emoji: "🇾🇹", + aliases: ["mayotte"], + tags: [], + category: "Flags", + description: "flag: Mayotte", + unicode_version: "6.0", + }, + { + emoji: "🇿🇦", + aliases: ["south_africa"], + tags: [], + category: "Flags", + description: "flag: South Africa", + unicode_version: "6.0", + }, + { + emoji: "🇿🇲", + aliases: ["zambia"], + tags: [], + category: "Flags", + description: "flag: Zambia", + unicode_version: "6.0", + }, + { + emoji: "🇿🇼", + aliases: ["zimbabwe"], + tags: [], + category: "Flags", + description: "flag: Zimbabwe", + unicode_version: "6.0", + }, + { + emoji: "🏴󠁧󠁢󠁥󠁮󠁧󠁿", + aliases: ["england"], + tags: [], + category: "Flags", + description: "flag: England", + unicode_version: "11.0", + }, + { + emoji: "🏴󠁧󠁢󠁳󠁣󠁴󠁿", + aliases: ["scotland"], + tags: [], + category: "Flags", + description: "flag: Scotland", + unicode_version: "11.0", + }, + { + emoji: "🏴󠁧󠁢󠁷󠁬󠁳󠁿", + aliases: ["wales"], + tags: [], + category: "Flags", + description: "flag: Wales", + unicode_version: "11.0", + }, +]; diff --git a/web/src/app/errors.js b/web/src/app/errors.js index 38165a24..96aaf86f 100644 --- a/web/src/app/errors.js +++ b/web/src/app/errors.js @@ -1,66 +1,80 @@ // This is a subset of, and the counterpart to errors.go export const fetchOrThrow = async (url, options) => { - const response = await fetch(url, options); - if (response.status !== 200) { - await throwAppError(response); - } - return response; // Promise! + const response = await fetch(url, options); + if (response.status !== 200) { + await throwAppError(response); + } + return response; // Promise! }; export const throwAppError = async (response) => { - if (response.status === 401 || response.status === 403) { - console.log(`[Error] HTTP ${response.status}`, response); - throw new UnauthorizedError(); + if (response.status === 401 || response.status === 403) { + console.log(`[Error] HTTP ${response.status}`, response); + throw new UnauthorizedError(); + } + const error = await maybeToJson(response); + if (error?.code) { + console.log( + `[Error] HTTP ${response.status}, ntfy error ${error.code}: ${ + error.error || "" + }`, + response + ); + if (error.code === UserExistsError.CODE) { + throw new UserExistsError(); + } else if (error.code === TopicReservedError.CODE) { + throw new TopicReservedError(); + } else if (error.code === AccountCreateLimitReachedError.CODE) { + throw new AccountCreateLimitReachedError(); + } else if (error.code === IncorrectPasswordError.CODE) { + throw new IncorrectPasswordError(); + } else if (error?.error) { + throw new Error(`Error ${error.code}: ${error.error}`); } - const error = await maybeToJson(response); - if (error?.code) { - console.log(`[Error] HTTP ${response.status}, ntfy error ${error.code}: ${error.error || ""}`, response); - if (error.code === UserExistsError.CODE) { - throw new UserExistsError(); - } else if (error.code === TopicReservedError.CODE) { - throw new TopicReservedError(); - } else if (error.code === AccountCreateLimitReachedError.CODE) { - throw new AccountCreateLimitReachedError(); - } else if (error.code === IncorrectPasswordError.CODE) { - throw new IncorrectPasswordError(); - } else if (error?.error) { - throw new Error(`Error ${error.code}: ${error.error}`); - } - } - console.log(`[Error] HTTP ${response.status}, not a ntfy error`, response); - throw new Error(`Unexpected response ${response.status}`); + } + console.log(`[Error] HTTP ${response.status}, not a ntfy error`, response); + throw new Error(`Unexpected response ${response.status}`); }; const maybeToJson = async (response) => { - try { - return await response.json(); - } catch (e) { - return null; - } -} + try { + return await response.json(); + } catch (e) { + return null; + } +}; export class UnauthorizedError extends Error { - constructor() { super("Unauthorized"); } + constructor() { + super("Unauthorized"); + } } export class UserExistsError extends Error { - static CODE = 40901; // errHTTPConflictUserExists - constructor() { super("Username already exists"); } + static CODE = 40901; // errHTTPConflictUserExists + constructor() { + super("Username already exists"); + } } export class TopicReservedError extends Error { - static CODE = 40902; // errHTTPConflictTopicReserved - constructor() { super("Topic already reserved"); } + static CODE = 40902; // errHTTPConflictTopicReserved + constructor() { + super("Topic already reserved"); + } } export class AccountCreateLimitReachedError extends Error { - static CODE = 42906; // errHTTPTooManyRequestsLimitAccountCreation - constructor() { super("Account creation limit reached"); } + static CODE = 42906; // errHTTPTooManyRequestsLimitAccountCreation + constructor() { + super("Account creation limit reached"); + } } export class IncorrectPasswordError extends Error { - static CODE = 40026; // errHTTPBadRequestIncorrectPasswordConfirmation - constructor() { super("Password incorrect"); } + static CODE = 40026; // errHTTPBadRequestIncorrectPasswordConfirmation + constructor() { + super("Password incorrect"); + } } - diff --git a/web/src/app/utils.js b/web/src/app/utils.js index 346df37f..f67c2d4b 100644 --- a/web/src/app/utils.js +++ b/web/src/app/utils.js @@ -1,4 +1,4 @@ -import {rawEmojis} from "./emojis"; +import { rawEmojis } from "./emojis"; import beep from "../sounds/beep.mp3"; import juntos from "../sounds/juntos.mp3"; import pristine from "../sounds/pristine.mp3"; @@ -7,300 +7,316 @@ import dadum from "../sounds/dadum.mp3"; import pop from "../sounds/pop.mp3"; import popSwoosh from "../sounds/pop-swoosh.mp3"; import config from "./config"; -import {Base64} from 'js-base64'; +import { Base64 } from "js-base64"; export const topicUrl = (baseUrl, topic) => `${baseUrl}/${topic}`; -export const topicUrlWs = (baseUrl, topic) => `${topicUrl(baseUrl, topic)}/ws` +export const topicUrlWs = (baseUrl, topic) => + `${topicUrl(baseUrl, topic)}/ws` .replaceAll("https://", "wss://") .replaceAll("http://", "ws://"); -export const topicUrlJson = (baseUrl, topic) => `${topicUrl(baseUrl, topic)}/json`; -export const topicUrlJsonPoll = (baseUrl, topic) => `${topicUrlJson(baseUrl, topic)}?poll=1`; -export const topicUrlJsonPollWithSince = (baseUrl, topic, since) => `${topicUrlJson(baseUrl, topic)}?poll=1&since=${since}`; -export const topicUrlAuth = (baseUrl, topic) => `${topicUrl(baseUrl, topic)}/auth`; -export const topicShortUrl = (baseUrl, topic) => shortUrl(topicUrl(baseUrl, topic)); +export const topicUrlJson = (baseUrl, topic) => + `${topicUrl(baseUrl, topic)}/json`; +export const topicUrlJsonPoll = (baseUrl, topic) => + `${topicUrlJson(baseUrl, topic)}?poll=1`; +export const topicUrlJsonPollWithSince = (baseUrl, topic, since) => + `${topicUrlJson(baseUrl, topic)}?poll=1&since=${since}`; +export const topicUrlAuth = (baseUrl, topic) => + `${topicUrl(baseUrl, topic)}/auth`; +export const topicShortUrl = (baseUrl, topic) => + shortUrl(topicUrl(baseUrl, topic)); export const accountUrl = (baseUrl) => `${baseUrl}/v1/account`; export const accountPasswordUrl = (baseUrl) => `${baseUrl}/v1/account/password`; export const accountTokenUrl = (baseUrl) => `${baseUrl}/v1/account/token`; export const accountSettingsUrl = (baseUrl) => `${baseUrl}/v1/account/settings`; -export const accountSubscriptionUrl = (baseUrl) => `${baseUrl}/v1/account/subscription`; -export const accountReservationUrl = (baseUrl) => `${baseUrl}/v1/account/reservation`; -export const accountReservationSingleUrl = (baseUrl, topic) => `${baseUrl}/v1/account/reservation/${topic}`; -export const accountBillingSubscriptionUrl = (baseUrl) => `${baseUrl}/v1/account/billing/subscription`; -export const accountBillingPortalUrl = (baseUrl) => `${baseUrl}/v1/account/billing/portal`; +export const accountSubscriptionUrl = (baseUrl) => + `${baseUrl}/v1/account/subscription`; +export const accountReservationUrl = (baseUrl) => + `${baseUrl}/v1/account/reservation`; +export const accountReservationSingleUrl = (baseUrl, topic) => + `${baseUrl}/v1/account/reservation/${topic}`; +export const accountBillingSubscriptionUrl = (baseUrl) => + `${baseUrl}/v1/account/billing/subscription`; +export const accountBillingPortalUrl = (baseUrl) => + `${baseUrl}/v1/account/billing/portal`; export const accountPhoneUrl = (baseUrl) => `${baseUrl}/v1/account/phone`; -export const accountPhoneVerifyUrl = (baseUrl) => `${baseUrl}/v1/account/phone/verify`; +export const accountPhoneVerifyUrl = (baseUrl) => + `${baseUrl}/v1/account/phone/verify`; export const tiersUrl = (baseUrl) => `${baseUrl}/v1/tiers`; export const shortUrl = (url) => url.replaceAll(/https?:\/\//g, ""); export const expandUrl = (url) => [`https://${url}`, `http://${url}`]; export const expandSecureUrl = (url) => `https://${url}`; export const validUrl = (url) => { - return url.match(/^https?:\/\/.+/); -} + return url.match(/^https?:\/\/.+/); +}; export const validTopic = (topic) => { - if (disallowedTopic(topic)) { - return false; - } - return topic.match(/^([-_a-zA-Z0-9]{1,64})$/); // Regex must match Go & Android app! -} + if (disallowedTopic(topic)) { + return false; + } + return topic.match(/^([-_a-zA-Z0-9]{1,64})$/); // Regex must match Go & Android app! +}; export const disallowedTopic = (topic) => { - return config.disallowed_topics.includes(topic); -} + return config.disallowed_topics.includes(topic); +}; export const topicDisplayName = (subscription) => { - if (subscription.displayName) { - return subscription.displayName; - } else if (subscription.baseUrl === config.base_url) { - return subscription.topic; - } - return topicShortUrl(subscription.baseUrl, subscription.topic); + if (subscription.displayName) { + return subscription.displayName; + } else if (subscription.baseUrl === config.base_url) { + return subscription.topic; + } + return topicShortUrl(subscription.baseUrl, subscription.topic); }; // Format emojis (see emoji.js) const emojis = {}; -rawEmojis.forEach(emoji => { - emoji.aliases.forEach(alias => { - emojis[alias] = emoji.emoji; - }); +rawEmojis.forEach((emoji) => { + emoji.aliases.forEach((alias) => { + emojis[alias] = emoji.emoji; + }); }); const toEmojis = (tags) => { - if (!tags) return []; - else return tags.filter(tag => tag in emojis).map(tag => emojis[tag]); -} + if (!tags) return []; + else return tags.filter((tag) => tag in emojis).map((tag) => emojis[tag]); +}; export const formatTitleWithDefault = (m, fallback) => { - if (m.title) { - return formatTitle(m); - } - return fallback; + if (m.title) { + return formatTitle(m); + } + return fallback; }; export const formatTitle = (m) => { - const emojiList = toEmojis(m.tags); - if (emojiList.length > 0) { - return `${emojiList.join(" ")} ${m.title}`; - } else { - return m.title; - } + const emojiList = toEmojis(m.tags); + if (emojiList.length > 0) { + return `${emojiList.join(" ")} ${m.title}`; + } else { + return m.title; + } }; export const formatMessage = (m) => { - if (m.title) { - return m.message; + if (m.title) { + return m.message; + } else { + const emojiList = toEmojis(m.tags); + if (emojiList.length > 0) { + return `${emojiList.join(" ")} ${m.message}`; } else { - const emojiList = toEmojis(m.tags); - if (emojiList.length > 0) { - return `${emojiList.join(" ")} ${m.message}`; - } else { - return m.message; - } + return m.message; } + } }; export const unmatchedTags = (tags) => { - if (!tags) return []; - else return tags.filter(tag => !(tag in emojis)); -} + if (!tags) return []; + else return tags.filter((tag) => !(tag in emojis)); +}; export const maybeWithAuth = (headers, user) => { - if (user && user.password) { - return withBasicAuth(headers, user.username, user.password); - } else if (user && user.token) { - return withBearerAuth(headers, user.token); - } - return headers; -} + if (user && user.password) { + return withBasicAuth(headers, user.username, user.password); + } else if (user && user.token) { + return withBearerAuth(headers, user.token); + } + return headers; +}; export const maybeWithBearerAuth = (headers, token) => { - if (token) { - return withBearerAuth(headers, token); - } - return headers; -} + if (token) { + return withBearerAuth(headers, token); + } + return headers; +}; export const withBasicAuth = (headers, username, password) => { - headers['Authorization'] = basicAuth(username, password); - return headers; -} + headers["Authorization"] = basicAuth(username, password); + return headers; +}; export const basicAuth = (username, password) => { - return `Basic ${encodeBase64(`${username}:${password}`)}`; -} + return `Basic ${encodeBase64(`${username}:${password}`)}`; +}; export const withBearerAuth = (headers, token) => { - headers['Authorization'] = bearerAuth(token); - return headers; -} + headers["Authorization"] = bearerAuth(token); + return headers; +}; export const bearerAuth = (token) => { - return `Bearer ${token}`; -} + return `Bearer ${token}`; +}; export const encodeBase64 = (s) => { - return Base64.encode(s); -} + return Base64.encode(s); +}; export const encodeBase64Url = (s) => { - return Base64.encodeURI(s); -} + return Base64.encodeURI(s); +}; export const maybeAppendActionErrors = (message, notification) => { - const actionErrors = (notification.actions ?? []) - .map(action => action.error) - .filter(action => !!action) - .join("\n") - if (actionErrors.length === 0) { - return message; - } else { - return `${message}\n\n${actionErrors}`; - } -} + const actionErrors = (notification.actions ?? []) + .map((action) => action.error) + .filter((action) => !!action) + .join("\n"); + if (actionErrors.length === 0) { + return message; + } else { + return `${message}\n\n${actionErrors}`; + } +}; export const shuffle = (arr) => { - let j, x; - for (let index = arr.length - 1; index > 0; index--) { - j = Math.floor(Math.random() * (index + 1)); - x = arr[index]; - arr[index] = arr[j]; - arr[j] = x; - } - return arr; -} + let j, x; + for (let index = arr.length - 1; index > 0; index--) { + j = Math.floor(Math.random() * (index + 1)); + x = arr[index]; + arr[index] = arr[j]; + arr[j] = x; + } + return arr; +}; export const splitNoEmpty = (s, delimiter) => { - return s - .split(delimiter) - .map(x => x.trim()) - .filter(x => x !== ""); -} + return s + .split(delimiter) + .map((x) => x.trim()) + .filter((x) => x !== ""); +}; /** Non-cryptographic hash function, see https://stackoverflow.com/a/8831937/1440785 */ export const hashCode = async (s) => { - let hash = 0; - for (let i = 0; i < s.length; i++) { - const char = s.charCodeAt(i); - hash = ((hash<<5)-hash)+char; - hash = hash & hash; // Convert to 32bit integer - } - return hash; -} + let hash = 0; + for (let i = 0; i < s.length; i++) { + const char = s.charCodeAt(i); + hash = (hash << 5) - hash + char; + hash = hash & hash; // Convert to 32bit integer + } + return hash; +}; export const formatShortDateTime = (timestamp) => { - return new Intl.DateTimeFormat('default', {dateStyle: 'short', timeStyle: 'short'}) - .format(new Date(timestamp * 1000)); -} + return new Intl.DateTimeFormat("default", { + dateStyle: "short", + timeStyle: "short", + }).format(new Date(timestamp * 1000)); +}; export const formatShortDate = (timestamp) => { - return new Intl.DateTimeFormat('default', {dateStyle: 'short'}) - .format(new Date(timestamp * 1000)); -} + return new Intl.DateTimeFormat("default", { dateStyle: "short" }).format( + new Date(timestamp * 1000) + ); +}; export const formatBytes = (bytes, decimals = 2) => { - if (bytes === 0) return '0 bytes'; - const k = 1024; - const dm = decimals < 0 ? 0 : decimals; - const sizes = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; -} + if (bytes === 0) return "0 bytes"; + const k = 1024; + const dm = decimals < 0 ? 0 : decimals; + const sizes = ["bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i]; +}; export const formatNumber = (n) => { - if (n === 0) { - return n; - } else if (n % 1000 === 0) { - return `${n/1000}k`; - } - return n.toLocaleString(); -} + if (n === 0) { + return n; + } else if (n % 1000 === 0) { + return `${n / 1000}k`; + } + return n.toLocaleString(); +}; export const formatPrice = (n) => { - if (n % 100 === 0) { - return `$${n/100}`; - } - return `$${(n/100).toPrecision(2)}`; -} + if (n % 100 === 0) { + return `$${n / 100}`; + } + return `$${(n / 100).toPrecision(2)}`; +}; export const openUrl = (url) => { - window.open(url, "_blank", "noopener,noreferrer"); + window.open(url, "_blank", "noopener,noreferrer"); }; export const sounds = { - "ding": { - file: ding, - label: "Ding" - }, - "juntos": { - file: juntos, - label: "Juntos" - }, - "pristine": { - file: pristine, - label: "Pristine" - }, - "dadum": { - file: dadum, - label: "Dadum" - }, - "pop": { - file: pop, - label: "Pop" - }, - "pop-swoosh": { - file: popSwoosh, - label: "Pop swoosh" - }, - "beep": { - file: beep, - label: "Beep" - } + ding: { + file: ding, + label: "Ding", + }, + juntos: { + file: juntos, + label: "Juntos", + }, + pristine: { + file: pristine, + label: "Pristine", + }, + dadum: { + file: dadum, + label: "Dadum", + }, + pop: { + file: pop, + label: "Pop", + }, + "pop-swoosh": { + file: popSwoosh, + label: "Pop swoosh", + }, + beep: { + file: beep, + label: "Beep", + }, }; export const playSound = async (id) => { - const audio = new Audio(sounds[id].file); - return audio.play(); + const audio = new Audio(sounds[id].file); + return audio.play(); }; // From: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch export async function* fetchLinesIterator(fileURL, headers) { - const utf8Decoder = new TextDecoder('utf-8'); - const response = await fetch(fileURL, { - headers: headers - }); - const reader = response.body.getReader(); - let { value: chunk, done: readerDone } = await reader.read(); - chunk = chunk ? utf8Decoder.decode(chunk) : ''; + const utf8Decoder = new TextDecoder("utf-8"); + const response = await fetch(fileURL, { + headers: headers, + }); + const reader = response.body.getReader(); + let { value: chunk, done: readerDone } = await reader.read(); + chunk = chunk ? utf8Decoder.decode(chunk) : ""; - const re = /\n|\r|\r\n/gm; - let startIndex = 0; + const re = /\n|\r|\r\n/gm; + let startIndex = 0; - for (;;) { - let result = re.exec(chunk); - if (!result) { - if (readerDone) { - break; - } - let remainder = chunk.substr(startIndex); - ({ value: chunk, done: readerDone } = await reader.read()); - chunk = remainder + (chunk ? utf8Decoder.decode(chunk) : ''); - startIndex = re.lastIndex = 0; - continue; - } - yield chunk.substring(startIndex, result.index); - startIndex = re.lastIndex; - } - if (startIndex < chunk.length) { - yield chunk.substr(startIndex); // last line didn't end in a newline char + for (;;) { + let result = re.exec(chunk); + if (!result) { + if (readerDone) { + break; + } + let remainder = chunk.substr(startIndex); + ({ value: chunk, done: readerDone } = await reader.read()); + chunk = remainder + (chunk ? utf8Decoder.decode(chunk) : ""); + startIndex = re.lastIndex = 0; + continue; } + yield chunk.substring(startIndex, result.index); + startIndex = re.lastIndex; + } + if (startIndex < chunk.length) { + yield chunk.substr(startIndex); // last line didn't end in a newline char + } } export const randomAlphanumericString = (len) => { - const alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; - let id = ""; - for (let i = 0; i < len; i++) { - id += alphabet[(Math.random() * alphabet.length) | 0]; - } - return id; -} + const alphabet = + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + let id = ""; + for (let i = 0; i < len; i++) { + id += alphabet[(Math.random() * alphabet.length) | 0]; + } + return id; +}; diff --git a/web/src/components/Account.js b/web/src/components/Account.js index 710510d2..bb8e7a74 100644 --- a/web/src/components/Account.js +++ b/web/src/components/Account.js @@ -1,36 +1,36 @@ -import * as React from 'react'; -import {useContext, useState} from 'react'; +import * as React from "react"; +import { useContext, useState } from "react"; import { - Alert, - CardActions, - CardContent, - Chip, - FormControl, - FormControlLabel, - LinearProgress, - Link, - Portal, - Radio, - RadioGroup, - Select, - Snackbar, - Stack, - Table, - TableBody, - TableCell, - TableHead, - TableRow, - useMediaQuery + Alert, + CardActions, + CardContent, + Chip, + FormControl, + FormControlLabel, + LinearProgress, + Link, + Portal, + Radio, + RadioGroup, + Select, + Snackbar, + Stack, + Table, + TableBody, + TableCell, + TableHead, + TableRow, + useMediaQuery, } from "@mui/material"; -import Tooltip from '@mui/material/Tooltip'; +import Tooltip from "@mui/material/Tooltip"; import Typography from "@mui/material/Typography"; -import EditIcon from '@mui/icons-material/Edit'; +import EditIcon from "@mui/icons-material/Edit"; import Container from "@mui/material/Container"; import Card from "@mui/material/Card"; import Button from "@mui/material/Button"; -import {Trans, useTranslation} from "react-i18next"; +import { Trans, useTranslation } from "react-i18next"; import session from "../app/Session"; -import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline'; +import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline"; import theme from "./theme"; import Dialog from "@mui/material/Dialog"; import DialogTitle from "@mui/material/DialogTitle"; @@ -38,997 +38,1342 @@ import DialogContent from "@mui/material/DialogContent"; import TextField from "@mui/material/TextField"; import routes from "./routes"; import IconButton from "@mui/material/IconButton"; -import {formatBytes, formatShortDate, formatShortDateTime, openUrl} from "../app/utils"; -import accountApi, {LimitBasis, Role, SubscriptionInterval, SubscriptionStatus} from "../app/AccountApi"; -import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; -import {Pref, PrefGroup} from "./Pref"; +import { + formatBytes, + formatShortDate, + formatShortDateTime, + openUrl, +} from "../app/utils"; +import accountApi, { + LimitBasis, + Role, + SubscriptionInterval, + SubscriptionStatus, +} from "../app/AccountApi"; +import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined"; +import { Pref, PrefGroup } from "./Pref"; import db from "../app/db"; import i18n from "i18next"; import humanizeDuration from "humanize-duration"; import UpgradeDialog from "./UpgradeDialog"; import CelebrationIcon from "@mui/icons-material/Celebration"; -import {AccountContext} from "./App"; +import { AccountContext } from "./App"; import DialogFooter from "./DialogFooter"; -import {Paragraph} from "./styles"; +import { Paragraph } from "./styles"; import CloseIcon from "@mui/icons-material/Close"; -import {ContentCopy, Public} from "@mui/icons-material"; +import { ContentCopy, Public } from "@mui/icons-material"; import MenuItem from "@mui/material/MenuItem"; import DialogContentText from "@mui/material/DialogContentText"; -import {IncorrectPasswordError, UnauthorizedError} from "../app/errors"; -import {ProChip} from "./SubscriptionPopup"; +import { IncorrectPasswordError, UnauthorizedError } from "../app/errors"; +import { ProChip } from "./SubscriptionPopup"; import AddIcon from "@mui/icons-material/Add"; const Account = () => { - if (!session.exists()) { - window.location.href = routes.app; - return <>; - } - return ( - - - - - - - - - ); + if (!session.exists()) { + window.location.href = routes.app; + return <>; + } + return ( + + + + + + + + + ); }; const Basics = () => { - const { t } = useTranslation(); - return ( - - - {t("account_basics_title")} - - - - - - - - - ); + const { t } = useTranslation(); + return ( + + + {t("account_basics_title")} + + + + + + + + + ); }; const Username = () => { - const { t } = useTranslation(); - const { account } = useContext(AccountContext); - const labelId = "prefUsername"; + const { t } = useTranslation(); + const { account } = useContext(AccountContext); + const labelId = "prefUsername"; - return ( - -
- {session.username()} - {account?.role === Role.ADMIN - ? <>{" "}👑 - : ""} -
-
- ) + return ( + +
+ {session.username()} + {account?.role === Role.ADMIN ? ( + <> + {" "} + + 👑 + + + ) : ( + "" + )} +
+
+ ); }; const ChangePassword = () => { - const { t } = useTranslation(); - const [dialogKey, setDialogKey] = useState(0); - const [dialogOpen, setDialogOpen] = useState(false); - const labelId = "prefChangePassword"; + const { t } = useTranslation(); + const [dialogKey, setDialogKey] = useState(0); + const [dialogOpen, setDialogOpen] = useState(false); + const labelId = "prefChangePassword"; - const handleDialogOpen = () => { - setDialogKey(prev => prev+1); - setDialogOpen(true); - }; + const handleDialogOpen = () => { + setDialogKey((prev) => prev + 1); + setDialogOpen(true); + }; - const handleDialogClose = () => { - setDialogOpen(false); - }; + const handleDialogClose = () => { + setDialogOpen(false); + }; - return ( - -
- ⬤⬤⬤⬤⬤⬤⬤⬤⬤⬤ - - - -
- -
- ) + return ( + +
+ + ⬤⬤⬤⬤⬤⬤⬤⬤⬤⬤ + + + + +
+ +
+ ); }; const ChangePasswordDialog = (props) => { - const { t } = useTranslation(); - const [error, setError] = useState(""); - const [currentPassword, setCurrentPassword] = useState(""); - const [newPassword, setNewPassword] = useState(""); - const [confirmPassword, setConfirmPassword] = useState(""); - const fullScreen = useMediaQuery(theme.breakpoints.down('sm')); + const { t } = useTranslation(); + const [error, setError] = useState(""); + const [currentPassword, setCurrentPassword] = useState(""); + const [newPassword, setNewPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + const fullScreen = useMediaQuery(theme.breakpoints.down("sm")); - const handleDialogSubmit = async () => { - try { - console.debug(`[Account] Changing password`); - await accountApi.changePassword(currentPassword, newPassword); - props.onClose(); - } catch (e) { - console.log(`[Account] Error changing password`, e); - if (e instanceof IncorrectPasswordError) { - setError(t("account_basics_password_dialog_current_password_incorrect")); - } else if (e instanceof UnauthorizedError) { - session.resetAndRedirect(routes.login); - } else { - setError(e.message); - } - } - }; + const handleDialogSubmit = async () => { + try { + console.debug(`[Account] Changing password`); + await accountApi.changePassword(currentPassword, newPassword); + props.onClose(); + } catch (e) { + console.log(`[Account] Error changing password`, e); + if (e instanceof IncorrectPasswordError) { + setError( + t("account_basics_password_dialog_current_password_incorrect") + ); + } else if (e instanceof UnauthorizedError) { + session.resetAndRedirect(routes.login); + } else { + setError(e.message); + } + } + }; - return ( - - {t("account_basics_password_dialog_title")} - - setCurrentPassword(ev.target.value)} - fullWidth - variant="standard" - /> - setNewPassword(ev.target.value)} - fullWidth - variant="standard" - /> - setConfirmPassword(ev.target.value)} - fullWidth - variant="standard" - /> - - - - - - - ); + return ( + + {t("account_basics_password_dialog_title")} + + setCurrentPassword(ev.target.value)} + fullWidth + variant="standard" + /> + setNewPassword(ev.target.value)} + fullWidth + variant="standard" + /> + setConfirmPassword(ev.target.value)} + fullWidth + variant="standard" + /> + + + + + + + ); }; const AccountType = () => { - const { t } = useTranslation(); - const { account } = useContext(AccountContext); - const [upgradeDialogKey, setUpgradeDialogKey] = useState(0); - const [upgradeDialogOpen, setUpgradeDialogOpen] = useState(false); - const [showPortalError, setShowPortalError] = useState(false); + const { t } = useTranslation(); + const { account } = useContext(AccountContext); + const [upgradeDialogKey, setUpgradeDialogKey] = useState(0); + const [upgradeDialogOpen, setUpgradeDialogOpen] = useState(false); + const [showPortalError, setShowPortalError] = useState(false); - if (!account) { - return <>; + if (!account) { + return <>; + } + + const handleUpgradeClick = () => { + setUpgradeDialogKey((k) => k + 1); + setUpgradeDialogOpen(true); + }; + + const handleManageBilling = async () => { + try { + const response = await accountApi.createBillingPortalSession(); + window.open(response.redirect_url, "billing_portal"); + } catch (e) { + console.log(`[Account] Error opening billing portal`, e); + if (e instanceof UnauthorizedError) { + session.resetAndRedirect(routes.login); + } else { + setShowPortalError(true); + } } + }; - const handleUpgradeClick = () => { - setUpgradeDialogKey(k => k + 1); - setUpgradeDialogOpen(true); + let accountType; + if (account.role === Role.ADMIN) { + const tierSuffix = account.tier + ? t("account_basics_tier_admin_suffix_with_tier", { + tier: account.tier.name, + }) + : t("account_basics_tier_admin_suffix_no_tier"); + accountType = `${t("account_basics_tier_admin")} ${tierSuffix}`; + } else if (!account.tier) { + accountType = config.enable_payments + ? t("account_basics_tier_free") + : t("account_basics_tier_basic"); + } else { + accountType = account.tier.name; + if (account.billing?.interval === SubscriptionInterval.MONTH) { + accountType += ` (${t("account_basics_tier_interval_monthly")})`; + } else if (account.billing?.interval === SubscriptionInterval.YEAR) { + accountType += ` (${t("account_basics_tier_interval_yearly")})`; } + } - const handleManageBilling = async () => { - try { - const response = await accountApi.createBillingPortalSession(); - window.open(response.redirect_url, "billing_portal"); - } catch (e) { - console.log(`[Account] Error opening billing portal`, e); - if (e instanceof UnauthorizedError) { - session.resetAndRedirect(routes.login); - } else { - setShowPortalError(true); - } - } - }; - - let accountType; - if (account.role === Role.ADMIN) { - const tierSuffix = (account.tier) ? t("account_basics_tier_admin_suffix_with_tier", { tier: account.tier.name }) : t("account_basics_tier_admin_suffix_no_tier"); - accountType = `${t("account_basics_tier_admin")} ${tierSuffix}`; - } else if (!account.tier) { - accountType = (config.enable_payments) ? t("account_basics_tier_free") : t("account_basics_tier_basic"); - } else { - accountType = account.tier.name; - if (account.billing?.interval === SubscriptionInterval.MONTH) { - accountType += ` (${t("account_basics_tier_interval_monthly")})`; - } else if (account.billing?.interval === SubscriptionInterval.YEAR) { - accountType += ` (${t("account_basics_tier_interval_yearly")})`; - } - } - - return ( - 0} - title={t("account_basics_tier_title")} - description={t("account_basics_tier_description")} - > -
- {accountType} - {account.billing?.paid_until && !account.billing?.cancel_at && - - - - } - {config.enable_payments && account.role === Role.USER && !account.billing?.subscription && - - } - {config.enable_payments && account.role === Role.USER && account.billing?.subscription && - - } - {config.enable_payments && account.role === Role.USER && account.billing?.customer && - - } - {config.enable_payments && - setUpgradeDialogOpen(false)} - /> - } -
- {account.billing?.status === SubscriptionStatus.PAST_DUE && - {t("account_basics_tier_payment_overdue")} - } - {account.billing?.cancel_at > 0 && - {t("account_basics_tier_canceled_subscription", { date: formatShortDate(account.billing.cancel_at) })} - } - - setShowPortalError(false)} - message={t("account_usage_cannot_create_portal_session")} - /> - -
- ) + return ( + 0 + } + title={t("account_basics_tier_title")} + description={t("account_basics_tier_description")} + > +
+ {accountType} + {account.billing?.paid_until && !account.billing?.cancel_at && ( + + + + + + )} + {config.enable_payments && + account.role === Role.USER && + !account.billing?.subscription && ( + + )} + {config.enable_payments && + account.role === Role.USER && + account.billing?.subscription && ( + + )} + {config.enable_payments && + account.role === Role.USER && + account.billing?.customer && ( + + )} + {config.enable_payments && ( + setUpgradeDialogOpen(false)} + /> + )} +
+ {account.billing?.status === SubscriptionStatus.PAST_DUE && ( + + {t("account_basics_tier_payment_overdue")} + + )} + {account.billing?.cancel_at > 0 && ( + + {t("account_basics_tier_canceled_subscription", { + date: formatShortDate(account.billing.cancel_at), + })} + + )} + + setShowPortalError(false)} + message={t("account_usage_cannot_create_portal_session")} + /> + +
+ ); }; const PhoneNumbers = () => { - const { t } = useTranslation(); - const { account } = useContext(AccountContext); - const [dialogKey, setDialogKey] = useState(0); - const [dialogOpen, setDialogOpen] = useState(false); - const [snackOpen, setSnackOpen] = useState(false); - const labelId = "prefPhoneNumbers"; + const { t } = useTranslation(); + const { account } = useContext(AccountContext); + const [dialogKey, setDialogKey] = useState(0); + const [dialogOpen, setDialogOpen] = useState(false); + const [snackOpen, setSnackOpen] = useState(false); + const labelId = "prefPhoneNumbers"; - const handleDialogOpen = () => { - setDialogKey(prev => prev+1); - setDialogOpen(true); - }; + const handleDialogOpen = () => { + setDialogKey((prev) => prev + 1); + setDialogOpen(true); + }; - const handleDialogClose = () => { - setDialogOpen(false); - }; + const handleDialogClose = () => { + setDialogOpen(false); + }; - const handleCopy = (phoneNumber) => { - navigator.clipboard.writeText(phoneNumber); - setSnackOpen(true); - }; + const handleCopy = (phoneNumber) => { + navigator.clipboard.writeText(phoneNumber); + setSnackOpen(true); + }; - const handleDelete = async (phoneNumber) => { - try { - await accountApi.deletePhoneNumber(phoneNumber); - } catch (e) { - console.log(`[Account] Error deleting phone number`, e); - if (e instanceof UnauthorizedError) { - session.resetAndRedirect(routes.login); - } - } - }; - - if (!config.enable_calls) { - return null; + const handleDelete = async (phoneNumber) => { + try { + await accountApi.deletePhoneNumber(phoneNumber); + } catch (e) { + console.log(`[Account] Error deleting phone number`, e); + if (e instanceof UnauthorizedError) { + session.resetAndRedirect(routes.login); + } } + }; - if (account?.limits.calls === 0) { - return ( - {t("account_basics_phone_numbers_title")}{config.enable_payments && }} description={t("account_basics_phone_numbers_description")}> - {t("account_usage_calls_none")} - - ) - } + if (!config.enable_calls) { + return null; + } + if (account?.limits.calls === 0) { return ( - -
- {account?.phone_numbers?.map(phoneNumber => - - {phoneNumber} - - } - variant="outlined" - onClick={() => handleCopy(phoneNumber)} - onDelete={() => handleDelete(phoneNumber)} - /> - )} - {!account?.phone_numbers && - {t("account_basics_phone_numbers_no_phone_numbers_yet")} - } - -
- - - setSnackOpen(false)} - message={t("account_basics_phone_numbers_copied_to_clipboard")} - /> - -
- ) + + {t("account_basics_phone_numbers_title")} + {config.enable_payments && } + + } + description={t("account_basics_phone_numbers_description")} + > + {t("account_usage_calls_none")} + + ); + } + + return ( + +
+ {account?.phone_numbers?.map((phoneNumber) => ( + + {phoneNumber} + + } + variant="outlined" + onClick={() => handleCopy(phoneNumber)} + onDelete={() => handleDelete(phoneNumber)} + /> + ))} + {!account?.phone_numbers && ( + {t("account_basics_phone_numbers_no_phone_numbers_yet")} + )} + + + +
+ + + setSnackOpen(false)} + message={t("account_basics_phone_numbers_copied_to_clipboard")} + /> + +
+ ); }; const AddPhoneNumberDialog = (props) => { - const { t } = useTranslation(); - const [error, setError] = useState(""); - const [phoneNumber, setPhoneNumber] = useState(""); - const [channel, setChannel] = useState("sms"); - const [code, setCode] = useState(""); - const [sending, setSending] = useState(false); - const [verificationCodeSent, setVerificationCodeSent] = useState(false); - const fullScreen = useMediaQuery(theme.breakpoints.down('sm')); + const { t } = useTranslation(); + const [error, setError] = useState(""); + const [phoneNumber, setPhoneNumber] = useState(""); + const [channel, setChannel] = useState("sms"); + const [code, setCode] = useState(""); + const [sending, setSending] = useState(false); + const [verificationCodeSent, setVerificationCodeSent] = useState(false); + const fullScreen = useMediaQuery(theme.breakpoints.down("sm")); - const handleDialogSubmit = async () => { - if (!verificationCodeSent) { - await verifyPhone(); - } else { - await checkVerifyPhone(); - } - }; + const handleDialogSubmit = async () => { + if (!verificationCodeSent) { + await verifyPhone(); + } else { + await checkVerifyPhone(); + } + }; - const handleCancel = () => { - if (verificationCodeSent) { - setVerificationCodeSent(false); - setCode(""); - } else { - props.onClose(); - } - }; + const handleCancel = () => { + if (verificationCodeSent) { + setVerificationCodeSent(false); + setCode(""); + } else { + props.onClose(); + } + }; - const verifyPhone = async () => { - try { - setSending(true); - await accountApi.verifyPhoneNumber(phoneNumber, channel); - setVerificationCodeSent(true); - } catch (e) { - console.log(`[Account] Error sending verification`, e); - if (e instanceof UnauthorizedError) { - session.resetAndRedirect(routes.login); - } else { - setError(e.message); - } - } finally { - setSending(false); - } - }; + const verifyPhone = async () => { + try { + setSending(true); + await accountApi.verifyPhoneNumber(phoneNumber, channel); + setVerificationCodeSent(true); + } catch (e) { + console.log(`[Account] Error sending verification`, e); + if (e instanceof UnauthorizedError) { + session.resetAndRedirect(routes.login); + } else { + setError(e.message); + } + } finally { + setSending(false); + } + }; - const checkVerifyPhone = async () => { - try { - setSending(true); - await accountApi.addPhoneNumber(phoneNumber, code); - props.onClose(); - } catch (e) { - console.log(`[Account] Error confirming verification`, e); - if (e instanceof UnauthorizedError) { - session.resetAndRedirect(routes.login); - } else { - setError(e.message); - } - } finally { - setSending(false); - } - }; + const checkVerifyPhone = async () => { + try { + setSending(true); + await accountApi.addPhoneNumber(phoneNumber, code); + props.onClose(); + } catch (e) { + console.log(`[Account] Error confirming verification`, e); + if (e instanceof UnauthorizedError) { + session.resetAndRedirect(routes.login); + } else { + setError(e.message); + } + } finally { + setSending(false); + } + }; - return ( - - {t("account_basics_phone_numbers_dialog_title")} - - - {t("account_basics_phone_numbers_dialog_description")} - - {!verificationCodeSent && -
- setPhoneNumber(ev.target.value)} - inputProps={{ inputMode: 'tel', pattern: '\+[0-9]*' }} - variant="standard" - sx={{ flexGrow: 1 }} - /> - - - setChannel(e.target.value)} />} label={t("account_basics_phone_numbers_dialog_channel_sms")} /> - setChannel(e.target.value)} />} label={t("account_basics_phone_numbers_dialog_channel_call")} sx={{ marginRight: 0 }} /> - - -
- } - {verificationCodeSent && - setCode(ev.target.value)} - fullWidth - inputProps={{ inputMode: 'numeric', pattern: '[0-9]*' }} - variant="standard" + return ( + + + {t("account_basics_phone_numbers_dialog_title")} + + + + {t("account_basics_phone_numbers_dialog_description")} + + {!verificationCodeSent && ( +
+ setPhoneNumber(ev.target.value)} + inputProps={{ inputMode: "tel", pattern: "+[0-9]*" }} + variant="standard" + sx={{ flexGrow: 1 }} + /> + + + setChannel(e.target.value)} /> - } - - - - - -
- ); + } + label={t("account_basics_phone_numbers_dialog_channel_sms")} + /> + setChannel(e.target.value)} + /> + } + label={t("account_basics_phone_numbers_dialog_channel_call")} + sx={{ marginRight: 0 }} + /> + + + + )} + {verificationCodeSent && ( + setCode(ev.target.value)} + fullWidth + inputProps={{ inputMode: "numeric", pattern: "[0-9]*" }} + variant="standard" + /> + )} +
+ + + + +
+ ); }; - const Stats = () => { - const { t } = useTranslation(); - const { account } = useContext(AccountContext); + const { t } = useTranslation(); + const { account } = useContext(AccountContext); - if (!account) { - return <>; - } + if (!account) { + return <>; + } - const normalize = (value, max) => { - return Math.min(value / max * 100, 100); - }; + const normalize = (value, max) => { + return Math.min((value / max) * 100, 100); + }; - return ( - - - {t("account_usage_title")} + return ( + + + {t("account_usage_title")} + + + {(account.role === Role.ADMIN || account.limits.reservations > 0) && ( + +
+ + {account.stats.reservations.toLocaleString()} + + + {account.role === Role.USER + ? t("account_usage_of_limit", { + limit: account.limits.reservations.toLocaleString(), + }) + : t("account_usage_unlimited")} + +
+ 0 + ? normalize( + account.stats.reservations, + account.limits.reservations + ) + : 100 + } + /> +
+ )} + + {t("account_usage_messages_title")} + + + + + + + } + > +
+ + {account.stats.messages.toLocaleString()} - - {(account.role === Role.ADMIN || account.limits.reservations > 0) && - -
- {account.stats.reservations.toLocaleString()} - {account.role === Role.USER ? t("account_usage_of_limit", { limit: account.limits.reservations.toLocaleString() }) : t("account_usage_unlimited")} -
- 0 ? normalize(account.stats.reservations, account.limits.reservations) : 100} - /> -
- } - - {t("account_usage_messages_title")} - - - }> -
- {account.stats.messages.toLocaleString()} - {account.role === Role.USER ? t("account_usage_of_limit", { limit: account.limits.messages.toLocaleString() }) : t("account_usage_unlimited")} -
- -
- {config.enable_emails && - - {t("account_usage_emails_title")} - - - }> -
- {account.stats.emails.toLocaleString()} - {account.role === Role.USER ? t("account_usage_of_limit", { limit: account.limits.emails.toLocaleString() }) : t("account_usage_unlimited")} -
- -
- } - {config.enable_calls && (account.role === Role.ADMIN || account.limits.calls > 0) && - - {t("account_usage_calls_title")} - - - }> -
- {account.stats.calls.toLocaleString()} - {account.role === Role.USER ? t("account_usage_of_limit", { limit: account.limits.calls.toLocaleString() }) : t("account_usage_unlimited")} -
- 0 ? normalize(account.stats.calls, account.limits.calls) : 100} - /> -
- } - -
- {formatBytes(account.stats.attachment_total_size)} - {account.role === Role.USER ? t("account_usage_of_limit", { limit: formatBytes(account.limits.attachment_total_size) }) : t("account_usage_unlimited")} -
- -
- {config.enable_reservations && account.role === Role.USER && account.limits.reservations === 0 && - {t("account_usage_reservations_title")}{config.enable_payments && }}> - {t("account_usage_reservations_none")} - - } - {config.enable_calls && account.role === Role.USER && account.limits.calls === 0 && - {t("account_usage_calls_title")}{config.enable_payments && }}> - {t("account_usage_calls_none")} - - } -
- {account.role === Role.USER && account.limits.basis === LimitBasis.IP && - - {t("account_usage_basis_ip_description")} - + + {account.role === Role.USER + ? t("account_usage_of_limit", { + limit: account.limits.messages.toLocaleString(), + }) + : t("account_usage_unlimited")} + +
+ - ); + /> +
+ {config.enable_emails && ( + + {t("account_usage_emails_title")} + + + + + + + } + > +
+ + {account.stats.emails.toLocaleString()} + + + {account.role === Role.USER + ? t("account_usage_of_limit", { + limit: account.limits.emails.toLocaleString(), + }) + : t("account_usage_unlimited")} + +
+ +
+ )} + {config.enable_calls && + (account.role === Role.ADMIN || account.limits.calls > 0) && ( + + {t("account_usage_calls_title")} + + + + + + + } + > +
+ + {account.stats.calls.toLocaleString()} + + + {account.role === Role.USER + ? t("account_usage_of_limit", { + limit: account.limits.calls.toLocaleString(), + }) + : t("account_usage_unlimited")} + +
+ 0 + ? normalize(account.stats.calls, account.limits.calls) + : 100 + } + /> +
+ )} + +
+ + {formatBytes(account.stats.attachment_total_size)} + + + {account.role === Role.USER + ? t("account_usage_of_limit", { + limit: formatBytes(account.limits.attachment_total_size), + }) + : t("account_usage_unlimited")} + +
+ +
+ {config.enable_reservations && + account.role === Role.USER && + account.limits.reservations === 0 && ( + + {t("account_usage_reservations_title")} + {config.enable_payments && } + + } + > + {t("account_usage_reservations_none")} + + )} + {config.enable_calls && + account.role === Role.USER && + account.limits.calls === 0 && ( + + {t("account_usage_calls_title")} + {config.enable_payments && } + + } + > + {t("account_usage_calls_none")} + + )} +
+ {account.role === Role.USER && account.limits.basis === LimitBasis.IP && ( + + {t("account_usage_basis_ip_description")} + + )} +
+ ); }; const InfoIcon = () => { - return ( - - ); -} - + return ( + + ); +}; const Tokens = () => { - const { t } = useTranslation(); - const { account } = useContext(AccountContext); - const [dialogKey, setDialogKey] = useState(0); - const [dialogOpen, setDialogOpen] = useState(false); - const tokens = account?.tokens || []; + const { t } = useTranslation(); + const { account } = useContext(AccountContext); + const [dialogKey, setDialogKey] = useState(0); + const [dialogOpen, setDialogOpen] = useState(false); + const tokens = account?.tokens || []; - const handleCreateClick = () => { - setDialogKey(prev => prev+1); - setDialogOpen(true); - }; + const handleCreateClick = () => { + setDialogKey((prev) => prev + 1); + setDialogOpen(true); + }; - const handleDialogClose = () => { - setDialogOpen(false); - }; + const handleDialogClose = () => { + setDialogOpen(false); + }; - const handleDialogSubmit = async (user) => { - setDialogOpen(false); - // - }; - return ( - - - - {t("account_tokens_title")} - - - - }} - /> - - {tokens?.length > 0 && } - - - - - - - ); + const handleDialogSubmit = async (user) => { + setDialogOpen(false); + // + }; + return ( + + + + {t("account_tokens_title")} + + + , + }} + /> + + {tokens?.length > 0 && } + + + + + + + ); }; const TokensTable = (props) => { - const { t } = useTranslation(); - const [snackOpen, setSnackOpen] = useState(false); - const [upsertDialogKey, setUpsertDialogKey] = useState(0); - const [upsertDialogOpen, setUpsertDialogOpen] = useState(false); - const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); - const [selectedToken, setSelectedToken] = useState(null); + const { t } = useTranslation(); + const [snackOpen, setSnackOpen] = useState(false); + const [upsertDialogKey, setUpsertDialogKey] = useState(0); + const [upsertDialogOpen, setUpsertDialogOpen] = useState(false); + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [selectedToken, setSelectedToken] = useState(null); - const tokens = (props.tokens || []) - .sort( (a, b) => { - if (a.token === session.token()) { - return -1; - } else if (b.token === session.token()) { - return 1; - } - return a.token.localeCompare(b.token); - }); + const tokens = (props.tokens || []).sort((a, b) => { + if (a.token === session.token()) { + return -1; + } else if (b.token === session.token()) { + return 1; + } + return a.token.localeCompare(b.token); + }); - const handleEditClick = (token) => { - setUpsertDialogKey(prev => prev+1); - setSelectedToken(token); - setUpsertDialogOpen(true); - }; + const handleEditClick = (token) => { + setUpsertDialogKey((prev) => prev + 1); + setSelectedToken(token); + setUpsertDialogOpen(true); + }; - const handleDialogClose = () => { - setUpsertDialogOpen(false); - setDeleteDialogOpen(false); - setSelectedToken(null); - }; + const handleDialogClose = () => { + setUpsertDialogOpen(false); + setDeleteDialogOpen(false); + setSelectedToken(null); + }; - const handleDeleteClick = async (token) => { - setSelectedToken(token); - setDeleteDialogOpen(true); - }; + const handleDeleteClick = async (token) => { + setSelectedToken(token); + setDeleteDialogOpen(true); + }; - const handleCopy = async (token) => { - await navigator.clipboard.writeText(token); - setSnackOpen(true); - }; + const handleCopy = async (token) => { + await navigator.clipboard.writeText(token); + setSnackOpen(true); + }; - return ( - - - - {t("account_tokens_table_token_header")} - {t("account_tokens_table_label_header")} - {t("account_tokens_table_expires_header")} - {t("account_tokens_table_last_access_header")} - - - - - {tokens.map(token => ( - - - - {token.token.slice(0, 12)} - ... - - handleCopy(token.token)}> - - - - - {token.token === session.token() && {t("account_tokens_table_current_session")}} - {token.token !== session.token() && (token.label || "-")} - - - {token.expires ? formatShortDateTime(token.expires) : {t("account_tokens_table_never_expires")}} - - -
- {formatShortDateTime(token.last_access)} - - openUrl(`https://whatismyipaddress.com/ip/${token.last_origin}`)}> - - - -
-
- - {token.token !== session.token() && - <> - handleEditClick(token)} aria-label={t("account_tokens_dialog_title_edit")}> - - - handleDeleteClick(token)} aria-label={t("account_tokens_dialog_title_delete")}> - - - - } - {token.token === session.token() && - - - - - - - } - -
- ))} -
- - setSnackOpen(false)} - message={t("account_tokens_table_copied_to_clipboard")} - /> - - - -
- ); + return ( + + + + + {t("account_tokens_table_token_header")} + + {t("account_tokens_table_label_header")} + {t("account_tokens_table_expires_header")} + {t("account_tokens_table_last_access_header")} + + + + + {tokens.map((token) => ( + + + + + {token.token.slice(0, 12)} + + ... + + handleCopy(token.token)}> + + + + + + + {token.token === session.token() && ( + {t("account_tokens_table_current_session")} + )} + {token.token !== session.token() && (token.label || "-")} + + + {token.expires ? ( + formatShortDateTime(token.expires) + ) : ( + {t("account_tokens_table_never_expires")} + )} + + +
+ {formatShortDateTime(token.last_access)} + + + openUrl( + `https://whatismyipaddress.com/ip/${token.last_origin}` + ) + } + > + + + +
+
+ + {token.token !== session.token() && ( + <> + handleEditClick(token)} + aria-label={t("account_tokens_dialog_title_edit")} + > + + + handleDeleteClick(token)} + aria-label={t("account_tokens_dialog_title_delete")} + > + + + + )} + {token.token === session.token() && ( + + + + + + + + + + + )} + +
+ ))} +
+ + setSnackOpen(false)} + message={t("account_tokens_table_copied_to_clipboard")} + /> + + + +
+ ); }; const TokenDialog = (props) => { - const { t } = useTranslation(); - const [error, setError] = useState(""); - const [label, setLabel] = useState(props.token?.label || ""); - const [expires, setExpires] = useState(props.token ? -1 : 0); - const fullScreen = useMediaQuery(theme.breakpoints.down('sm')); - const editMode = !!props.token; + const { t } = useTranslation(); + const [error, setError] = useState(""); + const [label, setLabel] = useState(props.token?.label || ""); + const [expires, setExpires] = useState(props.token ? -1 : 0); + const fullScreen = useMediaQuery(theme.breakpoints.down("sm")); + const editMode = !!props.token; - const handleSubmit = async () => { - try { - if (editMode) { - await accountApi.updateToken(props.token.token, label, expires); - } else { - await accountApi.createToken(label, expires); - } - props.onClose(); - } catch (e) { - console.log(`[Account] Error creating token`, e); - if (e instanceof UnauthorizedError) { - session.resetAndRedirect(routes.login); - } else { - setError(e.message); - } - } - }; + const handleSubmit = async () => { + try { + if (editMode) { + await accountApi.updateToken(props.token.token, label, expires); + } else { + await accountApi.createToken(label, expires); + } + props.onClose(); + } catch (e) { + console.log(`[Account] Error creating token`, e); + if (e instanceof UnauthorizedError) { + session.resetAndRedirect(routes.login); + } else { + setError(e.message); + } + } + }; - return ( - - {editMode ? t("account_tokens_dialog_title_edit") : t("account_tokens_dialog_title_create")} - - setLabel(ev.target.value)} - fullWidth - variant="standard" - /> - - - - - - - - - - ); + return ( + + + {editMode + ? t("account_tokens_dialog_title_edit") + : t("account_tokens_dialog_title_create")} + + + setLabel(ev.target.value)} + fullWidth + variant="standard" + /> + + + + + + + + + + ); }; const TokenDeleteDialog = (props) => { - const { t } = useTranslation(); - const [error, setError] = useState(""); + const { t } = useTranslation(); + const [error, setError] = useState(""); - const handleSubmit = async () => { - try { - await accountApi.deleteToken(props.token.token); - props.onClose(); - } catch (e) { - console.log(`[Account] Error deleting token`, e); - if (e instanceof UnauthorizedError) { - session.resetAndRedirect(routes.login); - } else { - setError(e.message); - } - } - }; - - return ( - - {t("account_tokens_delete_dialog_title")} - - - - - - - - - - - ); -} + const handleSubmit = async () => { + try { + await accountApi.deleteToken(props.token.token); + props.onClose(); + } catch (e) { + console.log(`[Account] Error deleting token`, e); + if (e instanceof UnauthorizedError) { + session.resetAndRedirect(routes.login); + } else { + setError(e.message); + } + } + }; + return ( + + {t("account_tokens_delete_dialog_title")} + + + + + + + + + + + ); +}; const Delete = () => { - const { t } = useTranslation(); - return ( - - - {t("account_delete_title")} - - - - - - ); + const { t } = useTranslation(); + return ( + + + {t("account_delete_title")} + + + + + + ); }; const DeleteAccount = () => { - const { t } = useTranslation(); - const [dialogKey, setDialogKey] = useState(0); - const [dialogOpen, setDialogOpen] = useState(false); + const { t } = useTranslation(); + const [dialogKey, setDialogKey] = useState(0); + const [dialogOpen, setDialogOpen] = useState(false); - const handleDialogOpen = () => { - setDialogKey(prev => prev+1); - setDialogOpen(true); - }; + const handleDialogOpen = () => { + setDialogKey((prev) => prev + 1); + setDialogOpen(true); + }; - const handleDialogClose = () => { - setDialogOpen(false); - }; + const handleDialogClose = () => { + setDialogOpen(false); + }; - return ( - -
- -
- -
- ) + return ( + +
+ +
+ +
+ ); }; const DeleteAccountDialog = (props) => { - const { t } = useTranslation(); - const { account } = useContext(AccountContext); - const [error, setError] = useState(""); - const [password, setPassword] = useState(""); - const fullScreen = useMediaQuery(theme.breakpoints.down('sm')); + const { t } = useTranslation(); + const { account } = useContext(AccountContext); + const [error, setError] = useState(""); + const [password, setPassword] = useState(""); + const fullScreen = useMediaQuery(theme.breakpoints.down("sm")); - const handleSubmit = async () => { - try { - await accountApi.delete(password); - await db.delete(); - console.debug(`[Account] Account deleted`); - session.resetAndRedirect(routes.app); - } catch (e) { - console.log(`[Account] Error deleting account`, e); - if (e instanceof IncorrectPasswordError) { - setError(t("account_basics_password_dialog_current_password_incorrect")); - } else if (e instanceof UnauthorizedError) { - session.resetAndRedirect(routes.login); - } else { - setError(e.message); - } - } - }; + const handleSubmit = async () => { + try { + await accountApi.delete(password); + await db.delete(); + console.debug(`[Account] Account deleted`); + session.resetAndRedirect(routes.app); + } catch (e) { + console.log(`[Account] Error deleting account`, e); + if (e instanceof IncorrectPasswordError) { + setError( + t("account_basics_password_dialog_current_password_incorrect") + ); + } else if (e instanceof UnauthorizedError) { + session.resetAndRedirect(routes.login); + } else { + setError(e.message); + } + } + }; - return ( - - {t("account_delete_title")} - - - {t("account_delete_dialog_description")} - - setPassword(ev.target.value)} - fullWidth - variant="standard" - /> - {account?.billing?.subscription && - {t("account_delete_dialog_billing_warning")} - } - - - - - - - ); + return ( + + {t("account_delete_title")} + + + {t("account_delete_dialog_description")} + + setPassword(ev.target.value)} + fullWidth + variant="standard" + /> + {account?.billing?.subscription && ( + + {t("account_delete_dialog_billing_warning")} + + )} + + + + + + + ); }; export default Account; diff --git a/web/src/components/ActionBar.js b/web/src/components/ActionBar.js index 189ae1cb..b6c84169 100644 --- a/web/src/components/ActionBar.js +++ b/web/src/components/ActionBar.js @@ -5,179 +5,219 @@ import IconButton from "@mui/material/IconButton"; import MenuIcon from "@mui/icons-material/Menu"; import Typography from "@mui/material/Typography"; import * as React from "react"; -import {useState} from "react"; +import { useState } from "react"; import Box from "@mui/material/Box"; -import {topicDisplayName} from "../app/utils"; +import { topicDisplayName } from "../app/utils"; import db from "../app/db"; -import {useLocation, useNavigate} from "react-router-dom"; -import MenuItem from '@mui/material/MenuItem'; +import { useLocation, useNavigate } from "react-router-dom"; +import MenuItem from "@mui/material/MenuItem"; import MoreVertIcon from "@mui/icons-material/MoreVert"; -import NotificationsIcon from '@mui/icons-material/Notifications'; -import NotificationsOffIcon from '@mui/icons-material/NotificationsOff'; +import NotificationsIcon from "@mui/icons-material/Notifications"; +import NotificationsOffIcon from "@mui/icons-material/NotificationsOff"; import routes from "./routes"; import subscriptionManager from "../app/SubscriptionManager"; import logo from "../img/ntfy.svg"; -import {useTranslation} from "react-i18next"; +import { useTranslation } from "react-i18next"; import session from "../app/Session"; -import AccountCircleIcon from '@mui/icons-material/AccountCircle'; +import AccountCircleIcon from "@mui/icons-material/AccountCircle"; import Button from "@mui/material/Button"; import Divider from "@mui/material/Divider"; -import {Logout, Person, Settings} from "@mui/icons-material"; +import { Logout, Person, Settings } from "@mui/icons-material"; import ListItemIcon from "@mui/material/ListItemIcon"; import accountApi from "../app/AccountApi"; import PopupMenu from "./PopupMenu"; import { SubscriptionPopup } from "./SubscriptionPopup"; const ActionBar = (props) => { - const { t } = useTranslation(); - const location = useLocation(); - let title = "ntfy"; - if (props.selected) { - title = topicDisplayName(props.selected); - } else if (location.pathname === routes.settings) { - title = t("action_bar_settings"); - } else if (location.pathname === routes.account) { - title = t("action_bar_account"); - } - return ( - Navigation (1200), but < Dialog (1300) - ml: { sm: `${Navigation.width}px` } - }}> - - - - - - - {title} - - {props.selected && - } - - - - ); + const { t } = useTranslation(); + const location = useLocation(); + let title = "ntfy"; + if (props.selected) { + title = topicDisplayName(props.selected); + } else if (location.pathname === routes.settings) { + title = t("action_bar_settings"); + } else if (location.pathname === routes.account) { + title = t("action_bar_account"); + } + return ( + Navigation (1200), but < Dialog (1300) + ml: { sm: `${Navigation.width}px` }, + }} + > + + + + + + + {title} + + {props.selected && ( + + )} + + + + ); }; const SettingsIcons = (props) => { - const { t } = useTranslation(); - const [anchorEl, setAnchorEl] = useState(null); - const subscription = props.subscription; + const { t } = useTranslation(); + const [anchorEl, setAnchorEl] = useState(null); + const subscription = props.subscription; - const handleToggleMute = async () => { - const mutedUntil = (subscription.mutedUntil) ? 0 : 1; // Make this a timestamp in the future - await subscriptionManager.setMutedUntil(subscription.id, mutedUntil); - } + const handleToggleMute = async () => { + const mutedUntil = subscription.mutedUntil ? 0 : 1; // Make this a timestamp in the future + await subscriptionManager.setMutedUntil(subscription.id, mutedUntil); + }; - return ( - <> - - {subscription.mutedUntil ? : } - - setAnchorEl(ev.currentTarget)} aria-label={t("action_bar_toggle_action_menu")}> - - - setAnchorEl(null)} - /> - - ); + return ( + <> + + {subscription.mutedUntil ? ( + + ) : ( + + )} + + setAnchorEl(ev.currentTarget)} + aria-label={t("action_bar_toggle_action_menu")} + > + + + setAnchorEl(null)} + /> + + ); }; const ProfileIcon = () => { - const { t } = useTranslation(); - const [anchorEl, setAnchorEl] = useState(null); - const open = Boolean(anchorEl); - const navigate = useNavigate(); + const { t } = useTranslation(); + const [anchorEl, setAnchorEl] = useState(null); + const open = Boolean(anchorEl); + const navigate = useNavigate(); - const handleClick = (event) => { - setAnchorEl(event.currentTarget); - }; + const handleClick = (event) => { + setAnchorEl(event.currentTarget); + }; - const handleClose = () => { - setAnchorEl(null); - }; + const handleClose = () => { + setAnchorEl(null); + }; - const handleLogout = async () => { - try { - await accountApi.logout(); - await db.delete(); - } finally { - session.resetAndRedirect(routes.app); - } - }; + const handleLogout = async () => { + try { + await accountApi.logout(); + await db.delete(); + } finally { + session.resetAndRedirect(routes.app); + } + }; - return ( - <> - {session.exists() && - - - - } - {!session.exists() && config.enable_login && - - } - {!session.exists() && config.enable_signup && - - } - - navigate(routes.account)}> - - - - {session.username()} - - - navigate(routes.settings)}> - - - - {t("action_bar_profile_settings")} - - - - - - {t("action_bar_profile_logout")} - - - - ); + return ( + <> + {session.exists() && ( + + + + )} + {!session.exists() && config.enable_login && ( + + )} + {!session.exists() && config.enable_signup && ( + + )} + + navigate(routes.account)}> + + + + {session.username()} + + + navigate(routes.settings)}> + + + + {t("action_bar_profile_settings")} + + + + + + {t("action_bar_profile_logout")} + + + + ); }; export default ActionBar; diff --git a/web/src/components/App.js b/web/src/components/App.js index 861a3709..b2c204a4 100644 --- a/web/src/components/App.js +++ b/web/src/components/App.js @@ -1,27 +1,43 @@ -import * as React from 'react'; -import {createContext, Suspense, useContext, useEffect, useState} from 'react'; -import Box from '@mui/material/Box'; -import {ThemeProvider} from '@mui/material/styles'; -import CssBaseline from '@mui/material/CssBaseline'; -import Toolbar from '@mui/material/Toolbar'; -import {AllSubscriptions, SingleSubscription} from "./Notifications"; +import * as React from "react"; +import { + createContext, + Suspense, + useContext, + useEffect, + useState, +} from "react"; +import Box from "@mui/material/Box"; +import { ThemeProvider } from "@mui/material/styles"; +import CssBaseline from "@mui/material/CssBaseline"; +import Toolbar from "@mui/material/Toolbar"; +import { AllSubscriptions, SingleSubscription } from "./Notifications"; import theme from "./theme"; import Navigation from "./Navigation"; import ActionBar from "./ActionBar"; import notifier from "../app/Notifier"; import Preferences from "./Preferences"; -import {useLiveQuery} from "dexie-react-hooks"; +import { useLiveQuery } from "dexie-react-hooks"; import subscriptionManager from "../app/SubscriptionManager"; import userManager from "../app/UserManager"; -import {BrowserRouter, Outlet, Route, Routes, useParams} from "react-router-dom"; -import {expandUrl} from "../app/utils"; +import { + BrowserRouter, + Outlet, + Route, + Routes, + useParams, +} from "react-router-dom"; +import { expandUrl } from "../app/utils"; import ErrorBoundary from "./ErrorBoundary"; import routes from "./routes"; -import {useAccountListener, useBackgroundProcesses, useConnectionListeners} from "./hooks"; +import { + useAccountListener, + useBackgroundProcesses, + useConnectionListeners, +} from "./hooks"; import PublishDialog from "./PublishDialog"; import Messaging from "./Messaging"; import "./i18n"; // Translations! -import {Backdrop, CircularProgress} from "@mui/material"; +import { Backdrop, CircularProgress } from "@mui/material"; import Login from "./Login"; import Signup from "./Signup"; import Account from "./Account"; @@ -29,119 +45,145 @@ import Account from "./Account"; export const AccountContext = createContext(null); const App = () => { - const [account, setAccount] = useState(null); - return ( - }> - - - - - - - }/> - }/> - }> - }/> - }/> - }/> - }/> - }/> - - - - - - - - ); -} + const [account, setAccount] = useState(null); + return ( + }> + + + + + + + } /> + } /> + }> + } /> + } /> + } /> + } + /> + } + /> + + + + + + + + ); +}; const Layout = () => { - const params = useParams(); - const { account, setAccount } = useContext(AccountContext); - const [mobileDrawerOpen, setMobileDrawerOpen] = useState(false); - const [notificationsGranted, setNotificationsGranted] = useState(notifier.granted()); - const [sendDialogOpenMode, setSendDialogOpenMode] = useState(""); - const users = useLiveQuery(() => userManager.all()); - const subscriptions = useLiveQuery(() => subscriptionManager.all()); - const subscriptionsWithoutInternal = subscriptions?.filter(s => !s.internal); - const newNotificationsCount = subscriptionsWithoutInternal?.reduce((prev, cur) => prev + cur.new, 0) || 0; - const [selected] = (subscriptionsWithoutInternal || []).filter(s => { - return (params.baseUrl && expandUrl(params.baseUrl).includes(s.baseUrl) && params.topic === s.topic) - || (config.base_url === s.baseUrl && params.topic === s.topic) - }); - - useConnectionListeners(account, subscriptions, users); - useAccountListener(setAccount) - useBackgroundProcesses(); - useEffect(() => updateTitle(newNotificationsCount), [newNotificationsCount]); - + const params = useParams(); + const { account, setAccount } = useContext(AccountContext); + const [mobileDrawerOpen, setMobileDrawerOpen] = useState(false); + const [notificationsGranted, setNotificationsGranted] = useState( + notifier.granted() + ); + const [sendDialogOpenMode, setSendDialogOpenMode] = useState(""); + const users = useLiveQuery(() => userManager.all()); + const subscriptions = useLiveQuery(() => subscriptionManager.all()); + const subscriptionsWithoutInternal = subscriptions?.filter( + (s) => !s.internal + ); + const newNotificationsCount = + subscriptionsWithoutInternal?.reduce((prev, cur) => prev + cur.new, 0) || 0; + const [selected] = (subscriptionsWithoutInternal || []).filter((s) => { return ( - - setMobileDrawerOpen(!mobileDrawerOpen)} - /> - setMobileDrawerOpen(!mobileDrawerOpen)} - onNotificationGranted={setNotificationsGranted} - onPublishMessageClick={() => setSendDialogOpenMode(PublishDialog.OPEN_MODE_DEFAULT)} - /> -
- - -
- -
+ (params.baseUrl && + expandUrl(params.baseUrl).includes(s.baseUrl) && + params.topic === s.topic) || + (config.base_url === s.baseUrl && params.topic === s.topic) ); -} + }); + + useConnectionListeners(account, subscriptions, users); + useAccountListener(setAccount); + useBackgroundProcesses(); + useEffect(() => updateTitle(newNotificationsCount), [newNotificationsCount]); + + return ( + + setMobileDrawerOpen(!mobileDrawerOpen)} + /> + setMobileDrawerOpen(!mobileDrawerOpen)} + onNotificationGranted={setNotificationsGranted} + onPublishMessageClick={() => + setSendDialogOpenMode(PublishDialog.OPEN_MODE_DEFAULT) + } + /> +
+ + +
+ +
+ ); +}; const Main = (props) => { - return ( - theme.palette.mode === 'light' ? theme.palette.grey[100] : theme.palette.grey[900] - }} - > - {props.children} - - ); + return ( + + theme.palette.mode === "light" + ? theme.palette.grey[100] + : theme.palette.grey[900], + }} + > + {props.children} + + ); }; const Loader = () => ( - theme.palette.mode === 'light' ? theme.palette.grey[100] : theme.palette.grey[900] - }} - > - - + + theme.palette.mode === "light" + ? theme.palette.grey[100] + : theme.palette.grey[900], + }} + > + + ); const updateTitle = (newNotificationsCount) => { - document.title = (newNotificationsCount > 0) ? `(${newNotificationsCount}) ntfy` : "ntfy"; -} + document.title = + newNotificationsCount > 0 ? `(${newNotificationsCount}) ntfy` : "ntfy"; +}; export default App; diff --git a/web/src/components/AttachmentIcon.js b/web/src/components/AttachmentIcon.js index 337760b7..9939b3b3 100644 --- a/web/src/components/AttachmentIcon.js +++ b/web/src/components/AttachmentIcon.js @@ -5,43 +5,43 @@ import fileImage from "../img/file-image.svg"; import fileVideo from "../img/file-video.svg"; import fileAudio from "../img/file-audio.svg"; import fileApp from "../img/file-app.svg"; -import {useTranslation} from "react-i18next"; +import { useTranslation } from "react-i18next"; const AttachmentIcon = (props) => { - const { t } = useTranslation(); - const type = props.type; - let imageFile, imageLabel; - if (!type) { - imageFile = fileDocument; - imageLabel = t("notifications_attachment_file_image"); - } else if (type.startsWith('image/')) { - imageFile = fileImage; - imageLabel = t("notifications_attachment_file_video"); - } else if (type.startsWith('video/')) { - imageFile = fileVideo; - imageLabel = t("notifications_attachment_file_video"); - } else if (type.startsWith('audio/')) { - imageFile = fileAudio; - imageLabel = t("notifications_attachment_file_audio"); - } else if (type === "application/vnd.android.package-archive") { - imageFile = fileApp; - imageLabel = t("notifications_attachment_file_app"); - } else { - imageFile = fileDocument; - imageLabel = t("notifications_attachment_file_document"); - } - return ( - - ); -} + const { t } = useTranslation(); + const type = props.type; + let imageFile, imageLabel; + if (!type) { + imageFile = fileDocument; + imageLabel = t("notifications_attachment_file_image"); + } else if (type.startsWith("image/")) { + imageFile = fileImage; + imageLabel = t("notifications_attachment_file_video"); + } else if (type.startsWith("video/")) { + imageFile = fileVideo; + imageLabel = t("notifications_attachment_file_video"); + } else if (type.startsWith("audio/")) { + imageFile = fileAudio; + imageLabel = t("notifications_attachment_file_audio"); + } else if (type === "application/vnd.android.package-archive") { + imageFile = fileApp; + imageLabel = t("notifications_attachment_file_app"); + } else { + imageFile = fileDocument; + imageLabel = t("notifications_attachment_file_document"); + } + return ( + + ); +}; export default AttachmentIcon; diff --git a/web/src/components/AvatarBox.js b/web/src/components/AvatarBox.js index 2278f605..5a612f1d 100644 --- a/web/src/components/AvatarBox.js +++ b/web/src/components/AvatarBox.js @@ -1,29 +1,29 @@ -import * as React from 'react'; -import {Avatar} from "@mui/material"; +import * as React from "react"; +import { Avatar } from "@mui/material"; import Box from "@mui/material/Box"; import logo from "../img/ntfy-filled.svg"; const AvatarBox = (props) => { - return ( - - - {props.children} - - ); -} + return ( + + + {props.children} + + ); +}; export default AvatarBox; diff --git a/web/src/components/DialogFooter.js b/web/src/components/DialogFooter.js index 68d17c73..5a2bd7aa 100644 --- a/web/src/components/DialogFooter.js +++ b/web/src/components/DialogFooter.js @@ -4,30 +4,30 @@ import DialogContentText from "@mui/material/DialogContentText"; import DialogActions from "@mui/material/DialogActions"; const DialogFooter = (props) => { - return ( - - - {props.status} - - - {props.children} - - - ); + return ( + + + {props.status} + + {props.children} + + ); }; export default DialogFooter; diff --git a/web/src/components/EmojiPicker.js b/web/src/components/EmojiPicker.js index 9b29e8f0..03badb7f 100644 --- a/web/src/components/EmojiPicker.js +++ b/web/src/components/EmojiPicker.js @@ -1,15 +1,15 @@ -import * as React from 'react'; -import {useRef, useState} from 'react'; -import Typography from '@mui/material/Typography'; -import {rawEmojis} from '../app/emojis'; +import * as React from "react"; +import { useRef, useState } from "react"; +import Typography from "@mui/material/Typography"; +import { rawEmojis } from "../app/emojis"; import Box from "@mui/material/Box"; import TextField from "@mui/material/TextField"; -import {ClickAwayListener, Fade, InputAdornment, styled} from "@mui/material"; +import { ClickAwayListener, Fade, InputAdornment, styled } from "@mui/material"; import IconButton from "@mui/material/IconButton"; -import {Close} from "@mui/icons-material"; +import { Close } from "@mui/icons-material"; import Popper from "@mui/material/Popper"; -import {splitNoEmpty} from "../app/utils"; -import {useTranslation} from "react-i18next"; +import { splitNoEmpty } from "../app/utils"; +import { useTranslation } from "react-i18next"; // Create emoji list by category and create a search base (string with all search words) // @@ -17,163 +17,185 @@ import {useTranslation} from "react-i18next"; // This is a hack, but on Ubuntu 18.04, with Chrome 99, only Emoji <= 11 are supported. const emojisByCategory = {}; -const isDesktopChrome = /Chrome/.test(navigator.userAgent) && !/Mobile/.test(navigator.userAgent); +const isDesktopChrome = + /Chrome/.test(navigator.userAgent) && !/Mobile/.test(navigator.userAgent); const maxSupportedVersionForDesktopChrome = 11; -rawEmojis.forEach(emoji => { - if (!emojisByCategory[emoji.category]) { - emojisByCategory[emoji.category] = []; - } - try { - const unicodeVersion = parseFloat(emoji.unicode_version); - const supportedEmoji = unicodeVersion <= maxSupportedVersionForDesktopChrome || !isDesktopChrome; - if (supportedEmoji) { - const searchBase = `${emoji.description.toLowerCase()} ${emoji.aliases.join(" ")} ${emoji.tags.join(" ")}`; - const emojiWithSearchBase = { ...emoji, searchBase: searchBase }; - emojisByCategory[emoji.category].push(emojiWithSearchBase); - } - } catch (e) { - // Nothing. Ignore. +rawEmojis.forEach((emoji) => { + if (!emojisByCategory[emoji.category]) { + emojisByCategory[emoji.category] = []; + } + try { + const unicodeVersion = parseFloat(emoji.unicode_version); + const supportedEmoji = + unicodeVersion <= maxSupportedVersionForDesktopChrome || !isDesktopChrome; + if (supportedEmoji) { + const searchBase = `${emoji.description.toLowerCase()} ${emoji.aliases.join( + " " + )} ${emoji.tags.join(" ")}`; + const emojiWithSearchBase = { ...emoji, searchBase: searchBase }; + emojisByCategory[emoji.category].push(emojiWithSearchBase); } + } catch (e) { + // Nothing. Ignore. + } }); const EmojiPicker = (props) => { - const { t } = useTranslation(); - const open = Boolean(props.anchorEl); - const [search, setSearch] = useState(""); - const searchRef = useRef(null); - const searchFields = splitNoEmpty(search.toLowerCase(), " "); + const { t } = useTranslation(); + const open = Boolean(props.anchorEl); + const [search, setSearch] = useState(""); + const searchRef = useRef(null); + const searchFields = splitNoEmpty(search.toLowerCase(), " "); - const handleSearchClear = () => { - setSearch(""); - searchRef.current?.focus(); - }; + const handleSearchClear = () => { + setSearch(""); + searchRef.current?.focus(); + }; - return ( - - {({ TransitionProps }) => ( - - - - setSearch(ev.target.value)} - type="text" - variant="standard" - fullWidth - sx={{ marginTop: 0, marginBottom: "12px", paddingRight: 2 }} - inputProps={{ - role: "searchbox", - "aria-label": t("emoji_picker_search_placeholder") - }} - InputProps={{ - endAdornment: - - - - - - }} - /> - - {Object.keys(emojisByCategory).map(category => - - )} - - - - - )} - - ); + return ( + + {({ TransitionProps }) => ( + + + + setSearch(ev.target.value)} + type="text" + variant="standard" + fullWidth + sx={{ marginTop: 0, marginBottom: "12px", paddingRight: 2 }} + inputProps={{ + role: "searchbox", + "aria-label": t("emoji_picker_search_placeholder"), + }} + InputProps={{ + endAdornment: ( + + + + + + ), + }} + /> + + {Object.keys(emojisByCategory).map((category) => ( + + ))} + + + + + )} + + ); }; const Category = (props) => { - const showTitle = props.search.length === 0; - return ( - <> - {showTitle && - - {props.title} - - } - {props.emojis.map(emoji => - props.onPick(emoji.aliases[0])} - /> - )} - - ); + const showTitle = props.search.length === 0; + return ( + <> + {showTitle && ( + + {props.title} + + )} + {props.emojis.map((emoji) => ( + props.onPick(emoji.aliases[0])} + /> + ))} + + ); }; const Emoji = (props) => { - const emoji = props.emoji; - const matches = emojiMatches(emoji, props.search); - const title = `${emoji.description} (${emoji.aliases[0]})`; - return ( - - {props.emoji.emoji} - - ); + const emoji = props.emoji; + const matches = emojiMatches(emoji, props.search); + const title = `${emoji.description} (${emoji.aliases[0]})`; + return ( + + {props.emoji.emoji} + + ); }; const EmojiDiv = styled("div")({ - fontSize: "30px", - width: "30px", - height: "30px", - marginTop: "8px", - marginBottom: "8px", - marginRight: "8px", - lineHeight: "30px", - cursor: "pointer", - opacity: 0.85, - "&:hover": { - opacity: 1 - } + fontSize: "30px", + width: "30px", + height: "30px", + marginTop: "8px", + marginBottom: "8px", + marginRight: "8px", + lineHeight: "30px", + cursor: "pointer", + opacity: 0.85, + "&:hover": { + opacity: 1, + }, }); const emojiMatches = (emoji, words) => { - if (words.length === 0) { - return true; - } - for (const word of words) { - if (emoji.searchBase.indexOf(word) === -1) { - return false; - } - } + if (words.length === 0) { return true; -} + } + for (const word of words) { + if (emoji.searchBase.indexOf(word) === -1) { + return false; + } + } + return true; +}; export default EmojiPicker; diff --git a/web/src/components/ErrorBoundary.js b/web/src/components/ErrorBoundary.js index c6d789a3..f1ce7c2c 100644 --- a/web/src/components/ErrorBoundary.js +++ b/web/src/components/ErrorBoundary.js @@ -1,128 +1,151 @@ import * as React from "react"; import StackTrace from "stacktrace-js"; -import {CircularProgress, Link} from "@mui/material"; +import { CircularProgress, Link } from "@mui/material"; import Button from "@mui/material/Button"; -import {Trans, withTranslation} from "react-i18next"; +import { Trans, withTranslation } from "react-i18next"; class ErrorBoundaryImpl extends React.Component { - constructor(props) { - super(props); - this.state = { - error: false, - originalStack: null, - niceStack: null, - unsupportedIndexedDB: false - }; + constructor(props) { + super(props); + this.state = { + error: false, + originalStack: null, + niceStack: null, + unsupportedIndexedDB: false, + }; + } + + componentDidCatch(error, info) { + console.error("[ErrorBoundary] Error caught", error, info); + + // Special case for unsupported IndexedDB in Private Browsing mode (Firefox, Safari), see + // - https://github.com/dexie/Dexie.js/issues/312 + // - https://bugzilla.mozilla.org/show_bug.cgi?id=781982 + const isUnsupportedIndexedDB = + error?.name === "InvalidStateError" || + (error?.name === "DatabaseClosedError" && + error?.message?.indexOf("InvalidStateError") !== -1); + + if (isUnsupportedIndexedDB) { + this.handleUnsupportedIndexedDB(); + } else { + this.handleError(error, info); } + } - componentDidCatch(error, info) { - console.error("[ErrorBoundary] Error caught", error, info); + handleError(error, info) { + // Immediately render original stack trace + const prettierOriginalStack = info.componentStack + .trim() + .split("\n") + .map((line) => ` at ${line}`) + .join("\n"); + this.setState({ + error: true, + originalStack: `${error.toString()}\n${prettierOriginalStack}`, + }); - // Special case for unsupported IndexedDB in Private Browsing mode (Firefox, Safari), see - // - https://github.com/dexie/Dexie.js/issues/312 - // - https://bugzilla.mozilla.org/show_bug.cgi?id=781982 - const isUnsupportedIndexedDB = error?.name === "InvalidStateError" || - (error?.name === "DatabaseClosedError" && error?.message?.indexOf("InvalidStateError") !== -1); + // Fetch additional info and a better stack trace + StackTrace.fromError(error).then((stack) => { + console.error("[ErrorBoundary] Stacktrace fetched", stack); + const niceStack = + `${error.toString()}\n` + + stack + .map( + (el) => + ` at ${el.functionName} (${el.fileName}:${el.columnNumber}:${el.lineNumber})` + ) + .join("\n"); + this.setState({ niceStack }); + }); + } - if (isUnsupportedIndexedDB) { - this.handleUnsupportedIndexedDB(); - } else { - this.handleError(error, info); - } + handleUnsupportedIndexedDB() { + this.setState({ + error: true, + unsupportedIndexedDB: true, + }); + } + + copyStack() { + let stack = ""; + if (this.state.niceStack) { + stack += `${this.state.niceStack}\n\n`; } + stack += `${this.state.originalStack}\n`; + navigator.clipboard.writeText(stack); + } - handleError(error, info) { - // Immediately render original stack trace - const prettierOriginalStack = info.componentStack - .trim() - .split("\n") - .map(line => ` at ${line}`) - .join("\n"); - this.setState({ - error: true, - originalStack: `${error.toString()}\n${prettierOriginalStack}` - }); - - // Fetch additional info and a better stack trace - StackTrace.fromError(error).then(stack => { - console.error("[ErrorBoundary] Stacktrace fetched", stack); - const niceStack = `${error.toString()}\n` + stack.map( el => ` at ${el.functionName} (${el.fileName}:${el.columnNumber}:${el.lineNumber})`).join("\n"); - this.setState({ niceStack }); - }); + render() { + if (this.state.error) { + if (this.state.unsupportedIndexedDB) { + return this.renderUnsupportedIndexedDB(); + } else { + return this.renderError(); + } } + return this.props.children; + } - handleUnsupportedIndexedDB() { - this.setState({ - error: true, - unsupportedIndexedDB: true - }); - } + renderUnsupportedIndexedDB() { + const { t } = this.props; + return ( +
+

{t("error_boundary_unsupported_indexeddb_title")} 😮

+

+ + ), + discordLink: , + matrixLink: , + }} + /> +

+
+ ); + } - copyStack() { - let stack = ""; - if (this.state.niceStack) { - stack += `${this.state.niceStack}\n\n`; - } - stack += `${this.state.originalStack}\n`; - navigator.clipboard.writeText(stack); - } - - render() { - if (this.state.error) { - if (this.state.unsupportedIndexedDB) { - return this.renderUnsupportedIndexedDB(); - } else { - return this.renderError(); - } - } - return this.props.children; - } - - renderUnsupportedIndexedDB() { - const { t } = this.props; - return ( -
-

{t("error_boundary_unsupported_indexeddb_title")} 😮

-

- , - discordLink: , - matrixLink: - }} - /> -

-
- ); - } - - renderError() { - const { t } = this.props; - return ( -
-

{t("error_boundary_title")} 😮

-

- , - discordLink: , - matrixLink: - }} - /> -

-

- -

-

{t("error_boundary_stack_trace")}

- {this.state.niceStack - ?
{this.state.niceStack}
- : <> {t("error_boundary_gathering_info")}} -
{this.state.originalStack}
-
- ); - } + renderError() { + const { t } = this.props; + return ( +
+

{t("error_boundary_title")} 😮

+

+ + ), + discordLink: , + matrixLink: , + }} + /> +

+

+ +

+

{t("error_boundary_stack_trace")}

+ {this.state.niceStack ? ( +
{this.state.niceStack}
+ ) : ( + <> + {" "} + {t("error_boundary_gathering_info")} + + )} +
{this.state.originalStack}
+
+ ); + } } const ErrorBoundary = withTranslation()(ErrorBoundaryImpl); // Adds props.t diff --git a/web/src/components/Login.js b/web/src/components/Login.js index 8b14c53d..a109ae6f 100644 --- a/web/src/components/Login.js +++ b/web/src/components/Login.js @@ -1,122 +1,135 @@ -import * as React from 'react'; -import {useState} from 'react'; +import * as React from "react"; +import { useState } from "react"; import Typography from "@mui/material/Typography"; -import WarningAmberIcon from '@mui/icons-material/WarningAmber'; +import WarningAmberIcon from "@mui/icons-material/WarningAmber"; import TextField from "@mui/material/TextField"; import Button from "@mui/material/Button"; import Box from "@mui/material/Box"; import routes from "./routes"; import session from "../app/Session"; -import {NavLink} from "react-router-dom"; +import { NavLink } from "react-router-dom"; import AvatarBox from "./AvatarBox"; -import {useTranslation} from "react-i18next"; +import { useTranslation } from "react-i18next"; import accountApi from "../app/AccountApi"; import IconButton from "@mui/material/IconButton"; -import {InputAdornment} from "@mui/material"; -import {Visibility, VisibilityOff} from "@mui/icons-material"; -import {UnauthorizedError} from "../app/errors"; +import { InputAdornment } from "@mui/material"; +import { Visibility, VisibilityOff } from "@mui/icons-material"; +import { UnauthorizedError } from "../app/errors"; const Login = () => { - const { t } = useTranslation(); - const [error, setError] = useState(""); - const [username, setUsername] = useState(""); - const [password, setPassword] = useState(""); - const [showPassword, setShowPassword] = useState(false); + const { t } = useTranslation(); + const [error, setError] = useState(""); + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + const [showPassword, setShowPassword] = useState(false); - const handleSubmit = async (event) => { - event.preventDefault(); - const user = { username, password }; - try { - const token = await accountApi.login(user); - console.log(`[Login] User auth for user ${user.username} successful, token is ${token}`); - session.store(user.username, token); - window.location.href = routes.app; - } catch (e) { - console.log(`[Login] User auth for user ${user.username} failed`, e); - if (e instanceof UnauthorizedError) { - setError(t("Login failed: Invalid username or password")); - } else { - setError(e.message); - } - } - }; - if (!config.enable_login) { - return ( - - {t("login_disabled")} - - ); + const handleSubmit = async (event) => { + event.preventDefault(); + const user = { username, password }; + try { + const token = await accountApi.login(user); + console.log( + `[Login] User auth for user ${user.username} successful, token is ${token}` + ); + session.store(user.username, token); + window.location.href = routes.app; + } catch (e) { + console.log(`[Login] User auth for user ${user.username} failed`, e); + if (e instanceof UnauthorizedError) { + setError(t("Login failed: Invalid username or password")); + } else { + setError(e.message); + } } + }; + if (!config.enable_login) { return ( - - - {t("login_title")} - - - setUsername(ev.target.value.trim())} - autoFocus - /> - setPassword(ev.target.value.trim())} - autoComplete="current-password" - InputProps={{ - endAdornment: ( - - setShowPassword(!showPassword)} - onMouseDown={(ev) => ev.preventDefault()} - edge="end" - > - {showPassword ? : } - - - ) - }} - /> - - {error && - - - {error} - - } - - {/* This is where the password reset link would go */} - {config.enable_signup &&
{t("login_link_signup")}
} -
-
-
+ + {t("login_disabled")} + ); -} + } + return ( + + {t("login_title")} + + setUsername(ev.target.value.trim())} + autoFocus + /> + setPassword(ev.target.value.trim())} + autoComplete="current-password" + InputProps={{ + endAdornment: ( + + setShowPassword(!showPassword)} + onMouseDown={(ev) => ev.preventDefault()} + edge="end" + > + {showPassword ? : } + + + ), + }} + /> + + {error && ( + + + {error} + + )} + + {/* This is where the password reset link would go */} + {config.enable_signup && ( +
+ + {t("login_link_signup")} + +
+ )} +
+
+
+ ); +}; export default Login; diff --git a/web/src/components/Messaging.js b/web/src/components/Messaging.js index b1f11a96..d69bc578 100644 --- a/web/src/components/Messaging.js +++ b/web/src/components/Messaging.js @@ -1,5 +1,5 @@ -import * as React from 'react'; -import {useState} from 'react'; +import * as React from "react"; +import { useState } from "react"; import Navigation from "./Navigation"; import Paper from "@mui/material/Paper"; import IconButton from "@mui/material/IconButton"; @@ -7,108 +7,135 @@ import TextField from "@mui/material/TextField"; import SendIcon from "@mui/icons-material/Send"; import api from "../app/Api"; import PublishDialog from "./PublishDialog"; -import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp'; -import {Portal, Snackbar} from "@mui/material"; -import {useTranslation} from "react-i18next"; +import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp"; +import { Portal, Snackbar } from "@mui/material"; +import { useTranslation } from "react-i18next"; const Messaging = (props) => { - const [message, setMessage] = useState(""); - const [dialogKey, setDialogKey] = useState(0); + const [message, setMessage] = useState(""); + const [dialogKey, setDialogKey] = useState(0); - const dialogOpenMode = props.dialogOpenMode; - const subscription = props.selected; + const dialogOpenMode = props.dialogOpenMode; + const subscription = props.selected; - const handleOpenDialogClick = () => { - props.onDialogOpenModeChange(PublishDialog.OPEN_MODE_DEFAULT); - }; + const handleOpenDialogClick = () => { + props.onDialogOpenModeChange(PublishDialog.OPEN_MODE_DEFAULT); + }; - const handleDialogClose = () => { - props.onDialogOpenModeChange(""); - setDialogKey(prev => prev+1); - }; + const handleDialogClose = () => { + props.onDialogOpenModeChange(""); + setDialogKey((prev) => prev + 1); + }; - return ( - <> - {subscription && } - props.onDialogOpenModeChange(prev => (prev) ? prev : PublishDialog.OPEN_MODE_DRAG)} // Only update if not already open - onResetOpenMode={() => props.onDialogOpenModeChange(PublishDialog.OPEN_MODE_DEFAULT)} - /> - - ); -} + return ( + <> + {subscription && ( + + )} + + props.onDialogOpenModeChange((prev) => + prev ? prev : PublishDialog.OPEN_MODE_DRAG + ) + } // Only update if not already open + onResetOpenMode={() => + props.onDialogOpenModeChange(PublishDialog.OPEN_MODE_DEFAULT) + } + /> + + ); +}; const MessageBar = (props) => { - const { t } = useTranslation(); - const subscription = props.subscription; - const [snackOpen, setSnackOpen] = useState(false); - const handleSendClick = async () => { - try { - await api.publish(subscription.baseUrl, subscription.topic, props.message); - } catch (e) { - console.log(`[MessageBar] Error publishing message`, e); - setSnackOpen(true); - } - props.onMessageChange(""); - }; - return ( - theme.palette.mode === 'light' ? theme.palette.grey[100] : theme.palette.grey[900] - }} - > - - - - props.onMessageChange(ev.target.value)} - onKeyPress={(ev) => { - if (ev.key === 'Enter') { - ev.preventDefault(); - handleSendClick(); - } - }} - /> - - - - - setSnackOpen(false)} - message={t("message_bar_error_publishing")} - /> - - - ); + const { t } = useTranslation(); + const subscription = props.subscription; + const [snackOpen, setSnackOpen] = useState(false); + const handleSendClick = async () => { + try { + await api.publish( + subscription.baseUrl, + subscription.topic, + props.message + ); + } catch (e) { + console.log(`[MessageBar] Error publishing message`, e); + setSnackOpen(true); + } + props.onMessageChange(""); + }; + return ( + + theme.palette.mode === "light" + ? theme.palette.grey[100] + : theme.palette.grey[900], + }} + > + + + + props.onMessageChange(ev.target.value)} + onKeyPress={(ev) => { + if (ev.key === "Enter") { + ev.preventDefault(); + handleSendClick(); + } + }} + /> + + + + + setSnackOpen(false)} + message={t("message_bar_error_publishing")} + /> + + + ); }; export default Messaging; diff --git a/web/src/components/Navigation.js b/web/src/components/Navigation.js index a7d0da0e..654e29be 100644 --- a/web/src/components/Navigation.js +++ b/web/src/components/Navigation.js @@ -1,6 +1,6 @@ import Drawer from "@mui/material/Drawer"; import * as React from "react"; -import {useContext, useState} from "react"; +import { useContext, useState } from "react"; import ListItemButton from "@mui/material/ListItemButton"; import ListItemIcon from "@mui/material/ListItemIcon"; import ChatBubbleOutlineIcon from "@mui/icons-material/ChatBubbleOutline"; @@ -12,360 +12,485 @@ import List from "@mui/material/List"; import SettingsIcon from "@mui/icons-material/Settings"; import AddIcon from "@mui/icons-material/Add"; import SubscribeDialog from "./SubscribeDialog"; -import {Alert, AlertTitle, Badge, CircularProgress, Link, ListSubheader, Portal, Tooltip} from "@mui/material"; +import { + Alert, + AlertTitle, + Badge, + CircularProgress, + Link, + ListSubheader, + Portal, + Tooltip, +} from "@mui/material"; import Button from "@mui/material/Button"; import Typography from "@mui/material/Typography"; -import {openUrl, topicDisplayName, topicUrl} from "../app/utils"; +import { openUrl, topicDisplayName, topicUrl } from "../app/utils"; import routes from "./routes"; -import {ConnectionState} from "../app/Connection"; -import {useLocation, useNavigate} from "react-router-dom"; +import { ConnectionState } from "../app/Connection"; +import { useLocation, useNavigate } from "react-router-dom"; import subscriptionManager from "../app/SubscriptionManager"; -import {ChatBubble, MoreVert, NotificationsOffOutlined, Send} from "@mui/icons-material"; +import { + ChatBubble, + MoreVert, + NotificationsOffOutlined, + Send, +} from "@mui/icons-material"; import Box from "@mui/material/Box"; import notifier from "../app/Notifier"; import config from "../app/config"; -import ArticleIcon from '@mui/icons-material/Article'; -import {Trans, useTranslation} from "react-i18next"; +import ArticleIcon from "@mui/icons-material/Article"; +import { Trans, useTranslation } from "react-i18next"; import session from "../app/Session"; -import accountApi, {Permission, Role} from "../app/AccountApi"; -import CelebrationIcon from '@mui/icons-material/Celebration'; +import accountApi, { Permission, Role } from "../app/AccountApi"; +import CelebrationIcon from "@mui/icons-material/Celebration"; import UpgradeDialog from "./UpgradeDialog"; -import {AccountContext} from "./App"; -import {PermissionDenyAll, PermissionRead, PermissionReadWrite, PermissionWrite} from "./ReserveIcons"; +import { AccountContext } from "./App"; +import { + PermissionDenyAll, + PermissionRead, + PermissionReadWrite, + PermissionWrite, +} from "./ReserveIcons"; import IconButton from "@mui/material/IconButton"; import { SubscriptionPopup } from "./SubscriptionPopup"; const navWidth = 280; const Navigation = (props) => { - const navigationList = ; - return ( - - {/* Mobile drawer; only shown if menu icon clicked (mobile open) and display is small */} - - {navigationList} - - {/* Big screen drawer; persistent, shown if screen is big */} - - {navigationList} - - - ); + const navigationList = ; + return ( + + {/* Mobile drawer; only shown if menu icon clicked (mobile open) and display is small */} + + {navigationList} + + {/* Big screen drawer; persistent, shown if screen is big */} + + {navigationList} + + + ); }; Navigation.width = navWidth; const NavList = (props) => { - const { t } = useTranslation(); - const navigate = useNavigate(); - const location = useLocation(); - const { account } = useContext(AccountContext); - const [subscribeDialogKey, setSubscribeDialogKey] = useState(0); - const [subscribeDialogOpen, setSubscribeDialogOpen] = useState(false); + const { t } = useTranslation(); + const navigate = useNavigate(); + const location = useLocation(); + const { account } = useContext(AccountContext); + const [subscribeDialogKey, setSubscribeDialogKey] = useState(0); + const [subscribeDialogOpen, setSubscribeDialogOpen] = useState(false); - const handleSubscribeReset = () => { - setSubscribeDialogOpen(false); - setSubscribeDialogKey(prev => prev+1); - } + const handleSubscribeReset = () => { + setSubscribeDialogOpen(false); + setSubscribeDialogKey((prev) => prev + 1); + }; - const handleSubscribeSubmit = (subscription) => { - console.log(`[Navigation] New subscription: ${subscription.id}`, subscription); - handleSubscribeReset(); - navigate(routes.forSubscription(subscription)); - handleRequestNotificationPermission(); - } - - const handleRequestNotificationPermission = () => { - notifier.maybeRequestPermission(granted => props.onNotificationGranted(granted)) - }; - - const handleAccountClick = () => { - accountApi.sync(); // Dangle! - navigate(routes.account); - }; - - const isAdmin = account?.role === Role.ADMIN; - const isPaid = account?.billing?.subscription; - const showUpgradeBanner = config.enable_payments && !isAdmin && !isPaid; - const showSubscriptionsList = props.subscriptions?.length > 0; - const showNotificationBrowserNotSupportedBox = !notifier.browserSupported(); - const showNotificationContextNotSupportedBox = notifier.browserSupported() && !notifier.contextSupported(); // Only show if notifications are generally supported in the browser - const showNotificationGrantBox = notifier.supported() && props.subscriptions?.length > 0 && !props.notificationsGranted; - const navListPadding = (showNotificationGrantBox || showNotificationBrowserNotSupportedBox || showNotificationContextNotSupportedBox) ? '0' : ''; - - return ( - <> - - - {showNotificationBrowserNotSupportedBox && } - {showNotificationContextNotSupportedBox && } - {showNotificationGrantBox && } - {!showSubscriptionsList && - navigate(routes.app)} selected={location.pathname === config.app_root}> - - - } - {showSubscriptionsList && - <> - {t("nav_topics_title")} - navigate(routes.app)} selected={location.pathname === config.app_root}> - - - - - - } - {session.exists() && - - - - - } - navigate(routes.settings)} selected={location.pathname === routes.settings}> - - - - openUrl("/docs")}> - - - - props.onPublishMessageClick()}> - - - - setSubscribeDialogOpen(true)}> - - - - {showUpgradeBanner && - - } - - - + const handleSubscribeSubmit = (subscription) => { + console.log( + `[Navigation] New subscription: ${subscription.id}`, + subscription ); + handleSubscribeReset(); + navigate(routes.forSubscription(subscription)); + handleRequestNotificationPermission(); + }; + + const handleRequestNotificationPermission = () => { + notifier.maybeRequestPermission((granted) => + props.onNotificationGranted(granted) + ); + }; + + const handleAccountClick = () => { + accountApi.sync(); // Dangle! + navigate(routes.account); + }; + + const isAdmin = account?.role === Role.ADMIN; + const isPaid = account?.billing?.subscription; + const showUpgradeBanner = config.enable_payments && !isAdmin && !isPaid; + const showSubscriptionsList = props.subscriptions?.length > 0; + const showNotificationBrowserNotSupportedBox = !notifier.browserSupported(); + const showNotificationContextNotSupportedBox = + notifier.browserSupported() && !notifier.contextSupported(); // Only show if notifications are generally supported in the browser + const showNotificationGrantBox = + notifier.supported() && + props.subscriptions?.length > 0 && + !props.notificationsGranted; + const navListPadding = + showNotificationGrantBox || + showNotificationBrowserNotSupportedBox || + showNotificationContextNotSupportedBox + ? "0" + : ""; + + return ( + <> + + + {showNotificationBrowserNotSupportedBox && ( + + )} + {showNotificationContextNotSupportedBox && ( + + )} + {showNotificationGrantBox && ( + + )} + {!showSubscriptionsList && ( + navigate(routes.app)} + selected={location.pathname === config.app_root} + > + + + + + + )} + {showSubscriptionsList && ( + <> + {t("nav_topics_title")} + navigate(routes.app)} + selected={location.pathname === config.app_root} + > + + + + + + + + + )} + {session.exists() && ( + + + + + + + )} + navigate(routes.settings)} + selected={location.pathname === routes.settings} + > + + + + + + openUrl("/docs")}> + + + + + + props.onPublishMessageClick()}> + + + + + + setSubscribeDialogOpen(true)}> + + + + + + {showUpgradeBanner && } + + + + ); }; const UpgradeBanner = () => { - const { t } = useTranslation(); - const [dialogKey, setDialogKey] = useState(0); - const [dialogOpen, setDialogOpen] = useState(false); + const { t } = useTranslation(); + const [dialogKey, setDialogKey] = useState(0); + const [dialogOpen, setDialogOpen] = useState(false); - const handleClick = () => { - setDialogKey(k => k + 1); - setDialogOpen(true); - }; + const handleClick = () => { + setDialogKey((k) => k + 1); + setDialogOpen(true); + }; - return ( - - - - - - - setDialogOpen(false)} - /> - - ); + return ( + + + + + + + + + setDialogOpen(false)} + /> + + ); }; const SubscriptionList = (props) => { - const sortedSubscriptions = props.subscriptions - .filter(s => !s.internal) - .sort((a, b) => { - return (topicUrl(a.baseUrl, a.topic) < topicUrl(b.baseUrl, b.topic)) ? -1 : 1; - }); - return ( - <> - {sortedSubscriptions.map(subscription => - )} - - ); -} + const sortedSubscriptions = props.subscriptions + .filter((s) => !s.internal) + .sort((a, b) => { + return topicUrl(a.baseUrl, a.topic) < topicUrl(b.baseUrl, b.topic) + ? -1 + : 1; + }); + return ( + <> + {sortedSubscriptions.map((subscription) => ( + + ))} + + ); +}; const SubscriptionItem = (props) => { - const { t } = useTranslation(); - const navigate = useNavigate(); - const [menuAnchorEl, setMenuAnchorEl] = useState(null); + const { t } = useTranslation(); + const navigate = useNavigate(); + const [menuAnchorEl, setMenuAnchorEl] = useState(null); - const subscription = props.subscription; - const iconBadge = (subscription.new <= 99) ? subscription.new : "99+"; - const displayName = topicDisplayName(subscription); - const ariaLabel = (subscription.state === ConnectionState.Connecting) - ? `${displayName} (${t("nav_button_connecting")})` - : displayName; - const icon = (subscription.state === ConnectionState.Connecting) - ? - : ; - - const handleClick = async () => { - navigate(routes.forSubscription(subscription)); - await subscriptionManager.markNotificationsRead(subscription.id); - }; - - return ( - <> - - {icon} - - {subscription.reservation?.everyone && - - {subscription.reservation?.everyone === Permission.READ_WRITE && - - } - {subscription.reservation?.everyone === Permission.READ_ONLY && - - } - {subscription.reservation?.everyone === Permission.WRITE_ONLY && - - } - {subscription.reservation?.everyone === Permission.DENY_ALL && - - } - - } - {subscription.mutedUntil > 0 && - - - - } - - e.stopPropagation()} - onClick={(e) => { - e.stopPropagation(); - setMenuAnchorEl(e.currentTarget); - }} - > - - - - - - setMenuAnchorEl(null)} - /> - - + const subscription = props.subscription; + const iconBadge = subscription.new <= 99 ? subscription.new : "99+"; + const displayName = topicDisplayName(subscription); + const ariaLabel = + subscription.state === ConnectionState.Connecting + ? `${displayName} (${t("nav_button_connecting")})` + : displayName; + const icon = + subscription.state === ConnectionState.Connecting ? ( + + ) : ( + + + ); + + const handleClick = async () => { + navigate(routes.forSubscription(subscription)); + await subscriptionManager.markNotificationsRead(subscription.id); + }; + + return ( + <> + + {icon} + + {subscription.reservation?.everyone && ( + + {subscription.reservation?.everyone === Permission.READ_WRITE && ( + + + + )} + {subscription.reservation?.everyone === Permission.READ_ONLY && ( + + + + )} + {subscription.reservation?.everyone === Permission.WRITE_ONLY && ( + + + + )} + {subscription.reservation?.everyone === Permission.DENY_ALL && ( + + + + )} + + )} + {subscription.mutedUntil > 0 && ( + + + + + + )} + + e.stopPropagation()} + onClick={(e) => { + e.stopPropagation(); + setMenuAnchorEl(e.currentTarget); + }} + > + + + + + + setMenuAnchorEl(null)} + /> + + + ); }; const NotificationGrantAlert = (props) => { - const { t } = useTranslation(); - return ( - <> - - {t("alert_grant_title")} - {t("alert_grant_description")} - - - - - ); + const { t } = useTranslation(); + return ( + <> + + {t("alert_grant_title")} + {t("alert_grant_description")} + + + + + ); }; const NotificationBrowserNotSupportedAlert = () => { - const { t } = useTranslation(); - return ( - <> - - {t("alert_not_supported_title")} - {t("alert_not_supported_description")} - - - - ); + const { t } = useTranslation(); + return ( + <> + + {t("alert_not_supported_title")} + + {t("alert_not_supported_description")} + + + + + ); }; const NotificationContextNotSupportedAlert = () => { - const { t } = useTranslation(); - return ( - <> - - {t("alert_not_supported_title")} - - - }} - /> - - - - - ); + const { t } = useTranslation(); + return ( + <> + + {t("alert_not_supported_title")} + + + ), + }} + /> + + + + + ); }; export default Navigation; diff --git a/web/src/components/Notifications.js b/web/src/components/Notifications.js index 10bcad81..e55674b3 100644 --- a/web/src/components/Notifications.js +++ b/web/src/components/Notifications.js @@ -1,36 +1,40 @@ import Container from "@mui/material/Container"; import { - ButtonBase, - CardActions, - CardContent, - CircularProgress, - Fade, - Link, - Modal, - Snackbar, - Stack, - Tooltip + ButtonBase, + CardActions, + CardContent, + CircularProgress, + Fade, + Link, + Modal, + Snackbar, + Stack, + Tooltip, } from "@mui/material"; import Card from "@mui/material/Card"; import Typography from "@mui/material/Typography"; import * as React from "react"; -import {useEffect, useState} from "react"; +import { useEffect, useState } from "react"; import { - formatBytes, - formatMessage, - formatShortDateTime, - formatTitle, - maybeAppendActionErrors, - openUrl, - shortUrl, - topicShortUrl, - unmatchedTags + formatBytes, + formatMessage, + formatShortDateTime, + formatTitle, + maybeAppendActionErrors, + openUrl, + shortUrl, + topicShortUrl, + unmatchedTags, } from "../app/utils"; import IconButton from "@mui/material/IconButton"; -import CheckIcon from '@mui/icons-material/Check'; -import CloseIcon from '@mui/icons-material/Close'; -import {LightboxBackdrop, Paragraph, VerticallyCenteredContainer} from "./styles"; -import {useLiveQuery} from "dexie-react-hooks"; +import CheckIcon from "@mui/icons-material/Check"; +import CloseIcon from "@mui/icons-material/Close"; +import { + LightboxBackdrop, + Paragraph, + VerticallyCenteredContainer, +} from "./styles"; +import { useLiveQuery } from "dexie-react-hooks"; import Box from "@mui/material/Box"; import Button from "@mui/material/Button"; import subscriptionManager from "../app/SubscriptionManager"; @@ -41,187 +45,263 @@ import priority4 from "../img/priority-4.svg"; import priority5 from "../img/priority-5.svg"; import logoOutline from "../img/ntfy-outline.svg"; import AttachmentIcon from "./AttachmentIcon"; -import {Trans, useTranslation} from "react-i18next"; -import {useOutletContext} from "react-router-dom"; -import {useAutoSubscribe} from "./hooks"; +import { Trans, useTranslation } from "react-i18next"; +import { useOutletContext } from "react-router-dom"; +import { useAutoSubscribe } from "./hooks"; export const AllSubscriptions = () => { - const { subscriptions } = useOutletContext(); - if (!subscriptions) { - return ; - } - return ; + const { subscriptions } = useOutletContext(); + if (!subscriptions) { + return ; + } + return ; }; export const SingleSubscription = () => { - const { subscriptions, selected } = useOutletContext(); - useAutoSubscribe(subscriptions, selected); - if (!selected) { - return ; - } - return ; + const { subscriptions, selected } = useOutletContext(); + useAutoSubscribe(subscriptions, selected); + if (!selected) { + return ; + } + return ; }; const AllSubscriptionsList = (props) => { - const subscriptions = props.subscriptions; - const notifications = useLiveQuery(() => subscriptionManager.getAllNotifications(), []); - if (notifications === null || notifications === undefined) { - return ; - } else if (subscriptions.length === 0) { - return ; - } else if (notifications.length === 0) { - return ; - } - return ; -} + const subscriptions = props.subscriptions; + const notifications = useLiveQuery( + () => subscriptionManager.getAllNotifications(), + [] + ); + if (notifications === null || notifications === undefined) { + return ; + } else if (subscriptions.length === 0) { + return ; + } else if (notifications.length === 0) { + return ; + } + return ( + + ); +}; const SingleSubscriptionList = (props) => { - const subscription = props.subscription; - const notifications = useLiveQuery(() => subscriptionManager.getNotifications(subscription.id), [subscription]); - if (notifications === null || notifications === undefined) { - return ; - } else if (notifications.length === 0) { - return ; - } - return ; -} + const subscription = props.subscription; + const notifications = useLiveQuery( + () => subscriptionManager.getNotifications(subscription.id), + [subscription] + ); + if (notifications === null || notifications === undefined) { + return ; + } else if (notifications.length === 0) { + return ; + } + return ( + + ); +}; const NotificationList = (props) => { - const { t } = useTranslation(); - const pageSize = 20; - const notifications = props.notifications; - const [snackOpen, setSnackOpen] = useState(false); - const [maxCount, setMaxCount] = useState(pageSize); - const count = Math.min(notifications.length, maxCount); + const { t } = useTranslation(); + const pageSize = 20; + const notifications = props.notifications; + const [snackOpen, setSnackOpen] = useState(false); + const [maxCount, setMaxCount] = useState(pageSize); + const count = Math.min(notifications.length, maxCount); - useEffect(() => { - return () => { - setMaxCount(pageSize); - const main = document.getElementById("main"); - if (main) { - main.scrollTo(0, 0); - } - } - }, [props.id]); + useEffect(() => { + return () => { + setMaxCount(pageSize); + const main = document.getElementById("main"); + if (main) { + main.scrollTo(0, 0); + } + }; + }, [props.id]); - return ( - setMaxCount(prev => prev + pageSize)} - hasMore={count < notifications.length} - loader={<>Loading ...} - scrollThreshold={0.7} - scrollableTarget="main" - > - - - {notifications.slice(0, count).map(notification => - setSnackOpen(true)} - />)} - setSnackOpen(false)} - message={t("notifications_copied_to_clipboard")} - /> - - - - ); -} + return ( + setMaxCount((prev) => prev + pageSize)} + hasMore={count < notifications.length} + loader={<>Loading ...} + scrollThreshold={0.7} + scrollableTarget="main" + > + + + {notifications.slice(0, count).map((notification) => ( + setSnackOpen(true)} + /> + ))} + setSnackOpen(false)} + message={t("notifications_copied_to_clipboard")} + /> + + + + ); +}; const NotificationItem = (props) => { - const { t } = useTranslation(); - const notification = props.notification; - const attachment = notification.attachment; - const date = formatShortDateTime(notification.time); - const otherTags = unmatchedTags(notification.tags); - const tags = (otherTags.length > 0) ? otherTags.join(', ') : null; - const handleDelete = async () => { - console.log(`[Notifications] Deleting notification ${notification.id}`); - await subscriptionManager.deleteNotification(notification.id) - } - const handleMarkRead = async () => { - console.log(`[Notifications] Marking notification ${notification.id} as read`); - await subscriptionManager.markNotificationRead(notification.id) - } - const handleCopy = (s) => { - navigator.clipboard.writeText(s); - props.onShowSnack(); - }; - const expired = attachment && attachment.expires && attachment.expires < Date.now()/1000; - const hasAttachmentActions = attachment && !expired; - const hasClickAction = notification.click; - const hasUserActions = notification.actions && notification.actions.length > 0; - const showActions = hasAttachmentActions || hasClickAction || hasUserActions; - return ( - - - - - - - - {notification.new === 1 && - - - - - } - - {date} - {[1,2,4,5].includes(notification.priority) && - {t("notifications_priority_x",} - {notification.new === 1 && - - - } - - {notification.title && {formatTitle(notification)}} - - {autolink(maybeAppendActionErrors(formatMessage(notification), notification))} - - {attachment && } - {tags && {t("notifications_tags")}: {tags}} - - {showActions && - - {hasAttachmentActions && <> - - - - - - - } - {hasClickAction && <> - - - - - - - } - {hasUserActions && } - } - + const { t } = useTranslation(); + const notification = props.notification; + const attachment = notification.attachment; + const date = formatShortDateTime(notification.time); + const otherTags = unmatchedTags(notification.tags); + const tags = otherTags.length > 0 ? otherTags.join(", ") : null; + const handleDelete = async () => { + console.log(`[Notifications] Deleting notification ${notification.id}`); + await subscriptionManager.deleteNotification(notification.id); + }; + const handleMarkRead = async () => { + console.log( + `[Notifications] Marking notification ${notification.id} as read` ); -} + await subscriptionManager.markNotificationRead(notification.id); + }; + const handleCopy = (s) => { + navigator.clipboard.writeText(s); + props.onShowSnack(); + }; + const expired = + attachment && attachment.expires && attachment.expires < Date.now() / 1000; + const hasAttachmentActions = attachment && !expired; + const hasClickAction = notification.click; + const hasUserActions = + notification.actions && notification.actions.length > 0; + const showActions = hasAttachmentActions || hasClickAction || hasUserActions; + return ( + + + + + + + + {notification.new === 1 && ( + + + + + + )} + + {date} + {[1, 2, 4, 5].includes(notification.priority) && ( + {t("notifications_priority_x", + )} + {notification.new === 1 && ( + + + + )} + + {notification.title && ( + + {formatTitle(notification)} + + )} + + {autolink( + maybeAppendActionErrors(formatMessage(notification), notification) + )} + + {attachment && } + {tags && ( + + {t("notifications_tags")}: {tags} + + )} + + {showActions && ( + + {hasAttachmentActions && ( + <> + + + + + + + + )} + {hasClickAction && ( + <> + + + + + + + + )} + {hasUserActions && } + + )} + + ); +}; /** * Replace links with components; this is a combination of the genius function @@ -231,318 +311,415 @@ const NotificationItem = (props) => { * [2] https://github.com/bryanwoods/autolink-js/blob/master/autolink.js#L9 */ const autolink = (s) => { - const parts = s.split(/(\bhttps?:\/\/[\-A-Z0-9+\u0026\u2019@#\/%?=()~_|!:,.;]*[\-A-Z0-9+\u0026@#\/%=~()_|]\b)/gi); - for (let i = 1; i < parts.length; i += 2) { - parts[i] = {shortUrl(parts[i])}; - } - return <>{parts}; + const parts = s.split( + /(\bhttps?:\/\/[\-A-Z0-9+\u0026\u2019@#\/%?=()~_|!:,.;]*[\-A-Z0-9+\u0026@#\/%=~()_|]\b)/gi + ); + for (let i = 1; i < parts.length; i += 2) { + parts[i] = ( + + {shortUrl(parts[i])} + + ); + } + return <>{parts}; }; const priorityFiles = { - 1: priority1, - 2: priority2, - 4: priority4, - 5: priority5 + 1: priority1, + 2: priority2, + 4: priority4, + 5: priority5, }; const Attachment = (props) => { - const { t } = useTranslation(); - const attachment = props.attachment; - const expired = attachment.expires && attachment.expires < Date.now()/1000; - const expires = attachment.expires && attachment.expires > Date.now()/1000; - const displayableImage = !expired && attachment.type && attachment.type.startsWith("image/"); + const { t } = useTranslation(); + const attachment = props.attachment; + const expired = attachment.expires && attachment.expires < Date.now() / 1000; + const expires = attachment.expires && attachment.expires > Date.now() / 1000; + const displayableImage = + !expired && attachment.type && attachment.type.startsWith("image/"); - // Unexpired image - if (displayableImage) { - return ; - } + // Unexpired image + if (displayableImage) { + return ; + } - // Anything else: Show box - const infos = []; - if (attachment.size) { - infos.push(formatBytes(attachment.size)); - } - if (expires) { - infos.push(t("notifications_attachment_link_expires", { date: formatShortDateTime(attachment.expires) })); - } - if (expired) { - infos.push(t("notifications_attachment_link_expired")); - } - const maybeInfoText = (infos.length > 0) ? <>
{infos.join(", ")} : null; - - // If expired, just show infos without click target - if (expired) { - return ( - - - - {attachment.name} - {maybeInfoText} - - - ); - } - - // Not expired - return ( - - - - - {attachment.name} - {maybeInfoText} - - - + // Anything else: Show box + const infos = []; + if (attachment.size) { + infos.push(formatBytes(attachment.size)); + } + if (expires) { + infos.push( + t("notifications_attachment_link_expires", { + date: formatShortDateTime(attachment.expires), + }) ); + } + if (expired) { + infos.push(t("notifications_attachment_link_expired")); + } + const maybeInfoText = + infos.length > 0 ? ( + <> +
+ {infos.join(", ")} + + ) : null; + + // If expired, just show infos without click target + if (expired) { + return ( + + + + {attachment.name} + {maybeInfoText} + + + ); + } + + // Not expired + return ( + + + + + {attachment.name} + {maybeInfoText} + + + + ); }; const Image = (props) => { - const { t } = useTranslation(); - const [open, setOpen] = useState(false); - return ( - <> - setOpen(true)} - sx={{ - marginTop: 2, - borderRadius: '4px', - boxShadow: 2, - width: 1, - maxHeight: '400px', - objectFit: 'cover', - cursor: 'pointer' - }} - /> - setOpen(false)} - BackdropComponent={LightboxBackdrop} - > - - - - - - ); -} + const { t } = useTranslation(); + const [open, setOpen] = useState(false); + return ( + <> + setOpen(true)} + sx={{ + marginTop: 2, + borderRadius: "4px", + boxShadow: 2, + width: 1, + maxHeight: "400px", + objectFit: "cover", + cursor: "pointer", + }} + /> + setOpen(false)} + BackdropComponent={LightboxBackdrop} + > + + + + + + ); +}; const UserActions = (props) => { - return ( - <>{props.notification.actions.map(action => - )} - ); + return ( + <> + {props.notification.actions.map((action) => ( + + ))} + + ); }; const UserAction = (props) => { - const { t } = useTranslation(); - const notification = props.notification; - const action = props.action; - if (action.action === "broadcast") { - return ( - - - - ); - } else if (action.action === "view") { - return ( - - - - ); - } else if (action.action === "http") { - const method = action.method ?? "POST"; - const label = action.label + (ACTION_LABEL_SUFFIX[action.progress ?? 0] ?? ""); - return ( - - - - ); - } - return null; // Others + const { t } = useTranslation(); + const notification = props.notification; + const action = props.action; + if (action.action === "broadcast") { + return ( + + + + + + ); + } else if (action.action === "view") { + return ( + + + + ); + } else if (action.action === "http") { + const method = action.method ?? "POST"; + const label = + action.label + (ACTION_LABEL_SUFFIX[action.progress ?? 0] ?? ""); + return ( + + + + ); + } + return null; // Others }; const performHttpAction = async (notification, action) => { - console.log(`[Notifications] Performing HTTP user action`, action); - try { - updateActionStatus(notification, action, ACTION_PROGRESS_ONGOING, null); - const response = await fetch(action.url, { - method: action.method ?? "POST", - headers: action.headers ?? {}, - // This must not null-coalesce to a non nullish value. Otherwise, the fetch API - // will reject it for "having a body" - body: action.body - }); - console.log(`[Notifications] HTTP user action response`, response); - const success = response.status >= 200 && response.status <= 299; - if (success) { - updateActionStatus(notification, action, ACTION_PROGRESS_SUCCESS, null); - } else { - updateActionStatus(notification, action, ACTION_PROGRESS_FAILED, `${action.label}: Unexpected response HTTP ${response.status}`); - } - } catch (e) { - console.log(`[Notifications] HTTP action failed`, e); - updateActionStatus(notification, action, ACTION_PROGRESS_FAILED, `${action.label}: ${e} Check developer console for details.`); + console.log(`[Notifications] Performing HTTP user action`, action); + try { + updateActionStatus(notification, action, ACTION_PROGRESS_ONGOING, null); + const response = await fetch(action.url, { + method: action.method ?? "POST", + headers: action.headers ?? {}, + // This must not null-coalesce to a non nullish value. Otherwise, the fetch API + // will reject it for "having a body" + body: action.body, + }); + console.log(`[Notifications] HTTP user action response`, response); + const success = response.status >= 200 && response.status <= 299; + if (success) { + updateActionStatus(notification, action, ACTION_PROGRESS_SUCCESS, null); + } else { + updateActionStatus( + notification, + action, + ACTION_PROGRESS_FAILED, + `${action.label}: Unexpected response HTTP ${response.status}` + ); } + } catch (e) { + console.log(`[Notifications] HTTP action failed`, e); + updateActionStatus( + notification, + action, + ACTION_PROGRESS_FAILED, + `${action.label}: ${e} Check developer console for details.` + ); + } }; const updateActionStatus = (notification, action, progress, error) => { - notification.actions = notification.actions.map(a => { - if (a.id !== action.id) { - return a; - } - return { ...a, progress: progress, error: error }; - }); - subscriptionManager.updateNotification(notification); -} + notification.actions = notification.actions.map((a) => { + if (a.id !== action.id) { + return a; + } + return { ...a, progress: progress, error: error }; + }); + subscriptionManager.updateNotification(notification); +}; const ACTION_PROGRESS_ONGOING = 1; const ACTION_PROGRESS_SUCCESS = 2; const ACTION_PROGRESS_FAILED = 3; const ACTION_LABEL_SUFFIX = { - [ACTION_PROGRESS_ONGOING]: " …", - [ACTION_PROGRESS_SUCCESS]: " ✔", - [ACTION_PROGRESS_FAILED]: " ❌" + [ACTION_PROGRESS_ONGOING]: " …", + [ACTION_PROGRESS_SUCCESS]: " ✔", + [ACTION_PROGRESS_FAILED]: " ❌", }; const NoNotifications = (props) => { - const { t } = useTranslation(); - const shortUrl = topicShortUrl(props.subscription.baseUrl, props.subscription.topic); - return ( - - - {t("action_bar_logo_alt")}/
- {t("notifications_none_for_topic_title")} -
- - {t("notifications_none_for_topic_description")} - - - {t("notifications_example")}:
- - $ curl -d "Hi" {shortUrl} - -
- - - -
- ); + const { t } = useTranslation(); + const shortUrl = topicShortUrl( + props.subscription.baseUrl, + props.subscription.topic + ); + return ( + + + {t("action_bar_logo_alt")} +
+ {t("notifications_none_for_topic_title")} +
+ {t("notifications_none_for_topic_description")} + + {t("notifications_example")}:
+ $ curl -d "Hi" {shortUrl} +
+ + + +
+ ); }; const NoNotificationsWithoutSubscription = (props) => { - const { t } = useTranslation(); - const subscription = props.subscriptions[0]; - const shortUrl = topicShortUrl(subscription.baseUrl, subscription.topic); - return ( - - - {t("action_bar_logo_alt")}/
- {t("notifications_none_for_any_title")} -
- - {t("notifications_none_for_any_description")} - - - {t("notifications_example")}:
- - $ curl -d "Hi" {shortUrl} - -
- - - -
- ); + const { t } = useTranslation(); + const subscription = props.subscriptions[0]; + const shortUrl = topicShortUrl(subscription.baseUrl, subscription.topic); + return ( + + + {t("action_bar_logo_alt")} +
+ {t("notifications_none_for_any_title")} +
+ {t("notifications_none_for_any_description")} + + {t("notifications_example")}:
+ $ curl -d "Hi" {shortUrl} +
+ + + +
+ ); }; const NoSubscriptions = () => { - const { t } = useTranslation(); - return ( - - - {t("action_bar_logo_alt")}/
- {t("notifications_no_subscriptions_title")} -
- - {t("notifications_no_subscriptions_description", { - linktext: t("nav_button_subscribe") - })} - - - - -
- ); + const { t } = useTranslation(); + return ( + + + {t("action_bar_logo_alt")} +
+ {t("notifications_no_subscriptions_title")} +
+ + {t("notifications_no_subscriptions_description", { + linktext: t("nav_button_subscribe"), + })} + + + + +
+ ); }; const ForMoreDetails = () => { - return ( - , - docsLink: - }} - /> - ); + return ( + + ), + docsLink: ( + + ), + }} + /> + ); }; const Loading = () => { - const { t } = useTranslation(); - return ( - - -
- {t("notifications_loading")} -
-
- ); + const { t } = useTranslation(); + return ( + + + +
+ {t("notifications_loading")} +
+
+ ); }; diff --git a/web/src/components/PopupMenu.js b/web/src/components/PopupMenu.js index 4d22398b..501f86a8 100644 --- a/web/src/components/PopupMenu.js +++ b/web/src/components/PopupMenu.js @@ -1,48 +1,48 @@ -import {Fade, Menu} from "@mui/material"; +import { Fade, Menu } from "@mui/material"; import * as React from "react"; const PopupMenu = (props) => { - const horizontal = props.horizontal ?? "left"; - const arrow = (horizontal === "right") ? { right: 19 } : { left: 19 }; - return ( - - {props.children} - - ); + const horizontal = props.horizontal ?? "left"; + const arrow = horizontal === "right" ? { right: 19 } : { left: 19 }; + return ( + + {props.children} + + ); }; export default PopupMenu; diff --git a/web/src/components/Pref.js b/web/src/components/Pref.js index 622d9bbf..07052f63 100644 --- a/web/src/components/Pref.js +++ b/web/src/components/Pref.js @@ -1,51 +1,54 @@ import * as React from "react"; export const PrefGroup = (props) => { - return ( -
- {props.children} -
- ) + return
{props.children}
; }; export const Pref = (props) => { - const justifyContent = (props.alignTop) ? "normal" : "center"; - return ( -
-
-
{props.title}{props.subtitle && ({props.subtitle})}
- {props.description &&
{props.description}
} -
-
- {props.children} -
+ const justifyContent = props.alignTop ? "normal" : "center"; + return ( +
+
+
+ {props.title} + {props.subtitle && ({props.subtitle})}
- ); + {props.description && ( +
+ {props.description} +
+ )} +
+
+ {props.children} +
+
+ ); }; diff --git a/web/src/components/Preferences.js b/web/src/components/Preferences.js index fc8cb35b..cbbf1a81 100644 --- a/web/src/components/Preferences.js +++ b/web/src/components/Preferences.js @@ -1,654 +1,832 @@ -import * as React from 'react'; -import {useContext, useEffect, useState} from 'react'; +import * as React from "react"; +import { useContext, useEffect, useState } from "react"; import { - Alert, - CardActions, - CardContent, - Chip, - FormControl, - Select, - Stack, - Table, - TableBody, - TableCell, - TableHead, - TableRow, - Tooltip, - useMediaQuery + Alert, + CardActions, + CardContent, + Chip, + FormControl, + Select, + Stack, + Table, + TableBody, + TableCell, + TableHead, + TableRow, + Tooltip, + useMediaQuery, } from "@mui/material"; import Typography from "@mui/material/Typography"; import prefs from "../app/Prefs"; -import {Paragraph} from "./styles"; -import EditIcon from '@mui/icons-material/Edit'; +import { Paragraph } from "./styles"; +import EditIcon from "@mui/icons-material/Edit"; import CloseIcon from "@mui/icons-material/Close"; import IconButton from "@mui/material/IconButton"; -import PlayArrowIcon from '@mui/icons-material/PlayArrow'; +import PlayArrowIcon from "@mui/icons-material/PlayArrow"; import Container from "@mui/material/Container"; import TextField from "@mui/material/TextField"; import MenuItem from "@mui/material/MenuItem"; import Card from "@mui/material/Card"; import Button from "@mui/material/Button"; -import {useLiveQuery} from "dexie-react-hooks"; +import { useLiveQuery } from "dexie-react-hooks"; import theme from "./theme"; import Dialog from "@mui/material/Dialog"; import DialogTitle from "@mui/material/DialogTitle"; import DialogContent from "@mui/material/DialogContent"; import DialogActions from "@mui/material/DialogActions"; import userManager from "../app/UserManager"; -import {playSound, shuffle, sounds, validUrl} from "../app/utils"; -import {useTranslation} from "react-i18next"; +import { playSound, shuffle, sounds, validUrl } from "../app/utils"; +import { useTranslation } from "react-i18next"; import session from "../app/Session"; import routes from "./routes"; -import accountApi, {Permission, Role} from "../app/AccountApi"; -import {Pref, PrefGroup} from "./Pref"; -import {Info} from "@mui/icons-material"; -import {AccountContext} from "./App"; -import {useOutletContext} from "react-router-dom"; -import {PermissionDenyAll, PermissionRead, PermissionReadWrite, PermissionWrite} from "./ReserveIcons"; -import {ReserveAddDialog, ReserveDeleteDialog, ReserveEditDialog} from "./ReserveDialogs"; -import {UnauthorizedError} from "../app/errors"; +import accountApi, { Permission, Role } from "../app/AccountApi"; +import { Pref, PrefGroup } from "./Pref"; +import { Info } from "@mui/icons-material"; +import { AccountContext } from "./App"; +import { useOutletContext } from "react-router-dom"; +import { + PermissionDenyAll, + PermissionRead, + PermissionReadWrite, + PermissionWrite, +} from "./ReserveIcons"; +import { + ReserveAddDialog, + ReserveDeleteDialog, + ReserveEditDialog, +} from "./ReserveDialogs"; +import { UnauthorizedError } from "../app/errors"; import subscriptionManager from "../app/SubscriptionManager"; -import {subscribeTopic} from "./SubscribeDialog"; +import { subscribeTopic } from "./SubscribeDialog"; const Preferences = () => { - return ( - - - - - - - - - ); + return ( + + + + + + + + + ); }; const Notifications = () => { - const { t } = useTranslation(); - return ( - - - {t("prefs_notifications_title")} - - - - - - - - ); + const { t } = useTranslation(); + return ( + + + {t("prefs_notifications_title")} + + + + + + + + ); }; const Sound = () => { - const { t } = useTranslation(); - const labelId = "prefSound"; - const sound = useLiveQuery(async () => prefs.sound()); - const handleChange = async (ev) => { - await prefs.setSound(ev.target.value); - await maybeUpdateAccountSettings({ - notification: { - sound: ev.target.value - } - }); - } - if (!sound) { - return null; // While loading - } - let description; - if (sound === "none") { - description = t("prefs_notifications_sound_description_none"); - } else { - description = t("prefs_notifications_sound_description_some", { sound: sounds[sound].label }); - } - return ( - -
- - - - playSound(sound)} disabled={sound === "none"} aria-label={t("prefs_notifications_sound_play")}> - - -
-
- ) + const { t } = useTranslation(); + const labelId = "prefSound"; + const sound = useLiveQuery(async () => prefs.sound()); + const handleChange = async (ev) => { + await prefs.setSound(ev.target.value); + await maybeUpdateAccountSettings({ + notification: { + sound: ev.target.value, + }, + }); + }; + if (!sound) { + return null; // While loading + } + let description; + if (sound === "none") { + description = t("prefs_notifications_sound_description_none"); + } else { + description = t("prefs_notifications_sound_description_some", { + sound: sounds[sound].label, + }); + } + return ( + +
+ + + + playSound(sound)} + disabled={sound === "none"} + aria-label={t("prefs_notifications_sound_play")} + > + + +
+
+ ); }; const MinPriority = () => { - const { t } = useTranslation(); - const labelId = "prefMinPriority"; - const minPriority = useLiveQuery(async () => prefs.minPriority()); - const handleChange = async (ev) => { - await prefs.setMinPriority(ev.target.value); - await maybeUpdateAccountSettings({ - notification: { - min_priority: ev.target.value - } - }); - } - if (!minPriority) { - return null; // While loading - } - const priorities = { - 1: t("priority_min"), - 2: t("priority_low"), - 3: t("priority_default"), - 4: t("priority_high"), - 5: t("priority_max") - } - let description; - if (minPriority === 1) { - description = t("prefs_notifications_min_priority_description_any"); - } else if (minPriority === 5) { - description = t("prefs_notifications_min_priority_description_max"); - } else { - description = t("prefs_notifications_min_priority_description_x_or_higher", { - number: minPriority, - name: priorities[minPriority] - }); - } - return ( - - - - - - ) + const { t } = useTranslation(); + const labelId = "prefMinPriority"; + const minPriority = useLiveQuery(async () => prefs.minPriority()); + const handleChange = async (ev) => { + await prefs.setMinPriority(ev.target.value); + await maybeUpdateAccountSettings({ + notification: { + min_priority: ev.target.value, + }, + }); + }; + if (!minPriority) { + return null; // While loading + } + const priorities = { + 1: t("priority_min"), + 2: t("priority_low"), + 3: t("priority_default"), + 4: t("priority_high"), + 5: t("priority_max"), + }; + let description; + if (minPriority === 1) { + description = t("prefs_notifications_min_priority_description_any"); + } else if (minPriority === 5) { + description = t("prefs_notifications_min_priority_description_max"); + } else { + description = t( + "prefs_notifications_min_priority_description_x_or_higher", + { + number: minPriority, + name: priorities[minPriority], + } + ); + } + return ( + + + + + + ); }; const DeleteAfter = () => { - const { t } = useTranslation(); - const labelId = "prefDeleteAfter"; - const deleteAfter = useLiveQuery(async () => prefs.deleteAfter()); - const handleChange = async (ev) => { - await prefs.setDeleteAfter(ev.target.value); - await maybeUpdateAccountSettings({ - notification: { - delete_after: ev.target.value - } - }); + const { t } = useTranslation(); + const labelId = "prefDeleteAfter"; + const deleteAfter = useLiveQuery(async () => prefs.deleteAfter()); + const handleChange = async (ev) => { + await prefs.setDeleteAfter(ev.target.value); + await maybeUpdateAccountSettings({ + notification: { + delete_after: ev.target.value, + }, + }); + }; + if (deleteAfter === null || deleteAfter === undefined) { + // !deleteAfter will not work with "0" + return null; // While loading + } + const description = (() => { + switch (deleteAfter) { + case 0: + return t("prefs_notifications_delete_after_never_description"); + case 10800: + return t("prefs_notifications_delete_after_three_hours_description"); + case 86400: + return t("prefs_notifications_delete_after_one_day_description"); + case 604800: + return t("prefs_notifications_delete_after_one_week_description"); + case 2592000: + return t("prefs_notifications_delete_after_one_month_description"); } - if (deleteAfter === null || deleteAfter === undefined) { // !deleteAfter will not work with "0" - return null; // While loading - } - const description = (() => { - switch (deleteAfter) { - case 0: return t("prefs_notifications_delete_after_never_description"); - case 10800: return t("prefs_notifications_delete_after_three_hours_description"); - case 86400: return t("prefs_notifications_delete_after_one_day_description"); - case 604800: return t("prefs_notifications_delete_after_one_week_description"); - case 2592000: return t("prefs_notifications_delete_after_one_month_description"); - } - })(); - return ( - - - - - - ) + })(); + return ( + + + + + + ); }; const Users = () => { - const { t } = useTranslation(); - const [dialogKey, setDialogKey] = useState(0); - const [dialogOpen, setDialogOpen] = useState(false); - const users = useLiveQuery(() => userManager.all()); - const handleAddClick = () => { - setDialogKey(prev => prev+1); - setDialogOpen(true); - }; - const handleDialogCancel = () => { - setDialogOpen(false); - }; - const handleDialogSubmit = async (user) => { - setDialogOpen(false); - try { - await userManager.save(user); - console.debug(`[Preferences] User ${user.username} for ${user.baseUrl} added`); - } catch (e) { - console.log(`[Preferences] Error adding user.`, e); - } - }; - return ( - - - - {t("prefs_users_title")} - - - {t("prefs_users_description")} - {session.exists() && <>{" " + t("prefs_users_description_no_sync")}} - - {users?.length > 0 && } - - - - - - - ); + const { t } = useTranslation(); + const [dialogKey, setDialogKey] = useState(0); + const [dialogOpen, setDialogOpen] = useState(false); + const users = useLiveQuery(() => userManager.all()); + const handleAddClick = () => { + setDialogKey((prev) => prev + 1); + setDialogOpen(true); + }; + const handleDialogCancel = () => { + setDialogOpen(false); + }; + const handleDialogSubmit = async (user) => { + setDialogOpen(false); + try { + await userManager.save(user); + console.debug( + `[Preferences] User ${user.username} for ${user.baseUrl} added` + ); + } catch (e) { + console.log(`[Preferences] Error adding user.`, e); + } + }; + return ( + + + + {t("prefs_users_title")} + + + {t("prefs_users_description")} + {session.exists() && ( + <>{" " + t("prefs_users_description_no_sync")} + )} + + {users?.length > 0 && } + + + + + + + ); }; const UserTable = (props) => { - const { t } = useTranslation(); - const [dialogKey, setDialogKey] = useState(0); - const [dialogOpen, setDialogOpen] = useState(false); - const [dialogUser, setDialogUser] = useState(null); + const { t } = useTranslation(); + const [dialogKey, setDialogKey] = useState(0); + const [dialogOpen, setDialogOpen] = useState(false); + const [dialogUser, setDialogUser] = useState(null); - const handleEditClick = (user) => { - setDialogKey(prev => prev+1); - setDialogUser(user); - setDialogOpen(true); - }; + const handleEditClick = (user) => { + setDialogKey((prev) => prev + 1); + setDialogUser(user); + setDialogOpen(true); + }; - const handleDialogCancel = () => { - setDialogOpen(false); - }; + const handleDialogCancel = () => { + setDialogOpen(false); + }; - const handleDialogSubmit = async (user) => { - setDialogOpen(false); - try { - await userManager.save(user); - console.debug(`[Preferences] User ${user.username} for ${user.baseUrl} updated`); - } catch (e) { - console.log(`[Preferences] Error updating user.`, e); - } - }; + const handleDialogSubmit = async (user) => { + setDialogOpen(false); + try { + await userManager.save(user); + console.debug( + `[Preferences] User ${user.username} for ${user.baseUrl} updated` + ); + } catch (e) { + console.log(`[Preferences] Error updating user.`, e); + } + }; - const handleDeleteClick = async (user) => { - try { - await userManager.delete(user.baseUrl); - console.debug(`[Preferences] User ${user.username} for ${user.baseUrl} deleted`); - } catch (e) { - console.error(`[Preferences] Error deleting user for ${user.baseUrl}`, e); - } - }; + const handleDeleteClick = async (user) => { + try { + await userManager.delete(user.baseUrl); + console.debug( + `[Preferences] User ${user.username} for ${user.baseUrl} deleted` + ); + } catch (e) { + console.error(`[Preferences] Error deleting user for ${user.baseUrl}`, e); + } + }; - return ( - - - - {t("prefs_users_table_user_header")} - {t("prefs_users_table_base_url_header")} - - - - - {props.users?.map(user => ( - - {user.username} - {user.baseUrl} - - {(!session.exists() || user.baseUrl !== config.base_url) && - <> - handleEditClick(user)} aria-label={t("prefs_users_edit_button")}> - - - handleDeleteClick(user)} aria-label={t("prefs_users_delete_button")}> - - - - } - {session.exists() && user.baseUrl === config.base_url && - - - - - - - } - - - ))} - - -
- ); + return ( + + + + + {t("prefs_users_table_user_header")} + + {t("prefs_users_table_base_url_header")} + + + + + {props.users?.map((user) => ( + + + {user.username} + + + {user.baseUrl} + + + {(!session.exists() || user.baseUrl !== config.base_url) && ( + <> + handleEditClick(user)} + aria-label={t("prefs_users_edit_button")} + > + + + handleDeleteClick(user)} + aria-label={t("prefs_users_delete_button")} + > + + + + )} + {session.exists() && user.baseUrl === config.base_url && ( + + + + + + + + + + + )} + + + ))} + + +
+ ); }; const UserDialog = (props) => { - const { t } = useTranslation(); - const [baseUrl, setBaseUrl] = useState(""); - const [username, setUsername] = useState(""); - const [password, setPassword] = useState(""); - const fullScreen = useMediaQuery(theme.breakpoints.down('sm')); - const editMode = props.user !== null; - const addButtonEnabled = (() => { - if (editMode) { - return username.length > 0 && password.length > 0; - } - const baseUrlValid = validUrl(baseUrl); - const baseUrlExists = props.users?.map(user => user.baseUrl).includes(baseUrl); - return baseUrlValid - && !baseUrlExists - && username.length > 0 - && password.length > 0; - })(); - const handleSubmit = async () => { - props.onSubmit({ - baseUrl: baseUrl, - username: username, - password: password - }) - }; - useEffect(() => { - if (editMode) { - setBaseUrl(props.user.baseUrl); - setUsername(props.user.username); - setPassword(props.user.password); - } - }, [editMode, props.user]); + const { t } = useTranslation(); + const [baseUrl, setBaseUrl] = useState(""); + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + const fullScreen = useMediaQuery(theme.breakpoints.down("sm")); + const editMode = props.user !== null; + const addButtonEnabled = (() => { + if (editMode) { + return username.length > 0 && password.length > 0; + } + const baseUrlValid = validUrl(baseUrl); + const baseUrlExists = props.users + ?.map((user) => user.baseUrl) + .includes(baseUrl); return ( - - {editMode ? t("prefs_users_dialog_title_edit") : t("prefs_users_dialog_title_add")} - - {!editMode && setBaseUrl(ev.target.value)} - type="url" - fullWidth - variant="standard" - />} - setUsername(ev.target.value)} - type="text" - fullWidth - variant="standard" - /> - setPassword(ev.target.value)} - fullWidth - variant="standard" - /> - - - - - - + baseUrlValid && + !baseUrlExists && + username.length > 0 && + password.length > 0 ); + })(); + const handleSubmit = async () => { + props.onSubmit({ + baseUrl: baseUrl, + username: username, + password: password, + }); + }; + useEffect(() => { + if (editMode) { + setBaseUrl(props.user.baseUrl); + setUsername(props.user.username); + setPassword(props.user.password); + } + }, [editMode, props.user]); + return ( + + + {editMode + ? t("prefs_users_dialog_title_edit") + : t("prefs_users_dialog_title_add")} + + + {!editMode && ( + setBaseUrl(ev.target.value)} + type="url" + fullWidth + variant="standard" + /> + )} + setUsername(ev.target.value)} + type="text" + fullWidth + variant="standard" + /> + setPassword(ev.target.value)} + fullWidth + variant="standard" + /> + + + + + + + ); }; const Appearance = () => { - const { t } = useTranslation(); - return ( - - - {t("prefs_appearance_title")} - - - - - - ); + const { t } = useTranslation(); + return ( + + + {t("prefs_appearance_title")} + + + + + + ); }; const Language = () => { - const { t, i18n } = useTranslation(); - const labelId = "prefLanguage"; - const lang = i18n.resolvedLanguage ?? "en"; + const { t, i18n } = useTranslation(); + const labelId = "prefLanguage"; + const lang = i18n.resolvedLanguage ?? "en"; - // Country flags are displayed using emoji. Emoji rendering is handled by platform fonts. - // Windows in particular does not yet play nicely with flag emoji so for now, hide flags on Windows. - const randomFlags = shuffle(["🇬🇧", "🇺🇸", "🇪🇸", "🇫🇷", "🇧🇬", "🇨🇿", "🇩🇪", "🇵🇱", "🇺🇦", "🇨🇳", "🇮🇹", "🇭🇺", "🇧🇷", "🇳🇱", "🇮🇩", "🇯🇵", "🇷🇺", "🇹🇷"]).slice(0, 3); - const showFlags = !navigator.userAgent.includes("Windows"); - let title = t("prefs_appearance_language_title"); - if (showFlags) { - title += " " + randomFlags.join(" "); - } + // Country flags are displayed using emoji. Emoji rendering is handled by platform fonts. + // Windows in particular does not yet play nicely with flag emoji so for now, hide flags on Windows. + const randomFlags = shuffle([ + "🇬🇧", + "🇺🇸", + "🇪🇸", + "🇫🇷", + "🇧🇬", + "🇨🇿", + "🇩🇪", + "🇵🇱", + "🇺🇦", + "🇨🇳", + "🇮🇹", + "🇭🇺", + "🇧🇷", + "🇳🇱", + "🇮🇩", + "🇯🇵", + "🇷🇺", + "🇹🇷", + ]).slice(0, 3); + const showFlags = !navigator.userAgent.includes("Windows"); + let title = t("prefs_appearance_language_title"); + if (showFlags) { + title += " " + randomFlags.join(" "); + } - const handleChange = async (ev) => { - await i18n.changeLanguage(ev.target.value); - await maybeUpdateAccountSettings({ - language: ev.target.value - }); - }; + const handleChange = async (ev) => { + await i18n.changeLanguage(ev.target.value); + await maybeUpdateAccountSettings({ + language: ev.target.value, + }); + }; - // Remember: Flags are not languages. Don't put flags next to the language in the list. - // Languages names from: https://www.omniglot.com/language/names.htm - // Better: Sidebar in Wikipedia: https://en.wikipedia.org/wiki/Bokm%C3%A5l + // Remember: Flags are not languages. Don't put flags next to the language in the list. + // Languages names from: https://www.omniglot.com/language/names.htm + // Better: Sidebar in Wikipedia: https://en.wikipedia.org/wiki/Bokm%C3%A5l - return ( - - - - - - ) + return ( + + + + + + ); }; const Reservations = () => { - const { t } = useTranslation(); - const { account } = useContext(AccountContext); - const [dialogKey, setDialogKey] = useState(0); - const [dialogOpen, setDialogOpen] = useState(false); + const { t } = useTranslation(); + const { account } = useContext(AccountContext); + const [dialogKey, setDialogKey] = useState(0); + const [dialogOpen, setDialogOpen] = useState(false); - if (!config.enable_reservations || !session.exists() || !account) { - return <>; - } - const reservations = account.reservations || []; - const limitReached = account.role === Role.USER && account.stats.reservations_remaining === 0; + if (!config.enable_reservations || !session.exists() || !account) { + return <>; + } + const reservations = account.reservations || []; + const limitReached = + account.role === Role.USER && account.stats.reservations_remaining === 0; - const handleAddClick = () => { - setDialogKey(prev => prev+1); - setDialogOpen(true); - }; + const handleAddClick = () => { + setDialogKey((prev) => prev + 1); + setDialogOpen(true); + }; - return ( - - - - {t("prefs_reservations_title")} - - - {t("prefs_reservations_description")} - - {reservations.length > 0 && } - {limitReached && {t("prefs_reservations_limit_reached")}} - - - - setDialogOpen(false)} - /> - - - ); + return ( + + + + {t("prefs_reservations_title")} + + {t("prefs_reservations_description")} + {reservations.length > 0 && ( + + )} + {limitReached && ( + {t("prefs_reservations_limit_reached")} + )} + + + + setDialogOpen(false)} + /> + + + ); }; const ReservationsTable = (props) => { - const { t } = useTranslation(); - const [dialogKey, setDialogKey] = useState(0); - const [dialogReservation, setDialogReservation] = useState(null); - const [editDialogOpen, setEditDialogOpen] = useState(false); - const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); - const { subscriptions } = useOutletContext(); - const localSubscriptions = (subscriptions?.length > 0) - ? Object.assign({}, ...subscriptions.filter(s => s.baseUrl === config.base_url).map(s => ({[s.topic]: s}))) - : {}; + const { t } = useTranslation(); + const [dialogKey, setDialogKey] = useState(0); + const [dialogReservation, setDialogReservation] = useState(null); + const [editDialogOpen, setEditDialogOpen] = useState(false); + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const { subscriptions } = useOutletContext(); + const localSubscriptions = + subscriptions?.length > 0 + ? Object.assign( + {}, + ...subscriptions + .filter((s) => s.baseUrl === config.base_url) + .map((s) => ({ [s.topic]: s })) + ) + : {}; - const handleEditClick = (reservation) => { - setDialogKey(prev => prev+1); - setDialogReservation(reservation); - setEditDialogOpen(true); - }; + const handleEditClick = (reservation) => { + setDialogKey((prev) => prev + 1); + setDialogReservation(reservation); + setEditDialogOpen(true); + }; - const handleDeleteClick = async (reservation) => { - setDialogKey(prev => prev+1); - setDialogReservation(reservation); - setDeleteDialogOpen(true); - }; + const handleDeleteClick = async (reservation) => { + setDialogKey((prev) => prev + 1); + setDialogReservation(reservation); + setDeleteDialogOpen(true); + }; - const handleSubscribeClick = async (reservation) => { - await subscribeTopic(config.base_url, reservation.topic); - }; + const handleSubscribeClick = async (reservation) => { + await subscribeTopic(config.base_url, reservation.topic); + }; - return ( - - - - {t("prefs_reservations_table_topic_header")} - {t("prefs_reservations_table_access_header")} - - - - - {props.reservations.map(reservation => ( - - - {reservation.topic} - - - {reservation.everyone === Permission.READ_WRITE && - <> - - {t("prefs_reservations_table_everyone_read_write")} - - } - {reservation.everyone === Permission.READ_ONLY && - <> - - {t("prefs_reservations_table_everyone_read_only")} - - } - {reservation.everyone === Permission.WRITE_ONLY && - <> - - {t("prefs_reservations_table_everyone_write_only")} - - } - {reservation.everyone === Permission.DENY_ALL && - <> - - {t("prefs_reservations_table_everyone_deny_all")} - - } - - - {!localSubscriptions[reservation.topic] && - - } onClick={() => handleSubscribeClick(reservation)} label={t("prefs_reservations_table_not_subscribed")} color="primary" variant="outlined"/> - - } - handleEditClick(reservation)} aria-label={t("prefs_reservations_edit_button")}> - - - handleDeleteClick(reservation)} aria-label={t("prefs_reservations_delete_button")}> - - - - - ))} - - setEditDialogOpen(false)} - /> - setDeleteDialogOpen(false)} - /> -
- ); + return ( + + + + + {t("prefs_reservations_table_topic_header")} + + {t("prefs_reservations_table_access_header")} + + + + + {props.reservations.map((reservation) => ( + + + {reservation.topic} + + + {reservation.everyone === Permission.READ_WRITE && ( + <> + + {t("prefs_reservations_table_everyone_read_write")} + + )} + {reservation.everyone === Permission.READ_ONLY && ( + <> + + {t("prefs_reservations_table_everyone_read_only")} + + )} + {reservation.everyone === Permission.WRITE_ONLY && ( + <> + + {t("prefs_reservations_table_everyone_write_only")} + + )} + {reservation.everyone === Permission.DENY_ALL && ( + <> + + {t("prefs_reservations_table_everyone_deny_all")} + + )} + + + {!localSubscriptions[reservation.topic] && ( + + } + onClick={() => handleSubscribeClick(reservation)} + label={t("prefs_reservations_table_not_subscribed")} + color="primary" + variant="outlined" + /> + + )} + handleEditClick(reservation)} + aria-label={t("prefs_reservations_edit_button")} + > + + + handleDeleteClick(reservation)} + aria-label={t("prefs_reservations_delete_button")} + > + + + + + ))} + + setEditDialogOpen(false)} + /> + setDeleteDialogOpen(false)} + /> +
+ ); }; const maybeUpdateAccountSettings = async (payload) => { - if (!session.exists()) { - return; - } - try { - await accountApi.updateSettings(payload); - } catch (e) { - console.log(`[Preferences] Error updating account settings`, e); - if (e instanceof UnauthorizedError) { - session.resetAndRedirect(routes.login); - } + if (!session.exists()) { + return; + } + try { + await accountApi.updateSettings(payload); + } catch (e) { + console.log(`[Preferences] Error updating account settings`, e); + if (e instanceof UnauthorizedError) { + session.resetAndRedirect(routes.login); } + } }; export default Preferences; diff --git a/web/src/components/PublishDialog.js b/web/src/components/PublishDialog.js index bfaccfc5..e8825de2 100644 --- a/web/src/components/PublishDialog.js +++ b/web/src/components/PublishDialog.js @@ -1,16 +1,16 @@ -import * as React from 'react'; -import {useContext, useEffect, useRef, useState} from 'react'; +import * as React from "react"; +import { useContext, useEffect, useRef, useState } from "react"; import theme from "./theme"; import { - Checkbox, - Chip, - FormControl, - FormControlLabel, - InputLabel, - Link, - Select, - Tooltip, - useMediaQuery + Checkbox, + Chip, + FormControl, + FormControlLabel, + InputLabel, + Link, + Select, + Tooltip, + useMediaQuery, } from "@mui/material"; import TextField from "@mui/material/TextField"; import priority1 from "../img/priority-1.svg"; @@ -24,764 +24,972 @@ import DialogContent from "@mui/material/DialogContent"; import Button from "@mui/material/Button"; import Typography from "@mui/material/Typography"; import IconButton from "@mui/material/IconButton"; -import InsertEmoticonIcon from '@mui/icons-material/InsertEmoticon'; -import {Close} from "@mui/icons-material"; +import InsertEmoticonIcon from "@mui/icons-material/InsertEmoticon"; +import { Close } from "@mui/icons-material"; import MenuItem from "@mui/material/MenuItem"; -import {formatBytes, maybeWithAuth, topicShortUrl, topicUrl, validTopic, validUrl} from "../app/utils"; +import { + formatBytes, + maybeWithAuth, + topicShortUrl, + topicUrl, + validTopic, + validUrl, +} from "../app/utils"; import Box from "@mui/material/Box"; import AttachmentIcon from "./AttachmentIcon"; import DialogFooter from "./DialogFooter"; import api from "../app/Api"; import userManager from "../app/UserManager"; import EmojiPicker from "./EmojiPicker"; -import {Trans, useTranslation} from "react-i18next"; +import { Trans, useTranslation } from "react-i18next"; import session from "../app/Session"; import routes from "./routes"; import accountApi from "../app/AccountApi"; -import {UnauthorizedError} from "../app/errors"; -import {AccountContext} from "./App"; +import { UnauthorizedError } from "../app/errors"; +import { AccountContext } from "./App"; const PublishDialog = (props) => { - const { t } = useTranslation(); - const { account } = useContext(AccountContext); - const [baseUrl, setBaseUrl] = useState(""); - const [topic, setTopic] = useState(""); - const [message, setMessage] = useState(""); - const [messageFocused, setMessageFocused] = useState(true); - const [title, setTitle] = useState(""); - const [tags, setTags] = useState(""); - const [priority, setPriority] = useState(3); - const [clickUrl, setClickUrl] = useState(""); - const [attachUrl, setAttachUrl] = useState(""); - const [attachFile, setAttachFile] = useState(null); - const [filename, setFilename] = useState(""); - const [filenameEdited, setFilenameEdited] = useState(false); - const [email, setEmail] = useState(""); - const [call, setCall] = useState(""); - const [delay, setDelay] = useState(""); - const [publishAnother, setPublishAnother] = useState(false); + const { t } = useTranslation(); + const { account } = useContext(AccountContext); + const [baseUrl, setBaseUrl] = useState(""); + const [topic, setTopic] = useState(""); + const [message, setMessage] = useState(""); + const [messageFocused, setMessageFocused] = useState(true); + const [title, setTitle] = useState(""); + const [tags, setTags] = useState(""); + const [priority, setPriority] = useState(3); + const [clickUrl, setClickUrl] = useState(""); + const [attachUrl, setAttachUrl] = useState(""); + const [attachFile, setAttachFile] = useState(null); + const [filename, setFilename] = useState(""); + const [filenameEdited, setFilenameEdited] = useState(false); + const [email, setEmail] = useState(""); + const [call, setCall] = useState(""); + const [delay, setDelay] = useState(""); + const [publishAnother, setPublishAnother] = useState(false); - const [showTopicUrl, setShowTopicUrl] = useState(""); - const [showClickUrl, setShowClickUrl] = useState(false); - const [showAttachUrl, setShowAttachUrl] = useState(false); - const [showEmail, setShowEmail] = useState(false); - const [showCall, setShowCall] = useState(false); - const [showDelay, setShowDelay] = useState(false); + const [showTopicUrl, setShowTopicUrl] = useState(""); + const [showClickUrl, setShowClickUrl] = useState(false); + const [showAttachUrl, setShowAttachUrl] = useState(false); + const [showEmail, setShowEmail] = useState(false); + const [showCall, setShowCall] = useState(false); + const [showDelay, setShowDelay] = useState(false); - const showAttachFile = !!attachFile && !showAttachUrl; - const attachFileInput = useRef(); - const [attachFileError, setAttachFileError] = useState(""); + const showAttachFile = !!attachFile && !showAttachUrl; + const attachFileInput = useRef(); + const [attachFileError, setAttachFileError] = useState(""); - const [activeRequest, setActiveRequest] = useState(null); - const [status, setStatus] = useState(""); - const disabled = !!activeRequest; + const [activeRequest, setActiveRequest] = useState(null); + const [status, setStatus] = useState(""); + const disabled = !!activeRequest; - const [emojiPickerAnchorEl, setEmojiPickerAnchorEl] = useState(null); + const [emojiPickerAnchorEl, setEmojiPickerAnchorEl] = useState(null); - const [dropZone, setDropZone] = useState(false); - const [sendButtonEnabled, setSendButtonEnabled] = useState(true); + const [dropZone, setDropZone] = useState(false); + const [sendButtonEnabled, setSendButtonEnabled] = useState(true); - const open = !!props.openMode; - const fullScreen = useMediaQuery(theme.breakpoints.down('sm')); + const open = !!props.openMode; + const fullScreen = useMediaQuery(theme.breakpoints.down("sm")); - useEffect(() => { - window.addEventListener('dragenter', () => { - props.onDragEnter(); - setDropZone(true); - }); - }, []); + useEffect(() => { + window.addEventListener("dragenter", () => { + props.onDragEnter(); + setDropZone(true); + }); + }, []); - useEffect(() => { - setBaseUrl(props.baseUrl); - setTopic(props.topic); - setShowTopicUrl(!props.baseUrl || !props.topic); - setMessageFocused(!!props.topic); // Focus message only if topic is set - }, [props.baseUrl, props.topic]); + useEffect(() => { + setBaseUrl(props.baseUrl); + setTopic(props.topic); + setShowTopicUrl(!props.baseUrl || !props.topic); + setMessageFocused(!!props.topic); // Focus message only if topic is set + }, [props.baseUrl, props.topic]); - useEffect(() => { - const valid = validUrl(baseUrl) && validTopic(topic) && !attachFileError; - setSendButtonEnabled(valid); - }, [baseUrl, topic, attachFileError]); + useEffect(() => { + const valid = validUrl(baseUrl) && validTopic(topic) && !attachFileError; + setSendButtonEnabled(valid); + }, [baseUrl, topic, attachFileError]); - useEffect(() => { - setMessage(props.message); - }, [props.message]); + useEffect(() => { + setMessage(props.message); + }, [props.message]); - const updateBaseUrl = (newVal) => { - if (validUrl(newVal)) { - setBaseUrl(newVal.replace(/\/$/, '')); // strip traililng slash after https?:// + const updateBaseUrl = (newVal) => { + if (validUrl(newVal)) { + setBaseUrl(newVal.replace(/\/$/, "")); // strip traililng slash after https?:// + } else { + setBaseUrl(newVal); + } + }; + + const handleSubmit = async () => { + const url = new URL(topicUrl(baseUrl, topic)); + if (title.trim()) { + url.searchParams.append("title", title.trim()); + } + if (tags.trim()) { + url.searchParams.append("tags", tags.trim()); + } + if (priority && priority !== 3) { + url.searchParams.append("priority", priority.toString()); + } + if (clickUrl.trim()) { + url.searchParams.append("click", clickUrl.trim()); + } + if (attachUrl.trim()) { + url.searchParams.append("attach", attachUrl.trim()); + } + if (filename.trim()) { + url.searchParams.append("filename", filename.trim()); + } + if (email.trim()) { + url.searchParams.append("email", email.trim()); + } + if (call.trim()) { + url.searchParams.append("call", call.trim()); + } + if (delay.trim()) { + url.searchParams.append("delay", delay.trim()); + } + if (attachFile && message.trim()) { + url.searchParams.append( + "message", + message.replaceAll("\n", "\\n").trim() + ); + } + const body = attachFile ? attachFile : message; + try { + const user = await userManager.get(baseUrl); + const headers = maybeWithAuth({}, user); + const progressFn = (ev) => { + if (ev.loaded > 0 && ev.total > 0) { + setStatus( + t("publish_dialog_progress_uploading_detail", { + loaded: formatBytes(ev.loaded), + total: formatBytes(ev.total), + percent: Math.round((ev.loaded * 100.0) / ev.total), + }) + ); } else { - setBaseUrl(newVal); + setStatus(t("publish_dialog_progress_uploading")); } - }; + }; + const request = api.publishXHR(url, body, headers, progressFn); + setActiveRequest(request); + await request; + if (!publishAnother) { + props.onClose(); + } else { + setStatus(t("publish_dialog_message_published")); + setActiveRequest(null); + } + } catch (e) { + setStatus( + + {e} + + ); + setActiveRequest(null); + } + }; - const handleSubmit = async () => { - const url = new URL(topicUrl(baseUrl, topic)); - if (title.trim()) { - url.searchParams.append("title", title.trim()); - } - if (tags.trim()) { - url.searchParams.append("tags", tags.trim()); - } - if (priority && priority !== 3) { - url.searchParams.append("priority", priority.toString()); - } - if (clickUrl.trim()) { - url.searchParams.append("click", clickUrl.trim()); - } - if (attachUrl.trim()) { - url.searchParams.append("attach", attachUrl.trim()); - } - if (filename.trim()) { - url.searchParams.append("filename", filename.trim()); - } - if (email.trim()) { - url.searchParams.append("email", email.trim()); - } - if (call.trim()) { - url.searchParams.append("call", call.trim()); - } - if (delay.trim()) { - url.searchParams.append("delay", delay.trim()); - } - if (attachFile && message.trim()) { - url.searchParams.append("message", message.replaceAll("\n", "\\n").trim()); - } - const body = (attachFile) ? attachFile : message; - try { - const user = await userManager.get(baseUrl); - const headers = maybeWithAuth({}, user); - const progressFn = (ev) => { - if (ev.loaded > 0 && ev.total > 0) { - setStatus(t("publish_dialog_progress_uploading_detail", { - loaded: formatBytes(ev.loaded), - total: formatBytes(ev.total), - percent: Math.round(ev.loaded * 100.0 / ev.total) - })); - } else { - setStatus(t("publish_dialog_progress_uploading")); + const checkAttachmentLimits = async (file) => { + try { + const account = await accountApi.get(); + const fileSizeLimit = account.limits.attachment_file_size ?? 0; + const remainingBytes = account.stats.attachment_total_size_remaining; + const fileSizeLimitReached = + fileSizeLimit > 0 && file.size > fileSizeLimit; + const quotaReached = remainingBytes > 0 && file.size > remainingBytes; + if (fileSizeLimitReached && quotaReached) { + return setAttachFileError( + t("publish_dialog_attachment_limits_file_and_quota_reached", { + fileSizeLimit: formatBytes(fileSizeLimit), + remainingBytes: formatBytes(remainingBytes), + }) + ); + } else if (fileSizeLimitReached) { + return setAttachFileError( + t("publish_dialog_attachment_limits_file_reached", { + fileSizeLimit: formatBytes(fileSizeLimit), + }) + ); + } else if (quotaReached) { + return setAttachFileError( + t("publish_dialog_attachment_limits_quota_reached", { + remainingBytes: formatBytes(remainingBytes), + }) + ); + } + setAttachFileError(""); + } catch (e) { + console.log(`[PublishDialog] Retrieving attachment limits failed`, e); + if (e instanceof UnauthorizedError) { + session.resetAndRedirect(routes.login); + } else { + setAttachFileError(""); // Reset error (rely on server-side checking) + } + } + }; + + const handleAttachFileClick = () => { + attachFileInput.current.click(); + }; + + const handleAttachFileChanged = async (ev) => { + await updateAttachFile(ev.target.files[0]); + }; + + const handleAttachFileDrop = async (ev) => { + ev.preventDefault(); + setDropZone(false); + await updateAttachFile(ev.dataTransfer.files[0]); + }; + + const updateAttachFile = async (file) => { + setAttachFile(file); + setFilename(file.name); + props.onResetOpenMode(); + await checkAttachmentLimits(file); + }; + + const handleAttachFileDragLeave = () => { + setDropZone(false); + if (props.openMode === PublishDialog.OPEN_MODE_DRAG) { + props.onClose(); // Only close dialog if it was not open before dragging file in + } + }; + + const handleEmojiClick = (ev) => { + setEmojiPickerAnchorEl(ev.currentTarget); + }; + + const handleEmojiPick = (emoji) => { + setTags((tags) => (tags.trim() ? `${tags.trim()}, ${emoji}` : emoji)); + }; + + const handleEmojiClose = () => { + setEmojiPickerAnchorEl(null); + }; + + const priorities = { + 1: { label: t("publish_dialog_priority_min"), file: priority1 }, + 2: { label: t("publish_dialog_priority_low"), file: priority2 }, + 3: { label: t("publish_dialog_priority_default"), file: priority3 }, + 4: { label: t("publish_dialog_priority_high"), file: priority4 }, + 5: { label: t("publish_dialog_priority_max"), file: priority5 }, + }; + + return ( + <> + {dropZone && ( + + )} + + + {baseUrl && topic + ? t("publish_dialog_title_topic", { + topic: topicShortUrl(baseUrl, topic), + }) + : t("publish_dialog_title_no_topic")} + + + {dropZone && } + {showTopicUrl && ( + { + setBaseUrl(props.baseUrl); + setTopic(props.topic); + setShowTopicUrl(false); + }} + > + updateBaseUrl(ev.target.value)} + disabled={disabled} + type="url" + variant="standard" + sx={{ flexGrow: 1, marginRight: 1 }} + inputProps={{ + "aria-label": t("publish_dialog_base_url_label"), + }} + /> + setTopic(ev.target.value)} + disabled={disabled} + type="text" + variant="standard" + autoFocus={!messageFocused} + sx={{ flexGrow: 1 }} + inputProps={{ + "aria-label": t("publish_dialog_topic_label"), + }} + /> + + )} + setTitle(ev.target.value)} + disabled={disabled} + type="text" + fullWidth + variant="standard" + inputProps={{ + "aria-label": t("publish_dialog_title_label"), + }} + /> + setMessage(ev.target.value)} + disabled={disabled} + type="text" + variant="standard" + rows={5} + autoFocus={messageFocused} + fullWidth + multiline + inputProps={{ + "aria-label": t("publish_dialog_message_label"), + }} + /> +
+ + + + + setTags(ev.target.value)} + disabled={disabled} + type="text" + variant="standard" + sx={{ flexGrow: 1, marginRight: 1 }} + inputProps={{ + "aria-label": t("publish_dialog_tags_label"), + }} + /> + + + + +
+ {showClickUrl && ( + { + setClickUrl(""); + setShowClickUrl(false); + }} + > + setClickUrl(ev.target.value)} + disabled={disabled} + type="url" + fullWidth + variant="standard" + inputProps={{ + "aria-label": t("publish_dialog_click_label"), + }} + /> + + )} + {showEmail && ( + { + setEmail(""); + setShowEmail(false); + }} + > + setEmail(ev.target.value)} + disabled={disabled} + type="email" + variant="standard" + fullWidth + inputProps={{ + "aria-label": t("publish_dialog_email_label"), + }} + /> + + )} + {showCall && ( + { + setCall(""); + setShowCall(false); + }} + > + + + + + + )} + {showAttachUrl && ( + { + setAttachUrl(""); + setFilename(""); + setFilenameEdited(false); + setShowAttachUrl(false); + }} + > + { + const url = ev.target.value; + setAttachUrl(url); + if (!filenameEdited) { + try { + const u = new URL(url); + const parts = u.pathname.split("/"); + if (parts.length > 0) { + setFilename(parts[parts.length - 1]); + } + } catch (e) { + // Do nothing + } + } + }} + disabled={disabled} + type="url" + variant="standard" + sx={{ flexGrow: 5, marginRight: 1 }} + inputProps={{ + "aria-label": t("publish_dialog_attach_label"), + }} + /> + { + setFilename(ev.target.value); + setFilenameEdited(true); + }} + disabled={disabled} + type="text" + variant="standard" + sx={{ flexGrow: 1 }} + inputProps={{ + "aria-label": t("publish_dialog_filename_label"), + }} + /> + + )} + + {showAttachFile && ( + setFilename(f)} + onClose={() => { + setAttachFile(null); + setAttachFileError(""); + setFilename(""); + }} + /> + )} + {showDelay && ( + { + setDelay(""); + setShowDelay(false); + }} + > + setDelay(ev.target.value)} + disabled={disabled} + type="text" + variant="standard" + fullWidth + inputProps={{ + "aria-label": t("publish_dialog_delay_label"), + }} + /> + + )} + + {t("publish_dialog_other_features")} + +
+ {!showClickUrl && ( + setShowClickUrl(true)} + sx={{ marginRight: 1, marginBottom: 1 }} + /> + )} + {!showEmail && ( + setShowEmail(true)} + sx={{ marginRight: 1, marginBottom: 1 }} + /> + )} + {account?.phone_numbers?.length > 0 && !showCall && ( + { + setShowCall(true); + setCall(account.phone_numbers[0]); + }} + sx={{ marginRight: 1, marginBottom: 1 }} + /> + )} + {!showAttachUrl && !showAttachFile && ( + setShowAttachUrl(true)} + sx={{ marginRight: 1, marginBottom: 1 }} + /> + )} + {!showAttachFile && !showAttachUrl && ( + handleAttachFileClick()} + sx={{ marginRight: 1, marginBottom: 1 }} + /> + )} + {!showDelay && ( + setShowDelay(true)} + sx={{ marginRight: 1, marginBottom: 1 }} + /> + )} + {!showTopicUrl && ( + setShowTopicUrl(true)} + sx={{ marginRight: 1, marginBottom: 1 }} + /> + )} + {account && !account?.phone_numbers && ( + + + + + + )} +
+ + + ), + }} + /> + +
+ + {activeRequest && ( + + )} + {!activeRequest && ( + <> + setPublishAnother(ev.target.checked)} + inputProps={{ + "aria-label": t( + "publish_dialog_checkbox_publish_another" + ), + }} + /> } - }; - const request = api.publishXHR(url, body, headers, progressFn); - setActiveRequest(request); - await request; - if (!publishAnother) { - props.onClose(); - } else { - setStatus(t("publish_dialog_message_published")); - setActiveRequest(null); - } - } catch (e) { - setStatus({e}); - setActiveRequest(null); - } - }; - - const checkAttachmentLimits = async (file) => { - try { - const account = await accountApi.get(); - const fileSizeLimit = account.limits.attachment_file_size ?? 0; - const remainingBytes = account.stats.attachment_total_size_remaining; - const fileSizeLimitReached = fileSizeLimit > 0 && file.size > fileSizeLimit; - const quotaReached = remainingBytes > 0 && file.size > remainingBytes; - if (fileSizeLimitReached && quotaReached) { - return setAttachFileError(t("publish_dialog_attachment_limits_file_and_quota_reached", { - fileSizeLimit: formatBytes(fileSizeLimit), - remainingBytes: formatBytes(remainingBytes) - })); - } else if (fileSizeLimitReached) { - return setAttachFileError(t("publish_dialog_attachment_limits_file_reached", { fileSizeLimit: formatBytes(fileSizeLimit) })); - } else if (quotaReached) { - return setAttachFileError(t("publish_dialog_attachment_limits_quota_reached", { remainingBytes: formatBytes(remainingBytes) })); - } - setAttachFileError(""); - } catch (e) { - console.log(`[PublishDialog] Retrieving attachment limits failed`, e); - if (e instanceof UnauthorizedError) { - session.resetAndRedirect(routes.login); - } else { - setAttachFileError(""); // Reset error (rely on server-side checking) - } - } - }; - - const handleAttachFileClick = () => { - attachFileInput.current.click(); - }; - - const handleAttachFileChanged = async (ev) => { - await updateAttachFile(ev.target.files[0]); - }; - - const handleAttachFileDrop = async (ev) => { - ev.preventDefault(); - setDropZone(false); - await updateAttachFile(ev.dataTransfer.files[0]); - }; - - const updateAttachFile = async (file) => { - setAttachFile(file); - setFilename(file.name); - props.onResetOpenMode(); - await checkAttachmentLimits(file); - }; - - const handleAttachFileDragLeave = () => { - setDropZone(false); - if (props.openMode === PublishDialog.OPEN_MODE_DRAG) { - props.onClose(); // Only close dialog if it was not open before dragging file in - } - }; - - const handleEmojiClick = (ev) => { - setEmojiPickerAnchorEl(ev.currentTarget); - }; - - const handleEmojiPick = (emoji) => { - setTags(tags => (tags.trim()) ? `${tags.trim()}, ${emoji}` : emoji); - }; - - const handleEmojiClose = () => { - setEmojiPickerAnchorEl(null); - }; - - const priorities = { - 1: { label: t("publish_dialog_priority_min"), file: priority1 }, - 2: { label: t("publish_dialog_priority_low"), file: priority2 }, - 3: { label: t("publish_dialog_priority_default"), file: priority3 }, - 4: { label: t("publish_dialog_priority_high"), file: priority4 }, - 5: { label: t("publish_dialog_priority_max"), file: priority5 } - }; - - return ( - <> - {dropZone && - } - - {(baseUrl && topic) ? t("publish_dialog_title_topic", { topic: topicShortUrl(baseUrl, topic) }) : t("publish_dialog_title_no_topic")} - - {dropZone && } - {showTopicUrl && - { - setBaseUrl(props.baseUrl); - setTopic(props.topic); - setShowTopicUrl(false); - }}> - updateBaseUrl(ev.target.value)} - disabled={disabled} - type="url" - variant="standard" - sx={{flexGrow: 1, marginRight: 1}} - inputProps={{ - "aria-label": t("publish_dialog_base_url_label") - }} - /> - setTopic(ev.target.value)} - disabled={disabled} - type="text" - variant="standard" - autoFocus={!messageFocused} - sx={{flexGrow: 1}} - inputProps={{ - "aria-label": t("publish_dialog_topic_label") - }} - /> - - } - setTitle(ev.target.value)} - disabled={disabled} - type="text" - fullWidth - variant="standard" - inputProps={{ - "aria-label": t("publish_dialog_title_label") - }} - /> - setMessage(ev.target.value)} - disabled={disabled} - type="text" - variant="standard" - rows={5} - autoFocus={messageFocused} - fullWidth - multiline - inputProps={{ - "aria-label": t("publish_dialog_message_label") - }} - /> -
- - - - - setTags(ev.target.value)} - disabled={disabled} - type="text" - variant="standard" - sx={{flexGrow: 1, marginRight: 1}} - inputProps={{ - "aria-label": t("publish_dialog_tags_label") - }} - /> - - - - -
- {showClickUrl && - { - setClickUrl(""); - setShowClickUrl(false); - }}> - setClickUrl(ev.target.value)} - disabled={disabled} - type="url" - fullWidth - variant="standard" - inputProps={{ - "aria-label": t("publish_dialog_click_label") - }} - /> - - } - {showEmail && - { - setEmail(""); - setShowEmail(false); - }}> - setEmail(ev.target.value)} - disabled={disabled} - type="email" - variant="standard" - fullWidth - inputProps={{ - "aria-label": t("publish_dialog_email_label") - }} - /> - - } - {showCall && - { - setCall(""); - setShowCall(false); - }}> - - - - - - } - {showAttachUrl && - { - setAttachUrl(""); - setFilename(""); - setFilenameEdited(false); - setShowAttachUrl(false); - }}> - { - const url = ev.target.value; - setAttachUrl(url); - if (!filenameEdited) { - try { - const u = new URL(url); - const parts = u.pathname.split("/"); - if (parts.length > 0) { - setFilename(parts[parts.length-1]); - } - } catch (e) { - // Do nothing - } - } - }} - disabled={disabled} - type="url" - variant="standard" - sx={{flexGrow: 5, marginRight: 1}} - inputProps={{ - "aria-label": t("publish_dialog_attach_label") - }} - /> - { - setFilename(ev.target.value); - setFilenameEdited(true); - }} - disabled={disabled} - type="text" - variant="standard" - sx={{flexGrow: 1}} - inputProps={{ - "aria-label": t("publish_dialog_filename_label") - }} - /> - - } - - {showAttachFile && setFilename(f)} - onClose={() => { - setAttachFile(null); - setAttachFileError(""); - setFilename(""); - }} - />} - {showDelay && - { - setDelay(""); - setShowDelay(false); - }}> - setDelay(ev.target.value)} - disabled={disabled} - type="text" - variant="standard" - fullWidth - inputProps={{ - "aria-label": t("publish_dialog_delay_label") - }} - /> - - } - - {t("publish_dialog_other_features")} - -
- {!showClickUrl && setShowClickUrl(true)} sx={{marginRight: 1, marginBottom: 1}}/>} - {!showEmail && setShowEmail(true)} sx={{marginRight: 1, marginBottom: 1}}/>} - {account?.phone_numbers?.length > 0 && !showCall && { setShowCall(true); setCall(account.phone_numbers[0]); }} sx={{marginRight: 1, marginBottom: 1}}/>} - {!showAttachUrl && !showAttachFile && setShowAttachUrl(true)} sx={{marginRight: 1, marginBottom: 1}}/>} - {!showAttachFile && !showAttachUrl && handleAttachFileClick()} sx={{marginRight: 1, marginBottom: 1}}/>} - {!showDelay && setShowDelay(true)} sx={{marginRight: 1, marginBottom: 1}}/>} - {!showTopicUrl && setShowTopicUrl(true)} sx={{marginRight: 1, marginBottom: 1}}/>} - {account && !account?.phone_numbers && } -
- - - }} - /> - -
- - {activeRequest && } - {!activeRequest && - <> - setPublishAnother(ev.target.checked)} - inputProps={{ - "aria-label": t("publish_dialog_checkbox_publish_another") - }} /> - } /> - - - - } - -
- - ); + /> + + + + )} +
+
+ + ); }; const Row = (props) => { - return ( -
- {props.children} -
- ); + return ( +
+ {props.children} +
+ ); }; const ClosableRow = (props) => { - const closable = (props.hasOwnProperty("closable")) ? props.closable : true; - return ( - - {props.children} - {closable && - - - - } - - ); + const closable = props.hasOwnProperty("closable") ? props.closable : true; + return ( + + {props.children} + {closable && ( + + + + )} + + ); }; const DialogIconButton = (props) => { - const sx = props.sx || {}; - return ( - - {props.children} - - ); + const sx = props.sx || {}; + return ( + + {props.children} + + ); }; const AttachmentBox = (props) => { - const { t } = useTranslation(); - const file = props.file; - return ( - <> - - {t("publish_dialog_attached_file_title")} - - - - - props.onChangeFilename(ev.target.value)} - disabled={props.disabled} - /> -
- - {formatBytes(file.size)} - {props.error && - - {" "}({props.error}) - - } - -
- - - -
- - ); + const { t } = useTranslation(); + const file = props.file; + return ( + <> + + {t("publish_dialog_attached_file_title")} + + + + + props.onChangeFilename(ev.target.value)} + disabled={props.disabled} + /> +
+ + {formatBytes(file.size)} + {props.error && ( + + {" "} + ({props.error}) + + )} + +
+ + + +
+ + ); }; const ExpandingTextField = (props) => { - const invisibleFieldRef = useRef(); - const [textWidth, setTextWidth] = useState(props.minWidth); - const determineTextWidth = () => { - const boundingRect = invisibleFieldRef?.current?.getBoundingClientRect(); - if (!boundingRect) { - return props.minWidth; - } - return (boundingRect.width >= props.minWidth) ? Math.round(boundingRect.width) : props.minWidth; - }; - useEffect(() => { - setTextWidth(determineTextWidth() + 5); - }, [props.value]); - return ( - <> - - {props.value} - - - - ) + const invisibleFieldRef = useRef(); + const [textWidth, setTextWidth] = useState(props.minWidth); + const determineTextWidth = () => { + const boundingRect = invisibleFieldRef?.current?.getBoundingClientRect(); + if (!boundingRect) { + return props.minWidth; + } + return boundingRect.width >= props.minWidth + ? Math.round(boundingRect.width) + : props.minWidth; + }; + useEffect(() => { + setTextWidth(determineTextWidth() + 5); + }, [props.value]); + return ( + <> + + {props.value} + + + + ); }; const DropArea = (props) => { - const allowDrag = (ev) => { - // This is where we could disallow certain files to be dragged in. - // For now we allow all files. + const allowDrag = (ev) => { + // This is where we could disallow certain files to be dragged in. + // For now we allow all files. - ev.dataTransfer.dropEffect = 'copy'; - ev.preventDefault(); - }; + ev.dataTransfer.dropEffect = "copy"; + ev.preventDefault(); + }; - return ( - - ); + return ( + + ); }; const DropBox = () => { - const { t } = useTranslation(); - return ( - - - {t("publish_dialog_drop_file_here")} - - - ); -} + const { t } = useTranslation(); + return ( + + + + {t("publish_dialog_drop_file_here")} + + + + ); +}; PublishDialog.OPEN_MODE_DEFAULT = "default"; PublishDialog.OPEN_MODE_DRAG = "drag"; diff --git a/web/src/components/ReserveDialogs.js b/web/src/components/ReserveDialogs.js index e466269a..f36ea6cc 100644 --- a/web/src/components/ReserveDialogs.js +++ b/web/src/components/ReserveDialogs.js @@ -1,199 +1,239 @@ -import * as React from 'react'; -import {useState} from 'react'; -import Button from '@mui/material/Button'; -import TextField from '@mui/material/TextField'; -import Dialog from '@mui/material/Dialog'; -import DialogContent from '@mui/material/DialogContent'; -import DialogContentText from '@mui/material/DialogContentText'; -import DialogTitle from '@mui/material/DialogTitle'; -import {Alert, FormControl, Select, useMediaQuery} from "@mui/material"; +import * as React from "react"; +import { useState } from "react"; +import Button from "@mui/material/Button"; +import TextField from "@mui/material/TextField"; +import Dialog from "@mui/material/Dialog"; +import DialogContent from "@mui/material/DialogContent"; +import DialogContentText from "@mui/material/DialogContentText"; +import DialogTitle from "@mui/material/DialogTitle"; +import { Alert, FormControl, Select, useMediaQuery } from "@mui/material"; import theme from "./theme"; -import {validTopic} from "../app/utils"; +import { validTopic } from "../app/utils"; import DialogFooter from "./DialogFooter"; -import {useTranslation} from "react-i18next"; +import { useTranslation } from "react-i18next"; import session from "../app/Session"; import routes from "./routes"; -import accountApi, {Permission} from "../app/AccountApi"; +import accountApi, { Permission } from "../app/AccountApi"; import ReserveTopicSelect from "./ReserveTopicSelect"; import MenuItem from "@mui/material/MenuItem"; import ListItemIcon from "@mui/material/ListItemIcon"; import ListItemText from "@mui/material/ListItemText"; -import {Check, DeleteForever} from "@mui/icons-material"; -import {TopicReservedError, UnauthorizedError} from "../app/errors"; +import { Check, DeleteForever } from "@mui/icons-material"; +import { TopicReservedError, UnauthorizedError } from "../app/errors"; export const ReserveAddDialog = (props) => { - const { t } = useTranslation(); - const [error, setError] = useState(""); - const [topic, setTopic] = useState(props.topic || ""); - const [everyone, setEveryone] = useState(Permission.DENY_ALL); - const fullScreen = useMediaQuery(theme.breakpoints.down('sm')); - const allowTopicEdit = !props.topic; - const alreadyReserved = props.reservations.filter(r => r.topic === topic).length > 0; - const submitButtonEnabled = validTopic(topic) && !alreadyReserved; + const { t } = useTranslation(); + const [error, setError] = useState(""); + const [topic, setTopic] = useState(props.topic || ""); + const [everyone, setEveryone] = useState(Permission.DENY_ALL); + const fullScreen = useMediaQuery(theme.breakpoints.down("sm")); + const allowTopicEdit = !props.topic; + const alreadyReserved = + props.reservations.filter((r) => r.topic === topic).length > 0; + const submitButtonEnabled = validTopic(topic) && !alreadyReserved; - const handleSubmit = async () => { - try { - await accountApi.upsertReservation(topic, everyone); - console.debug(`[ReserveAddDialog] Added reservation for topic ${topic}: ${everyone}`); - } catch (e) { - console.log(`[ReserveAddDialog] Error adding topic reservation.`, e); - if (e instanceof UnauthorizedError) { - session.resetAndRedirect(routes.login); - } else if (e instanceof TopicReservedError) { - setError(t("subscribe_dialog_error_topic_already_reserved")); - return; - } else { - setError(e.message); - return; - } - } - props.onClose(); - }; + const handleSubmit = async () => { + try { + await accountApi.upsertReservation(topic, everyone); + console.debug( + `[ReserveAddDialog] Added reservation for topic ${topic}: ${everyone}` + ); + } catch (e) { + console.log(`[ReserveAddDialog] Error adding topic reservation.`, e); + if (e instanceof UnauthorizedError) { + session.resetAndRedirect(routes.login); + } else if (e instanceof TopicReservedError) { + setError(t("subscribe_dialog_error_topic_already_reserved")); + return; + } else { + setError(e.message); + return; + } + } + props.onClose(); + }; - return ( - - {t("prefs_reservations_dialog_title_add")} - - - {t("prefs_reservations_dialog_description")} - - {allowTopicEdit && setTopic(ev.target.value)} - type="url" - fullWidth - variant="standard" - />} - - - - - - - - ); + return ( + + {t("prefs_reservations_dialog_title_add")} + + + {t("prefs_reservations_dialog_description")} + + {allowTopicEdit && ( + setTopic(ev.target.value)} + type="url" + fullWidth + variant="standard" + /> + )} + + + + + + + + ); }; export const ReserveEditDialog = (props) => { - const { t } = useTranslation(); - const [error, setError] = useState(""); - const [everyone, setEveryone] = useState(props.reservation?.everyone || Permission.DENY_ALL); - const fullScreen = useMediaQuery(theme.breakpoints.down('sm')); + const { t } = useTranslation(); + const [error, setError] = useState(""); + const [everyone, setEveryone] = useState( + props.reservation?.everyone || Permission.DENY_ALL + ); + const fullScreen = useMediaQuery(theme.breakpoints.down("sm")); - const handleSubmit = async () => { - try { - await accountApi.upsertReservation(props.reservation.topic, everyone); - console.debug(`[ReserveEditDialog] Updated reservation for topic ${t}: ${everyone}`); - } catch (e) { - console.log(`[ReserveEditDialog] Error updating topic reservation.`, e); - if (e instanceof UnauthorizedError) { - session.resetAndRedirect(routes.login); - } else { - setError(e.message); - return; - } - } - props.onClose(); - }; + const handleSubmit = async () => { + try { + await accountApi.upsertReservation(props.reservation.topic, everyone); + console.debug( + `[ReserveEditDialog] Updated reservation for topic ${t}: ${everyone}` + ); + } catch (e) { + console.log(`[ReserveEditDialog] Error updating topic reservation.`, e); + if (e instanceof UnauthorizedError) { + session.resetAndRedirect(routes.login); + } else { + setError(e.message); + return; + } + } + props.onClose(); + }; - return ( - - {t("prefs_reservations_dialog_title_edit")} - - - {t("prefs_reservations_dialog_description")} - - - - - - - - - ); + return ( + + {t("prefs_reservations_dialog_title_edit")} + + + {t("prefs_reservations_dialog_description")} + + + + + + + + + ); }; export const ReserveDeleteDialog = (props) => { - const { t } = useTranslation(); - const [error, setError] = useState(""); - const [deleteMessages, setDeleteMessages] = useState(false); - const fullScreen = useMediaQuery(theme.breakpoints.down('sm')); + const { t } = useTranslation(); + const [error, setError] = useState(""); + const [deleteMessages, setDeleteMessages] = useState(false); + const fullScreen = useMediaQuery(theme.breakpoints.down("sm")); - const handleSubmit = async () => { - try { - await accountApi.deleteReservation(props.topic, deleteMessages); - console.debug(`[ReserveDeleteDialog] Deleted reservation for topic ${props.topic}`); - } catch (e) { - console.log(`[ReserveDeleteDialog] Error deleting topic reservation.`, e); - if (e instanceof UnauthorizedError) { - session.resetAndRedirect(routes.login); - } else { - setError(e.message); - return; - } - } - props.onClose(); - }; + const handleSubmit = async () => { + try { + await accountApi.deleteReservation(props.topic, deleteMessages); + console.debug( + `[ReserveDeleteDialog] Deleted reservation for topic ${props.topic}` + ); + } catch (e) { + console.log(`[ReserveDeleteDialog] Error deleting topic reservation.`, e); + if (e instanceof UnauthorizedError) { + session.resetAndRedirect(routes.login); + } else { + setError(e.message); + return; + } + } + props.onClose(); + }; - return ( - - {t("prefs_reservations_dialog_title_delete")} - - - {t("reservation_delete_dialog_description")} - - - - - {!deleteMessages && - - {t("reservation_delete_dialog_action_keep_description")} - - } - {deleteMessages && - - {t("reservation_delete_dialog_action_delete_description")} - - } - - - - - - - ); + return ( + + {t("prefs_reservations_dialog_title_delete")} + + + {t("reservation_delete_dialog_description")} + + + + + {!deleteMessages && ( + + {t("reservation_delete_dialog_action_keep_description")} + + )} + {deleteMessages && ( + + {t("reservation_delete_dialog_action_delete_description")} + + )} + + + + + + + ); }; - diff --git a/web/src/components/ReserveIcons.js b/web/src/components/ReserveIcons.js index 0d7b05bd..3b22df37 100644 --- a/web/src/components/ReserveIcons.js +++ b/web/src/components/ReserveIcons.js @@ -1,46 +1,55 @@ -import * as React from 'react'; -import {Lock, Public} from "@mui/icons-material"; +import * as React from "react"; +import { Lock, Public } from "@mui/icons-material"; import Box from "@mui/material/Box"; export const PermissionReadWrite = React.forwardRef((props, ref) => { - return ; + return ; }); export const PermissionDenyAll = React.forwardRef((props, ref) => { - return ; + return ; }); export const PermissionRead = React.forwardRef((props, ref) => { - return ; + return ; }); export const PermissionWrite = React.forwardRef((props, ref) => { - return ; + return ; }); const PermissionInternal = React.forwardRef((props, ref) => { - const size = props.size ?? "medium"; - const Icon = props.icon; - return ( - - - {props.text && - - {props.text} - - } + const size = props.size ?? "medium"; + const Icon = props.icon; + return ( + + + {props.text && ( + + {props.text} - ); + )} + + ); }); diff --git a/web/src/components/ReserveTopicSelect.js b/web/src/components/ReserveTopicSelect.js index e5daf695..76113bad 100644 --- a/web/src/components/ReserveTopicSelect.js +++ b/web/src/components/ReserveTopicSelect.js @@ -1,49 +1,70 @@ -import * as React from 'react'; -import {FormControl, Select} from "@mui/material"; -import {useTranslation} from "react-i18next"; +import * as React from "react"; +import { FormControl, Select } from "@mui/material"; +import { useTranslation } from "react-i18next"; import MenuItem from "@mui/material/MenuItem"; import ListItemIcon from "@mui/material/ListItemIcon"; import ListItemText from "@mui/material/ListItemText"; -import {PermissionDenyAll, PermissionRead, PermissionReadWrite, PermissionWrite} from "./ReserveIcons"; -import {Permission} from "../app/AccountApi"; +import { + PermissionDenyAll, + PermissionRead, + PermissionReadWrite, + PermissionWrite, +} from "./ReserveIcons"; +import { Permission } from "../app/AccountApi"; const ReserveTopicSelect = (props) => { - const { t } = useTranslation(); - const sx = props.sx || {}; - return ( - - - - ); + const { t } = useTranslation(); + const sx = props.sx || {}; + return ( + + + + ); }; export default ReserveTopicSelect; diff --git a/web/src/components/Signup.js b/web/src/components/Signup.js index 856ce8f1..39409a50 100644 --- a/web/src/components/Signup.js +++ b/web/src/components/Signup.js @@ -1,158 +1,167 @@ -import * as React from 'react'; -import {useState} from 'react'; +import * as React from "react"; +import { useState } from "react"; import TextField from "@mui/material/TextField"; import Button from "@mui/material/Button"; import Box from "@mui/material/Box"; import routes from "./routes"; import session from "../app/Session"; import Typography from "@mui/material/Typography"; -import {NavLink} from "react-router-dom"; +import { NavLink } from "react-router-dom"; import AvatarBox from "./AvatarBox"; -import {useTranslation} from "react-i18next"; +import { useTranslation } from "react-i18next"; import WarningAmberIcon from "@mui/icons-material/WarningAmber"; import accountApi from "../app/AccountApi"; -import {InputAdornment} from "@mui/material"; +import { InputAdornment } from "@mui/material"; import IconButton from "@mui/material/IconButton"; -import {Visibility, VisibilityOff} from "@mui/icons-material"; -import {AccountCreateLimitReachedError, UserExistsError} from "../app/errors"; +import { Visibility, VisibilityOff } from "@mui/icons-material"; +import { AccountCreateLimitReachedError, UserExistsError } from "../app/errors"; const Signup = () => { - const { t } = useTranslation(); - const [error, setError] = useState(""); - const [username, setUsername] = useState(""); - const [password, setPassword] = useState(""); - const [confirm, setConfirm] = useState(""); - const [showPassword, setShowPassword] = useState(false); - const [showConfirm, setShowConfirm] = useState(false); + const { t } = useTranslation(); + const [error, setError] = useState(""); + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + const [confirm, setConfirm] = useState(""); + const [showPassword, setShowPassword] = useState(false); + const [showConfirm, setShowConfirm] = useState(false); - const handleSubmit = async (event) => { - event.preventDefault(); - const user = { username, password }; - try { - await accountApi.create(user.username, user.password); - const token = await accountApi.login(user); - console.log(`[Signup] User signup for user ${user.username} successful, token is ${token}`); - session.store(user.username, token); - window.location.href = routes.app; - } catch (e) { - console.log(`[Signup] Signup for user ${user.username} failed`, e); - if (e instanceof UserExistsError) { - setError(t("signup_error_username_taken", { username: e.username })); - } else if ((e instanceof AccountCreateLimitReachedError)) { - setError(t("signup_error_creation_limit_reached")); - } else { - setError(e.message); - } - } - }; - - if (!config.enable_signup) { - return ( - - {t("signup_disabled")} - - ); + const handleSubmit = async (event) => { + event.preventDefault(); + const user = { username, password }; + try { + await accountApi.create(user.username, user.password); + const token = await accountApi.login(user); + console.log( + `[Signup] User signup for user ${user.username} successful, token is ${token}` + ); + session.store(user.username, token); + window.location.href = routes.app; + } catch (e) { + console.log(`[Signup] Signup for user ${user.username} failed`, e); + if (e instanceof UserExistsError) { + setError(t("signup_error_username_taken", { username: e.username })); + } else if (e instanceof AccountCreateLimitReachedError) { + setError(t("signup_error_creation_limit_reached")); + } else { + setError(e.message); + } } + }; + if (!config.enable_signup) { return ( - - - {t("signup_title")} - - - setUsername(ev.target.value.trim())} - autoFocus - /> - setPassword(ev.target.value.trim())} - InputProps={{ - endAdornment: ( - - setShowPassword(!showPassword)} - onMouseDown={(ev) => ev.preventDefault()} - edge="end" - > - {showPassword ? : } - - - ) - }} - /> - setConfirm(ev.target.value.trim())} - InputProps={{ - endAdornment: ( - - setShowConfirm(!showConfirm)} - onMouseDown={(ev) => ev.preventDefault()} - edge="end" - > - {showConfirm ? : } - - - ) - }} - /> - - {error && - - - {error} - - } - - {config.enable_login && - - - {t("signup_already_have_account")} - - - } - + + + {t("signup_disabled")} + + ); -} + } + + return ( + + {t("signup_title")} + + setUsername(ev.target.value.trim())} + autoFocus + /> + setPassword(ev.target.value.trim())} + InputProps={{ + endAdornment: ( + + setShowPassword(!showPassword)} + onMouseDown={(ev) => ev.preventDefault()} + edge="end" + > + {showPassword ? : } + + + ), + }} + /> + setConfirm(ev.target.value.trim())} + InputProps={{ + endAdornment: ( + + setShowConfirm(!showConfirm)} + onMouseDown={(ev) => ev.preventDefault()} + edge="end" + > + {showConfirm ? : } + + + ), + }} + /> + + {error && ( + + + {error} + + )} + + {config.enable_login && ( + + + {t("signup_already_have_account")} + + + )} + + ); +}; export default Signup; diff --git a/web/src/components/SubscribeDialog.js b/web/src/components/SubscribeDialog.js index 95f1c473..940eafe3 100644 --- a/web/src/components/SubscribeDialog.js +++ b/web/src/components/SubscribeDialog.js @@ -1,313 +1,388 @@ -import * as React from 'react'; -import {useContext, useState} from 'react'; -import Button from '@mui/material/Button'; -import TextField from '@mui/material/TextField'; -import Dialog from '@mui/material/Dialog'; -import DialogContent from '@mui/material/DialogContent'; -import DialogContentText from '@mui/material/DialogContentText'; -import DialogTitle from '@mui/material/DialogTitle'; -import {Autocomplete, Checkbox, FormControlLabel, FormGroup, useMediaQuery} from "@mui/material"; +import * as React from "react"; +import { useContext, useState } from "react"; +import Button from "@mui/material/Button"; +import TextField from "@mui/material/TextField"; +import Dialog from "@mui/material/Dialog"; +import DialogContent from "@mui/material/DialogContent"; +import DialogContentText from "@mui/material/DialogContentText"; +import DialogTitle from "@mui/material/DialogTitle"; +import { + Autocomplete, + Checkbox, + FormControlLabel, + FormGroup, + useMediaQuery, +} from "@mui/material"; import theme from "./theme"; import api from "../app/Api"; -import {randomAlphanumericString, topicUrl, validTopic, validUrl} from "../app/utils"; +import { + randomAlphanumericString, + topicUrl, + validTopic, + validUrl, +} from "../app/utils"; import userManager from "../app/UserManager"; import subscriptionManager from "../app/SubscriptionManager"; import poller from "../app/Poller"; import DialogFooter from "./DialogFooter"; -import {useTranslation} from "react-i18next"; +import { useTranslation } from "react-i18next"; import session from "../app/Session"; import routes from "./routes"; -import accountApi, {Permission, Role} from "../app/AccountApi"; +import accountApi, { Permission, Role } from "../app/AccountApi"; import ReserveTopicSelect from "./ReserveTopicSelect"; -import {AccountContext} from "./App"; -import {TopicReservedError, UnauthorizedError} from "../app/errors"; -import {ReserveLimitChip} from "./SubscriptionPopup"; +import { AccountContext } from "./App"; +import { TopicReservedError, UnauthorizedError } from "../app/errors"; +import { ReserveLimitChip } from "./SubscriptionPopup"; const publicBaseUrl = "https://ntfy.sh"; const SubscribeDialog = (props) => { - const [baseUrl, setBaseUrl] = useState(""); - const [topic, setTopic] = useState(""); - const [showLoginPage, setShowLoginPage] = useState(false); - const fullScreen = useMediaQuery(theme.breakpoints.down('sm')); + const [baseUrl, setBaseUrl] = useState(""); + const [topic, setTopic] = useState(""); + const [showLoginPage, setShowLoginPage] = useState(false); + const fullScreen = useMediaQuery(theme.breakpoints.down("sm")); - const handleSuccess = async () => { - console.log(`[SubscribeDialog] Subscribing to topic ${topic}`); - const actualBaseUrl = (baseUrl) ? baseUrl : config.base_url; - const subscription = await subscribeTopic(actualBaseUrl, topic); - poller.pollInBackground(subscription); // Dangle! - props.onSuccess(subscription); - } + const handleSuccess = async () => { + console.log(`[SubscribeDialog] Subscribing to topic ${topic}`); + const actualBaseUrl = baseUrl ? baseUrl : config.base_url; + const subscription = await subscribeTopic(actualBaseUrl, topic); + poller.pollInBackground(subscription); // Dangle! + props.onSuccess(subscription); + }; - return ( - - {!showLoginPage && setShowLoginPage(true)} - onSuccess={handleSuccess} - />} - {showLoginPage && setShowLoginPage(false)} - onSuccess={handleSuccess} - />} - - ); + return ( + + {!showLoginPage && ( + setShowLoginPage(true)} + onSuccess={handleSuccess} + /> + )} + {showLoginPage && ( + setShowLoginPage(false)} + onSuccess={handleSuccess} + /> + )} + + ); }; const SubscribePage = (props) => { - const { t } = useTranslation(); - const { account } = useContext(AccountContext); - const [error, setError] = useState(""); - const [reserveTopicVisible, setReserveTopicVisible] = useState(false); - const [anotherServerVisible, setAnotherServerVisible] = useState(false); - const [everyone, setEveryone] = useState(Permission.DENY_ALL); - const baseUrl = (anotherServerVisible) ? props.baseUrl : config.base_url; - const topic = props.topic; - const existingTopicUrls = props.subscriptions.map(s => topicUrl(s.baseUrl, s.topic)); - const existingBaseUrls = Array - .from(new Set([publicBaseUrl, ...props.subscriptions.map(s => s.baseUrl)])) - .filter(s => s !== config.base_url); - const showReserveTopicCheckbox = config.enable_reservations && !anotherServerVisible && (config.enable_payments || account); - const reserveTopicEnabled = session.exists() && (account?.role === Role.ADMIN || (account?.role === Role.USER && (account?.stats.reservations_remaining || 0) > 0)); + const { t } = useTranslation(); + const { account } = useContext(AccountContext); + const [error, setError] = useState(""); + const [reserveTopicVisible, setReserveTopicVisible] = useState(false); + const [anotherServerVisible, setAnotherServerVisible] = useState(false); + const [everyone, setEveryone] = useState(Permission.DENY_ALL); + const baseUrl = anotherServerVisible ? props.baseUrl : config.base_url; + const topic = props.topic; + const existingTopicUrls = props.subscriptions.map((s) => + topicUrl(s.baseUrl, s.topic) + ); + const existingBaseUrls = Array.from( + new Set([publicBaseUrl, ...props.subscriptions.map((s) => s.baseUrl)]) + ).filter((s) => s !== config.base_url); + const showReserveTopicCheckbox = + config.enable_reservations && + !anotherServerVisible && + (config.enable_payments || account); + const reserveTopicEnabled = + session.exists() && + (account?.role === Role.ADMIN || + (account?.role === Role.USER && + (account?.stats.reservations_remaining || 0) > 0)); - const handleSubscribe = async () => { - const user = await userManager.get(baseUrl); // May be undefined - const username = (user) ? user.username : t("subscribe_dialog_error_user_anonymous"); + const handleSubscribe = async () => { + const user = await userManager.get(baseUrl); // May be undefined + const username = user + ? user.username + : t("subscribe_dialog_error_user_anonymous"); - // Check read access to topic - const success = await api.topicAuth(baseUrl, topic, user); - if (!success) { - console.log(`[SubscribeDialog] Login to ${topicUrl(baseUrl, topic)} failed for user ${username}`); - if (user) { - setError(t("subscribe_dialog_error_user_not_authorized", { username: username })); - return; - } else { - props.onNeedsLogin(); - return; - } + // Check read access to topic + const success = await api.topicAuth(baseUrl, topic, user); + if (!success) { + console.log( + `[SubscribeDialog] Login to ${topicUrl( + baseUrl, + topic + )} failed for user ${username}` + ); + if (user) { + setError( + t("subscribe_dialog_error_user_not_authorized", { + username: username, + }) + ); + return; + } else { + props.onNeedsLogin(); + return; + } + } + + // Reserve topic (if requested) + if ( + session.exists() && + baseUrl === config.base_url && + reserveTopicVisible + ) { + console.log( + `[SubscribeDialog] Reserving topic ${topic} with everyone access ${everyone}` + ); + try { + await accountApi.upsertReservation(topic, everyone); + } catch (e) { + console.log(`[SubscribeDialog] Error reserving topic`, e); + if (e instanceof UnauthorizedError) { + session.resetAndRedirect(routes.login); + } else if (e instanceof TopicReservedError) { + setError(t("subscribe_dialog_error_topic_already_reserved")); + return; } + } + } - // Reserve topic (if requested) - if (session.exists() && baseUrl === config.base_url && reserveTopicVisible) { - console.log(`[SubscribeDialog] Reserving topic ${topic} with everyone access ${everyone}`); - try { - await accountApi.upsertReservation(topic, everyone); - } catch (e) { - console.log(`[SubscribeDialog] Error reserving topic`, e); - if (e instanceof UnauthorizedError) { - session.resetAndRedirect(routes.login); - } else if (e instanceof TopicReservedError) { - setError(t("subscribe_dialog_error_topic_already_reserved")); - return; - } - } - } - - console.log(`[SubscribeDialog] Successful login to ${topicUrl(baseUrl, topic)} for user ${username}`); - props.onSuccess(); - }; - - const handleUseAnotherChanged = (e) => { - props.setBaseUrl(""); - setAnotherServerVisible(e.target.checked); - }; - - const subscribeButtonEnabled = (() => { - if (anotherServerVisible) { - const isExistingTopicUrl = existingTopicUrls.includes(topicUrl(baseUrl, topic)); - return validTopic(topic) && validUrl(baseUrl) && !isExistingTopicUrl; - } else { - const isExistingTopicUrl = existingTopicUrls.includes(topicUrl(config.base_url, topic)); - return validTopic(topic) && !isExistingTopicUrl; - } - })(); - - const updateBaseUrl = (ev, newVal) => { - if (validUrl(newVal)) { - props.setBaseUrl(newVal.replace(/\/$/, '')); // strip trailing slash after https?:// - } else { - props.setBaseUrl(newVal); - } - }; - - return ( - <> - {t("subscribe_dialog_subscribe_title")} - - - {t("subscribe_dialog_subscribe_description")} - -
- props.setTopic(ev.target.value)} - type="text" - fullWidth - variant="standard" - inputProps={{ - maxLength: 64, - "aria-label": t("subscribe_dialog_subscribe_topic_placeholder") - }} - /> - -
- {showReserveTopicCheckbox && - - setReserveTopicVisible(ev.target.checked)} - inputProps={{ - "aria-label": t("reserve_dialog_checkbox_label") - }} - /> - } - label={ - <> - {t("reserve_dialog_checkbox_label")} - - - } - /> - {reserveTopicVisible && - - } - - } - {!reserveTopicVisible && - - - } - label={t("subscribe_dialog_subscribe_use_another_label")}/> - {anotherServerVisible && - - } - />} - - } -
- - - - - + console.log( + `[SubscribeDialog] Successful login to ${topicUrl( + baseUrl, + topic + )} for user ${username}` ); + props.onSuccess(); + }; + + const handleUseAnotherChanged = (e) => { + props.setBaseUrl(""); + setAnotherServerVisible(e.target.checked); + }; + + const subscribeButtonEnabled = (() => { + if (anotherServerVisible) { + const isExistingTopicUrl = existingTopicUrls.includes( + topicUrl(baseUrl, topic) + ); + return validTopic(topic) && validUrl(baseUrl) && !isExistingTopicUrl; + } else { + const isExistingTopicUrl = existingTopicUrls.includes( + topicUrl(config.base_url, topic) + ); + return validTopic(topic) && !isExistingTopicUrl; + } + })(); + + const updateBaseUrl = (ev, newVal) => { + if (validUrl(newVal)) { + props.setBaseUrl(newVal.replace(/\/$/, "")); // strip trailing slash after https?:// + } else { + props.setBaseUrl(newVal); + } + }; + + return ( + <> + {t("subscribe_dialog_subscribe_title")} + + + {t("subscribe_dialog_subscribe_description")} + +
+ props.setTopic(ev.target.value)} + type="text" + fullWidth + variant="standard" + inputProps={{ + maxLength: 64, + "aria-label": t("subscribe_dialog_subscribe_topic_placeholder"), + }} + /> + +
+ {showReserveTopicCheckbox && ( + + setReserveTopicVisible(ev.target.checked)} + inputProps={{ + "aria-label": t("reserve_dialog_checkbox_label"), + }} + /> + } + label={ + <> + {t("reserve_dialog_checkbox_label")} + + + } + /> + {reserveTopicVisible && ( + + )} + + )} + {!reserveTopicVisible && ( + + + } + label={t("subscribe_dialog_subscribe_use_another_label")} + /> + {anotherServerVisible && ( + ( + + )} + /> + )} + + )} +
+ + + + + + ); }; const LoginPage = (props) => { - const { t } = useTranslation(); - const [username, setUsername] = useState(""); - const [password, setPassword] = useState(""); - const [error, setError] = useState(""); - const baseUrl = (props.baseUrl) ? props.baseUrl : config.base_url; - const topic = props.topic; + const { t } = useTranslation(); + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + const [error, setError] = useState(""); + const baseUrl = props.baseUrl ? props.baseUrl : config.base_url; + const topic = props.topic; - const handleLogin = async () => { - const user = {baseUrl, username, password}; - const success = await api.topicAuth(baseUrl, topic, user); - if (!success) { - console.log(`[SubscribeDialog] Login to ${topicUrl(baseUrl, topic)} failed for user ${username}`); - setError(t("subscribe_dialog_error_user_not_authorized", { username: username })); - return; - } - console.log(`[SubscribeDialog] Successful login to ${topicUrl(baseUrl, topic)} for user ${username}`); - await userManager.save(user); - props.onSuccess(); - }; - - return ( - <> - {t("subscribe_dialog_login_title")} - - - {t("subscribe_dialog_login_description")} - - setUsername(ev.target.value)} - type="text" - fullWidth - variant="standard" - inputProps={{ - "aria-label": t("subscribe_dialog_login_username_label") - }} - /> - setPassword(ev.target.value)} - fullWidth - variant="standard" - inputProps={{ - "aria-label": t("subscribe_dialog_login_password_label") - }} - /> - - - - - - + const handleLogin = async () => { + const user = { baseUrl, username, password }; + const success = await api.topicAuth(baseUrl, topic, user); + if (!success) { + console.log( + `[SubscribeDialog] Login to ${topicUrl( + baseUrl, + topic + )} failed for user ${username}` + ); + setError( + t("subscribe_dialog_error_user_not_authorized", { username: username }) + ); + return; + } + console.log( + `[SubscribeDialog] Successful login to ${topicUrl( + baseUrl, + topic + )} for user ${username}` ); + await userManager.save(user); + props.onSuccess(); + }; + + return ( + <> + {t("subscribe_dialog_login_title")} + + + {t("subscribe_dialog_login_description")} + + setUsername(ev.target.value)} + type="text" + fullWidth + variant="standard" + inputProps={{ + "aria-label": t("subscribe_dialog_login_username_label"), + }} + /> + setPassword(ev.target.value)} + fullWidth + variant="standard" + inputProps={{ + "aria-label": t("subscribe_dialog_login_password_label"), + }} + /> + + + + + + + ); }; export const subscribeTopic = async (baseUrl, topic) => { - const subscription = await subscriptionManager.add(baseUrl, topic); - if (session.exists()) { - try { - await accountApi.addSubscription(baseUrl, topic); - } catch (e) { - console.log(`[SubscribeDialog] Subscribing to topic ${topic} failed`, e); - if (e instanceof UnauthorizedError) { - session.resetAndRedirect(routes.login); - } - } + const subscription = await subscriptionManager.add(baseUrl, topic); + if (session.exists()) { + try { + await accountApi.addSubscription(baseUrl, topic); + } catch (e) { + console.log(`[SubscribeDialog] Subscribing to topic ${topic} failed`, e); + if (e instanceof UnauthorizedError) { + session.resetAndRedirect(routes.login); + } } - return subscription; + } + return subscription; }; export default SubscribeDialog; diff --git a/web/src/components/SubscriptionPopup.js b/web/src/components/SubscriptionPopup.js index 024b6f23..eb575dcb 100644 --- a/web/src/components/SubscriptionPopup.js +++ b/web/src/components/SubscriptionPopup.js @@ -1,292 +1,393 @@ -import * as React from 'react'; -import {useContext, useState} from 'react'; -import Button from '@mui/material/Button'; -import TextField from '@mui/material/TextField'; -import Dialog from '@mui/material/Dialog'; -import DialogContent from '@mui/material/DialogContent'; -import DialogContentText from '@mui/material/DialogContentText'; -import DialogTitle from '@mui/material/DialogTitle'; -import {Chip, InputAdornment, Portal, Snackbar, useMediaQuery} from "@mui/material"; +import * as React from "react"; +import { useContext, useState } from "react"; +import Button from "@mui/material/Button"; +import TextField from "@mui/material/TextField"; +import Dialog from "@mui/material/Dialog"; +import DialogContent from "@mui/material/DialogContent"; +import DialogContentText from "@mui/material/DialogContentText"; +import DialogTitle from "@mui/material/DialogTitle"; +import { + Chip, + InputAdornment, + Portal, + Snackbar, + useMediaQuery, +} from "@mui/material"; import theme from "./theme"; import subscriptionManager from "../app/SubscriptionManager"; import DialogFooter from "./DialogFooter"; -import {useTranslation} from "react-i18next"; -import accountApi, {Role} from "../app/AccountApi"; +import { useTranslation } from "react-i18next"; +import accountApi, { Role } from "../app/AccountApi"; import session from "../app/Session"; import routes from "./routes"; import MenuItem from "@mui/material/MenuItem"; import PopupMenu from "./PopupMenu"; -import {formatShortDateTime, shuffle} from "../app/utils"; +import { formatShortDateTime, shuffle } from "../app/utils"; import api from "../app/Api"; -import {useNavigate} from "react-router-dom"; +import { useNavigate } from "react-router-dom"; import IconButton from "@mui/material/IconButton"; -import {Clear} from "@mui/icons-material"; -import {AccountContext} from "./App"; -import {ReserveAddDialog, ReserveDeleteDialog, ReserveEditDialog} from "./ReserveDialogs"; -import {UnauthorizedError} from "../app/errors"; +import { Clear } from "@mui/icons-material"; +import { AccountContext } from "./App"; +import { + ReserveAddDialog, + ReserveDeleteDialog, + ReserveEditDialog, +} from "./ReserveDialogs"; +import { UnauthorizedError } from "../app/errors"; export const SubscriptionPopup = (props) => { - const { t } = useTranslation(); - const { account } = useContext(AccountContext); - const navigate = useNavigate(); - const [displayNameDialogOpen, setDisplayNameDialogOpen] = useState(false); - const [reserveAddDialogOpen, setReserveAddDialogOpen] = useState(false); - const [reserveEditDialogOpen, setReserveEditDialogOpen] = useState(false); - const [reserveDeleteDialogOpen, setReserveDeleteDialogOpen] = useState(false); - const [showPublishError, setShowPublishError] = useState(false); - const subscription = props.subscription; - const placement = props.placement ?? "left"; - const reservations = account?.reservations || []; + const { t } = useTranslation(); + const { account } = useContext(AccountContext); + const navigate = useNavigate(); + const [displayNameDialogOpen, setDisplayNameDialogOpen] = useState(false); + const [reserveAddDialogOpen, setReserveAddDialogOpen] = useState(false); + const [reserveEditDialogOpen, setReserveEditDialogOpen] = useState(false); + const [reserveDeleteDialogOpen, setReserveDeleteDialogOpen] = useState(false); + const [showPublishError, setShowPublishError] = useState(false); + const subscription = props.subscription; + const placement = props.placement ?? "left"; + const reservations = account?.reservations || []; - const showReservationAdd = config.enable_reservations && !subscription?.reservation && account?.stats.reservations_remaining > 0; - const showReservationAddDisabled = !showReservationAdd && config.enable_reservations && !subscription?.reservation && (config.enable_payments || account?.stats.reservations_remaining === 0); - const showReservationEdit = config.enable_reservations && !!subscription?.reservation; - const showReservationDelete = config.enable_reservations && !!subscription?.reservation; + const showReservationAdd = + config.enable_reservations && + !subscription?.reservation && + account?.stats.reservations_remaining > 0; + const showReservationAddDisabled = + !showReservationAdd && + config.enable_reservations && + !subscription?.reservation && + (config.enable_payments || account?.stats.reservations_remaining === 0); + const showReservationEdit = + config.enable_reservations && !!subscription?.reservation; + const showReservationDelete = + config.enable_reservations && !!subscription?.reservation; - const handleChangeDisplayName = async () => { - setDisplayNameDialogOpen(true); + const handleChangeDisplayName = async () => { + setDisplayNameDialogOpen(true); + }; + + const handleReserveAdd = async () => { + setReserveAddDialogOpen(true); + }; + + const handleReserveEdit = async () => { + setReserveEditDialogOpen(true); + }; + + const handleReserveDelete = async () => { + setReserveDeleteDialogOpen(true); + }; + + const handleSendTestMessage = async () => { + const baseUrl = props.subscription.baseUrl; + const topic = props.subscription.topic; + const tags = shuffle([ + "grinning", + "octopus", + "upside_down_face", + "palm_tree", + "maple_leaf", + "apple", + "skull", + "warning", + "jack_o_lantern", + "de-server-1", + "backups", + "cron-script", + "script-error", + "phils-automation", + "mouse", + "go-rocks", + "hi-ben", + ]).slice(0, Math.round(Math.random() * 4)); + const priority = shuffle([1, 2, 3, 4, 5])[0]; + const title = shuffle([ + "", + "", + "", // Higher chance of no title + "Oh my, another test message?", + "Titles are optional, did you know that?", + "ntfy is open source, and will always be free. Cool, right?", + "I don't really like apples", + "My favorite TV show is The Wire. You should watch it!", + "You can attach files and URLs to messages too", + "You can delay messages up to 3 days", + ])[0]; + const nowSeconds = Math.round(Date.now() / 1000); + const message = shuffle([ + `Hello friend, this is a test notification from ntfy web. It's ${formatShortDateTime( + nowSeconds + )} right now. Is that early or late?`, + `So I heard you like ntfy? If that's true, go to GitHub and star it, or to the Play store and rate it. Thanks! Oh yeah, this is a test notification.`, + `It's almost like you want to hear what I have to say. I'm not even a machine. I'm just a sentence that Phil typed on a random Thursday.`, + `Alright then, it's ${formatShortDateTime( + nowSeconds + )} already. Boy oh boy, where did the time go? I hope you're alright, friend.`, + `There are nine million bicycles in Beijing That's a fact; It's a thing we can't deny. I wonder if that's true ...`, + `I'm really excited that you're trying out ntfy. Did you know that there are a few public topics, such as ntfy.sh/stats and ntfy.sh/announcements.`, + `It's interesting to hear what people use ntfy for. I've heard people talk about using it for so many cool things. What do you use it for?`, + ])[0]; + try { + await api.publish(baseUrl, topic, message, { + title: title, + priority: priority, + tags: tags, + }); + } catch (e) { + console.log(`[SubscriptionPopup] Error publishing message`, e); + setShowPublishError(true); } + }; - const handleReserveAdd = async () => { - setReserveAddDialogOpen(true); - } - - const handleReserveEdit = async () => { - setReserveEditDialogOpen(true); - } - - const handleReserveDelete = async () => { - setReserveDeleteDialogOpen(true); - } - - const handleSendTestMessage = async () => { - const baseUrl = props.subscription.baseUrl; - const topic = props.subscription.topic; - const tags = shuffle([ - "grinning", "octopus", "upside_down_face", "palm_tree", "maple_leaf", "apple", "skull", "warning", "jack_o_lantern", - "de-server-1", "backups", "cron-script", "script-error", "phils-automation", "mouse", "go-rocks", "hi-ben"]) - .slice(0, Math.round(Math.random() * 4)); - const priority = shuffle([1, 2, 3, 4, 5])[0]; - const title = shuffle([ - "", - "", - "", // Higher chance of no title - "Oh my, another test message?", - "Titles are optional, did you know that?", - "ntfy is open source, and will always be free. Cool, right?", - "I don't really like apples", - "My favorite TV show is The Wire. You should watch it!", - "You can attach files and URLs to messages too", - "You can delay messages up to 3 days" - ])[0]; - const nowSeconds = Math.round(Date.now()/1000); - const message = shuffle([ - `Hello friend, this is a test notification from ntfy web. It's ${formatShortDateTime(nowSeconds)} right now. Is that early or late?`, - `So I heard you like ntfy? If that's true, go to GitHub and star it, or to the Play store and rate it. Thanks! Oh yeah, this is a test notification.`, - `It's almost like you want to hear what I have to say. I'm not even a machine. I'm just a sentence that Phil typed on a random Thursday.`, - `Alright then, it's ${formatShortDateTime(nowSeconds)} already. Boy oh boy, where did the time go? I hope you're alright, friend.`, - `There are nine million bicycles in Beijing That's a fact; It's a thing we can't deny. I wonder if that's true ...`, - `I'm really excited that you're trying out ntfy. Did you know that there are a few public topics, such as ntfy.sh/stats and ntfy.sh/announcements.`, - `It's interesting to hear what people use ntfy for. I've heard people talk about using it for so many cool things. What do you use it for?` - ])[0]; - try { - await api.publish(baseUrl, topic, message, { - title: title, - priority: priority, - tags: tags - }); - } catch (e) { - console.log(`[SubscriptionPopup] Error publishing message`, e); - setShowPublishError(true); - } - } - - const handleClearAll = async () => { - console.log(`[SubscriptionPopup] Deleting all notifications from ${props.subscription.id}`); - await subscriptionManager.deleteNotifications(props.subscription.id); - }; - - const handleUnsubscribe = async () => { - console.log(`[SubscriptionPopup] Unsubscribing from ${props.subscription.id}`, props.subscription); - await subscriptionManager.remove(props.subscription.id); - if (session.exists() && !subscription.internal) { - try { - await accountApi.deleteSubscription(props.subscription.baseUrl, props.subscription.topic); - } catch (e) { - console.log(`[SubscriptionPopup] Error unsubscribing`, e); - if (e instanceof UnauthorizedError) { - session.resetAndRedirect(routes.login); - } - } - } - const newSelected = await subscriptionManager.first(); // May be undefined - if (newSelected && !newSelected.internal) { - navigate(routes.forSubscription(newSelected)); - } else { - navigate(routes.app); - } - }; - - return ( - <> - - {t("action_bar_change_display_name")} - {showReservationAdd && {t("action_bar_reservation_add")}} - {showReservationAddDisabled && - - {t("action_bar_reservation_add")} - - - } - {showReservationEdit && {t("action_bar_reservation_edit")}} - {showReservationDelete && {t("action_bar_reservation_delete")}} - {t("action_bar_send_test_notification")} - {t("action_bar_clear_notifications")} - {t("action_bar_unsubscribe")} - - - setShowPublishError(false)} - message={t("message_bar_error_publishing")} - /> - setDisplayNameDialogOpen(false)} - /> - {showReservationAdd && - setReserveAddDialogOpen(false)} - /> - } - {showReservationEdit && - setReserveEditDialogOpen(false)} - /> - } - {showReservationDelete && - setReserveDeleteDialogOpen(false)} - /> - } - - + const handleClearAll = async () => { + console.log( + `[SubscriptionPopup] Deleting all notifications from ${props.subscription.id}` ); + await subscriptionManager.deleteNotifications(props.subscription.id); + }; + + const handleUnsubscribe = async () => { + console.log( + `[SubscriptionPopup] Unsubscribing from ${props.subscription.id}`, + props.subscription + ); + await subscriptionManager.remove(props.subscription.id); + if (session.exists() && !subscription.internal) { + try { + await accountApi.deleteSubscription( + props.subscription.baseUrl, + props.subscription.topic + ); + } catch (e) { + console.log(`[SubscriptionPopup] Error unsubscribing`, e); + if (e instanceof UnauthorizedError) { + session.resetAndRedirect(routes.login); + } + } + } + const newSelected = await subscriptionManager.first(); // May be undefined + if (newSelected && !newSelected.internal) { + navigate(routes.forSubscription(newSelected)); + } else { + navigate(routes.app); + } + }; + + return ( + <> + + + {t("action_bar_change_display_name")} + + {showReservationAdd && ( + + {t("action_bar_reservation_add")} + + )} + {showReservationAddDisabled && ( + + + {t("action_bar_reservation_add")} + + + + )} + {showReservationEdit && ( + + {t("action_bar_reservation_edit")} + + )} + {showReservationDelete && ( + + {t("action_bar_reservation_delete")} + + )} + + {t("action_bar_send_test_notification")} + + + {t("action_bar_clear_notifications")} + + + {t("action_bar_unsubscribe")} + + + + setShowPublishError(false)} + message={t("message_bar_error_publishing")} + /> + setDisplayNameDialogOpen(false)} + /> + {showReservationAdd && ( + setReserveAddDialogOpen(false)} + /> + )} + {showReservationEdit && ( + setReserveEditDialogOpen(false)} + /> + )} + {showReservationDelete && ( + setReserveDeleteDialogOpen(false)} + /> + )} + + + ); }; const DisplayNameDialog = (props) => { - const { t } = useTranslation(); - const subscription = props.subscription; - const [error, setError] = useState(""); - const [displayName, setDisplayName] = useState(subscription.displayName ?? ""); - const fullScreen = useMediaQuery(theme.breakpoints.down('sm')); + const { t } = useTranslation(); + const subscription = props.subscription; + const [error, setError] = useState(""); + const [displayName, setDisplayName] = useState( + subscription.displayName ?? "" + ); + const fullScreen = useMediaQuery(theme.breakpoints.down("sm")); - const handleSave = async () => { - await subscriptionManager.setDisplayName(subscription.id, displayName); - if (session.exists() && !subscription.internal) { - try { - console.log(`[SubscriptionSettingsDialog] Updating subscription display name to ${displayName}`); - await accountApi.updateSubscription(subscription.baseUrl, subscription.topic, { display_name: displayName }); - } catch (e) { - console.log(`[SubscriptionSettingsDialog] Error updating subscription`, e); - if (e instanceof UnauthorizedError) { - session.resetAndRedirect(routes.login); - } else { - setError(e.message); - return; - } - } + const handleSave = async () => { + await subscriptionManager.setDisplayName(subscription.id, displayName); + if (session.exists() && !subscription.internal) { + try { + console.log( + `[SubscriptionSettingsDialog] Updating subscription display name to ${displayName}` + ); + await accountApi.updateSubscription( + subscription.baseUrl, + subscription.topic, + { display_name: displayName } + ); + } catch (e) { + console.log( + `[SubscriptionSettingsDialog] Error updating subscription`, + e + ); + if (e instanceof UnauthorizedError) { + session.resetAndRedirect(routes.login); + } else { + setError(e.message); + return; } - props.onClose(); + } } + props.onClose(); + }; - return ( - - {t("display_name_dialog_title")} - - - {t("display_name_dialog_description")} - - setDisplayName(ev.target.value)} - type="text" - fullWidth - variant="standard" - inputProps={{ - maxLength: 64, - "aria-label": t("display_name_dialog_placeholder") - }} - InputProps={{ - endAdornment: ( - - setDisplayName("")} edge="end"> - - - - ) - }} - /> - - - - - - - ); + return ( + + {t("display_name_dialog_title")} + + + {t("display_name_dialog_description")} + + setDisplayName(ev.target.value)} + type="text" + fullWidth + variant="standard" + inputProps={{ + maxLength: 64, + "aria-label": t("display_name_dialog_placeholder"), + }} + InputProps={{ + endAdornment: ( + + setDisplayName("")} edge="end"> + + + + ), + }} + /> + + + + + + + ); }; export const ReserveLimitChip = () => { - const { account } = useContext(AccountContext); - if (account?.role === Role.ADMIN || account?.stats.reservations_remaining > 0) { - return <>; - } else if (config.enable_payments) { - return (account?.limits.reservations > 0) ? : ; - } else if (account) { - return ; - } + const { account } = useContext(AccountContext); + if ( + account?.role === Role.ADMIN || + account?.stats.reservations_remaining > 0 + ) { return <>; + } else if (config.enable_payments) { + return account?.limits.reservations > 0 ? ( + + ) : ( + + ); + } else if (account) { + return ; + } + return <>; }; const LimitReachedChip = () => { - const { t } = useTranslation(); - return ( - - ); + const { t } = useTranslation(); + return ( + + ); }; export const ProChip = () => { - const { t } = useTranslation(); - return ( - - ); + const { t } = useTranslation(); + return ( + + ); }; - - diff --git a/web/src/components/UpgradeDialog.js b/web/src/components/UpgradeDialog.js index 0b91b1b1..94b878c2 100644 --- a/web/src/components/UpgradeDialog.js +++ b/web/src/components/UpgradeDialog.js @@ -1,367 +1,500 @@ -import * as React from 'react'; -import {useContext, useEffect, useState} from 'react'; -import Dialog from '@mui/material/Dialog'; -import DialogContent from '@mui/material/DialogContent'; -import DialogTitle from '@mui/material/DialogTitle'; -import {Alert, CardActionArea, CardContent, Chip, Link, ListItem, Switch, useMediaQuery} from "@mui/material"; +import * as React from "react"; +import { useContext, useEffect, useState } from "react"; +import Dialog from "@mui/material/Dialog"; +import DialogContent from "@mui/material/DialogContent"; +import DialogTitle from "@mui/material/DialogTitle"; +import { + Alert, + CardActionArea, + CardContent, + Chip, + Link, + ListItem, + Switch, + useMediaQuery, +} from "@mui/material"; import theme from "./theme"; import Button from "@mui/material/Button"; -import accountApi, {SubscriptionInterval} from "../app/AccountApi"; +import accountApi, { SubscriptionInterval } from "../app/AccountApi"; import session from "../app/Session"; import routes from "./routes"; import Card from "@mui/material/Card"; import Typography from "@mui/material/Typography"; -import {AccountContext} from "./App"; -import {formatBytes, formatNumber, formatPrice, formatShortDate} from "../app/utils"; -import {Trans, useTranslation} from "react-i18next"; +import { AccountContext } from "./App"; +import { + formatBytes, + formatNumber, + formatPrice, + formatShortDate, +} from "../app/utils"; +import { Trans, useTranslation } from "react-i18next"; import List from "@mui/material/List"; -import {Check, Close} from "@mui/icons-material"; +import { Check, Close } from "@mui/icons-material"; import ListItemIcon from "@mui/material/ListItemIcon"; import ListItemText from "@mui/material/ListItemText"; import Box from "@mui/material/Box"; -import {NavLink} from "react-router-dom"; -import {UnauthorizedError} from "../app/errors"; +import { NavLink } from "react-router-dom"; +import { UnauthorizedError } from "../app/errors"; import DialogContentText from "@mui/material/DialogContentText"; import DialogActions from "@mui/material/DialogActions"; const UpgradeDialog = (props) => { - const { t } = useTranslation(); - const { account } = useContext(AccountContext); // May be undefined! - const [error, setError] = useState(""); - const [tiers, setTiers] = useState(null); - const [interval, setInterval] = useState(account?.billing?.interval || SubscriptionInterval.YEAR); - const [newTierCode, setNewTierCode] = useState(account?.tier?.code); // May be undefined - const [loading, setLoading] = useState(false); - const fullScreen = useMediaQuery(theme.breakpoints.down('sm')); + const { t } = useTranslation(); + const { account } = useContext(AccountContext); // May be undefined! + const [error, setError] = useState(""); + const [tiers, setTiers] = useState(null); + const [interval, setInterval] = useState( + account?.billing?.interval || SubscriptionInterval.YEAR + ); + const [newTierCode, setNewTierCode] = useState(account?.tier?.code); // May be undefined + const [loading, setLoading] = useState(false); + const fullScreen = useMediaQuery(theme.breakpoints.down("sm")); - useEffect(() => { - const fetchTiers = async () => { - setTiers(await accountApi.billingTiers()); - } - fetchTiers(); // Dangle - }, []); + useEffect(() => { + const fetchTiers = async () => { + setTiers(await accountApi.billingTiers()); + }; + fetchTiers(); // Dangle + }, []); - if (!tiers) { - return <>; + if (!tiers) { + return <>; + } + + const tiersMap = Object.assign( + ...tiers.map((tier) => ({ [tier.code]: tier })) + ); + const newTier = tiersMap[newTierCode]; // May be undefined + const currentTier = account?.tier; // May be undefined + const currentInterval = account?.billing?.interval; // May be undefined + const currentTierCode = currentTier?.code; // May be undefined + + // Figure out buttons, labels and the submit action + let submitAction, submitButtonLabel, banner; + if (!account) { + submitButtonLabel = t("account_upgrade_dialog_button_redirect_signup"); + submitAction = Action.REDIRECT_SIGNUP; + banner = null; + } else if ( + currentTierCode === newTierCode && + (currentInterval === undefined || currentInterval === interval) + ) { + submitButtonLabel = t("account_upgrade_dialog_button_update_subscription"); + submitAction = null; + banner = currentTierCode ? Banner.PRORATION_INFO : null; + } else if (!currentTierCode) { + submitButtonLabel = t("account_upgrade_dialog_button_pay_now"); + submitAction = Action.CREATE_SUBSCRIPTION; + banner = null; + } else if (!newTierCode) { + submitButtonLabel = t("account_upgrade_dialog_button_cancel_subscription"); + submitAction = Action.CANCEL_SUBSCRIPTION; + banner = Banner.CANCEL_WARNING; + } else { + submitButtonLabel = t("account_upgrade_dialog_button_update_subscription"); + submitAction = Action.UPDATE_SUBSCRIPTION; + banner = Banner.PRORATION_INFO; + } + + // Exceptional conditions + if (loading) { + submitAction = null; + } else if ( + newTier?.code && + account?.reservations?.length > newTier?.limits?.reservations + ) { + submitAction = null; + banner = Banner.RESERVATIONS_WARNING; + } + + const handleSubmit = async () => { + if (submitAction === Action.REDIRECT_SIGNUP) { + window.location.href = routes.signup; + return; } - - const tiersMap = Object.assign(...tiers.map(tier => ({[tier.code]: tier}))); - const newTier = tiersMap[newTierCode]; // May be undefined - const currentTier = account?.tier; // May be undefined - const currentInterval = account?.billing?.interval; // May be undefined - const currentTierCode = currentTier?.code; // May be undefined - - // Figure out buttons, labels and the submit action - let submitAction, submitButtonLabel, banner; - if (!account) { - submitButtonLabel = t("account_upgrade_dialog_button_redirect_signup"); - submitAction = Action.REDIRECT_SIGNUP; - banner = null; - } else if (currentTierCode === newTierCode && (currentInterval === undefined || currentInterval === interval)) { - submitButtonLabel = t("account_upgrade_dialog_button_update_subscription"); - submitAction = null; - banner = (currentTierCode) ? Banner.PRORATION_INFO : null; - } else if (!currentTierCode) { - submitButtonLabel = t("account_upgrade_dialog_button_pay_now"); - submitAction = Action.CREATE_SUBSCRIPTION; - banner = null; - } else if (!newTierCode) { - submitButtonLabel = t("account_upgrade_dialog_button_cancel_subscription"); - submitAction = Action.CANCEL_SUBSCRIPTION; - banner = Banner.CANCEL_WARNING; - } else { - submitButtonLabel = t("account_upgrade_dialog_button_update_subscription"); - submitAction = Action.UPDATE_SUBSCRIPTION; - banner = Banner.PRORATION_INFO; + try { + setLoading(true); + if (submitAction === Action.CREATE_SUBSCRIPTION) { + const response = await accountApi.createBillingSubscription( + newTierCode, + interval + ); + window.location.href = response.redirect_url; + } else if (submitAction === Action.UPDATE_SUBSCRIPTION) { + await accountApi.updateBillingSubscription(newTierCode, interval); + } else if (submitAction === Action.CANCEL_SUBSCRIPTION) { + await accountApi.deleteBillingSubscription(); + } + props.onCancel(); + } catch (e) { + console.log(`[UpgradeDialog] Error changing billing subscription`, e); + if (e instanceof UnauthorizedError) { + session.resetAndRedirect(routes.login); + } else { + setError(e.message); + } + } finally { + setLoading(false); } + }; - // Exceptional conditions - if (loading) { - submitAction = null; - } else if (newTier?.code && account?.reservations?.length > newTier?.limits?.reservations) { - submitAction = null; - banner = Banner.RESERVATIONS_WARNING; - } - - const handleSubmit = async () => { - if (submitAction === Action.REDIRECT_SIGNUP) { - window.location.href = routes.signup; - return; - } - try { - setLoading(true); - if (submitAction === Action.CREATE_SUBSCRIPTION) { - const response = await accountApi.createBillingSubscription(newTierCode, interval); - window.location.href = response.redirect_url; - } else if (submitAction === Action.UPDATE_SUBSCRIPTION) { - await accountApi.updateBillingSubscription(newTierCode, interval); - } else if (submitAction === Action.CANCEL_SUBSCRIPTION) { - await accountApi.deleteBillingSubscription(); - } - props.onCancel(); - } catch (e) { - console.log(`[UpgradeDialog] Error changing billing subscription`, e); - if (e instanceof UnauthorizedError) { - session.resetAndRedirect(routes.login); - } else { - setError(e.message); - } - } finally { - setLoading(false); - } - } - - // Figure out discount - let discount = 0, upto = false; - if (newTier?.prices) { - discount = Math.round(((newTier.prices.month*12/newTier.prices.year)-1)*100); - } else { - let n = 0; - for (const t of tiers) { - if (t.prices) { - const tierDiscount = Math.round(((t.prices.month*12/t.prices.year)-1)*100); - if (tierDiscount > discount) { - discount = tierDiscount; - n++; - } - } - } - upto = n > 1; - } - - return ( - - -
-
{t("account_upgrade_dialog_title")}
-
- {t("account_upgrade_dialog_interval_monthly")} - setInterval(ev.target.checked ? SubscriptionInterval.YEAR : SubscriptionInterval.MONTH)} - /> - {t("account_upgrade_dialog_interval_yearly")} - {discount > 0 && - - } -
-
-
- -
- {tiers.map(tier => - setNewTierCode(tier.code)} // tier.code may be undefined! - /> - )} -
- {banner === Banner.CANCEL_WARNING && - - - - } - {banner === Banner.PRORATION_INFO && - - - - } - {banner === Banner.RESERVATIONS_WARNING && - - , - }} - /> - - } -
- - - {config.billing_contact.indexOf('@') !== -1 && - <> }}/>{" "} - } - {config.billing_contact.match(`^http?s://`) && - <> }}/>{" "} - } - {error} - - - - - - -
+ // Figure out discount + let discount = 0, + upto = false; + if (newTier?.prices) { + discount = Math.round( + ((newTier.prices.month * 12) / newTier.prices.year - 1) * 100 ); + } else { + let n = 0; + for (const t of tiers) { + if (t.prices) { + const tierDiscount = Math.round( + ((t.prices.month * 12) / t.prices.year - 1) * 100 + ); + if (tierDiscount > discount) { + discount = tierDiscount; + n++; + } + } + } + upto = n > 1; + } + + return ( + + +
+
{t("account_upgrade_dialog_title")}
+
+ + {t("account_upgrade_dialog_interval_monthly")} + + + setInterval( + ev.target.checked + ? SubscriptionInterval.YEAR + : SubscriptionInterval.MONTH + ) + } + /> + + {t("account_upgrade_dialog_interval_yearly")} + + {discount > 0 && ( + + )} +
+
+
+ +
+ {tiers.map((tier) => ( + setNewTierCode(tier.code)} // tier.code may be undefined! + /> + ))} +
+ {banner === Banner.CANCEL_WARNING && ( + + + + )} + {banner === Banner.PRORATION_INFO && ( + + + + )} + {banner === Banner.RESERVATIONS_WARNING && ( + + , + }} + /> + + )} +
+ + + {config.billing_contact.indexOf("@") !== -1 && ( + <> + , + }} + />{" "} + + )} + {config.billing_contact.match(`^http?s://`) && ( + <> + , + }} + />{" "} + + )} + {error} + + + + + + +
+ ); }; const TierCard = (props) => { - const { t } = useTranslation(); - const tier = props.tier; + const { t } = useTranslation(); + const tier = props.tier; - let cardStyle, labelStyle, labelText; - if (props.selected) { - cardStyle = { background: "#eee", border: "3px solid #338574" }; - labelStyle = { background: "#338574", color: "white" }; - labelText = t("account_upgrade_dialog_tier_selected_label"); - } else if (props.current) { - cardStyle = { border: "3px solid #eee" }; - labelStyle = { background: "#eee", color: "black" }; - labelText = t("account_upgrade_dialog_tier_current_label"); - } else { - cardStyle = { border: "3px solid transparent" }; - } + let cardStyle, labelStyle, labelText; + if (props.selected) { + cardStyle = { background: "#eee", border: "3px solid #338574" }; + labelStyle = { background: "#338574", color: "white" }; + labelText = t("account_upgrade_dialog_tier_selected_label"); + } else if (props.current) { + cardStyle = { border: "3px solid #eee" }; + labelStyle = { background: "#eee", color: "black" }; + labelText = t("account_upgrade_dialog_tier_current_label"); + } else { + cardStyle = { border: "3px solid transparent" }; + } - let monthlyPrice; - if (!tier.prices) { - monthlyPrice = 0; - } else if (props.interval === SubscriptionInterval.YEAR) { - monthlyPrice = tier.prices.year/12; - } else if (props.interval === SubscriptionInterval.MONTH) { - monthlyPrice = tier.prices.month; - } + let monthlyPrice; + if (!tier.prices) { + monthlyPrice = 0; + } else if (props.interval === SubscriptionInterval.YEAR) { + monthlyPrice = tier.prices.year / 12; + } else if (props.interval === SubscriptionInterval.MONTH) { + monthlyPrice = tier.prices.month; + } - return ( - - - - - {labelStyle && -
{labelText}
- } - - {tier.name || t("account_basics_tier_free")} - -
- {formatPrice(monthlyPrice)} - {monthlyPrice > 0 && <>/ {t("account_upgrade_dialog_tier_price_per_month")}} -
- - {tier.limits.reservations > 0 && {t("account_upgrade_dialog_tier_features_reservations", { reservations: tier.limits.reservations, count: tier.limits.reservations })}} - {t("account_upgrade_dialog_tier_features_messages", { messages: formatNumber(tier.limits.messages), count: tier.limits.messages })} - {t("account_upgrade_dialog_tier_features_emails", { emails: formatNumber(tier.limits.emails), count: tier.limits.emails })} - {tier.limits.calls > 0 && {t("account_upgrade_dialog_tier_features_calls", { calls: formatNumber(tier.limits.calls), count: tier.limits.calls })}} - {t("account_upgrade_dialog_tier_features_attachment_file_size", { filesize: formatBytes(tier.limits.attachment_file_size, 0) })} - {tier.limits.reservations === 0 && {t("account_upgrade_dialog_tier_features_no_reservations")}} - {tier.limits.calls === 0 && {t("account_upgrade_dialog_tier_features_no_calls")}} - - {tier.prices && props.interval === SubscriptionInterval.MONTH && - - {t("account_upgrade_dialog_tier_price_billed_monthly", { price: formatPrice(tier.prices.month*12) })} - - } - {tier.prices && props.interval === SubscriptionInterval.YEAR && - - {t("account_upgrade_dialog_tier_price_billed_yearly", { price: formatPrice(tier.prices.year), save: formatPrice(tier.prices.month*12-tier.prices.year) })} - - } -
-
-
-
- - ); -} + return ( + + + + + {labelStyle && ( +
+ {labelText} +
+ )} + + {tier.name || t("account_basics_tier_free")} + +
+ + {formatPrice(monthlyPrice)} + + {monthlyPrice > 0 && ( + <>/ {t("account_upgrade_dialog_tier_price_per_month")} + )} +
+ + {tier.limits.reservations > 0 && ( + + {t("account_upgrade_dialog_tier_features_reservations", { + reservations: tier.limits.reservations, + count: tier.limits.reservations, + })} + + )} + + {t("account_upgrade_dialog_tier_features_messages", { + messages: formatNumber(tier.limits.messages), + count: tier.limits.messages, + })} + + + {t("account_upgrade_dialog_tier_features_emails", { + emails: formatNumber(tier.limits.emails), + count: tier.limits.emails, + })} + + {tier.limits.calls > 0 && ( + + {t("account_upgrade_dialog_tier_features_calls", { + calls: formatNumber(tier.limits.calls), + count: tier.limits.calls, + })} + + )} + + {t( + "account_upgrade_dialog_tier_features_attachment_file_size", + { filesize: formatBytes(tier.limits.attachment_file_size, 0) } + )} + + {tier.limits.reservations === 0 && ( + + {t("account_upgrade_dialog_tier_features_no_reservations")} + + )} + {tier.limits.calls === 0 && ( + + {t("account_upgrade_dialog_tier_features_no_calls")} + + )} + + {tier.prices && props.interval === SubscriptionInterval.MONTH && ( + + {t("account_upgrade_dialog_tier_price_billed_monthly", { + price: formatPrice(tier.prices.month * 12), + })} + + )} + {tier.prices && props.interval === SubscriptionInterval.YEAR && ( + + {t("account_upgrade_dialog_tier_price_billed_yearly", { + price: formatPrice(tier.prices.year), + save: formatPrice(tier.prices.month * 12 - tier.prices.year), + })} + + )} +
+
+
+
+ ); +}; const Feature = (props) => { - return {props.children}; -} + return {props.children}; +}; const NoFeature = (props) => { - return {props.children}; -} + return {props.children}; +}; const FeatureItem = (props) => { - return ( - - - {props.feature && } - {!props.feature && } - - - {props.children} - - } - /> - - - ); + return ( + + + {props.feature && } + {!props.feature && } + + {props.children}} + /> + + ); }; const Action = { - REDIRECT_SIGNUP: 1, - CREATE_SUBSCRIPTION: 2, - UPDATE_SUBSCRIPTION: 3, - CANCEL_SUBSCRIPTION: 4 + REDIRECT_SIGNUP: 1, + CREATE_SUBSCRIPTION: 2, + UPDATE_SUBSCRIPTION: 3, + CANCEL_SUBSCRIPTION: 4, }; const Banner = { - CANCEL_WARNING: 1, - PRORATION_INFO: 2, - RESERVATIONS_WARNING: 3 + CANCEL_WARNING: 1, + PRORATION_INFO: 2, + RESERVATIONS_WARNING: 3, }; export default UpgradeDialog; diff --git a/web/src/components/hooks.js b/web/src/components/hooks.js index b1ce8ffb..0fc0204d 100644 --- a/web/src/components/hooks.js +++ b/web/src/components/hooks.js @@ -1,7 +1,7 @@ -import {useNavigate, useParams} from "react-router-dom"; -import {useEffect, useState} from "react"; +import { useNavigate, useParams } from "react-router-dom"; +import { useEffect, useState } from "react"; import subscriptionManager from "../app/SubscriptionManager"; -import {disallowedTopic, expandSecureUrl, topicUrl} from "../app/utils"; +import { disallowedTopic, expandSecureUrl, topicUrl } from "../app/utils"; import notifier from "../app/Notifier"; import routes from "./routes"; import connectionManager from "../app/ConnectionManager"; @@ -9,7 +9,7 @@ import poller from "../app/Poller"; import pruner from "../app/Pruner"; import session from "../app/Session"; import accountApi from "../app/AccountApi"; -import {UnauthorizedError} from "../app/errors"; +import { UnauthorizedError } from "../app/errors"; /** * Wire connectionManager and subscriptionManager so that subscriptions are updated when the connection @@ -17,65 +17,82 @@ import {UnauthorizedError} from "../app/errors"; * to the connection being re-established). */ export const useConnectionListeners = (account, subscriptions, users) => { - const navigate = useNavigate(); + const navigate = useNavigate(); - // Register listeners for incoming messages, and connection state changes - useEffect(() => { - const handleMessage = async (subscriptionId, message) => { - const subscription = await subscriptionManager.get(subscriptionId); - if (subscription.internal) { - await handleInternalMessage(message); - } else { - await handleNotification(subscriptionId, message); - } - }; - - const handleInternalMessage = async (message) => { - console.log(`[ConnectionListener] Received message on sync topic`, message.message); - try { - const data = JSON.parse(message.message); - if (data.event === "sync") { - console.log(`[ConnectionListener] Triggering account sync`); - await accountApi.sync(); - } else { - console.log(`[ConnectionListener] Unknown message type. Doing nothing.`); - } - } catch (e) { - console.log(`[ConnectionListener] Error parsing sync topic message`, e); - } - }; - - const handleNotification = async (subscriptionId, notification) => { - const added = await subscriptionManager.addNotification(subscriptionId, notification); - if (added) { - const defaultClickAction = (subscription) => navigate(routes.forSubscription(subscription)); - await notifier.notify(subscriptionId, notification, defaultClickAction) - } - }; - connectionManager.registerStateListener(subscriptionManager.updateState); - connectionManager.registerMessageListener(handleMessage); - return () => { - connectionManager.resetStateListener(); - connectionManager.resetMessageListener(); - } - }, - // We have to disable dep checking for "navigate". This is fine, it never changes. - // eslint-disable-next-line - [] - ); - - // Sync topic listener: For accounts with sync_topic, subscribe to an internal topic - useEffect(() => { - if (!account || !account.sync_topic) { - return; + // Register listeners for incoming messages, and connection state changes + useEffect( + () => { + const handleMessage = async (subscriptionId, message) => { + const subscription = await subscriptionManager.get(subscriptionId); + if (subscription.internal) { + await handleInternalMessage(message); + } else { + await handleNotification(subscriptionId, message); } - subscriptionManager.add(config.base_url, account.sync_topic, true); // Dangle! - }, [account]); + }; - // When subscriptions or users change, refresh the connections - useEffect(() => { - connectionManager.refresh(subscriptions, users); // Dangle - }, [subscriptions, users]); + const handleInternalMessage = async (message) => { + console.log( + `[ConnectionListener] Received message on sync topic`, + message.message + ); + try { + const data = JSON.parse(message.message); + if (data.event === "sync") { + console.log(`[ConnectionListener] Triggering account sync`); + await accountApi.sync(); + } else { + console.log( + `[ConnectionListener] Unknown message type. Doing nothing.` + ); + } + } catch (e) { + console.log( + `[ConnectionListener] Error parsing sync topic message`, + e + ); + } + }; + + const handleNotification = async (subscriptionId, notification) => { + const added = await subscriptionManager.addNotification( + subscriptionId, + notification + ); + if (added) { + const defaultClickAction = (subscription) => + navigate(routes.forSubscription(subscription)); + await notifier.notify( + subscriptionId, + notification, + defaultClickAction + ); + } + }; + connectionManager.registerStateListener(subscriptionManager.updateState); + connectionManager.registerMessageListener(handleMessage); + return () => { + connectionManager.resetStateListener(); + connectionManager.resetMessageListener(); + }; + }, + // We have to disable dep checking for "navigate". This is fine, it never changes. + // eslint-disable-next-line + [] + ); + + // Sync topic listener: For accounts with sync_topic, subscribe to an internal topic + useEffect(() => { + if (!account || !account.sync_topic) { + return; + } + subscriptionManager.add(config.base_url, account.sync_topic, true); // Dangle! + }, [account]); + + // When subscriptions or users change, refresh the connections + useEffect(() => { + connectionManager.refresh(subscriptions, users); // Dangle + }, [subscriptions, users]); }; /** @@ -83,35 +100,43 @@ export const useConnectionListeners = (account, subscriptions, users) => { * This will only be run once after the initial page load. */ export const useAutoSubscribe = (subscriptions, selected) => { - const [hasRun, setHasRun] = useState(false); - const params = useParams(); + const [hasRun, setHasRun] = useState(false); + const params = useParams(); - useEffect(() => { - const loaded = subscriptions !== null && subscriptions !== undefined; - if (!loaded || hasRun) { - return; + useEffect(() => { + const loaded = subscriptions !== null && subscriptions !== undefined; + if (!loaded || hasRun) { + return; + } + setHasRun(true); + const eligible = + params.topic && !selected && !disallowedTopic(params.topic); + if (eligible) { + const baseUrl = params.baseUrl + ? expandSecureUrl(params.baseUrl) + : config.base_url; + console.log( + `[Hooks] Auto-subscribing to ${topicUrl(baseUrl, params.topic)}` + ); + (async () => { + const subscription = await subscriptionManager.add( + baseUrl, + params.topic + ); + if (session.exists()) { + try { + await accountApi.addSubscription(baseUrl, params.topic); + } catch (e) { + console.log(`[Hooks] Auto-subscribing failed`, e); + if (e instanceof UnauthorizedError) { + session.resetAndRedirect(routes.login); + } + } } - setHasRun(true); - const eligible = params.topic && !selected && !disallowedTopic(params.topic); - if (eligible) { - const baseUrl = (params.baseUrl) ? expandSecureUrl(params.baseUrl) : config.base_url; - console.log(`[Hooks] Auto-subscribing to ${topicUrl(baseUrl, params.topic)}`); - (async () => { - const subscription = await subscriptionManager.add(baseUrl, params.topic); - if (session.exists()) { - try { - await accountApi.addSubscription(baseUrl, params.topic); - } catch (e) { - console.log(`[Hooks] Auto-subscribing failed`, e); - if (e instanceof UnauthorizedError) { - session.resetAndRedirect(routes.login); - } - } - } - poller.pollInBackground(subscription); // Dangle! - })(); - } - }, [params, subscriptions, selected, hasRun]); + poller.pollInBackground(subscription); // Dangle! + })(); + } + }, [params, subscriptions, selected, hasRun]); }; /** @@ -120,19 +145,19 @@ export const useAutoSubscribe = (subscriptions, selected) => { * up "unused" imports. See https://github.com/binwiederhier/ntfy/issues/186. */ export const useBackgroundProcesses = () => { - useEffect(() => { - poller.startWorker(); - pruner.startWorker(); - accountApi.startWorker(); - }, []); -} + useEffect(() => { + poller.startWorker(); + pruner.startWorker(); + accountApi.startWorker(); + }, []); +}; export const useAccountListener = (setAccount) => { - useEffect(() => { - accountApi.registerListener(setAccount); - accountApi.sync(); // Dangle - return () => { - accountApi.resetListener(); - } - }, []); -} + useEffect(() => { + accountApi.registerListener(setAccount); + accountApi.sync(); // Dangle + return () => { + accountApi.resetListener(); + }; + }, []); +}; diff --git a/web/src/components/i18n.js b/web/src/components/i18n.js index 42eb5721..2bc315c0 100644 --- a/web/src/components/i18n.js +++ b/web/src/components/i18n.js @@ -1,7 +1,7 @@ -import i18n from 'i18next'; -import Backend from 'i18next-http-backend'; -import LanguageDetector from 'i18next-browser-languagedetector'; -import { initReactI18next } from 'react-i18next'; +import i18n from "i18next"; +import Backend from "i18next-http-backend"; +import LanguageDetector from "i18next-browser-languagedetector"; +import { initReactI18next } from "react-i18next"; // Translations using i18next // - Options: https://www.i18next.com/overview/configuration-options @@ -12,18 +12,18 @@ import { initReactI18next } from 'react-i18next'; // https://github.com/i18next/react-i18next/tree/master/example/react i18n - .use(Backend) - .use(LanguageDetector) - .use(initReactI18next) - .init({ - fallbackLng: 'en', - debug: true, - interpolation: { - escapeValue: false, // not needed for react as it escapes by default - }, - backend: { - loadPath: '/static/langs/{{lng}}.json', - } - }); + .use(Backend) + .use(LanguageDetector) + .use(initReactI18next) + .init({ + fallbackLng: "en", + debug: true, + interpolation: { + escapeValue: false, // not needed for react as it escapes by default + }, + backend: { + loadPath: "/static/langs/{{lng}}.json", + }, + }); export default i18n; diff --git a/web/src/components/routes.js b/web/src/components/routes.js index d1db160a..17e0eac6 100644 --- a/web/src/components/routes.js +++ b/web/src/components/routes.js @@ -1,20 +1,20 @@ import config from "../app/config"; -import {shortUrl} from "../app/utils"; +import { shortUrl } from "../app/utils"; const routes = { - login: "/login", - signup: "/signup", - app: config.app_root, - account: "/account", - settings: "/settings", - subscription: "/:topic", - subscriptionExternal: "/:baseUrl/:topic", - forSubscription: (subscription) => { - if (subscription.baseUrl !== config.base_url) { - return `/${shortUrl(subscription.baseUrl)}/${subscription.topic}`; - } - return `/${subscription.topic}`; + login: "/login", + signup: "/signup", + app: config.app_root, + account: "/account", + settings: "/settings", + subscription: "/:topic", + subscriptionExternal: "/:baseUrl/:topic", + forSubscription: (subscription) => { + if (subscription.baseUrl !== config.base_url) { + return `/${shortUrl(subscription.baseUrl)}/${subscription.topic}`; } + return `/${subscription.topic}`; + }, }; export default routes; diff --git a/web/src/components/styles.js b/web/src/components/styles.js index d6127941..6f1e30b8 100644 --- a/web/src/components/styles.js +++ b/web/src/components/styles.js @@ -1,7 +1,7 @@ import Typography from "@mui/material/Typography"; import theme from "./theme"; import Container from "@mui/material/Container"; -import {Backdrop, styled} from "@mui/material"; +import { Backdrop, styled } from "@mui/material"; export const Paragraph = styled(Typography)({ paddingTop: 8, @@ -9,14 +9,14 @@ export const Paragraph = styled(Typography)({ }); export const VerticallyCenteredContainer = styled(Container)({ - display: 'flex', + display: "flex", flexGrow: 1, - flexDirection: 'column', - justifyContent: 'center', - alignContent: 'center', - color: theme.palette.text.primary + flexDirection: "column", + justifyContent: "center", + alignContent: "center", + color: theme.palette.text.primary, }); export const LightboxBackdrop = styled(Backdrop)({ - backgroundColor: 'rgba(0, 0, 0, 0.8)' // was: 0.5 + backgroundColor: "rgba(0, 0, 0, 0.8)", // was: 0.5 }); diff --git a/web/src/components/theme.js b/web/src/components/theme.js index 3fdafae8..ca77cdc8 100644 --- a/web/src/components/theme.js +++ b/web/src/components/theme.js @@ -1,13 +1,13 @@ -import { red } from '@mui/material/colors'; -import { createTheme } from '@mui/material/styles'; +import { red } from "@mui/material/colors"; +import { createTheme } from "@mui/material/styles"; const theme = createTheme({ palette: { primary: { - main: '#338574', + main: "#338574", }, secondary: { - main: '#6cead0', + main: "#6cead0", }, error: { main: red.A400, @@ -17,19 +17,19 @@ const theme = createTheme({ MuiListItemIcon: { styleOverrides: { root: { - minWidth: '36px', + minWidth: "36px", }, }, }, MuiCardContent: { styleOverrides: { root: { - ':last-child': { - paddingBottom: '16px' - } - } - } - } + ":last-child": { + paddingBottom: "16px", + }, + }, + }, + }, }, }); diff --git a/web/src/index.js b/web/src/index.js index 659bcb8f..d60c05a4 100644 --- a/web/src/index.js +++ b/web/src/index.js @@ -1,6 +1,6 @@ -import * as React from 'react'; -import { createRoot } from 'react-dom/client'; -import App from './components/App'; +import * as React from "react"; +import { createRoot } from "react-dom/client"; +import App from "./components/App"; -const root = createRoot(document.querySelector('#root')); +const root = createRoot(document.querySelector("#root")); root.render();