diff --git a/.docker/selfhost/schema.json b/.docker/selfhost/schema.json index d8356b1dc1..4e3405b820 100644 --- a/.docker/selfhost/schema.json +++ b/.docker/selfhost/schema.json @@ -148,6 +148,11 @@ "description": "Whether allow new registrations.\n@default true", "default": true }, + "allowSignupForOauth": { + "type": "boolean", + "description": "Whether allow new registrations via configured oauth.\n@default true", + "default": true + }, "requireEmailDomainVerification": { "type": "boolean", "description": "Whether require email domain record verification before accessing restricted resources.\n@default false", @@ -190,6 +195,11 @@ "type": "object", "description": "Configuration for mailer module", "properties": { + "SMTP.name": { + "type": "string", + "description": "Name of the email server (e.g. your domain name)\n@default \"AFFiNE Server\"\n@environment `MAILER_SERVERNAME`", + "default": "AFFiNE Server" + }, "SMTP.host": { "type": "string", "description": "Host of the email server (e.g. smtp.gmail.com)\n@default \"\"\n@environment `MAILER_HOST`", @@ -225,6 +235,11 @@ "description": "The emails from these domains are always sent using the fallback SMTP server.\n@default []", "default": [] }, + "fallbackSMTP.name": { + "type": "string", + "description": "Name of the fallback email server (e.g. your domain name)\n@default \"AFFiNE Server\"", + "default": "AFFiNE Server" + }, "fallbackSMTP.host": { "type": "string", "description": "Host of the email server (e.g. smtp.gmail.com)\n@default \"\"", @@ -1093,18 +1108,33 @@ }, "apiKey": { "type": "string", - "description": "Stripe API key to enable payment service.\n@default \"\"\n@environment `STRIPE_API_KEY`", + "description": "[Deprecated] Stripe API key. Use payment.stripe.apiKey instead.\n@default \"\"\n@environment `STRIPE_API_KEY`", "default": "" }, "webhookKey": { "type": "string", - "description": "Stripe webhook key to enable payment service.\n@default \"\"\n@environment `STRIPE_WEBHOOK_KEY`", + "description": "[Deprecated] Stripe webhook key. Use payment.stripe.webhookKey instead.\n@default \"\"\n@environment `STRIPE_WEBHOOK_KEY`", "default": "" }, "stripe": { "type": "object", - "description": "Stripe sdk options\n@default {}\n@link https://docs.stripe.com/api", - "default": {} + "description": "Stripe sdk options and credentials\n@default {\"apiKey\":\"\",\"webhookKey\":\"\"}\n@link https://docs.stripe.com/api", + "default": { + "apiKey": "", + "webhookKey": "" + } + }, + "revenuecat": { + "type": "object", + "description": "RevenueCat integration configs\n@default {\"enabled\":false,\"apiKey\":\"\",\"projectId\":\"\",\"webhookAuth\":\"\",\"environment\":\"production\",\"productMap\":{}}\n@link https://www.revenuecat.com/docs/", + "default": { + "enabled": false, + "apiKey": "", + "projectId": "", + "webhookAuth": "", + "environment": "production", + "productMap": {} + } } } }, diff --git a/README.md b/README.md index 8e188dfba4..da2fcc4f43 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ Star us, and you will receive all release notifications from GitHub without any **Multimodal AI partner ready to kick in any work** -- Write up professional work report? Turn an outline into expressive and presentable slides? Summary an article into a well-structured mindmap? Sorting your job plan and backlog for tasks? Or... draw and code prototype apps and web pages directly all with one prompt? With you, [AFFiNE AI](https://affine.pro/ai) pushes your creativity to the edge of your imagination,just like [Canvas AI](https://affine.pro/blog/best-canvas-ai) to generate mind map for brainstorming. +- Write up professional work report? Turn an outline into expressive and presentable slides? Summary an article into a well-structured mindmap? Sorting your job plan and backlog for tasks? Or... draw and code prototype apps and web pages directly all with one prompt? With you, [AFFiNE AI](https://affine.pro/ai) pushes your creativity to the edge of your imagination, just like [Canvas AI](https://affine.pro/blog/best-canvas-ai) to generate mind map for brainstorming. **Local-first & Real-time collaborative** diff --git a/blocksuite/affine/blocks/attachment/package.json b/blocksuite/affine/blocks/attachment/package.json index b875dcff88..f79bc1551c 100644 --- a/blocksuite/affine/blocks/attachment/package.json +++ b/blocksuite/affine/blocks/attachment/package.json @@ -17,7 +17,7 @@ "@blocksuite/affine-shared": "workspace:*", "@blocksuite/affine-widget-slash-menu": "workspace:*", "@blocksuite/global": "workspace:*", - "@blocksuite/icons": "^2.2.12", + "@blocksuite/icons": "^2.2.17", "@blocksuite/std": "workspace:*", "@blocksuite/store": "workspace:*", "@floating-ui/dom": "^1.6.13", diff --git a/blocksuite/affine/blocks/bookmark/package.json b/blocksuite/affine/blocks/bookmark/package.json index a08778a501..5f493ae4b9 100644 --- a/blocksuite/affine/blocks/bookmark/package.json +++ b/blocksuite/affine/blocks/bookmark/package.json @@ -19,7 +19,7 @@ "@blocksuite/affine-shared": "workspace:*", "@blocksuite/affine-widget-slash-menu": "workspace:*", "@blocksuite/global": "workspace:*", - "@blocksuite/icons": "^2.2.12", + "@blocksuite/icons": "^2.2.17", "@blocksuite/std": "workspace:*", "@blocksuite/store": "workspace:*", "@lit/context": "^1.1.2", diff --git a/blocksuite/affine/blocks/callout/package.json b/blocksuite/affine/blocks/callout/package.json index 2505c91b6d..b396853fa5 100644 --- a/blocksuite/affine/blocks/callout/package.json +++ b/blocksuite/affine/blocks/callout/package.json @@ -18,7 +18,7 @@ "@blocksuite/affine-shared": "workspace:*", "@blocksuite/affine-widget-slash-menu": "workspace:*", "@blocksuite/global": "workspace:*", - "@blocksuite/icons": "^2.2.12", + "@blocksuite/icons": "^2.2.17", "@blocksuite/std": "workspace:*", "@blocksuite/store": "workspace:*", "@emoji-mart/data": "^1.2.1", diff --git a/blocksuite/affine/blocks/code/package.json b/blocksuite/affine/blocks/code/package.json index fd25a5f5e5..04c74b9e00 100644 --- a/blocksuite/affine/blocks/code/package.json +++ b/blocksuite/affine/blocks/code/package.json @@ -22,7 +22,7 @@ "@blocksuite/affine-shared": "workspace:*", "@blocksuite/affine-widget-slash-menu": "workspace:*", "@blocksuite/global": "workspace:*", - "@blocksuite/icons": "^2.2.12", + "@blocksuite/icons": "^2.2.17", "@blocksuite/std": "workspace:*", "@blocksuite/store": "workspace:*", "@floating-ui/dom": "^1.6.13", diff --git a/blocksuite/affine/blocks/data-view/package.json b/blocksuite/affine/blocks/data-view/package.json index bd38ff0086..993c50fe44 100644 --- a/blocksuite/affine/blocks/data-view/package.json +++ b/blocksuite/affine/blocks/data-view/package.json @@ -18,7 +18,7 @@ "@blocksuite/affine-widget-slash-menu": "workspace:*", "@blocksuite/data-view": "workspace:*", "@blocksuite/global": "workspace:*", - "@blocksuite/icons": "^2.2.12", + "@blocksuite/icons": "^2.2.17", "@blocksuite/std": "workspace:*", "@blocksuite/store": "workspace:*", "@floating-ui/dom": "^1.6.13", diff --git a/blocksuite/affine/blocks/database/package.json b/blocksuite/affine/blocks/database/package.json index 36d943de19..272e9c759b 100644 --- a/blocksuite/affine/blocks/database/package.json +++ b/blocksuite/affine/blocks/database/package.json @@ -21,7 +21,7 @@ "@blocksuite/affine-widget-slash-menu": "workspace:*", "@blocksuite/data-view": "workspace:*", "@blocksuite/global": "workspace:*", - "@blocksuite/icons": "^2.2.12", + "@blocksuite/icons": "^2.2.17", "@blocksuite/std": "workspace:*", "@blocksuite/store": "workspace:*", "@emotion/css": "^11.13.5", diff --git a/blocksuite/affine/blocks/edgeless-text/package.json b/blocksuite/affine/blocks/edgeless-text/package.json index 564f5f9d1f..4643642b52 100644 --- a/blocksuite/affine/blocks/edgeless-text/package.json +++ b/blocksuite/affine/blocks/edgeless-text/package.json @@ -20,7 +20,7 @@ "@blocksuite/affine-shared": "workspace:*", "@blocksuite/affine-widget-edgeless-toolbar": "workspace:*", "@blocksuite/global": "workspace:*", - "@blocksuite/icons": "^2.2.12", + "@blocksuite/icons": "^2.2.17", "@blocksuite/std": "workspace:*", "@blocksuite/store": "workspace:*", "@floating-ui/dom": "^1.6.13", diff --git a/blocksuite/affine/blocks/embed-doc/package.json b/blocksuite/affine/blocks/embed-doc/package.json index bf847107f4..06f0cfab35 100644 --- a/blocksuite/affine/blocks/embed-doc/package.json +++ b/blocksuite/affine/blocks/embed-doc/package.json @@ -20,7 +20,7 @@ "@blocksuite/affine-shared": "workspace:*", "@blocksuite/affine-widget-slash-menu": "workspace:*", "@blocksuite/global": "workspace:*", - "@blocksuite/icons": "^2.2.12", + "@blocksuite/icons": "^2.2.17", "@blocksuite/std": "workspace:*", "@blocksuite/store": "workspace:*", "@floating-ui/dom": "^1.6.13", diff --git a/blocksuite/affine/blocks/embed/package.json b/blocksuite/affine/blocks/embed/package.json index 855a170dc6..3237a7776f 100644 --- a/blocksuite/affine/blocks/embed/package.json +++ b/blocksuite/affine/blocks/embed/package.json @@ -20,7 +20,7 @@ "@blocksuite/affine-shared": "workspace:*", "@blocksuite/affine-widget-slash-menu": "workspace:*", "@blocksuite/global": "workspace:*", - "@blocksuite/icons": "^2.2.12", + "@blocksuite/icons": "^2.2.17", "@blocksuite/std": "workspace:*", "@blocksuite/store": "workspace:*", "@floating-ui/dom": "^1.6.13", diff --git a/blocksuite/affine/blocks/frame/package.json b/blocksuite/affine/blocks/frame/package.json index c313333909..95913aff31 100644 --- a/blocksuite/affine/blocks/frame/package.json +++ b/blocksuite/affine/blocks/frame/package.json @@ -19,7 +19,7 @@ "@blocksuite/affine-widget-edgeless-toolbar": "workspace:*", "@blocksuite/affine-widget-frame-title": "workspace:*", "@blocksuite/global": "workspace:*", - "@blocksuite/icons": "^2.2.12", + "@blocksuite/icons": "^2.2.17", "@blocksuite/std": "workspace:*", "@blocksuite/store": "workspace:*", "@floating-ui/dom": "^1.6.13", diff --git a/blocksuite/affine/blocks/image/package.json b/blocksuite/affine/blocks/image/package.json index 6f1f64e981..d582d7a9d2 100644 --- a/blocksuite/affine/blocks/image/package.json +++ b/blocksuite/affine/blocks/image/package.json @@ -19,7 +19,7 @@ "@blocksuite/affine-shared": "workspace:*", "@blocksuite/affine-widget-slash-menu": "workspace:*", "@blocksuite/global": "workspace:*", - "@blocksuite/icons": "^2.2.12", + "@blocksuite/icons": "^2.2.17", "@blocksuite/std": "workspace:*", "@blocksuite/store": "workspace:*", "@floating-ui/dom": "^1.6.13", diff --git a/blocksuite/affine/blocks/latex/package.json b/blocksuite/affine/blocks/latex/package.json index 55b86d3c48..b0fa652961 100644 --- a/blocksuite/affine/blocks/latex/package.json +++ b/blocksuite/affine/blocks/latex/package.json @@ -19,7 +19,7 @@ "@blocksuite/affine-shared": "workspace:*", "@blocksuite/affine-widget-slash-menu": "workspace:*", "@blocksuite/global": "workspace:*", - "@blocksuite/icons": "^2.2.12", + "@blocksuite/icons": "^2.2.17", "@blocksuite/std": "workspace:*", "@blocksuite/store": "workspace:*", "@floating-ui/dom": "^1.6.13", diff --git a/blocksuite/affine/blocks/list/package.json b/blocksuite/affine/blocks/list/package.json index e342ee99a7..f03b9e2f47 100644 --- a/blocksuite/affine/blocks/list/package.json +++ b/blocksuite/affine/blocks/list/package.json @@ -18,7 +18,7 @@ "@blocksuite/affine-rich-text": "workspace:*", "@blocksuite/affine-shared": "workspace:*", "@blocksuite/global": "workspace:*", - "@blocksuite/icons": "^2.2.12", + "@blocksuite/icons": "^2.2.17", "@blocksuite/std": "workspace:*", "@blocksuite/store": "workspace:*", "@floating-ui/dom": "^1.6.13", diff --git a/blocksuite/affine/blocks/note/package.json b/blocksuite/affine/blocks/note/package.json index 3c7175223e..913dd543b6 100644 --- a/blocksuite/affine/blocks/note/package.json +++ b/blocksuite/affine/blocks/note/package.json @@ -22,7 +22,7 @@ "@blocksuite/affine-shared": "workspace:*", "@blocksuite/affine-widget-slash-menu": "workspace:*", "@blocksuite/global": "workspace:*", - "@blocksuite/icons": "^2.2.12", + "@blocksuite/icons": "^2.2.17", "@blocksuite/std": "workspace:*", "@blocksuite/store": "workspace:*", "@lit/context": "^1.1.2", diff --git a/blocksuite/affine/blocks/root/package.json b/blocksuite/affine/blocks/root/package.json index 95d43349b6..56be69e4db 100644 --- a/blocksuite/affine/blocks/root/package.json +++ b/blocksuite/affine/blocks/root/package.json @@ -38,7 +38,7 @@ "@blocksuite/affine-widget-edgeless-toolbar": "workspace:*", "@blocksuite/data-view": "workspace:*", "@blocksuite/global": "workspace:*", - "@blocksuite/icons": "^2.2.12", + "@blocksuite/icons": "^2.2.17", "@blocksuite/std": "workspace:*", "@blocksuite/store": "workspace:*", "@floating-ui/dom": "^1.6.13", diff --git a/blocksuite/affine/blocks/surface-ref/package.json b/blocksuite/affine/blocks/surface-ref/package.json index 69f40b90ee..08bc03586e 100644 --- a/blocksuite/affine/blocks/surface-ref/package.json +++ b/blocksuite/affine/blocks/surface-ref/package.json @@ -19,7 +19,7 @@ "@blocksuite/affine-shared": "workspace:*", "@blocksuite/affine-widget-slash-menu": "workspace:*", "@blocksuite/global": "workspace:*", - "@blocksuite/icons": "^2.2.12", + "@blocksuite/icons": "^2.2.17", "@blocksuite/std": "workspace:*", "@blocksuite/store": "workspace:*", "@floating-ui/dom": "^1.6.13", diff --git a/blocksuite/affine/blocks/table/package.json b/blocksuite/affine/blocks/table/package.json index 7f7d13eff0..a07b491822 100644 --- a/blocksuite/affine/blocks/table/package.json +++ b/blocksuite/affine/blocks/table/package.json @@ -20,7 +20,7 @@ "@blocksuite/affine-widget-slash-menu": "workspace:*", "@blocksuite/data-view": "workspace:*", "@blocksuite/global": "workspace:*", - "@blocksuite/icons": "^2.2.12", + "@blocksuite/icons": "^2.2.17", "@blocksuite/std": "workspace:*", "@blocksuite/store": "workspace:*", "@emotion/css": "^11.13.5", diff --git a/blocksuite/affine/components/package.json b/blocksuite/affine/components/package.json index 8cdeb59946..a218db52fc 100644 --- a/blocksuite/affine/components/package.json +++ b/blocksuite/affine/components/package.json @@ -13,7 +13,7 @@ "@blocksuite/affine-model": "workspace:*", "@blocksuite/affine-shared": "workspace:*", "@blocksuite/global": "workspace:*", - "@blocksuite/icons": "^2.2.12", + "@blocksuite/icons": "^2.2.17", "@blocksuite/std": "workspace:*", "@blocksuite/store": "workspace:*", "@blocksuite/sync": "workspace:*", diff --git a/blocksuite/affine/components/src/context-menu/button.ts b/blocksuite/affine/components/src/context-menu/button.ts index 9aef8269aa..aab7bb9393 100644 --- a/blocksuite/affine/components/src/context-menu/button.ts +++ b/blocksuite/affine/components/src/context-menu/button.ts @@ -193,6 +193,7 @@ export const menuButtonItems = { (config: { name: string; label?: () => TemplateResult; + info?: TemplateResult; prefix?: TemplateResult; postfix?: TemplateResult; isSelected?: boolean; @@ -211,7 +212,7 @@ export const menuButtonItems = { return html` ${config.prefix}
- ${config.label?.() ?? config.name} + ${config.label?.() ?? config.name} ${config.info}
${config.postfix ?? (config.isSelected ? DoneIcon() : undefined)} `; diff --git a/blocksuite/affine/data-view/package.json b/blocksuite/affine/data-view/package.json index 038a1120e9..047f18895e 100644 --- a/blocksuite/affine/data-view/package.json +++ b/blocksuite/affine/data-view/package.json @@ -13,7 +13,7 @@ "@blocksuite/affine-components": "workspace:*", "@blocksuite/affine-shared": "workspace:*", "@blocksuite/global": "workspace:*", - "@blocksuite/icons": "^2.2.12", + "@blocksuite/icons": "^2.2.17", "@blocksuite/std": "workspace:*", "@blocksuite/store": "workspace:*", "@emotion/css": "^11.13.5", diff --git a/blocksuite/affine/foundation/package.json b/blocksuite/affine/foundation/package.json index e20c0148cb..0410882926 100644 --- a/blocksuite/affine/foundation/package.json +++ b/blocksuite/affine/foundation/package.json @@ -16,7 +16,7 @@ "@blocksuite/affine-shared": "workspace:*", "@blocksuite/data-view": "workspace:*", "@blocksuite/global": "workspace:*", - "@blocksuite/icons": "^2.2.12", + "@blocksuite/icons": "^2.2.17", "@blocksuite/std": "workspace:*", "@blocksuite/store": "workspace:*", "@floating-ui/dom": "^1.6.13", diff --git a/blocksuite/affine/fragments/adapter-panel/package.json b/blocksuite/affine/fragments/adapter-panel/package.json index 9f94637d37..82dd6316d2 100644 --- a/blocksuite/affine/fragments/adapter-panel/package.json +++ b/blocksuite/affine/fragments/adapter-panel/package.json @@ -15,7 +15,7 @@ "@blocksuite/affine-model": "workspace:*", "@blocksuite/affine-shared": "workspace:*", "@blocksuite/global": "workspace:*", - "@blocksuite/icons": "^2.2.12", + "@blocksuite/icons": "^2.2.17", "@blocksuite/std": "workspace:*", "@blocksuite/store": "workspace:*", "@floating-ui/dom": "^1.6.13", diff --git a/blocksuite/affine/fragments/doc-title/package.json b/blocksuite/affine/fragments/doc-title/package.json index e8894a3b6d..b6ecb479d5 100644 --- a/blocksuite/affine/fragments/doc-title/package.json +++ b/blocksuite/affine/fragments/doc-title/package.json @@ -18,7 +18,7 @@ "@blocksuite/affine-rich-text": "workspace:*", "@blocksuite/affine-shared": "workspace:*", "@blocksuite/global": "workspace:*", - "@blocksuite/icons": "^2.2.12", + "@blocksuite/icons": "^2.2.17", "@blocksuite/std": "workspace:*", "@blocksuite/store": "workspace:*", "@floating-ui/dom": "^1.6.13", diff --git a/blocksuite/affine/fragments/doc-title/src/doc-title.ts b/blocksuite/affine/fragments/doc-title/src/doc-title.ts index acf3baab4d..61c966c580 100644 --- a/blocksuite/affine/fragments/doc-title/src/doc-title.ts +++ b/blocksuite/affine/fragments/doc-title/src/doc-title.ts @@ -19,6 +19,7 @@ const DOC_BLOCK_CHILD_PADDING = 24; export class DocTitle extends WithDisposable(ShadowlessElement) { static override styles = css` + .doc-icon-container, .doc-title-container { box-sizing: border-box; font-family: var(--affine-font-family); @@ -49,6 +50,7 @@ export class DocTitle extends WithDisposable(ShadowlessElement) { /* Extra small devices (phones, 640px and down) */ @container viewport (width <= 640px) { + .doc-icon-container, .doc-title-container { padding-left: ${DOC_BLOCK_CHILD_PADDING}px; padding-right: ${DOC_BLOCK_CHILD_PADDING}px; diff --git a/blocksuite/affine/fragments/frame-panel/package.json b/blocksuite/affine/fragments/frame-panel/package.json index dc8c76dccc..1f9ee512c1 100644 --- a/blocksuite/affine/fragments/frame-panel/package.json +++ b/blocksuite/affine/fragments/frame-panel/package.json @@ -18,7 +18,7 @@ "@blocksuite/affine-rich-text": "workspace:*", "@blocksuite/affine-shared": "workspace:*", "@blocksuite/global": "workspace:*", - "@blocksuite/icons": "^2.2.12", + "@blocksuite/icons": "^2.2.17", "@blocksuite/std": "workspace:*", "@blocksuite/store": "workspace:*", "@floating-ui/dom": "^1.6.13", diff --git a/blocksuite/affine/fragments/outline/package.json b/blocksuite/affine/fragments/outline/package.json index b763cea5b9..7bafbcbc0e 100644 --- a/blocksuite/affine/fragments/outline/package.json +++ b/blocksuite/affine/fragments/outline/package.json @@ -18,7 +18,7 @@ "@blocksuite/affine-rich-text": "workspace:*", "@blocksuite/affine-shared": "workspace:*", "@blocksuite/global": "workspace:*", - "@blocksuite/icons": "^2.2.12", + "@blocksuite/icons": "^2.2.17", "@blocksuite/std": "workspace:*", "@blocksuite/store": "workspace:*", "@floating-ui/dom": "^1.6.13", diff --git a/blocksuite/affine/gfx/brush/package.json b/blocksuite/affine/gfx/brush/package.json index 73af5abc7b..f1c2720f2c 100644 --- a/blocksuite/affine/gfx/brush/package.json +++ b/blocksuite/affine/gfx/brush/package.json @@ -18,7 +18,7 @@ "@blocksuite/affine-shared": "workspace:*", "@blocksuite/affine-widget-edgeless-toolbar": "workspace:*", "@blocksuite/global": "workspace:*", - "@blocksuite/icons": "^2.2.12", + "@blocksuite/icons": "^2.2.17", "@blocksuite/std": "workspace:*", "@blocksuite/store": "workspace:*", "@lit/context": "^1.1.2", diff --git a/blocksuite/affine/gfx/connector/package.json b/blocksuite/affine/gfx/connector/package.json index b9c23cb440..babd671915 100644 --- a/blocksuite/affine/gfx/connector/package.json +++ b/blocksuite/affine/gfx/connector/package.json @@ -19,7 +19,7 @@ "@blocksuite/affine-shared": "workspace:*", "@blocksuite/affine-widget-edgeless-toolbar": "workspace:*", "@blocksuite/global": "workspace:*", - "@blocksuite/icons": "^2.2.12", + "@blocksuite/icons": "^2.2.17", "@blocksuite/std": "workspace:*", "@blocksuite/store": "workspace:*", "@lit/context": "^1.1.2", diff --git a/blocksuite/affine/gfx/group/package.json b/blocksuite/affine/gfx/group/package.json index 828bbbfb43..98b557c92e 100644 --- a/blocksuite/affine/gfx/group/package.json +++ b/blocksuite/affine/gfx/group/package.json @@ -19,7 +19,7 @@ "@blocksuite/affine-shared": "workspace:*", "@blocksuite/affine-widget-edgeless-toolbar": "workspace:*", "@blocksuite/global": "workspace:*", - "@blocksuite/icons": "^2.2.12", + "@blocksuite/icons": "^2.2.17", "@blocksuite/std": "workspace:*", "@blocksuite/store": "workspace:*", "@lit/context": "^1.1.2", diff --git a/blocksuite/affine/gfx/link/package.json b/blocksuite/affine/gfx/link/package.json index 291d26b2ea..2064622b29 100644 --- a/blocksuite/affine/gfx/link/package.json +++ b/blocksuite/affine/gfx/link/package.json @@ -21,7 +21,7 @@ "@blocksuite/affine-shared": "workspace:*", "@blocksuite/affine-widget-edgeless-toolbar": "workspace:*", "@blocksuite/global": "workspace:*", - "@blocksuite/icons": "^2.2.12", + "@blocksuite/icons": "^2.2.17", "@blocksuite/std": "workspace:*", "@blocksuite/store": "workspace:*", "@lit/context": "^1.1.2", diff --git a/blocksuite/affine/gfx/mindmap/package.json b/blocksuite/affine/gfx/mindmap/package.json index 959167d574..3615c82fcb 100644 --- a/blocksuite/affine/gfx/mindmap/package.json +++ b/blocksuite/affine/gfx/mindmap/package.json @@ -25,7 +25,7 @@ "@blocksuite/affine-shared": "workspace:*", "@blocksuite/affine-widget-edgeless-toolbar": "workspace:*", "@blocksuite/global": "workspace:*", - "@blocksuite/icons": "^2.2.12", + "@blocksuite/icons": "^2.2.17", "@blocksuite/std": "workspace:*", "@blocksuite/store": "workspace:*", "@lit/context": "^1.1.2", diff --git a/blocksuite/affine/gfx/note/package.json b/blocksuite/affine/gfx/note/package.json index 665dd7d2df..98980b0678 100644 --- a/blocksuite/affine/gfx/note/package.json +++ b/blocksuite/affine/gfx/note/package.json @@ -21,7 +21,7 @@ "@blocksuite/affine-shared": "workspace:*", "@blocksuite/affine-widget-edgeless-toolbar": "workspace:*", "@blocksuite/global": "workspace:*", - "@blocksuite/icons": "^2.2.12", + "@blocksuite/icons": "^2.2.17", "@blocksuite/std": "workspace:*", "@blocksuite/store": "workspace:*", "@lit/context": "^1.1.2", diff --git a/blocksuite/affine/gfx/pointer/package.json b/blocksuite/affine/gfx/pointer/package.json index c41cc3ed50..50af7ea941 100644 --- a/blocksuite/affine/gfx/pointer/package.json +++ b/blocksuite/affine/gfx/pointer/package.json @@ -18,7 +18,7 @@ "@blocksuite/affine-shared": "workspace:*", "@blocksuite/affine-widget-edgeless-toolbar": "workspace:*", "@blocksuite/global": "workspace:*", - "@blocksuite/icons": "^2.2.12", + "@blocksuite/icons": "^2.2.17", "@blocksuite/std": "workspace:*", "@blocksuite/store": "workspace:*", "@lit/context": "^1.1.2", diff --git a/blocksuite/affine/gfx/shape/package.json b/blocksuite/affine/gfx/shape/package.json index 296e60a04c..31f5d3f198 100644 --- a/blocksuite/affine/gfx/shape/package.json +++ b/blocksuite/affine/gfx/shape/package.json @@ -19,7 +19,7 @@ "@blocksuite/affine-shared": "workspace:*", "@blocksuite/affine-widget-edgeless-toolbar": "workspace:*", "@blocksuite/global": "workspace:*", - "@blocksuite/icons": "^2.2.12", + "@blocksuite/icons": "^2.2.17", "@blocksuite/std": "workspace:*", "@blocksuite/store": "workspace:*", "@lit/context": "^1.1.2", diff --git a/blocksuite/affine/gfx/template/package.json b/blocksuite/affine/gfx/template/package.json index cd0a25b5d7..18b9e8c58c 100644 --- a/blocksuite/affine/gfx/template/package.json +++ b/blocksuite/affine/gfx/template/package.json @@ -19,7 +19,7 @@ "@blocksuite/affine-shared": "workspace:*", "@blocksuite/affine-widget-edgeless-toolbar": "workspace:*", "@blocksuite/global": "workspace:*", - "@blocksuite/icons": "^2.2.12", + "@blocksuite/icons": "^2.2.17", "@blocksuite/std": "workspace:*", "@blocksuite/store": "workspace:*", "@floating-ui/dom": "^1.6.13", diff --git a/blocksuite/affine/gfx/text/package.json b/blocksuite/affine/gfx/text/package.json index 6f90200418..db1b10befe 100644 --- a/blocksuite/affine/gfx/text/package.json +++ b/blocksuite/affine/gfx/text/package.json @@ -18,7 +18,7 @@ "@blocksuite/affine-shared": "workspace:*", "@blocksuite/affine-widget-edgeless-toolbar": "workspace:*", "@blocksuite/global": "workspace:*", - "@blocksuite/icons": "^2.2.12", + "@blocksuite/icons": "^2.2.17", "@blocksuite/std": "workspace:*", "@blocksuite/store": "workspace:*", "@lit/context": "^1.1.2", diff --git a/blocksuite/affine/inlines/footnote/package.json b/blocksuite/affine/inlines/footnote/package.json index c9b617eee2..49b60775e1 100644 --- a/blocksuite/affine/inlines/footnote/package.json +++ b/blocksuite/affine/inlines/footnote/package.json @@ -16,7 +16,7 @@ "@blocksuite/affine-model": "workspace:*", "@blocksuite/affine-shared": "workspace:*", "@blocksuite/global": "workspace:*", - "@blocksuite/icons": "^2.2.12", + "@blocksuite/icons": "^2.2.17", "@blocksuite/std": "workspace:*", "@blocksuite/store": "workspace:*", "@floating-ui/dom": "^1.6.13", diff --git a/blocksuite/affine/inlines/latex/package.json b/blocksuite/affine/inlines/latex/package.json index 4c71e35a8b..6a050a644c 100644 --- a/blocksuite/affine/inlines/latex/package.json +++ b/blocksuite/affine/inlines/latex/package.json @@ -17,7 +17,7 @@ "@blocksuite/affine-rich-text": "workspace:*", "@blocksuite/affine-shared": "workspace:*", "@blocksuite/global": "workspace:*", - "@blocksuite/icons": "^2.2.12", + "@blocksuite/icons": "^2.2.17", "@blocksuite/std": "workspace:*", "@blocksuite/store": "workspace:*", "@floating-ui/dom": "^1.6.13", diff --git a/blocksuite/affine/inlines/link/package.json b/blocksuite/affine/inlines/link/package.json index be803582e0..8d92023353 100644 --- a/blocksuite/affine/inlines/link/package.json +++ b/blocksuite/affine/inlines/link/package.json @@ -16,7 +16,7 @@ "@blocksuite/affine-model": "workspace:*", "@blocksuite/affine-shared": "workspace:*", "@blocksuite/global": "workspace:*", - "@blocksuite/icons": "^2.2.12", + "@blocksuite/icons": "^2.2.17", "@blocksuite/std": "workspace:*", "@blocksuite/store": "workspace:*", "@floating-ui/dom": "^1.6.13", diff --git a/blocksuite/affine/inlines/mention/package.json b/blocksuite/affine/inlines/mention/package.json index 134fad242c..10ab71064c 100644 --- a/blocksuite/affine/inlines/mention/package.json +++ b/blocksuite/affine/inlines/mention/package.json @@ -15,7 +15,7 @@ "@blocksuite/affine-model": "workspace:*", "@blocksuite/affine-shared": "workspace:*", "@blocksuite/global": "workspace:*", - "@blocksuite/icons": "^2.2.12", + "@blocksuite/icons": "^2.2.17", "@blocksuite/std": "workspace:*", "@blocksuite/store": "workspace:*", "@floating-ui/dom": "^1.6.13", diff --git a/blocksuite/affine/inlines/preset/package.json b/blocksuite/affine/inlines/preset/package.json index c53003fd01..8dd9241260 100644 --- a/blocksuite/affine/inlines/preset/package.json +++ b/blocksuite/affine/inlines/preset/package.json @@ -22,7 +22,7 @@ "@blocksuite/affine-rich-text": "workspace:*", "@blocksuite/affine-shared": "workspace:*", "@blocksuite/global": "workspace:*", - "@blocksuite/icons": "^2.2.12", + "@blocksuite/icons": "^2.2.17", "@blocksuite/std": "workspace:*", "@blocksuite/store": "workspace:*", "@floating-ui/dom": "^1.6.13", diff --git a/blocksuite/affine/inlines/reference/package.json b/blocksuite/affine/inlines/reference/package.json index bdff33b28b..d097a2ba47 100644 --- a/blocksuite/affine/inlines/reference/package.json +++ b/blocksuite/affine/inlines/reference/package.json @@ -15,7 +15,7 @@ "@blocksuite/affine-model": "workspace:*", "@blocksuite/affine-shared": "workspace:*", "@blocksuite/global": "workspace:*", - "@blocksuite/icons": "^2.2.12", + "@blocksuite/icons": "^2.2.17", "@blocksuite/std": "workspace:*", "@blocksuite/store": "workspace:*", "@floating-ui/dom": "^1.6.13", diff --git a/blocksuite/affine/rich-text/package.json b/blocksuite/affine/rich-text/package.json index 97cba891ef..6ddc1fa6ab 100644 --- a/blocksuite/affine/rich-text/package.json +++ b/blocksuite/affine/rich-text/package.json @@ -14,7 +14,7 @@ "@blocksuite/affine-model": "workspace:*", "@blocksuite/affine-shared": "workspace:*", "@blocksuite/global": "workspace:*", - "@blocksuite/icons": "^2.2.12", + "@blocksuite/icons": "^2.2.17", "@blocksuite/std": "workspace:*", "@blocksuite/store": "workspace:*", "@floating-ui/dom": "^1.6.13", diff --git a/blocksuite/affine/shared/package.json b/blocksuite/affine/shared/package.json index d1f15ddbc7..cde4b2ba87 100644 --- a/blocksuite/affine/shared/package.json +++ b/blocksuite/affine/shared/package.json @@ -12,7 +12,7 @@ "dependencies": { "@blocksuite/affine-model": "workspace:*", "@blocksuite/global": "workspace:*", - "@blocksuite/icons": "^2.2.12", + "@blocksuite/icons": "^2.2.17", "@blocksuite/std": "workspace:*", "@blocksuite/store": "workspace:*", "@floating-ui/dom": "^1.6.13", diff --git a/blocksuite/affine/widgets/drag-handle/package.json b/blocksuite/affine/widgets/drag-handle/package.json index 71e136a3ad..bf64f68893 100644 --- a/blocksuite/affine/widgets/drag-handle/package.json +++ b/blocksuite/affine/widgets/drag-handle/package.json @@ -21,7 +21,7 @@ "@blocksuite/affine-model": "workspace:*", "@blocksuite/affine-shared": "workspace:*", "@blocksuite/global": "workspace:*", - "@blocksuite/icons": "^2.2.12", + "@blocksuite/icons": "^2.2.17", "@blocksuite/std": "workspace:*", "@blocksuite/store": "workspace:*", "@floating-ui/dom": "^1.6.13", diff --git a/blocksuite/affine/widgets/edgeless-auto-connect/package.json b/blocksuite/affine/widgets/edgeless-auto-connect/package.json index 6a7de6dae4..1ec6cb28db 100644 --- a/blocksuite/affine/widgets/edgeless-auto-connect/package.json +++ b/blocksuite/affine/widgets/edgeless-auto-connect/package.json @@ -17,7 +17,7 @@ "@blocksuite/affine-model": "workspace:*", "@blocksuite/affine-shared": "workspace:*", "@blocksuite/global": "workspace:*", - "@blocksuite/icons": "^2.2.12", + "@blocksuite/icons": "^2.2.17", "@blocksuite/std": "workspace:*", "@preact/signals-core": "^1.8.0", "@toeverything/theme": "^1.1.16", diff --git a/blocksuite/affine/widgets/edgeless-dragging-area/package.json b/blocksuite/affine/widgets/edgeless-dragging-area/package.json index df1e1c0138..24c34c67a0 100644 --- a/blocksuite/affine/widgets/edgeless-dragging-area/package.json +++ b/blocksuite/affine/widgets/edgeless-dragging-area/package.json @@ -17,7 +17,7 @@ "@blocksuite/affine-model": "workspace:*", "@blocksuite/affine-shared": "workspace:*", "@blocksuite/global": "workspace:*", - "@blocksuite/icons": "^2.2.12", + "@blocksuite/icons": "^2.2.17", "@blocksuite/std": "workspace:*", "@lit/context": "^1.1.2", "@preact/signals-core": "^1.8.0", diff --git a/blocksuite/affine/widgets/edgeless-selected-rect/package.json b/blocksuite/affine/widgets/edgeless-selected-rect/package.json index 80c1def720..10b048766a 100644 --- a/blocksuite/affine/widgets/edgeless-selected-rect/package.json +++ b/blocksuite/affine/widgets/edgeless-selected-rect/package.json @@ -21,7 +21,7 @@ "@blocksuite/affine-model": "workspace:*", "@blocksuite/affine-shared": "workspace:*", "@blocksuite/global": "workspace:*", - "@blocksuite/icons": "^2.2.12", + "@blocksuite/icons": "^2.2.17", "@blocksuite/std": "workspace:*", "@lit/context": "^1.1.2", "@preact/signals-core": "^1.8.0", diff --git a/blocksuite/affine/widgets/edgeless-toolbar/package.json b/blocksuite/affine/widgets/edgeless-toolbar/package.json index 84060c39d4..f40b7be647 100644 --- a/blocksuite/affine/widgets/edgeless-toolbar/package.json +++ b/blocksuite/affine/widgets/edgeless-toolbar/package.json @@ -17,7 +17,7 @@ "@blocksuite/affine-rich-text": "workspace:*", "@blocksuite/affine-shared": "workspace:*", "@blocksuite/global": "workspace:*", - "@blocksuite/icons": "^2.2.12", + "@blocksuite/icons": "^2.2.17", "@blocksuite/std": "workspace:*", "@floating-ui/dom": "^1.6.13", "@lit/context": "^1.1.2", diff --git a/blocksuite/affine/widgets/edgeless-zoom-toolbar/package.json b/blocksuite/affine/widgets/edgeless-zoom-toolbar/package.json index c6addfbed2..30ce1f5c56 100644 --- a/blocksuite/affine/widgets/edgeless-zoom-toolbar/package.json +++ b/blocksuite/affine/widgets/edgeless-zoom-toolbar/package.json @@ -16,7 +16,7 @@ "@blocksuite/affine-model": "workspace:*", "@blocksuite/affine-shared": "workspace:*", "@blocksuite/global": "workspace:*", - "@blocksuite/icons": "^2.2.12", + "@blocksuite/icons": "^2.2.17", "@blocksuite/std": "workspace:*", "@floating-ui/dom": "^1.6.13", "@preact/signals-core": "^1.8.0", diff --git a/blocksuite/affine/widgets/keyboard-toolbar/package.json b/blocksuite/affine/widgets/keyboard-toolbar/package.json index b9c43f050f..3506cb6cc9 100644 --- a/blocksuite/affine/widgets/keyboard-toolbar/package.json +++ b/blocksuite/affine/widgets/keyboard-toolbar/package.json @@ -33,7 +33,7 @@ "@blocksuite/affine-shared": "workspace:*", "@blocksuite/data-view": "workspace:*", "@blocksuite/global": "workspace:*", - "@blocksuite/icons": "^2.2.12", + "@blocksuite/icons": "^2.2.17", "@blocksuite/std": "workspace:*", "@blocksuite/store": "workspace:*", "@lit/context": "^1.1.2", diff --git a/blocksuite/affine/widgets/linked-doc/package.json b/blocksuite/affine/widgets/linked-doc/package.json index ab3946fd15..21bb7c464b 100644 --- a/blocksuite/affine/widgets/linked-doc/package.json +++ b/blocksuite/affine/widgets/linked-doc/package.json @@ -18,7 +18,7 @@ "@blocksuite/affine-rich-text": "workspace:*", "@blocksuite/affine-shared": "workspace:*", "@blocksuite/global": "workspace:*", - "@blocksuite/icons": "^2.2.12", + "@blocksuite/icons": "^2.2.17", "@blocksuite/std": "workspace:*", "@blocksuite/store": "workspace:*", "@lit/context": "^1.1.2", diff --git a/blocksuite/affine/widgets/note-slicer/package.json b/blocksuite/affine/widgets/note-slicer/package.json index 56a3305d07..4775eab8f2 100644 --- a/blocksuite/affine/widgets/note-slicer/package.json +++ b/blocksuite/affine/widgets/note-slicer/package.json @@ -18,7 +18,7 @@ "@blocksuite/affine-shared": "workspace:*", "@blocksuite/affine-widget-edgeless-selected-rect": "workspace:*", "@blocksuite/global": "workspace:*", - "@blocksuite/icons": "^2.2.12", + "@blocksuite/icons": "^2.2.17", "@blocksuite/std": "workspace:*", "@lit/context": "^1.1.2", "@preact/signals-core": "^1.8.0", diff --git a/blocksuite/affine/widgets/page-dragging-area/package.json b/blocksuite/affine/widgets/page-dragging-area/package.json index 46f20ca21b..ae9f9c53b4 100644 --- a/blocksuite/affine/widgets/page-dragging-area/package.json +++ b/blocksuite/affine/widgets/page-dragging-area/package.json @@ -15,7 +15,7 @@ "@blocksuite/affine-model": "workspace:*", "@blocksuite/affine-shared": "workspace:*", "@blocksuite/global": "workspace:*", - "@blocksuite/icons": "^2.2.12", + "@blocksuite/icons": "^2.2.17", "@blocksuite/std": "workspace:*", "@blocksuite/store": "workspace:*", "@lit/context": "^1.1.2", diff --git a/blocksuite/affine/widgets/remote-selection/package.json b/blocksuite/affine/widgets/remote-selection/package.json index 244bf388fa..1ce2950d00 100644 --- a/blocksuite/affine/widgets/remote-selection/package.json +++ b/blocksuite/affine/widgets/remote-selection/package.json @@ -16,7 +16,7 @@ "@blocksuite/affine-model": "workspace:*", "@blocksuite/affine-shared": "workspace:*", "@blocksuite/global": "workspace:*", - "@blocksuite/icons": "^2.2.12", + "@blocksuite/icons": "^2.2.17", "@blocksuite/std": "workspace:*", "@preact/signals-core": "^1.8.0", "@toeverything/theme": "^1.1.16", diff --git a/blocksuite/affine/widgets/slash-menu/package.json b/blocksuite/affine/widgets/slash-menu/package.json index 5768c1dd4d..df85b90e29 100644 --- a/blocksuite/affine/widgets/slash-menu/package.json +++ b/blocksuite/affine/widgets/slash-menu/package.json @@ -15,7 +15,7 @@ "@blocksuite/affine-rich-text": "workspace:*", "@blocksuite/affine-shared": "workspace:*", "@blocksuite/global": "workspace:*", - "@blocksuite/icons": "^2.2.12", + "@blocksuite/icons": "^2.2.17", "@blocksuite/std": "workspace:*", "@blocksuite/store": "workspace:*", "@floating-ui/dom": "^1.6.13", diff --git a/blocksuite/affine/widgets/toolbar/package.json b/blocksuite/affine/widgets/toolbar/package.json index 886f31e6a8..f30590a15b 100644 --- a/blocksuite/affine/widgets/toolbar/package.json +++ b/blocksuite/affine/widgets/toolbar/package.json @@ -18,7 +18,7 @@ "@blocksuite/affine-model": "workspace:*", "@blocksuite/affine-shared": "workspace:*", "@blocksuite/global": "workspace:*", - "@blocksuite/icons": "^2.2.12", + "@blocksuite/icons": "^2.2.17", "@blocksuite/std": "workspace:*", "@floating-ui/dom": "^1.6.13", "@preact/signals-core": "^1.8.0", diff --git a/blocksuite/affine/widgets/viewport-overlay/package.json b/blocksuite/affine/widgets/viewport-overlay/package.json index 5240ff5b5f..e75a70e9fb 100644 --- a/blocksuite/affine/widgets/viewport-overlay/package.json +++ b/blocksuite/affine/widgets/viewport-overlay/package.json @@ -15,7 +15,7 @@ "@blocksuite/affine-model": "workspace:*", "@blocksuite/affine-shared": "workspace:*", "@blocksuite/global": "workspace:*", - "@blocksuite/icons": "^2.2.12", + "@blocksuite/icons": "^2.2.17", "@blocksuite/std": "workspace:*", "@floating-ui/dom": "^1.6.13", "@preact/signals-core": "^1.8.0", diff --git a/blocksuite/integration-test/package.json b/blocksuite/integration-test/package.json index d6e2c06958..c2315ac7dc 100644 --- a/blocksuite/integration-test/package.json +++ b/blocksuite/integration-test/package.json @@ -14,7 +14,7 @@ "license": "MIT", "dependencies": { "@blocksuite/affine": "workspace:*", - "@blocksuite/icons": "^2.2.12", + "@blocksuite/icons": "^2.2.17", "@floating-ui/dom": "^1.6.13", "@lit/context": "^1.1.3", "@lottiefiles/dotlottie-wc": "^0.5.0", diff --git a/blocksuite/playground/package.json b/blocksuite/playground/package.json index 338f8cc5d7..bf77b5dadc 100644 --- a/blocksuite/playground/package.json +++ b/blocksuite/playground/package.json @@ -14,7 +14,7 @@ "@blocksuite/affine-model": "workspace:*", "@blocksuite/affine-shared": "workspace:*", "@blocksuite/data-view": "workspace:*", - "@blocksuite/icons": "^2.2.12", + "@blocksuite/icons": "^2.2.17", "@blocksuite/integration-test": "workspace:*", "@preact/signals-core": "^1.8.0", "@shoelace-style/shoelace": "2.20.1", diff --git a/package.json b/package.json index 34fa8995cb..7e7c2601ef 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "@affine-tools/cli": "workspace:*", "@capacitor/cli": "^7.0.0", "@eslint/js": "^9.16.0", - "@faker-js/faker": "^9.3.0", + "@faker-js/faker": "^10.0.0", "@istanbuljs/schema": "^0.1.3", "@magic-works/i18n-codegen": "^0.6.1", "@playwright/test": "=1.52.0", diff --git a/packages/backend/server/migrations/20250917092818_payment_rc_fields/migration.sql b/packages/backend/server/migrations/20250917092818_payment_rc_fields/migration.sql new file mode 100644 index 0000000000..da56c11396 --- /dev/null +++ b/packages/backend/server/migrations/20250917092818_payment_rc_fields/migration.sql @@ -0,0 +1,12 @@ +-- CreateEnum +CREATE TYPE "Provider" AS ENUM ('stripe', 'revenuecat'); + +-- CreateEnum +CREATE TYPE "IapStore" AS ENUM ('app_store', 'play_store'); + +-- AlterTable +ALTER TABLE "subscriptions" ADD COLUMN "iap_store" "IapStore", +ADD COLUMN "provider" "Provider" NOT NULL DEFAULT 'stripe', +ADD COLUMN "rc_entitlement" VARCHAR, +ADD COLUMN "rc_external_ref" VARCHAR, +ADD COLUMN "rc_product_id" VARCHAR; diff --git a/packages/backend/server/package.json b/packages/backend/server/package.json index 3bfa141246..44f98ffabb 100644 --- a/packages/backend/server/package.json +++ b/packages/backend/server/package.json @@ -127,7 +127,8 @@ "@affine-tools/cli": "workspace:*", "@affine-tools/utils": "workspace:*", "@affine/graphql": "workspace:*", - "@faker-js/faker": "^9.6.0", + "@faker-js/faker": "^10.0.0", + "@nestjs/swagger": "^11.2.0", "@nestjs/testing": "patch:@nestjs/testing@npm%3A10.4.15#~/.yarn/patches/@nestjs-testing-npm-10.4.15-d591a1705a.patch", "@types/cookie-parser": "^1.4.8", "@types/express": "^5.0.1", diff --git a/packages/backend/server/schema.prisma b/packages/backend/server/schema.prisma index 4561eafdd9..d33304f48e 100644 --- a/packages/backend/server/schema.prisma +++ b/packages/backend/server/schema.prisma @@ -749,6 +749,16 @@ model Subscription { stripeSubscriptionId String? @unique @map("stripe_subscription_id") // stripe schedule id stripeScheduleId String? @map("stripe_schedule_id") @db.VarChar + // subscription provider: stripe or revenuecat + provider Provider @default(stripe) + // iap store for revenuecat subscriptions + iapStore IapStore? @map("iap_store") + // revenuecat entitlement name like "Pro" / "AI" + rcEntitlement String? @map("rc_entitlement") @db.VarChar + // revenuecat product id like "app.affine.pro.Annual" + rcProductId String? @map("rc_product_id") @db.VarChar + // external reference, appstore originalTransactionId or play purchaseToken + rcExternalRef String? @map("rc_external_ref") @db.VarChar // subscription.status, active/past_due/canceled/unpaid... status String @db.VarChar(20) // subscription.current_period_start @@ -770,6 +780,16 @@ model Subscription { @@map("subscriptions") } +enum Provider { + stripe + revenuecat +} + +enum IapStore { + app_store + play_store +} + model Invoice { stripeInvoiceId String @id @map("stripe_invoice_id") targetId String @map("target_id") @db.VarChar diff --git a/packages/backend/server/src/__tests__/payment/__snapshots__/revenuecat.spec.ts.md b/packages/backend/server/src/__tests__/payment/__snapshots__/revenuecat.spec.ts.md new file mode 100644 index 0000000000..b46249297c --- /dev/null +++ b/packages/backend/server/src/__tests__/payment/__snapshots__/revenuecat.spec.ts.md @@ -0,0 +1,253 @@ +# Snapshot report for `src/__tests__/payment/revenuecat.spec.ts` + +The actual snapshot is saved in `revenuecat.spec.ts.snap`. + +Generated by [AVA](https://avajs.dev). + +## should resolve product mapping consistently (whitelist, override, unknown) + +> should map product for whitelist/override/unknown + + { + override: { + customMonthly: { + plan: 'pro', + recurring: 'monthly', + }, + }, + unknown: null, + whitelist: { + aiAnnual: { + plan: 'ai', + recurring: 'yearly', + }, + proAnnual: { + plan: 'pro', + recurring: 'yearly', + }, + proMonthly: { + plan: 'pro', + recurring: 'monthly', + }, + }, + } + +## should standardize RC subscriber response and upsert subscription with observability fields + +> should standardize payload and have events + + { + activatedCount: 1, + canceledCount: 0, + dbObservability: { + iapStore: 'app_store', + provider: 'revenuecat', + rcEntitlement: 'Pro', + rcExternalRef: 'orig-tx-1', + rcProductId: 'app.affine.pro.Annual', + }, + lastActivated: { + plan: 'pro', + recurring: 'yearly', + }, + subscriberCount: 1, + } + +## should process expiration/refund by deleting subscription and emitting canceled + +> should process expiration/refund and emit canceled + + { + activatedCount: 0, + canceledCount: 1, + finalDBCount: 0, + lastCanceled: { + plan: 'pro', + recurring: 'yearly', + }, + subscriberCount: 1, + } + +## should enqueue per-user reconciliation jobs for existing RC active/trialing/past_due subscriptions + +> should enqueue per-user RC reconciliation jobs (deduplicated by userId) + + { + queued: [ + { + name: 'nightly.revenuecat.syncUser', + opts: { + attempts: 3, + backoff: { + delay: 60000, + type: 'exponential', + }, + jobId: 'nightly-rc-sync-u1', + }, + payload: { + userId: 'u1', + }, + }, + { + name: 'nightly.revenuecat.syncUser', + opts: { + attempts: 3, + backoff: { + delay: 60000, + type: 'exponential', + }, + jobId: 'nightly-rc-sync-u2', + }, + payload: { + userId: 'u2', + }, + }, + ], + uniqueJobCount: 2, + } + +## should activate subscriptions via webhook for whitelisted products across stores (iOS/Android) + +> should activate subscriptions via webhook for whitelisted products across stores (iOS/Android) + + { + results: [ + { + activatedCount: 1, + name: 'Pro monthly on iOS', + rec: { + iapStore: 'app_store', + plan: 'pro', + provider: 'revenuecat', + rcEntitlement: 'Pro', + rcExternalRef: 'orig-ios-1', + rcProductId: 'app.affine.pro.Monthly', + recurring: 'monthly', + status: 'active', + }, + }, + { + activatedCount: 1, + name: 'AI annual on Android', + rec: { + iapStore: 'play_store', + plan: 'ai', + provider: 'revenuecat', + rcEntitlement: 'AI', + rcExternalRef: 'token-android-1', + rcProductId: 'app.affine.pro.ai.Annual', + recurring: 'yearly', + status: 'active', + }, + }, + ], + } + +## should keep active and advance period dates when a trialing subscription renews + +> should keep active after trial renewal + + { + activatedCount: 2, + canceledCount: 0, + status: 'active', + } + +## should remove or cancel the record and revoke entitlement when a trialing subscription expires + +> should remove record + + { + canceledCount: 1, + finalDBCount: 0, + } + +## should set canceledAt and keep active until expiration when will_renew is false (cancellation before period end) + +> should keep active until period end when will_renew is false + + { + activatedCount: 1, + canceledCount: 0, + hasCanceledAt: true, + status: 'active', + } + +## should retain record as past_due (inactive but not expired) and NOT emit canceled event + +> should retain past_due record and NOT emit canceled event + + { + canceledCount: 0, + status: 'past_due', + } + +## should skip RC upsert when Stripe active already exists for same plan + +> should skip RC upsert when Stripe active already exists + + { + activatedCount: 0, + hasRCRecord: false, + } + +## should reconcile and fix missing or out-of-order states for revenuecat Active/Trialing/PastDue records + +> should reconcile and fix missing or out-of-order states for revenuecat records + + { + activatedCount: 1, + canceledCount: 0, + subscriberCount: 1, + } + +## should treat refund as early expiration and revoke immediately + +> should delete record and emit canceled on refund + + { + canceledCount: 1, + finalDBCount: 0, + } + +## should ignore non-whitelisted productId and not write to DB + +> should ignore non-whitelisted productId and not write to DB + + { + activatedCount: 0, + canceledCount: 0, + dbCount: 0, + } + +## should map via entitlement+duration when productId not whitelisted (P1M/P1Y only) + +> should map via entitlement+duration fallback and ignore unsupported durations + + { + aiViaFallback: { + plan: 'ai', + provider: 'revenuecat', + recurring: 'yearly', + }, + eventsCounts: { + afterFirst: { + a: 1, + c: 0, + }, + afterSecond: { + a: 2, + c: 0, + }, + afterThird: { + a: 2, + c: 0, + }, + }, + proViaFallback: { + plan: 'pro', + provider: 'revenuecat', + recurring: 'monthly', + }, + totalCount: 2, + } diff --git a/packages/backend/server/src/__tests__/payment/__snapshots__/revenuecat.spec.ts.snap b/packages/backend/server/src/__tests__/payment/__snapshots__/revenuecat.spec.ts.snap new file mode 100644 index 0000000000..b031908d70 Binary files /dev/null and b/packages/backend/server/src/__tests__/payment/__snapshots__/revenuecat.spec.ts.snap differ diff --git a/packages/backend/server/src/__tests__/payment/revenuecat.spec.ts b/packages/backend/server/src/__tests__/payment/revenuecat.spec.ts new file mode 100644 index 0000000000..21f37e9c77 --- /dev/null +++ b/packages/backend/server/src/__tests__/payment/revenuecat.spec.ts @@ -0,0 +1,929 @@ +import { PrismaClient, User } from '@prisma/client'; +import ava, { TestFn } from 'ava'; +import { omit } from 'lodash-es'; +import Sinon from 'sinon'; + +import { + EventBus, + ManagedByAppStoreOrPlay, + SubscriptionAlreadyExists, +} from '../../base'; +import { ConfigModule } from '../../base/config'; +import { FeatureService } from '../../core/features'; +import { Models } from '../../models'; +import { PaymentModule } from '../../plugins/payment'; +import { SubscriptionCronJobs } from '../../plugins/payment/cron'; +import { UserSubscriptionManager } from '../../plugins/payment/manager'; +import { + RcEvent, + resolveProductMapping, + RevenueCatService, + RevenueCatWebhookController, + RevenueCatWebhookHandler, + type Subscription, +} from '../../plugins/payment/revenuecat'; +import { SubscriptionService } from '../../plugins/payment/service'; +import { + SubscriptionPlan, + SubscriptionRecurring, +} from '../../plugins/payment/types'; +import { createTestingApp, TestingApp } from '../utils'; + +type Ctx = { + module: TestingApp; + db: PrismaClient; + models: Models; + event: Sinon.SinonStubbedInstance; + + service: SubscriptionService; + rc: RevenueCatService; + webhook: RevenueCatWebhookHandler; + controller: RevenueCatWebhookController; + + mockSub: (subs: Subscription[]) => Sinon.SinonStub; + mockSubSeq: (sequences: Subscription[][]) => Sinon.SinonStub; + triggerWebhook: ( + userId: string, + event: Omit + ) => Promise; + collectEvents: () => { + activatedCount: number; + canceledCount: number; + events: Record; + }; +}; + +const test = ava as TestFn; +let user: User; + +test.beforeEach(async t => { + const app = await createTestingApp({ + imports: [ + ConfigModule.override({ + payment: { + revenuecat: { + enabled: true, + webhookAuth: '42', + }, + }, + }), + PaymentModule, + ], + tapModule: m => { + m.overrideProvider(FeatureService).useValue( + Sinon.createStubInstance(FeatureService) + ); + m.overrideProvider(EventBus).useValue(Sinon.createStubInstance(EventBus)); + }, + }); + + const db = app.get(PrismaClient); + const models = app.get(Models); + const event = app.get(EventBus) as Sinon.SinonStubbedInstance; + + const service = app.get(SubscriptionService); + const rc = app.get(RevenueCatService); + const webhook = app.get(RevenueCatWebhookHandler); + const controller = app.get(RevenueCatWebhookController); + + t.context.module = app; + t.context.db = db; + t.context.models = models; + t.context.event = event; + + t.context.service = service; + t.context.rc = rc; + t.context.webhook = webhook; + t.context.controller = controller; + + t.context.mockSub = subs => Sinon.stub(rc, 'getSubscriptions').resolves(subs); + t.context.mockSubSeq = sequences => { + const stub = Sinon.stub(rc, 'getSubscriptions'); + sequences.forEach((seq, idx) => { + if (idx === 0) stub.onFirstCall().resolves(seq); + else if (idx === 1) stub.onSecondCall().resolves(seq); + else stub.onCall(idx).resolves(seq); + }); + return stub; + }; + t.context.triggerWebhook = async (appUserId, event) => { + await webhook.onWebhook({ + appUserId, + event: { + ...event, + app_id: 'app.affine.pro', + environment: 'SANDBOX', + } as RcEvent, + }); + }; + + t.context.collectEvents = () => { + const events = event.emit.getCalls().reduce( + (acc, c) => { + const [key, value] = c.args; + acc[key] = acc[key] || []; + acc[key].push(value); + return acc; + }, + {} as { [key: string]: any[] } + ); + const activatedCount = events['user.subscription.activated']?.length || 0; + const canceledCount = events['user.subscription.canceled']?.length || 0; + + return { activatedCount, canceledCount, events }; + }; +}); + +test.beforeEach(async t => { + await t.context.module.initTestingDB(); + user = await t.context.models.user.create({ + email: 'test@affine.pro', + }); +}); + +test.afterEach.always(async t => { + Sinon.reset(); + await t.context.module.close(); +}); + +test('should resolve product mapping consistently (whitelist, override, unknown)', t => { + const override = { + 'custom.sku.monthly': { plan: 'pro', recurring: 'monthly' }, + } as Record; + + const actual = { + whitelist: { + proMonthly: resolveProductMapping({ + productId: 'app.affine.pro.Monthly', + }), + proAnnual: resolveProductMapping({ productId: 'app.affine.pro.Annual' }), + aiAnnual: resolveProductMapping({ + productId: 'app.affine.pro.ai.Annual', + }), + }, + override: { + customMonthly: resolveProductMapping( + { productId: 'custom.sku.monthly' }, + override + ), + }, + unknown: resolveProductMapping({ productId: 'unknown.sku' }), + }; + + t.snapshot(actual, 'should map product for whitelist/override/unknown'); +}); + +test('should standardize RC subscriber response and upsert subscription with observability fields', async t => { + const { webhook, collectEvents, mockSub } = t.context; + + const subscriber = mockSub([ + { + identifier: 'Pro', + isTrial: false, + isActive: true, + latestPurchaseDate: new Date('2025-01-01T00:00:00.000Z'), + expirationDate: new Date('2026-01-01T00:00:00.000Z'), + productId: 'app.affine.pro.Annual', + store: 'app_store', + willRenew: true, + duration: null, + }, + ]); + + await webhook.onWebhook({ + appUserId: user.id, + event: { + id: 'evt_1', + environment: 'PRODUCTION', + app_id: 'app.affine.pro', + type: 'INITIAL_PURCHASE', + store: 'app_store', + original_transaction_id: 'orig-tx-1', + }, + }); + const { activatedCount, canceledCount, events } = collectEvents(); + + const record = await t.context.db.subscription.findUnique({ + where: { targetId_plan: { targetId: user.id, plan: 'pro' } }, + select: { + provider: true, + iapStore: true, + rcEntitlement: true, + rcProductId: true, + rcExternalRef: true, + }, + }); + + t.snapshot( + { + subscriberCount: subscriber.getCalls()?.length || 0, + activatedCount, + canceledCount, + lastActivated: omit( + events['user.subscription.activated']?.slice(-1)?.[0], + 'userId' + ), + dbObservability: record, + }, + 'should standardize payload and have events' + ); +}); + +test('should process expiration/refund by deleting subscription and emitting canceled', async t => { + const { db, collectEvents, mockSub, triggerWebhook } = t.context; + + await db.subscription.create({ + data: { + targetId: user.id, + plan: 'pro', + status: 'active', + provider: 'revenuecat', + recurring: 'annual', + start: new Date('2025-01-01T00:00:00.000Z'), + }, + }); + + const subscriber = mockSub([ + { + identifier: 'Pro', + isTrial: false, + isActive: false, + latestPurchaseDate: new Date('2024-01-01T00:00:00.000Z'), + expirationDate: new Date('2024-02-01T00:00:00.000Z'), + productId: 'app.affine.pro.Annual', + store: 'app_store', + willRenew: false, + duration: null, + }, + ]); + + await triggerWebhook(user.id, { + id: 'evt_2', + type: 'EXPIRATION', + store: 'app_store', + original_transaction_id: 'orig-tx-2', + }); + + const finalDBCount = await db.subscription.count({ + where: { targetId: user.id, plan: 'pro' }, + }); + + const { activatedCount, canceledCount, events } = collectEvents(); + t.snapshot( + { + finalDBCount, + subscriberCount: subscriber.getCalls()?.length || 0, + activatedCount, + canceledCount, + lastCanceled: omit( + events['user.subscription.canceled']?.slice(-1)?.[0], + 'userId' + ), + }, + 'should process expiration/refund and emit canceled' + ); +}); + +test('should enqueue per-user reconciliation jobs for existing RC active/trialing/past_due subscriptions', async t => { + const { module, db } = t.context; + + const cron = module.get(SubscriptionCronJobs); + + const common = { provider: 'revenuecat', start: new Date() } as const; + await db.subscription.createMany({ + data: [ + { + targetId: 'u1', + plan: 'pro', + status: 'active', + recurring: 'monthly', + ...common, + }, + { + targetId: 'u2', + plan: 'ai', + status: 'trialing', + recurring: 'annual', + ...common, + }, + { + targetId: 'u1', + plan: 'ai', + status: 'past_due', + recurring: 'monthly', + ...common, + }, + ], + }); + + await cron.reconcileRevenueCatSubscriptions(); + + const calls = module.queue.add.getCalls().map(c => ({ + name: c.args[0], + payload: c.args[1], + opts: c.args[2], + })); + t.snapshot( + { + queued: calls, + uniqueJobCount: calls.filter( + c => c.name === 'nightly.revenuecat.syncUser' + ).length, + }, + 'should enqueue per-user RC reconciliation jobs (deduplicated by userId)' + ); +}); + +test('should activate subscriptions via webhook for whitelisted products across stores (iOS/Android)', async t => { + const { db, event, collectEvents, mockSubSeq, triggerWebhook } = t.context; + + const scenarios = [ + { + name: 'Pro monthly on iOS', + stub: [ + { + identifier: 'Pro', + isTrial: false, + isActive: true, + latestPurchaseDate: new Date('2025-01-10T00:00:00.000Z'), + expirationDate: new Date('2025-02-10T00:00:00.000Z'), + productId: 'app.affine.pro.Monthly', + store: 'app_store' as const, + willRenew: true, + duration: null, + }, + ], + event: { + id: 'evt_ios_1', + type: 'INITIAL_PURCHASE', + store: 'app_store', + original_transaction_id: 'orig-ios-1', + }, + expectedPlan: 'pro' as const, + }, + { + name: 'AI annual on Android', + stub: [ + { + identifier: 'AI', + isTrial: false, + isActive: true, + latestPurchaseDate: new Date('2025-03-01T00:00:00.000Z'), + expirationDate: new Date('2026-03-01T00:00:00.000Z'), + productId: 'app.affine.pro.ai.Annual', + store: 'play_store' as const, + willRenew: true, + duration: null, + }, + ], + event: { + id: 'evt_android_1', + type: 'INITIAL_PURCHASE', + store: 'play_store', + purchase_token: 'token-android-1', + }, + expectedPlan: 'ai' as const, + }, + ]; + + const results: any[] = []; + + mockSubSeq(scenarios.map(s => s.stub)); + + for (const s of scenarios) { + // reset event history between scenarios for clean counts + event.emit.resetHistory?.(); + await triggerWebhook(user.id, s.event); + const rec = await db.subscription.findUnique({ + where: { targetId_plan: { targetId: user.id, plan: s.expectedPlan } }, + select: { + plan: true, + recurring: true, + status: true, + provider: true, + iapStore: true, + rcEntitlement: true, + rcProductId: true, + rcExternalRef: true, + }, + }); + const { activatedCount } = collectEvents(); + results.push({ name: s.name, rec, activatedCount }); + } + + t.snapshot( + { results }, + 'should activate subscriptions via webhook for whitelisted products across stores (iOS/Android)' + ); +}); + +test('should keep active and advance period dates when a trialing subscription renews', async t => { + const { db, collectEvents, mockSubSeq, triggerWebhook } = t.context; + mockSubSeq([ + [ + { + identifier: 'Pro', + isTrial: false, + isActive: true, + latestPurchaseDate: new Date('2025-04-01T00:00:00.000Z'), + expirationDate: new Date('2025-04-08T00:00:00.000Z'), + productId: 'app.affine.pro.Annual', + store: 'app_store', + willRenew: true, + duration: null, + }, + ], + [ + { + identifier: 'Pro', + isTrial: false, + isActive: true, + latestPurchaseDate: new Date('2025-04-08T00:00:00.000Z'), + expirationDate: new Date('2026-04-08T00:00:00.000Z'), + productId: 'app.affine.pro.Annual', + store: 'app_store', + willRenew: true, + duration: null, + }, + ], + ]); + + await triggerWebhook(user.id, { + id: 'evt_trial', + type: 'INITIAL_PURCHASE', + period_type: 'trial', + store: 'app_store', + }); + await triggerWebhook(user.id, { + id: 'evt_renew', + type: 'RENEWAL', + store: 'app_store', + }); + + const rec = await db.subscription.findUnique({ + where: { targetId_plan: { targetId: user.id, plan: 'pro' } }, + select: { status: true, start: true, end: true }, + }); + const { activatedCount, canceledCount } = collectEvents(); + t.snapshot( + { status: rec?.status, activatedCount, canceledCount }, + 'should keep active after trial renewal' + ); +}); + +test('should remove or cancel the record and revoke entitlement when a trialing subscription expires', async t => { + const { db, collectEvents, mockSubSeq, triggerWebhook } = t.context; + mockSubSeq([ + [ + { + identifier: 'Pro', + isTrial: false, + isActive: true, + latestPurchaseDate: new Date('2025-04-01T00:00:00.000Z'), + expirationDate: new Date('2025-04-08T00:00:00.000Z'), + productId: 'app.affine.pro.Annual', + store: 'app_store', + willRenew: false, + duration: null, + }, + ], + [ + { + identifier: 'Pro', + isTrial: false, + isActive: false, + latestPurchaseDate: new Date('2025-04-01T00:00:00.000Z'), + expirationDate: new Date('2024-01-01T00:00:00.000Z'), + productId: 'app.affine.pro.Annual', + store: 'app_store', + willRenew: false, + duration: null, + }, + ], + ]); + + await triggerWebhook(user.id, { + id: 'evt_trial2', + type: 'INITIAL_PURCHASE', + period_type: 'trial', + store: 'app_store', + }); + await triggerWebhook(user.id, { + id: 'evt_expire_trial', + type: 'EXPIRATION', + store: 'app_store', + }); + + const finalDBCount = await db.subscription.count({ + where: { targetId: user.id, plan: 'pro' }, + }); + const { canceledCount } = collectEvents(); + t.snapshot({ finalDBCount, canceledCount }, 'should remove record'); +}); + +test('should set canceledAt and keep active until expiration when will_renew is false (cancellation before period end)', async t => { + const { db, collectEvents, mockSub, triggerWebhook } = t.context; + mockSub([ + { + identifier: 'Pro', + isTrial: false, + isActive: true, + latestPurchaseDate: new Date('2025-05-01T00:00:00.000Z'), + expirationDate: new Date('2025-06-01T00:00:00.000Z'), + productId: 'app.affine.pro.Annual', + store: 'app_store', + willRenew: false, + duration: null, + }, + ]); + + await triggerWebhook(user.id, { + id: 'evt_cancel_before_end', + type: 'CANCELLATION', + store: 'app_store', + }); + const rec = await db.subscription.findUnique({ + where: { targetId_plan: { targetId: user.id, plan: 'pro' } }, + select: { status: true, canceledAt: true }, + }); + const { activatedCount, canceledCount } = collectEvents(); + t.snapshot( + { + status: rec?.status, + hasCanceledAt: !!rec?.canceledAt, + activatedCount, + canceledCount, + }, + 'should keep active until period end when will_renew is false' + ); +}); + +test('should retain record as past_due (inactive but not expired) and NOT emit canceled event', async t => { + const { db, collectEvents, mockSub, triggerWebhook } = t.context; + mockSub([ + { + identifier: 'Pro', + isTrial: false, + isActive: false, + latestPurchaseDate: new Date('2025-05-01T00:00:00.000Z'), + expirationDate: new Date('2999-01-01T00:00:00.000Z'), + productId: 'app.affine.pro.Annual', + store: 'app_store', + willRenew: true, + duration: null, + }, + ]); + await triggerWebhook(user.id, { + id: 'evt_pastdue', + type: 'BILLING_ISSUE', + store: 'app_store', + }); + + const rec = await db.subscription.findUnique({ + where: { targetId_plan: { targetId: user.id, plan: 'pro' } }, + select: { status: true }, + }); + const { canceledCount } = collectEvents(); + t.snapshot( + { status: rec?.status, canceledCount }, + 'should retain past_due record and NOT emit canceled event' + ); +}); + +test('should block checkout when an existing subscription of the same plan is active', async t => { + const { module, db } = t.context; + + const manager = module.get(UserSubscriptionManager); + + { + await db.subscription.create({ + data: { + targetId: user.id, + plan: 'pro', + status: 'active', + provider: 'revenuecat', + recurring: 'monthly', + start: new Date('2025-01-01T00:00:00.000Z'), + }, + }); + + await t.throwsAsync( + manager.checkout( + { + plan: SubscriptionPlan.Pro, + recurring: SubscriptionRecurring.Monthly, + variant: null, + }, + { + successCallbackLink: '/', + plan: SubscriptionPlan.Pro, + recurring: SubscriptionRecurring.Monthly, + }, + { user: { id: user.id, email: user.email } } + ), + { instanceOf: ManagedByAppStoreOrPlay } + ); + } + + { + await db.subscription.update({ + where: { targetId_plan: { targetId: user.id, plan: 'pro' } }, + data: { provider: 'stripe' }, + }); + + await t.throwsAsync( + () => + manager.checkout( + { + plan: SubscriptionPlan.Pro, + recurring: SubscriptionRecurring.Monthly, + variant: null, + }, + { + successCallbackLink: '/', + plan: SubscriptionPlan.Pro, + recurring: SubscriptionRecurring.Monthly, + }, + { user: { id: user.id, email: user.email } } + ), + { instanceOf: SubscriptionAlreadyExists } + ); + } +}); + +test('should skip RC upsert when Stripe active already exists for same plan', async t => { + const { db, collectEvents, mockSub, triggerWebhook } = t.context; + await db.subscription.create({ + data: { + targetId: user.id, + plan: 'pro', + status: 'active', + provider: 'stripe', + recurring: 'monthly', + start: new Date('2025-01-01T00:00:00.000Z'), + }, + }); + + mockSub([ + { + identifier: 'Pro', + isTrial: false, + isActive: true, + latestPurchaseDate: new Date('2025-06-01T00:00:00.000Z'), + expirationDate: new Date('2025-07-01T00:00:00.000Z'), + productId: 'app.affine.pro.Monthly', + store: 'app_store', + willRenew: true, + duration: null, + }, + ]); + + await triggerWebhook(user.id, { + id: 'evt_conflict', + type: 'INITIAL_PURCHASE', + store: 'app_store', + }); + + const rcRec = await db.subscription.findFirst({ + where: { targetId: user.id, plan: 'pro', provider: 'revenuecat' }, + }); + const { activatedCount } = collectEvents(); + t.snapshot( + { hasRCRecord: !!rcRec, activatedCount }, + 'should skip RC upsert when Stripe active already exists' + ); +}); + +test('should block read-write ops on revenuecat-managed record (cancel/resume/updateRecurring)', async t => { + const { db, service } = t.context; + await db.subscription.create({ + data: { + targetId: user.id, + plan: 'pro', + status: 'active', + provider: 'revenuecat', + recurring: 'monthly', + start: new Date(), + }, + }); + + // local helper used multiple times within this test + const expectManaged = async (fn: () => Promise) => + t.throwsAsync(() => fn(), { instanceOf: ManagedByAppStoreOrPlay }); + + await expectManaged(() => + service.cancelSubscription({ plan: SubscriptionPlan.Pro, userId: user.id }) + ); + + await expectManaged(() => + service.resumeSubscription({ plan: SubscriptionPlan.Pro, userId: user.id }) + ); + + await expectManaged(() => + service.updateSubscriptionRecurring( + { plan: SubscriptionPlan.Pro, userId: user.id }, + SubscriptionRecurring.Yearly + ) + ); +}); + +test('should reconcile and fix missing or out-of-order states for revenuecat Active/Trialing/PastDue records', async t => { + const { webhook, collectEvents, mockSub } = t.context; + + const subscriber = mockSub([ + { + identifier: 'Pro', + isTrial: false, + isActive: true, + latestPurchaseDate: new Date('2025-03-01T00:00:00.000Z'), + expirationDate: new Date('2026-03-01T00:00:00.000Z'), + productId: 'app.affine.pro.Annual', + store: 'play_store', + willRenew: true, + duration: null, + }, + ]); + + await webhook.syncAppUser(user.id); + const { activatedCount, canceledCount } = collectEvents(); + const subscriberCount = subscriber.getCalls()?.length || 0; + + t.snapshot( + { subscriberCount, activatedCount, canceledCount }, + 'should reconcile and fix missing or out-of-order states for revenuecat records' + ); +}); + +test('should treat refund as early expiration and revoke immediately', async t => { + const { db, collectEvents, mockSub, triggerWebhook } = t.context; + + await db.subscription.create({ + data: { + targetId: user.id, + plan: 'pro', + status: 'active', + provider: 'revenuecat', + recurring: 'monthly', + start: new Date('2025-01-01T00:00:00.000Z'), + }, + }); + + mockSub([ + { + identifier: 'Pro', + isTrial: false, + isActive: false, + latestPurchaseDate: new Date('2025-01-01T00:00:00.000Z'), + expirationDate: new Date('2025-01-15T00:00:00.000Z'), + productId: 'app.affine.pro.Monthly', + store: 'app_store', + willRenew: false, + duration: null, + }, + ]); + + await triggerWebhook(user.id, { + id: 'evt_refund', + type: 'CANCELLATION', + store: 'app_store', + }); + + const count = await db.subscription.count({ + where: { targetId: user.id, plan: 'pro' }, + }); + const { canceledCount } = collectEvents(); + t.snapshot( + { finalDBCount: count, canceledCount }, + 'should delete record and emit canceled on refund' + ); +}); + +test('should ignore non-whitelisted productId and not write to DB', async t => { + const { db, collectEvents, mockSub, triggerWebhook } = t.context; + mockSub([ + { + identifier: 'Weird', + isTrial: false, + isActive: true, + latestPurchaseDate: new Date('2025-07-01T00:00:00.000Z'), + expirationDate: new Date('2026-07-01T00:00:00.000Z'), + productId: 'unknown.sku', + store: 'app_store', + willRenew: true, + duration: null, + }, + ]); + await triggerWebhook(user.id, { + id: 'evt_unknown', + type: 'INITIAL_PURCHASE', + store: 'app_store', + }); + const dbCount = await db.subscription.count({ where: { targetId: user.id } }); + const { activatedCount, canceledCount } = collectEvents(); + t.snapshot( + { dbCount, activatedCount, canceledCount }, + 'should ignore non-whitelisted productId and not write to DB' + ); +}); + +test('should map via entitlement+duration when productId not whitelisted (P1M/P1Y only)', async t => { + const { db, collectEvents, mockSubSeq, triggerWebhook } = t.context; + + mockSubSeq([ + [ + { + identifier: 'Pro', + isTrial: false, + isActive: true, + latestPurchaseDate: new Date('2025-08-01T00:00:00.000Z'), + expirationDate: new Date('2025-09-01T00:00:00.000Z'), + productId: 'unknown.sku', + store: 'app_store', + willRenew: true, + duration: 'P1M', + }, + ], + [ + { + identifier: 'AI', + isTrial: false, + isActive: true, + latestPurchaseDate: new Date('2025-10-01T00:00:00.000Z'), + expirationDate: new Date('2026-10-01T00:00:00.000Z'), + productId: 'unknown.sku', + store: 'play_store', + willRenew: true, + duration: 'P1Y', + }, + ], + [ + { + identifier: 'Pro', + isTrial: false, + isActive: true, + latestPurchaseDate: new Date('2025-11-01T00:00:00.000Z'), + expirationDate: new Date('2026-02-01T00:00:00.000Z'), + productId: 'unknown.sku', + store: 'app_store', + willRenew: true, + duration: 'P3M', // not supported -> ignore + }, + ], + ]); + + // pro monthly via fallback + await triggerWebhook(user.id, { + id: 'evt_fb1', + type: 'INITIAL_PURCHASE', + store: 'app_store', + }); + const r1 = await db.subscription.findUnique({ + where: { targetId_plan: { targetId: user.id, plan: 'pro' } }, + select: { plan: true, recurring: true, provider: true }, + }); + const s1 = collectEvents(); + + // ai yearly via fallback + await triggerWebhook(user.id, { + id: 'evt_fb2', + type: 'INITIAL_PURCHASE', + store: 'play_store', + }); + const r2 = await db.subscription.findUnique({ + where: { targetId_plan: { targetId: user.id, plan: 'ai' } }, + select: { plan: true, recurring: true, provider: true }, + }); + const s2 = collectEvents(); + + // unsupported duration ignored + await triggerWebhook(user.id, { + id: 'evt_fb3', + type: 'INITIAL_PURCHASE', + store: 'app_store', + }); + const count = await db.subscription.count({ where: { targetId: user.id } }); + const s3 = collectEvents(); + + t.snapshot( + { + proViaFallback: r1, + aiViaFallback: r2, + totalCount: count, + eventsCounts: { + afterFirst: { a: s1.activatedCount, c: s1.canceledCount }, + afterSecond: { a: s2.activatedCount, c: s2.canceledCount }, + afterThird: { a: s3.activatedCount, c: s3.canceledCount }, + }, + }, + 'should map via entitlement+duration fallback and ignore unsupported durations' + ); +}); + +test('should not dispatch webhook event when authorization header is missing or mismatched', async t => { + const { controller, event } = t.context; + const before = event.emitAsync.getCalls()?.length || 0; + const e = { id: '42', type: 'INITIAL_PURCHASE', app_user_id: user.id }; + await controller.handleWebhook({ body: { event: e } } as any, undefined); + const after = event.emitAsync.getCalls()?.length || 0; + t.is(after - before, 0, 'should not emit event'); +}); diff --git a/packages/backend/server/src/__tests__/payment/service.spec.ts b/packages/backend/server/src/__tests__/payment/service.spec.ts index 4c0da9c178..e1852e4946 100644 --- a/packages/backend/server/src/__tests__/payment/service.spec.ts +++ b/packages/backend/server/src/__tests__/payment/service.spec.ts @@ -192,8 +192,10 @@ test.before(async t => { payment: { enabled: true, showLifetimePrice: true, - apiKey: '1', - webhookKey: '1', + stripe: { + apiKey: '1', + webhookKey: '1', + }, }, }), AppModule, diff --git a/packages/backend/server/src/base/error/def.ts b/packages/backend/server/src/base/error/def.ts index 5cbdfc6c28..05a2ec7c5c 100644 --- a/packages/backend/server/src/base/error/def.ts +++ b/packages/backend/server/src/base/error/def.ts @@ -637,6 +637,11 @@ export const USER_FRIENDLY_ERRORS = { type: 'invalid_input', message: 'Workspace id is required to update team subscription.', }, + managed_by_app_store_or_play: { + type: 'action_forbidden', + message: + 'This subscription is managed by App Store or Google Play. Please manage it in the corresponding store.', + }, // Copilot errors copilot_session_not_found: { diff --git a/packages/backend/server/src/base/error/errors.gen.ts b/packages/backend/server/src/base/error/errors.gen.ts index 0400572f1e..7c6047d7ff 100644 --- a/packages/backend/server/src/base/error/errors.gen.ts +++ b/packages/backend/server/src/base/error/errors.gen.ts @@ -651,6 +651,12 @@ export class WorkspaceIdRequiredToUpdateTeamSubscription extends UserFriendlyErr } } +export class ManagedByAppStoreOrPlay extends UserFriendlyError { + constructor(message?: string) { + super('action_forbidden', 'managed_by_app_store_or_play', message); + } +} + export class CopilotSessionNotFound extends UserFriendlyError { constructor(message?: string) { super('resource_not_found', 'copilot_session_not_found', message); @@ -1189,6 +1195,7 @@ export enum ErrorNames { CANT_UPDATE_ONETIME_PAYMENT_SUBSCRIPTION, WORKSPACE_ID_REQUIRED_FOR_TEAM_SUBSCRIPTION, WORKSPACE_ID_REQUIRED_TO_UPDATE_TEAM_SUBSCRIPTION, + MANAGED_BY_APP_STORE_OR_PLAY, COPILOT_SESSION_NOT_FOUND, COPILOT_SESSION_INVALID_INPUT, COPILOT_SESSION_DELETED, diff --git a/packages/backend/server/src/base/graphql/index.ts b/packages/backend/server/src/base/graphql/index.ts index 8f8811bc99..57da5da3f6 100644 --- a/packages/backend/server/src/base/graphql/index.ts +++ b/packages/backend/server/src/base/graphql/index.ts @@ -8,6 +8,7 @@ import { Global, Module } from '@nestjs/common'; import { GraphQLModule } from '@nestjs/graphql'; import type { Request, Response } from 'express'; +import { NodeEnv } from '../../env'; import { Config } from '../config'; import { mapAnyError } from '../nestjs/exception'; import { GQLLoggerPlugin } from './logger-plugin'; @@ -30,7 +31,7 @@ export type GraphqlContext = { numberScalarMode: 'integer', }, useGlobalPrefix: true, - playground: true, + graphiql: env.NODE_ENV === NodeEnv.Development, sortSchema: true, autoSchemaFile: join( env.projectRoot, diff --git a/packages/backend/server/src/core/access-token/resolver.ts b/packages/backend/server/src/core/access-token/resolver.ts index 4bedb252f8..b80a83d27c 100644 --- a/packages/backend/server/src/core/access-token/resolver.ts +++ b/packages/backend/server/src/core/access-token/resolver.ts @@ -50,6 +50,13 @@ export class AccessTokenResolver { return await this.models.accessToken.list(user.id); } + @Query(() => [RevealedAccessToken]) + async revealedAccessTokens( + @CurrentUser() user: CurrentUser + ): Promise { + return await this.models.accessToken.list(user.id, true); + } + @Mutation(() => RevealedAccessToken) async generateUserAccessToken( @CurrentUser() user: CurrentUser, diff --git a/packages/backend/server/src/core/auth/config.ts b/packages/backend/server/src/core/auth/config.ts index 05e5695c6f..afd5900541 100644 --- a/packages/backend/server/src/core/auth/config.ts +++ b/packages/backend/server/src/core/auth/config.ts @@ -8,6 +8,7 @@ export interface AuthConfig { ttr: number; }; allowSignup: boolean; + allowSignupForOauth: boolean; requireEmailDomainVerification: boolean; requireEmailVerification: boolean; passwordRequirements: ConfigItem<{ @@ -27,6 +28,10 @@ defineModuleConfig('auth', { desc: 'Whether allow new registrations.', default: true, }, + allowSignupForOauth: { + desc: 'Whether allow new registrations via configured oauth.', + default: true, + }, requireEmailDomainVerification: { desc: 'Whether require email domain record verification before accessing restricted resources.', default: false, diff --git a/packages/backend/server/src/core/mail/config.ts b/packages/backend/server/src/core/mail/config.ts index fb4d2273b5..81cc02d55c 100644 --- a/packages/backend/server/src/core/mail/config.ts +++ b/packages/backend/server/src/core/mail/config.ts @@ -6,6 +6,7 @@ declare global { interface AppConfigSchema { mailer: { SMTP: { + name: string; host: string; port: number; username: string; @@ -16,6 +17,7 @@ declare global { fallbackDomains: ConfigItem; fallbackSMTP: { + name: string; host: string; port: number; username: string; @@ -28,6 +30,11 @@ declare global { } defineModuleConfig('mailer', { + 'SMTP.name': { + desc: 'Name of the email server (e.g. your domain name)', + default: 'AFFiNE Server', + env: 'MAILER_SERVERNAME', + }, 'SMTP.host': { desc: 'Host of the email server (e.g. smtp.gmail.com)', default: '', @@ -64,6 +71,10 @@ defineModuleConfig('mailer', { default: [], shape: z.array(z.string()), }, + 'fallbackSMTP.name': { + desc: 'Name of the fallback email server (e.g. your domain name)', + default: 'AFFiNE Server', + }, 'fallbackSMTP.host': { desc: 'Host of the email server (e.g. smtp.gmail.com)', default: '', diff --git a/packages/backend/server/src/core/mail/sender.ts b/packages/backend/server/src/core/mail/sender.ts index 424dea85f0..70bf39007e 100644 --- a/packages/backend/server/src/core/mail/sender.ts +++ b/packages/backend/server/src/core/mail/sender.ts @@ -20,6 +20,7 @@ function configToSMTPOptions( config: AppConfig['mailer']['SMTP'] ): SMTPTransport.Options { return { + name: config.name, host: config.host, port: config.port, tls: { diff --git a/packages/backend/server/src/models/access-token.ts b/packages/backend/server/src/models/access-token.ts index c299f9f1b0..58d1a7fcf9 100644 --- a/packages/backend/server/src/models/access-token.ts +++ b/packages/backend/server/src/models/access-token.ts @@ -15,13 +15,14 @@ export class AccessTokenModel extends BaseModel { super(); } - async list(userId: string) { + async list(userId: string, revealed: boolean = false) { return await this.db.accessToken.findMany({ select: { id: true, name: true, createdAt: true, expiresAt: true, + token: revealed, }, where: { userId, diff --git a/packages/backend/server/src/models/copilot-session.ts b/packages/backend/server/src/models/copilot-session.ts index 7912e5373d..4c2e8db9de 100644 --- a/packages/backend/server/src/models/copilot-session.ts +++ b/packages/backend/server/src/models/copilot-session.ts @@ -459,27 +459,19 @@ export class CopilotSessionModel extends BaseModel { docId: options.docId, deletedAt: null, }, - select: { id: true, prompt: true }, + select: { id: true }, }); + const sessionIds = sessions.map(({ id }) => id); // cleanup all messages await this.db.aiSessionMessage.deleteMany({ where: { sessionId: { in: sessionIds } }, }); - // only mark action session as deleted - // chat session always can be reuse - const actionIds = sessions - .filter(({ prompt }) => !!prompt.action) - .map(({ id }) => id); - - // 标记 action session 为已删除 - if (actionIds.length > 0) { - await this.db.aiSession.updateMany({ - where: { id: { in: actionIds } }, - data: { pinned: false, deletedAt: new Date() }, - }); - } + await this.db.aiSession.updateMany({ + where: { id: { in: sessionIds } }, + data: { pinned: false, deletedAt: new Date() }, + }); return sessionIds; } diff --git a/packages/backend/server/src/plugins/copilot/prompt/prompts.ts b/packages/backend/server/src/plugins/copilot/prompt/prompts.ts index 07e9d1ad5d..d4e30b496f 100644 --- a/packages/backend/server/src/plugins/copilot/prompt/prompts.ts +++ b/packages/backend/server/src/plugins/copilot/prompt/prompts.ts @@ -1930,16 +1930,9 @@ Now apply the \`updates\` to the \`content\`, following the intent in \`op\`, an const CHAT_PROMPT: Omit = { model: 'gemini-2.5-flash', optionalModels: [ - 'gpt-4.1', - 'gpt-5', - 'o3', - 'o4-mini', 'gemini-2.5-flash', 'gemini-2.5-pro', - 'claude-opus-4@20250514', 'claude-sonnet-4@20250514', - 'claude-3-7-sonnet@20250219', - 'claude-3-5-sonnet-v2@20241022', ], messages: [ { @@ -2099,13 +2092,7 @@ Below is the user's query. Please respond in the user's preferred language witho 'codeArtifact', 'blobRead', ], - proModels: [ - 'gemini-2.5-pro', - 'claude-opus-4@20250514', - 'claude-sonnet-4@20250514', - 'claude-3-7-sonnet@20250219', - 'claude-3-5-sonnet-v2@20241022', - ], + proModels: ['gemini-2.5-pro', 'claude-sonnet-4@20250514'], }, }; diff --git a/packages/backend/server/src/plugins/oauth/controller.ts b/packages/backend/server/src/plugins/oauth/controller.ts index 7738688587..eb7e909e43 100644 --- a/packages/backend/server/src/plugins/oauth/controller.ts +++ b/packages/backend/server/src/plugins/oauth/controller.ts @@ -224,7 +224,7 @@ export class OAuthController { return connectedAccount.user; } - if (!this.config.auth.allowSignup) { + if (!this.config.auth.allowSignupForOauth) { throw new SignUpForbidden(); } diff --git a/packages/backend/server/src/plugins/payment/config.ts b/packages/backend/server/src/plugins/payment/config.ts index e234d4cfcb..fdc8c70b08 100644 --- a/packages/backend/server/src/plugins/payment/config.ts +++ b/packages/backend/server/src/plugins/payment/config.ts @@ -9,6 +9,13 @@ export interface PaymentStartupConfig { webhookKey: string; }; } & Stripe.StripeConfig; + revenuecat?: { + apiKey?: string; + webhookAuth?: string; + enabled?: boolean; + environment?: 'sandbox' | 'production'; + productMap?: Record; + }; } export interface PaymentRuntimeConfig { @@ -20,9 +27,36 @@ declare global { payment: { enabled: boolean; showLifetimePrice: boolean; + /** + * @deprecated use payment.stripe.apiKey + */ apiKey: string; + /** + * @deprecated use payment.stripe.webhookKey + */ webhookKey: string; - stripe: ConfigItem<{} & Stripe.StripeConfig>; + stripe: ConfigItem< + { + /** Preferred place for Stripe API key */ + apiKey?: string; + /** Preferred place for Stripe Webhook key */ + webhookKey?: string; + } & Stripe.StripeConfig + >; + revenuecat: ConfigItem<{ + /** Whether enable RevenueCat integration */ + enabled?: boolean; + /** RevenueCat REST API Key */ + apiKey?: string; + /** RevenueCat Project Id */ + projectId?: string; + /** Authorization header value required by webhook */ + webhookAuth?: string; + /** RC environment */ + environment?: 'sandbox' | 'production'; + /** Product whitelist mapping: productId -> { plan, recurring } */ + productMap?: Record; + }>; }; } } @@ -37,18 +71,33 @@ defineModuleConfig('payment', { default: true, }, apiKey: { - desc: 'Stripe API key to enable payment service.', + desc: '[Deprecated] Stripe API key. Use payment.stripe.apiKey instead.', default: '', env: 'STRIPE_API_KEY', }, webhookKey: { - desc: 'Stripe webhook key to enable payment service.', + desc: '[Deprecated] Stripe webhook key. Use payment.stripe.webhookKey instead.', default: '', env: 'STRIPE_WEBHOOK_KEY', }, stripe: { - desc: 'Stripe sdk options', - default: {}, + desc: 'Stripe sdk options and credentials', + default: { + apiKey: '', + webhookKey: '', + }, link: 'https://docs.stripe.com/api', }, + revenuecat: { + desc: 'RevenueCat integration configs', + default: { + enabled: false, + apiKey: '', + projectId: '', + webhookAuth: '', + environment: 'production', + productMap: {}, + }, + link: 'https://www.revenuecat.com/docs/', + }, }); diff --git a/packages/backend/server/src/plugins/payment/controller.ts b/packages/backend/server/src/plugins/payment/controller.ts index 03074d39fe..605502b0d5 100644 --- a/packages/backend/server/src/plugins/payment/controller.ts +++ b/packages/backend/server/src/plugins/payment/controller.ts @@ -19,7 +19,9 @@ export class StripeWebhookController { @Public() @Post('/webhook') async handleWebhook(@Req() req: RawBodyRequest) { - const webhookKey = this.config.payment.webhookKey; + const nestedWebhookKey = this.config.payment.stripe?.webhookKey; + const legacyWebhookKey = this.config.payment.webhookKey; + const webhookKey = nestedWebhookKey || legacyWebhookKey || ''; // Retrieve the event by verifying the signature using the raw body and secret. const signature = req.headers['stripe-signature']; try { diff --git a/packages/backend/server/src/plugins/payment/cron.ts b/packages/backend/server/src/plugins/payment/cron.ts index d612a05afc..eb2d5d1ce6 100644 --- a/packages/backend/server/src/plugins/payment/cron.ts +++ b/packages/backend/server/src/plugins/payment/cron.ts @@ -1,11 +1,13 @@ import { Injectable } from '@nestjs/common'; import { Cron, CronExpression } from '@nestjs/schedule'; -import { PrismaClient } from '@prisma/client'; +import { PrismaClient, Provider } from '@prisma/client'; import { EventBus, JobQueue, OnJob } from '../../base'; +import { RevenueCatWebhookHandler } from './revenuecat'; import { SubscriptionPlan, SubscriptionRecurring, + SubscriptionStatus, SubscriptionVariant, } from './types'; @@ -13,6 +15,8 @@ declare global { interface Jobs { 'nightly.cleanExpiredOnetimeSubscriptions': {}; 'nightly.notifyAboutToExpireWorkspaceSubscriptions': {}; + 'nightly.reconcileRevenueCatSubscriptions': {}; + 'nightly.revenuecat.syncUser': { userId: string }; } } @@ -21,7 +25,8 @@ export class SubscriptionCronJobs { constructor( private readonly db: PrismaClient, private readonly event: EventBus, - private readonly queue: JobQueue + private readonly queue: JobQueue, + private readonly rcHandler: RevenueCatWebhookHandler ) {} private getDateRange(after: number, base: number | Date = Date.now()) { @@ -45,6 +50,12 @@ export class SubscriptionCronJobs { } ); + await this.queue.add( + 'nightly.reconcileRevenueCatSubscriptions', + {}, + { jobId: 'nightly-payment-reconcile-revenuecat-subscriptions' } + ); + // FIXME(@forehalo): the strategy is totally wrong, for monthly plan. redesign required // await this.queue.add( // 'nightly.notifyAboutToExpireWorkspaceSubscriptions', @@ -142,4 +153,41 @@ export class SubscriptionCronJobs { }); } } + + @OnJob('nightly.reconcileRevenueCatSubscriptions') + async reconcileRevenueCatSubscriptions() { + // Find active/trialing/past_due RC subscriptions and resync via RC REST + const subs = await this.db.subscription.findMany({ + where: { + provider: Provider.revenuecat, + status: { + in: [ + SubscriptionStatus.Active, + SubscriptionStatus.Trialing, + SubscriptionStatus.PastDue, + ], + }, + }, + select: { targetId: true }, + }); + + // de-duplicate targetIds + const userIds = Array.from(new Set(subs.map(s => s.targetId))); + for (const userId of userIds) { + await this.queue.add( + 'nightly.revenuecat.syncUser', + { userId }, + { + attempts: 3, + backoff: { type: 'exponential', delay: 60_000 }, + jobId: `nightly-rc-sync-${userId}`, + } + ); + } + } + + @OnJob('nightly.revenuecat.syncUser') + async reconcileRevenueCatSubscriptionOfUser(payload: { userId: string }) { + await this.rcHandler.syncAppUser(payload.userId); + } } diff --git a/packages/backend/server/src/plugins/payment/index.ts b/packages/backend/server/src/plugins/payment/index.ts index f0b98a30dc..bca6936c30 100644 --- a/packages/backend/server/src/plugins/payment/index.ts +++ b/packages/backend/server/src/plugins/payment/index.ts @@ -23,6 +23,11 @@ import { UserSubscriptionResolver, WorkspaceSubscriptionResolver, } from './resolver'; +import { + RevenueCatService, + RevenueCatWebhookController, + RevenueCatWebhookHandler, +} from './revenuecat'; import { SubscriptionService } from './service'; import { StripeFactory, StripeProvider } from './stripe'; import { StripeWebhook } from './webhook'; @@ -40,10 +45,12 @@ import { StripeWebhook } from './webhook'; providers: [ StripeFactory, StripeProvider, + RevenueCatService, SubscriptionService, SubscriptionResolver, UserSubscriptionResolver, StripeWebhook, + RevenueCatWebhookHandler, UserSubscriptionManager, WorkspaceSubscriptionManager, SelfhostTeamSubscriptionManager, @@ -51,6 +58,10 @@ import { StripeWebhook } from './webhook'; WorkspaceSubscriptionResolver, PaymentEventHandlers, ], - controllers: [StripeWebhookController, LicenseController], + controllers: [ + StripeWebhookController, + LicenseController, + RevenueCatWebhookController, + ], }) export class PaymentModule {} diff --git a/packages/backend/server/src/plugins/payment/manager/common.ts b/packages/backend/server/src/plugins/payment/manager/common.ts index a10d963f74..c5a5324e75 100644 --- a/packages/backend/server/src/plugins/payment/manager/common.ts +++ b/packages/backend/server/src/plugins/payment/manager/common.ts @@ -30,6 +30,9 @@ export interface Subscription { trialEnd: Date | null; nextBillAt: Date | null; canceledAt: Date | null; + // read-only metadata for IAP integration + provider?: string | null; + iapStore?: string | null; } export interface Invoice { diff --git a/packages/backend/server/src/plugins/payment/manager/selfhost.ts b/packages/backend/server/src/plugins/payment/manager/selfhost.ts index aa14be5994..53fad460a2 100644 --- a/packages/backend/server/src/plugins/payment/manager/selfhost.ts +++ b/packages/backend/server/src/plugins/payment/manager/selfhost.ts @@ -1,8 +1,8 @@ import { randomUUID } from 'node:crypto'; import { Injectable } from '@nestjs/common'; -import { PrismaClient, UserStripeCustomer } from '@prisma/client'; -import { pick } from 'lodash-es'; +import { PrismaClient, Provider, UserStripeCustomer } from '@prisma/client'; +import { omit, pick } from 'lodash-es'; import { z } from 'zod'; import { SubscriptionPlanNotFound, URLHelper } from '../../../base'; @@ -132,8 +132,9 @@ export class SelfhostTeamSubscriptionManager extends SubscriptionManager { const [subscription] = await this.db.$transaction([ this.db.subscription.create({ data: { + provider: Provider.stripe, targetId: key, - ...subscriptionData, + ...omit(subscriptionData, 'provider', 'iapStore'), }, }), this.db.license.create({ diff --git a/packages/backend/server/src/plugins/payment/manager/user.ts b/packages/backend/server/src/plugins/payment/manager/user.ts index bd59641932..83e167373c 100644 --- a/packages/backend/server/src/plugins/payment/manager/user.ts +++ b/packages/backend/server/src/plugins/payment/manager/user.ts @@ -9,6 +9,7 @@ import { EventBus, InternalServerError, InvalidCheckoutParameters, + ManagedByAppStoreOrPlay, Mutex, OnEvent, SubscriptionAlreadyExists, @@ -103,6 +104,14 @@ export class UserSubscriptionManager extends SubscriptionManager { throw new InvalidCheckoutParameters(); } + const active = await this.getActiveSubscription({ + plan: lookupKey.plan, + userId: user.id, + }); + if (active?.provider === 'revenuecat') { + throw new ManagedByAppStoreOrPlay(); + } + const subscription = await this.getSubscription({ plan: lookupKey.plan, userId: user.id, @@ -256,7 +265,7 @@ export class UserSubscriptionManager extends SubscriptionManager { ]), create: { targetId: userId, - ...subscriptionData, + ...omit(subscriptionData, ['provider', 'iapStore']), }, }); } diff --git a/packages/backend/server/src/plugins/payment/manager/workspace.ts b/packages/backend/server/src/plugins/payment/manager/workspace.ts index 42602112c6..8e2b3a4d71 100644 --- a/packages/backend/server/src/plugins/payment/manager/workspace.ts +++ b/packages/backend/server/src/plugins/payment/manager/workspace.ts @@ -1,5 +1,5 @@ import { Injectable } from '@nestjs/common'; -import { PrismaClient, UserStripeCustomer } from '@prisma/client'; +import { PrismaClient, Provider, UserStripeCustomer } from '@prisma/client'; import { omit, pick } from 'lodash-es'; import { z } from 'zod'; @@ -157,6 +157,7 @@ export class WorkspaceSubscriptionManager extends SubscriptionManager { return this.db.subscription.upsert({ where: { + provider: Provider.stripe, stripeSubscriptionId: stripeSubscription.id, }, update: { @@ -171,7 +172,7 @@ export class WorkspaceSubscriptionManager extends SubscriptionManager { }, create: { targetId: workspaceId, - ...subscriptionData, + ...omit(subscriptionData, 'provider', 'iapStore'), }, }); } diff --git a/packages/backend/server/src/plugins/payment/resolver.ts b/packages/backend/server/src/plugins/payment/resolver.ts index 62a6b4831f..43ede3fbe1 100644 --- a/packages/backend/server/src/plugins/payment/resolver.ts +++ b/packages/backend/server/src/plugins/payment/resolver.ts @@ -108,6 +108,23 @@ export class SubscriptionType implements Partial { @Field(() => Date) updatedAt!: Date; + // read-only fields for display purpose + // provider: 'stripe' | 'revenuecat' + @Field(() => String, { + nullable: true, + description: + 'Payment provider of this subscription. Read-only. One of: stripe | revenuecat', + }) + provider?: string | null; + + // iapStore: 'app_store' | 'play_store' | null when provider is stripe + @Field(() => String, { + nullable: true, + description: + 'If provider is revenuecat, indicates underlying store. Read-only. One of: app_store | play_store', + }) + iapStore?: string | null; + // deprecated fields @Field(() => String, { name: 'id', diff --git a/packages/backend/server/src/plugins/payment/revenuecat/controller.ts b/packages/backend/server/src/plugins/payment/revenuecat/controller.ts new file mode 100644 index 0000000000..6c60b84d56 --- /dev/null +++ b/packages/backend/server/src/plugins/payment/revenuecat/controller.ts @@ -0,0 +1,112 @@ +import { Body, Controller, Headers, Logger, Post } from '@nestjs/common'; +import { z } from 'zod'; + +import { Config, EventBus } from '../../../base'; +import { Public } from '../../../core/auth'; + +const RcEventSchema = z + .object({ + type: z.enum([ + 'TEST', + 'INITIAL_PURCHASE', + 'NON_RENEWING_PURCHASE', + 'RENEWAL', + 'PRODUCT_CHANGE', + 'CANCELLATION', + 'BILLING_ISSUE', + 'SUBSCRIBER_ALIAS', + 'SUBSCRIPTION_PAUSED', + 'UNCANCELLATION', + 'TRANSFER', + 'SUBSCRIPTION_EXTENDED', + 'EXPIRATION', + 'TEMPORARY_ENTITLEMENT_GRANT', + 'INVOICE_ISSUANCE', + 'VIRTUAL_CURRENCY_TRANSACTION', + ]), + id: z.string(), + app_id: z.string(), + environment: z.enum(['PRODUCTION', 'SANDBOX']), + + app_user_id: z.string().optional(), + store: z.string().optional(), + is_family_share: z.boolean().nullable().optional(), + period_type: z + .enum(['TRIAL', 'INTRO', 'NORMAL', 'PROMOTIONAL', 'PREPAID']) + .nullable() + .optional(), + original_transaction_id: z.string().nullable().optional(), + transaction_id: z.string().nullable().optional(), + purchase_token: z.string().nullable().optional(), + }) + .passthrough(); + +const RcWebhookPayloadSchema = z.object({ event: RcEventSchema }).passthrough(); + +export type RcEvent = z.infer; +type RcPayload = z.infer; + +@Controller('/api/revenuecat') +export class RevenueCatWebhookController { + private readonly logger = new Logger(RevenueCatWebhookController.name); + + constructor( + private readonly config: Config, + private readonly event: EventBus + ) {} + + @Public() + @Post('/webhook') + async handleWebhook( + @Body() body: RcPayload, + @Headers('authorization') authorization?: string + ) { + const { enabled, webhookAuth, environment } = + this.config.payment.revenuecat || {}; + if (enabled) { + if (webhookAuth && authorization === webhookAuth) { + try { + const parsed = RcWebhookPayloadSchema.safeParse(body); + if (parsed.success) { + const event = parsed.data.event; + const { id, app_user_id: appUserId, type } = event; + if ( + event.environment.toLowerCase() === environment?.toLowerCase() + ) { + this.logger.log( + `[${id}] RevenueCat Webhook {${type}} received for appUserId=${appUserId}.` + ); + + if ( + appUserId && + (typeof event.is_family_share !== 'boolean' || + !event.is_family_share) + ) { + // immediately ack and process asynchronously + this.event + .emitAsync('revenuecat.webhook', { appUserId, event }) + .catch((e: Error) => { + this.logger.error( + 'Failed to handle RevenueCat Webhook event.', + e + ); + }); + } + } + } else { + this.logger.warn( + 'RevenueCat webhook invalid payload received.', + parsed.error + ); + } + } catch (e) { + this.logger.error('RevenueCat webhook error', e as Error); + } + } else { + this.logger.warn('RevenueCat webhook unauthorized.'); + } + } + + return { ok: true }; + } +} diff --git a/packages/backend/server/src/plugins/payment/revenuecat/index.ts b/packages/backend/server/src/plugins/payment/revenuecat/index.ts new file mode 100644 index 0000000000..11b0031755 --- /dev/null +++ b/packages/backend/server/src/plugins/payment/revenuecat/index.ts @@ -0,0 +1,4 @@ +export { type RcEvent, RevenueCatWebhookController } from './controller'; +export { resolveProductMapping } from './map'; +export { RevenueCatService, type Subscription } from './service'; +export { RevenueCatWebhookHandler } from './webhook'; diff --git a/packages/backend/server/src/plugins/payment/revenuecat/map.ts b/packages/backend/server/src/plugins/payment/revenuecat/map.ts new file mode 100644 index 0000000000..e8c42a733e --- /dev/null +++ b/packages/backend/server/src/plugins/payment/revenuecat/map.ts @@ -0,0 +1,69 @@ +import { SubscriptionPlan, SubscriptionRecurring } from '../types'; +import { Subscription } from './service'; + +export interface ProductMapping { + plan: SubscriptionPlan; + recurring: SubscriptionRecurring; +} + +// default whitelist mapping per PRD +export const DEFAULT_PRODUCT_MAP: Record = { + 'app.affine.pro.Monthly': { + plan: SubscriptionPlan.Pro, + recurring: SubscriptionRecurring.Monthly, + }, + 'app.affine.pro.Annual': { + plan: SubscriptionPlan.Pro, + recurring: SubscriptionRecurring.Yearly, + }, + 'app.affine.pro.ai.Annual': { + plan: SubscriptionPlan.AI, + recurring: SubscriptionRecurring.Yearly, + }, +}; + +function resolveFallbackFromEntitlement( + entitlement: string | null | undefined, + duration: string | null | undefined +): ProductMapping | null { + const ent = (entitlement || '').toLowerCase(); + const dur = (duration || '').toUpperCase(); + const isPro = ent === 'pro'; + const isAI = ent === 'ai'; + const isM = dur === 'P1M'; + const isY = dur === 'P1Y'; + if ((isPro || isAI) && (isM || isY)) { + return { + plan: isPro ? SubscriptionPlan.Pro : SubscriptionPlan.AI, + recurring: isM + ? SubscriptionRecurring.Monthly + : SubscriptionRecurring.Yearly, + }; + } + return null; +} + +export function resolveProductMapping( + sub: Partial, + override?: Record +): ProductMapping | null { + const { productId, identifier, duration } = sub; + if (override && productId && productId in override) { + const m = override[productId]; + const plan = m.plan as SubscriptionPlan; + const recurring = m.recurring as SubscriptionRecurring; + if ( + [SubscriptionPlan.Pro, SubscriptionPlan.AI].includes(plan) && + [SubscriptionRecurring.Monthly, SubscriptionRecurring.Yearly].includes( + recurring + ) + ) { + return { plan, recurring }; + } + } + return ( + (productId && DEFAULT_PRODUCT_MAP[productId]) || + resolveFallbackFromEntitlement(identifier, duration) || + null + ); +} diff --git a/packages/backend/server/src/plugins/payment/revenuecat/service.ts b/packages/backend/server/src/plugins/payment/revenuecat/service.ts new file mode 100644 index 0000000000..e3da60fdd4 --- /dev/null +++ b/packages/backend/server/src/plugins/payment/revenuecat/service.ts @@ -0,0 +1,170 @@ +import { Injectable } from '@nestjs/common'; +import { z } from 'zod'; + +import { Config } from '../../../base'; + +const Store = z.enum([ + 'amazon', + 'app_store', + 'mac_app_store', + 'play_store', + 'promotional', + 'stripe', + 'rc_billing', + 'roku', + 'paddle', +]); + +const zRcV2RawProduct = z + .object({ + id: z.string().nonempty(), + store_identifier: z.string().nonempty(), + subscription: z + .object({ duration: z.string().nullable() }) + .partial() + .nullable(), + app: z.object({ type: Store }).partial(), + }) + .passthrough(); + +const zRcV2RawEntitlementItem = z + .object({ + lookup_key: z.string().nonempty(), + display_name: z.string().nonempty(), + products: z + .object({ items: z.array(zRcV2RawProduct).default([]) }) + .partial() + .nullable(), + }) + .passthrough(); + +const zRcV2RawEntitlements = z + .object({ items: z.array(zRcV2RawEntitlementItem).default([]) }) + .partial(); + +const zRcV2RawSubscription = z + .object({ + object: z.enum(['subscription']), + entitlements: zRcV2RawEntitlements, + starts_at: z.number(), + current_period_ends_at: z.number().nullable(), + store: Store, + auto_renewal_status: z.enum([ + 'will_renew', + 'will_not_renew', + 'will_change_product', + 'will_pause', + 'requires_price_increase_consent', + 'has_already_renewed', + ]), + status: z.enum([ + 'trialing', + 'active', + 'expired', + 'in_grace_period', + 'in_billing_retry', + 'paused', + 'unknown', + 'incomplete', + ]), + gives_access: z.boolean(), + }) + .passthrough(); + +const zRcV2RawEnvelope = z + .object({ + app_user_id: z.string().optional(), + id: z.string().optional(), + subscriptions: z.array(zRcV2RawSubscription).default([]), + }) + .passthrough(); + +// v2 minimal, simplified structure exposed to callers +export const Subscription = z.object({ + identifier: z.string(), + isTrial: z.boolean(), + isActive: z.boolean(), + latestPurchaseDate: z.date().nullable(), + expirationDate: z.date().nullable(), + productId: z.string(), + store: Store, + willRenew: z.boolean(), + duration: z.string().nullable(), +}); + +export type Subscription = z.infer; + +@Injectable() +export class RevenueCatService { + constructor(private readonly config: Config) {} + + private get apiKey(): string { + const key = this.config.payment.revenuecat?.apiKey; + if (!key) { + throw new Error('RevenueCat API key is not configured'); + } + return key; + } + + private get projectId(): string { + const id = this.config.payment.revenuecat?.projectId; + if (!id) { + throw new Error('RevenueCat Project ID is not configured'); + } + return id; + } + + async getSubscriptions(customerId: string): Promise { + const res = await fetch( + `https://api.revenuecat.com/v2/projects/${this.projectId}/customers/${customerId}/subscriptions`, + { + headers: { + Authorization: `Bearer ${this.apiKey}`, + 'Content-Type': 'application/json', + }, + } + ); + + if (!res.ok) { + const text = await res.text(); + throw new Error( + `RevenueCat getSubscriber failed: ${res.status} ${res.statusText} - ${text}` + ); + } + + const envParsed = zRcV2RawEnvelope.safeParse(await res.json()); + + if (envParsed.success) { + return envParsed.data.subscriptions + .flatMap(sub => { + const items = sub.entitlements.items ?? []; + return items.map(ent => { + const product = ent.products?.items?.[0]; + if (!product) { + return null; + } + return { + identifier: ent.lookup_key, + isTrial: sub.status === 'trialing', + isActive: + sub.gives_access === true || + sub.status === 'active' || + sub.status === 'trialing', + latestPurchaseDate: sub.starts_at + ? new Date(sub.starts_at * 1000) + : null, + expirationDate: sub.current_period_ends_at + ? new Date(sub.current_period_ends_at * 1000) + : null, + productId: product.store_identifier, + store: sub.store ?? product.app.type, + willRenew: sub.auto_renewal_status === 'will_renew', + duration: product.subscription?.duration ?? null, + }; + }); + }) + .filter((s): s is Subscription => s !== null); + } + return null; + } +} diff --git a/packages/backend/server/src/plugins/payment/revenuecat/webhook.ts b/packages/backend/server/src/plugins/payment/revenuecat/webhook.ts new file mode 100644 index 0000000000..0aa066a71d --- /dev/null +++ b/packages/backend/server/src/plugins/payment/revenuecat/webhook.ts @@ -0,0 +1,226 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { IapStore, PrismaClient, Provider } from '@prisma/client'; + +import { Config, EventBus, OnEvent } from '../../../base'; +import { SubscriptionStatus } from '../types'; +import { RcEvent } from './controller'; +import { resolveProductMapping } from './map'; +import { RevenueCatService, Subscription } from './service'; + +@Injectable() +export class RevenueCatWebhookHandler { + private readonly logger = new Logger(RevenueCatWebhookHandler.name); + + constructor( + private readonly rc: RevenueCatService, + private readonly db: PrismaClient, + private readonly config: Config, + private readonly event: EventBus + ) {} + + @OnEvent('revenuecat.webhook') + async onWebhook(evt: { appUserId?: string; event: RcEvent }) { + if (!this.config.payment.revenuecat?.enabled) return; + + const appUserId = evt.appUserId; + if (!appUserId) { + this.logger.warn('RevenueCat webhook missing appUserId'); + return; + } + await this.syncAppUser(appUserId, evt.event); + } + + // Exposed for reuse by reconcile job + async syncAppUser(appUserId: string, event?: RcEvent) { + // Pull latest state to be resilient to reorder/duplicate events + let subscriptions: Awaited< + ReturnType + >; + try { + subscriptions = await this.rc.getSubscriptions(appUserId); + if (!subscriptions) return; + } catch (e) { + this.logger.error(`Failed to fetch RC subscriber for ${appUserId}`, e); + return; + } + + const productOverride = this.config.payment.revenuecat?.productMap; + + for (const sub of subscriptions) { + const mapping = resolveProductMapping(sub, productOverride); + // ignore non-whitelisted and non-fallbackable products + if (!mapping) continue; + + const { status, deleteInstead, canceledAt, iapStore } = + this.mapStatus(sub); + + const rcExternalRef = this.pickExternalRef(event); + + // Mutual exclusion: skip if Stripe already active for the same plan + const conflict = await this.db.subscription.findFirst({ + where: { + targetId: appUserId, + plan: mapping.plan, + provider: Provider.stripe, + status: { + in: [SubscriptionStatus.Active, SubscriptionStatus.Trialing], + }, + }, + }); + if (conflict) { + this.logger.warn( + `Skip RC upsert: Stripe active exists. user=${appUserId} plan=${mapping.plan}` + ); + continue; + } + + if (deleteInstead) { + // delete record and emit cancellation if any record removed + const result = await this.db.subscription.deleteMany({ + where: { + targetId: appUserId, + plan: mapping.plan, + provider: Provider.revenuecat, + }, + }); + if (result.count > 0) { + this.event.emit('user.subscription.canceled', { + userId: appUserId, + plan: mapping.plan, + recurring: mapping.recurring, + }); + } + continue; + } + + // Upsert by unique (targetId, plan) for idempotency + const start = sub.latestPurchaseDate || new Date(); + const end = sub.expirationDate || null; + const nextBillAt = end; // period end serves as next bill anchor for IAP + + await this.db.subscription.upsert({ + where: { + targetId_plan: { targetId: appUserId, plan: mapping.plan }, + }, + update: { + recurring: mapping.recurring, + variant: null, + quantity: 1, + stripeSubscriptionId: null, + stripeScheduleId: null, + provider: Provider.revenuecat, + iapStore: iapStore, + rcEntitlement: sub.identifier ?? null, + rcProductId: sub.productId || null, + rcExternalRef: rcExternalRef, + status: status, + start, + end, + nextBillAt, + canceledAt: canceledAt ?? null, + trialStart: null, + trialEnd: null, + }, + create: { + targetId: appUserId, + plan: mapping.plan, + recurring: mapping.recurring, + variant: null, + quantity: 1, + stripeSubscriptionId: null, + stripeScheduleId: null, + provider: Provider.revenuecat, + iapStore: iapStore, + rcEntitlement: sub.identifier ?? null, + rcProductId: sub.productId || null, + rcExternalRef: rcExternalRef, + status: status, + start, + end, + nextBillAt, + canceledAt: canceledAt ?? null, + trialStart: null, + trialEnd: null, + }, + }); + + if ( + status === SubscriptionStatus.Active || + status === SubscriptionStatus.Trialing + ) { + this.event.emit('user.subscription.activated', { + userId: appUserId, + plan: mapping.plan, + recurring: mapping.recurring, + }); + } else if (status !== SubscriptionStatus.PastDue) { + // Do not emit canceled for PastDue (still within retry/grace window) + this.event.emit('user.subscription.canceled', { + userId: appUserId, + plan: mapping.plan, + recurring: mapping.recurring, + }); + } + } + } + + private pickExternalRef(e?: RcEvent): string | null { + return ( + (e && + (e.original_transaction_id || e.purchase_token || e.transaction_id)) || + null + ); + } + + private mapStatus(sub: Subscription): { + status: SubscriptionStatus; + iapStore: IapStore | null; + deleteInstead: boolean; + canceledAt?: Date | null; + } { + const now = Date.now(); + const exp = sub.expirationDate?.getTime(); + + // Determine iap store and external reference for observability + const iapStore = ['app_store', 'mac_app_store'].includes(sub.store) + ? IapStore.app_store + : ['play_store'].includes(sub.store) + ? IapStore.play_store + : null; + + if (sub.isActive) { + if (sub.isTrial) { + return { + iapStore, + status: SubscriptionStatus.Trialing, + deleteInstead: false, + canceledAt: null, + }; + } + // PastDue from subscriber is not directly indicated; treat active as Active + const canceledAt = sub.willRenew === false ? new Date() : null; + return { + iapStore, + status: SubscriptionStatus.Active, + deleteInstead: false, + canceledAt, + }; + } + + // inactive: if not expired yet (grace/pastdue), keep as PastDue; otherwise delete + if (exp && exp > now) { + return { + iapStore, + status: SubscriptionStatus.PastDue, + deleteInstead: false, + canceledAt: null, + }; + } + + return { + iapStore, + status: SubscriptionStatus.Canceled, + deleteInstead: true, + }; + } +} diff --git a/packages/backend/server/src/plugins/payment/service.ts b/packages/backend/server/src/plugins/payment/service.ts index f08e07c7f7..9d80cf2832 100644 --- a/packages/backend/server/src/plugins/payment/service.ts +++ b/packages/backend/server/src/plugins/payment/service.ts @@ -13,6 +13,7 @@ import { InvalidLicenseSessionId, InvalidSubscriptionParameters, LicenseRevealed, + ManagedByAppStoreOrPlay, OnEvent, SameSubscriptionRecurring, SubscriptionExpired, @@ -165,6 +166,11 @@ export class SubscriptionService { throw new SubscriptionNotExists({ plan: identity.plan }); } + // IAP read-only: RevenueCat-managed subscriptions cannot be modified on web + if (subscription.provider === 'revenuecat') { + throw new ManagedByAppStoreOrPlay(); + } + if (!subscription.stripeSubscriptionId) { throw new CantUpdateOnetimePaymentSubscription( 'Onetime payment subscription cannot be canceled.' @@ -211,6 +217,11 @@ export class SubscriptionService { throw new SubscriptionNotExists({ plan: identity.plan }); } + // IAP read-only: RevenueCat-managed subscriptions cannot be modified on web + if (subscription.provider === 'revenuecat') { + throw new ManagedByAppStoreOrPlay(); + } + if (!subscription.canceledAt) { throw new SubscriptionHasNotBeenCanceled(); } @@ -258,6 +269,11 @@ export class SubscriptionService { throw new SubscriptionNotExists({ plan: identity.plan }); } + // IAP read-only: RevenueCat-managed subscriptions cannot be modified on web + if (subscription.provider === 'revenuecat') { + throw new ManagedByAppStoreOrPlay(); + } + if (!subscription.stripeSubscriptionId) { throw new CantUpdateOnetimePaymentSubscription(); } @@ -312,6 +328,10 @@ export class SubscriptionService { throw new SubscriptionNotExists({ plan: identity.plan }); } + if (subscription.provider === 'revenuecat') { + throw new ManagedByAppStoreOrPlay(); + } + if (!subscription.stripeSubscriptionId) { throw new CantUpdateOnetimePaymentSubscription(); } diff --git a/packages/backend/server/src/plugins/payment/stripe.ts b/packages/backend/server/src/plugins/payment/stripe.ts index 68ccc8a34c..59a811a733 100644 --- a/packages/backend/server/src/plugins/payment/stripe.ts +++ b/packages/backend/server/src/plugins/payment/stripe.ts @@ -39,15 +39,20 @@ export class StripeFactory { } setup() { - // TODO@(@forehalo): use per-requests api key injection - this.#stripe = new Stripe( - this.config.payment.apiKey || - // NOTE(@forehalo): - // we always fake a key if not set because `new Stripe` will complain if it's empty string - // this will make code cleaner than providing `Stripe` instance as optional one. - 'stripe-api-key', - this.config.payment.stripe - ); + // Prefer new keys under payment.stripe.*, fallback to legacy root keys for backward compatibility + const { + apiKey: nestedApiKey, + webhookKey: _, + ...config + } = this.config.payment.stripe || {}; + // NOTE: + // we always fake a key if not set because `new Stripe` will complain if it's empty string + // this will make code cleaner than providing `Stripe` instance as optional one. + const apiKey = + nestedApiKey || this.config.payment.apiKey || 'stripe-api-key'; + + // TODO@(@darkskygit): use per-requests api key injection + this.#stripe = new Stripe(apiKey, config); if (this.config.payment.enabled) { this.server.enableFeature(ServerFeature.Payment); } else { diff --git a/packages/backend/server/src/plugins/payment/types.ts b/packages/backend/server/src/plugins/payment/types.ts index ac0497f9ee..6181e9a7f0 100644 --- a/packages/backend/server/src/plugins/payment/types.ts +++ b/packages/backend/server/src/plugins/payment/types.ts @@ -1,6 +1,8 @@ import type { User, Workspace } from '@prisma/client'; import Stripe from 'stripe'; +import type { RcEvent } from './revenuecat'; + export enum SubscriptionRecurring { Monthly = 'monthly', Yearly = 'yearly', @@ -86,6 +88,12 @@ declare global { 'stripe.customer.subscription.created': Stripe.CustomerSubscriptionCreatedEvent; 'stripe.customer.subscription.updated': Stripe.CustomerSubscriptionUpdatedEvent; 'stripe.customer.subscription.deleted': Stripe.CustomerSubscriptionDeletedEvent; + + // RevenueCat integration + 'revenuecat.webhook': { + appUserId?: string; + event: RcEvent; + }; } } diff --git a/packages/backend/server/src/schema.gql b/packages/backend/server/src/schema.gql index 15551a45bc..3f2d10b010 100644 --- a/packages/backend/server/src/schema.gql +++ b/packages/backend/server/src/schema.gql @@ -744,6 +744,7 @@ enum ErrorNames { LICENSE_REVEALED LINK_EXPIRED MAILER_SERVICE_IS_NOT_CONFIGURED + MANAGED_BY_APP_STORE_OR_PLAY MEMBER_NOT_FOUND_IN_SPACE MEMBER_QUOTA_EXCEEDED MENTION_USER_DOC_ACCESS_DENIED @@ -1595,6 +1596,7 @@ type Query { """query workspace embedding status""" queryWorkspaceEmbeddingStatus(workspaceId: String!): ContextWorkspaceEmbeddingStatus! + revealedAccessTokens: [RevealedAccessToken!]! """server config""" serverConfig: ServerConfigType! @@ -1962,6 +1964,11 @@ type SubscriptionType { canceledAt: DateTime createdAt: DateTime! end: DateTime + + """ + If provider is revenuecat, indicates underlying store. Read-only. One of: app_store | play_store + """ + iapStore: String id: String @deprecated(reason: "removed") nextBillAt: DateTime @@ -1970,6 +1977,11 @@ type SubscriptionType { There won't actually be a subscription with plan 'Free' """ plan: SubscriptionPlan! + + """ + Payment provider of this subscription. Read-only. One of: stripe | revenuecat + """ + provider: String recurring: SubscriptionRecurring! start: DateTime! status: SubscriptionStatus! diff --git a/packages/backend/server/src/server.ts b/packages/backend/server/src/server.ts index 69eb93dd1f..a130dd9f81 100644 --- a/packages/backend/server/src/server.ts +++ b/packages/backend/server/src/server.ts @@ -59,6 +59,21 @@ export async function run() { const adapter = new SocketIoAdapter(app); app.useWebSocketAdapter(adapter); + if (env.dev) { + const { SwaggerModule, DocumentBuilder } = await import('@nestjs/swagger'); + // Swagger API Docs + const docConfig = new DocumentBuilder() + .setTitle('AFFiNE API') + .setDescription(`AFFiNE Server ${env.version} API documentation`) + .setVersion(`${env.version}`) + .build(); + const documentFactory = () => SwaggerModule.createDocument(app, docConfig); + SwaggerModule.setup('/api/docs', app, documentFactory, { + useGlobalPrefix: true, + swaggerOptions: { persistAuthorization: true }, + }); + } + const url = app.get(URLHelper); const listeningHost = '0.0.0.0'; diff --git a/packages/common/graphql/src/graphql/access-token/list.gql b/packages/common/graphql/src/graphql/access-token/list.gql index e3ebeeec8d..13dd31027d 100644 --- a/packages/common/graphql/src/graphql/access-token/list.gql +++ b/packages/common/graphql/src/graphql/access-token/list.gql @@ -1,8 +1,9 @@ query listUserAccessTokens { - accessTokens { + revealedAccessTokens { id name createdAt expiresAt + token } } \ No newline at end of file diff --git a/packages/common/graphql/src/graphql/index.ts b/packages/common/graphql/src/graphql/index.ts index 4ad96df004..4f43d4dbfa 100644 --- a/packages/common/graphql/src/graphql/index.ts +++ b/packages/common/graphql/src/graphql/index.ts @@ -88,11 +88,12 @@ export const listUserAccessTokensQuery = { id: 'listUserAccessTokensQuery' as const, op: 'listUserAccessTokens', query: `query listUserAccessTokens { - accessTokens { + revealedAccessTokens { id name createdAt expiresAt + token } }`, }; diff --git a/packages/common/graphql/src/schema.ts b/packages/common/graphql/src/schema.ts index 541cb9fb6a..07adda5dd1 100644 --- a/packages/common/graphql/src/schema.ts +++ b/packages/common/graphql/src/schema.ts @@ -923,6 +923,7 @@ export enum ErrorNames { LICENSE_REVEALED = 'LICENSE_REVEALED', LINK_EXPIRED = 'LINK_EXPIRED', MAILER_SERVICE_IS_NOT_CONFIGURED = 'MAILER_SERVICE_IS_NOT_CONFIGURED', + MANAGED_BY_APP_STORE_OR_PLAY = 'MANAGED_BY_APP_STORE_OR_PLAY', MEMBER_NOT_FOUND_IN_SPACE = 'MEMBER_NOT_FOUND_IN_SPACE', MEMBER_QUOTA_EXCEEDED = 'MEMBER_QUOTA_EXCEEDED', MENTION_USER_DOC_ACCESS_DENIED = 'MENTION_USER_DOC_ACCESS_DENIED', @@ -2165,6 +2166,7 @@ export interface Query { publicUserById: Maybe; /** query workspace embedding status */ queryWorkspaceEmbeddingStatus: ContextWorkspaceEmbeddingStatus; + revealedAccessTokens: Array; /** server config */ serverConfig: ServerConfigType; /** Get user by email */ @@ -2583,6 +2585,8 @@ export interface SubscriptionType { canceledAt: Maybe; createdAt: Scalars['DateTime']['output']; end: Maybe; + /** If provider is revenuecat, indicates underlying store. Read-only. One of: app_store | play_store */ + iapStore: Maybe; /** @deprecated removed */ id: Maybe; nextBillAt: Maybe; @@ -2591,6 +2595,8 @@ export interface SubscriptionType { * There won't actually be a subscription with plan 'Free' */ plan: SubscriptionPlan; + /** Payment provider of this subscription. Read-only. One of: stripe | revenuecat */ + provider: Maybe; recurring: SubscriptionRecurring; start: Scalars['DateTime']['output']; status: SubscriptionStatus; @@ -3084,12 +3090,13 @@ export type ListUserAccessTokensQueryVariables = Exact<{ export type ListUserAccessTokensQuery = { __typename?: 'Query'; - accessTokens: Array<{ - __typename?: 'AccessToken'; + revealedAccessTokens: Array<{ + __typename?: 'RevealedAccessToken'; id: string; name: string; createdAt: string; expiresAt: string | null; + token: string; }>; }; diff --git a/packages/frontend/admin/package.json b/packages/frontend/admin/package.json index c387d88c45..ee34951807 100644 --- a/packages/frontend/admin/package.json +++ b/packages/frontend/admin/package.json @@ -8,7 +8,7 @@ "@affine/error": "workspace:*", "@affine/graphql": "workspace:*", "@affine/routes": "workspace:*", - "@blocksuite/icons": "^2.2.12", + "@blocksuite/icons": "^2.2.17", "@radix-ui/react-accordion": "^1.2.2", "@radix-ui/react-alert-dialog": "^1.1.3", "@radix-ui/react-aspect-ratio": "^1.1.1", diff --git a/packages/frontend/admin/src/config.json b/packages/frontend/admin/src/config.json index b267abd9a0..09f4368a23 100644 --- a/packages/frontend/admin/src/config.json +++ b/packages/frontend/admin/src/config.json @@ -63,6 +63,10 @@ "type": "Boolean", "desc": "Whether allow new registrations." }, + "allowSignupForOauth": { + "type": "Boolean", + "desc": "Whether allow new registrations via configured oauth." + }, "requireEmailDomainVerification": { "type": "Boolean", "desc": "Whether require email domain record verification before accessing restricted resources." @@ -85,6 +89,11 @@ } }, "mailer": { + "SMTP.name": { + "type": "String", + "desc": "Name of the email server (e.g. your domain name)", + "env": "MAILER_SERVERNAME" + }, "SMTP.host": { "type": "String", "desc": "Host of the email server (e.g. smtp.gmail.com)", @@ -119,6 +128,10 @@ "type": "Array", "desc": "The emails from these domains are always sent using the fallback SMTP server." }, + "fallbackSMTP.name": { + "type": "String", + "desc": "Name of the fallback email server (e.g. your domain name)" + }, "fallbackSMTP.host": { "type": "String", "desc": "Host of the email server (e.g. smtp.gmail.com)" @@ -387,18 +400,23 @@ }, "apiKey": { "type": "String", - "desc": "Stripe API key to enable payment service.", + "desc": "[Deprecated] Stripe API key. Use payment.stripe.apiKey instead.", "env": "STRIPE_API_KEY" }, "webhookKey": { "type": "String", - "desc": "Stripe webhook key to enable payment service.", + "desc": "[Deprecated] Stripe webhook key. Use payment.stripe.webhookKey instead.", "env": "STRIPE_WEBHOOK_KEY" }, "stripe": { "type": "Object", - "desc": "Stripe sdk options", + "desc": "Stripe sdk options and credentials", "link": "https://docs.stripe.com/api" + }, + "revenuecat": { + "type": "Object", + "desc": "RevenueCat integration configs", + "link": "https://www.revenuecat.com/docs/" } }, "worker": { diff --git a/packages/frontend/admin/src/modules/settings/config.ts b/packages/frontend/admin/src/modules/settings/config.ts index 4c36fd93ed..815ba3248d 100644 --- a/packages/frontend/admin/src/modules/settings/config.ts +++ b/packages/frontend/admin/src/modules/settings/config.ts @@ -55,6 +55,7 @@ export const KNOWN_CONFIG_GROUPS = [ module: 'auth', fields: [ 'allowSignup', + 'allowSignupForOauth', // nested json object { key: 'passwordRequirements', diff --git a/packages/frontend/apps/android/App/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/packages/frontend/apps/android/App/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index bbd3e02123..50ec886232 100644 --- a/packages/frontend/apps/android/App/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/packages/frontend/apps/android/App/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -2,4 +2,5 @@ + \ No newline at end of file diff --git a/packages/frontend/apps/android/App/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/packages/frontend/apps/android/App/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml index bbd3e02123..50ec886232 100644 --- a/packages/frontend/apps/android/App/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ b/packages/frontend/apps/android/App/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -2,4 +2,5 @@ + \ No newline at end of file diff --git a/packages/frontend/apps/android/package.json b/packages/frontend/apps/android/package.json index b96e02fb07..92399ddd01 100644 --- a/packages/frontend/apps/android/package.json +++ b/packages/frontend/apps/android/package.json @@ -17,7 +17,7 @@ "@affine/i18n": "workspace:*", "@affine/nbstore": "workspace:*", "@blocksuite/affine": "workspace:*", - "@blocksuite/icons": "^2.2.12", + "@blocksuite/icons": "^2.2.17", "@capacitor/android": "^7.0.0", "@capacitor/app": "^7.0.0", "@capacitor/core": "^7.0.0", diff --git a/packages/frontend/apps/ios/App/App.xcodeproj/project.pbxproj b/packages/frontend/apps/ios/App/App.xcodeproj/project.pbxproj index 04e88c07e7..92d8c25bc2 100644 --- a/packages/frontend/apps/ios/App/App.xcodeproj/project.pbxproj +++ b/packages/frontend/apps/ios/App/App.xcodeproj/project.pbxproj @@ -8,15 +8,15 @@ /* Begin PBXBuildFile section */ 2E0DD47B57B994A104B25EED /* Pods_AFFiNE.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF48636D7DB5BEE00770FD9A /* Pods_AFFiNE.framework */; }; + 500D72F72E825064006E553F /* Intelligents in Frameworks */ = {isa = PBXBuildFile; productRef = 500D72F62E825064006E553F /* Intelligents */; }; 5027D4782E7C5FBD00ADD25A /* AffinePaywall in Frameworks */ = {isa = PBXBuildFile; productRef = 5027D4772E7C5FBD00ADD25A /* AffinePaywall */; }; 5027D47A2E7C5FC100ADD25A /* AffineResources in Frameworks */ = {isa = PBXBuildFile; productRef = 5027D4792E7C5FC100ADD25A /* AffineResources */; }; 5027D47C2E7C5FC400ADD25A /* AffineGraphQL in Frameworks */ = {isa = PBXBuildFile; productRef = 5027D47B2E7C5FC400ADD25A /* AffineGraphQL */; }; 5027D4802E7C611900ADD25A /* Tools.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5027D47F2E7C611900ADD25A /* Tools.swift */; }; 5075136A2D1924C600AD60C0 /* RootViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 507513692D1924C600AD60C0 /* RootViewController.swift */; }; - 50802D612D112F8700694021 /* Intelligents in Frameworks */ = {isa = PBXBuildFile; productRef = 50802D602D112F8700694021 /* Intelligents */; settings = {ATTRIBUTES = (Required, ); }; }; 50A285D72D112A5E000D5A6D /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 50A285D62D112A5E000D5A6D /* Localizable.xcstrings */; }; 50A285D82D112A5E000D5A6D /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 50A285D52D112A5E000D5A6D /* InfoPlist.xcstrings */; }; - 50E218302E0BE1A700EA4C6E /* Intelligents in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 50802D602D112F8700694021 /* Intelligents */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + 50A485DE2E840A8000F220CE /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 50A485DD2E840A8000F220CE /* StoreKit.framework */; }; 50FF428A2D2E757E0050AA83 /* ApplicationBridgedWindowScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50FF42892D2E757E0050AA83 /* ApplicationBridgedWindowScript.swift */; }; 50FF428C2D2E77CC0050AA83 /* AffineViewController+AIButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50FF428B2D2E77CC0050AA83 /* AffineViewController+AIButton.swift */; }; 9D52FC432D26CDBF00105D0A /* JSValueContainerExt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D52FC422D26CDB600105D0A /* JSValueContainerExt.swift */; }; @@ -42,7 +42,6 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( - 50E218302E0BE1A700EA4C6E /* Intelligents in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; @@ -59,6 +58,8 @@ 50802D5E2D112F7D00694021 /* Intelligents */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Intelligents; sourceTree = ""; }; 50A285D52D112A5E000D5A6D /* InfoPlist.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = InfoPlist.xcstrings; sourceTree = ""; }; 50A285D62D112A5E000D5A6D /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; }; + 50A485DD2E840A8000F220CE /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; }; + 50BBC54E2E840DBC0067C5E2 /* Products.storekit */ = {isa = PBXFileReference; lastKnownFileType = text; path = Products.storekit; sourceTree = ""; }; 50CECF1E2E7C1084004487AA /* AffineResources */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = AffineResources; sourceTree = ""; }; 50FF42892D2E757E0050AA83 /* ApplicationBridgedWindowScript.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationBridgedWindowScript.swift; sourceTree = ""; }; 50FF428B2D2E77CC0050AA83 /* AffineViewController+AIButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AffineViewController+AIButton.swift"; sourceTree = ""; }; @@ -87,6 +88,8 @@ /* Begin PBXFileSystemSynchronizedRootGroup section */ 9DAE85B72E7BAC3B00DB9F1D /* Plugins */ = { isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + ); path = Plugins; sourceTree = ""; }; @@ -98,10 +101,11 @@ buildActionMask = 2147483647; files = ( 5027D47A2E7C5FC100ADD25A /* AffineResources in Frameworks */, + 50A485DE2E840A8000F220CE /* StoreKit.framework in Frameworks */, 5027D4782E7C5FBD00ADD25A /* AffinePaywall in Frameworks */, 9DFCD1462D27D1D70028C92B /* libaffine_mobile_native.a in Frameworks */, 5027D47C2E7C5FC400ADD25A /* AffineGraphQL in Frameworks */, - 50802D612D112F8700694021 /* Intelligents in Frameworks */, + 500D72F72E825064006E553F /* Intelligents in Frameworks */, 2E0DD47B57B994A104B25EED /* Pods_AFFiNE.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -112,6 +116,7 @@ 27E2DDA53C4D2A4D1A88CE4A /* Frameworks */ = { isa = PBXGroup; children = ( + 50A485DD2E840A8000F220CE /* StoreKit.framework */, C4C97C6B2D03027900BC2AD1 /* libaffine_mobile_native.a */, 9DFCD1452D27D1D70028C92B /* libaffine_mobile_native.a */, BF48636D7DB5BEE00770FD9A /* Pods_AFFiNE.framework */, @@ -176,6 +181,7 @@ 50FF428B2D2E77CC0050AA83 /* AffineViewController+AIButton.swift */, 50FF42892D2E757E0050AA83 /* ApplicationBridgedWindowScript.swift */, 5027D47F2E7C611900ADD25A /* Tools.swift */, + 50BBC54E2E840DBC0067C5E2 /* Products.storekit */, 9D90BE1D2CCB9876006677DB /* Assets.xcassets */, 9D90BE1E2CCB9876006677DB /* capacitor.config.json */, 9D90BE1F2CCB9876006677DB /* config.xml */, @@ -234,7 +240,7 @@ attributes = { BuildIndependentTargetsInParallel = YES; LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 0920; + LastUpgradeCheck = 2600; TargetAttributes = { 504EC3031FED79650016851F = { CreatedOnToolsVersion = 9.2; @@ -303,13 +309,9 @@ ); inputFileListPaths = ( ); - inputPaths = ( - ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( ); - outputPaths = ( - ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-AFFiNE/Pods-AFFiNE-frameworks.sh\"\n"; @@ -373,6 +375,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; @@ -383,6 +386,7 @@ CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; @@ -390,8 +394,10 @@ CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; @@ -424,6 +430,7 @@ ONLY_ACTIVE_ARCH = YES; PROVISIONING_PROFILE_SPECIFIER = ""; SDKROOT = iphoneos; + STRING_CATALOG_GENERATE_SYMBOLS = YES; SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -434,6 +441,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; @@ -444,6 +452,7 @@ CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; @@ -451,8 +460,10 @@ CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; @@ -478,6 +489,7 @@ MTL_ENABLE_DEBUG_INFO = NO; PROVISIONING_PROFILE_SPECIFIER = ""; SDKROOT = iphoneos; + STRING_CATALOG_GENERATE_SYMBOLS = YES; SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; @@ -493,7 +505,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 12; - DEVELOPMENT_TEAM = 964G86XT2P; + DEVELOPMENT_TEAM = 73YMMDVT2M; INFOPLIST_FILE = App/Info.plist; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity"; IPHONEOS_DEPLOYMENT_TARGET = 16.5; @@ -529,7 +541,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 12; - DEVELOPMENT_TEAM = 964G86XT2P; + DEVELOPMENT_TEAM = 73YMMDVT2M; INFOPLIST_FILE = App/Info.plist; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity"; IPHONEOS_DEPLOYMENT_TARGET = 16.5; @@ -581,6 +593,10 @@ /* End XCConfigurationList section */ /* Begin XCSwiftPackageProductDependency section */ + 500D72F62E825064006E553F /* Intelligents */ = { + isa = XCSwiftPackageProductDependency; + productName = Intelligents; + }; 5027D4772E7C5FBD00ADD25A /* AffinePaywall */ = { isa = XCSwiftPackageProductDependency; productName = AffinePaywall; @@ -593,10 +609,6 @@ isa = XCSwiftPackageProductDependency; productName = AffineGraphQL; }; - 50802D602D112F8700694021 /* Intelligents */ = { - isa = XCSwiftPackageProductDependency; - productName = Intelligents; - }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 504EC2FC1FED79650016851F /* Project object */; diff --git a/packages/frontend/apps/ios/App/App.xcodeproj/xcshareddata/xcschemes/AFFiNE.xcscheme b/packages/frontend/apps/ios/App/App.xcodeproj/xcshareddata/xcschemes/AFFiNE.xcscheme index dec463b303..eb1fc1a2a2 100644 --- a/packages/frontend/apps/ios/App/App.xcodeproj/xcshareddata/xcschemes/AFFiNE.xcscheme +++ b/packages/frontend/apps/ios/App/App.xcodeproj/xcshareddata/xcschemes/AFFiNE.xcscheme @@ -1,6 +1,6 @@ + + : View { + var axes: Axis.Set = [.vertical] + var showsIndicators = true + @Binding var offset: CGPoint + @ViewBuilder var content: () -> Content + + private let coordinateSpaceName = UUID() + + var body: some View { + ScrollView(axes, showsIndicators: showsIndicators) { + PositionObservingView( + coordinateSpace: .named(coordinateSpaceName), + position: Binding( + get: { offset }, + set: { newOffset in + offset = CGPoint( + x: -newOffset.x, + y: -newOffset.y + ) + } + ), + content: content + ) + } + .coordinateSpace(name: coordinateSpaceName) + } +} diff --git a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Components/AI/PositionObservingView.swift b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Components/AI/PositionObservingView.swift new file mode 100644 index 0000000000..dd1ebed921 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Components/AI/PositionObservingView.swift @@ -0,0 +1,37 @@ +// +// PositionObservingView.swift +// AffinePaywall +// +// Created by qaq on 9/23/25. +// + +import SwiftUI + +struct PositionObservingView: View { + var coordinateSpace: CoordinateSpace + @Binding var position: CGPoint + @ViewBuilder var content: () -> Content + + var body: some View { + content() + .background(GeometryReader { geometry in + Color.clear.preference( + key: PreferenceKey.self, + value: geometry.frame(in: coordinateSpace).origin + ) + }) + .onPreferenceChange(PreferenceKey.self) { position in + self.position = position + } + } +} + +private extension PositionObservingView { + struct PreferenceKey: SwiftUI.PreferenceKey { + static var defaultValue: CGPoint { .zero } + + static func reduce(value _: inout CGPoint, nextValue _: () -> CGPoint) { + // No-op + } + } +} diff --git a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Components/AI/SKUnitIntelligentDetailView.swift b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Components/AI/SKUnitIntelligentDetailView.swift index b6f0784684..82e0aa9762 100644 --- a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Components/AI/SKUnitIntelligentDetailView.swift +++ b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Components/AI/SKUnitIntelligentDetailView.swift @@ -10,11 +10,14 @@ import SwiftUI struct SKUnitIntelligentDetailView: View { @StateObject var viewModel: ViewModel + + @State var detailIndexInSwitching: UUID? = nil @State var detailIndex: Int = 0 { didSet { lastInteractionDate = Date() } } @State var lastInteractionDate: Date = .init() + @State var scrollOffset: CGPoint = .zero let timer = Timer .publish(every: 5, on: .main, in: .common) @@ -28,15 +31,11 @@ struct SKUnitIntelligentDetailView: View { let height = r.size.height let width = r.size.width ScrollViewReader { scrollView in - ScrollView(.horizontal, showsIndicators: false) { - GeometryReader { geometry in - Color.clear - .preference( - key: ViewOffsetKey.self, - value: geometry.frame(in: .named("scrollView")).origin - ) - } - .frame(width: 0, height: 0) + OffsetObservingScrollView( + axes: .horizontal, + showsIndicators: false, + offset: $scrollOffset + ) { HStack(spacing: 0) { ForEach(0 ..< Self.features.count, id: \.self) { featureIndex in let feature = Self.features[featureIndex] @@ -47,13 +46,6 @@ struct SKUnitIntelligentDetailView: View { } } } - .coordinateSpace(name: "scrollView") - .onPreferenceChange(ViewOffsetKey.self) { newValue in - let page = Int(round(-newValue.x / width)) - guard page != detailIndex else { return } - guard page >= 0, page < Self.features.count else { return } - detailIndex = page - } .frame(height: height) .onChange(of: detailIndex) { newValue in withAnimation(.spring) { @@ -61,6 +53,15 @@ struct SKUnitIntelligentDetailView: View { } } } + .onChange(of: scrollOffset) { _ in + guard detailIndexInSwitching == nil else { return } + guard width > 0 else { return } + let offset = scrollOffset.x + let newIndex = Int((offset + width / 2) / width) + if newIndex != detailIndex, + (0 ..< Self.features.count).contains(newIndex) + { detailIndex = newIndex } + } } PageDotsView( @@ -68,6 +69,12 @@ struct SKUnitIntelligentDetailView: View { total: Self.features.count ) { index in detailIndex = index + let token = UUID() + detailIndexInSwitching = token + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + guard detailIndexInSwitching == token else { return } + detailIndexInSwitching = nil + } } } .onReceive(timer) { _ in diff --git a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Components/Believer/SKUnitBelieverDetailView.swift b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Components/Believer/SKUnitBelieverDetailView.swift deleted file mode 100644 index a16a417743..0000000000 --- a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Components/Believer/SKUnitBelieverDetailView.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// SKUnitBelieverDetailView.swift -// AffinePaywall -// -// Created by qaq on 9/18/25. -// - -import SwiftUI - -struct SKUnitBelieverDetailView: View { - @StateObject var viewModel: ViewModel - - let features: [Feature] = [ - .init("Everything in AFFiNE Pro"), - .init("Life-time Personal usage"), - .init("1TB Cloud Storage"), - ] - - var body: some View { - VStack(spacing: 24) { - HeadlineView(viewModel: viewModel) - Image("BELIVER_ICON", bundle: .module) - .resizable() - .aspectRatio(contentMode: .fit) - ForEach(features.indices, id: \.self) { index in - let feature = features[index] - ProFeatureRowView(feature: feature, index: index) - } - } - } -} - -#Preview { - SKUnitBelieverDetailView(viewModel: .vmPreviewForBeliever) - .padding() -} diff --git a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Components/Components/CategorySelectionView.swift b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Components/Components/CategorySelectionView.swift index 957b6078be..7610284252 100644 --- a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Components/Components/CategorySelectionView.swift +++ b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Components/Components/CategorySelectionView.swift @@ -61,7 +61,6 @@ struct CategorySelectionView: View { return VStack(alignment: .leading, spacing: 12) { CategorySelectionView(selectedTab: .pro, onSelect: { _ in }) CategorySelectionView(selectedTab: .ai, onSelect: { _ in }) - CategorySelectionView(selectedTab: .believer, onSelect: { _ in }) Divider() PreviewWrapper() } diff --git a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Components/Components/HeadlineView.swift b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Components/Components/HeadlineView.swift index a8939712ad..fbf9e6f137 100644 --- a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Components/Components/HeadlineView.swift +++ b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Components/Components/HeadlineView.swift @@ -13,7 +13,7 @@ struct HeadlineView: View { var body: some View { VStack(spacing: 8) { Text(viewModel.selectedUnit.primaryText) - .font(.system(size: 24, weight: .heavy)) + .font(.system(size: 24, weight: .semibold)) .contentTransition(.numericText()) .animation(.spring.speed(2), value: viewModel.category) .padding(.top, 8) diff --git a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Components/Components/PricingOptionView.swift b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Components/Components/PackageOptionView.swift similarity index 95% rename from packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Components/Components/PricingOptionView.swift rename to packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Components/Components/PackageOptionView.swift index a00058e9b0..d3a89c7e75 100644 --- a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Components/Components/PricingOptionView.swift +++ b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Components/Components/PackageOptionView.swift @@ -1,5 +1,5 @@ // -// PricingOptionView.swift +// PackageOptionView.swift // AffinePaywall // // Created by qaq on 9/18/25. @@ -8,7 +8,7 @@ import AffineResources import SwiftUI -struct PricingOptionView: View { +struct PackageOptionView: View { let price: String let description: String var badge: String @@ -102,12 +102,12 @@ struct PricingOptionView: View { #Preview { VStack(spacing: 16) { HStack(spacing: 16) { - PricingOptionView( + PackageOptionView( price: "$7.99", description: "Monthly", isSelected: false ) {} - PricingOptionView( + PackageOptionView( price: "$6.75", description: "Annually", badge: "Save 15%", @@ -115,13 +115,13 @@ struct PricingOptionView: View { ) {} } HStack(spacing: 16) { - PricingOptionView( + PackageOptionView( price: "$114514", description: "Monthly", badge: "Most Popular", isSelected: true ) {} - PricingOptionView( + PackageOptionView( price: "$6.75", description: "Annually", badge: "Save 15%", diff --git a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Components/Components/PurchaseFooterView.swift b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Components/Components/PurchaseFooterView.swift index b6e010f9d7..6ce814039c 100644 --- a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Components/Components/PurchaseFooterView.swift +++ b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Components/Components/PurchaseFooterView.swift @@ -11,36 +11,67 @@ import SwiftUI struct PurchaseFooterView: View { @StateObject var viewModel: ViewModel + var isPurchased: Bool { + let package = viewModel.selectePackageOption + return viewModel.purchasedItems.contains(package.productIdentifier) + } + var body: some View { VStack(spacing: 16) { - if viewModel.availablePricingOptions.count > 1 { + if viewModel.availablePackageOptions.count > 1 { HStack(spacing: 8) { - ForEach(viewModel.availablePricingOptions) { option in - PricingOptionView( + ForEach(viewModel.availablePackageOptions) { option in + PackageOptionView( price: option.price, description: option.description, badge: option.badge ?? "", - isSelected: option.id == viewModel.selectedPricingIdentifier + isSelected: option.id == viewModel.selectedPackageIdentifier ) { - viewModel.select(pricingOption: option) + viewModel.select(packageOption: option) } } } + .disabled(isPurchased) } - TheGiveMeMoneyButtonView( - primaryTitle: viewModel.selectedPricingOption.primaryTitle, - secondaryTitle: viewModel.selectedPricingOption.secondaryTitle, - callback: viewModel.purchase - ) + if viewModel.updating { + TheGiveMeMoneyButtonView( + primaryTitle: "Height Placeholder", + secondaryTitle: "", + isPurchased: false + ) {} + .hidden() + .background(AffineColors.buttonPrimary.color) + .clipShape(RoundedRectangle(cornerRadius: 8)) + .overlay { + ProgressView() + .progressViewStyle(.circular) + } + .transition(.opacity) + } else { + TheGiveMeMoneyButtonView( + primaryTitle: viewModel.selectePackageOption.primaryTitle, + secondaryTitle: viewModel.selectePackageOption.secondaryTitle, + isPurchased: isPurchased, + callback: viewModel.purchase + ) + .transition(.opacity) + } Button(action: viewModel.restore) { - Text("Restore Purchase") + if isPurchased { + Text("Already Purchased") + } else { + Text("Restore Purchase") + } } .font(.system(size: 12)) .buttonStyle(.plain) .foregroundStyle(AffineColors.textSecondary.color) + .opacity(viewModel.products.isEmpty ? 0 : 1) + .disabled(isPurchased) } + .animation(.spring, value: viewModel.updating) } } diff --git a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Components/Components/TheGiveMeMoneyButtonView.swift b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Components/Components/TheGiveMeMoneyButtonView.swift index b92b78cfbc..b56fa459a2 100644 --- a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Components/Components/TheGiveMeMoneyButtonView.swift +++ b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Components/Components/TheGiveMeMoneyButtonView.swift @@ -11,43 +11,55 @@ import SwiftUI struct TheGiveMeMoneyButtonView: View { let primaryTitle: String let secondaryTitle: String + let isPurchased: Bool let callback: () -> Void init( primaryTitle: String = "", secondaryTitle: String = "", + isPurchased: Bool, callback: @escaping () -> Void = {} ) { self.primaryTitle = primaryTitle self.secondaryTitle = secondaryTitle + self.isPurchased = isPurchased self.callback = callback } var body: some View { Button { callback() } label: { - HStack(spacing: 4) { - if !primaryTitle.isEmpty { - Text(primaryTitle) - .bold() - .font(.system(size: 16)) - .contentTransition(.numericText()) - } - if !secondaryTitle.isEmpty { - Text("(\(secondaryTitle))") - .font(.system(size: 12)) - .opacity(0.8) - .contentTransition(.numericText()) + if isPurchased { + Image(systemName: "checkmark") + .foregroundColor(AffineColors.layerPureWhite.color) + .font(.system(size: 16, weight: .bold)) + .padding(12) + } else { + HStack(spacing: 4) { + if !primaryTitle.isEmpty { + Text(primaryTitle) + .bold() + .font(.system(size: 16)) + .contentTransition(.numericText()) + } + if !secondaryTitle.isEmpty { + Text("(\(secondaryTitle))") + .font(.system(size: 12)) + .opacity(0.8) + .contentTransition(.numericText()) + } } + .foregroundColor(AffineColors.layerPureWhite.color) + .padding(12) } - .foregroundColor(AffineColors.layerPureWhite.color) - .padding(12) } .animation(.spring, value: primaryTitle) .animation(.spring, value: secondaryTitle) .buttonStyle(.plain) .frame(maxWidth: .infinity) + .frame(minHeight: 32) .background(AffineColors.buttonPrimary.color) .clipShape(RoundedRectangle(cornerRadius: 8)) + .disabled(isPurchased) } } @@ -57,19 +69,28 @@ struct TheGiveMeMoneyButtonView: View { VStack(spacing: 16) { TheGiveMeMoneyButtonView( primaryTitle: "Upgrade for $6.75 per month", - secondaryTitle: "" + secondaryTitle: "", + isPurchased: false ) TheGiveMeMoneyButtonView( primaryTitle: "Upgrade for $10 per month", - secondaryTitle: "" + secondaryTitle: "", + isPurchased: false ) TheGiveMeMoneyButtonView( primaryTitle: "$8.9 per month", - secondaryTitle: "billed annually" + secondaryTitle: "billed annually", + isPurchased: false ) TheGiveMeMoneyButtonView( primaryTitle: "Upgrade for $499", - secondaryTitle: "" + secondaryTitle: "", + isPurchased: false + ) + TheGiveMeMoneyButtonView( + primaryTitle: "Upgrade for $499", + secondaryTitle: "", + isPurchased: true ) } .padding(32) diff --git a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Components/Pro/ProFeaturesCardView.swift b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Components/Pro/ProFeaturesCardView.swift deleted file mode 100644 index bf82bf3b70..0000000000 --- a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Components/Pro/ProFeaturesCardView.swift +++ /dev/null @@ -1,65 +0,0 @@ -// -// ProFeaturesCardView.swift -// AffinePaywall -// -// Created by qaq on 9/18/25. -// - -import AffineResources -import SwiftUI - -struct ProFeaturesCardView: View { - let features: [Feature] - let headerText: String - - let timer = Timer - .publish(every: 0.08, on: .main, in: .common) - .autoconnect() - @State var animationIndex: Int64 = 0 - - var body: some View { - VStack(alignment: .leading, spacing: 16) { - if !headerText.isEmpty { - Text(headerText) - .font(.system(size: 13)) - .foregroundColor(AffineColors.textSecondary.color) - .padding(.horizontal, 4) - } - - ForEach(Array(features.enumerated()), id: \.element.id) { index, feature in - ProFeatureRowView(feature: feature, index: index) - .opacity(index < animationIndex ? 1 : 0) - } - } - .animation(.spring.speed(2), value: animationIndex) - .onChange(of: features) { _ in animationIndex = 0 } - .onReceive(timer) { _ in animationIndex += 1 } - .clipped() - .padding(16) - .background(AffineColors.layerBackgroundPrimary.color) - .cornerRadius(16) - .shadow(color: AffineColors.layerBorder.color.opacity(0.08), radius: 8, y: 2) - .animation(.spring.speed(2), value: features) - } -} - -#Preview("Pro") { - ProFeaturesCardView(features: SKUnitSubcategoryProPlan.default.features, headerText: SKUnitSubcategoryProPlan.default.headerText) - .padding() - .background(Color.gray.ignoresSafeArea()) -} - -#Preview("Pro team") { - ProFeaturesCardView( - features: SKUnitSubcategoryProPlan.team.features, - headerText: SKUnitSubcategoryProPlan.team.headerText - ) - .padding() - .background(Color.gray.ignoresSafeArea()) -} - -#Preview("Self Hosted") { - ProFeaturesCardView(features: SKUnitSubcategoryProPlan.selfHost.features, headerText: SKUnitSubcategoryProPlan.selfHost.headerText) - .padding() - .background(Color.gray.ignoresSafeArea()) -} diff --git a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Components/Pro/SKUnitProDetailView.swift b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Components/Pro/SKUnitProDetailView.swift index 7a9e949ca3..5b976fc57c 100644 --- a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Components/Pro/SKUnitProDetailView.swift +++ b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Components/Pro/SKUnitProDetailView.swift @@ -11,35 +11,82 @@ import SwiftUI struct SKUnitProDetailView: View { @StateObject var viewModel: ViewModel - @State var selection: SKUnitSubcategoryProPlan = .default + @State var selection: SKUnitSubcategoryProPlan + @State var headerText: String + @State var features: [Feature] + @State var animationIndex: Int64 = 0 + + let timer = Timer + .publish(every: 0.075, on: .main, in: .common) + .autoconnect() + + init(viewModel: ViewModel) { + _viewModel = .init(wrappedValue: viewModel) + let item = SKUnitSubcategoryProPlan.default + _selection = .init(initialValue: item) + _headerText = .init(initialValue: item.headerText) + _features = .init(initialValue: item.features) + } var body: some View { VStack(spacing: 24) { - Picker("Plan", selection: $selection) { - ForEach(SKUnitSubcategoryProPlan.allCases) { plan in - Text(plan.title).tag(plan) + if SKUnitSubcategoryProPlan.allCases.count > 1 { + Picker("Plan", selection: $selection) { + ForEach(SKUnitSubcategoryProPlan.allCases) { plan in + Text(plan.title).tag(plan) + } + } + .pickerStyle(.segmented) + .onChange(of: selection) { _ in + viewModel.select(subcategory: selection) } - } - .pickerStyle(.segmented) - .onChange(of: selection) { _ in - viewModel.select(subcategory: selection) } HeadlineView(viewModel: viewModel) ScrollView { - ProFeaturesCardView( - features: selection.features, - headerText: selection.headerText - ) + VStack(alignment: .leading, spacing: 16) { + if !headerText.isEmpty { + Text(headerText) + .font(.system(size: 13)) + .foregroundColor(AffineColors.textSecondary.color) + .contentTransition(.numericText()) + .transition(.opacity) + .padding(.horizontal, 4) + } + + ForEach(Array(features.enumerated()), id: \.element.id) { index, feature in + ProFeatureRowView(feature: feature, index: index) + .opacity(index < animationIndex ? 1 : 0) + } + } + .clipped() + .padding(16) + .background(AffineColors.layerBackgroundPrimary.color) + .cornerRadius(16) + .shadow(color: AffineColors.layerBorder.color.opacity(0.08), radius: 8, y: 2) .padding(16) } .padding(-16) + .animation(.spring.speed(2), value: animationIndex) + .onReceive(timer) { _ in animationIndex += 1 } .frame( maxWidth: .infinity, maxHeight: .infinity, alignment: .top ) + .onChange(of: selection) { _ in updateSelectionContents() } + } + } + + func updateSelectionContents() { + let headerText = selection.headerText + if self.headerText != headerText { + self.headerText = headerText + } + let features = selection.features + if self.features != features { + self.features = features } } } diff --git a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/Feature.swift b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/Feature.swift index a020af78f5..307eb0e2d7 100644 --- a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/Feature.swift +++ b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/Feature.swift @@ -7,7 +7,7 @@ import Foundation -struct Feature: Identifiable, Equatable { +struct Feature: Identifiable, Equatable, Hashable { var id = UUID() var text: String var isHighlighted: Bool // For text like "Everything in AFFINE Pro" diff --git a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/SKUnit+AI.swift b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/SKUnit+AI.swift index b812bbf835..3825234942 100644 --- a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/SKUnit+AI.swift +++ b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/SKUnit+AI.swift @@ -13,13 +13,15 @@ extension SKUnit { category: SKUnitCategory.ai, primaryText: "AFFINE AI", secondaryText: "A true multimodal AI copilot.", - pricing: [ - SKUnitPricingOption( + package: [ + SKUnitPackageOption( price: "$8.9 per month", description: "", isDefaultSelected: true, primaryTitle: "$8.9 per month", - secondaryTitle: "billed annually" + secondaryTitle: "billed annually", + productIdentifier: "app.affine.pro.ai.Annual", + revenueCatIdentifier: "app.affine.pro.ai.Annual" ), ] ), diff --git a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/SKUnit+Believer.swift b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/SKUnit+Believer.swift deleted file mode 100644 index 92472b3bf4..0000000000 --- a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/SKUnit+Believer.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// SKUnit+Believer.swift -// AffinePaywall -// -// Created by qaq on 9/18/25. -// - -import Foundation - -extension SKUnit { - static let believerUnits: [SKUnit] = [ - SKUnit( - category: SKUnitCategory.believer, - primaryText: "Believer Plan", - secondaryText: "AFFINE's Everything", - pricing: [ - SKUnitPricingOption( - price: "$499", - description: "", - isDefaultSelected: true, - primaryTitle: "Upgrade for $499", - secondaryTitle: "" - ), - ] - ), - ] -} diff --git a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/SKUnit+Pro.swift b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/SKUnit+Pro.swift index f2355972f8..3488882cf6 100644 --- a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/SKUnit+Pro.swift +++ b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/SKUnit+Pro.swift @@ -14,67 +14,25 @@ extension SKUnit { subcategory: SKUnitSubcategoryProPlan.default, primaryText: "Pro", secondaryText: "For family and small teams.", - pricing: [ - SKUnitPricingOption( + package: [ + SKUnitPackageOption( price: "$7.99", description: "Monthly", isDefaultSelected: false, primaryTitle: "Upgrade for $7.99/month", - secondaryTitle: "" + secondaryTitle: "", + productIdentifier: "app.affine.pro.Monthly", + revenueCatIdentifier: "app.affine.pro.Monthly" ), - SKUnitPricingOption( + SKUnitPackageOption( price: "$6.75", description: "Annual", badge: "Save 15%", isDefaultSelected: true, primaryTitle: "Upgrade for $6.75/month", - secondaryTitle: "" - ), - ] - ), - SKUnit( - category: SKUnitCategory.pro, - subcategory: SKUnitSubcategoryProPlan.team, - primaryText: "Pro team", - secondaryText: "Best for scalable teams.", - pricing: [ - SKUnitPricingOption( - price: "$12", - description: "Per seat monthly", - isDefaultSelected: false, - primaryTitle: "Upgrade for $12/month", - secondaryTitle: "" - ), - SKUnitPricingOption( - price: "$10", - description: "Annual", - badge: "Save 15%", - isDefaultSelected: true, - primaryTitle: "Upgrade for $10/month", - secondaryTitle: "" - ), - ] - ), - SKUnit( - category: SKUnitCategory.pro, - subcategory: SKUnitSubcategoryProPlan.selfHost, - primaryText: "Self Hosted team", - secondaryText: "Best for scalable teams.", - pricing: [ - SKUnitPricingOption( - price: "$12", - description: "Per seat monthly", - isDefaultSelected: false, - primaryTitle: "Upgrade for $12/month", - secondaryTitle: "" - ), - SKUnitPricingOption( - price: "$10", - description: "Annual", - badge: "Save 15%", - isDefaultSelected: true, - primaryTitle: "Upgrade for $10/month", - secondaryTitle: "" + secondaryTitle: "", + productIdentifier: "app.affine.pro.Annual", + revenueCatIdentifier: "app.affine.pro.Annual" ), ] ), diff --git a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/SKUnit.swift b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/SKUnit.swift index 2ef8177ae0..9020c7b3cf 100644 --- a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/SKUnit.swift +++ b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/SKUnit.swift @@ -13,20 +13,20 @@ struct SKUnit: Identifiable, Sendable { let subcategory: any SKUnitSubcategorizable let primaryText: String let secondaryText: String - let pricing: [SKUnitPricingOption] + let package: [SKUnitPackageOption] init( category: SKUnitCategory, subcategory: (any SKUnitSubcategorizable) = SKUnitSingleSubcategory.single, primaryText: String, secondaryText: String, - pricing: [SKUnitPricingOption] + package: [SKUnitPackageOption] ) { self.category = category self.subcategory = subcategory self.primaryText = primaryText self.secondaryText = secondaryText - self.pricing = pricing + self.package = package } } @@ -34,7 +34,6 @@ extension SKUnit { static let allUnits: [SKUnit] = [ proUnits, aiUnits, - believerUnits, ].flatMap(\.self) static func units(for category: SKUnitCategory) -> [SKUnit] { diff --git a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/SKUnitCategory.swift b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/SKUnitCategory.swift index 40ea3d4ffb..091c16da1a 100644 --- a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/SKUnitCategory.swift +++ b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/SKUnitCategory.swift @@ -12,7 +12,6 @@ enum SKUnitCategory: Int, CaseIterable, Equatable, Identifiable { case pro case ai - case believer } extension SKUnitCategory { @@ -20,7 +19,6 @@ extension SKUnitCategory { switch self { case .pro: "AFFINE.Pro" case .ai: "AI" - case .believer: "Believer" } } } diff --git a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/SKUnitPricingOption.swift b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/SKUnitPackageOption.swift similarity index 62% rename from packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/SKUnitPricingOption.swift rename to packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/SKUnitPackageOption.swift index 8a790911e3..b3dbfc272d 100644 --- a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/SKUnitPricingOption.swift +++ b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/SKUnitPackageOption.swift @@ -1,5 +1,5 @@ // -// SKUnitPricingOption.swift +// SKUnitPackageOption.swift // AffinePaywall // // Created by qaq on 9/18/25. @@ -7,10 +7,10 @@ import Foundation -struct SKUnitPricingOption: Identifiable, Equatable { +struct SKUnitPackageOption: Identifiable, Equatable { var id: UUID - // pricing selection button + // package selection button var price: String var description: String var badge: String? @@ -20,6 +20,10 @@ struct SKUnitPricingOption: Identifiable, Equatable { var primaryTitle: String var secondaryTitle: String + // product identifiers + var productIdentifier: String + var revenueCatIdentifier: String + init( id: UUID = UUID(), price: String, @@ -27,7 +31,9 @@ struct SKUnitPricingOption: Identifiable, Equatable { badge: String? = nil, isDefaultSelected: Bool = false, primaryTitle: String, - secondaryTitle: String + secondaryTitle: String, + productIdentifier: String, + revenueCatIdentifier: String ) { self.id = id self.price = price @@ -36,5 +42,7 @@ struct SKUnitPricingOption: Identifiable, Equatable { self.isDefaultSelected = isDefaultSelected self.primaryTitle = primaryTitle self.secondaryTitle = secondaryTitle + self.productIdentifier = productIdentifier + self.revenueCatIdentifier = revenueCatIdentifier } } diff --git a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/SKUnitSubcategoryProPlan.swift b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/SKUnitSubcategoryProPlan.swift index 78ae59e36d..082ae14d9c 100644 --- a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/SKUnitSubcategoryProPlan.swift +++ b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/SKUnitSubcategoryProPlan.swift @@ -9,14 +9,10 @@ import Foundation enum SKUnitSubcategoryProPlan: String, SKUnitSubcategorizable { case `default` - case team - case selfHost var title: String { switch self { case .default: "Pro" - case .team: "Pro team" - case .selfHost: "Self Hosted" } } @@ -24,10 +20,6 @@ enum SKUnitSubcategoryProPlan: String, SKUnitSubcategorizable { switch self { case .default: "For family and small teams." - case .team: - "Best for scalable teams." - case .selfHost: - "Best for scalable teams." } } } @@ -37,10 +29,6 @@ extension SKUnitSubcategoryProPlan { switch self { case .default: "Include in Pro" - case .team: - "Include in Team Workspace" - case .selfHost: - "Both in Teams & Enterprise" } } @@ -56,24 +44,6 @@ extension SKUnitSubcategoryProPlan { Feature("Community Support"), Feature("Real-time Syncing & Collaboration for more people"), ] - case .team: - [ - Feature("Everything in AFFINE Pro", isHighlighted: true), - Feature("100 GB initial storage + 20 GB per seat"), - Feature("500 MB of maximum file size"), - Feature("Unlimited team members (10+ seats)"), - Feature("Multiple admin roles"), - Feature("Priority customer support"), - ] - case .selfHost: - [ - Feature("Everything in Self Hosted FOSS"), - Feature("100 GB initial storage + 20 GB per seat"), - Feature("500 MB of maximum file size"), - Feature("Unlimited team members (10+ seats)"), - Feature("Multiple admin roles"), - Feature("Priority customer support"), - ] } } } diff --git a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/Store.swift b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/Store.swift new file mode 100644 index 0000000000..b9b89f26e5 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/Store.swift @@ -0,0 +1,59 @@ +// +// Store.swift +// AffinePaywall +// +// Created by qaq on 9/24/25. +// + +import StoreKit + +let store = Store.shared + +final nonisolated class Store: ObservableObject, Sendable { + static let shared = Store() + + private init() {} + + func fetchAppStoreContents() async throws { + try await AppStore.sync() + } + + func fetchProducts() async throws -> [Product] { + let identifiers = SKUnit.allUnits + .flatMap(\.package) + .map(\.productIdentifier) + print("fetching products for identifiers: \(identifiers)") + let products = try await Product.products( + for: identifiers.map { .init($0) } + ) + if products.count != identifiers.count { + throw NSError(domain: "AffinePaywall", code: -1, userInfo: [ + NSLocalizedDescriptionKey: String(localized: "Failed to fetch all products from App Store."), + ]) + } + return products + } + + func fetchEntitlements() async throws -> Set { + var purchasedItems: Set = [] + + for await result in Transaction.currentEntitlements { + if case let .verified(transaction) = result { + guard transaction.revocationDate == nil else { continue } + + switch transaction.productType { + case .nonConsumable, .consumable: + purchasedItems.insert(transaction.productID) + case .autoRenewable, .nonRenewable: + if let status = await transaction.subscriptionStatus, + status.state == .subscribed + { purchasedItems.insert(transaction.productID) } + default: + break + } + } + } + + return purchasedItems + } +} diff --git a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/ViewModel+Action.swift b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/ViewModel+Action.swift index 377ae6808d..ff2f843309 100644 --- a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/ViewModel+Action.swift +++ b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/ViewModel+Action.swift @@ -6,26 +6,122 @@ // import Foundation +import UIKit extension ViewModel { func purchase() { let unit = selectedUnit - let option = selectedPricingOption + let option = selectePackageOption + assert(!updating) + guard !updating else { return } print(#function, unit, option) + + Task.detached { + await MainActor.run { self.updating = true } + var shouldDismiss = false + + let product = await self.products.first { + $0.id == option.productIdentifier + } + + if let product { + let result = try await product.purchase() + switch result { + case .pending: + break + case let .success(transaction): + print("purchase success", transaction) + shouldDismiss = true + case .userCancelled: + break + @unknown default: + assertionFailure() + } + } else { assertionFailure() } // should never happen + + await MainActor.run { + self.updating = false + if shouldDismiss { self.dismiss() } + } + } } func restore() { let unit = selectedUnit - let option = selectedPricingOption + let option = selectePackageOption + assert(!updating) + guard !updating else { return } print(#function, unit, option) + + updateAppStoreStatus(initial: false) } func dismiss() { - let unit = selectedUnit - let option = selectedPricingOption - - print(#function, unit, option) + print(#function) + associatedController?.dismiss(animated: true) + } +} + +nonisolated extension ViewModel { + func updateAppStoreStatusExecute(initial: Bool) async { + guard await !updating else { return } + guard let controller = await associatedController else { return } + await MainActor.run { self.updating = true } + + do { + // before we continue, sync any changes from App Store + // this will ask user to sign in if needed + do { + try await store.fetchAppStoreContents() + } catch { + // ignore user's cancellation on restore, not a huge deal + print("updateAppStoreItems error:", error) + } + + // now we fetch records from app store + let products = try await store.fetchProducts() + await MainActor.run { self.products = products } + + // fetch purchased items if signed in + do { + let purchase = try await store.fetchEntitlements() + await MainActor.run { self.purchasedItems = purchase } + } catch { + print("fetchEntitlements error:", error) + if !initial { throw error } + } + + // select the package under purchased items if any + let availablePackages = await availablePackageOptions + let purchase = await purchasedItems + let purchasedPackages = availablePackages.filter { + purchase.contains($0.productIdentifier) + } + assert(purchasedPackages.count <= 1) + if let firstPurchased = purchasedPackages.first { + await MainActor.run { + self.select(packageOption: firstPurchased) + } + } + } catch { + await MainActor.run { + let alert = UIAlertController( + title: String(localized: "Error"), + message: error.localizedDescription, + preferredStyle: .alert + ) + alert.addAction( + UIAlertAction( + title: String(localized: "OK"), + style: .default + ) { [self] _ in dismiss() } + ) + controller.present(alert, animated: true) + } + } + + await MainActor.run { self.updating = false } } } diff --git a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/ViewModel+Preview.swift b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/ViewModel+Preview.swift index 9b0a730c91..79286fefa6 100644 --- a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/ViewModel+Preview.swift +++ b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/ViewModel+Preview.swift @@ -20,10 +20,4 @@ extension ViewModel { vm.select(category: .ai) return vm }() - - static let vmPreviewForBeliever: ViewModel = { - let vm = ViewModel() - vm.select(category: .believer) - return vm - }() } diff --git a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/ViewModel.swift b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/ViewModel.swift index aa2a52e675..8dd52c2fd4 100644 --- a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/ViewModel.swift +++ b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/ViewModel.swift @@ -5,6 +5,7 @@ // Created by qaq on 9/18/25. // +import StoreKit import SwiftUI @MainActor @@ -15,12 +16,30 @@ class ViewModel: ObservableObject { @Published private(set) var category: SKUnitCategory = .pro @Published private(set) var subcategory: any SKUnitSubcategorizable = SKUnitSubcategoryProPlan.default - @Published private(set) var selectedPricingIdentifier: UUID = SKUnit.unit( + @Published private(set) var selectedPackageIdentifier: UUID = SKUnit.unit( for: .pro, subcategory: SKUnitSubcategoryProPlan.default - )!.pricing.first { $0.isDefaultSelected }!.id + )!.package.first { $0.isDefaultSelected }!.id - init() {} + @Published var updating = false + @Published var products: [Product] = [] + @Published var purchasedItems: Set = [] + + private(set) weak var associatedController: UIViewController? + + init() { + updateAppStoreStatus(initial: true) + } + + func updateAppStoreStatus(initial: Bool) { + Task.detached { + await self.updateAppStoreStatusExecute(initial: initial) + } + } + + func bind(controller: UIViewController) { + associatedController = controller + } func select(category: SKUnitCategory) { self.category = category @@ -30,7 +49,7 @@ class ViewModel: ObservableObject { if !subcategoryExists { subcategory = units.first!.subcategory } - _ = selectedPricingOption // ensure selectedPricingIdentifier is valid + _ = selectePackageOption // ensure selectePackageOption is valid } func select(subcategory: any SKUnitSubcategorizable) { @@ -45,20 +64,20 @@ class ViewModel: ObservableObject { } else { self.subcategory = subcategory } - _ = selectedPricingOption // ensure selectedPricingIdentifier is valid + _ = selectePackageOption // ensure selectePackageOption is valid } - func select(pricingOption option: SKUnitPricingOption) { - selectedPricingIdentifier = option.id + func select(packageOption option: SKUnitPackageOption) { + selectedPackageIdentifier = option.id let unit = availableUnits .first { unit in - unit.pricing.contains { $0.id == option.id } + unit.package.contains { $0.id == option.id } }! category = unit.category subcategory = unit.subcategory - _ = selectedPricingOption // ensure selectedPricingIdentifier is valid + _ = selectePackageOption // ensure selectePackageOption is valid } } @@ -79,21 +98,21 @@ extension ViewModel { return item } - var selectedPricingOption: SKUnitPricingOption { - let item = selectedUnit.pricing - .first { $0.id == selectedPricingIdentifier } + var selectePackageOption: SKUnitPackageOption { + let item = selectedUnit.package + .first { $0.id == selectedPackageIdentifier } if let item { return item } - let defaultItem = selectedUnit.pricing.first { $0.isDefaultSelected } + let defaultItem = selectedUnit.package.first { $0.isDefaultSelected } if let defaultItem { - selectedPricingIdentifier = defaultItem.id + selectedPackageIdentifier = defaultItem.id return defaultItem } - let lastItem = selectedUnit.pricing.last! - selectedPricingIdentifier = lastItem.id + let lastItem = selectedUnit.package.last! + selectedPackageIdentifier = lastItem.id return lastItem } - var availablePricingOptions: [SKUnitPricingOption] { - selectedUnit.pricing + var availablePackageOptions: [SKUnitPackageOption] { + selectedUnit.package } } diff --git a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Page/AffinePaywallPageView.swift b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Page/AffinePaywallPageView.swift index 9ee871a659..f9a57431f6 100644 --- a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Page/AffinePaywallPageView.swift +++ b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Page/AffinePaywallPageView.swift @@ -13,6 +13,9 @@ struct AffinePaywallPageView: View { @Environment(\.dismiss) var dismiss + @State private var presentAnimation = false + @State private var showCloseButton = false + var body: some View { VStack(alignment: .leading, spacing: 16) { HStack { @@ -24,10 +27,17 @@ struct AffinePaywallPageView: View { Button { viewModel.dismiss() } label: { - Image(AffineIcons.close.rawValue) + AffineIcons.close.image + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 24, height: 24) + .foregroundStyle(.primary) } .buttonStyle(.plain) .foregroundColor(AffineColors.textSecondary.color) + .opacity(showCloseButton ? 1 : 0) + .disabled(!showCloseButton) + .animation(.spring, value: showCloseButton) } ZStack(alignment: .topLeading) { Spacer() @@ -45,11 +55,22 @@ struct AffinePaywallPageView: View { .animation(.spring.speed(2), value: viewModel.category) PurchaseFooterView(viewModel: viewModel) - .animation(.spring.speed(2), value: viewModel.selectedPricingIdentifier) + .animation(.spring.speed(2), value: viewModel.selectedPackageIdentifier) } .padding() + .opacity(presentAnimation ? 1 : 0) + .scaleEffect(presentAnimation ? 1 : 0.95, anchor: .top) + .animation(.spring, value: presentAnimation) + .onAppear { + presentAnimation = true + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + showCloseButton = true + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) .background( AffineColors.layerBackgroundSecondary.color + .ignoresSafeArea() ) } @@ -60,8 +81,6 @@ struct AffinePaywallPageView: View { SKUnitProDetailView(viewModel: viewModel) case .ai: SKUnitIntelligentDetailView(viewModel: viewModel) - case .believer: - SKUnitBelieverDetailView(viewModel: viewModel) } } } diff --git a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Paywall.swift b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Paywall.swift index cd4b5b28a0..b0869aac4a 100644 --- a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Paywall.swift +++ b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Paywall.swift @@ -1,12 +1,12 @@ // -// File.swift +// Paywall.swift // AffinePaywall // // Created by qaq on 9/18/25. // -import UIKit import SwiftUI +import UIKit public enum Paywall { @MainActor @@ -15,13 +15,19 @@ public enum Paywall { type: String ) { let viewModel = ViewModel() - switch type { - // TODO: FIGURE OUT PAYWALL TYPES + switch type.lowercased() { + case "pro": + viewModel.select(category: .pro) + viewModel.select(subcategory: SKUnitSubcategoryProPlan.default) + case "ai": + viewModel.select(category: .ai) + viewModel.select(subcategory: SKUnitSingleSubcategory.single) default: break } let view = AffinePaywallPageView(viewModel: viewModel) let hostingController = UIHostingController(rootView: view) + viewModel.bind(controller: hostingController) hostingController.modalPresentationStyle = .overFullScreen hostingController.modalTransitionStyle = .coverVertical hostingController.preferredContentSize = CGSize(width: 555, height: 555) // for iPads diff --git a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Utils/ViewOffsetKey.swift b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Utils/ViewOffsetKey.swift deleted file mode 100644 index 9e803e3035..0000000000 --- a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Utils/ViewOffsetKey.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// ViewOffsetKey.swift -// AffinePaywall -// -// Created by qaq on 9/18/25. -// - -import SwiftUI - -@MainActor -struct ViewOffsetKey: @MainActor PreferenceKey { - static var defaultValue: CGPoint = .zero - static func reduce(value: inout CGPoint, nextValue: () -> CGPoint) { - value = nextValue() - } -} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Package.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Package.swift index 054c18b070..7b371cdf3b 100644 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Package.swift +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Package.swift @@ -10,16 +10,17 @@ let package = Package( .iOS(.v16), ], products: [ - .library(name: "Intelligents", type: .dynamic, targets: ["Intelligents"]), + // static linking is required or GraphQL (ApolloAPI) will crash + .library(name: "Intelligents", type: .static, targets: ["Intelligents"]), ], dependencies: [ .package(path: "../AffineGraphQL"), .package(path: "../AffineResources"), - .package(url: "https://github.com/apollographql/apollo-ios.git", from: "1.22.0"), - .package(url: "https://github.com/apple/swift-collections.git", from: "1.2.0"), + .package(url: "https://github.com/apollographql/apollo-ios.git", from: "1.23.0"), + .package(url: "https://github.com/apple/swift-collections.git", from: "1.2.1"), .package(url: "https://github.com/SnapKit/SnapKit.git", from: "5.7.1"), .package(url: "https://github.com/SwifterSwift/SwifterSwift.git", from: "6.0.0"), - .package(url: "https://github.com/Recouse/EventSource.git", from: "0.1.4"), + .package(url: "https://github.com/Recouse/EventSource.git", from: "0.1.5"), .package(url: "https://github.com/Lakr233/ListViewKit.git", from: "1.1.6"), .package(url: "https://github.com/Lakr233/MarkdownView.git", from: "3.4.2"), ], diff --git a/packages/frontend/apps/ios/App/Podfile.lock b/packages/frontend/apps/ios/App/Podfile.lock index ef43285496..87d06e4df2 100644 --- a/packages/frontend/apps/ios/App/Podfile.lock +++ b/packages/frontend/apps/ios/App/Podfile.lock @@ -45,13 +45,13 @@ EXTERNAL SOURCES: :path: "../../../../../node_modules/capacitor-plugin-app-tracking-transparency" SPEC CHECKSUMS: - Capacitor: 03bc7cbdde6a629a8b910a9d7d78c3cc7ed09ea7 - CapacitorApp: febecbb9582cb353aed037e18ec765141f880fe9 - CapacitorBrowser: 6299776d496e968505464884d565992faa20444a + Capacitor: 106e7a4205f4618d582b886a975657c61179138d + CapacitorApp: d63334c052278caf5d81585d80b21905c6f93f39 + CapacitorBrowser: 081852cf532acf77b9d2953f3a88fe5b9711fb06 CapacitorCordova: 5967b9ba03915ef1d585469d6e31f31dc49be96f - CapacitorHaptics: 1f1e17041f435d8ead9ff2a34edd592c6aa6a8d6 - CapacitorKeyboard: 09fd91dcde4f8a37313e7f11bde553ad1ed52036 - CapacitorPluginAppTrackingTransparency: 92ae9c1cfb5cf477753db9269689332a686f675a + CapacitorHaptics: 70e47470fa1a6bd6338cd102552e3846b7f9a1b3 + CapacitorKeyboard: 969647d0ca2e5c737d7300088e2517aa832434e2 + CapacitorPluginAppTrackingTransparency: 2a2792623a5a72795f2e8f9ab3f1147573732fd8 CryptoSwift: 967f37cea5a3294d9cce358f78861652155be483 PODFILE CHECKSUM: 2c1e4be82121f2d9724ecf7e31dd14e165aeb082 diff --git a/packages/frontend/apps/ios/apollo-codegen-chore.sh b/packages/frontend/apps/ios/apollo-codegen-chore.sh index d0619a80cc..b50275a4bc 100755 --- a/packages/frontend/apps/ios/apollo-codegen-chore.sh +++ b/packages/frontend/apps/ios/apollo-codegen-chore.sh @@ -4,8 +4,7 @@ cd "$(dirname "$0")" set -euo pipefail -VERSION=$(grep -o 'apollo-ios", exact: "[^"]*"' "App/Packages/AffineGraphQL/Package.swift" | sed 's/.*exact: "\([^"]*\)".*/\1/') -[ -z "$VERSION" ] && { echo "❌ Failed to extract version"; exit 1; } +VERSION=${1:-"1.23.0"} # Use first argument or default to 1.23.0 echo "📦 Apollo Version: $VERSION" sed -i '' "s|apollo-ios\.git\", from: \"[^\"]*\"|apollo-ios.git\", from: \"$VERSION\"|" "App/Packages/Intelligents/Package.swift" diff --git a/packages/frontend/apps/ios/codegen.ts b/packages/frontend/apps/ios/codegen.ts index fa61df1e34..596cd206ca 100644 --- a/packages/frontend/apps/ios/codegen.ts +++ b/packages/frontend/apps/ios/codegen.ts @@ -9,8 +9,12 @@ const PackageRoot = iosPackage.path; console.log('[*] PackageRoot', PackageRoot); +const version = process.argv[2] || '1.23.0'; // Default to 1.23.0 if no version provided + console.log('[*] graphql...'); -execSync(`${PackageRoot}/apollo-codegen-chore.sh`, { stdio: 'inherit' }); +execSync(`${PackageRoot}/apollo-codegen-chore.sh "${version}"`, { + stdio: 'inherit', +}); console.log('[*] rust...'); execSync( diff --git a/packages/frontend/apps/ios/package.json b/packages/frontend/apps/ios/package.json index d008c0fd3c..30d40b8628 100644 --- a/packages/frontend/apps/ios/package.json +++ b/packages/frontend/apps/ios/package.json @@ -20,7 +20,7 @@ "@affine/i18n": "workspace:*", "@affine/nbstore": "workspace:*", "@blocksuite/affine": "workspace:*", - "@blocksuite/icons": "^2.2.12", + "@blocksuite/icons": "^2.2.17", "@capacitor/app": "^7.0.0", "@capacitor/browser": "^7.0.0", "@capacitor/core": "^7.0.0", diff --git a/packages/frontend/apps/ios/setup.sh b/packages/frontend/apps/ios/setup.sh index 5ca9fe5077..b5efe9e708 100755 --- a/packages/frontend/apps/ios/setup.sh +++ b/packages/frontend/apps/ios/setup.sh @@ -38,9 +38,23 @@ rustup target add aarch64-apple-ios rustup target add aarch64-apple-ios-sim rustup target add aarch64-apple-darwin +echo "[*] syncing apollo version..." +LATEST_VERSION="1.23.0" +sed -i '' "s/exact: \"[^\"]*\"/exact: \"$LATEST_VERSION\"/g" $SCRIPT_DIR_PATH/App/Packages/AffineGraphQL/Package.swift +echo "[*] apollo version synced to $LATEST_VERSION" + +echo "[*] backing up CustomScalars..." +TEMP_DIR=$(mktemp -d) +mkdir -p "$TEMP_DIR" +function cleanup { rm -rf "$TEMP_DIR"; } +trap cleanup EXIT +CUSTOM_SCALARS_DIR=$SCRIPT_DIR_PATH/App/Packages/AffineGraphQL/Sources/Schema/CustomScalars +cp -r $CUSTOM_SCALARS_DIR/* $TEMP_DIR/ + echo "[*] codegen..." -yarn affine @affine/ios codegen +rm -rf $CUSTOM_SCALARS_DIR/* +yarn affine @affine/ios codegen "1.23.0" +cp -r $TEMP_DIR/* $CUSTOM_SCALARS_DIR/ echo "[+] setup complete" - yarn affine @affine/ios xcode diff --git a/packages/frontend/apps/ios/src/app.tsx b/packages/frontend/apps/ios/src/app.tsx index bb8bd6f2a5..825404201d 100644 --- a/packages/frontend/apps/ios/src/app.tsx +++ b/packages/frontend/apps/ios/src/app.tsx @@ -21,6 +21,7 @@ import { FeatureFlagService } from '@affine/core/modules/feature-flag'; import { GlobalContextService } from '@affine/core/modules/global-context'; import { I18nProvider } from '@affine/core/modules/i18n'; import { LifecycleService } from '@affine/core/modules/lifecycle'; +import { NativePaywallProvider } from '@affine/core/modules/paywall'; import { configureLocalStorageStateStorageImpls, NbstoreProvider, @@ -63,6 +64,7 @@ import { ModalConfigProvider } from './modal-config'; import { Auth } from './plugins/auth'; import { Hashcash } from './plugins/hashcash'; import { NbStoreNativeDBApis } from './plugins/nbstore'; +import { PayWall } from './plugins/paywall'; import { writeEndpointToken } from './proxy'; import { enableNavigationGesture$ } from './web-navigation-control'; @@ -198,6 +200,11 @@ framework.scope(ServerScope).override(AuthProvider, resolver => { }, }; }); +framework.impl(NativePaywallProvider, { + showPaywall: async (type: 'Pro' | 'AI') => { + await PayWall.showPayWall({ type }); + }, +}); const frameworkProvider = framework.provider(); diff --git a/packages/frontend/apps/ios/src/plugins/paywall/definitions.ts b/packages/frontend/apps/ios/src/plugins/paywall/definitions.ts index edc258657e..f57ddaf0db 100644 --- a/packages/frontend/apps/ios/src/plugins/paywall/definitions.ts +++ b/packages/frontend/apps/ios/src/plugins/paywall/definitions.ts @@ -1,5 +1,5 @@ export interface PayWallPlugin { showPayWall(options: { - type: string; + type: 'Pro' | 'AI'; }): Promise<{ success: boolean; type: string }>; } diff --git a/packages/frontend/apps/mobile/package.json b/packages/frontend/apps/mobile/package.json index 06b496236b..74021e9541 100644 --- a/packages/frontend/apps/mobile/package.json +++ b/packages/frontend/apps/mobile/package.json @@ -15,7 +15,7 @@ "@affine/i18n": "workspace:*", "@affine/nbstore": "workspace:*", "@blocksuite/affine": "workspace:*", - "@blocksuite/icons": "^2.2.12", + "@blocksuite/icons": "^2.2.17", "@sentry/react": "^9.2.0", "@toeverything/infra": "workspace:*", "react": "^19.0.0", diff --git a/packages/frontend/component/package.json b/packages/frontend/component/package.json index 8d053d4d9f..744746c480 100644 --- a/packages/frontend/component/package.json +++ b/packages/frontend/component/package.json @@ -27,9 +27,7 @@ "@affine/i18n": "workspace:*", "@atlaskit/pragmatic-drag-and-drop": "^1.4.0", "@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.0.3", - "@blocksuite/icons": "^2.2.12", - "@emoji-mart/data": "^1.2.1", - "@emoji-mart/react": "^1.1.1", + "@blocksuite/icons": "^2.2.17", "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.0", "@radix-ui/react-avatar": "^1.1.2", @@ -54,6 +52,7 @@ "clsx": "^2.1.1", "dayjs": "^1.11.13", "emoji-mart": "^5.6.0", + "emojibase-data": "^16.0.3", "foxact": "^0.2.45", "jotai": "^2.10.3", "lit": "^3.2.1", diff --git a/packages/frontend/component/src/index.ts b/packages/frontend/component/src/index.ts index 6b20735072..12022f7024 100644 --- a/packages/frontend/component/src/index.ts +++ b/packages/frontend/component/src/index.ts @@ -13,6 +13,7 @@ export * from './ui/editable'; export * from './ui/empty'; export * from './ui/error-message'; export * from './ui/icon-name-editor'; +export * from './ui/icon-picker'; export * from './ui/input'; export * from './ui/layout'; export * from './ui/loading'; diff --git a/packages/frontend/component/src/ui/icon-name-editor/icon-name-editor.css.ts b/packages/frontend/component/src/ui/icon-name-editor/icon-name-editor.css.ts index 1446acdc04..89aa208ad0 100644 --- a/packages/frontend/component/src/ui/icon-name-editor/icon-name-editor.css.ts +++ b/packages/frontend/component/src/ui/icon-name-editor/icon-name-editor.css.ts @@ -14,13 +14,20 @@ export const contentRoot = style({ }); export const iconPicker = style({ - border: `1px solid ${cssVarV2.layer.insideBorder.border}`, - width: 32, - height: 32, padding: 0, + lineHeight: 1, +}); +globalStyle(`${iconPicker} span:has(svg)`, { + lineHeight: 0, +}); + +export const iconNamePickerIcon = style({ flexShrink: 0, fontSize: 24, borderRadius: 4, + width: 32, + height: 32, + border: `1px solid ${cssVarV2.layer.insideBorder.border}`, selectors: { '&[data-icon-type="emoji"]': { fontSize: 20, diff --git a/packages/frontend/component/src/ui/icon-name-editor/icon-name-editor.stories.tsx b/packages/frontend/component/src/ui/icon-name-editor/icon-name-editor.stories.tsx index 514b92f58a..c63a8bda65 100644 --- a/packages/frontend/component/src/ui/icon-name-editor/icon-name-editor.stories.tsx +++ b/packages/frontend/component/src/ui/icon-name-editor/icon-name-editor.stories.tsx @@ -2,12 +2,12 @@ import type { Meta, StoryFn } from '@storybook/react'; import { useCallback, useState } from 'react'; import { Button } from '../button'; +import { type IconData, IconType } from '../icon-picker'; import { ResizePanel } from '../resize-panel/resize-panel'; import { IconAndNameEditorMenu, type IconAndNameEditorMenuProps, IconEditor, - type IconType, } from './icon-name-editor'; export default { @@ -16,10 +16,13 @@ export default { } satisfies Meta; export const Basic: StoryFn = () => { - const [icon, setIcon] = useState('👋'); + const [icon, setIcon] = useState({ + type: IconType.Emoji, + unicode: '👋', + }); const [name, setName] = useState('Hello'); - const handleIconChange = useCallback((_: IconType, icon: string) => { + const handleIconChange = useCallback((icon?: IconData) => { setIcon(icon); }, []); const handleNameChange = useCallback((name: string) => { @@ -28,7 +31,7 @@ export const Basic: StoryFn = () => { return (
-

Icon: {icon}

+

Icon: {JSON.stringify(icon)}

Name: {name}

= () => { }} > void; + onIconChange?: (data?: IconData) => void; triggerClassName?: string; } @@ -24,6 +20,8 @@ export interface IconAndNameEditorContentProps extends IconEditorProps { name: string; namePlaceholder?: string; onNameChange?: (name: string) => void; + onEnter?: () => void; + inputTestId?: string; } export interface IconAndNameEditorMenuProps @@ -33,25 +31,10 @@ export interface IconAndNameEditorMenuProps onOpenChange?: (open: boolean) => void; width?: string | number; + skipIfNotChanged?: boolean; } -const IconRenderer = ({ - iconType, - icon, -}: { - iconType: IconType; - icon: string; -}) => { - switch (iconType) { - case 'emoji': - return
{icon}
; - default: - throw new Error(`Unsupported icon type: ${iconType}`); - } -}; - export const IconEditor = ({ - iconType, icon, closeAfterSelect, iconPlaceholder, @@ -59,21 +42,24 @@ export const IconEditor = ({ onIconChange, alignOffset, sideOffset = 4, + triggerVariant, }: IconEditorProps & { alignOffset?: number; sideOffset?: number; + triggerVariant?: ButtonProps['variant']; }) => { const [isPickerOpen, setIsPickerOpen] = useState(false); - const { resolvedTheme } = useTheme(); - const handleEmojiClick = useCallback( - (emoji: any) => { - onIconChange?.('emoji', emoji.native); + + const handleSelect = useCallback( + (data?: IconData) => { + onIconChange?.(data); if (closeAfterSelect) { setIsPickerOpen(false); } }, [closeAfterSelect, onIconChange] ); + return ( e.stopPropagation()}> - +
} > ); @@ -115,17 +96,28 @@ export const IconEditor = ({ export const IconAndNameEditorContent = ({ name, namePlaceholder, + inputTestId, onNameChange, + onEnter, ...iconEditorProps }: IconAndNameEditorContentProps) => { return (
- +
); @@ -135,42 +127,44 @@ export const IconAndNameEditorMenu = ({ open, onOpenChange, width = 300, - iconType: initialIconType, icon: initialIcon, name: initialName, onIconChange, onNameChange, + contentOptions, + iconPlaceholder, + skipIfNotChanged = true, + inputTestId, + closeAfterSelect, ...menuProps }: IconAndNameEditorMenuProps) => { - const [iconType, setIconType] = useState(initialIconType); const [icon, setIcon] = useState(initialIcon); const [name, setName] = useState(initialName); const commit = useCallback(() => { - if (iconType !== initialIconType || icon !== initialIcon) { - onIconChange?.(iconType, icon); + if (icon !== initialIcon) { + onIconChange?.(icon); } - if (name !== initialName) { + if (skipIfNotChanged) { + if (name !== initialName) onNameChange?.(name); + } else { onNameChange?.(name); } }, [ icon, - iconType, initialIcon, - initialIconType, initialName, name, onIconChange, onNameChange, + skipIfNotChanged, ]); const abort = useCallback(() => { - setIconType(initialIconType); setIcon(initialIcon); setName(initialName); - }, [initialIcon, initialIconType, initialName]); - const handleIconChange = useCallback((type: IconType, icon: string) => { - setIconType(type); - setIcon(icon); + }, [initialIcon, initialName]); + const handleIconChange = useCallback((data?: IconData) => { + setIcon(data); }, []); const handleNameChange = useCallback((name: string) => { setName(name); @@ -178,13 +172,12 @@ export const IconAndNameEditorMenu = ({ const handleMenuOpenChange = useCallback( (open: boolean) => { if (open) { - setIconType(initialIconType); setIcon(initialIcon); setName(initialName); } onOpenChange?.(open); }, - [initialIcon, initialIconType, initialName, onOpenChange] + [initialIcon, initialName, onOpenChange] ); return ( @@ -205,20 +198,23 @@ export const IconAndNameEditorMenu = ({ style: { width }, onPointerDownOutside: commit, onEscapeKeyDown: abort, - ...menuProps.contentOptions, - className: clsx( - styles.menuContent, - menuProps.contentOptions?.className - ), + ...contentOptions, + className: clsx(styles.menuContent, contentOptions?.className), }} {...menuProps} items={ { + commit(); + onOpenChange?.(false); + }} /> } /> diff --git a/packages/frontend/component/src/ui/icon-picker/icon-picker.css.ts b/packages/frontend/component/src/ui/icon-picker/icon-picker.css.ts new file mode 100644 index 0000000000..4c28acc018 --- /dev/null +++ b/packages/frontend/component/src/ui/icon-picker/icon-picker.css.ts @@ -0,0 +1,31 @@ +import { cssVarV2 } from '@toeverything/theme/v2'; +import { style } from '@vanilla-extract/css'; + +export const container = style({ + width: 390, + maxWidth: '100%', + height: 400, + display: 'flex', + flexDirection: 'column', +}); + +export const header = style({ + padding: '12px 12px 0px 12px', +}); +export const headerContent = style({ + borderBottom: `0.5px solid ${cssVarV2.layer.insideBorder.border}`, + display: 'flex', + justifyContent: 'space-between', +}); +export const headerNav = style({ + backgroundColor: 'transparent', +}); +export const headerNavItem = style({ + marginBottom: 6, + fontSize: 14, +}); + +export const main = style({ + height: 0, + flexGrow: 1, +}); diff --git a/packages/frontend/component/src/ui/icon-picker/icon-picker.stories.tsx b/packages/frontend/component/src/ui/icon-picker/icon-picker.stories.tsx new file mode 100644 index 0000000000..3094af634e --- /dev/null +++ b/packages/frontend/component/src/ui/icon-picker/icon-picker.stories.tsx @@ -0,0 +1,20 @@ +import { IconPicker } from './icon-picker'; + +export default { + title: 'UI/IconPicker', + component: IconPicker, +}; + +export const Default = () => { + return ( + <> +
Selected: // TODO
+ + + ); +}; diff --git a/packages/frontend/component/src/ui/icon-picker/icon-picker.tsx b/packages/frontend/component/src/ui/icon-picker/icon-picker.tsx new file mode 100644 index 0000000000..40effc149f --- /dev/null +++ b/packages/frontend/component/src/ui/icon-picker/icon-picker.tsx @@ -0,0 +1,74 @@ +import { cssVarV2 } from '@toeverything/theme/v2'; +import clsx from 'clsx'; +import { type HTMLAttributes, useState } from 'react'; + +import { Button } from '../button'; +import { RadioGroup, type RadioItem } from '../radio'; +import * as styles from './icon-picker.css'; +import { AffineIconPicker } from './picker/affine-icon/affine-icon-picker'; +import { EmojiPicker } from './picker/emoji/emoji-picker'; +import { type IconData, IconType } from './type'; + +const panels: Array = [ + { value: 'Emoji', className: styles.headerNavItem }, + { value: 'Icons', className: styles.headerNavItem }, +]; + +export const IconPicker = ({ + className, + style, + onSelect, +}: Omit, 'onSelect'> & { + onSelect?: (data?: IconData) => void; +}) => { + const [activePanel, setActivePanel] = useState('Emoji'); + + return ( +
+
+
+ {/* Nav */} + + + {/* Remove */} + +
+
+
+ {activePanel === 'Emoji' ? ( + { + onSelect?.({ type: IconType.Emoji, unicode: emoji }); + }} + /> + ) : activePanel === 'Icons' ? ( + { + onSelect?.({ type: IconType.AffineIcon, name: icon, color }); + }} + /> + ) : null} +
+
+ ); +}; diff --git a/packages/frontend/component/src/ui/icon-picker/index.ts b/packages/frontend/component/src/ui/icon-picker/index.ts new file mode 100644 index 0000000000..d4da1beb36 --- /dev/null +++ b/packages/frontend/component/src/ui/icon-picker/index.ts @@ -0,0 +1,3 @@ +export * from './icon-picker'; +export * from './renderer'; +export * from './type'; diff --git a/packages/frontend/component/src/ui/icon-picker/picker/affine-icon/affine-icon-picker.css.ts b/packages/frontend/component/src/ui/icon-picker/picker/affine-icon/affine-icon-picker.css.ts new file mode 100644 index 0000000000..36ab18bc3d --- /dev/null +++ b/packages/frontend/component/src/ui/icon-picker/picker/affine-icon/affine-icon-picker.css.ts @@ -0,0 +1,17 @@ +import { style } from '@vanilla-extract/css'; + +export const colorList = style({ + display: 'flex', + gap: 4, +}); + +export const colorItem = style({ + padding: 2, +}); + +export const colorDot = style({ + width: '1em', + height: '1em', + borderRadius: '50%', + background: 'currentColor', +}); diff --git a/packages/frontend/component/src/ui/icon-picker/picker/affine-icon/affine-icon-picker.tsx b/packages/frontend/component/src/ui/icon-picker/picker/affine-icon/affine-icon-picker.tsx new file mode 100644 index 0000000000..673bb3973b --- /dev/null +++ b/packages/frontend/component/src/ui/icon-picker/picker/affine-icon/affine-icon-picker.tsx @@ -0,0 +1,201 @@ +import keywords from '@blocksuite/icons/keywords/en.json'; +import * as allIcons from '@blocksuite/icons/rc'; +import { cssVarV2 } from '@toeverything/theme/v2'; +import { startTransition, useCallback, useEffect, useState } from 'react'; + +import { IconButton } from '../../../button'; +import Input from '../../../input'; +import { Menu } from '../../../menu'; +import { Scrollable } from '../../../scrollbar'; +import { AffineIconRenderer } from '../../renderer/affine-icon'; +import * as pickerStyles from '../picker.css'; +import * as styles from './affine-icon-picker.css'; + +type Icon = { + name: string; + keywords: string[]; +}; + +const icons = keywords['Emoji Panel'] as Icon[]; + +const colorList: string[] = [ + cssVarV2.block.callout.icon.red, + cssVarV2.block.callout.icon.orange, + cssVarV2.block.callout.icon.yellow, + cssVarV2.block.callout.icon.green, + cssVarV2.block.callout.icon.teal, + cssVarV2.block.callout.icon.blue, + cssVarV2.block.callout.icon.purple, + cssVarV2.block.callout.icon.magenta, + cssVarV2.block.callout.icon.grey, +]; + +const useRecentIcons = () => { + const [recentIcons, setRecentIcons] = useState>([]); + + useEffect(() => { + const recentIcons = localStorage.getItem('recentIcons'); + setRecentIcons(recentIcons ? recentIcons.split(',') : []); + }, []); + + const add = useCallback((icon: string) => { + setRecentIcons(prevRecentIcons => { + const newRecentIcons = [ + icon, + ...prevRecentIcons.filter(e => e !== icon), + ].slice(0, 10); + localStorage.setItem('recentIcons', newRecentIcons.join(',')); + return newRecentIcons; + }); + }, []); + + return { + recentIcons, + add, + }; +}; + +export const AffineIconPicker = ({ + onSelect, +}: { + onSelect?: (icon: string, color: string) => void; +}) => { + const [filteredIcons, setFilteredIcons] = useState([]); + const [keyword, setKeyword] = useState(''); + const [color, setColor] = useState(cssVarV2.block.callout.icon.blue); + + const { recentIcons, add: addRecentIcon } = useRecentIcons(); + + useEffect(() => { + startTransition(() => { + if (!keyword) { + setFilteredIcons(icons); + return; + } + + setFilteredIcons( + icons.filter(icon => + icon.keywords.some(kw => kw.includes(keyword.toLowerCase())) + ) + ); + }); + }, [keyword]); + + const handleIconSelect = useCallback( + (icon: string) => { + addRecentIcon(icon); + onSelect?.(icon, color); + }, + [addRecentIcon, onSelect, color] + ); + + return ( +
+ {/* Search */} +
+ + +
+ } + placeholder="Filter..." + /> + + {/* Color Picker */} + + {colorList.map(color => ( + + } + onClick={() => setColor(color)} + /> + ))} + + } + > + + } + /> + + + + {/* Content */} + + + {/* Recent */} + {recentIcons.length ? ( +
+
+ Recent +
+
+ {recentIcons.map(iconName => ( + + } + onClick={() => handleIconSelect(iconName)} + /> + ))} +
+
+ ) : null} + + {/* Groups */} +
+
+ Icons +
+
+ {filteredIcons.map(icon => { + return ( + + } + onClick={() => handleIconSelect(icon.name)} + /> + ); + })} +
+
+
+ +
+ + ); +}; diff --git a/packages/frontend/component/src/ui/icon-picker/picker/emoji/constants.ts b/packages/frontend/component/src/ui/icon-picker/picker/emoji/constants.ts new file mode 100644 index 0000000000..8255ef6be7 --- /dev/null +++ b/packages/frontend/component/src/ui/icon-picker/picker/emoji/constants.ts @@ -0,0 +1,43 @@ +import type { ComponentType, SVGProps } from 'react'; + +import { ActivityIcon } from './icons/activity'; +import { AnimalIcon } from './icons/animal'; +import { FlagIcon } from './icons/flag'; +import { FoodIcon } from './icons/food'; +import { ObjectIcon } from './icons/object'; +import { SmileIcon } from './icons/smile'; +import { SymbolIcon } from './icons/symbol'; +import { TravelIcon } from './icons/travel'; + +export type GroupName = + | 'Smileys & People' + | 'Animals & Nature' + | 'Food & Drink' + | 'Activity' + | 'Travel & Places' + | 'Objects' + | 'Symbols' + | 'Flags'; +export const GROUPS: GroupName[] = [ + 'Smileys & People', + 'Animals & Nature', + 'Food & Drink', + 'Activity', + 'Travel & Places', + 'Objects', + 'Symbols', + 'Flags', +]; +export const GROUP_ICON_MAP: Record< + GroupName, + ComponentType> +> = { + 'Smileys & People': SmileIcon, + 'Animals & Nature': AnimalIcon, + 'Food & Drink': FoodIcon, + Activity: ActivityIcon, + 'Travel & Places': TravelIcon, + Objects: ObjectIcon, + Symbols: SymbolIcon, + Flags: FlagIcon, +}; diff --git a/packages/frontend/component/src/ui/icon-picker/picker/emoji/data/en.json b/packages/frontend/component/src/ui/icon-picker/picker/emoji/data/en.json new file mode 100644 index 0000000000..c81d71e41d --- /dev/null +++ b/packages/frontend/component/src/ui/icon-picker/picker/emoji/data/en.json @@ -0,0 +1,35020 @@ +[ + { + "name": "Smileys & People", + "emojis": [ + { + "group": 0, + "hexcode": "1F600", + "label": "grinning face", + "order": 1, + "tags": [ + "cheerful", + "cheery", + "face", + "grin", + "grinning", + "happy", + "laugh", + "nice", + "smile", + "smiling", + "teeth" + ], + "unicode": "😀" + }, + { + "group": 0, + "hexcode": "1F603", + "label": "grinning face with big eyes", + "order": 2, + "tags": [ + "awesome", + "big", + "eyes", + "face", + "grin", + "grinning", + "happy", + "mouth", + "open", + "smile", + "smiling", + "teeth", + "yay" + ], + "unicode": "😃" + }, + { + "group": 0, + "hexcode": "1F604", + "label": "grinning face with smiling eyes", + "order": 3, + "tags": [ + "eye", + "eyes", + "face", + "grin", + "grinning", + "happy", + "laugh", + "lol", + "mouth", + "open", + "smile", + "smiling" + ], + "unicode": "😄", + "emoticon": ":D" + }, + { + "group": 0, + "hexcode": "1F601", + "label": "beaming face with smiling eyes", + "order": 4, + "tags": [ + "beaming", + "eye", + "eyes", + "face", + "grin", + "grinning", + "happy", + "nice", + "smile", + "smiling", + "teeth" + ], + "unicode": "😁" + }, + { + "group": 0, + "hexcode": "1F606", + "label": "grinning squinting face", + "order": 5, + "tags": [ + "closed", + "eyes", + "face", + "grinning", + "haha", + "hahaha", + "happy", + "laugh", + "lol", + "mouth", + "open", + "rofl", + "smile", + "smiling", + "squinting" + ], + "unicode": "😆", + "emoticon": ["xD", "XD"] + }, + { + "group": 0, + "hexcode": "1F605", + "label": "grinning face with sweat", + "order": 6, + "tags": [ + "cold", + "dejected", + "excited", + "face", + "grinning", + "mouth", + "nervous", + "open", + "smile", + "smiling", + "stress", + "stressed", + "sweat" + ], + "unicode": "😅" + }, + { + "group": 0, + "hexcode": "1F923", + "label": "rolling on the floor laughing", + "order": 7, + "tags": [ + "crying", + "face", + "floor", + "funny", + "haha", + "happy", + "hehe", + "hilarious", + "joy", + "laugh", + "lmao", + "lol", + "rofl", + "roflmao", + "rolling", + "tear" + ], + "unicode": "🤣", + "emoticon": ":'D" + }, + { + "group": 0, + "hexcode": "1F602", + "label": "face with tears of joy", + "order": 8, + "tags": [ + "crying", + "face", + "feels", + "funny", + "haha", + "happy", + "hehe", + "hilarious", + "joy", + "laugh", + "lmao", + "lol", + "rofl", + "roflmao", + "tear" + ], + "unicode": "😂", + "emoticon": ":')" + }, + { + "group": 0, + "hexcode": "1F642", + "label": "slightly smiling face", + "order": 9, + "tags": ["face", "happy", "slightly", "smile", "smiling"], + "unicode": "🙂", + "emoticon": ":)" + }, + { + "group": 0, + "hexcode": "1F643", + "label": "upside-down face", + "order": 10, + "tags": ["face", "hehe", "smile", "upside-down"], + "unicode": "🙃" + }, + { + "group": 0, + "hexcode": "1FAE0", + "label": "melting face", + "order": 11, + "tags": [ + "disappear", + "dissolve", + "embarrassed", + "face", + "haha", + "heat", + "hot", + "liquid", + "lol", + "melt", + "melting", + "sarcasm", + "sarcastic" + ], + "unicode": "🫠" + }, + { + "group": 0, + "hexcode": "1F609", + "label": "winking face", + "order": 12, + "tags": [ + "face", + "flirt", + "heartbreaker", + "sexy", + "slide", + "tease", + "wink", + "winking", + "winks" + ], + "unicode": "😉", + "emoticon": ";)" + }, + { + "group": 0, + "hexcode": "1F60A", + "label": "smiling face with smiling eyes", + "order": 13, + "tags": [ + "blush", + "eye", + "eyes", + "face", + "glad", + "satisfied", + "smile", + "smiling" + ], + "unicode": "😊", + "emoticon": ":>" + }, + { + "group": 0, + "hexcode": "1F607", + "label": "smiling face with halo", + "order": 14, + "tags": [ + "angel", + "angelic", + "angels", + "blessed", + "face", + "fairy", + "fairytale", + "fantasy", + "halo", + "happy", + "innocent", + "peaceful", + "smile", + "smiling", + "spirit", + "tale" + ], + "unicode": "😇", + "emoticon": ["o:)", "O:)"] + }, + { + "group": 0, + "hexcode": "1F970", + "label": "smiling face with hearts", + "order": 15, + "tags": [ + "3", + "adore", + "crush", + "face", + "heart", + "hearts", + "ily", + "love", + "romance", + "smile", + "smiling", + "you" + ], + "unicode": "🥰" + }, + { + "group": 0, + "hexcode": "1F60D", + "label": "smiling face with heart-eyes", + "order": 16, + "tags": [ + "143", + "bae", + "eye", + "face", + "feels", + "heart-eyes", + "hearts", + "ily", + "kisses", + "love", + "romance", + "romantic", + "smile", + "xoxo" + ], + "unicode": "😍" + }, + { + "group": 0, + "hexcode": "1F929", + "label": "star-struck", + "order": 17, + "tags": [ + "excited", + "eyes", + "face", + "grinning", + "smile", + "star", + "starry-eyed", + "wow" + ], + "unicode": "🤩" + }, + { + "group": 0, + "hexcode": "1F618", + "label": "face blowing a kiss", + "order": 18, + "tags": [ + "adorbs", + "bae", + "blowing", + "face", + "flirt", + "heart", + "ily", + "kiss", + "love", + "lover", + "miss", + "muah", + "romantic", + "smooch", + "xoxo", + "you" + ], + "unicode": "😘", + "emoticon": [":x", ":X"] + }, + { + "group": 0, + "hexcode": "1F617", + "label": "kissing face", + "order": 19, + "tags": [ + "143", + "date", + "dating", + "face", + "flirt", + "ily", + "kiss", + "love", + "smooch", + "smooches", + "xoxo", + "you" + ], + "unicode": "😗" + }, + { + "group": 0, + "hexcode": "263A", + "label": "smiling face", + "order": 21, + "tags": ["face", "happy", "outlined", "relaxed", "smile", "smiling"], + "unicode": "☺️" + }, + { + "group": 0, + "hexcode": "1F61A", + "label": "kissing face with closed eyes", + "order": 22, + "tags": [ + "143", + "bae", + "blush", + "closed", + "date", + "dating", + "eye", + "eyes", + "face", + "flirt", + "ily", + "kisses", + "kissing", + "smooches", + "xoxo" + ], + "unicode": "😚", + "emoticon": ":*" + }, + { + "group": 0, + "hexcode": "1F619", + "label": "kissing face with smiling eyes", + "order": 23, + "tags": [ + "143", + "closed", + "date", + "dating", + "eye", + "eyes", + "face", + "flirt", + "ily", + "kiss", + "kisses", + "kissing", + "love", + "night", + "smile", + "smiling" + ], + "unicode": "😙" + }, + { + "group": 0, + "hexcode": "1F972", + "label": "smiling face with tear", + "order": 24, + "tags": [ + "face", + "glad", + "grateful", + "happy", + "joy", + "pain", + "proud", + "relieved", + "smile", + "smiley", + "smiling", + "tear", + "touched" + ], + "unicode": "🥲" + }, + { + "group": 0, + "hexcode": "1F60B", + "label": "face savoring food", + "order": 25, + "tags": [ + "delicious", + "eat", + "face", + "food", + "full", + "hungry", + "savor", + "smile", + "smiling", + "tasty", + "um", + "yum", + "yummy" + ], + "unicode": "😋" + }, + { + "group": 0, + "hexcode": "1F61B", + "label": "face with tongue", + "order": 26, + "tags": [ + "awesome", + "cool", + "face", + "nice", + "party", + "stuck-out", + "sweet", + "tongue" + ], + "unicode": "😛", + "emoticon": [":p", ":P"] + }, + { + "group": 0, + "hexcode": "1F61C", + "label": "winking face with tongue", + "order": 27, + "tags": [ + "crazy", + "epic", + "eye", + "face", + "funny", + "joke", + "loopy", + "nutty", + "party", + "stuck-out", + "tongue", + "wacky", + "weirdo", + "wink", + "winking", + "yolo" + ], + "unicode": "😜", + "emoticon": [";p", ";P"] + }, + { + "group": 0, + "hexcode": "1F92A", + "label": "zany face", + "order": 28, + "tags": [ + "crazy", + "eye", + "eyes", + "face", + "goofy", + "large", + "small", + "zany" + ], + "unicode": "🤪" + }, + { + "group": 0, + "hexcode": "1F61D", + "label": "squinting face with tongue", + "order": 29, + "tags": [ + "closed", + "eye", + "eyes", + "face", + "gross", + "horrible", + "omg", + "squinting", + "stuck-out", + "taste", + "tongue", + "whatever", + "yolo" + ], + "unicode": "😝", + "emoticon": ["xp", "xP", "XP"] + }, + { + "group": 0, + "hexcode": "1F911", + "label": "money-mouth face", + "order": 30, + "tags": ["face", "money", "money-mouth", "mouth", "paid"], + "unicode": "🤑" + }, + { + "group": 0, + "hexcode": "1F917", + "label": "smiling face with open hands", + "order": 31, + "tags": ["face", "hands", "hug", "hugging", "open", "smiling"], + "unicode": "🤗" + }, + { + "group": 0, + "hexcode": "1F92D", + "label": "face with hand over mouth", + "order": 32, + "tags": [ + "face", + "giggle", + "giggling", + "hand", + "mouth", + "oops", + "realization", + "secret", + "shock", + "sudden", + "surprise", + "whoops" + ], + "unicode": "🤭" + }, + { + "group": 0, + "hexcode": "1FAE2", + "label": "face with open eyes and hand over mouth", + "order": 33, + "tags": [ + "amazement", + "awe", + "disbelief", + "embarrass", + "eyes", + "face", + "gasp", + "hand", + "mouth", + "omg", + "open", + "over", + "quiet", + "scared", + "shock", + "surprise" + ], + "unicode": "🫢" + }, + { + "group": 0, + "hexcode": "1FAE3", + "label": "face with peeking eye", + "order": 34, + "tags": [ + "captivated", + "embarrass", + "eye", + "face", + "hide", + "hiding", + "peek", + "peeking", + "peep", + "scared", + "shy", + "stare" + ], + "unicode": "🫣" + }, + { + "group": 0, + "hexcode": "1F92B", + "label": "shushing face", + "order": 35, + "tags": ["face", "quiet", "shh", "shush", "shushing"], + "unicode": "🤫" + }, + { + "group": 0, + "hexcode": "1F914", + "label": "thinking face", + "order": 36, + "tags": [ + "chin", + "consider", + "face", + "hmm", + "ponder", + "pondering", + "thinking", + "wondering" + ], + "unicode": "🤔", + "emoticon": [":l", ":L"] + }, + { + "group": 0, + "hexcode": "1FAE1", + "label": "saluting face", + "order": 37, + "tags": [ + "face", + "good", + "luck", + "ma’am", + "ok", + "respect", + "salute", + "saluting", + "sir", + "troops", + "yes" + ], + "unicode": "🫡" + }, + { + "group": 0, + "hexcode": "1F910", + "label": "zipper-mouth face", + "order": 38, + "tags": [ + "face", + "keep", + "mouth", + "quiet", + "secret", + "shut", + "zip", + "zipper", + "zipper-mouth" + ], + "unicode": "🤐", + "emoticon": [":z", ":Z"] + }, + { + "group": 0, + "hexcode": "1F928", + "label": "face with raised eyebrow", + "order": 39, + "tags": [ + "disapproval", + "disbelief", + "distrust", + "emoji", + "eyebrow", + "face", + "hmm", + "mild", + "raised", + "skeptic", + "skeptical", + "skepticism", + "surprise", + "what" + ], + "unicode": "🤨" + }, + { + "group": 0, + "hexcode": "1F610", + "label": "neutral face", + "order": 40, + "tags": [ + "awkward", + "blank", + "deadpan", + "expressionless", + "face", + "fine", + "jealous", + "meh", + "neutral", + "oh", + "shade", + "straight", + "unamused", + "unhappy", + "unimpressed", + "whatever" + ], + "unicode": "😐️", + "emoticon": ":|" + }, + { + "group": 0, + "hexcode": "1F611", + "label": "expressionless face", + "order": 41, + "tags": [ + "awkward", + "dead", + "expressionless", + "face", + "fine", + "inexpressive", + "jealous", + "meh", + "not", + "oh", + "omg", + "straight", + "uh", + "unhappy", + "unimpressed", + "whatever" + ], + "unicode": "😑" + }, + { + "group": 0, + "hexcode": "1F636", + "label": "face without mouth", + "order": 42, + "tags": [ + "awkward", + "blank", + "expressionless", + "face", + "mouth", + "mouthless", + "mute", + "quiet", + "secret", + "silence", + "silent", + "speechless" + ], + "unicode": "😶", + "emoticon": ":#" + }, + { + "group": 0, + "hexcode": "1FAE5", + "label": "dotted line face", + "order": 43, + "tags": [ + "depressed", + "disappear", + "dotted", + "face", + "hidden", + "hide", + "introvert", + "invisible", + "line", + "meh", + "whatever", + "wtv" + ], + "unicode": "🫥" + }, + { + "group": 0, + "hexcode": "1F636-200D-1F32B-FE0F", + "label": "face in clouds", + "order": 44, + "tags": ["absentminded", "clouds", "face", "fog", "head"], + "unicode": "😶‍🌫️" + }, + { + "group": 0, + "hexcode": "1F60F", + "label": "smirking face", + "order": 46, + "tags": [ + "boss", + "dapper", + "face", + "flirt", + "homie", + "kidding", + "leer", + "shade", + "slick", + "sly", + "smirk", + "smug", + "snicker", + "suave", + "suspicious", + "swag" + ], + "unicode": "😏", + "emoticon": ":j" + }, + { + "group": 0, + "hexcode": "1F612", + "label": "unamused face", + "order": 47, + "tags": [ + "...", + "bored", + "face", + "fine", + "jealous", + "jel", + "jelly", + "pissed", + "smh", + "ugh", + "uhh", + "unamused", + "unhappy", + "weird", + "whatever" + ], + "unicode": "😒", + "emoticon": ":?" + }, + { + "group": 0, + "hexcode": "1F644", + "label": "face with rolling eyes", + "order": 48, + "tags": [ + "eyeroll", + "eyes", + "face", + "rolling", + "shade", + "ugh", + "whatever" + ], + "unicode": "🙄" + }, + { + "group": 0, + "hexcode": "1F62C", + "label": "grimacing face", + "order": 49, + "tags": [ + "awk", + "awkward", + "dentist", + "face", + "grimace", + "grimacing", + "grinning", + "smile", + "smiling" + ], + "unicode": "😬", + "emoticon": "8D" + }, + { + "group": 0, + "hexcode": "1F62E-200D-1F4A8", + "label": "face exhaling", + "order": 50, + "tags": [ + "blow", + "blowing", + "exhale", + "exhaling", + "exhausted", + "face", + "gasp", + "groan", + "relief", + "sigh", + "smiley", + "smoke", + "whisper", + "whistle" + ], + "unicode": "😮‍💨" + }, + { + "group": 0, + "hexcode": "1F925", + "label": "lying face", + "order": 51, + "tags": ["face", "liar", "lie", "lying", "pinocchio"], + "unicode": "🤥" + }, + { + "group": 0, + "hexcode": "1FAE8", + "label": "shaking face", + "order": 52, + "tags": [ + "crazy", + "daze", + "earthquake", + "face", + "omg", + "panic", + "shaking", + "shock", + "surprise", + "vibrate", + "whoa", + "wow" + ], + "unicode": "🫨" + }, + { + "group": 0, + "hexcode": "1F642-200D-2194-FE0F", + "label": "head shaking horizontally", + "order": 53, + "tags": ["head", "horizontally", "no", "shake", "shaking"], + "unicode": "🙂‍↔️" + }, + { + "group": 0, + "hexcode": "1F642-200D-2195-FE0F", + "label": "head shaking vertically", + "order": 55, + "tags": ["head", "nod", "shaking", "vertically", "yes"], + "unicode": "🙂‍↕️" + }, + { + "group": 0, + "hexcode": "1F60C", + "label": "relieved face", + "order": 57, + "tags": ["calm", "face", "peace", "relief", "relieved", "zen"], + "unicode": "😌" + }, + { + "group": 0, + "hexcode": "1F614", + "label": "pensive face", + "order": 58, + "tags": [ + "awful", + "bored", + "dejected", + "died", + "disappointed", + "face", + "losing", + "lost", + "pensive", + "sad", + "sucks" + ], + "unicode": "😔" + }, + { + "group": 0, + "hexcode": "1F62A", + "label": "sleepy face", + "order": 59, + "tags": [ + "crying", + "face", + "good", + "night", + "sad", + "sleep", + "sleeping", + "sleepy", + "tired" + ], + "unicode": "😪" + }, + { + "group": 0, + "hexcode": "1F924", + "label": "drooling face", + "order": 60, + "tags": ["drooling", "face"], + "unicode": "🤤" + }, + { + "group": 0, + "hexcode": "1F634", + "label": "sleeping face", + "order": 61, + "tags": [ + "bed", + "bedtime", + "face", + "good", + "goodnight", + "nap", + "night", + "sleep", + "sleeping", + "tired", + "whatever", + "yawn", + "zzz" + ], + "unicode": "😴" + }, + { + "group": 0, + "hexcode": "1FAE9", + "label": "face with bags under eyes", + "order": 62, + "tags": [ + "bags", + "bored", + "exhausted", + "eyes", + "face", + "fatigued", + "late", + "sleepy", + "tired", + "weary" + ], + "unicode": "🫩" + }, + { + "group": 0, + "hexcode": "1F637", + "label": "face with medical mask", + "order": 63, + "tags": [ + "cold", + "dentist", + "dermatologist", + "doctor", + "dr", + "face", + "germs", + "mask", + "medical", + "medicine", + "sick" + ], + "unicode": "😷" + }, + { + "group": 0, + "hexcode": "1F912", + "label": "face with thermometer", + "order": 64, + "tags": ["face", "ill", "sick", "thermometer"], + "unicode": "🤒" + }, + { + "group": 0, + "hexcode": "1F915", + "label": "face with head-bandage", + "order": 65, + "tags": ["bandage", "face", "head-bandage", "hurt", "injury", "ouch"], + "unicode": "🤕" + }, + { + "group": 0, + "hexcode": "1F922", + "label": "nauseated face", + "order": 66, + "tags": ["face", "gross", "nasty", "nauseated", "sick", "vomit"], + "unicode": "🤢", + "emoticon": "%(" + }, + { + "group": 0, + "hexcode": "1F92E", + "label": "face vomiting", + "order": 67, + "tags": [ + "barf", + "ew", + "face", + "gross", + "puke", + "sick", + "spew", + "throw", + "up", + "vomit", + "vomiting" + ], + "unicode": "🤮" + }, + { + "group": 0, + "hexcode": "1F927", + "label": "sneezing face", + "order": 68, + "tags": [ + "face", + "fever", + "flu", + "gesundheit", + "sick", + "sneeze", + "sneezing" + ], + "unicode": "🤧" + }, + { + "group": 0, + "hexcode": "1F975", + "label": "hot face", + "order": 69, + "tags": [ + "dying", + "face", + "feverish", + "heat", + "hot", + "panting", + "red-faced", + "stroke", + "sweating", + "tongue" + ], + "unicode": "🥵" + }, + { + "group": 0, + "hexcode": "1F976", + "label": "cold face", + "order": 70, + "tags": [ + "blue", + "blue-faced", + "cold", + "face", + "freezing", + "frostbite", + "icicles", + "subzero", + "teeth" + ], + "unicode": "🥶" + }, + { + "group": 0, + "hexcode": "1F974", + "label": "woozy face", + "order": 71, + "tags": [ + "dizzy", + "drunk", + "eyes", + "face", + "intoxicated", + "mouth", + "tipsy", + "uneven", + "wavy", + "woozy" + ], + "unicode": "🥴", + "emoticon": ":&" + }, + { + "group": 0, + "hexcode": "1F635", + "label": "face with crossed-out eyes", + "order": 72, + "tags": [ + "crossed-out", + "dead", + "dizzy", + "eyes", + "face", + "feels", + "knocked", + "out", + "sick", + "tired" + ], + "unicode": "😵", + "emoticon": ["xo", "XO"] + }, + { + "group": 0, + "hexcode": "1F635-200D-1F4AB", + "label": "face with spiral eyes", + "order": 73, + "tags": [ + "confused", + "dizzy", + "eyes", + "face", + "hypnotized", + "omg", + "smiley", + "spiral", + "trouble", + "whoa", + "woah", + "woozy" + ], + "unicode": "😵‍💫" + }, + { + "group": 0, + "hexcode": "1F92F", + "label": "exploding head", + "order": 74, + "tags": [ + "blown", + "explode", + "exploding", + "head", + "mind", + "mindblown", + "no", + "shocked", + "way" + ], + "unicode": "🤯" + }, + { + "group": 0, + "hexcode": "1F920", + "label": "cowboy hat face", + "order": 75, + "tags": ["cowboy", "cowgirl", "face", "hat"], + "unicode": "🤠" + }, + { + "group": 0, + "hexcode": "1F973", + "label": "partying face", + "order": 76, + "tags": [ + "bday", + "birthday", + "celebrate", + "celebration", + "excited", + "face", + "happy", + "hat", + "hooray", + "horn", + "party", + "partying" + ], + "unicode": "🥳" + }, + { + "group": 0, + "hexcode": "1F978", + "label": "disguised face", + "order": 77, + "tags": [ + "disguise", + "eyebrow", + "face", + "glasses", + "incognito", + "moustache", + "mustache", + "nose", + "person", + "spy", + "tache", + "tash" + ], + "unicode": "🥸" + }, + { + "group": 0, + "hexcode": "1F60E", + "label": "smiling face with sunglasses", + "order": 78, + "tags": [ + "awesome", + "beach", + "bright", + "bro", + "chilling", + "cool", + "face", + "rad", + "relaxed", + "shades", + "slay", + "smile", + "style", + "sunglasses", + "swag", + "win" + ], + "unicode": "😎", + "emoticon": "8)" + }, + { + "group": 0, + "hexcode": "1F913", + "label": "nerd face", + "order": 79, + "tags": [ + "brainy", + "clever", + "expert", + "face", + "geek", + "gifted", + "glasses", + "intelligent", + "nerd", + "smart" + ], + "unicode": "🤓", + "emoticon": ":B" + }, + { + "group": 0, + "hexcode": "1F9D0", + "label": "face with monocle", + "order": 80, + "tags": [ + "classy", + "face", + "fancy", + "monocle", + "rich", + "stuffy", + "wealthy" + ], + "unicode": "🧐" + }, + { + "group": 0, + "hexcode": "1F615", + "label": "confused face", + "order": 81, + "tags": [ + "befuddled", + "confused", + "confusing", + "dunno", + "face", + "frown", + "hm", + "meh", + "not", + "sad", + "sorry", + "sure" + ], + "unicode": "😕", + "emoticon": ":/" + }, + { + "group": 0, + "hexcode": "1FAE4", + "label": "face with diagonal mouth", + "order": 82, + "tags": [ + "confused", + "confusion", + "diagonal", + "disappointed", + "doubt", + "doubtful", + "face", + "frustrated", + "frustration", + "meh", + "mouth", + "skeptical", + "unsure", + "whatever", + "wtv" + ], + "unicode": "🫤" + }, + { + "group": 0, + "hexcode": "1F61F", + "label": "worried face", + "order": 83, + "tags": [ + "anxious", + "butterflies", + "face", + "nerves", + "nervous", + "sad", + "stress", + "stressed", + "surprised", + "worried", + "worry" + ], + "unicode": "😟" + }, + { + "group": 0, + "hexcode": "1F641", + "label": "slightly frowning face", + "order": 84, + "tags": ["face", "frown", "frowning", "sad", "slightly"], + "unicode": "🙁" + }, + { + "group": 0, + "hexcode": "2639", + "label": "frowning face", + "order": 86, + "tags": ["face", "frown", "frowning", "sad"], + "unicode": "☹️", + "emoticon": ":(" + }, + { + "group": 0, + "hexcode": "1F62E", + "label": "face with open mouth", + "order": 87, + "tags": [ + "believe", + "face", + "forgot", + "mouth", + "omg", + "open", + "shocked", + "surprised", + "sympathy", + "unbelievable", + "unreal", + "whoa", + "wow", + "you" + ], + "unicode": "😮" + }, + { + "group": 0, + "hexcode": "1F62F", + "label": "hushed face", + "order": 88, + "tags": [ + "epic", + "face", + "hushed", + "omg", + "stunned", + "surprised", + "whoa", + "woah" + ], + "unicode": "😯" + }, + { + "group": 0, + "hexcode": "1F632", + "label": "astonished face", + "order": 89, + "tags": [ + "astonished", + "cost", + "face", + "no", + "omg", + "shocked", + "totally", + "way" + ], + "unicode": "😲", + "emoticon": [":o", ":O"] + }, + { + "group": 0, + "hexcode": "1F633", + "label": "flushed face", + "order": 90, + "tags": [ + "amazed", + "awkward", + "crazy", + "dazed", + "dead", + "disbelief", + "embarrassed", + "face", + "flushed", + "geez", + "heat", + "hot", + "impressed", + "jeez", + "what", + "wow" + ], + "unicode": "😳", + "emoticon": ":$" + }, + { + "group": 0, + "hexcode": "1F97A", + "label": "pleading face", + "order": 91, + "tags": [ + "begging", + "big", + "eyes", + "face", + "mercy", + "not", + "pleading", + "please", + "pretty", + "puppy", + "sad", + "why" + ], + "unicode": "🥺" + }, + { + "group": 0, + "hexcode": "1F979", + "label": "face holding back tears", + "order": 92, + "tags": [ + "admiration", + "aww", + "back", + "cry", + "embarrassed", + "face", + "feelings", + "grateful", + "gratitude", + "holding", + "joy", + "please", + "proud", + "resist", + "sad", + "tears" + ], + "unicode": "🥹" + }, + { + "group": 0, + "hexcode": "1F626", + "label": "frowning face with open mouth", + "order": 93, + "tags": [ + "caught", + "face", + "frown", + "frowning", + "guard", + "mouth", + "open", + "scared", + "scary", + "surprise", + "what", + "wow" + ], + "unicode": "😦" + }, + { + "group": 0, + "hexcode": "1F627", + "label": "anguished face", + "order": 94, + "tags": [ + "anguished", + "face", + "forgot", + "scared", + "scary", + "stressed", + "surprise", + "unhappy", + "what", + "wow" + ], + "unicode": "😧", + "emoticon": [":s", ":S"] + }, + { + "group": 0, + "hexcode": "1F628", + "label": "fearful face", + "order": 95, + "tags": [ + "afraid", + "anxious", + "blame", + "face", + "fear", + "fearful", + "scared", + "worried" + ], + "unicode": "😨" + }, + { + "group": 0, + "hexcode": "1F630", + "label": "anxious face with sweat", + "order": 96, + "tags": [ + "anxious", + "blue", + "cold", + "eek", + "face", + "mouth", + "nervous", + "open", + "rushed", + "scared", + "sweat", + "yikes" + ], + "unicode": "😰" + }, + { + "group": 0, + "hexcode": "1F625", + "label": "sad but relieved face", + "order": 97, + "tags": [ + "anxious", + "call", + "close", + "complicated", + "disappointed", + "face", + "not", + "relieved", + "sad", + "sweat", + "time", + "whew" + ], + "unicode": "😥" + }, + { + "group": 0, + "hexcode": "1F622", + "label": "crying face", + "order": 98, + "tags": [ + "awful", + "cry", + "crying", + "face", + "feels", + "miss", + "sad", + "tear", + "triste", + "unhappy" + ], + "unicode": "😢", + "emoticon": ":'(" + }, + { + "group": 0, + "hexcode": "1F62D", + "label": "loudly crying face", + "order": 99, + "tags": [ + "bawling", + "cry", + "crying", + "face", + "loudly", + "sad", + "sob", + "tear", + "tears", + "unhappy" + ], + "unicode": "😭", + "emoticon": ":'o" + }, + { + "group": 0, + "hexcode": "1F631", + "label": "face screaming in fear", + "order": 100, + "tags": [ + "epic", + "face", + "fear", + "fearful", + "munch", + "scared", + "scream", + "screamer", + "screaming", + "shocked", + "surprised", + "woah" + ], + "unicode": "😱", + "emoticon": "Dx" + }, + { + "group": 0, + "hexcode": "1F616", + "label": "confounded face", + "order": 101, + "tags": [ + "annoyed", + "confounded", + "confused", + "cringe", + "distraught", + "face", + "feels", + "frustrated", + "mad", + "sad" + ], + "unicode": "😖", + "emoticon": ["x(", "X("] + }, + { + "group": 0, + "hexcode": "1F623", + "label": "persevering face", + "order": 102, + "tags": [ + "concentrate", + "concentration", + "face", + "focus", + "headache", + "persevere", + "persevering" + ], + "unicode": "😣" + }, + { + "group": 0, + "hexcode": "1F61E", + "label": "disappointed face", + "order": 103, + "tags": [ + "awful", + "blame", + "dejected", + "disappointed", + "face", + "fail", + "losing", + "sad", + "unhappy" + ], + "unicode": "😞" + }, + { + "group": 0, + "hexcode": "1F613", + "label": "downcast face with sweat", + "order": 104, + "tags": [ + "close", + "cold", + "downcast", + "face", + "feels", + "headache", + "nervous", + "sad", + "scared", + "sweat", + "yikes" + ], + "unicode": "😓", + "emoticon": ":<" + }, + { + "group": 0, + "hexcode": "1F629", + "label": "weary face", + "order": 105, + "tags": [ + "crying", + "face", + "fail", + "feels", + "hungry", + "mad", + "nooo", + "sad", + "sleepy", + "tired", + "unhappy", + "weary" + ], + "unicode": "😩", + "emoticon": "D:" + }, + { + "group": 0, + "hexcode": "1F62B", + "label": "tired face", + "order": 106, + "tags": ["cost", "face", "feels", "nap", "sad", "sneeze", "tired"], + "unicode": "😫", + "emoticon": [":c", ":C"] + }, + { + "group": 0, + "hexcode": "1F971", + "label": "yawning face", + "order": 107, + "tags": [ + "bedtime", + "bored", + "face", + "goodnight", + "nap", + "night", + "sleep", + "sleepy", + "tired", + "whatever", + "yawn", + "yawning", + "zzz" + ], + "unicode": "🥱" + }, + { + "group": 0, + "hexcode": "1F624", + "label": "face with steam from nose", + "order": 108, + "tags": [ + "anger", + "angry", + "face", + "feels", + "fume", + "fuming", + "furious", + "fury", + "mad", + "nose", + "steam", + "triumph", + "unhappy", + "won" + ], + "unicode": "😤" + }, + { + "group": 0, + "hexcode": "1F621", + "label": "enraged face", + "order": 109, + "tags": [ + "anger", + "angry", + "enraged", + "face", + "feels", + "mad", + "maddening", + "pouting", + "rage", + "red", + "shade", + "unhappy", + "upset" + ], + "unicode": "😡", + "emoticon": ">:/" + }, + { + "group": 0, + "hexcode": "1F620", + "label": "angry face", + "order": 110, + "tags": [ + "anger", + "angry", + "blame", + "face", + "feels", + "frustrated", + "mad", + "maddening", + "rage", + "shade", + "unhappy", + "upset" + ], + "unicode": "😠" + }, + { + "group": 0, + "hexcode": "1F92C", + "label": "face with symbols on mouth", + "order": 111, + "tags": [ + "censor", + "cursing", + "cussing", + "face", + "mad", + "mouth", + "pissed", + "swearing", + "symbols" + ], + "unicode": "🤬", + "emoticon": ":@" + }, + { + "group": 0, + "hexcode": "1F608", + "label": "smiling face with horns", + "order": 112, + "tags": [ + "demon", + "devil", + "evil", + "face", + "fairy", + "fairytale", + "fantasy", + "horns", + "purple", + "shade", + "smile", + "smiling", + "tale" + ], + "unicode": "😈", + "emoticon": ">:)" + }, + { + "group": 0, + "hexcode": "1F47F", + "label": "angry face with horns", + "order": 113, + "tags": [ + "angry", + "demon", + "devil", + "evil", + "face", + "fairy", + "fairytale", + "fantasy", + "horns", + "imp", + "mischievous", + "purple", + "shade", + "tale" + ], + "unicode": "👿", + "emoticon": ">:(" + }, + { + "group": 0, + "hexcode": "1F480", + "label": "skull", + "order": 114, + "tags": [ + "body", + "dead", + "death", + "face", + "fairy", + "fairytale", + "i’m", + "lmao", + "monster", + "tale", + "yolo" + ], + "unicode": "💀" + }, + { + "group": 0, + "hexcode": "2620", + "label": "skull and crossbones", + "order": 116, + "tags": [ + "bone", + "crossbones", + "dead", + "death", + "face", + "monster", + "skull" + ], + "unicode": "☠️" + }, + { + "group": 0, + "hexcode": "1F4A9", + "label": "pile of poo", + "order": 117, + "tags": [ + "bs", + "comic", + "doo", + "dung", + "face", + "fml", + "monster", + "pile", + "poo", + "poop", + "smelly", + "smh", + "stink", + "stinks", + "stinky", + "turd" + ], + "unicode": "💩" + }, + { + "group": 0, + "hexcode": "1F921", + "label": "clown face", + "order": 118, + "tags": ["clown", "face"], + "unicode": "🤡" + }, + { + "group": 0, + "hexcode": "1F479", + "label": "ogre", + "order": 119, + "tags": [ + "creature", + "devil", + "face", + "fairy", + "fairytale", + "fantasy", + "mask", + "monster", + "scary", + "tale" + ], + "unicode": "👹", + "emoticon": ">0)" + }, + { + "group": 0, + "hexcode": "1F47A", + "label": "goblin", + "order": 120, + "tags": [ + "angry", + "creature", + "face", + "fairy", + "fairytale", + "fantasy", + "mask", + "mean", + "monster", + "tale" + ], + "unicode": "👺" + }, + { + "group": 0, + "hexcode": "1F47B", + "label": "ghost", + "order": 121, + "tags": [ + "boo", + "creature", + "excited", + "face", + "fairy", + "fairytale", + "fantasy", + "halloween", + "haunting", + "monster", + "scary", + "silly", + "tale" + ], + "unicode": "👻" + }, + { + "group": 0, + "hexcode": "1F47D", + "label": "alien", + "order": 122, + "tags": [ + "creature", + "extraterrestrial", + "face", + "fairy", + "fairytale", + "fantasy", + "monster", + "space", + "tale", + "ufo" + ], + "unicode": "👽️" + }, + { + "group": 0, + "hexcode": "1F47E", + "label": "alien monster", + "order": 123, + "tags": [ + "alien", + "creature", + "extraterrestrial", + "face", + "fairy", + "fairytale", + "fantasy", + "game", + "gamer", + "games", + "monster", + "pixelated", + "space", + "tale", + "ufo" + ], + "unicode": "👾" + }, + { + "group": 0, + "hexcode": "1F916", + "label": "robot", + "order": 124, + "tags": ["face", "monster"], + "unicode": "🤖" + }, + { + "group": 0, + "hexcode": "1F63A", + "label": "grinning cat", + "order": 125, + "tags": [ + "animal", + "cat", + "face", + "grinning", + "mouth", + "open", + "smile", + "smiling" + ], + "unicode": "😺" + }, + { + "group": 0, + "hexcode": "1F638", + "label": "grinning cat with smiling eyes", + "order": 126, + "tags": [ + "animal", + "cat", + "eye", + "eyes", + "face", + "grin", + "grinning", + "smile", + "smiling" + ], + "unicode": "😸" + }, + { + "group": 0, + "hexcode": "1F639", + "label": "cat with tears of joy", + "order": 127, + "tags": [ + "animal", + "cat", + "face", + "joy", + "laugh", + "laughing", + "lol", + "tear", + "tears" + ], + "unicode": "😹" + }, + { + "group": 0, + "hexcode": "1F63B", + "label": "smiling cat with heart-eyes", + "order": 128, + "tags": [ + "animal", + "cat", + "eye", + "face", + "heart", + "heart-eyes", + "love", + "smile", + "smiling" + ], + "unicode": "😻" + }, + { + "group": 0, + "hexcode": "1F63C", + "label": "cat with wry smile", + "order": 129, + "tags": ["animal", "cat", "face", "ironic", "smile", "wry"], + "unicode": "😼" + }, + { + "group": 0, + "hexcode": "1F63D", + "label": "kissing cat", + "order": 130, + "tags": [ + "animal", + "cat", + "closed", + "eye", + "eyes", + "face", + "kiss", + "kissing" + ], + "unicode": "😽", + "emoticon": ":3" + }, + { + "group": 0, + "hexcode": "1F640", + "label": "weary cat", + "order": 131, + "tags": ["animal", "cat", "face", "oh", "surprised", "weary"], + "unicode": "🙀" + }, + { + "group": 0, + "hexcode": "1F63F", + "label": "crying cat", + "order": 132, + "tags": ["animal", "cat", "cry", "crying", "face", "sad", "tear"], + "unicode": "😿" + }, + { + "group": 0, + "hexcode": "1F63E", + "label": "pouting cat", + "order": 133, + "tags": ["animal", "cat", "face", "pouting"], + "unicode": "😾" + }, + { + "group": 0, + "hexcode": "1F648", + "label": "see-no-evil monkey", + "order": 134, + "tags": [ + "embarrassed", + "evil", + "face", + "forbidden", + "forgot", + "gesture", + "hide", + "monkey", + "no", + "omg", + "prohibited", + "scared", + "secret", + "smh", + "watch" + ], + "unicode": "🙈" + }, + { + "group": 0, + "hexcode": "1F649", + "label": "hear-no-evil monkey", + "order": 135, + "tags": [ + "animal", + "ears", + "evil", + "face", + "forbidden", + "gesture", + "hear", + "listen", + "monkey", + "no", + "not", + "prohibited", + "secret", + "shh", + "tmi" + ], + "unicode": "🙉" + }, + { + "group": 0, + "hexcode": "1F64A", + "label": "speak-no-evil monkey", + "order": 136, + "tags": [ + "animal", + "evil", + "face", + "forbidden", + "gesture", + "monkey", + "no", + "not", + "oops", + "prohibited", + "quiet", + "secret", + "speak", + "stealth" + ], + "unicode": "🙊" + }, + { + "group": 0, + "hexcode": "1F48C", + "label": "love letter", + "order": 137, + "tags": ["heart", "letter", "love", "mail", "romance", "valentine"], + "unicode": "💌" + }, + { + "group": 0, + "hexcode": "1F498", + "label": "heart with arrow", + "order": 138, + "tags": [ + "143", + "adorbs", + "arrow", + "cupid", + "date", + "emotion", + "heart", + "ily", + "love", + "romance", + "valentine" + ], + "unicode": "💘" + }, + { + "group": 0, + "hexcode": "1F49D", + "label": "heart with ribbon", + "order": 139, + "tags": [ + "143", + "anniversary", + "emotion", + "heart", + "ily", + "kisses", + "ribbon", + "valentine", + "xoxo" + ], + "unicode": "💝" + }, + { + "group": 0, + "hexcode": "1F496", + "label": "sparkling heart", + "order": 140, + "tags": [ + "143", + "emotion", + "excited", + "good", + "heart", + "ily", + "kisses", + "morning", + "night", + "sparkle", + "sparkling", + "xoxo" + ], + "unicode": "💖" + }, + { + "group": 0, + "hexcode": "1F497", + "label": "growing heart", + "order": 141, + "tags": [ + "143", + "emotion", + "excited", + "growing", + "heart", + "heartpulse", + "ily", + "kisses", + "muah", + "nervous", + "pulse", + "xoxo" + ], + "unicode": "💗" + }, + { + "group": 0, + "hexcode": "1F493", + "label": "beating heart", + "order": 142, + "tags": [ + "143", + "beating", + "cardio", + "emotion", + "heart", + "heartbeat", + "ily", + "love", + "pulsating", + "pulse" + ], + "unicode": "💓" + }, + { + "group": 0, + "hexcode": "1F49E", + "label": "revolving hearts", + "order": 143, + "tags": [ + "143", + "adorbs", + "anniversary", + "emotion", + "heart", + "hearts", + "revolving" + ], + "unicode": "💞" + }, + { + "group": 0, + "hexcode": "1F495", + "label": "two hearts", + "order": 144, + "tags": [ + "143", + "anniversary", + "date", + "dating", + "emotion", + "heart", + "hearts", + "ily", + "kisses", + "love", + "loving", + "two", + "xoxo" + ], + "unicode": "💕" + }, + { + "group": 0, + "hexcode": "1F49F", + "label": "heart decoration", + "order": 145, + "tags": [ + "143", + "decoration", + "emotion", + "heart", + "hearth", + "purple", + "white" + ], + "unicode": "💟" + }, + { + "group": 0, + "hexcode": "2763", + "label": "heart exclamation", + "order": 147, + "tags": ["exclamation", "heart", "heavy", "mark", "punctuation"], + "unicode": "❣️" + }, + { + "group": 0, + "hexcode": "1F494", + "label": "broken heart", + "order": 148, + "tags": [ + "break", + "broken", + "crushed", + "emotion", + "heart", + "heartbroken", + "lonely", + "sad" + ], + "unicode": "💔", + "emoticon": "", + "skins": [ + { + "group": 1, + "hexcode": "1F9D9-1F3FB-200D-2642-FE0F", + "label": "man mage: light skin tone", + "order": 1736, + "unicode": "🧙🏻‍♂️" + }, + { + "group": 1, + "hexcode": "1F9D9-1F3FC-200D-2642-FE0F", + "label": "man mage: medium-light skin tone", + "order": 1738, + "unicode": "🧙🏼‍♂️" + }, + { + "group": 1, + "hexcode": "1F9D9-1F3FD-200D-2642-FE0F", + "label": "man mage: medium skin tone", + "order": 1740, + "unicode": "🧙🏽‍♂️" + }, + { + "group": 1, + "hexcode": "1F9D9-1F3FE-200D-2642-FE0F", + "label": "man mage: medium-dark skin tone", + "order": 1742, + "unicode": "🧙🏾‍♂️" + }, + { + "group": 1, + "hexcode": "1F9D9-1F3FF-200D-2642-FE0F", + "label": "man mage: dark skin tone", + "order": 1744, + "unicode": "🧙🏿‍♂️" + } + ] + }, + { + "group": 1, + "hexcode": "1F9D9-200D-2640-FE0F", + "label": "woman mage", + "order": 1746, + "tags": [ + "fantasy", + "mage", + "magic", + "play", + "sorcerer", + "sorceress", + "sorcery", + "spell", + "summon", + "witch", + "wizard", + "woman" + ], + "unicode": "🧙‍♀️", + "skins": [ + { + "group": 1, + "hexcode": "1F9D9-1F3FB-200D-2640-FE0F", + "label": "woman mage: light skin tone", + "order": 1748, + "unicode": "🧙🏻‍♀️" + }, + { + "group": 1, + "hexcode": "1F9D9-1F3FC-200D-2640-FE0F", + "label": "woman mage: medium-light skin tone", + "order": 1750, + "unicode": "🧙🏼‍♀️" + }, + { + "group": 1, + "hexcode": "1F9D9-1F3FD-200D-2640-FE0F", + "label": "woman mage: medium skin tone", + "order": 1752, + "unicode": "🧙🏽‍♀️" + }, + { + "group": 1, + "hexcode": "1F9D9-1F3FE-200D-2640-FE0F", + "label": "woman mage: medium-dark skin tone", + "order": 1754, + "unicode": "🧙🏾‍♀️" + }, + { + "group": 1, + "hexcode": "1F9D9-1F3FF-200D-2640-FE0F", + "label": "woman mage: dark skin tone", + "order": 1756, + "unicode": "🧙🏿‍♀️" + } + ] + }, + { + "group": 1, + "hexcode": "1F9DA", + "label": "fairy", + "order": 1758, + "tags": [ + "fairytale", + "fantasy", + "myth", + "person", + "pixie", + "tale", + "wings" + ], + "unicode": "🧚", + "skins": [ + { + "group": 1, + "hexcode": "1F9DA-1F3FB", + "label": "fairy: light skin tone", + "order": 1759, + "unicode": "🧚🏻" + }, + { + "group": 1, + "hexcode": "1F9DA-1F3FC", + "label": "fairy: medium-light skin tone", + "order": 1760, + "unicode": "🧚🏼" + }, + { + "group": 1, + "hexcode": "1F9DA-1F3FD", + "label": "fairy: medium skin tone", + "order": 1761, + "unicode": "🧚🏽" + }, + { + "group": 1, + "hexcode": "1F9DA-1F3FE", + "label": "fairy: medium-dark skin tone", + "order": 1762, + "unicode": "🧚🏾" + }, + { + "group": 1, + "hexcode": "1F9DA-1F3FF", + "label": "fairy: dark skin tone", + "order": 1763, + "unicode": "🧚🏿" + } + ] + }, + { + "group": 1, + "hexcode": "1F9DA-200D-2642-FE0F", + "label": "man fairy", + "order": 1764, + "tags": [ + "fairy", + "fairytale", + "fantasy", + "man", + "myth", + "oberon", + "person", + "pixie", + "puck", + "tale", + "wings" + ], + "unicode": "🧚‍♂️", + "skins": [ + { + "group": 1, + "hexcode": "1F9DA-1F3FB-200D-2642-FE0F", + "label": "man fairy: light skin tone", + "order": 1766, + "unicode": "🧚🏻‍♂️" + }, + { + "group": 1, + "hexcode": "1F9DA-1F3FC-200D-2642-FE0F", + "label": "man fairy: medium-light skin tone", + "order": 1768, + "unicode": "🧚🏼‍♂️" + }, + { + "group": 1, + "hexcode": "1F9DA-1F3FD-200D-2642-FE0F", + "label": "man fairy: medium skin tone", + "order": 1770, + "unicode": "🧚🏽‍♂️" + }, + { + "group": 1, + "hexcode": "1F9DA-1F3FE-200D-2642-FE0F", + "label": "man fairy: medium-dark skin tone", + "order": 1772, + "unicode": "🧚🏾‍♂️" + }, + { + "group": 1, + "hexcode": "1F9DA-1F3FF-200D-2642-FE0F", + "label": "man fairy: dark skin tone", + "order": 1774, + "unicode": "🧚🏿‍♂️" + } + ] + }, + { + "group": 1, + "hexcode": "1F9DA-200D-2640-FE0F", + "label": "woman fairy", + "order": 1776, + "tags": [ + "fairy", + "fairytale", + "fantasy", + "myth", + "person", + "pixie", + "tale", + "titania", + "wings", + "woman" + ], + "unicode": "🧚‍♀️", + "skins": [ + { + "group": 1, + "hexcode": "1F9DA-1F3FB-200D-2640-FE0F", + "label": "woman fairy: light skin tone", + "order": 1778, + "unicode": "🧚🏻‍♀️" + }, + { + "group": 1, + "hexcode": "1F9DA-1F3FC-200D-2640-FE0F", + "label": "woman fairy: medium-light skin tone", + "order": 1780, + "unicode": "🧚🏼‍♀️" + }, + { + "group": 1, + "hexcode": "1F9DA-1F3FD-200D-2640-FE0F", + "label": "woman fairy: medium skin tone", + "order": 1782, + "unicode": "🧚🏽‍♀️" + }, + { + "group": 1, + "hexcode": "1F9DA-1F3FE-200D-2640-FE0F", + "label": "woman fairy: medium-dark skin tone", + "order": 1784, + "unicode": "🧚🏾‍♀️" + }, + { + "group": 1, + "hexcode": "1F9DA-1F3FF-200D-2640-FE0F", + "label": "woman fairy: dark skin tone", + "order": 1786, + "unicode": "🧚🏿‍♀️" + } + ] + }, + { + "group": 1, + "hexcode": "1F9DB", + "label": "vampire", + "order": 1788, + "tags": [ + "blood", + "dracula", + "fangs", + "halloween", + "scary", + "supernatural", + "teeth", + "undead" + ], + "unicode": "🧛", + "emoticon": ":E", + "skins": [ + { + "group": 1, + "hexcode": "1F9DB-1F3FB", + "label": "vampire: light skin tone", + "order": 1789, + "unicode": "🧛🏻" + }, + { + "group": 1, + "hexcode": "1F9DB-1F3FC", + "label": "vampire: medium-light skin tone", + "order": 1790, + "unicode": "🧛🏼" + }, + { + "group": 1, + "hexcode": "1F9DB-1F3FD", + "label": "vampire: medium skin tone", + "order": 1791, + "unicode": "🧛🏽" + }, + { + "group": 1, + "hexcode": "1F9DB-1F3FE", + "label": "vampire: medium-dark skin tone", + "order": 1792, + "unicode": "🧛🏾" + }, + { + "group": 1, + "hexcode": "1F9DB-1F3FF", + "label": "vampire: dark skin tone", + "order": 1793, + "unicode": "🧛🏿" + } + ] + }, + { + "group": 1, + "hexcode": "1F9DB-200D-2642-FE0F", + "label": "man vampire", + "order": 1794, + "tags": [ + "blood", + "fangs", + "halloween", + "man", + "scary", + "supernatural", + "teeth", + "undead", + "vampire" + ], + "unicode": "🧛‍♂️", + "skins": [ + { + "group": 1, + "hexcode": "1F9DB-1F3FB-200D-2642-FE0F", + "label": "man vampire: light skin tone", + "order": 1796, + "unicode": "🧛🏻‍♂️" + }, + { + "group": 1, + "hexcode": "1F9DB-1F3FC-200D-2642-FE0F", + "label": "man vampire: medium-light skin tone", + "order": 1798, + "unicode": "🧛🏼‍♂️" + }, + { + "group": 1, + "hexcode": "1F9DB-1F3FD-200D-2642-FE0F", + "label": "man vampire: medium skin tone", + "order": 1800, + "unicode": "🧛🏽‍♂️" + }, + { + "group": 1, + "hexcode": "1F9DB-1F3FE-200D-2642-FE0F", + "label": "man vampire: medium-dark skin tone", + "order": 1802, + "unicode": "🧛🏾‍♂️" + }, + { + "group": 1, + "hexcode": "1F9DB-1F3FF-200D-2642-FE0F", + "label": "man vampire: dark skin tone", + "order": 1804, + "unicode": "🧛🏿‍♂️" + } + ] + }, + { + "group": 1, + "hexcode": "1F9DB-200D-2640-FE0F", + "label": "woman vampire", + "order": 1806, + "tags": [ + "blood", + "fangs", + "halloween", + "scary", + "supernatural", + "teeth", + "undead", + "vampire", + "woman" + ], + "unicode": "🧛‍♀️", + "skins": [ + { + "group": 1, + "hexcode": "1F9DB-1F3FB-200D-2640-FE0F", + "label": "woman vampire: light skin tone", + "order": 1808, + "unicode": "🧛🏻‍♀️" + }, + { + "group": 1, + "hexcode": "1F9DB-1F3FC-200D-2640-FE0F", + "label": "woman vampire: medium-light skin tone", + "order": 1810, + "unicode": "🧛🏼‍♀️" + }, + { + "group": 1, + "hexcode": "1F9DB-1F3FD-200D-2640-FE0F", + "label": "woman vampire: medium skin tone", + "order": 1812, + "unicode": "🧛🏽‍♀️" + }, + { + "group": 1, + "hexcode": "1F9DB-1F3FE-200D-2640-FE0F", + "label": "woman vampire: medium-dark skin tone", + "order": 1814, + "unicode": "🧛🏾‍♀️" + }, + { + "group": 1, + "hexcode": "1F9DB-1F3FF-200D-2640-FE0F", + "label": "woman vampire: dark skin tone", + "order": 1816, + "unicode": "🧛🏿‍♀️" + } + ] + }, + { + "group": 1, + "hexcode": "1F9DC", + "label": "merperson", + "order": 1818, + "tags": [ + "creature", + "fairytale", + "folklore", + "ocean", + "sea", + "siren", + "trident" + ], + "unicode": "🧜", + "skins": [ + { + "group": 1, + "hexcode": "1F9DC-1F3FB", + "label": "merperson: light skin tone", + "order": 1819, + "unicode": "🧜🏻" + }, + { + "group": 1, + "hexcode": "1F9DC-1F3FC", + "label": "merperson: medium-light skin tone", + "order": 1820, + "unicode": "🧜🏼" + }, + { + "group": 1, + "hexcode": "1F9DC-1F3FD", + "label": "merperson: medium skin tone", + "order": 1821, + "unicode": "🧜🏽" + }, + { + "group": 1, + "hexcode": "1F9DC-1F3FE", + "label": "merperson: medium-dark skin tone", + "order": 1822, + "unicode": "🧜🏾" + }, + { + "group": 1, + "hexcode": "1F9DC-1F3FF", + "label": "merperson: dark skin tone", + "order": 1823, + "unicode": "🧜🏿" + } + ] + }, + { + "group": 1, + "hexcode": "1F9DC-200D-2642-FE0F", + "label": "merman", + "order": 1824, + "tags": [ + "creature", + "fairytale", + "folklore", + "neptune", + "ocean", + "poseidon", + "sea", + "siren", + "trident", + "triton" + ], + "unicode": "🧜‍♂️", + "skins": [ + { + "group": 1, + "hexcode": "1F9DC-1F3FB-200D-2642-FE0F", + "label": "merman: light skin tone", + "order": 1826, + "unicode": "🧜🏻‍♂️" + }, + { + "group": 1, + "hexcode": "1F9DC-1F3FC-200D-2642-FE0F", + "label": "merman: medium-light skin tone", + "order": 1828, + "unicode": "🧜🏼‍♂️" + }, + { + "group": 1, + "hexcode": "1F9DC-1F3FD-200D-2642-FE0F", + "label": "merman: medium skin tone", + "order": 1830, + "unicode": "🧜🏽‍♂️" + }, + { + "group": 1, + "hexcode": "1F9DC-1F3FE-200D-2642-FE0F", + "label": "merman: medium-dark skin tone", + "order": 1832, + "unicode": "🧜🏾‍♂️" + }, + { + "group": 1, + "hexcode": "1F9DC-1F3FF-200D-2642-FE0F", + "label": "merman: dark skin tone", + "order": 1834, + "unicode": "🧜🏿‍♂️" + } + ] + }, + { + "group": 1, + "hexcode": "1F9DC-200D-2640-FE0F", + "label": "mermaid", + "order": 1836, + "tags": [ + "creature", + "fairytale", + "folklore", + "merwoman", + "ocean", + "sea", + "siren", + "trident" + ], + "unicode": "🧜‍♀️", + "skins": [ + { + "group": 1, + "hexcode": "1F9DC-1F3FB-200D-2640-FE0F", + "label": "mermaid: light skin tone", + "order": 1838, + "unicode": "🧜🏻‍♀️" + }, + { + "group": 1, + "hexcode": "1F9DC-1F3FC-200D-2640-FE0F", + "label": "mermaid: medium-light skin tone", + "order": 1840, + "unicode": "🧜🏼‍♀️" + }, + { + "group": 1, + "hexcode": "1F9DC-1F3FD-200D-2640-FE0F", + "label": "mermaid: medium skin tone", + "order": 1842, + "unicode": "🧜🏽‍♀️" + }, + { + "group": 1, + "hexcode": "1F9DC-1F3FE-200D-2640-FE0F", + "label": "mermaid: medium-dark skin tone", + "order": 1844, + "unicode": "🧜🏾‍♀️" + }, + { + "group": 1, + "hexcode": "1F9DC-1F3FF-200D-2640-FE0F", + "label": "mermaid: dark skin tone", + "order": 1846, + "unicode": "🧜🏿‍♀️" + } + ] + }, + { + "group": 1, + "hexcode": "1F9DD", + "label": "elf", + "order": 1848, + "tags": [ + "elves", + "enchantment", + "fantasy", + "folklore", + "magic", + "magical", + "myth" + ], + "unicode": "🧝", + "skins": [ + { + "group": 1, + "hexcode": "1F9DD-1F3FB", + "label": "elf: light skin tone", + "order": 1849, + "unicode": "🧝🏻" + }, + { + "group": 1, + "hexcode": "1F9DD-1F3FC", + "label": "elf: medium-light skin tone", + "order": 1850, + "unicode": "🧝🏼" + }, + { + "group": 1, + "hexcode": "1F9DD-1F3FD", + "label": "elf: medium skin tone", + "order": 1851, + "unicode": "🧝🏽" + }, + { + "group": 1, + "hexcode": "1F9DD-1F3FE", + "label": "elf: medium-dark skin tone", + "order": 1852, + "unicode": "🧝🏾" + }, + { + "group": 1, + "hexcode": "1F9DD-1F3FF", + "label": "elf: dark skin tone", + "order": 1853, + "unicode": "🧝🏿" + } + ] + }, + { + "group": 1, + "hexcode": "1F9DD-200D-2642-FE0F", + "label": "man elf", + "order": 1854, + "tags": [ + "elf", + "elves", + "enchantment", + "fantasy", + "folklore", + "magic", + "magical", + "man", + "myth" + ], + "unicode": "🧝‍♂️", + "skins": [ + { + "group": 1, + "hexcode": "1F9DD-1F3FB-200D-2642-FE0F", + "label": "man elf: light skin tone", + "order": 1856, + "unicode": "🧝🏻‍♂️" + }, + { + "group": 1, + "hexcode": "1F9DD-1F3FC-200D-2642-FE0F", + "label": "man elf: medium-light skin tone", + "order": 1858, + "unicode": "🧝🏼‍♂️" + }, + { + "group": 1, + "hexcode": "1F9DD-1F3FD-200D-2642-FE0F", + "label": "man elf: medium skin tone", + "order": 1860, + "unicode": "🧝🏽‍♂️" + }, + { + "group": 1, + "hexcode": "1F9DD-1F3FE-200D-2642-FE0F", + "label": "man elf: medium-dark skin tone", + "order": 1862, + "unicode": "🧝🏾‍♂️" + }, + { + "group": 1, + "hexcode": "1F9DD-1F3FF-200D-2642-FE0F", + "label": "man elf: dark skin tone", + "order": 1864, + "unicode": "🧝🏿‍♂️" + } + ] + }, + { + "group": 1, + "hexcode": "1F9DD-200D-2640-FE0F", + "label": "woman elf", + "order": 1866, + "tags": [ + "elf", + "elves", + "enchantment", + "fantasy", + "folklore", + "magic", + "magical", + "myth", + "woman" + ], + "unicode": "🧝‍♀️", + "skins": [ + { + "group": 1, + "hexcode": "1F9DD-1F3FB-200D-2640-FE0F", + "label": "woman elf: light skin tone", + "order": 1868, + "unicode": "🧝🏻‍♀️" + }, + { + "group": 1, + "hexcode": "1F9DD-1F3FC-200D-2640-FE0F", + "label": "woman elf: medium-light skin tone", + "order": 1870, + "unicode": "🧝🏼‍♀️" + }, + { + "group": 1, + "hexcode": "1F9DD-1F3FD-200D-2640-FE0F", + "label": "woman elf: medium skin tone", + "order": 1872, + "unicode": "🧝🏽‍♀️" + }, + { + "group": 1, + "hexcode": "1F9DD-1F3FE-200D-2640-FE0F", + "label": "woman elf: medium-dark skin tone", + "order": 1874, + "unicode": "🧝🏾‍♀️" + }, + { + "group": 1, + "hexcode": "1F9DD-1F3FF-200D-2640-FE0F", + "label": "woman elf: dark skin tone", + "order": 1876, + "unicode": "🧝🏿‍♀️" + } + ] + }, + { + "group": 1, + "hexcode": "1F9DE", + "label": "genie", + "order": 1878, + "tags": ["djinn", "fantasy", "jinn", "lamp", "myth", "rub", "wishes"], + "unicode": "🧞" + }, + { + "group": 1, + "hexcode": "1F9DE-200D-2642-FE0F", + "label": "man genie", + "order": 1879, + "tags": [ + "djinn", + "fantasy", + "genie", + "jinn", + "lamp", + "man", + "myth", + "rub", + "wishes" + ], + "unicode": "🧞‍♂️" + }, + { + "group": 1, + "hexcode": "1F9DE-200D-2640-FE0F", + "label": "woman genie", + "order": 1881, + "tags": [ + "djinn", + "fantasy", + "genie", + "jinn", + "lamp", + "myth", + "rub", + "wishes", + "woman" + ], + "unicode": "🧞‍♀️" + }, + { + "group": 1, + "hexcode": "1F9DF", + "label": "zombie", + "order": 1883, + "tags": [ + "apocalypse", + "dead", + "halloween", + "horror", + "scary", + "undead", + "walking" + ], + "unicode": "🧟", + "emoticon": "8#" + }, + { + "group": 1, + "hexcode": "1F9DF-200D-2642-FE0F", + "label": "man zombie", + "order": 1884, + "tags": [ + "apocalypse", + "dead", + "halloween", + "horror", + "man", + "scary", + "undead", + "walking", + "zombie" + ], + "unicode": "🧟‍♂️" + }, + { + "group": 1, + "hexcode": "1F9DF-200D-2640-FE0F", + "label": "woman zombie", + "order": 1886, + "tags": [ + "apocalypse", + "dead", + "halloween", + "horror", + "scary", + "undead", + "walking", + "woman", + "zombie" + ], + "unicode": "🧟‍♀️" + }, + { + "group": 1, + "hexcode": "1F9CC", + "label": "troll", + "order": 1888, + "tags": ["fairy", "fantasy", "monster", "tale", "trolling"], + "unicode": "🧌" + }, + { + "group": 1, + "hexcode": "1F486", + "label": "person getting massage", + "order": 1889, + "tags": [ + "face", + "getting", + "headache", + "massage", + "person", + "relax", + "relaxing", + "salon", + "soothe", + "spa", + "tension", + "therapy", + "treatment" + ], + "unicode": "💆", + "skins": [ + { + "group": 1, + "hexcode": "1F486-1F3FB", + "label": "person getting massage: light skin tone", + "order": 1890, + "unicode": "💆🏻" + }, + { + "group": 1, + "hexcode": "1F486-1F3FC", + "label": "person getting massage: medium-light skin tone", + "order": 1891, + "unicode": "💆🏼" + }, + { + "group": 1, + "hexcode": "1F486-1F3FD", + "label": "person getting massage: medium skin tone", + "order": 1892, + "unicode": "💆🏽" + }, + { + "group": 1, + "hexcode": "1F486-1F3FE", + "label": "person getting massage: medium-dark skin tone", + "order": 1893, + "unicode": "💆🏾" + }, + { + "group": 1, + "hexcode": "1F486-1F3FF", + "label": "person getting massage: dark skin tone", + "order": 1894, + "unicode": "💆🏿" + } + ] + }, + { + "group": 1, + "hexcode": "1F486-200D-2642-FE0F", + "label": "man getting massage", + "order": 1895, + "tags": [ + "face", + "getting", + "headache", + "man", + "massage", + "relax", + "relaxing", + "salon", + "soothe", + "spa", + "tension", + "therapy", + "treatment" + ], + "unicode": "💆‍♂️", + "skins": [ + { + "group": 1, + "hexcode": "1F486-1F3FB-200D-2642-FE0F", + "label": "man getting massage: light skin tone", + "order": 1897, + "unicode": "💆🏻‍♂️" + }, + { + "group": 1, + "hexcode": "1F486-1F3FC-200D-2642-FE0F", + "label": "man getting massage: medium-light skin tone", + "order": 1899, + "unicode": "💆🏼‍♂️" + }, + { + "group": 1, + "hexcode": "1F486-1F3FD-200D-2642-FE0F", + "label": "man getting massage: medium skin tone", + "order": 1901, + "unicode": "💆🏽‍♂️" + }, + { + "group": 1, + "hexcode": "1F486-1F3FE-200D-2642-FE0F", + "label": "man getting massage: medium-dark skin tone", + "order": 1903, + "unicode": "💆🏾‍♂️" + }, + { + "group": 1, + "hexcode": "1F486-1F3FF-200D-2642-FE0F", + "label": "man getting massage: dark skin tone", + "order": 1905, + "unicode": "💆🏿‍♂️" + } + ] + }, + { + "group": 1, + "hexcode": "1F486-200D-2640-FE0F", + "label": "woman getting massage", + "order": 1907, + "tags": [ + "face", + "getting", + "headache", + "massage", + "relax", + "relaxing", + "salon", + "soothe", + "spa", + "tension", + "therapy", + "treatment", + "woman" + ], + "unicode": "💆‍♀️", + "skins": [ + { + "group": 1, + "hexcode": "1F486-1F3FB-200D-2640-FE0F", + "label": "woman getting massage: light skin tone", + "order": 1909, + "unicode": "💆🏻‍♀️" + }, + { + "group": 1, + "hexcode": "1F486-1F3FC-200D-2640-FE0F", + "label": "woman getting massage: medium-light skin tone", + "order": 1911, + "unicode": "💆🏼‍♀️" + }, + { + "group": 1, + "hexcode": "1F486-1F3FD-200D-2640-FE0F", + "label": "woman getting massage: medium skin tone", + "order": 1913, + "unicode": "💆🏽‍♀️" + }, + { + "group": 1, + "hexcode": "1F486-1F3FE-200D-2640-FE0F", + "label": "woman getting massage: medium-dark skin tone", + "order": 1915, + "unicode": "💆🏾‍♀️" + }, + { + "group": 1, + "hexcode": "1F486-1F3FF-200D-2640-FE0F", + "label": "woman getting massage: dark skin tone", + "order": 1917, + "unicode": "💆🏿‍♀️" + } + ] + }, + { + "group": 1, + "hexcode": "1F487", + "label": "person getting haircut", + "order": 1919, + "tags": [ + "barber", + "beauty", + "chop", + "cosmetology", + "cut", + "groom", + "hair", + "haircut", + "parlor", + "person", + "shears", + "style" + ], + "unicode": "💇", + "skins": [ + { + "group": 1, + "hexcode": "1F487-1F3FB", + "label": "person getting haircut: light skin tone", + "order": 1920, + "unicode": "💇🏻" + }, + { + "group": 1, + "hexcode": "1F487-1F3FC", + "label": "person getting haircut: medium-light skin tone", + "order": 1921, + "unicode": "💇🏼" + }, + { + "group": 1, + "hexcode": "1F487-1F3FD", + "label": "person getting haircut: medium skin tone", + "order": 1922, + "unicode": "💇🏽" + }, + { + "group": 1, + "hexcode": "1F487-1F3FE", + "label": "person getting haircut: medium-dark skin tone", + "order": 1923, + "unicode": "💇🏾" + }, + { + "group": 1, + "hexcode": "1F487-1F3FF", + "label": "person getting haircut: dark skin tone", + "order": 1924, + "unicode": "💇🏿" + } + ] + }, + { + "group": 1, + "hexcode": "1F487-200D-2642-FE0F", + "label": "man getting haircut", + "order": 1925, + "tags": [ + "barber", + "beauty", + "chop", + "cosmetology", + "cut", + "groom", + "hair", + "haircut", + "man", + "parlor", + "person", + "shears", + "style" + ], + "unicode": "💇‍♂️", + "skins": [ + { + "group": 1, + "hexcode": "1F487-1F3FB-200D-2642-FE0F", + "label": "man getting haircut: light skin tone", + "order": 1927, + "unicode": "💇🏻‍♂️" + }, + { + "group": 1, + "hexcode": "1F487-1F3FC-200D-2642-FE0F", + "label": "man getting haircut: medium-light skin tone", + "order": 1929, + "unicode": "💇🏼‍♂️" + }, + { + "group": 1, + "hexcode": "1F487-1F3FD-200D-2642-FE0F", + "label": "man getting haircut: medium skin tone", + "order": 1931, + "unicode": "💇🏽‍♂️" + }, + { + "group": 1, + "hexcode": "1F487-1F3FE-200D-2642-FE0F", + "label": "man getting haircut: medium-dark skin tone", + "order": 1933, + "unicode": "💇🏾‍♂️" + }, + { + "group": 1, + "hexcode": "1F487-1F3FF-200D-2642-FE0F", + "label": "man getting haircut: dark skin tone", + "order": 1935, + "unicode": "💇🏿‍♂️" + } + ] + }, + { + "group": 1, + "hexcode": "1F487-200D-2640-FE0F", + "label": "woman getting haircut", + "order": 1937, + "tags": [ + "barber", + "beauty", + "chop", + "cosmetology", + "cut", + "groom", + "hair", + "haircut", + "parlor", + "person", + "shears", + "style", + "woman" + ], + "unicode": "💇‍♀️", + "skins": [ + { + "group": 1, + "hexcode": "1F487-1F3FB-200D-2640-FE0F", + "label": "woman getting haircut: light skin tone", + "order": 1939, + "unicode": "💇🏻‍♀️" + }, + { + "group": 1, + "hexcode": "1F487-1F3FC-200D-2640-FE0F", + "label": "woman getting haircut: medium-light skin tone", + "order": 1941, + "unicode": "💇🏼‍♀️" + }, + { + "group": 1, + "hexcode": "1F487-1F3FD-200D-2640-FE0F", + "label": "woman getting haircut: medium skin tone", + "order": 1943, + "unicode": "💇🏽‍♀️" + }, + { + "group": 1, + "hexcode": "1F487-1F3FE-200D-2640-FE0F", + "label": "woman getting haircut: medium-dark skin tone", + "order": 1945, + "unicode": "💇🏾‍♀️" + }, + { + "group": 1, + "hexcode": "1F487-1F3FF-200D-2640-FE0F", + "label": "woman getting haircut: dark skin tone", + "order": 1947, + "unicode": "💇🏿‍♀️" + } + ] + }, + { + "group": 1, + "hexcode": "1F6B6", + "label": "person walking", + "order": 1949, + "tags": [ + "amble", + "gait", + "hike", + "man", + "pace", + "pedestrian", + "person", + "stride", + "stroll", + "walk", + "walking" + ], + "unicode": "🚶", + "skins": [ + { + "group": 1, + "hexcode": "1F6B6-1F3FB", + "label": "person walking: light skin tone", + "order": 1950, + "unicode": "🚶🏻" + }, + { + "group": 1, + "hexcode": "1F6B6-1F3FC", + "label": "person walking: medium-light skin tone", + "order": 1951, + "unicode": "🚶🏼" + }, + { + "group": 1, + "hexcode": "1F6B6-1F3FD", + "label": "person walking: medium skin tone", + "order": 1952, + "unicode": "🚶🏽" + }, + { + "group": 1, + "hexcode": "1F6B6-1F3FE", + "label": "person walking: medium-dark skin tone", + "order": 1953, + "unicode": "🚶🏾" + }, + { + "group": 1, + "hexcode": "1F6B6-1F3FF", + "label": "person walking: dark skin tone", + "order": 1954, + "unicode": "🚶🏿" + } + ] + }, + { + "group": 1, + "hexcode": "1F6B6-200D-2642-FE0F", + "label": "man walking", + "order": 1955, + "tags": [ + "amble", + "gait", + "hike", + "man", + "pace", + "pedestrian", + "stride", + "stroll", + "walk", + "walking" + ], + "unicode": "🚶‍♂️", + "skins": [ + { + "group": 1, + "hexcode": "1F6B6-1F3FB-200D-2642-FE0F", + "label": "man walking: light skin tone", + "order": 1957, + "unicode": "🚶🏻‍♂️" + }, + { + "group": 1, + "hexcode": "1F6B6-1F3FC-200D-2642-FE0F", + "label": "man walking: medium-light skin tone", + "order": 1959, + "unicode": "🚶🏼‍♂️" + }, + { + "group": 1, + "hexcode": "1F6B6-1F3FD-200D-2642-FE0F", + "label": "man walking: medium skin tone", + "order": 1961, + "unicode": "🚶🏽‍♂️" + }, + { + "group": 1, + "hexcode": "1F6B6-1F3FE-200D-2642-FE0F", + "label": "man walking: medium-dark skin tone", + "order": 1963, + "unicode": "🚶🏾‍♂️" + }, + { + "group": 1, + "hexcode": "1F6B6-1F3FF-200D-2642-FE0F", + "label": "man walking: dark skin tone", + "order": 1965, + "unicode": "🚶🏿‍♂️" + } + ] + }, + { + "group": 1, + "hexcode": "1F6B6-200D-2640-FE0F", + "label": "woman walking", + "order": 1967, + "tags": [ + "amble", + "gait", + "hike", + "man", + "pace", + "pedestrian", + "stride", + "stroll", + "walk", + "walking", + "woman" + ], + "unicode": "🚶‍♀️", + "skins": [ + { + "group": 1, + "hexcode": "1F6B6-1F3FB-200D-2640-FE0F", + "label": "woman walking: light skin tone", + "order": 1969, + "unicode": "🚶🏻‍♀️" + }, + { + "group": 1, + "hexcode": "1F6B6-1F3FC-200D-2640-FE0F", + "label": "woman walking: medium-light skin tone", + "order": 1971, + "unicode": "🚶🏼‍♀️" + }, + { + "group": 1, + "hexcode": "1F6B6-1F3FD-200D-2640-FE0F", + "label": "woman walking: medium skin tone", + "order": 1973, + "unicode": "🚶🏽‍♀️" + }, + { + "group": 1, + "hexcode": "1F6B6-1F3FE-200D-2640-FE0F", + "label": "woman walking: medium-dark skin tone", + "order": 1975, + "unicode": "🚶🏾‍♀️" + }, + { + "group": 1, + "hexcode": "1F6B6-1F3FF-200D-2640-FE0F", + "label": "woman walking: dark skin tone", + "order": 1977, + "unicode": "🚶🏿‍♀️" + } + ] + }, + { + "group": 1, + "hexcode": "1F6B6-200D-27A1-FE0F", + "label": "person walking facing right", + "order": 1979, + "tags": [ + "amble", + "gait", + "hike", + "man", + "pace", + "pedestrian", + "person", + "stride", + "stroll", + "walk", + "walking" + ], + "unicode": "🚶‍➡️", + "skins": [ + { + "group": 1, + "hexcode": "1F6B6-1F3FB-200D-27A1-FE0F", + "label": "person walking facing right", + "order": 1981, + "unicode": "🚶🏻‍➡️" + }, + { + "group": 1, + "hexcode": "1F6B6-1F3FC-200D-27A1-FE0F", + "label": "person walking facing right", + "order": 1983, + "unicode": "🚶🏼‍➡️" + }, + { + "group": 1, + "hexcode": "1F6B6-1F3FD-200D-27A1-FE0F", + "label": "person walking facing right", + "order": 1985, + "unicode": "🚶🏽‍➡️" + }, + { + "group": 1, + "hexcode": "1F6B6-1F3FE-200D-27A1-FE0F", + "label": "person walking facing right", + "order": 1987, + "unicode": "🚶🏾‍➡️" + }, + { + "group": 1, + "hexcode": "1F6B6-1F3FF-200D-27A1-FE0F", + "label": "person walking facing right", + "order": 1989, + "unicode": "🚶🏿‍➡️" + } + ] + }, + { + "group": 1, + "hexcode": "1F6B6-200D-2640-FE0F-200D-27A1-FE0F", + "label": "woman walking facing right", + "order": 1991, + "tags": [ + "amble", + "gait", + "hike", + "man", + "pace", + "pedestrian", + "stride", + "stroll", + "walk", + "walking", + "woman" + ], + "unicode": "🚶‍♀️‍➡️", + "skins": [ + { + "group": 1, + "hexcode": "1F6B6-1F3FB-200D-2640-FE0F-200D-27A1-FE0F", + "label": "woman walking facing right", + "order": 1995, + "unicode": "🚶🏻‍♀️‍➡️" + }, + { + "group": 1, + "hexcode": "1F6B6-1F3FC-200D-2640-FE0F-200D-27A1-FE0F", + "label": "woman walking facing right", + "order": 1999, + "unicode": "🚶🏼‍♀️‍➡️" + }, + { + "group": 1, + "hexcode": "1F6B6-1F3FD-200D-2640-FE0F-200D-27A1-FE0F", + "label": "woman walking facing right", + "order": 2003, + "unicode": "🚶🏽‍♀️‍➡️" + }, + { + "group": 1, + "hexcode": "1F6B6-1F3FE-200D-2640-FE0F-200D-27A1-FE0F", + "label": "woman walking facing right", + "order": 2007, + "unicode": "🚶🏾‍♀️‍➡️" + }, + { + "group": 1, + "hexcode": "1F6B6-1F3FF-200D-2640-FE0F-200D-27A1-FE0F", + "label": "woman walking facing right", + "order": 2011, + "unicode": "🚶🏿‍♀️‍➡️" + } + ] + }, + { + "group": 1, + "hexcode": "1F6B6-200D-2642-FE0F-200D-27A1-FE0F", + "label": "man walking facing right", + "order": 2015, + "tags": [ + "amble", + "gait", + "hike", + "man", + "pace", + "pedestrian", + "stride", + "stroll", + "walk", + "walking" + ], + "unicode": "🚶‍♂️‍➡️", + "skins": [ + { + "group": 1, + "hexcode": "1F6B6-1F3FB-200D-2642-FE0F-200D-27A1-FE0F", + "label": "man walking facing right", + "order": 2019, + "unicode": "🚶🏻‍♂️‍➡️" + }, + { + "group": 1, + "hexcode": "1F6B6-1F3FC-200D-2642-FE0F-200D-27A1-FE0F", + "label": "man walking facing right", + "order": 2023, + "unicode": "🚶🏼‍♂️‍➡️" + }, + { + "group": 1, + "hexcode": "1F6B6-1F3FD-200D-2642-FE0F-200D-27A1-FE0F", + "label": "man walking facing right", + "order": 2027, + "unicode": "🚶🏽‍♂️‍➡️" + }, + { + "group": 1, + "hexcode": "1F6B6-1F3FE-200D-2642-FE0F-200D-27A1-FE0F", + "label": "man walking facing right", + "order": 2031, + "unicode": "🚶🏾‍♂️‍➡️" + }, + { + "group": 1, + "hexcode": "1F6B6-1F3FF-200D-2642-FE0F-200D-27A1-FE0F", + "label": "man walking facing right", + "order": 2035, + "unicode": "🚶🏿‍♂️‍➡️" + } + ] + }, + { + "group": 1, + "hexcode": "1F9CD", + "label": "person standing", + "order": 2039, + "tags": ["person", "stand", "standing"], + "unicode": "🧍", + "skins": [ + { + "group": 1, + "hexcode": "1F9CD-1F3FB", + "label": "person standing: light skin tone", + "order": 2040, + "unicode": "🧍🏻" + }, + { + "group": 1, + "hexcode": "1F9CD-1F3FC", + "label": "person standing: medium-light skin tone", + "order": 2041, + "unicode": "🧍🏼" + }, + { + "group": 1, + "hexcode": "1F9CD-1F3FD", + "label": "person standing: medium skin tone", + "order": 2042, + "unicode": "🧍🏽" + }, + { + "group": 1, + "hexcode": "1F9CD-1F3FE", + "label": "person standing: medium-dark skin tone", + "order": 2043, + "unicode": "🧍🏾" + }, + { + "group": 1, + "hexcode": "1F9CD-1F3FF", + "label": "person standing: dark skin tone", + "order": 2044, + "unicode": "🧍🏿" + } + ] + }, + { + "group": 1, + "hexcode": "1F9CD-200D-2642-FE0F", + "label": "man standing", + "order": 2045, + "tags": ["man", "stand", "standing"], + "unicode": "🧍‍♂️", + "skins": [ + { + "group": 1, + "hexcode": "1F9CD-1F3FB-200D-2642-FE0F", + "label": "man standing: light skin tone", + "order": 2047, + "unicode": "🧍🏻‍♂️" + }, + { + "group": 1, + "hexcode": "1F9CD-1F3FC-200D-2642-FE0F", + "label": "man standing: medium-light skin tone", + "order": 2049, + "unicode": "🧍🏼‍♂️" + }, + { + "group": 1, + "hexcode": "1F9CD-1F3FD-200D-2642-FE0F", + "label": "man standing: medium skin tone", + "order": 2051, + "unicode": "🧍🏽‍♂️" + }, + { + "group": 1, + "hexcode": "1F9CD-1F3FE-200D-2642-FE0F", + "label": "man standing: medium-dark skin tone", + "order": 2053, + "unicode": "🧍🏾‍♂️" + }, + { + "group": 1, + "hexcode": "1F9CD-1F3FF-200D-2642-FE0F", + "label": "man standing: dark skin tone", + "order": 2055, + "unicode": "🧍🏿‍♂️" + } + ] + }, + { + "group": 1, + "hexcode": "1F9CD-200D-2640-FE0F", + "label": "woman standing", + "order": 2057, + "tags": ["stand", "standing", "woman"], + "unicode": "🧍‍♀️", + "skins": [ + { + "group": 1, + "hexcode": "1F9CD-1F3FB-200D-2640-FE0F", + "label": "woman standing: light skin tone", + "order": 2059, + "unicode": "🧍🏻‍♀️" + }, + { + "group": 1, + "hexcode": "1F9CD-1F3FC-200D-2640-FE0F", + "label": "woman standing: medium-light skin tone", + "order": 2061, + "unicode": "🧍🏼‍♀️" + }, + { + "group": 1, + "hexcode": "1F9CD-1F3FD-200D-2640-FE0F", + "label": "woman standing: medium skin tone", + "order": 2063, + "unicode": "🧍🏽‍♀️" + }, + { + "group": 1, + "hexcode": "1F9CD-1F3FE-200D-2640-FE0F", + "label": "woman standing: medium-dark skin tone", + "order": 2065, + "unicode": "🧍🏾‍♀️" + }, + { + "group": 1, + "hexcode": "1F9CD-1F3FF-200D-2640-FE0F", + "label": "woman standing: dark skin tone", + "order": 2067, + "unicode": "🧍🏿‍♀️" + } + ] + }, + { + "group": 1, + "hexcode": "1F9CE", + "label": "person kneeling", + "order": 2069, + "tags": ["kneel", "kneeling", "knees", "person"], + "unicode": "🧎", + "skins": [ + { + "group": 1, + "hexcode": "1F9CE-1F3FB", + "label": "person kneeling: light skin tone", + "order": 2070, + "unicode": "🧎🏻" + }, + { + "group": 1, + "hexcode": "1F9CE-1F3FC", + "label": "person kneeling: medium-light skin tone", + "order": 2071, + "unicode": "🧎🏼" + }, + { + "group": 1, + "hexcode": "1F9CE-1F3FD", + "label": "person kneeling: medium skin tone", + "order": 2072, + "unicode": "🧎🏽" + }, + { + "group": 1, + "hexcode": "1F9CE-1F3FE", + "label": "person kneeling: medium-dark skin tone", + "order": 2073, + "unicode": "🧎🏾" + }, + { + "group": 1, + "hexcode": "1F9CE-1F3FF", + "label": "person kneeling: dark skin tone", + "order": 2074, + "unicode": "🧎🏿" + } + ] + }, + { + "group": 1, + "hexcode": "1F9CE-200D-2642-FE0F", + "label": "man kneeling", + "order": 2075, + "tags": ["kneel", "kneeling", "knees", "man"], + "unicode": "🧎‍♂️", + "skins": [ + { + "group": 1, + "hexcode": "1F9CE-1F3FB-200D-2642-FE0F", + "label": "man kneeling: light skin tone", + "order": 2077, + "unicode": "🧎🏻‍♂️" + }, + { + "group": 1, + "hexcode": "1F9CE-1F3FC-200D-2642-FE0F", + "label": "man kneeling: medium-light skin tone", + "order": 2079, + "unicode": "🧎🏼‍♂️" + }, + { + "group": 1, + "hexcode": "1F9CE-1F3FD-200D-2642-FE0F", + "label": "man kneeling: medium skin tone", + "order": 2081, + "unicode": "🧎🏽‍♂️" + }, + { + "group": 1, + "hexcode": "1F9CE-1F3FE-200D-2642-FE0F", + "label": "man kneeling: medium-dark skin tone", + "order": 2083, + "unicode": "🧎🏾‍♂️" + }, + { + "group": 1, + "hexcode": "1F9CE-1F3FF-200D-2642-FE0F", + "label": "man kneeling: dark skin tone", + "order": 2085, + "unicode": "🧎🏿‍♂️" + } + ] + }, + { + "group": 1, + "hexcode": "1F9CE-200D-2640-FE0F", + "label": "woman kneeling", + "order": 2087, + "tags": ["kneel", "kneeling", "knees", "woman"], + "unicode": "🧎‍♀️", + "skins": [ + { + "group": 1, + "hexcode": "1F9CE-1F3FB-200D-2640-FE0F", + "label": "woman kneeling: light skin tone", + "order": 2089, + "unicode": "🧎🏻‍♀️" + }, + { + "group": 1, + "hexcode": "1F9CE-1F3FC-200D-2640-FE0F", + "label": "woman kneeling: medium-light skin tone", + "order": 2091, + "unicode": "🧎🏼‍♀️" + }, + { + "group": 1, + "hexcode": "1F9CE-1F3FD-200D-2640-FE0F", + "label": "woman kneeling: medium skin tone", + "order": 2093, + "unicode": "🧎🏽‍♀️" + }, + { + "group": 1, + "hexcode": "1F9CE-1F3FE-200D-2640-FE0F", + "label": "woman kneeling: medium-dark skin tone", + "order": 2095, + "unicode": "🧎🏾‍♀️" + }, + { + "group": 1, + "hexcode": "1F9CE-1F3FF-200D-2640-FE0F", + "label": "woman kneeling: dark skin tone", + "order": 2097, + "unicode": "🧎🏿‍♀️" + } + ] + }, + { + "group": 1, + "hexcode": "1F9CE-200D-27A1-FE0F", + "label": "person kneeling facing right", + "order": 2099, + "tags": ["kneel", "kneeling", "knees", "person"], + "unicode": "🧎‍➡️", + "skins": [ + { + "group": 1, + "hexcode": "1F9CE-1F3FB-200D-27A1-FE0F", + "label": "person kneeling facing right", + "order": 2101, + "unicode": "🧎🏻‍➡️" + }, + { + "group": 1, + "hexcode": "1F9CE-1F3FC-200D-27A1-FE0F", + "label": "person kneeling facing right", + "order": 2103, + "unicode": "🧎🏼‍➡️" + }, + { + "group": 1, + "hexcode": "1F9CE-1F3FD-200D-27A1-FE0F", + "label": "person kneeling facing right", + "order": 2105, + "unicode": "🧎🏽‍➡️" + }, + { + "group": 1, + "hexcode": "1F9CE-1F3FE-200D-27A1-FE0F", + "label": "person kneeling facing right", + "order": 2107, + "unicode": "🧎🏾‍➡️" + }, + { + "group": 1, + "hexcode": "1F9CE-1F3FF-200D-27A1-FE0F", + "label": "person kneeling facing right", + "order": 2109, + "unicode": "🧎🏿‍➡️" + } + ] + }, + { + "group": 1, + "hexcode": "1F9CE-200D-2640-FE0F-200D-27A1-FE0F", + "label": "woman kneeling facing right", + "order": 2111, + "tags": ["kneel", "kneeling", "knees", "woman"], + "unicode": "🧎‍♀️‍➡️", + "skins": [ + { + "group": 1, + "hexcode": "1F9CE-1F3FB-200D-2640-FE0F-200D-27A1-FE0F", + "label": "woman kneeling facing right", + "order": 2115, + "unicode": "🧎🏻‍♀️‍➡️" + }, + { + "group": 1, + "hexcode": "1F9CE-1F3FC-200D-2640-FE0F-200D-27A1-FE0F", + "label": "woman kneeling facing right", + "order": 2119, + "unicode": "🧎🏼‍♀️‍➡️" + }, + { + "group": 1, + "hexcode": "1F9CE-1F3FD-200D-2640-FE0F-200D-27A1-FE0F", + "label": "woman kneeling facing right", + "order": 2123, + "unicode": "🧎🏽‍♀️‍➡️" + }, + { + "group": 1, + "hexcode": "1F9CE-1F3FE-200D-2640-FE0F-200D-27A1-FE0F", + "label": "woman kneeling facing right", + "order": 2127, + "unicode": "🧎🏾‍♀️‍➡️" + }, + { + "group": 1, + "hexcode": "1F9CE-1F3FF-200D-2640-FE0F-200D-27A1-FE0F", + "label": "woman kneeling facing right", + "order": 2131, + "unicode": "🧎🏿‍♀️‍➡️" + } + ] + }, + { + "group": 1, + "hexcode": "1F9CE-200D-2642-FE0F-200D-27A1-FE0F", + "label": "man kneeling facing right", + "order": 2135, + "tags": ["kneel", "kneeling", "knees", "man"], + "unicode": "🧎‍♂️‍➡️", + "skins": [ + { + "group": 1, + "hexcode": "1F9CE-1F3FB-200D-2642-FE0F-200D-27A1-FE0F", + "label": "man kneeling facing right", + "order": 2139, + "unicode": "🧎🏻‍♂️‍➡️" + }, + { + "group": 1, + "hexcode": "1F9CE-1F3FC-200D-2642-FE0F-200D-27A1-FE0F", + "label": "man kneeling facing right", + "order": 2143, + "unicode": "🧎🏼‍♂️‍➡️" + }, + { + "group": 1, + "hexcode": "1F9CE-1F3FD-200D-2642-FE0F-200D-27A1-FE0F", + "label": "man kneeling facing right", + "order": 2147, + "unicode": "🧎🏽‍♂️‍➡️" + }, + { + "group": 1, + "hexcode": "1F9CE-1F3FE-200D-2642-FE0F-200D-27A1-FE0F", + "label": "man kneeling facing right", + "order": 2151, + "unicode": "🧎🏾‍♂️‍➡️" + }, + { + "group": 1, + "hexcode": "1F9CE-1F3FF-200D-2642-FE0F-200D-27A1-FE0F", + "label": "man kneeling facing right", + "order": 2155, + "unicode": "🧎🏿‍♂️‍➡️" + } + ] + }, + { + "group": 1, + "hexcode": "1F9D1-200D-1F9AF", + "label": "person with white cane", + "order": 2159, + "tags": [ + "accessibility", + "blind", + "cane", + "person", + "probing", + "white" + ], + "unicode": "🧑‍🦯", + "skins": [ + { + "group": 1, + "hexcode": "1F9D1-1F3FB-200D-1F9AF", + "label": "person with white cane: light skin tone", + "order": 2160, + "unicode": "🧑🏻‍🦯" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FC-200D-1F9AF", + "label": "person with white cane: medium-light skin tone", + "order": 2161, + "unicode": "🧑🏼‍🦯" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FD-200D-1F9AF", + "label": "person with white cane: medium skin tone", + "order": 2162, + "unicode": "🧑🏽‍🦯" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FE-200D-1F9AF", + "label": "person with white cane: medium-dark skin tone", + "order": 2163, + "unicode": "🧑🏾‍🦯" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FF-200D-1F9AF", + "label": "person with white cane: dark skin tone", + "order": 2164, + "unicode": "🧑🏿‍🦯" + } + ] + }, + { + "group": 1, + "hexcode": "1F9D1-200D-1F9AF-200D-27A1-FE0F", + "label": "person with white cane facing right", + "order": 2165, + "tags": [ + "accessibility", + "blind", + "cane", + "person", + "probing", + "white" + ], + "unicode": "🧑‍🦯‍➡️", + "skins": [ + { + "group": 1, + "hexcode": "1F9D1-1F3FB-200D-1F9AF-200D-27A1-FE0F", + "label": "person with white cane facing right", + "order": 2167, + "unicode": "🧑🏻‍🦯‍➡️" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FC-200D-1F9AF-200D-27A1-FE0F", + "label": "person with white cane facing right", + "order": 2169, + "unicode": "🧑🏼‍🦯‍➡️" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FD-200D-1F9AF-200D-27A1-FE0F", + "label": "person with white cane facing right", + "order": 2171, + "unicode": "🧑🏽‍🦯‍➡️" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FE-200D-1F9AF-200D-27A1-FE0F", + "label": "person with white cane facing right", + "order": 2173, + "unicode": "🧑🏾‍🦯‍➡️" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FF-200D-1F9AF-200D-27A1-FE0F", + "label": "person with white cane facing right", + "order": 2175, + "unicode": "🧑🏿‍🦯‍➡️" + } + ] + }, + { + "group": 1, + "hexcode": "1F468-200D-1F9AF", + "label": "man with white cane", + "order": 2177, + "tags": ["accessibility", "blind", "cane", "man", "probing", "white"], + "unicode": "👨‍🦯", + "skins": [ + { + "group": 1, + "hexcode": "1F468-1F3FB-200D-1F9AF", + "label": "man with white cane: light skin tone", + "order": 2178, + "unicode": "👨🏻‍🦯" + }, + { + "group": 1, + "hexcode": "1F468-1F3FC-200D-1F9AF", + "label": "man with white cane: medium-light skin tone", + "order": 2179, + "unicode": "👨🏼‍🦯" + }, + { + "group": 1, + "hexcode": "1F468-1F3FD-200D-1F9AF", + "label": "man with white cane: medium skin tone", + "order": 2180, + "unicode": "👨🏽‍🦯" + }, + { + "group": 1, + "hexcode": "1F468-1F3FE-200D-1F9AF", + "label": "man with white cane: medium-dark skin tone", + "order": 2181, + "unicode": "👨🏾‍🦯" + }, + { + "group": 1, + "hexcode": "1F468-1F3FF-200D-1F9AF", + "label": "man with white cane: dark skin tone", + "order": 2182, + "unicode": "👨🏿‍🦯" + } + ] + }, + { + "group": 1, + "hexcode": "1F468-200D-1F9AF-200D-27A1-FE0F", + "label": "man with white cane facing right", + "order": 2183, + "tags": ["accessibility", "blind", "cane", "man", "probing", "white"], + "unicode": "👨‍🦯‍➡️", + "skins": [ + { + "group": 1, + "hexcode": "1F468-1F3FB-200D-1F9AF-200D-27A1-FE0F", + "label": "man with white cane facing right", + "order": 2185, + "unicode": "👨🏻‍🦯‍➡️" + }, + { + "group": 1, + "hexcode": "1F468-1F3FC-200D-1F9AF-200D-27A1-FE0F", + "label": "man with white cane facing right", + "order": 2187, + "unicode": "👨🏼‍🦯‍➡️" + }, + { + "group": 1, + "hexcode": "1F468-1F3FD-200D-1F9AF-200D-27A1-FE0F", + "label": "man with white cane facing right", + "order": 2189, + "unicode": "👨🏽‍🦯‍➡️" + }, + { + "group": 1, + "hexcode": "1F468-1F3FE-200D-1F9AF-200D-27A1-FE0F", + "label": "man with white cane facing right", + "order": 2191, + "unicode": "👨🏾‍🦯‍➡️" + }, + { + "group": 1, + "hexcode": "1F468-1F3FF-200D-1F9AF-200D-27A1-FE0F", + "label": "man with white cane facing right", + "order": 2193, + "unicode": "👨🏿‍🦯‍➡️" + } + ] + }, + { + "group": 1, + "hexcode": "1F469-200D-1F9AF", + "label": "woman with white cane", + "order": 2195, + "tags": ["accessibility", "blind", "cane", "probing", "white", "woman"], + "unicode": "👩‍🦯", + "skins": [ + { + "group": 1, + "hexcode": "1F469-1F3FB-200D-1F9AF", + "label": "woman with white cane: light skin tone", + "order": 2196, + "unicode": "👩🏻‍🦯" + }, + { + "group": 1, + "hexcode": "1F469-1F3FC-200D-1F9AF", + "label": "woman with white cane: medium-light skin tone", + "order": 2197, + "unicode": "👩🏼‍🦯" + }, + { + "group": 1, + "hexcode": "1F469-1F3FD-200D-1F9AF", + "label": "woman with white cane: medium skin tone", + "order": 2198, + "unicode": "👩🏽‍🦯" + }, + { + "group": 1, + "hexcode": "1F469-1F3FE-200D-1F9AF", + "label": "woman with white cane: medium-dark skin tone", + "order": 2199, + "unicode": "👩🏾‍🦯" + }, + { + "group": 1, + "hexcode": "1F469-1F3FF-200D-1F9AF", + "label": "woman with white cane: dark skin tone", + "order": 2200, + "unicode": "👩🏿‍🦯" + } + ] + }, + { + "group": 1, + "hexcode": "1F469-200D-1F9AF-200D-27A1-FE0F", + "label": "woman with white cane facing right", + "order": 2201, + "tags": ["accessibility", "blind", "cane", "probing", "white", "woman"], + "unicode": "👩‍🦯‍➡️", + "skins": [ + { + "group": 1, + "hexcode": "1F469-1F3FB-200D-1F9AF-200D-27A1-FE0F", + "label": "woman with white cane facing right", + "order": 2203, + "unicode": "👩🏻‍🦯‍➡️" + }, + { + "group": 1, + "hexcode": "1F469-1F3FC-200D-1F9AF-200D-27A1-FE0F", + "label": "woman with white cane facing right", + "order": 2205, + "unicode": "👩🏼‍🦯‍➡️" + }, + { + "group": 1, + "hexcode": "1F469-1F3FD-200D-1F9AF-200D-27A1-FE0F", + "label": "woman with white cane facing right", + "order": 2207, + "unicode": "👩🏽‍🦯‍➡️" + }, + { + "group": 1, + "hexcode": "1F469-1F3FE-200D-1F9AF-200D-27A1-FE0F", + "label": "woman with white cane facing right", + "order": 2209, + "unicode": "👩🏾‍🦯‍➡️" + }, + { + "group": 1, + "hexcode": "1F469-1F3FF-200D-1F9AF-200D-27A1-FE0F", + "label": "woman with white cane facing right", + "order": 2211, + "unicode": "👩🏿‍🦯‍➡️" + } + ] + }, + { + "group": 1, + "hexcode": "1F9D1-200D-1F9BC", + "label": "person in motorized wheelchair", + "order": 2213, + "tags": ["accessibility", "motorized", "person", "wheelchair"], + "unicode": "🧑‍🦼", + "skins": [ + { + "group": 1, + "hexcode": "1F9D1-1F3FB-200D-1F9BC", + "label": "person in motorized wheelchair: light skin tone", + "order": 2214, + "unicode": "🧑🏻‍🦼" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FC-200D-1F9BC", + "label": "person in motorized wheelchair: medium-light skin tone", + "order": 2215, + "unicode": "🧑🏼‍🦼" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FD-200D-1F9BC", + "label": "person in motorized wheelchair: medium skin tone", + "order": 2216, + "unicode": "🧑🏽‍🦼" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FE-200D-1F9BC", + "label": "person in motorized wheelchair: medium-dark skin tone", + "order": 2217, + "unicode": "🧑🏾‍🦼" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FF-200D-1F9BC", + "label": "person in motorized wheelchair: dark skin tone", + "order": 2218, + "unicode": "🧑🏿‍🦼" + } + ] + }, + { + "group": 1, + "hexcode": "1F9D1-200D-1F9BC-200D-27A1-FE0F", + "label": "person in motorized wheelchair facing right", + "order": 2219, + "tags": ["accessibility", "motorized", "person", "wheelchair"], + "unicode": "🧑‍🦼‍➡️", + "skins": [ + { + "group": 1, + "hexcode": "1F9D1-1F3FB-200D-1F9BC-200D-27A1-FE0F", + "label": "person in motorized wheelchair facing right", + "order": 2221, + "unicode": "🧑🏻‍🦼‍➡️" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FC-200D-1F9BC-200D-27A1-FE0F", + "label": "person in motorized wheelchair facing right", + "order": 2223, + "unicode": "🧑🏼‍🦼‍➡️" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FD-200D-1F9BC-200D-27A1-FE0F", + "label": "person in motorized wheelchair facing right", + "order": 2225, + "unicode": "🧑🏽‍🦼‍➡️" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FE-200D-1F9BC-200D-27A1-FE0F", + "label": "person in motorized wheelchair facing right", + "order": 2227, + "unicode": "🧑🏾‍🦼‍➡️" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FF-200D-1F9BC-200D-27A1-FE0F", + "label": "person in motorized wheelchair facing right", + "order": 2229, + "unicode": "🧑🏿‍🦼‍➡️" + } + ] + }, + { + "group": 1, + "hexcode": "1F468-200D-1F9BC", + "label": "man in motorized wheelchair", + "order": 2231, + "tags": ["accessibility", "man", "motorized", "wheelchair"], + "unicode": "👨‍🦼", + "skins": [ + { + "group": 1, + "hexcode": "1F468-1F3FB-200D-1F9BC", + "label": "man in motorized wheelchair: light skin tone", + "order": 2232, + "unicode": "👨🏻‍🦼" + }, + { + "group": 1, + "hexcode": "1F468-1F3FC-200D-1F9BC", + "label": "man in motorized wheelchair: medium-light skin tone", + "order": 2233, + "unicode": "👨🏼‍🦼" + }, + { + "group": 1, + "hexcode": "1F468-1F3FD-200D-1F9BC", + "label": "man in motorized wheelchair: medium skin tone", + "order": 2234, + "unicode": "👨🏽‍🦼" + }, + { + "group": 1, + "hexcode": "1F468-1F3FE-200D-1F9BC", + "label": "man in motorized wheelchair: medium-dark skin tone", + "order": 2235, + "unicode": "👨🏾‍🦼" + }, + { + "group": 1, + "hexcode": "1F468-1F3FF-200D-1F9BC", + "label": "man in motorized wheelchair: dark skin tone", + "order": 2236, + "unicode": "👨🏿‍🦼" + } + ] + }, + { + "group": 1, + "hexcode": "1F468-200D-1F9BC-200D-27A1-FE0F", + "label": "man in motorized wheelchair facing right", + "order": 2237, + "tags": ["accessibility", "man", "motorized", "wheelchair"], + "unicode": "👨‍🦼‍➡️", + "skins": [ + { + "group": 1, + "hexcode": "1F468-1F3FB-200D-1F9BC-200D-27A1-FE0F", + "label": "man in motorized wheelchair facing right", + "order": 2239, + "unicode": "👨🏻‍🦼‍➡️" + }, + { + "group": 1, + "hexcode": "1F468-1F3FC-200D-1F9BC-200D-27A1-FE0F", + "label": "man in motorized wheelchair facing right", + "order": 2241, + "unicode": "👨🏼‍🦼‍➡️" + }, + { + "group": 1, + "hexcode": "1F468-1F3FD-200D-1F9BC-200D-27A1-FE0F", + "label": "man in motorized wheelchair facing right", + "order": 2243, + "unicode": "👨🏽‍🦼‍➡️" + }, + { + "group": 1, + "hexcode": "1F468-1F3FE-200D-1F9BC-200D-27A1-FE0F", + "label": "man in motorized wheelchair facing right", + "order": 2245, + "unicode": "👨🏾‍🦼‍➡️" + }, + { + "group": 1, + "hexcode": "1F468-1F3FF-200D-1F9BC-200D-27A1-FE0F", + "label": "man in motorized wheelchair facing right", + "order": 2247, + "unicode": "👨🏿‍🦼‍➡️" + } + ] + }, + { + "group": 1, + "hexcode": "1F469-200D-1F9BC", + "label": "woman in motorized wheelchair", + "order": 2249, + "tags": ["accessibility", "motorized", "wheelchair", "woman"], + "unicode": "👩‍🦼", + "skins": [ + { + "group": 1, + "hexcode": "1F469-1F3FB-200D-1F9BC", + "label": "woman in motorized wheelchair: light skin tone", + "order": 2250, + "unicode": "👩🏻‍🦼" + }, + { + "group": 1, + "hexcode": "1F469-1F3FC-200D-1F9BC", + "label": "woman in motorized wheelchair: medium-light skin tone", + "order": 2251, + "unicode": "👩🏼‍🦼" + }, + { + "group": 1, + "hexcode": "1F469-1F3FD-200D-1F9BC", + "label": "woman in motorized wheelchair: medium skin tone", + "order": 2252, + "unicode": "👩🏽‍🦼" + }, + { + "group": 1, + "hexcode": "1F469-1F3FE-200D-1F9BC", + "label": "woman in motorized wheelchair: medium-dark skin tone", + "order": 2253, + "unicode": "👩🏾‍🦼" + }, + { + "group": 1, + "hexcode": "1F469-1F3FF-200D-1F9BC", + "label": "woman in motorized wheelchair: dark skin tone", + "order": 2254, + "unicode": "👩🏿‍🦼" + } + ] + }, + { + "group": 1, + "hexcode": "1F469-200D-1F9BC-200D-27A1-FE0F", + "label": "woman in motorized wheelchair facing right", + "order": 2255, + "tags": ["accessibility", "motorized", "wheelchair", "woman"], + "unicode": "👩‍🦼‍➡️", + "skins": [ + { + "group": 1, + "hexcode": "1F469-1F3FB-200D-1F9BC-200D-27A1-FE0F", + "label": "woman in motorized wheelchair facing right", + "order": 2257, + "unicode": "👩🏻‍🦼‍➡️" + }, + { + "group": 1, + "hexcode": "1F469-1F3FC-200D-1F9BC-200D-27A1-FE0F", + "label": "woman in motorized wheelchair facing right", + "order": 2259, + "unicode": "👩🏼‍🦼‍➡️" + }, + { + "group": 1, + "hexcode": "1F469-1F3FD-200D-1F9BC-200D-27A1-FE0F", + "label": "woman in motorized wheelchair facing right", + "order": 2261, + "unicode": "👩🏽‍🦼‍➡️" + }, + { + "group": 1, + "hexcode": "1F469-1F3FE-200D-1F9BC-200D-27A1-FE0F", + "label": "woman in motorized wheelchair facing right", + "order": 2263, + "unicode": "👩🏾‍🦼‍➡️" + }, + { + "group": 1, + "hexcode": "1F469-1F3FF-200D-1F9BC-200D-27A1-FE0F", + "label": "woman in motorized wheelchair facing right", + "order": 2265, + "unicode": "👩🏿‍🦼‍➡️" + } + ] + }, + { + "group": 1, + "hexcode": "1F9D1-200D-1F9BD", + "label": "person in manual wheelchair", + "order": 2267, + "tags": ["accessibility", "manual", "person", "wheelchair"], + "unicode": "🧑‍🦽", + "skins": [ + { + "group": 1, + "hexcode": "1F9D1-1F3FB-200D-1F9BD", + "label": "person in manual wheelchair: light skin tone", + "order": 2268, + "unicode": "🧑🏻‍🦽" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FC-200D-1F9BD", + "label": "person in manual wheelchair: medium-light skin tone", + "order": 2269, + "unicode": "🧑🏼‍🦽" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FD-200D-1F9BD", + "label": "person in manual wheelchair: medium skin tone", + "order": 2270, + "unicode": "🧑🏽‍🦽" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FE-200D-1F9BD", + "label": "person in manual wheelchair: medium-dark skin tone", + "order": 2271, + "unicode": "🧑🏾‍🦽" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FF-200D-1F9BD", + "label": "person in manual wheelchair: dark skin tone", + "order": 2272, + "unicode": "🧑🏿‍🦽" + } + ] + }, + { + "group": 1, + "hexcode": "1F9D1-200D-1F9BD-200D-27A1-FE0F", + "label": "person in manual wheelchair facing right", + "order": 2273, + "tags": ["accessibility", "manual", "person", "wheelchair"], + "unicode": "🧑‍🦽‍➡️", + "skins": [ + { + "group": 1, + "hexcode": "1F9D1-1F3FB-200D-1F9BD-200D-27A1-FE0F", + "label": "person in manual wheelchair facing right", + "order": 2275, + "unicode": "🧑🏻‍🦽‍➡️" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FC-200D-1F9BD-200D-27A1-FE0F", + "label": "person in manual wheelchair facing right", + "order": 2277, + "unicode": "🧑🏼‍🦽‍➡️" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FD-200D-1F9BD-200D-27A1-FE0F", + "label": "person in manual wheelchair facing right", + "order": 2279, + "unicode": "🧑🏽‍🦽‍➡️" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FE-200D-1F9BD-200D-27A1-FE0F", + "label": "person in manual wheelchair facing right", + "order": 2281, + "unicode": "🧑🏾‍🦽‍➡️" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FF-200D-1F9BD-200D-27A1-FE0F", + "label": "person in manual wheelchair facing right", + "order": 2283, + "unicode": "🧑🏿‍🦽‍➡️" + } + ] + }, + { + "group": 1, + "hexcode": "1F468-200D-1F9BD", + "label": "man in manual wheelchair", + "order": 2285, + "tags": ["accessibility", "man", "manual", "wheelchair"], + "unicode": "👨‍🦽", + "skins": [ + { + "group": 1, + "hexcode": "1F468-1F3FB-200D-1F9BD", + "label": "man in manual wheelchair: light skin tone", + "order": 2286, + "unicode": "👨🏻‍🦽" + }, + { + "group": 1, + "hexcode": "1F468-1F3FC-200D-1F9BD", + "label": "man in manual wheelchair: medium-light skin tone", + "order": 2287, + "unicode": "👨🏼‍🦽" + }, + { + "group": 1, + "hexcode": "1F468-1F3FD-200D-1F9BD", + "label": "man in manual wheelchair: medium skin tone", + "order": 2288, + "unicode": "👨🏽‍🦽" + }, + { + "group": 1, + "hexcode": "1F468-1F3FE-200D-1F9BD", + "label": "man in manual wheelchair: medium-dark skin tone", + "order": 2289, + "unicode": "👨🏾‍🦽" + }, + { + "group": 1, + "hexcode": "1F468-1F3FF-200D-1F9BD", + "label": "man in manual wheelchair: dark skin tone", + "order": 2290, + "unicode": "👨🏿‍🦽" + } + ] + }, + { + "group": 1, + "hexcode": "1F468-200D-1F9BD-200D-27A1-FE0F", + "label": "man in manual wheelchair facing right", + "order": 2291, + "tags": ["accessibility", "man", "manual", "wheelchair"], + "unicode": "👨‍🦽‍➡️", + "skins": [ + { + "group": 1, + "hexcode": "1F468-1F3FB-200D-1F9BD-200D-27A1-FE0F", + "label": "man in manual wheelchair facing right", + "order": 2293, + "unicode": "👨🏻‍🦽‍➡️" + }, + { + "group": 1, + "hexcode": "1F468-1F3FC-200D-1F9BD-200D-27A1-FE0F", + "label": "man in manual wheelchair facing right", + "order": 2295, + "unicode": "👨🏼‍🦽‍➡️" + }, + { + "group": 1, + "hexcode": "1F468-1F3FD-200D-1F9BD-200D-27A1-FE0F", + "label": "man in manual wheelchair facing right", + "order": 2297, + "unicode": "👨🏽‍🦽‍➡️" + }, + { + "group": 1, + "hexcode": "1F468-1F3FE-200D-1F9BD-200D-27A1-FE0F", + "label": "man in manual wheelchair facing right", + "order": 2299, + "unicode": "👨🏾‍🦽‍➡️" + }, + { + "group": 1, + "hexcode": "1F468-1F3FF-200D-1F9BD-200D-27A1-FE0F", + "label": "man in manual wheelchair facing right", + "order": 2301, + "unicode": "👨🏿‍🦽‍➡️" + } + ] + }, + { + "group": 1, + "hexcode": "1F469-200D-1F9BD", + "label": "woman in manual wheelchair", + "order": 2303, + "tags": ["accessibility", "manual", "wheelchair", "woman"], + "unicode": "👩‍🦽", + "skins": [ + { + "group": 1, + "hexcode": "1F469-1F3FB-200D-1F9BD", + "label": "woman in manual wheelchair: light skin tone", + "order": 2304, + "unicode": "👩🏻‍🦽" + }, + { + "group": 1, + "hexcode": "1F469-1F3FC-200D-1F9BD", + "label": "woman in manual wheelchair: medium-light skin tone", + "order": 2305, + "unicode": "👩🏼‍🦽" + }, + { + "group": 1, + "hexcode": "1F469-1F3FD-200D-1F9BD", + "label": "woman in manual wheelchair: medium skin tone", + "order": 2306, + "unicode": "👩🏽‍🦽" + }, + { + "group": 1, + "hexcode": "1F469-1F3FE-200D-1F9BD", + "label": "woman in manual wheelchair: medium-dark skin tone", + "order": 2307, + "unicode": "👩🏾‍🦽" + }, + { + "group": 1, + "hexcode": "1F469-1F3FF-200D-1F9BD", + "label": "woman in manual wheelchair: dark skin tone", + "order": 2308, + "unicode": "👩🏿‍🦽" + } + ] + }, + { + "group": 1, + "hexcode": "1F469-200D-1F9BD-200D-27A1-FE0F", + "label": "woman in manual wheelchair facing right", + "order": 2309, + "tags": ["accessibility", "manual", "wheelchair", "woman"], + "unicode": "👩‍🦽‍➡️", + "skins": [ + { + "group": 1, + "hexcode": "1F469-1F3FB-200D-1F9BD-200D-27A1-FE0F", + "label": "woman in manual wheelchair facing right", + "order": 2311, + "unicode": "👩🏻‍🦽‍➡️" + }, + { + "group": 1, + "hexcode": "1F469-1F3FC-200D-1F9BD-200D-27A1-FE0F", + "label": "woman in manual wheelchair facing right", + "order": 2313, + "unicode": "👩🏼‍🦽‍➡️" + }, + { + "group": 1, + "hexcode": "1F469-1F3FD-200D-1F9BD-200D-27A1-FE0F", + "label": "woman in manual wheelchair facing right", + "order": 2315, + "unicode": "👩🏽‍🦽‍➡️" + }, + { + "group": 1, + "hexcode": "1F469-1F3FE-200D-1F9BD-200D-27A1-FE0F", + "label": "woman in manual wheelchair facing right", + "order": 2317, + "unicode": "👩🏾‍🦽‍➡️" + }, + { + "group": 1, + "hexcode": "1F469-1F3FF-200D-1F9BD-200D-27A1-FE0F", + "label": "woman in manual wheelchair facing right", + "order": 2319, + "unicode": "👩🏿‍🦽‍➡️" + } + ] + }, + { + "group": 1, + "hexcode": "1F3C3", + "label": "person running", + "order": 2321, + "tags": [ + "fast", + "hurry", + "marathon", + "move", + "person", + "quick", + "race", + "racing", + "run", + "rush", + "speed" + ], + "unicode": "🏃", + "skins": [ + { + "group": 1, + "hexcode": "1F3C3-1F3FB", + "label": "person running: light skin tone", + "order": 2322, + "unicode": "🏃🏻" + }, + { + "group": 1, + "hexcode": "1F3C3-1F3FC", + "label": "person running: medium-light skin tone", + "order": 2323, + "unicode": "🏃🏼" + }, + { + "group": 1, + "hexcode": "1F3C3-1F3FD", + "label": "person running: medium skin tone", + "order": 2324, + "unicode": "🏃🏽" + }, + { + "group": 1, + "hexcode": "1F3C3-1F3FE", + "label": "person running: medium-dark skin tone", + "order": 2325, + "unicode": "🏃🏾" + }, + { + "group": 1, + "hexcode": "1F3C3-1F3FF", + "label": "person running: dark skin tone", + "order": 2326, + "unicode": "🏃🏿" + } + ] + }, + { + "group": 1, + "hexcode": "1F3C3-200D-2642-FE0F", + "label": "man running", + "order": 2327, + "tags": [ + "fast", + "hurry", + "man", + "marathon", + "move", + "quick", + "race", + "racing", + "run", + "rush", + "speed" + ], + "unicode": "🏃‍♂️", + "skins": [ + { + "group": 1, + "hexcode": "1F3C3-1F3FB-200D-2642-FE0F", + "label": "man running: light skin tone", + "order": 2329, + "unicode": "🏃🏻‍♂️" + }, + { + "group": 1, + "hexcode": "1F3C3-1F3FC-200D-2642-FE0F", + "label": "man running: medium-light skin tone", + "order": 2331, + "unicode": "🏃🏼‍♂️" + }, + { + "group": 1, + "hexcode": "1F3C3-1F3FD-200D-2642-FE0F", + "label": "man running: medium skin tone", + "order": 2333, + "unicode": "🏃🏽‍♂️" + }, + { + "group": 1, + "hexcode": "1F3C3-1F3FE-200D-2642-FE0F", + "label": "man running: medium-dark skin tone", + "order": 2335, + "unicode": "🏃🏾‍♂️" + }, + { + "group": 1, + "hexcode": "1F3C3-1F3FF-200D-2642-FE0F", + "label": "man running: dark skin tone", + "order": 2337, + "unicode": "🏃🏿‍♂️" + } + ] + }, + { + "group": 1, + "hexcode": "1F3C3-200D-2640-FE0F", + "label": "woman running", + "order": 2339, + "tags": [ + "fast", + "hurry", + "marathon", + "move", + "quick", + "race", + "racing", + "run", + "rush", + "speed", + "woman" + ], + "unicode": "🏃‍♀️", + "skins": [ + { + "group": 1, + "hexcode": "1F3C3-1F3FB-200D-2640-FE0F", + "label": "woman running: light skin tone", + "order": 2341, + "unicode": "🏃🏻‍♀️" + }, + { + "group": 1, + "hexcode": "1F3C3-1F3FC-200D-2640-FE0F", + "label": "woman running: medium-light skin tone", + "order": 2343, + "unicode": "🏃🏼‍♀️" + }, + { + "group": 1, + "hexcode": "1F3C3-1F3FD-200D-2640-FE0F", + "label": "woman running: medium skin tone", + "order": 2345, + "unicode": "🏃🏽‍♀️" + }, + { + "group": 1, + "hexcode": "1F3C3-1F3FE-200D-2640-FE0F", + "label": "woman running: medium-dark skin tone", + "order": 2347, + "unicode": "🏃🏾‍♀️" + }, + { + "group": 1, + "hexcode": "1F3C3-1F3FF-200D-2640-FE0F", + "label": "woman running: dark skin tone", + "order": 2349, + "unicode": "🏃🏿‍♀️" + } + ] + }, + { + "group": 1, + "hexcode": "1F3C3-200D-27A1-FE0F", + "label": "person running facing right", + "order": 2351, + "tags": [ + "fast", + "hurry", + "marathon", + "move", + "person", + "quick", + "race", + "racing", + "run", + "rush", + "speed" + ], + "unicode": "🏃‍➡️", + "skins": [ + { + "group": 1, + "hexcode": "1F3C3-1F3FB-200D-27A1-FE0F", + "label": "person running facing right", + "order": 2353, + "unicode": "🏃🏻‍➡️" + }, + { + "group": 1, + "hexcode": "1F3C3-1F3FC-200D-27A1-FE0F", + "label": "person running facing right", + "order": 2355, + "unicode": "🏃🏼‍➡️" + }, + { + "group": 1, + "hexcode": "1F3C3-1F3FD-200D-27A1-FE0F", + "label": "person running facing right", + "order": 2357, + "unicode": "🏃🏽‍➡️" + }, + { + "group": 1, + "hexcode": "1F3C3-1F3FE-200D-27A1-FE0F", + "label": "person running facing right", + "order": 2359, + "unicode": "🏃🏾‍➡️" + }, + { + "group": 1, + "hexcode": "1F3C3-1F3FF-200D-27A1-FE0F", + "label": "person running facing right", + "order": 2361, + "unicode": "🏃🏿‍➡️" + } + ] + }, + { + "group": 1, + "hexcode": "1F3C3-200D-2640-FE0F-200D-27A1-FE0F", + "label": "woman running facing right", + "order": 2363, + "tags": [ + "fast", + "hurry", + "marathon", + "move", + "quick", + "race", + "racing", + "run", + "rush", + "speed", + "woman" + ], + "unicode": "🏃‍♀️‍➡️", + "skins": [ + { + "group": 1, + "hexcode": "1F3C3-1F3FB-200D-2640-FE0F-200D-27A1-FE0F", + "label": "woman running facing right", + "order": 2367, + "unicode": "🏃🏻‍♀️‍➡️" + }, + { + "group": 1, + "hexcode": "1F3C3-1F3FC-200D-2640-FE0F-200D-27A1-FE0F", + "label": "woman running facing right", + "order": 2371, + "unicode": "🏃🏼‍♀️‍➡️" + }, + { + "group": 1, + "hexcode": "1F3C3-1F3FD-200D-2640-FE0F-200D-27A1-FE0F", + "label": "woman running facing right", + "order": 2375, + "unicode": "🏃🏽‍♀️‍➡️" + }, + { + "group": 1, + "hexcode": "1F3C3-1F3FE-200D-2640-FE0F-200D-27A1-FE0F", + "label": "woman running facing right", + "order": 2379, + "unicode": "🏃🏾‍♀️‍➡️" + }, + { + "group": 1, + "hexcode": "1F3C3-1F3FF-200D-2640-FE0F-200D-27A1-FE0F", + "label": "woman running facing right", + "order": 2383, + "unicode": "🏃🏿‍♀️‍➡️" + } + ] + }, + { + "group": 1, + "hexcode": "1F3C3-200D-2642-FE0F-200D-27A1-FE0F", + "label": "man running facing right", + "order": 2387, + "tags": [ + "fast", + "hurry", + "man", + "marathon", + "move", + "quick", + "race", + "racing", + "run", + "rush", + "speed" + ], + "unicode": "🏃‍♂️‍➡️", + "skins": [ + { + "group": 1, + "hexcode": "1F3C3-1F3FB-200D-2642-FE0F-200D-27A1-FE0F", + "label": "man running facing right", + "order": 2391, + "unicode": "🏃🏻‍♂️‍➡️" + }, + { + "group": 1, + "hexcode": "1F3C3-1F3FC-200D-2642-FE0F-200D-27A1-FE0F", + "label": "man running facing right", + "order": 2395, + "unicode": "🏃🏼‍♂️‍➡️" + }, + { + "group": 1, + "hexcode": "1F3C3-1F3FD-200D-2642-FE0F-200D-27A1-FE0F", + "label": "man running facing right", + "order": 2399, + "unicode": "🏃🏽‍♂️‍➡️" + }, + { + "group": 1, + "hexcode": "1F3C3-1F3FE-200D-2642-FE0F-200D-27A1-FE0F", + "label": "man running facing right", + "order": 2403, + "unicode": "🏃🏾‍♂️‍➡️" + }, + { + "group": 1, + "hexcode": "1F3C3-1F3FF-200D-2642-FE0F-200D-27A1-FE0F", + "label": "man running facing right", + "order": 2407, + "unicode": "🏃🏿‍♂️‍➡️" + } + ] + }, + { + "group": 1, + "hexcode": "1F483", + "label": "woman dancing", + "order": 2411, + "tags": [ + "dance", + "dancer", + "dancing", + "elegant", + "festive", + "flair", + "flamenco", + "groove", + "let’s", + "salsa", + "tango", + "woman" + ], + "unicode": "💃", + "skins": [ + { + "group": 1, + "hexcode": "1F483-1F3FB", + "label": "woman dancing: light skin tone", + "order": 2412, + "unicode": "💃🏻" + }, + { + "group": 1, + "hexcode": "1F483-1F3FC", + "label": "woman dancing: medium-light skin tone", + "order": 2413, + "unicode": "💃🏼" + }, + { + "group": 1, + "hexcode": "1F483-1F3FD", + "label": "woman dancing: medium skin tone", + "order": 2414, + "unicode": "💃🏽" + }, + { + "group": 1, + "hexcode": "1F483-1F3FE", + "label": "woman dancing: medium-dark skin tone", + "order": 2415, + "unicode": "💃🏾" + }, + { + "group": 1, + "hexcode": "1F483-1F3FF", + "label": "woman dancing: dark skin tone", + "order": 2416, + "unicode": "💃🏿" + } + ] + }, + { + "group": 1, + "hexcode": "1F57A", + "label": "man dancing", + "order": 2417, + "tags": [ + "dance", + "dancer", + "dancing", + "elegant", + "festive", + "flair", + "flamenco", + "groove", + "let’s", + "man", + "salsa", + "tango" + ], + "unicode": "🕺", + "skins": [ + { + "group": 1, + "hexcode": "1F57A-1F3FB", + "label": "man dancing: light skin tone", + "order": 2418, + "unicode": "🕺🏻" + }, + { + "group": 1, + "hexcode": "1F57A-1F3FC", + "label": "man dancing: medium-light skin tone", + "order": 2419, + "unicode": "🕺🏼" + }, + { + "group": 1, + "hexcode": "1F57A-1F3FD", + "label": "man dancing: medium skin tone", + "order": 2420, + "unicode": "🕺🏽" + }, + { + "group": 1, + "hexcode": "1F57A-1F3FE", + "label": "man dancing: medium-dark skin tone", + "order": 2421, + "unicode": "🕺🏾" + }, + { + "group": 1, + "hexcode": "1F57A-1F3FF", + "label": "man dancing: dark skin tone", + "order": 2422, + "unicode": "🕺🏿" + } + ] + }, + { + "group": 1, + "hexcode": "1F574", + "label": "person in suit levitating", + "order": 2424, + "tags": ["business", "levitating", "person", "suit"], + "unicode": "🕴️", + "skins": [ + { + "group": 1, + "hexcode": "1F574-1F3FB", + "label": "person in suit levitating: light skin tone", + "order": 2425, + "unicode": "🕴🏻" + }, + { + "group": 1, + "hexcode": "1F574-1F3FC", + "label": "person in suit levitating: medium-light skin tone", + "order": 2426, + "unicode": "🕴🏼" + }, + { + "group": 1, + "hexcode": "1F574-1F3FD", + "label": "person in suit levitating: medium skin tone", + "order": 2427, + "unicode": "🕴🏽" + }, + { + "group": 1, + "hexcode": "1F574-1F3FE", + "label": "person in suit levitating: medium-dark skin tone", + "order": 2428, + "unicode": "🕴🏾" + }, + { + "group": 1, + "hexcode": "1F574-1F3FF", + "label": "person in suit levitating: dark skin tone", + "order": 2429, + "unicode": "🕴🏿" + } + ] + }, + { + "group": 1, + "hexcode": "1F46F", + "label": "people with bunny ears", + "order": 2430, + "tags": [ + "bestie", + "bff", + "bunny", + "counterpart", + "dancer", + "double", + "ear", + "identical", + "pair", + "party", + "partying", + "people", + "soulmate", + "twin", + "twinsies" + ], + "unicode": "👯" + }, + { + "group": 1, + "hexcode": "1F46F-200D-2642-FE0F", + "label": "men with bunny ears", + "order": 2431, + "tags": [ + "bestie", + "bff", + "bunny", + "counterpart", + "dancer", + "double", + "ear", + "identical", + "men", + "pair", + "party", + "partying", + "people", + "soulmate", + "twin", + "twinsies" + ], + "unicode": "👯‍♂️" + }, + { + "group": 1, + "hexcode": "1F46F-200D-2640-FE0F", + "label": "women with bunny ears", + "order": 2433, + "tags": [ + "bestie", + "bff", + "bunny", + "counterpart", + "dancer", + "double", + "ear", + "identical", + "pair", + "party", + "partying", + "people", + "soulmate", + "twin", + "twinsies", + "women" + ], + "unicode": "👯‍♀️" + }, + { + "group": 1, + "hexcode": "1F9D6", + "label": "person in steamy room", + "order": 2435, + "tags": [ + "day", + "luxurious", + "pamper", + "person", + "relax", + "room", + "sauna", + "spa", + "steam", + "steambath", + "unwind" + ], + "unicode": "🧖", + "skins": [ + { + "group": 1, + "hexcode": "1F9D6-1F3FB", + "label": "person in steamy room: light skin tone", + "order": 2436, + "unicode": "🧖🏻" + }, + { + "group": 1, + "hexcode": "1F9D6-1F3FC", + "label": "person in steamy room: medium-light skin tone", + "order": 2437, + "unicode": "🧖🏼" + }, + { + "group": 1, + "hexcode": "1F9D6-1F3FD", + "label": "person in steamy room: medium skin tone", + "order": 2438, + "unicode": "🧖🏽" + }, + { + "group": 1, + "hexcode": "1F9D6-1F3FE", + "label": "person in steamy room: medium-dark skin tone", + "order": 2439, + "unicode": "🧖🏾" + }, + { + "group": 1, + "hexcode": "1F9D6-1F3FF", + "label": "person in steamy room: dark skin tone", + "order": 2440, + "unicode": "🧖🏿" + } + ] + }, + { + "group": 1, + "hexcode": "1F9D6-200D-2642-FE0F", + "label": "man in steamy room", + "order": 2441, + "tags": [ + "day", + "luxurious", + "man", + "pamper", + "relax", + "room", + "sauna", + "spa", + "steam", + "steambath", + "unwind" + ], + "unicode": "🧖‍♂️", + "skins": [ + { + "group": 1, + "hexcode": "1F9D6-1F3FB-200D-2642-FE0F", + "label": "man in steamy room: light skin tone", + "order": 2443, + "unicode": "🧖🏻‍♂️" + }, + { + "group": 1, + "hexcode": "1F9D6-1F3FC-200D-2642-FE0F", + "label": "man in steamy room: medium-light skin tone", + "order": 2445, + "unicode": "🧖🏼‍♂️" + }, + { + "group": 1, + "hexcode": "1F9D6-1F3FD-200D-2642-FE0F", + "label": "man in steamy room: medium skin tone", + "order": 2447, + "unicode": "🧖🏽‍♂️" + }, + { + "group": 1, + "hexcode": "1F9D6-1F3FE-200D-2642-FE0F", + "label": "man in steamy room: medium-dark skin tone", + "order": 2449, + "unicode": "🧖🏾‍♂️" + }, + { + "group": 1, + "hexcode": "1F9D6-1F3FF-200D-2642-FE0F", + "label": "man in steamy room: dark skin tone", + "order": 2451, + "unicode": "🧖🏿‍♂️" + } + ] + }, + { + "group": 1, + "hexcode": "1F9D6-200D-2640-FE0F", + "label": "woman in steamy room", + "order": 2453, + "tags": [ + "day", + "luxurious", + "pamper", + "relax", + "room", + "sauna", + "spa", + "steam", + "steambath", + "unwind", + "woman" + ], + "unicode": "🧖‍♀️", + "skins": [ + { + "group": 1, + "hexcode": "1F9D6-1F3FB-200D-2640-FE0F", + "label": "woman in steamy room: light skin tone", + "order": 2455, + "unicode": "🧖🏻‍♀️" + }, + { + "group": 1, + "hexcode": "1F9D6-1F3FC-200D-2640-FE0F", + "label": "woman in steamy room: medium-light skin tone", + "order": 2457, + "unicode": "🧖🏼‍♀️" + }, + { + "group": 1, + "hexcode": "1F9D6-1F3FD-200D-2640-FE0F", + "label": "woman in steamy room: medium skin tone", + "order": 2459, + "unicode": "🧖🏽‍♀️" + }, + { + "group": 1, + "hexcode": "1F9D6-1F3FE-200D-2640-FE0F", + "label": "woman in steamy room: medium-dark skin tone", + "order": 2461, + "unicode": "🧖🏾‍♀️" + }, + { + "group": 1, + "hexcode": "1F9D6-1F3FF-200D-2640-FE0F", + "label": "woman in steamy room: dark skin tone", + "order": 2463, + "unicode": "🧖🏿‍♀️" + } + ] + }, + { + "group": 1, + "hexcode": "1F9D7", + "label": "person climbing", + "order": 2465, + "tags": [ + "climb", + "climber", + "climbing", + "mountain", + "person", + "rock", + "scale", + "up" + ], + "unicode": "🧗", + "skins": [ + { + "group": 1, + "hexcode": "1F9D7-1F3FB", + "label": "person climbing: light skin tone", + "order": 2466, + "unicode": "🧗🏻" + }, + { + "group": 1, + "hexcode": "1F9D7-1F3FC", + "label": "person climbing: medium-light skin tone", + "order": 2467, + "unicode": "🧗🏼" + }, + { + "group": 1, + "hexcode": "1F9D7-1F3FD", + "label": "person climbing: medium skin tone", + "order": 2468, + "unicode": "🧗🏽" + }, + { + "group": 1, + "hexcode": "1F9D7-1F3FE", + "label": "person climbing: medium-dark skin tone", + "order": 2469, + "unicode": "🧗🏾" + }, + { + "group": 1, + "hexcode": "1F9D7-1F3FF", + "label": "person climbing: dark skin tone", + "order": 2470, + "unicode": "🧗🏿" + } + ] + }, + { + "group": 1, + "hexcode": "1F9D7-200D-2642-FE0F", + "label": "man climbing", + "order": 2471, + "tags": [ + "climb", + "climber", + "climbing", + "man", + "mountain", + "rock", + "scale", + "up" + ], + "unicode": "🧗‍♂️", + "skins": [ + { + "group": 1, + "hexcode": "1F9D7-1F3FB-200D-2642-FE0F", + "label": "man climbing: light skin tone", + "order": 2473, + "unicode": "🧗🏻‍♂️" + }, + { + "group": 1, + "hexcode": "1F9D7-1F3FC-200D-2642-FE0F", + "label": "man climbing: medium-light skin tone", + "order": 2475, + "unicode": "🧗🏼‍♂️" + }, + { + "group": 1, + "hexcode": "1F9D7-1F3FD-200D-2642-FE0F", + "label": "man climbing: medium skin tone", + "order": 2477, + "unicode": "🧗🏽‍♂️" + }, + { + "group": 1, + "hexcode": "1F9D7-1F3FE-200D-2642-FE0F", + "label": "man climbing: medium-dark skin tone", + "order": 2479, + "unicode": "🧗🏾‍♂️" + }, + { + "group": 1, + "hexcode": "1F9D7-1F3FF-200D-2642-FE0F", + "label": "man climbing: dark skin tone", + "order": 2481, + "unicode": "🧗🏿‍♂️" + } + ] + }, + { + "group": 1, + "hexcode": "1F9D7-200D-2640-FE0F", + "label": "woman climbing", + "order": 2483, + "tags": [ + "climb", + "climber", + "climbing", + "mountain", + "rock", + "scale", + "up", + "woman" + ], + "unicode": "🧗‍♀️", + "skins": [ + { + "group": 1, + "hexcode": "1F9D7-1F3FB-200D-2640-FE0F", + "label": "woman climbing: light skin tone", + "order": 2485, + "unicode": "🧗🏻‍♀️" + }, + { + "group": 1, + "hexcode": "1F9D7-1F3FC-200D-2640-FE0F", + "label": "woman climbing: medium-light skin tone", + "order": 2487, + "unicode": "🧗🏼‍♀️" + }, + { + "group": 1, + "hexcode": "1F9D7-1F3FD-200D-2640-FE0F", + "label": "woman climbing: medium skin tone", + "order": 2489, + "unicode": "🧗🏽‍♀️" + }, + { + "group": 1, + "hexcode": "1F9D7-1F3FE-200D-2640-FE0F", + "label": "woman climbing: medium-dark skin tone", + "order": 2491, + "unicode": "🧗🏾‍♀️" + }, + { + "group": 1, + "hexcode": "1F9D7-1F3FF-200D-2640-FE0F", + "label": "woman climbing: dark skin tone", + "order": 2493, + "unicode": "🧗🏿‍♀️" + } + ] + }, + { + "group": 1, + "hexcode": "1F93A", + "label": "person fencing", + "order": 2495, + "tags": ["fencer", "fencing", "person", "sword"], + "unicode": "🤺" + }, + { + "group": 1, + "hexcode": "1F3C7", + "label": "horse racing", + "order": 2496, + "tags": ["horse", "jockey", "racehorse", "racing", "riding", "sport"], + "unicode": "🏇", + "skins": [ + { + "group": 1, + "hexcode": "1F3C7-1F3FB", + "label": "horse racing: light skin tone", + "order": 2497, + "unicode": "🏇🏻" + }, + { + "group": 1, + "hexcode": "1F3C7-1F3FC", + "label": "horse racing: medium-light skin tone", + "order": 2498, + "unicode": "🏇🏼" + }, + { + "group": 1, + "hexcode": "1F3C7-1F3FD", + "label": "horse racing: medium skin tone", + "order": 2499, + "unicode": "🏇🏽" + }, + { + "group": 1, + "hexcode": "1F3C7-1F3FE", + "label": "horse racing: medium-dark skin tone", + "order": 2500, + "unicode": "🏇🏾" + }, + { + "group": 1, + "hexcode": "1F3C7-1F3FF", + "label": "horse racing: dark skin tone", + "order": 2501, + "unicode": "🏇🏿" + } + ] + }, + { + "group": 1, + "hexcode": "26F7", + "label": "skier", + "order": 2503, + "tags": ["ski", "snow"], + "unicode": "⛷️" + }, + { + "group": 1, + "hexcode": "1F3C2", + "label": "snowboarder", + "order": 2504, + "tags": ["ski", "snow", "snowboard", "sport"], + "unicode": "🏂️", + "skins": [ + { + "group": 1, + "hexcode": "1F3C2-1F3FB", + "label": "snowboarder: light skin tone", + "order": 2505, + "unicode": "🏂🏻" + }, + { + "group": 1, + "hexcode": "1F3C2-1F3FC", + "label": "snowboarder: medium-light skin tone", + "order": 2506, + "unicode": "🏂🏼" + }, + { + "group": 1, + "hexcode": "1F3C2-1F3FD", + "label": "snowboarder: medium skin tone", + "order": 2507, + "unicode": "🏂🏽" + }, + { + "group": 1, + "hexcode": "1F3C2-1F3FE", + "label": "snowboarder: medium-dark skin tone", + "order": 2508, + "unicode": "🏂🏾" + }, + { + "group": 1, + "hexcode": "1F3C2-1F3FF", + "label": "snowboarder: dark skin tone", + "order": 2509, + "unicode": "🏂🏿" + } + ] + }, + { + "group": 1, + "hexcode": "1F3CC", + "label": "person golfing", + "order": 2511, + "tags": [ + "ball", + "birdie", + "caddy", + "driving", + "golf", + "golfing", + "green", + "person", + "pga", + "putt", + "range", + "tee" + ], + "unicode": "🏌️", + "skins": [ + { + "group": 1, + "hexcode": "1F3CC-1F3FB", + "label": "person golfing: light skin tone", + "order": 2512, + "unicode": "🏌🏻" + }, + { + "group": 1, + "hexcode": "1F3CC-1F3FC", + "label": "person golfing: medium-light skin tone", + "order": 2513, + "unicode": "🏌🏼" + }, + { + "group": 1, + "hexcode": "1F3CC-1F3FD", + "label": "person golfing: medium skin tone", + "order": 2514, + "unicode": "🏌🏽" + }, + { + "group": 1, + "hexcode": "1F3CC-1F3FE", + "label": "person golfing: medium-dark skin tone", + "order": 2515, + "unicode": "🏌🏾" + }, + { + "group": 1, + "hexcode": "1F3CC-1F3FF", + "label": "person golfing: dark skin tone", + "order": 2516, + "unicode": "🏌🏿" + } + ] + }, + { + "group": 1, + "hexcode": "1F3CC-FE0F-200D-2642-FE0F", + "label": "man golfing", + "order": 2517, + "tags": [ + "ball", + "birdie", + "caddy", + "driving", + "golf", + "golfing", + "green", + "man", + "pga", + "putt", + "range", + "tee" + ], + "unicode": "🏌️‍♂️", + "skins": [ + { + "group": 1, + "hexcode": "1F3CC-1F3FB-200D-2642-FE0F", + "label": "man golfing: light skin tone", + "order": 2521, + "unicode": "🏌🏻‍♂️" + }, + { + "group": 1, + "hexcode": "1F3CC-1F3FC-200D-2642-FE0F", + "label": "man golfing: medium-light skin tone", + "order": 2523, + "unicode": "🏌🏼‍♂️" + }, + { + "group": 1, + "hexcode": "1F3CC-1F3FD-200D-2642-FE0F", + "label": "man golfing: medium skin tone", + "order": 2525, + "unicode": "🏌🏽‍♂️" + }, + { + "group": 1, + "hexcode": "1F3CC-1F3FE-200D-2642-FE0F", + "label": "man golfing: medium-dark skin tone", + "order": 2527, + "unicode": "🏌🏾‍♂️" + }, + { + "group": 1, + "hexcode": "1F3CC-1F3FF-200D-2642-FE0F", + "label": "man golfing: dark skin tone", + "order": 2529, + "unicode": "🏌🏿‍♂️" + } + ] + }, + { + "group": 1, + "hexcode": "1F3CC-FE0F-200D-2640-FE0F", + "label": "woman golfing", + "order": 2531, + "tags": [ + "ball", + "birdie", + "caddy", + "driving", + "golf", + "golfing", + "green", + "pga", + "putt", + "range", + "tee", + "woman" + ], + "unicode": "🏌️‍♀️", + "skins": [ + { + "group": 1, + "hexcode": "1F3CC-1F3FB-200D-2640-FE0F", + "label": "woman golfing: light skin tone", + "order": 2535, + "unicode": "🏌🏻‍♀️" + }, + { + "group": 1, + "hexcode": "1F3CC-1F3FC-200D-2640-FE0F", + "label": "woman golfing: medium-light skin tone", + "order": 2537, + "unicode": "🏌🏼‍♀️" + }, + { + "group": 1, + "hexcode": "1F3CC-1F3FD-200D-2640-FE0F", + "label": "woman golfing: medium skin tone", + "order": 2539, + "unicode": "🏌🏽‍♀️" + }, + { + "group": 1, + "hexcode": "1F3CC-1F3FE-200D-2640-FE0F", + "label": "woman golfing: medium-dark skin tone", + "order": 2541, + "unicode": "🏌🏾‍♀️" + }, + { + "group": 1, + "hexcode": "1F3CC-1F3FF-200D-2640-FE0F", + "label": "woman golfing: dark skin tone", + "order": 2543, + "unicode": "🏌🏿‍♀️" + } + ] + }, + { + "group": 1, + "hexcode": "1F3C4", + "label": "person surfing", + "order": 2545, + "tags": [ + "beach", + "ocean", + "person", + "sport", + "surf", + "surfer", + "surfing", + "swell", + "waves" + ], + "unicode": "🏄️", + "skins": [ + { + "group": 1, + "hexcode": "1F3C4-1F3FB", + "label": "person surfing: light skin tone", + "order": 2546, + "unicode": "🏄🏻" + }, + { + "group": 1, + "hexcode": "1F3C4-1F3FC", + "label": "person surfing: medium-light skin tone", + "order": 2547, + "unicode": "🏄🏼" + }, + { + "group": 1, + "hexcode": "1F3C4-1F3FD", + "label": "person surfing: medium skin tone", + "order": 2548, + "unicode": "🏄🏽" + }, + { + "group": 1, + "hexcode": "1F3C4-1F3FE", + "label": "person surfing: medium-dark skin tone", + "order": 2549, + "unicode": "🏄🏾" + }, + { + "group": 1, + "hexcode": "1F3C4-1F3FF", + "label": "person surfing: dark skin tone", + "order": 2550, + "unicode": "🏄🏿" + } + ] + }, + { + "group": 1, + "hexcode": "1F3C4-200D-2642-FE0F", + "label": "man surfing", + "order": 2551, + "tags": [ + "beach", + "man", + "ocean", + "sport", + "surf", + "surfer", + "surfing", + "swell", + "waves" + ], + "unicode": "🏄‍♂️", + "skins": [ + { + "group": 1, + "hexcode": "1F3C4-1F3FB-200D-2642-FE0F", + "label": "man surfing: light skin tone", + "order": 2553, + "unicode": "🏄🏻‍♂️" + }, + { + "group": 1, + "hexcode": "1F3C4-1F3FC-200D-2642-FE0F", + "label": "man surfing: medium-light skin tone", + "order": 2555, + "unicode": "🏄🏼‍♂️" + }, + { + "group": 1, + "hexcode": "1F3C4-1F3FD-200D-2642-FE0F", + "label": "man surfing: medium skin tone", + "order": 2557, + "unicode": "🏄🏽‍♂️" + }, + { + "group": 1, + "hexcode": "1F3C4-1F3FE-200D-2642-FE0F", + "label": "man surfing: medium-dark skin tone", + "order": 2559, + "unicode": "🏄🏾‍♂️" + }, + { + "group": 1, + "hexcode": "1F3C4-1F3FF-200D-2642-FE0F", + "label": "man surfing: dark skin tone", + "order": 2561, + "unicode": "🏄🏿‍♂️" + } + ] + }, + { + "group": 1, + "hexcode": "1F3C4-200D-2640-FE0F", + "label": "woman surfing", + "order": 2563, + "tags": [ + "beach", + "ocean", + "person", + "sport", + "surf", + "surfer", + "surfing", + "swell", + "waves" + ], + "unicode": "🏄‍♀️", + "skins": [ + { + "group": 1, + "hexcode": "1F3C4-1F3FB-200D-2640-FE0F", + "label": "woman surfing: light skin tone", + "order": 2565, + "unicode": "🏄🏻‍♀️" + }, + { + "group": 1, + "hexcode": "1F3C4-1F3FC-200D-2640-FE0F", + "label": "woman surfing: medium-light skin tone", + "order": 2567, + "unicode": "🏄🏼‍♀️" + }, + { + "group": 1, + "hexcode": "1F3C4-1F3FD-200D-2640-FE0F", + "label": "woman surfing: medium skin tone", + "order": 2569, + "unicode": "🏄🏽‍♀️" + }, + { + "group": 1, + "hexcode": "1F3C4-1F3FE-200D-2640-FE0F", + "label": "woman surfing: medium-dark skin tone", + "order": 2571, + "unicode": "🏄🏾‍♀️" + }, + { + "group": 1, + "hexcode": "1F3C4-1F3FF-200D-2640-FE0F", + "label": "woman surfing: dark skin tone", + "order": 2573, + "unicode": "🏄🏿‍♀️" + } + ] + }, + { + "group": 1, + "hexcode": "1F6A3", + "label": "person rowing boat", + "order": 2575, + "tags": [ + "boat", + "canoe", + "cruise", + "fishing", + "lake", + "oar", + "paddle", + "person", + "raft", + "river", + "row", + "rowboat", + "rowing" + ], + "unicode": "🚣", + "skins": [ + { + "group": 1, + "hexcode": "1F6A3-1F3FB", + "label": "person rowing boat: light skin tone", + "order": 2576, + "unicode": "🚣🏻" + }, + { + "group": 1, + "hexcode": "1F6A3-1F3FC", + "label": "person rowing boat: medium-light skin tone", + "order": 2577, + "unicode": "🚣🏼" + }, + { + "group": 1, + "hexcode": "1F6A3-1F3FD", + "label": "person rowing boat: medium skin tone", + "order": 2578, + "unicode": "🚣🏽" + }, + { + "group": 1, + "hexcode": "1F6A3-1F3FE", + "label": "person rowing boat: medium-dark skin tone", + "order": 2579, + "unicode": "🚣🏾" + }, + { + "group": 1, + "hexcode": "1F6A3-1F3FF", + "label": "person rowing boat: dark skin tone", + "order": 2580, + "unicode": "🚣🏿" + } + ] + }, + { + "group": 1, + "hexcode": "1F6A3-200D-2642-FE0F", + "label": "man rowing boat", + "order": 2581, + "tags": [ + "boat", + "canoe", + "cruise", + "fishing", + "lake", + "man", + "oar", + "paddle", + "raft", + "river", + "row", + "rowboat", + "rowing" + ], + "unicode": "🚣‍♂️", + "skins": [ + { + "group": 1, + "hexcode": "1F6A3-1F3FB-200D-2642-FE0F", + "label": "man rowing boat: light skin tone", + "order": 2583, + "unicode": "🚣🏻‍♂️" + }, + { + "group": 1, + "hexcode": "1F6A3-1F3FC-200D-2642-FE0F", + "label": "man rowing boat: medium-light skin tone", + "order": 2585, + "unicode": "🚣🏼‍♂️" + }, + { + "group": 1, + "hexcode": "1F6A3-1F3FD-200D-2642-FE0F", + "label": "man rowing boat: medium skin tone", + "order": 2587, + "unicode": "🚣🏽‍♂️" + }, + { + "group": 1, + "hexcode": "1F6A3-1F3FE-200D-2642-FE0F", + "label": "man rowing boat: medium-dark skin tone", + "order": 2589, + "unicode": "🚣🏾‍♂️" + }, + { + "group": 1, + "hexcode": "1F6A3-1F3FF-200D-2642-FE0F", + "label": "man rowing boat: dark skin tone", + "order": 2591, + "unicode": "🚣🏿‍♂️" + } + ] + }, + { + "group": 1, + "hexcode": "1F6A3-200D-2640-FE0F", + "label": "woman rowing boat", + "order": 2593, + "tags": [ + "boat", + "canoe", + "cruise", + "fishing", + "lake", + "oar", + "paddle", + "raft", + "river", + "row", + "rowboat", + "rowing", + "woman" + ], + "unicode": "🚣‍♀️", + "skins": [ + { + "group": 1, + "hexcode": "1F6A3-1F3FB-200D-2640-FE0F", + "label": "woman rowing boat: light skin tone", + "order": 2595, + "unicode": "🚣🏻‍♀️" + }, + { + "group": 1, + "hexcode": "1F6A3-1F3FC-200D-2640-FE0F", + "label": "woman rowing boat: medium-light skin tone", + "order": 2597, + "unicode": "🚣🏼‍♀️" + }, + { + "group": 1, + "hexcode": "1F6A3-1F3FD-200D-2640-FE0F", + "label": "woman rowing boat: medium skin tone", + "order": 2599, + "unicode": "🚣🏽‍♀️" + }, + { + "group": 1, + "hexcode": "1F6A3-1F3FE-200D-2640-FE0F", + "label": "woman rowing boat: medium-dark skin tone", + "order": 2601, + "unicode": "🚣🏾‍♀️" + }, + { + "group": 1, + "hexcode": "1F6A3-1F3FF-200D-2640-FE0F", + "label": "woman rowing boat: dark skin tone", + "order": 2603, + "unicode": "🚣🏿‍♀️" + } + ] + }, + { + "group": 1, + "hexcode": "1F3CA", + "label": "person swimming", + "order": 2605, + "tags": [ + "freestyle", + "person", + "sport", + "swim", + "swimmer", + "swimming", + "triathlon" + ], + "unicode": "🏊️", + "skins": [ + { + "group": 1, + "hexcode": "1F3CA-1F3FB", + "label": "person swimming: light skin tone", + "order": 2606, + "unicode": "🏊🏻" + }, + { + "group": 1, + "hexcode": "1F3CA-1F3FC", + "label": "person swimming: medium-light skin tone", + "order": 2607, + "unicode": "🏊🏼" + }, + { + "group": 1, + "hexcode": "1F3CA-1F3FD", + "label": "person swimming: medium skin tone", + "order": 2608, + "unicode": "🏊🏽" + }, + { + "group": 1, + "hexcode": "1F3CA-1F3FE", + "label": "person swimming: medium-dark skin tone", + "order": 2609, + "unicode": "🏊🏾" + }, + { + "group": 1, + "hexcode": "1F3CA-1F3FF", + "label": "person swimming: dark skin tone", + "order": 2610, + "unicode": "🏊🏿" + } + ] + }, + { + "group": 1, + "hexcode": "1F3CA-200D-2642-FE0F", + "label": "man swimming", + "order": 2611, + "tags": [ + "freestyle", + "man", + "sport", + "swim", + "swimmer", + "swimming", + "triathlon" + ], + "unicode": "🏊‍♂️", + "skins": [ + { + "group": 1, + "hexcode": "1F3CA-1F3FB-200D-2642-FE0F", + "label": "man swimming: light skin tone", + "order": 2613, + "unicode": "🏊🏻‍♂️" + }, + { + "group": 1, + "hexcode": "1F3CA-1F3FC-200D-2642-FE0F", + "label": "man swimming: medium-light skin tone", + "order": 2615, + "unicode": "🏊🏼‍♂️" + }, + { + "group": 1, + "hexcode": "1F3CA-1F3FD-200D-2642-FE0F", + "label": "man swimming: medium skin tone", + "order": 2617, + "unicode": "🏊🏽‍♂️" + }, + { + "group": 1, + "hexcode": "1F3CA-1F3FE-200D-2642-FE0F", + "label": "man swimming: medium-dark skin tone", + "order": 2619, + "unicode": "🏊🏾‍♂️" + }, + { + "group": 1, + "hexcode": "1F3CA-1F3FF-200D-2642-FE0F", + "label": "man swimming: dark skin tone", + "order": 2621, + "unicode": "🏊🏿‍♂️" + } + ] + }, + { + "group": 1, + "hexcode": "1F3CA-200D-2640-FE0F", + "label": "woman swimming", + "order": 2623, + "tags": [ + "freestyle", + "man", + "sport", + "swim", + "swimmer", + "swimming", + "triathlon" + ], + "unicode": "🏊‍♀️", + "skins": [ + { + "group": 1, + "hexcode": "1F3CA-1F3FB-200D-2640-FE0F", + "label": "woman swimming: light skin tone", + "order": 2625, + "unicode": "🏊🏻‍♀️" + }, + { + "group": 1, + "hexcode": "1F3CA-1F3FC-200D-2640-FE0F", + "label": "woman swimming: medium-light skin tone", + "order": 2627, + "unicode": "🏊🏼‍♀️" + }, + { + "group": 1, + "hexcode": "1F3CA-1F3FD-200D-2640-FE0F", + "label": "woman swimming: medium skin tone", + "order": 2629, + "unicode": "🏊🏽‍♀️" + }, + { + "group": 1, + "hexcode": "1F3CA-1F3FE-200D-2640-FE0F", + "label": "woman swimming: medium-dark skin tone", + "order": 2631, + "unicode": "🏊🏾‍♀️" + }, + { + "group": 1, + "hexcode": "1F3CA-1F3FF-200D-2640-FE0F", + "label": "woman swimming: dark skin tone", + "order": 2633, + "unicode": "🏊🏿‍♀️" + } + ] + }, + { + "group": 1, + "hexcode": "26F9", + "label": "person bouncing ball", + "order": 2636, + "tags": [ + "athletic", + "ball", + "basketball", + "bouncing", + "championship", + "dribble", + "net", + "person", + "player", + "throw" + ], + "unicode": "⛹️", + "skins": [ + { + "group": 1, + "hexcode": "26F9-1F3FB", + "label": "person bouncing ball: light skin tone", + "order": 2637, + "unicode": "⛹🏻" + }, + { + "group": 1, + "hexcode": "26F9-1F3FC", + "label": "person bouncing ball: medium-light skin tone", + "order": 2638, + "unicode": "⛹🏼" + }, + { + "group": 1, + "hexcode": "26F9-1F3FD", + "label": "person bouncing ball: medium skin tone", + "order": 2639, + "unicode": "⛹🏽" + }, + { + "group": 1, + "hexcode": "26F9-1F3FE", + "label": "person bouncing ball: medium-dark skin tone", + "order": 2640, + "unicode": "⛹🏾" + }, + { + "group": 1, + "hexcode": "26F9-1F3FF", + "label": "person bouncing ball: dark skin tone", + "order": 2641, + "unicode": "⛹🏿" + } + ] + }, + { + "group": 1, + "hexcode": "26F9-FE0F-200D-2642-FE0F", + "label": "man bouncing ball", + "order": 2642, + "tags": [ + "athletic", + "ball", + "basketball", + "bouncing", + "championship", + "dribble", + "man", + "net", + "player", + "throw" + ], + "unicode": "⛹️‍♂️", + "skins": [ + { + "group": 1, + "hexcode": "26F9-1F3FB-200D-2642-FE0F", + "label": "man bouncing ball: light skin tone", + "order": 2646, + "unicode": "⛹🏻‍♂️" + }, + { + "group": 1, + "hexcode": "26F9-1F3FC-200D-2642-FE0F", + "label": "man bouncing ball: medium-light skin tone", + "order": 2648, + "unicode": "⛹🏼‍♂️" + }, + { + "group": 1, + "hexcode": "26F9-1F3FD-200D-2642-FE0F", + "label": "man bouncing ball: medium skin tone", + "order": 2650, + "unicode": "⛹🏽‍♂️" + }, + { + "group": 1, + "hexcode": "26F9-1F3FE-200D-2642-FE0F", + "label": "man bouncing ball: medium-dark skin tone", + "order": 2652, + "unicode": "⛹🏾‍♂️" + }, + { + "group": 1, + "hexcode": "26F9-1F3FF-200D-2642-FE0F", + "label": "man bouncing ball: dark skin tone", + "order": 2654, + "unicode": "⛹🏿‍♂️" + } + ] + }, + { + "group": 1, + "hexcode": "26F9-FE0F-200D-2640-FE0F", + "label": "woman bouncing ball", + "order": 2656, + "tags": [ + "athletic", + "ball", + "basketball", + "bouncing", + "championship", + "dribble", + "net", + "player", + "throw", + "woman" + ], + "unicode": "⛹️‍♀️", + "skins": [ + { + "group": 1, + "hexcode": "26F9-1F3FB-200D-2640-FE0F", + "label": "woman bouncing ball: light skin tone", + "order": 2660, + "unicode": "⛹🏻‍♀️" + }, + { + "group": 1, + "hexcode": "26F9-1F3FC-200D-2640-FE0F", + "label": "woman bouncing ball: medium-light skin tone", + "order": 2662, + "unicode": "⛹🏼‍♀️" + }, + { + "group": 1, + "hexcode": "26F9-1F3FD-200D-2640-FE0F", + "label": "woman bouncing ball: medium skin tone", + "order": 2664, + "unicode": "⛹🏽‍♀️" + }, + { + "group": 1, + "hexcode": "26F9-1F3FE-200D-2640-FE0F", + "label": "woman bouncing ball: medium-dark skin tone", + "order": 2666, + "unicode": "⛹🏾‍♀️" + }, + { + "group": 1, + "hexcode": "26F9-1F3FF-200D-2640-FE0F", + "label": "woman bouncing ball: dark skin tone", + "order": 2668, + "unicode": "⛹🏿‍♀️" + } + ] + }, + { + "group": 1, + "hexcode": "1F3CB", + "label": "person lifting weights", + "order": 2671, + "tags": [ + "barbell", + "bodybuilder", + "deadlift", + "lifter", + "lifting", + "person", + "powerlifting", + "weight", + "weightlifter", + "weights", + "workout" + ], + "unicode": "🏋️", + "skins": [ + { + "group": 1, + "hexcode": "1F3CB-1F3FB", + "label": "person lifting weights: light skin tone", + "order": 2672, + "unicode": "🏋🏻" + }, + { + "group": 1, + "hexcode": "1F3CB-1F3FC", + "label": "person lifting weights: medium-light skin tone", + "order": 2673, + "unicode": "🏋🏼" + }, + { + "group": 1, + "hexcode": "1F3CB-1F3FD", + "label": "person lifting weights: medium skin tone", + "order": 2674, + "unicode": "🏋🏽" + }, + { + "group": 1, + "hexcode": "1F3CB-1F3FE", + "label": "person lifting weights: medium-dark skin tone", + "order": 2675, + "unicode": "🏋🏾" + }, + { + "group": 1, + "hexcode": "1F3CB-1F3FF", + "label": "person lifting weights: dark skin tone", + "order": 2676, + "unicode": "🏋🏿" + } + ] + }, + { + "group": 1, + "hexcode": "1F3CB-FE0F-200D-2642-FE0F", + "label": "man lifting weights", + "order": 2677, + "tags": [ + "barbell", + "bodybuilder", + "deadlift", + "lifter", + "lifting", + "man", + "powerlifting", + "weight", + "weightlifter", + "weights", + "workout" + ], + "unicode": "🏋️‍♂️", + "skins": [ + { + "group": 1, + "hexcode": "1F3CB-1F3FB-200D-2642-FE0F", + "label": "man lifting weights: light skin tone", + "order": 2681, + "unicode": "🏋🏻‍♂️" + }, + { + "group": 1, + "hexcode": "1F3CB-1F3FC-200D-2642-FE0F", + "label": "man lifting weights: medium-light skin tone", + "order": 2683, + "unicode": "🏋🏼‍♂️" + }, + { + "group": 1, + "hexcode": "1F3CB-1F3FD-200D-2642-FE0F", + "label": "man lifting weights: medium skin tone", + "order": 2685, + "unicode": "🏋🏽‍♂️" + }, + { + "group": 1, + "hexcode": "1F3CB-1F3FE-200D-2642-FE0F", + "label": "man lifting weights: medium-dark skin tone", + "order": 2687, + "unicode": "🏋🏾‍♂️" + }, + { + "group": 1, + "hexcode": "1F3CB-1F3FF-200D-2642-FE0F", + "label": "man lifting weights: dark skin tone", + "order": 2689, + "unicode": "🏋🏿‍♂️" + } + ] + }, + { + "group": 1, + "hexcode": "1F3CB-FE0F-200D-2640-FE0F", + "label": "woman lifting weights", + "order": 2691, + "tags": [ + "barbell", + "bodybuilder", + "deadlift", + "lifter", + "lifting", + "powerlifting", + "weight", + "weightlifter", + "weights", + "woman", + "workout" + ], + "unicode": "🏋️‍♀️", + "skins": [ + { + "group": 1, + "hexcode": "1F3CB-1F3FB-200D-2640-FE0F", + "label": "woman lifting weights: light skin tone", + "order": 2695, + "unicode": "🏋🏻‍♀️" + }, + { + "group": 1, + "hexcode": "1F3CB-1F3FC-200D-2640-FE0F", + "label": "woman lifting weights: medium-light skin tone", + "order": 2697, + "unicode": "🏋🏼‍♀️" + }, + { + "group": 1, + "hexcode": "1F3CB-1F3FD-200D-2640-FE0F", + "label": "woman lifting weights: medium skin tone", + "order": 2699, + "unicode": "🏋🏽‍♀️" + }, + { + "group": 1, + "hexcode": "1F3CB-1F3FE-200D-2640-FE0F", + "label": "woman lifting weights: medium-dark skin tone", + "order": 2701, + "unicode": "🏋🏾‍♀️" + }, + { + "group": 1, + "hexcode": "1F3CB-1F3FF-200D-2640-FE0F", + "label": "woman lifting weights: dark skin tone", + "order": 2703, + "unicode": "🏋🏿‍♀️" + } + ] + }, + { + "group": 1, + "hexcode": "1F6B4", + "label": "person biking", + "order": 2705, + "tags": [ + "bicycle", + "bicyclist", + "bike", + "biking", + "cycle", + "cyclist", + "person", + "riding", + "sport" + ], + "unicode": "🚴", + "skins": [ + { + "group": 1, + "hexcode": "1F6B4-1F3FB", + "label": "person biking: light skin tone", + "order": 2706, + "unicode": "🚴🏻" + }, + { + "group": 1, + "hexcode": "1F6B4-1F3FC", + "label": "person biking: medium-light skin tone", + "order": 2707, + "unicode": "🚴🏼" + }, + { + "group": 1, + "hexcode": "1F6B4-1F3FD", + "label": "person biking: medium skin tone", + "order": 2708, + "unicode": "🚴🏽" + }, + { + "group": 1, + "hexcode": "1F6B4-1F3FE", + "label": "person biking: medium-dark skin tone", + "order": 2709, + "unicode": "🚴🏾" + }, + { + "group": 1, + "hexcode": "1F6B4-1F3FF", + "label": "person biking: dark skin tone", + "order": 2710, + "unicode": "🚴🏿" + } + ] + }, + { + "group": 1, + "hexcode": "1F6B4-200D-2642-FE0F", + "label": "man biking", + "order": 2711, + "tags": [ + "bicycle", + "bicyclist", + "bike", + "biking", + "cycle", + "cyclist", + "man", + "riding", + "sport" + ], + "unicode": "🚴‍♂️", + "skins": [ + { + "group": 1, + "hexcode": "1F6B4-1F3FB-200D-2642-FE0F", + "label": "man biking: light skin tone", + "order": 2713, + "unicode": "🚴🏻‍♂️" + }, + { + "group": 1, + "hexcode": "1F6B4-1F3FC-200D-2642-FE0F", + "label": "man biking: medium-light skin tone", + "order": 2715, + "unicode": "🚴🏼‍♂️" + }, + { + "group": 1, + "hexcode": "1F6B4-1F3FD-200D-2642-FE0F", + "label": "man biking: medium skin tone", + "order": 2717, + "unicode": "🚴🏽‍♂️" + }, + { + "group": 1, + "hexcode": "1F6B4-1F3FE-200D-2642-FE0F", + "label": "man biking: medium-dark skin tone", + "order": 2719, + "unicode": "🚴🏾‍♂️" + }, + { + "group": 1, + "hexcode": "1F6B4-1F3FF-200D-2642-FE0F", + "label": "man biking: dark skin tone", + "order": 2721, + "unicode": "🚴🏿‍♂️" + } + ] + }, + { + "group": 1, + "hexcode": "1F6B4-200D-2640-FE0F", + "label": "woman biking", + "order": 2723, + "tags": [ + "bicycle", + "bicyclist", + "bike", + "biking", + "cycle", + "cyclist", + "riding", + "sport", + "woman" + ], + "unicode": "🚴‍♀️", + "skins": [ + { + "group": 1, + "hexcode": "1F6B4-1F3FB-200D-2640-FE0F", + "label": "woman biking: light skin tone", + "order": 2725, + "unicode": "🚴🏻‍♀️" + }, + { + "group": 1, + "hexcode": "1F6B4-1F3FC-200D-2640-FE0F", + "label": "woman biking: medium-light skin tone", + "order": 2727, + "unicode": "🚴🏼‍♀️" + }, + { + "group": 1, + "hexcode": "1F6B4-1F3FD-200D-2640-FE0F", + "label": "woman biking: medium skin tone", + "order": 2729, + "unicode": "🚴🏽‍♀️" + }, + { + "group": 1, + "hexcode": "1F6B4-1F3FE-200D-2640-FE0F", + "label": "woman biking: medium-dark skin tone", + "order": 2731, + "unicode": "🚴🏾‍♀️" + }, + { + "group": 1, + "hexcode": "1F6B4-1F3FF-200D-2640-FE0F", + "label": "woman biking: dark skin tone", + "order": 2733, + "unicode": "🚴🏿‍♀️" + } + ] + }, + { + "group": 1, + "hexcode": "1F6B5", + "label": "person mountain biking", + "order": 2735, + "tags": [ + "bicycle", + "bicyclist", + "bike", + "biking", + "cycle", + "cyclist", + "mountain", + "person", + "riding", + "sport" + ], + "unicode": "🚵", + "skins": [ + { + "group": 1, + "hexcode": "1F6B5-1F3FB", + "label": "person mountain biking: light skin tone", + "order": 2736, + "unicode": "🚵🏻" + }, + { + "group": 1, + "hexcode": "1F6B5-1F3FC", + "label": "person mountain biking: medium-light skin tone", + "order": 2737, + "unicode": "🚵🏼" + }, + { + "group": 1, + "hexcode": "1F6B5-1F3FD", + "label": "person mountain biking: medium skin tone", + "order": 2738, + "unicode": "🚵🏽" + }, + { + "group": 1, + "hexcode": "1F6B5-1F3FE", + "label": "person mountain biking: medium-dark skin tone", + "order": 2739, + "unicode": "🚵🏾" + }, + { + "group": 1, + "hexcode": "1F6B5-1F3FF", + "label": "person mountain biking: dark skin tone", + "order": 2740, + "unicode": "🚵🏿" + } + ] + }, + { + "group": 1, + "hexcode": "1F6B5-200D-2642-FE0F", + "label": "man mountain biking", + "order": 2741, + "tags": [ + "bicycle", + "bicyclist", + "bike", + "biking", + "cycle", + "cyclist", + "man", + "mountain", + "riding", + "sport" + ], + "unicode": "🚵‍♂️", + "skins": [ + { + "group": 1, + "hexcode": "1F6B5-1F3FB-200D-2642-FE0F", + "label": "man mountain biking: light skin tone", + "order": 2743, + "unicode": "🚵🏻‍♂️" + }, + { + "group": 1, + "hexcode": "1F6B5-1F3FC-200D-2642-FE0F", + "label": "man mountain biking: medium-light skin tone", + "order": 2745, + "unicode": "🚵🏼‍♂️" + }, + { + "group": 1, + "hexcode": "1F6B5-1F3FD-200D-2642-FE0F", + "label": "man mountain biking: medium skin tone", + "order": 2747, + "unicode": "🚵🏽‍♂️" + }, + { + "group": 1, + "hexcode": "1F6B5-1F3FE-200D-2642-FE0F", + "label": "man mountain biking: medium-dark skin tone", + "order": 2749, + "unicode": "🚵🏾‍♂️" + }, + { + "group": 1, + "hexcode": "1F6B5-1F3FF-200D-2642-FE0F", + "label": "man mountain biking: dark skin tone", + "order": 2751, + "unicode": "🚵🏿‍♂️" + } + ] + }, + { + "group": 1, + "hexcode": "1F6B5-200D-2640-FE0F", + "label": "woman mountain biking", + "order": 2753, + "tags": [ + "bicycle", + "bicyclist", + "bike", + "biking", + "cycle", + "cyclist", + "mountain", + "riding", + "sport", + "woman" + ], + "unicode": "🚵‍♀️", + "skins": [ + { + "group": 1, + "hexcode": "1F6B5-1F3FB-200D-2640-FE0F", + "label": "woman mountain biking: light skin tone", + "order": 2755, + "unicode": "🚵🏻‍♀️" + }, + { + "group": 1, + "hexcode": "1F6B5-1F3FC-200D-2640-FE0F", + "label": "woman mountain biking: medium-light skin tone", + "order": 2757, + "unicode": "🚵🏼‍♀️" + }, + { + "group": 1, + "hexcode": "1F6B5-1F3FD-200D-2640-FE0F", + "label": "woman mountain biking: medium skin tone", + "order": 2759, + "unicode": "🚵🏽‍♀️" + }, + { + "group": 1, + "hexcode": "1F6B5-1F3FE-200D-2640-FE0F", + "label": "woman mountain biking: medium-dark skin tone", + "order": 2761, + "unicode": "🚵🏾‍♀️" + }, + { + "group": 1, + "hexcode": "1F6B5-1F3FF-200D-2640-FE0F", + "label": "woman mountain biking: dark skin tone", + "order": 2763, + "unicode": "🚵🏿‍♀️" + } + ] + }, + { + "group": 1, + "hexcode": "1F938", + "label": "person cartwheeling", + "order": 2765, + "tags": [ + "active", + "cartwheel", + "cartwheeling", + "excited", + "flip", + "gymnastics", + "happy", + "person", + "somersault" + ], + "unicode": "🤸", + "skins": [ + { + "group": 1, + "hexcode": "1F938-1F3FB", + "label": "person cartwheeling: light skin tone", + "order": 2766, + "unicode": "🤸🏻" + }, + { + "group": 1, + "hexcode": "1F938-1F3FC", + "label": "person cartwheeling: medium-light skin tone", + "order": 2767, + "unicode": "🤸🏼" + }, + { + "group": 1, + "hexcode": "1F938-1F3FD", + "label": "person cartwheeling: medium skin tone", + "order": 2768, + "unicode": "🤸🏽" + }, + { + "group": 1, + "hexcode": "1F938-1F3FE", + "label": "person cartwheeling: medium-dark skin tone", + "order": 2769, + "unicode": "🤸🏾" + }, + { + "group": 1, + "hexcode": "1F938-1F3FF", + "label": "person cartwheeling: dark skin tone", + "order": 2770, + "unicode": "🤸🏿" + } + ] + }, + { + "group": 1, + "hexcode": "1F938-200D-2642-FE0F", + "label": "man cartwheeling", + "order": 2771, + "tags": [ + "active", + "cartwheel", + "cartwheeling", + "excited", + "flip", + "gymnastics", + "happy", + "man", + "somersault" + ], + "unicode": "🤸‍♂️", + "skins": [ + { + "group": 1, + "hexcode": "1F938-1F3FB-200D-2642-FE0F", + "label": "man cartwheeling: light skin tone", + "order": 2773, + "unicode": "🤸🏻‍♂️" + }, + { + "group": 1, + "hexcode": "1F938-1F3FC-200D-2642-FE0F", + "label": "man cartwheeling: medium-light skin tone", + "order": 2775, + "unicode": "🤸🏼‍♂️" + }, + { + "group": 1, + "hexcode": "1F938-1F3FD-200D-2642-FE0F", + "label": "man cartwheeling: medium skin tone", + "order": 2777, + "unicode": "🤸🏽‍♂️" + }, + { + "group": 1, + "hexcode": "1F938-1F3FE-200D-2642-FE0F", + "label": "man cartwheeling: medium-dark skin tone", + "order": 2779, + "unicode": "🤸🏾‍♂️" + }, + { + "group": 1, + "hexcode": "1F938-1F3FF-200D-2642-FE0F", + "label": "man cartwheeling: dark skin tone", + "order": 2781, + "unicode": "🤸🏿‍♂️" + } + ] + }, + { + "group": 1, + "hexcode": "1F938-200D-2640-FE0F", + "label": "woman cartwheeling", + "order": 2783, + "tags": [ + "active", + "cartwheel", + "cartwheeling", + "excited", + "flip", + "gymnastics", + "happy", + "somersault", + "woman" + ], + "unicode": "🤸‍♀️", + "skins": [ + { + "group": 1, + "hexcode": "1F938-1F3FB-200D-2640-FE0F", + "label": "woman cartwheeling: light skin tone", + "order": 2785, + "unicode": "🤸🏻‍♀️" + }, + { + "group": 1, + "hexcode": "1F938-1F3FC-200D-2640-FE0F", + "label": "woman cartwheeling: medium-light skin tone", + "order": 2787, + "unicode": "🤸🏼‍♀️" + }, + { + "group": 1, + "hexcode": "1F938-1F3FD-200D-2640-FE0F", + "label": "woman cartwheeling: medium skin tone", + "order": 2789, + "unicode": "🤸🏽‍♀️" + }, + { + "group": 1, + "hexcode": "1F938-1F3FE-200D-2640-FE0F", + "label": "woman cartwheeling: medium-dark skin tone", + "order": 2791, + "unicode": "🤸🏾‍♀️" + }, + { + "group": 1, + "hexcode": "1F938-1F3FF-200D-2640-FE0F", + "label": "woman cartwheeling: dark skin tone", + "order": 2793, + "unicode": "🤸🏿‍♀️" + } + ] + }, + { + "group": 1, + "hexcode": "1F93C", + "label": "people wrestling", + "order": 2795, + "tags": [ + "combat", + "duel", + "grapple", + "people", + "ring", + "tournament", + "wrestle", + "wrestling" + ], + "unicode": "🤼" + }, + { + "group": 1, + "hexcode": "1F93C-200D-2642-FE0F", + "label": "men wrestling", + "order": 2796, + "tags": [ + "combat", + "duel", + "grapple", + "men", + "ring", + "tournament", + "wrestle", + "wrestling" + ], + "unicode": "🤼‍♂️" + }, + { + "group": 1, + "hexcode": "1F93C-200D-2640-FE0F", + "label": "women wrestling", + "order": 2798, + "tags": [ + "combat", + "duel", + "grapple", + "ring", + "tournament", + "women", + "wrestle", + "wrestling" + ], + "unicode": "🤼‍♀️" + }, + { + "group": 1, + "hexcode": "1F93D", + "label": "person playing water polo", + "order": 2800, + "tags": [ + "person", + "playing", + "polo", + "sport", + "swimming", + "water", + "waterpolo" + ], + "unicode": "🤽", + "skins": [ + { + "group": 1, + "hexcode": "1F93D-1F3FB", + "label": "person playing water polo: light skin tone", + "order": 2801, + "unicode": "🤽🏻" + }, + { + "group": 1, + "hexcode": "1F93D-1F3FC", + "label": "person playing water polo: medium-light skin tone", + "order": 2802, + "unicode": "🤽🏼" + }, + { + "group": 1, + "hexcode": "1F93D-1F3FD", + "label": "person playing water polo: medium skin tone", + "order": 2803, + "unicode": "🤽🏽" + }, + { + "group": 1, + "hexcode": "1F93D-1F3FE", + "label": "person playing water polo: medium-dark skin tone", + "order": 2804, + "unicode": "🤽🏾" + }, + { + "group": 1, + "hexcode": "1F93D-1F3FF", + "label": "person playing water polo: dark skin tone", + "order": 2805, + "unicode": "🤽🏿" + } + ] + }, + { + "group": 1, + "hexcode": "1F93D-200D-2642-FE0F", + "label": "man playing water polo", + "order": 2806, + "tags": [ + "man", + "playing", + "polo", + "sport", + "swimming", + "water", + "waterpolo" + ], + "unicode": "🤽‍♂️", + "skins": [ + { + "group": 1, + "hexcode": "1F93D-1F3FB-200D-2642-FE0F", + "label": "man playing water polo: light skin tone", + "order": 2808, + "unicode": "🤽🏻‍♂️" + }, + { + "group": 1, + "hexcode": "1F93D-1F3FC-200D-2642-FE0F", + "label": "man playing water polo: medium-light skin tone", + "order": 2810, + "unicode": "🤽🏼‍♂️" + }, + { + "group": 1, + "hexcode": "1F93D-1F3FD-200D-2642-FE0F", + "label": "man playing water polo: medium skin tone", + "order": 2812, + "unicode": "🤽🏽‍♂️" + }, + { + "group": 1, + "hexcode": "1F93D-1F3FE-200D-2642-FE0F", + "label": "man playing water polo: medium-dark skin tone", + "order": 2814, + "unicode": "🤽🏾‍♂️" + }, + { + "group": 1, + "hexcode": "1F93D-1F3FF-200D-2642-FE0F", + "label": "man playing water polo: dark skin tone", + "order": 2816, + "unicode": "🤽🏿‍♂️" + } + ] + }, + { + "group": 1, + "hexcode": "1F93D-200D-2640-FE0F", + "label": "woman playing water polo", + "order": 2818, + "tags": [ + "playing", + "polo", + "sport", + "swimming", + "water", + "waterpolo", + "woman" + ], + "unicode": "🤽‍♀️", + "skins": [ + { + "group": 1, + "hexcode": "1F93D-1F3FB-200D-2640-FE0F", + "label": "woman playing water polo: light skin tone", + "order": 2820, + "unicode": "🤽🏻‍♀️" + }, + { + "group": 1, + "hexcode": "1F93D-1F3FC-200D-2640-FE0F", + "label": "woman playing water polo: medium-light skin tone", + "order": 2822, + "unicode": "🤽🏼‍♀️" + }, + { + "group": 1, + "hexcode": "1F93D-1F3FD-200D-2640-FE0F", + "label": "woman playing water polo: medium skin tone", + "order": 2824, + "unicode": "🤽🏽‍♀️" + }, + { + "group": 1, + "hexcode": "1F93D-1F3FE-200D-2640-FE0F", + "label": "woman playing water polo: medium-dark skin tone", + "order": 2826, + "unicode": "🤽🏾‍♀️" + }, + { + "group": 1, + "hexcode": "1F93D-1F3FF-200D-2640-FE0F", + "label": "woman playing water polo: dark skin tone", + "order": 2828, + "unicode": "🤽🏿‍♀️" + } + ] + }, + { + "group": 1, + "hexcode": "1F93E", + "label": "person playing handball", + "order": 2830, + "tags": [ + "athletics", + "ball", + "catch", + "chuck", + "handball", + "hurl", + "lob", + "person", + "pitch", + "playing", + "sport", + "throw", + "toss" + ], + "unicode": "🤾", + "skins": [ + { + "group": 1, + "hexcode": "1F93E-1F3FB", + "label": "person playing handball: light skin tone", + "order": 2831, + "unicode": "🤾🏻" + }, + { + "group": 1, + "hexcode": "1F93E-1F3FC", + "label": "person playing handball: medium-light skin tone", + "order": 2832, + "unicode": "🤾🏼" + }, + { + "group": 1, + "hexcode": "1F93E-1F3FD", + "label": "person playing handball: medium skin tone", + "order": 2833, + "unicode": "🤾🏽" + }, + { + "group": 1, + "hexcode": "1F93E-1F3FE", + "label": "person playing handball: medium-dark skin tone", + "order": 2834, + "unicode": "🤾🏾" + }, + { + "group": 1, + "hexcode": "1F93E-1F3FF", + "label": "person playing handball: dark skin tone", + "order": 2835, + "unicode": "🤾🏿" + } + ] + }, + { + "group": 1, + "hexcode": "1F93E-200D-2642-FE0F", + "label": "man playing handball", + "order": 2836, + "tags": [ + "athletics", + "ball", + "catch", + "chuck", + "handball", + "hurl", + "lob", + "man", + "pitch", + "playing", + "sport", + "throw", + "toss" + ], + "unicode": "🤾‍♂️", + "skins": [ + { + "group": 1, + "hexcode": "1F93E-1F3FB-200D-2642-FE0F", + "label": "man playing handball: light skin tone", + "order": 2838, + "unicode": "🤾🏻‍♂️" + }, + { + "group": 1, + "hexcode": "1F93E-1F3FC-200D-2642-FE0F", + "label": "man playing handball: medium-light skin tone", + "order": 2840, + "unicode": "🤾🏼‍♂️" + }, + { + "group": 1, + "hexcode": "1F93E-1F3FD-200D-2642-FE0F", + "label": "man playing handball: medium skin tone", + "order": 2842, + "unicode": "🤾🏽‍♂️" + }, + { + "group": 1, + "hexcode": "1F93E-1F3FE-200D-2642-FE0F", + "label": "man playing handball: medium-dark skin tone", + "order": 2844, + "unicode": "🤾🏾‍♂️" + }, + { + "group": 1, + "hexcode": "1F93E-1F3FF-200D-2642-FE0F", + "label": "man playing handball: dark skin tone", + "order": 2846, + "unicode": "🤾🏿‍♂️" + } + ] + }, + { + "group": 1, + "hexcode": "1F93E-200D-2640-FE0F", + "label": "woman playing handball", + "order": 2848, + "tags": [ + "athletics", + "ball", + "catch", + "chuck", + "handball", + "hurl", + "lob", + "pitch", + "playing", + "sport", + "throw", + "toss", + "woman" + ], + "unicode": "🤾‍♀️", + "skins": [ + { + "group": 1, + "hexcode": "1F93E-1F3FB-200D-2640-FE0F", + "label": "woman playing handball: light skin tone", + "order": 2850, + "unicode": "🤾🏻‍♀️" + }, + { + "group": 1, + "hexcode": "1F93E-1F3FC-200D-2640-FE0F", + "label": "woman playing handball: medium-light skin tone", + "order": 2852, + "unicode": "🤾🏼‍♀️" + }, + { + "group": 1, + "hexcode": "1F93E-1F3FD-200D-2640-FE0F", + "label": "woman playing handball: medium skin tone", + "order": 2854, + "unicode": "🤾🏽‍♀️" + }, + { + "group": 1, + "hexcode": "1F93E-1F3FE-200D-2640-FE0F", + "label": "woman playing handball: medium-dark skin tone", + "order": 2856, + "unicode": "🤾🏾‍♀️" + }, + { + "group": 1, + "hexcode": "1F93E-1F3FF-200D-2640-FE0F", + "label": "woman playing handball: dark skin tone", + "order": 2858, + "unicode": "🤾🏿‍♀️" + } + ] + }, + { + "group": 1, + "hexcode": "1F939", + "label": "person juggling", + "order": 2860, + "tags": [ + "act", + "balance", + "balancing", + "handle", + "juggle", + "juggling", + "manage", + "multitask", + "person", + "skill" + ], + "unicode": "🤹", + "skins": [ + { + "group": 1, + "hexcode": "1F939-1F3FB", + "label": "person juggling: light skin tone", + "order": 2861, + "unicode": "🤹🏻" + }, + { + "group": 1, + "hexcode": "1F939-1F3FC", + "label": "person juggling: medium-light skin tone", + "order": 2862, + "unicode": "🤹🏼" + }, + { + "group": 1, + "hexcode": "1F939-1F3FD", + "label": "person juggling: medium skin tone", + "order": 2863, + "unicode": "🤹🏽" + }, + { + "group": 1, + "hexcode": "1F939-1F3FE", + "label": "person juggling: medium-dark skin tone", + "order": 2864, + "unicode": "🤹🏾" + }, + { + "group": 1, + "hexcode": "1F939-1F3FF", + "label": "person juggling: dark skin tone", + "order": 2865, + "unicode": "🤹🏿" + } + ] + }, + { + "group": 1, + "hexcode": "1F939-200D-2642-FE0F", + "label": "man juggling", + "order": 2866, + "tags": [ + "act", + "balance", + "balancing", + "handle", + "juggle", + "juggling", + "man", + "manage", + "multitask", + "skill" + ], + "unicode": "🤹‍♂️", + "skins": [ + { + "group": 1, + "hexcode": "1F939-1F3FB-200D-2642-FE0F", + "label": "man juggling: light skin tone", + "order": 2868, + "unicode": "🤹🏻‍♂️" + }, + { + "group": 1, + "hexcode": "1F939-1F3FC-200D-2642-FE0F", + "label": "man juggling: medium-light skin tone", + "order": 2870, + "unicode": "🤹🏼‍♂️" + }, + { + "group": 1, + "hexcode": "1F939-1F3FD-200D-2642-FE0F", + "label": "man juggling: medium skin tone", + "order": 2872, + "unicode": "🤹🏽‍♂️" + }, + { + "group": 1, + "hexcode": "1F939-1F3FE-200D-2642-FE0F", + "label": "man juggling: medium-dark skin tone", + "order": 2874, + "unicode": "🤹🏾‍♂️" + }, + { + "group": 1, + "hexcode": "1F939-1F3FF-200D-2642-FE0F", + "label": "man juggling: dark skin tone", + "order": 2876, + "unicode": "🤹🏿‍♂️" + } + ] + }, + { + "group": 1, + "hexcode": "1F939-200D-2640-FE0F", + "label": "woman juggling", + "order": 2878, + "tags": [ + "act", + "balance", + "balancing", + "handle", + "juggle", + "juggling", + "manage", + "multitask", + "skill", + "woman" + ], + "unicode": "🤹‍♀️", + "skins": [ + { + "group": 1, + "hexcode": "1F939-1F3FB-200D-2640-FE0F", + "label": "woman juggling: light skin tone", + "order": 2880, + "unicode": "🤹🏻‍♀️" + }, + { + "group": 1, + "hexcode": "1F939-1F3FC-200D-2640-FE0F", + "label": "woman juggling: medium-light skin tone", + "order": 2882, + "unicode": "🤹🏼‍♀️" + }, + { + "group": 1, + "hexcode": "1F939-1F3FD-200D-2640-FE0F", + "label": "woman juggling: medium skin tone", + "order": 2884, + "unicode": "🤹🏽‍♀️" + }, + { + "group": 1, + "hexcode": "1F939-1F3FE-200D-2640-FE0F", + "label": "woman juggling: medium-dark skin tone", + "order": 2886, + "unicode": "🤹🏾‍♀️" + }, + { + "group": 1, + "hexcode": "1F939-1F3FF-200D-2640-FE0F", + "label": "woman juggling: dark skin tone", + "order": 2888, + "unicode": "🤹🏿‍♀️" + } + ] + }, + { + "group": 1, + "hexcode": "1F9D8", + "label": "person in lotus position", + "order": 2890, + "tags": [ + "cross", + "legged", + "legs", + "lotus", + "meditation", + "peace", + "person", + "position", + "relax", + "serenity", + "yoga", + "yogi", + "zen" + ], + "unicode": "🧘", + "skins": [ + { + "group": 1, + "hexcode": "1F9D8-1F3FB", + "label": "person in lotus position: light skin tone", + "order": 2891, + "unicode": "🧘🏻" + }, + { + "group": 1, + "hexcode": "1F9D8-1F3FC", + "label": "person in lotus position: medium-light skin tone", + "order": 2892, + "unicode": "🧘🏼" + }, + { + "group": 1, + "hexcode": "1F9D8-1F3FD", + "label": "person in lotus position: medium skin tone", + "order": 2893, + "unicode": "🧘🏽" + }, + { + "group": 1, + "hexcode": "1F9D8-1F3FE", + "label": "person in lotus position: medium-dark skin tone", + "order": 2894, + "unicode": "🧘🏾" + }, + { + "group": 1, + "hexcode": "1F9D8-1F3FF", + "label": "person in lotus position: dark skin tone", + "order": 2895, + "unicode": "🧘🏿" + } + ] + }, + { + "group": 1, + "hexcode": "1F9D8-200D-2642-FE0F", + "label": "man in lotus position", + "order": 2896, + "tags": [ + "cross", + "legged", + "legs", + "lotus", + "man", + "meditation", + "peace", + "position", + "relax", + "serenity", + "yoga", + "yogi", + "zen" + ], + "unicode": "🧘‍♂️", + "skins": [ + { + "group": 1, + "hexcode": "1F9D8-1F3FB-200D-2642-FE0F", + "label": "man in lotus position: light skin tone", + "order": 2898, + "unicode": "🧘🏻‍♂️" + }, + { + "group": 1, + "hexcode": "1F9D8-1F3FC-200D-2642-FE0F", + "label": "man in lotus position: medium-light skin tone", + "order": 2900, + "unicode": "🧘🏼‍♂️" + }, + { + "group": 1, + "hexcode": "1F9D8-1F3FD-200D-2642-FE0F", + "label": "man in lotus position: medium skin tone", + "order": 2902, + "unicode": "🧘🏽‍♂️" + }, + { + "group": 1, + "hexcode": "1F9D8-1F3FE-200D-2642-FE0F", + "label": "man in lotus position: medium-dark skin tone", + "order": 2904, + "unicode": "🧘🏾‍♂️" + }, + { + "group": 1, + "hexcode": "1F9D8-1F3FF-200D-2642-FE0F", + "label": "man in lotus position: dark skin tone", + "order": 2906, + "unicode": "🧘🏿‍♂️" + } + ] + }, + { + "group": 1, + "hexcode": "1F9D8-200D-2640-FE0F", + "label": "woman in lotus position", + "order": 2908, + "tags": [ + "cross", + "legged", + "legs", + "lotus", + "meditation", + "peace", + "position", + "relax", + "serenity", + "woman", + "yoga", + "yogi", + "zen" + ], + "unicode": "🧘‍♀️", + "skins": [ + { + "group": 1, + "hexcode": "1F9D8-1F3FB-200D-2640-FE0F", + "label": "woman in lotus position: light skin tone", + "order": 2910, + "unicode": "🧘🏻‍♀️" + }, + { + "group": 1, + "hexcode": "1F9D8-1F3FC-200D-2640-FE0F", + "label": "woman in lotus position: medium-light skin tone", + "order": 2912, + "unicode": "🧘🏼‍♀️" + }, + { + "group": 1, + "hexcode": "1F9D8-1F3FD-200D-2640-FE0F", + "label": "woman in lotus position: medium skin tone", + "order": 2914, + "unicode": "🧘🏽‍♀️" + }, + { + "group": 1, + "hexcode": "1F9D8-1F3FE-200D-2640-FE0F", + "label": "woman in lotus position: medium-dark skin tone", + "order": 2916, + "unicode": "🧘🏾‍♀️" + }, + { + "group": 1, + "hexcode": "1F9D8-1F3FF-200D-2640-FE0F", + "label": "woman in lotus position: dark skin tone", + "order": 2918, + "unicode": "🧘🏿‍♀️" + } + ] + }, + { + "group": 1, + "hexcode": "1F6C0", + "label": "person taking bath", + "order": 2920, + "tags": ["bath", "bathtub", "person", "taking", "tub"], + "unicode": "🛀", + "skins": [ + { + "group": 1, + "hexcode": "1F6C0-1F3FB", + "label": "person taking bath: light skin tone", + "order": 2921, + "unicode": "🛀🏻" + }, + { + "group": 1, + "hexcode": "1F6C0-1F3FC", + "label": "person taking bath: medium-light skin tone", + "order": 2922, + "unicode": "🛀🏼" + }, + { + "group": 1, + "hexcode": "1F6C0-1F3FD", + "label": "person taking bath: medium skin tone", + "order": 2923, + "unicode": "🛀🏽" + }, + { + "group": 1, + "hexcode": "1F6C0-1F3FE", + "label": "person taking bath: medium-dark skin tone", + "order": 2924, + "unicode": "🛀🏾" + }, + { + "group": 1, + "hexcode": "1F6C0-1F3FF", + "label": "person taking bath: dark skin tone", + "order": 2925, + "unicode": "🛀🏿" + } + ] + }, + { + "group": 1, + "hexcode": "1F6CC", + "label": "person in bed", + "order": 2926, + "tags": [ + "bed", + "bedtime", + "good", + "goodnight", + "hotel", + "nap", + "night", + "person", + "sleep", + "tired", + "zzz" + ], + "unicode": "🛌", + "skins": [ + { + "group": 1, + "hexcode": "1F6CC-1F3FB", + "label": "person in bed: light skin tone", + "order": 2927, + "unicode": "🛌🏻" + }, + { + "group": 1, + "hexcode": "1F6CC-1F3FC", + "label": "person in bed: medium-light skin tone", + "order": 2928, + "unicode": "🛌🏼" + }, + { + "group": 1, + "hexcode": "1F6CC-1F3FD", + "label": "person in bed: medium skin tone", + "order": 2929, + "unicode": "🛌🏽" + }, + { + "group": 1, + "hexcode": "1F6CC-1F3FE", + "label": "person in bed: medium-dark skin tone", + "order": 2930, + "unicode": "🛌🏾" + }, + { + "group": 1, + "hexcode": "1F6CC-1F3FF", + "label": "person in bed: dark skin tone", + "order": 2931, + "unicode": "🛌🏿" + } + ] + }, + { + "group": 1, + "hexcode": "1F9D1-200D-1F91D-200D-1F9D1", + "label": "people holding hands", + "order": 2932, + "tags": [ + "bae", + "bestie", + "bff", + "couple", + "dating", + "flirt", + "friends", + "hand", + "hold", + "people", + "twins" + ], + "unicode": "🧑‍🤝‍🧑", + "skins": [ + { + "group": 1, + "hexcode": "1F9D1-1F3FB-200D-1F91D-200D-1F9D1-1F3FB", + "label": "people holding hands: light skin tone", + "order": 2933, + "unicode": "🧑🏻‍🤝‍🧑🏻" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FB-200D-1F91D-200D-1F9D1-1F3FC", + "label": "people holding hands: light skin tone, medium-light skin tone", + "order": 2934, + "unicode": "🧑🏻‍🤝‍🧑🏼" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FB-200D-1F91D-200D-1F9D1-1F3FD", + "label": "people holding hands: light skin tone, medium skin tone", + "order": 2935, + "unicode": "🧑🏻‍🤝‍🧑🏽" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FB-200D-1F91D-200D-1F9D1-1F3FE", + "label": "people holding hands: light skin tone, medium-dark skin tone", + "order": 2936, + "unicode": "🧑🏻‍🤝‍🧑🏾" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FB-200D-1F91D-200D-1F9D1-1F3FF", + "label": "people holding hands: light skin tone, dark skin tone", + "order": 2937, + "unicode": "🧑🏻‍🤝‍🧑🏿" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FC-200D-1F91D-200D-1F9D1-1F3FB", + "label": "people holding hands: medium-light skin tone, light skin tone", + "order": 2938, + "unicode": "🧑🏼‍🤝‍🧑🏻" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FC-200D-1F91D-200D-1F9D1-1F3FC", + "label": "people holding hands: medium-light skin tone", + "order": 2939, + "unicode": "🧑🏼‍🤝‍🧑🏼" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FC-200D-1F91D-200D-1F9D1-1F3FD", + "label": "people holding hands: medium-light skin tone, medium skin tone", + "order": 2940, + "unicode": "🧑🏼‍🤝‍🧑🏽" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FC-200D-1F91D-200D-1F9D1-1F3FE", + "label": "people holding hands: medium-light skin tone, medium-dark skin tone", + "order": 2941, + "unicode": "🧑🏼‍🤝‍🧑🏾" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FC-200D-1F91D-200D-1F9D1-1F3FF", + "label": "people holding hands: medium-light skin tone, dark skin tone", + "order": 2942, + "unicode": "🧑🏼‍🤝‍🧑🏿" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FD-200D-1F91D-200D-1F9D1-1F3FB", + "label": "people holding hands: medium skin tone, light skin tone", + "order": 2943, + "unicode": "🧑🏽‍🤝‍🧑🏻" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FD-200D-1F91D-200D-1F9D1-1F3FC", + "label": "people holding hands: medium skin tone, medium-light skin tone", + "order": 2944, + "unicode": "🧑🏽‍🤝‍🧑🏼" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FD-200D-1F91D-200D-1F9D1-1F3FD", + "label": "people holding hands: medium skin tone", + "order": 2945, + "unicode": "🧑🏽‍🤝‍🧑🏽" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FD-200D-1F91D-200D-1F9D1-1F3FE", + "label": "people holding hands: medium skin tone, medium-dark skin tone", + "order": 2946, + "unicode": "🧑🏽‍🤝‍🧑🏾" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FD-200D-1F91D-200D-1F9D1-1F3FF", + "label": "people holding hands: medium skin tone, dark skin tone", + "order": 2947, + "unicode": "🧑🏽‍🤝‍🧑🏿" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FE-200D-1F91D-200D-1F9D1-1F3FB", + "label": "people holding hands: medium-dark skin tone, light skin tone", + "order": 2948, + "unicode": "🧑🏾‍🤝‍🧑🏻" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FE-200D-1F91D-200D-1F9D1-1F3FC", + "label": "people holding hands: medium-dark skin tone, medium-light skin tone", + "order": 2949, + "unicode": "🧑🏾‍🤝‍🧑🏼" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FE-200D-1F91D-200D-1F9D1-1F3FD", + "label": "people holding hands: medium-dark skin tone, medium skin tone", + "order": 2950, + "unicode": "🧑🏾‍🤝‍🧑🏽" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FE-200D-1F91D-200D-1F9D1-1F3FE", + "label": "people holding hands: medium-dark skin tone", + "order": 2951, + "unicode": "🧑🏾‍🤝‍🧑🏾" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FE-200D-1F91D-200D-1F9D1-1F3FF", + "label": "people holding hands: medium-dark skin tone, dark skin tone", + "order": 2952, + "unicode": "🧑🏾‍🤝‍🧑🏿" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FF-200D-1F91D-200D-1F9D1-1F3FB", + "label": "people holding hands: dark skin tone, light skin tone", + "order": 2953, + "unicode": "🧑🏿‍🤝‍🧑🏻" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FF-200D-1F91D-200D-1F9D1-1F3FC", + "label": "people holding hands: dark skin tone, medium-light skin tone", + "order": 2954, + "unicode": "🧑🏿‍🤝‍🧑🏼" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FF-200D-1F91D-200D-1F9D1-1F3FD", + "label": "people holding hands: dark skin tone, medium skin tone", + "order": 2955, + "unicode": "🧑🏿‍🤝‍🧑🏽" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FF-200D-1F91D-200D-1F9D1-1F3FE", + "label": "people holding hands: dark skin tone, medium-dark skin tone", + "order": 2956, + "unicode": "🧑🏿‍🤝‍🧑🏾" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FF-200D-1F91D-200D-1F9D1-1F3FF", + "label": "people holding hands: dark skin tone", + "order": 2957, + "unicode": "🧑🏿‍🤝‍🧑🏿" + } + ] + }, + { + "group": 1, + "hexcode": "1F46D", + "label": "women holding hands", + "order": 2958, + "tags": [ + "bae", + "bestie", + "bff", + "couple", + "dating", + "flirt", + "friends", + "girls", + "hand", + "hold", + "sisters", + "twins", + "women" + ], + "unicode": "👭", + "skins": [ + { + "group": 1, + "hexcode": "1F46D-1F3FB", + "label": "women holding hands: light skin tone", + "order": 2959, + "unicode": "👭🏻" + }, + { + "group": 1, + "hexcode": "1F46D-1F3FC", + "label": "women holding hands: medium-light skin tone", + "order": 2965, + "unicode": "👭🏼" + }, + { + "group": 1, + "hexcode": "1F46D-1F3FD", + "label": "women holding hands: medium skin tone", + "order": 2971, + "unicode": "👭🏽" + }, + { + "group": 1, + "hexcode": "1F46D-1F3FE", + "label": "women holding hands: medium-dark skin tone", + "order": 2977, + "unicode": "👭🏾" + }, + { + "group": 1, + "hexcode": "1F46D-1F3FF", + "label": "women holding hands: dark skin tone", + "order": 2983, + "unicode": "👭🏿" + }, + { + "group": 1, + "hexcode": "1F469-1F3FB-200D-1F91D-200D-1F469-1F3FC", + "label": "women holding hands: light skin tone, medium-light skin tone", + "order": 2960, + "unicode": "👩🏻‍🤝‍👩🏼" + }, + { + "group": 1, + "hexcode": "1F469-1F3FB-200D-1F91D-200D-1F469-1F3FD", + "label": "women holding hands: light skin tone, medium skin tone", + "order": 2961, + "unicode": "👩🏻‍🤝‍👩🏽" + }, + { + "group": 1, + "hexcode": "1F469-1F3FB-200D-1F91D-200D-1F469-1F3FE", + "label": "women holding hands: light skin tone, medium-dark skin tone", + "order": 2962, + "unicode": "👩🏻‍🤝‍👩🏾" + }, + { + "group": 1, + "hexcode": "1F469-1F3FB-200D-1F91D-200D-1F469-1F3FF", + "label": "women holding hands: light skin tone, dark skin tone", + "order": 2963, + "unicode": "👩🏻‍🤝‍👩🏿" + }, + { + "group": 1, + "hexcode": "1F469-1F3FC-200D-1F91D-200D-1F469-1F3FB", + "label": "women holding hands: medium-light skin tone, light skin tone", + "order": 2964, + "unicode": "👩🏼‍🤝‍👩🏻" + }, + { + "group": 1, + "hexcode": "1F469-1F3FC-200D-1F91D-200D-1F469-1F3FD", + "label": "women holding hands: medium-light skin tone, medium skin tone", + "order": 2966, + "unicode": "👩🏼‍🤝‍👩🏽" + }, + { + "group": 1, + "hexcode": "1F469-1F3FC-200D-1F91D-200D-1F469-1F3FE", + "label": "women holding hands: medium-light skin tone, medium-dark skin tone", + "order": 2967, + "unicode": "👩🏼‍🤝‍👩🏾" + }, + { + "group": 1, + "hexcode": "1F469-1F3FC-200D-1F91D-200D-1F469-1F3FF", + "label": "women holding hands: medium-light skin tone, dark skin tone", + "order": 2968, + "unicode": "👩🏼‍🤝‍👩🏿" + }, + { + "group": 1, + "hexcode": "1F469-1F3FD-200D-1F91D-200D-1F469-1F3FB", + "label": "women holding hands: medium skin tone, light skin tone", + "order": 2969, + "unicode": "👩🏽‍🤝‍👩🏻" + }, + { + "group": 1, + "hexcode": "1F469-1F3FD-200D-1F91D-200D-1F469-1F3FC", + "label": "women holding hands: medium skin tone, medium-light skin tone", + "order": 2970, + "unicode": "👩🏽‍🤝‍👩🏼" + }, + { + "group": 1, + "hexcode": "1F469-1F3FD-200D-1F91D-200D-1F469-1F3FE", + "label": "women holding hands: medium skin tone, medium-dark skin tone", + "order": 2972, + "unicode": "👩🏽‍🤝‍👩🏾" + }, + { + "group": 1, + "hexcode": "1F469-1F3FD-200D-1F91D-200D-1F469-1F3FF", + "label": "women holding hands: medium skin tone, dark skin tone", + "order": 2973, + "unicode": "👩🏽‍🤝‍👩🏿" + }, + { + "group": 1, + "hexcode": "1F469-1F3FE-200D-1F91D-200D-1F469-1F3FB", + "label": "women holding hands: medium-dark skin tone, light skin tone", + "order": 2974, + "unicode": "👩🏾‍🤝‍👩🏻" + }, + { + "group": 1, + "hexcode": "1F469-1F3FE-200D-1F91D-200D-1F469-1F3FC", + "label": "women holding hands: medium-dark skin tone, medium-light skin tone", + "order": 2975, + "unicode": "👩🏾‍🤝‍👩🏼" + }, + { + "group": 1, + "hexcode": "1F469-1F3FE-200D-1F91D-200D-1F469-1F3FD", + "label": "women holding hands: medium-dark skin tone, medium skin tone", + "order": 2976, + "unicode": "👩🏾‍🤝‍👩🏽" + }, + { + "group": 1, + "hexcode": "1F469-1F3FE-200D-1F91D-200D-1F469-1F3FF", + "label": "women holding hands: medium-dark skin tone, dark skin tone", + "order": 2978, + "unicode": "👩🏾‍🤝‍👩🏿" + }, + { + "group": 1, + "hexcode": "1F469-1F3FF-200D-1F91D-200D-1F469-1F3FB", + "label": "women holding hands: dark skin tone, light skin tone", + "order": 2979, + "unicode": "👩🏿‍🤝‍👩🏻" + }, + { + "group": 1, + "hexcode": "1F469-1F3FF-200D-1F91D-200D-1F469-1F3FC", + "label": "women holding hands: dark skin tone, medium-light skin tone", + "order": 2980, + "unicode": "👩🏿‍🤝‍👩🏼" + }, + { + "group": 1, + "hexcode": "1F469-1F3FF-200D-1F91D-200D-1F469-1F3FD", + "label": "women holding hands: dark skin tone, medium skin tone", + "order": 2981, + "unicode": "👩🏿‍🤝‍👩🏽" + }, + { + "group": 1, + "hexcode": "1F469-1F3FF-200D-1F91D-200D-1F469-1F3FE", + "label": "women holding hands: dark skin tone, medium-dark skin tone", + "order": 2982, + "unicode": "👩🏿‍🤝‍👩🏾" + } + ] + }, + { + "group": 1, + "hexcode": "1F46B", + "label": "woman and man holding hands", + "order": 2984, + "tags": [ + "bae", + "bestie", + "bff", + "couple", + "dating", + "flirt", + "friends", + "hand", + "hold", + "man", + "twins", + "woman" + ], + "unicode": "👫", + "skins": [ + { + "group": 1, + "hexcode": "1F46B-1F3FB", + "label": "woman and man holding hands: light skin tone", + "order": 2985, + "unicode": "👫🏻" + }, + { + "group": 1, + "hexcode": "1F46B-1F3FC", + "label": "woman and man holding hands: medium-light skin tone", + "order": 2991, + "unicode": "👫🏼" + }, + { + "group": 1, + "hexcode": "1F46B-1F3FD", + "label": "woman and man holding hands: medium skin tone", + "order": 2997, + "unicode": "👫🏽" + }, + { + "group": 1, + "hexcode": "1F46B-1F3FE", + "label": "woman and man holding hands: medium-dark skin tone", + "order": 3003, + "unicode": "👫🏾" + }, + { + "group": 1, + "hexcode": "1F46B-1F3FF", + "label": "woman and man holding hands: dark skin tone", + "order": 3009, + "unicode": "👫🏿" + }, + { + "group": 1, + "hexcode": "1F469-1F3FB-200D-1F91D-200D-1F468-1F3FC", + "label": "woman and man holding hands: light skin tone, medium-light skin tone", + "order": 2986, + "unicode": "👩🏻‍🤝‍👨🏼" + }, + { + "group": 1, + "hexcode": "1F469-1F3FB-200D-1F91D-200D-1F468-1F3FD", + "label": "woman and man holding hands: light skin tone, medium skin tone", + "order": 2987, + "unicode": "👩🏻‍🤝‍👨🏽" + }, + { + "group": 1, + "hexcode": "1F469-1F3FB-200D-1F91D-200D-1F468-1F3FE", + "label": "woman and man holding hands: light skin tone, medium-dark skin tone", + "order": 2988, + "unicode": "👩🏻‍🤝‍👨🏾" + }, + { + "group": 1, + "hexcode": "1F469-1F3FB-200D-1F91D-200D-1F468-1F3FF", + "label": "woman and man holding hands: light skin tone, dark skin tone", + "order": 2989, + "unicode": "👩🏻‍🤝‍👨🏿" + }, + { + "group": 1, + "hexcode": "1F469-1F3FC-200D-1F91D-200D-1F468-1F3FB", + "label": "woman and man holding hands: medium-light skin tone, light skin tone", + "order": 2990, + "unicode": "👩🏼‍🤝‍👨🏻" + }, + { + "group": 1, + "hexcode": "1F469-1F3FC-200D-1F91D-200D-1F468-1F3FD", + "label": "woman and man holding hands: medium-light skin tone, medium skin tone", + "order": 2992, + "unicode": "👩🏼‍🤝‍👨🏽" + }, + { + "group": 1, + "hexcode": "1F469-1F3FC-200D-1F91D-200D-1F468-1F3FE", + "label": "woman and man holding hands: medium-light skin tone, medium-dark skin tone", + "order": 2993, + "unicode": "👩🏼‍🤝‍👨🏾" + }, + { + "group": 1, + "hexcode": "1F469-1F3FC-200D-1F91D-200D-1F468-1F3FF", + "label": "woman and man holding hands: medium-light skin tone, dark skin tone", + "order": 2994, + "unicode": "👩🏼‍🤝‍👨🏿" + }, + { + "group": 1, + "hexcode": "1F469-1F3FD-200D-1F91D-200D-1F468-1F3FB", + "label": "woman and man holding hands: medium skin tone, light skin tone", + "order": 2995, + "unicode": "👩🏽‍🤝‍👨🏻" + }, + { + "group": 1, + "hexcode": "1F469-1F3FD-200D-1F91D-200D-1F468-1F3FC", + "label": "woman and man holding hands: medium skin tone, medium-light skin tone", + "order": 2996, + "unicode": "👩🏽‍🤝‍👨🏼" + }, + { + "group": 1, + "hexcode": "1F469-1F3FD-200D-1F91D-200D-1F468-1F3FE", + "label": "woman and man holding hands: medium skin tone, medium-dark skin tone", + "order": 2998, + "unicode": "👩🏽‍🤝‍👨🏾" + }, + { + "group": 1, + "hexcode": "1F469-1F3FD-200D-1F91D-200D-1F468-1F3FF", + "label": "woman and man holding hands: medium skin tone, dark skin tone", + "order": 2999, + "unicode": "👩🏽‍🤝‍👨🏿" + }, + { + "group": 1, + "hexcode": "1F469-1F3FE-200D-1F91D-200D-1F468-1F3FB", + "label": "woman and man holding hands: medium-dark skin tone, light skin tone", + "order": 3000, + "unicode": "👩🏾‍🤝‍👨🏻" + }, + { + "group": 1, + "hexcode": "1F469-1F3FE-200D-1F91D-200D-1F468-1F3FC", + "label": "woman and man holding hands: medium-dark skin tone, medium-light skin tone", + "order": 3001, + "unicode": "👩🏾‍🤝‍👨🏼" + }, + { + "group": 1, + "hexcode": "1F469-1F3FE-200D-1F91D-200D-1F468-1F3FD", + "label": "woman and man holding hands: medium-dark skin tone, medium skin tone", + "order": 3002, + "unicode": "👩🏾‍🤝‍👨🏽" + }, + { + "group": 1, + "hexcode": "1F469-1F3FE-200D-1F91D-200D-1F468-1F3FF", + "label": "woman and man holding hands: medium-dark skin tone, dark skin tone", + "order": 3004, + "unicode": "👩🏾‍🤝‍👨🏿" + }, + { + "group": 1, + "hexcode": "1F469-1F3FF-200D-1F91D-200D-1F468-1F3FB", + "label": "woman and man holding hands: dark skin tone, light skin tone", + "order": 3005, + "unicode": "👩🏿‍🤝‍👨🏻" + }, + { + "group": 1, + "hexcode": "1F469-1F3FF-200D-1F91D-200D-1F468-1F3FC", + "label": "woman and man holding hands: dark skin tone, medium-light skin tone", + "order": 3006, + "unicode": "👩🏿‍🤝‍👨🏼" + }, + { + "group": 1, + "hexcode": "1F469-1F3FF-200D-1F91D-200D-1F468-1F3FD", + "label": "woman and man holding hands: dark skin tone, medium skin tone", + "order": 3007, + "unicode": "👩🏿‍🤝‍👨🏽" + }, + { + "group": 1, + "hexcode": "1F469-1F3FF-200D-1F91D-200D-1F468-1F3FE", + "label": "woman and man holding hands: dark skin tone, medium-dark skin tone", + "order": 3008, + "unicode": "👩🏿‍🤝‍👨🏾" + } + ] + }, + { + "group": 1, + "hexcode": "1F46C", + "label": "men holding hands", + "order": 3010, + "tags": [ + "bae", + "bestie", + "bff", + "boys", + "brothers", + "couple", + "dating", + "flirt", + "friends", + "hand", + "hold", + "men", + "twins" + ], + "unicode": "👬", + "skins": [ + { + "group": 1, + "hexcode": "1F46C-1F3FB", + "label": "men holding hands: light skin tone", + "order": 3011, + "unicode": "👬🏻" + }, + { + "group": 1, + "hexcode": "1F46C-1F3FC", + "label": "men holding hands: medium-light skin tone", + "order": 3017, + "unicode": "👬🏼" + }, + { + "group": 1, + "hexcode": "1F46C-1F3FD", + "label": "men holding hands: medium skin tone", + "order": 3023, + "unicode": "👬🏽" + }, + { + "group": 1, + "hexcode": "1F46C-1F3FE", + "label": "men holding hands: medium-dark skin tone", + "order": 3029, + "unicode": "👬🏾" + }, + { + "group": 1, + "hexcode": "1F46C-1F3FF", + "label": "men holding hands: dark skin tone", + "order": 3035, + "unicode": "👬🏿" + }, + { + "group": 1, + "hexcode": "1F468-1F3FB-200D-1F91D-200D-1F468-1F3FC", + "label": "men holding hands: light skin tone, medium-light skin tone", + "order": 3012, + "unicode": "👨🏻‍🤝‍👨🏼" + }, + { + "group": 1, + "hexcode": "1F468-1F3FB-200D-1F91D-200D-1F468-1F3FD", + "label": "men holding hands: light skin tone, medium skin tone", + "order": 3013, + "unicode": "👨🏻‍🤝‍👨🏽" + }, + { + "group": 1, + "hexcode": "1F468-1F3FB-200D-1F91D-200D-1F468-1F3FE", + "label": "men holding hands: light skin tone, medium-dark skin tone", + "order": 3014, + "unicode": "👨🏻‍🤝‍👨🏾" + }, + { + "group": 1, + "hexcode": "1F468-1F3FB-200D-1F91D-200D-1F468-1F3FF", + "label": "men holding hands: light skin tone, dark skin tone", + "order": 3015, + "unicode": "👨🏻‍🤝‍👨🏿" + }, + { + "group": 1, + "hexcode": "1F468-1F3FC-200D-1F91D-200D-1F468-1F3FB", + "label": "men holding hands: medium-light skin tone, light skin tone", + "order": 3016, + "unicode": "👨🏼‍🤝‍👨🏻" + }, + { + "group": 1, + "hexcode": "1F468-1F3FC-200D-1F91D-200D-1F468-1F3FD", + "label": "men holding hands: medium-light skin tone, medium skin tone", + "order": 3018, + "unicode": "👨🏼‍🤝‍👨🏽" + }, + { + "group": 1, + "hexcode": "1F468-1F3FC-200D-1F91D-200D-1F468-1F3FE", + "label": "men holding hands: medium-light skin tone, medium-dark skin tone", + "order": 3019, + "unicode": "👨🏼‍🤝‍👨🏾" + }, + { + "group": 1, + "hexcode": "1F468-1F3FC-200D-1F91D-200D-1F468-1F3FF", + "label": "men holding hands: medium-light skin tone, dark skin tone", + "order": 3020, + "unicode": "👨🏼‍🤝‍👨🏿" + }, + { + "group": 1, + "hexcode": "1F468-1F3FD-200D-1F91D-200D-1F468-1F3FB", + "label": "men holding hands: medium skin tone, light skin tone", + "order": 3021, + "unicode": "👨🏽‍🤝‍👨🏻" + }, + { + "group": 1, + "hexcode": "1F468-1F3FD-200D-1F91D-200D-1F468-1F3FC", + "label": "men holding hands: medium skin tone, medium-light skin tone", + "order": 3022, + "unicode": "👨🏽‍🤝‍👨🏼" + }, + { + "group": 1, + "hexcode": "1F468-1F3FD-200D-1F91D-200D-1F468-1F3FE", + "label": "men holding hands: medium skin tone, medium-dark skin tone", + "order": 3024, + "unicode": "👨🏽‍🤝‍👨🏾" + }, + { + "group": 1, + "hexcode": "1F468-1F3FD-200D-1F91D-200D-1F468-1F3FF", + "label": "men holding hands: medium skin tone, dark skin tone", + "order": 3025, + "unicode": "👨🏽‍🤝‍👨🏿" + }, + { + "group": 1, + "hexcode": "1F468-1F3FE-200D-1F91D-200D-1F468-1F3FB", + "label": "men holding hands: medium-dark skin tone, light skin tone", + "order": 3026, + "unicode": "👨🏾‍🤝‍👨🏻" + }, + { + "group": 1, + "hexcode": "1F468-1F3FE-200D-1F91D-200D-1F468-1F3FC", + "label": "men holding hands: medium-dark skin tone, medium-light skin tone", + "order": 3027, + "unicode": "👨🏾‍🤝‍👨🏼" + }, + { + "group": 1, + "hexcode": "1F468-1F3FE-200D-1F91D-200D-1F468-1F3FD", + "label": "men holding hands: medium-dark skin tone, medium skin tone", + "order": 3028, + "unicode": "👨🏾‍🤝‍👨🏽" + }, + { + "group": 1, + "hexcode": "1F468-1F3FE-200D-1F91D-200D-1F468-1F3FF", + "label": "men holding hands: medium-dark skin tone, dark skin tone", + "order": 3030, + "unicode": "👨🏾‍🤝‍👨🏿" + }, + { + "group": 1, + "hexcode": "1F468-1F3FF-200D-1F91D-200D-1F468-1F3FB", + "label": "men holding hands: dark skin tone, light skin tone", + "order": 3031, + "unicode": "👨🏿‍🤝‍👨🏻" + }, + { + "group": 1, + "hexcode": "1F468-1F3FF-200D-1F91D-200D-1F468-1F3FC", + "label": "men holding hands: dark skin tone, medium-light skin tone", + "order": 3032, + "unicode": "👨🏿‍🤝‍👨🏼" + }, + { + "group": 1, + "hexcode": "1F468-1F3FF-200D-1F91D-200D-1F468-1F3FD", + "label": "men holding hands: dark skin tone, medium skin tone", + "order": 3033, + "unicode": "👨🏿‍🤝‍👨🏽" + }, + { + "group": 1, + "hexcode": "1F468-1F3FF-200D-1F91D-200D-1F468-1F3FE", + "label": "men holding hands: dark skin tone, medium-dark skin tone", + "order": 3034, + "unicode": "👨🏿‍🤝‍👨🏾" + } + ] + }, + { + "group": 1, + "hexcode": "1F48F", + "label": "kiss", + "order": 3036, + "tags": [ + "anniversary", + "babe", + "bae", + "couple", + "date", + "dating", + "heart", + "love", + "mwah", + "person", + "romance", + "together", + "xoxo" + ], + "unicode": "💏", + "skins": [ + { + "group": 1, + "hexcode": "1F48F-1F3FB", + "label": "kiss: light skin tone", + "order": 3037, + "unicode": "💏🏻" + }, + { + "group": 1, + "hexcode": "1F48F-1F3FC", + "label": "kiss: medium-light skin tone", + "order": 3038, + "unicode": "💏🏼" + }, + { + "group": 1, + "hexcode": "1F48F-1F3FD", + "label": "kiss: medium skin tone", + "order": 3039, + "unicode": "💏🏽" + }, + { + "group": 1, + "hexcode": "1F48F-1F3FE", + "label": "kiss: medium-dark skin tone", + "order": 3040, + "unicode": "💏🏾" + }, + { + "group": 1, + "hexcode": "1F48F-1F3FF", + "label": "kiss: dark skin tone", + "order": 3041, + "unicode": "💏🏿" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FB-200D-2764-FE0F-200D-1F48B-200D-1F9D1-1F3FC", + "label": "kiss: person, person, light skin tone, medium-light skin tone", + "order": 3042, + "unicode": "🧑🏻‍❤️‍💋‍🧑🏼" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FB-200D-2764-FE0F-200D-1F48B-200D-1F9D1-1F3FD", + "label": "kiss: person, person, light skin tone, medium skin tone", + "order": 3044, + "unicode": "🧑🏻‍❤️‍💋‍🧑🏽" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FB-200D-2764-FE0F-200D-1F48B-200D-1F9D1-1F3FE", + "label": "kiss: person, person, light skin tone, medium-dark skin tone", + "order": 3046, + "unicode": "🧑🏻‍❤️‍💋‍🧑🏾" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FB-200D-2764-FE0F-200D-1F48B-200D-1F9D1-1F3FF", + "label": "kiss: person, person, light skin tone, dark skin tone", + "order": 3048, + "unicode": "🧑🏻‍❤️‍💋‍🧑🏿" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FC-200D-2764-FE0F-200D-1F48B-200D-1F9D1-1F3FB", + "label": "kiss: person, person, medium-light skin tone, light skin tone", + "order": 3050, + "unicode": "🧑🏼‍❤️‍💋‍🧑🏻" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FC-200D-2764-FE0F-200D-1F48B-200D-1F9D1-1F3FD", + "label": "kiss: person, person, medium-light skin tone, medium skin tone", + "order": 3052, + "unicode": "🧑🏼‍❤️‍💋‍🧑🏽" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FC-200D-2764-FE0F-200D-1F48B-200D-1F9D1-1F3FE", + "label": "kiss: person, person, medium-light skin tone, medium-dark skin tone", + "order": 3054, + "unicode": "🧑🏼‍❤️‍💋‍🧑🏾" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FC-200D-2764-FE0F-200D-1F48B-200D-1F9D1-1F3FF", + "label": "kiss: person, person, medium-light skin tone, dark skin tone", + "order": 3056, + "unicode": "🧑🏼‍❤️‍💋‍🧑🏿" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FD-200D-2764-FE0F-200D-1F48B-200D-1F9D1-1F3FB", + "label": "kiss: person, person, medium skin tone, light skin tone", + "order": 3058, + "unicode": "🧑🏽‍❤️‍💋‍🧑🏻" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FD-200D-2764-FE0F-200D-1F48B-200D-1F9D1-1F3FC", + "label": "kiss: person, person, medium skin tone, medium-light skin tone", + "order": 3060, + "unicode": "🧑🏽‍❤️‍💋‍🧑🏼" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FD-200D-2764-FE0F-200D-1F48B-200D-1F9D1-1F3FE", + "label": "kiss: person, person, medium skin tone, medium-dark skin tone", + "order": 3062, + "unicode": "🧑🏽‍❤️‍💋‍🧑🏾" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FD-200D-2764-FE0F-200D-1F48B-200D-1F9D1-1F3FF", + "label": "kiss: person, person, medium skin tone, dark skin tone", + "order": 3064, + "unicode": "🧑🏽‍❤️‍💋‍🧑🏿" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FE-200D-2764-FE0F-200D-1F48B-200D-1F9D1-1F3FB", + "label": "kiss: person, person, medium-dark skin tone, light skin tone", + "order": 3066, + "unicode": "🧑🏾‍❤️‍💋‍🧑🏻" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FE-200D-2764-FE0F-200D-1F48B-200D-1F9D1-1F3FC", + "label": "kiss: person, person, medium-dark skin tone, medium-light skin tone", + "order": 3068, + "unicode": "🧑🏾‍❤️‍💋‍🧑🏼" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FE-200D-2764-FE0F-200D-1F48B-200D-1F9D1-1F3FD", + "label": "kiss: person, person, medium-dark skin tone, medium skin tone", + "order": 3070, + "unicode": "🧑🏾‍❤️‍💋‍🧑🏽" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FE-200D-2764-FE0F-200D-1F48B-200D-1F9D1-1F3FF", + "label": "kiss: person, person, medium-dark skin tone, dark skin tone", + "order": 3072, + "unicode": "🧑🏾‍❤️‍💋‍🧑🏿" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FF-200D-2764-FE0F-200D-1F48B-200D-1F9D1-1F3FB", + "label": "kiss: person, person, dark skin tone, light skin tone", + "order": 3074, + "unicode": "🧑🏿‍❤️‍💋‍🧑🏻" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FF-200D-2764-FE0F-200D-1F48B-200D-1F9D1-1F3FC", + "label": "kiss: person, person, dark skin tone, medium-light skin tone", + "order": 3076, + "unicode": "🧑🏿‍❤️‍💋‍🧑🏼" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FF-200D-2764-FE0F-200D-1F48B-200D-1F9D1-1F3FD", + "label": "kiss: person, person, dark skin tone, medium skin tone", + "order": 3078, + "unicode": "🧑🏿‍❤️‍💋‍🧑🏽" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FF-200D-2764-FE0F-200D-1F48B-200D-1F9D1-1F3FE", + "label": "kiss: person, person, dark skin tone, medium-dark skin tone", + "order": 3080, + "unicode": "🧑🏿‍❤️‍💋‍🧑🏾" + } + ] + }, + { + "group": 1, + "hexcode": "1F469-200D-2764-FE0F-200D-1F48B-200D-1F468", + "label": "kiss: woman, man", + "order": 3082, + "tags": [ + "anniversary", + "babe", + "bae", + "couple", + "date", + "dating", + "heart", + "kiss", + "love", + "man", + "mwah", + "person", + "romance", + "together", + "woman", + "xoxo" + ], + "unicode": "👩‍❤️‍💋‍👨", + "skins": [ + { + "group": 1, + "hexcode": "1F469-1F3FB-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FB", + "label": "kiss: woman, man, light skin tone", + "order": 3084, + "unicode": "👩🏻‍❤️‍💋‍👨🏻" + }, + { + "group": 1, + "hexcode": "1F469-1F3FB-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FC", + "label": "kiss: woman, man, light skin tone, medium-light skin tone", + "order": 3086, + "unicode": "👩🏻‍❤️‍💋‍👨🏼" + }, + { + "group": 1, + "hexcode": "1F469-1F3FB-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FD", + "label": "kiss: woman, man, light skin tone, medium skin tone", + "order": 3088, + "unicode": "👩🏻‍❤️‍💋‍👨🏽" + }, + { + "group": 1, + "hexcode": "1F469-1F3FB-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FE", + "label": "kiss: woman, man, light skin tone, medium-dark skin tone", + "order": 3090, + "unicode": "👩🏻‍❤️‍💋‍👨🏾" + }, + { + "group": 1, + "hexcode": "1F469-1F3FB-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FF", + "label": "kiss: woman, man, light skin tone, dark skin tone", + "order": 3092, + "unicode": "👩🏻‍❤️‍💋‍👨🏿" + }, + { + "group": 1, + "hexcode": "1F469-1F3FC-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FB", + "label": "kiss: woman, man, medium-light skin tone, light skin tone", + "order": 3094, + "unicode": "👩🏼‍❤️‍💋‍👨🏻" + }, + { + "group": 1, + "hexcode": "1F469-1F3FC-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FC", + "label": "kiss: woman, man, medium-light skin tone", + "order": 3096, + "unicode": "👩🏼‍❤️‍💋‍👨🏼" + }, + { + "group": 1, + "hexcode": "1F469-1F3FC-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FD", + "label": "kiss: woman, man, medium-light skin tone, medium skin tone", + "order": 3098, + "unicode": "👩🏼‍❤️‍💋‍👨🏽" + }, + { + "group": 1, + "hexcode": "1F469-1F3FC-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FE", + "label": "kiss: woman, man, medium-light skin tone, medium-dark skin tone", + "order": 3100, + "unicode": "👩🏼‍❤️‍💋‍👨🏾" + }, + { + "group": 1, + "hexcode": "1F469-1F3FC-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FF", + "label": "kiss: woman, man, medium-light skin tone, dark skin tone", + "order": 3102, + "unicode": "👩🏼‍❤️‍💋‍👨🏿" + }, + { + "group": 1, + "hexcode": "1F469-1F3FD-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FB", + "label": "kiss: woman, man, medium skin tone, light skin tone", + "order": 3104, + "unicode": "👩🏽‍❤️‍💋‍👨🏻" + }, + { + "group": 1, + "hexcode": "1F469-1F3FD-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FC", + "label": "kiss: woman, man, medium skin tone, medium-light skin tone", + "order": 3106, + "unicode": "👩🏽‍❤️‍💋‍👨🏼" + }, + { + "group": 1, + "hexcode": "1F469-1F3FD-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FD", + "label": "kiss: woman, man, medium skin tone", + "order": 3108, + "unicode": "👩🏽‍❤️‍💋‍👨🏽" + }, + { + "group": 1, + "hexcode": "1F469-1F3FD-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FE", + "label": "kiss: woman, man, medium skin tone, medium-dark skin tone", + "order": 3110, + "unicode": "👩🏽‍❤️‍💋‍👨🏾" + }, + { + "group": 1, + "hexcode": "1F469-1F3FD-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FF", + "label": "kiss: woman, man, medium skin tone, dark skin tone", + "order": 3112, + "unicode": "👩🏽‍❤️‍💋‍👨🏿" + }, + { + "group": 1, + "hexcode": "1F469-1F3FE-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FB", + "label": "kiss: woman, man, medium-dark skin tone, light skin tone", + "order": 3114, + "unicode": "👩🏾‍❤️‍💋‍👨🏻" + }, + { + "group": 1, + "hexcode": "1F469-1F3FE-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FC", + "label": "kiss: woman, man, medium-dark skin tone, medium-light skin tone", + "order": 3116, + "unicode": "👩🏾‍❤️‍💋‍👨🏼" + }, + { + "group": 1, + "hexcode": "1F469-1F3FE-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FD", + "label": "kiss: woman, man, medium-dark skin tone, medium skin tone", + "order": 3118, + "unicode": "👩🏾‍❤️‍💋‍👨🏽" + }, + { + "group": 1, + "hexcode": "1F469-1F3FE-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FE", + "label": "kiss: woman, man, medium-dark skin tone", + "order": 3120, + "unicode": "👩🏾‍❤️‍💋‍👨🏾" + }, + { + "group": 1, + "hexcode": "1F469-1F3FE-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FF", + "label": "kiss: woman, man, medium-dark skin tone, dark skin tone", + "order": 3122, + "unicode": "👩🏾‍❤️‍💋‍👨🏿" + }, + { + "group": 1, + "hexcode": "1F469-1F3FF-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FB", + "label": "kiss: woman, man, dark skin tone, light skin tone", + "order": 3124, + "unicode": "👩🏿‍❤️‍💋‍👨🏻" + }, + { + "group": 1, + "hexcode": "1F469-1F3FF-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FC", + "label": "kiss: woman, man, dark skin tone, medium-light skin tone", + "order": 3126, + "unicode": "👩🏿‍❤️‍💋‍👨🏼" + }, + { + "group": 1, + "hexcode": "1F469-1F3FF-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FD", + "label": "kiss: woman, man, dark skin tone, medium skin tone", + "order": 3128, + "unicode": "👩🏿‍❤️‍💋‍👨🏽" + }, + { + "group": 1, + "hexcode": "1F469-1F3FF-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FE", + "label": "kiss: woman, man, dark skin tone, medium-dark skin tone", + "order": 3130, + "unicode": "👩🏿‍❤️‍💋‍👨🏾" + }, + { + "group": 1, + "hexcode": "1F469-1F3FF-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FF", + "label": "kiss: woman, man, dark skin tone", + "order": 3132, + "unicode": "👩🏿‍❤️‍💋‍👨🏿" + } + ] + }, + { + "group": 1, + "hexcode": "1F468-200D-2764-FE0F-200D-1F48B-200D-1F468", + "label": "kiss: man, man", + "order": 3134, + "tags": [ + "anniversary", + "babe", + "bae", + "couple", + "date", + "dating", + "heart", + "kiss", + "love", + "man", + "mwah", + "person", + "romance", + "together", + "xoxo" + ], + "unicode": "👨‍❤️‍💋‍👨", + "skins": [ + { + "group": 1, + "hexcode": "1F468-1F3FB-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FB", + "label": "kiss: man, man, light skin tone", + "order": 3136, + "unicode": "👨🏻‍❤️‍💋‍👨🏻" + }, + { + "group": 1, + "hexcode": "1F468-1F3FB-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FC", + "label": "kiss: man, man, light skin tone, medium-light skin tone", + "order": 3138, + "unicode": "👨🏻‍❤️‍💋‍👨🏼" + }, + { + "group": 1, + "hexcode": "1F468-1F3FB-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FD", + "label": "kiss: man, man, light skin tone, medium skin tone", + "order": 3140, + "unicode": "👨🏻‍❤️‍💋‍👨🏽" + }, + { + "group": 1, + "hexcode": "1F468-1F3FB-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FE", + "label": "kiss: man, man, light skin tone, medium-dark skin tone", + "order": 3142, + "unicode": "👨🏻‍❤️‍💋‍👨🏾" + }, + { + "group": 1, + "hexcode": "1F468-1F3FB-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FF", + "label": "kiss: man, man, light skin tone, dark skin tone", + "order": 3144, + "unicode": "👨🏻‍❤️‍💋‍👨🏿" + }, + { + "group": 1, + "hexcode": "1F468-1F3FC-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FB", + "label": "kiss: man, man, medium-light skin tone, light skin tone", + "order": 3146, + "unicode": "👨🏼‍❤️‍💋‍👨🏻" + }, + { + "group": 1, + "hexcode": "1F468-1F3FC-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FC", + "label": "kiss: man, man, medium-light skin tone", + "order": 3148, + "unicode": "👨🏼‍❤️‍💋‍👨🏼" + }, + { + "group": 1, + "hexcode": "1F468-1F3FC-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FD", + "label": "kiss: man, man, medium-light skin tone, medium skin tone", + "order": 3150, + "unicode": "👨🏼‍❤️‍💋‍👨🏽" + }, + { + "group": 1, + "hexcode": "1F468-1F3FC-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FE", + "label": "kiss: man, man, medium-light skin tone, medium-dark skin tone", + "order": 3152, + "unicode": "👨🏼‍❤️‍💋‍👨🏾" + }, + { + "group": 1, + "hexcode": "1F468-1F3FC-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FF", + "label": "kiss: man, man, medium-light skin tone, dark skin tone", + "order": 3154, + "unicode": "👨🏼‍❤️‍💋‍👨🏿" + }, + { + "group": 1, + "hexcode": "1F468-1F3FD-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FB", + "label": "kiss: man, man, medium skin tone, light skin tone", + "order": 3156, + "unicode": "👨🏽‍❤️‍💋‍👨🏻" + }, + { + "group": 1, + "hexcode": "1F468-1F3FD-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FC", + "label": "kiss: man, man, medium skin tone, medium-light skin tone", + "order": 3158, + "unicode": "👨🏽‍❤️‍💋‍👨🏼" + }, + { + "group": 1, + "hexcode": "1F468-1F3FD-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FD", + "label": "kiss: man, man, medium skin tone", + "order": 3160, + "unicode": "👨🏽‍❤️‍💋‍👨🏽" + }, + { + "group": 1, + "hexcode": "1F468-1F3FD-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FE", + "label": "kiss: man, man, medium skin tone, medium-dark skin tone", + "order": 3162, + "unicode": "👨🏽‍❤️‍💋‍👨🏾" + }, + { + "group": 1, + "hexcode": "1F468-1F3FD-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FF", + "label": "kiss: man, man, medium skin tone, dark skin tone", + "order": 3164, + "unicode": "👨🏽‍❤️‍💋‍👨🏿" + }, + { + "group": 1, + "hexcode": "1F468-1F3FE-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FB", + "label": "kiss: man, man, medium-dark skin tone, light skin tone", + "order": 3166, + "unicode": "👨🏾‍❤️‍💋‍👨🏻" + }, + { + "group": 1, + "hexcode": "1F468-1F3FE-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FC", + "label": "kiss: man, man, medium-dark skin tone, medium-light skin tone", + "order": 3168, + "unicode": "👨🏾‍❤️‍💋‍👨🏼" + }, + { + "group": 1, + "hexcode": "1F468-1F3FE-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FD", + "label": "kiss: man, man, medium-dark skin tone, medium skin tone", + "order": 3170, + "unicode": "👨🏾‍❤️‍💋‍👨🏽" + }, + { + "group": 1, + "hexcode": "1F468-1F3FE-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FE", + "label": "kiss: man, man, medium-dark skin tone", + "order": 3172, + "unicode": "👨🏾‍❤️‍💋‍👨🏾" + }, + { + "group": 1, + "hexcode": "1F468-1F3FE-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FF", + "label": "kiss: man, man, medium-dark skin tone, dark skin tone", + "order": 3174, + "unicode": "👨🏾‍❤️‍💋‍👨🏿" + }, + { + "group": 1, + "hexcode": "1F468-1F3FF-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FB", + "label": "kiss: man, man, dark skin tone, light skin tone", + "order": 3176, + "unicode": "👨🏿‍❤️‍💋‍👨🏻" + }, + { + "group": 1, + "hexcode": "1F468-1F3FF-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FC", + "label": "kiss: man, man, dark skin tone, medium-light skin tone", + "order": 3178, + "unicode": "👨🏿‍❤️‍💋‍👨🏼" + }, + { + "group": 1, + "hexcode": "1F468-1F3FF-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FD", + "label": "kiss: man, man, dark skin tone, medium skin tone", + "order": 3180, + "unicode": "👨🏿‍❤️‍💋‍👨🏽" + }, + { + "group": 1, + "hexcode": "1F468-1F3FF-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FE", + "label": "kiss: man, man, dark skin tone, medium-dark skin tone", + "order": 3182, + "unicode": "👨🏿‍❤️‍💋‍👨🏾" + }, + { + "group": 1, + "hexcode": "1F468-1F3FF-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FF", + "label": "kiss: man, man, dark skin tone", + "order": 3184, + "unicode": "👨🏿‍❤️‍💋‍👨🏿" + } + ] + }, + { + "group": 1, + "hexcode": "1F469-200D-2764-FE0F-200D-1F48B-200D-1F469", + "label": "kiss: woman, woman", + "order": 3186, + "tags": [ + "anniversary", + "babe", + "bae", + "couple", + "date", + "dating", + "heart", + "kiss", + "love", + "mwah", + "person", + "romance", + "together", + "woman", + "xoxo" + ], + "unicode": "👩‍❤️‍💋‍👩", + "skins": [ + { + "group": 1, + "hexcode": "1F469-1F3FB-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FB", + "label": "kiss: woman, woman, light skin tone", + "order": 3188, + "unicode": "👩🏻‍❤️‍💋‍👩🏻" + }, + { + "group": 1, + "hexcode": "1F469-1F3FB-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FC", + "label": "kiss: woman, woman, light skin tone, medium-light skin tone", + "order": 3190, + "unicode": "👩🏻‍❤️‍💋‍👩🏼" + }, + { + "group": 1, + "hexcode": "1F469-1F3FB-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FD", + "label": "kiss: woman, woman, light skin tone, medium skin tone", + "order": 3192, + "unicode": "👩🏻‍❤️‍💋‍👩🏽" + }, + { + "group": 1, + "hexcode": "1F469-1F3FB-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FE", + "label": "kiss: woman, woman, light skin tone, medium-dark skin tone", + "order": 3194, + "unicode": "👩🏻‍❤️‍💋‍👩🏾" + }, + { + "group": 1, + "hexcode": "1F469-1F3FB-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FF", + "label": "kiss: woman, woman, light skin tone, dark skin tone", + "order": 3196, + "unicode": "👩🏻‍❤️‍💋‍👩🏿" + }, + { + "group": 1, + "hexcode": "1F469-1F3FC-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FB", + "label": "kiss: woman, woman, medium-light skin tone, light skin tone", + "order": 3198, + "unicode": "👩🏼‍❤️‍💋‍👩🏻" + }, + { + "group": 1, + "hexcode": "1F469-1F3FC-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FC", + "label": "kiss: woman, woman, medium-light skin tone", + "order": 3200, + "unicode": "👩🏼‍❤️‍💋‍👩🏼" + }, + { + "group": 1, + "hexcode": "1F469-1F3FC-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FD", + "label": "kiss: woman, woman, medium-light skin tone, medium skin tone", + "order": 3202, + "unicode": "👩🏼‍❤️‍💋‍👩🏽" + }, + { + "group": 1, + "hexcode": "1F469-1F3FC-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FE", + "label": "kiss: woman, woman, medium-light skin tone, medium-dark skin tone", + "order": 3204, + "unicode": "👩🏼‍❤️‍💋‍👩🏾" + }, + { + "group": 1, + "hexcode": "1F469-1F3FC-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FF", + "label": "kiss: woman, woman, medium-light skin tone, dark skin tone", + "order": 3206, + "unicode": "👩🏼‍❤️‍💋‍👩🏿" + }, + { + "group": 1, + "hexcode": "1F469-1F3FD-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FB", + "label": "kiss: woman, woman, medium skin tone, light skin tone", + "order": 3208, + "unicode": "👩🏽‍❤️‍💋‍👩🏻" + }, + { + "group": 1, + "hexcode": "1F469-1F3FD-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FC", + "label": "kiss: woman, woman, medium skin tone, medium-light skin tone", + "order": 3210, + "unicode": "👩🏽‍❤️‍💋‍👩🏼" + }, + { + "group": 1, + "hexcode": "1F469-1F3FD-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FD", + "label": "kiss: woman, woman, medium skin tone", + "order": 3212, + "unicode": "👩🏽‍❤️‍💋‍👩🏽" + }, + { + "group": 1, + "hexcode": "1F469-1F3FD-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FE", + "label": "kiss: woman, woman, medium skin tone, medium-dark skin tone", + "order": 3214, + "unicode": "👩🏽‍❤️‍💋‍👩🏾" + }, + { + "group": 1, + "hexcode": "1F469-1F3FD-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FF", + "label": "kiss: woman, woman, medium skin tone, dark skin tone", + "order": 3216, + "unicode": "👩🏽‍❤️‍💋‍👩🏿" + }, + { + "group": 1, + "hexcode": "1F469-1F3FE-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FB", + "label": "kiss: woman, woman, medium-dark skin tone, light skin tone", + "order": 3218, + "unicode": "👩🏾‍❤️‍💋‍👩🏻" + }, + { + "group": 1, + "hexcode": "1F469-1F3FE-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FC", + "label": "kiss: woman, woman, medium-dark skin tone, medium-light skin tone", + "order": 3220, + "unicode": "👩🏾‍❤️‍💋‍👩🏼" + }, + { + "group": 1, + "hexcode": "1F469-1F3FE-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FD", + "label": "kiss: woman, woman, medium-dark skin tone, medium skin tone", + "order": 3222, + "unicode": "👩🏾‍❤️‍💋‍👩🏽" + }, + { + "group": 1, + "hexcode": "1F469-1F3FE-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FE", + "label": "kiss: woman, woman, medium-dark skin tone", + "order": 3224, + "unicode": "👩🏾‍❤️‍💋‍👩🏾" + }, + { + "group": 1, + "hexcode": "1F469-1F3FE-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FF", + "label": "kiss: woman, woman, medium-dark skin tone, dark skin tone", + "order": 3226, + "unicode": "👩🏾‍❤️‍💋‍👩🏿" + }, + { + "group": 1, + "hexcode": "1F469-1F3FF-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FB", + "label": "kiss: woman, woman, dark skin tone, light skin tone", + "order": 3228, + "unicode": "👩🏿‍❤️‍💋‍👩🏻" + }, + { + "group": 1, + "hexcode": "1F469-1F3FF-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FC", + "label": "kiss: woman, woman, dark skin tone, medium-light skin tone", + "order": 3230, + "unicode": "👩🏿‍❤️‍💋‍👩🏼" + }, + { + "group": 1, + "hexcode": "1F469-1F3FF-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FD", + "label": "kiss: woman, woman, dark skin tone, medium skin tone", + "order": 3232, + "unicode": "👩🏿‍❤️‍💋‍👩🏽" + }, + { + "group": 1, + "hexcode": "1F469-1F3FF-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FE", + "label": "kiss: woman, woman, dark skin tone, medium-dark skin tone", + "order": 3234, + "unicode": "👩🏿‍❤️‍💋‍👩🏾" + }, + { + "group": 1, + "hexcode": "1F469-1F3FF-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FF", + "label": "kiss: woman, woman, dark skin tone", + "order": 3236, + "unicode": "👩🏿‍❤️‍💋‍👩🏿" + } + ] + }, + { + "group": 1, + "hexcode": "1F491", + "label": "couple with heart", + "order": 3238, + "tags": [ + "anniversary", + "babe", + "bae", + "couple", + "dating", + "heart", + "kiss", + "love", + "person", + "relationship", + "romance", + "together", + "you" + ], + "unicode": "💑", + "skins": [ + { + "group": 1, + "hexcode": "1F491-1F3FB", + "label": "couple with heart: light skin tone", + "order": 3239, + "unicode": "💑🏻" + }, + { + "group": 1, + "hexcode": "1F491-1F3FC", + "label": "couple with heart: medium-light skin tone", + "order": 3240, + "unicode": "💑🏼" + }, + { + "group": 1, + "hexcode": "1F491-1F3FD", + "label": "couple with heart: medium skin tone", + "order": 3241, + "unicode": "💑🏽" + }, + { + "group": 1, + "hexcode": "1F491-1F3FE", + "label": "couple with heart: medium-dark skin tone", + "order": 3242, + "unicode": "💑🏾" + }, + { + "group": 1, + "hexcode": "1F491-1F3FF", + "label": "couple with heart: dark skin tone", + "order": 3243, + "unicode": "💑🏿" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FB-200D-2764-FE0F-200D-1F9D1-1F3FC", + "label": "couple with heart: person, person, light skin tone, medium-light skin tone", + "order": 3244, + "unicode": "🧑🏻‍❤️‍🧑🏼" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FB-200D-2764-FE0F-200D-1F9D1-1F3FD", + "label": "couple with heart: person, person, light skin tone, medium skin tone", + "order": 3246, + "unicode": "🧑🏻‍❤️‍🧑🏽" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FB-200D-2764-FE0F-200D-1F9D1-1F3FE", + "label": "couple with heart: person, person, light skin tone, medium-dark skin tone", + "order": 3248, + "unicode": "🧑🏻‍❤️‍🧑🏾" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FB-200D-2764-FE0F-200D-1F9D1-1F3FF", + "label": "couple with heart: person, person, light skin tone, dark skin tone", + "order": 3250, + "unicode": "🧑🏻‍❤️‍🧑🏿" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FC-200D-2764-FE0F-200D-1F9D1-1F3FB", + "label": "couple with heart: person, person, medium-light skin tone, light skin tone", + "order": 3252, + "unicode": "🧑🏼‍❤️‍🧑🏻" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FC-200D-2764-FE0F-200D-1F9D1-1F3FD", + "label": "couple with heart: person, person, medium-light skin tone, medium skin tone", + "order": 3254, + "unicode": "🧑🏼‍❤️‍🧑🏽" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FC-200D-2764-FE0F-200D-1F9D1-1F3FE", + "label": "couple with heart: person, person, medium-light skin tone, medium-dark skin tone", + "order": 3256, + "unicode": "🧑🏼‍❤️‍🧑🏾" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FC-200D-2764-FE0F-200D-1F9D1-1F3FF", + "label": "couple with heart: person, person, medium-light skin tone, dark skin tone", + "order": 3258, + "unicode": "🧑🏼‍❤️‍🧑🏿" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FD-200D-2764-FE0F-200D-1F9D1-1F3FB", + "label": "couple with heart: person, person, medium skin tone, light skin tone", + "order": 3260, + "unicode": "🧑🏽‍❤️‍🧑🏻" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FD-200D-2764-FE0F-200D-1F9D1-1F3FC", + "label": "couple with heart: person, person, medium skin tone, medium-light skin tone", + "order": 3262, + "unicode": "🧑🏽‍❤️‍🧑🏼" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FD-200D-2764-FE0F-200D-1F9D1-1F3FE", + "label": "couple with heart: person, person, medium skin tone, medium-dark skin tone", + "order": 3264, + "unicode": "🧑🏽‍❤️‍🧑🏾" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FD-200D-2764-FE0F-200D-1F9D1-1F3FF", + "label": "couple with heart: person, person, medium skin tone, dark skin tone", + "order": 3266, + "unicode": "🧑🏽‍❤️‍🧑🏿" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FE-200D-2764-FE0F-200D-1F9D1-1F3FB", + "label": "couple with heart: person, person, medium-dark skin tone, light skin tone", + "order": 3268, + "unicode": "🧑🏾‍❤️‍🧑🏻" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FE-200D-2764-FE0F-200D-1F9D1-1F3FC", + "label": "couple with heart: person, person, medium-dark skin tone, medium-light skin tone", + "order": 3270, + "unicode": "🧑🏾‍❤️‍🧑🏼" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FE-200D-2764-FE0F-200D-1F9D1-1F3FD", + "label": "couple with heart: person, person, medium-dark skin tone, medium skin tone", + "order": 3272, + "unicode": "🧑🏾‍❤️‍🧑🏽" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FE-200D-2764-FE0F-200D-1F9D1-1F3FF", + "label": "couple with heart: person, person, medium-dark skin tone, dark skin tone", + "order": 3274, + "unicode": "🧑🏾‍❤️‍🧑🏿" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FF-200D-2764-FE0F-200D-1F9D1-1F3FB", + "label": "couple with heart: person, person, dark skin tone, light skin tone", + "order": 3276, + "unicode": "🧑🏿‍❤️‍🧑🏻" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FF-200D-2764-FE0F-200D-1F9D1-1F3FC", + "label": "couple with heart: person, person, dark skin tone, medium-light skin tone", + "order": 3278, + "unicode": "🧑🏿‍❤️‍🧑🏼" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FF-200D-2764-FE0F-200D-1F9D1-1F3FD", + "label": "couple with heart: person, person, dark skin tone, medium skin tone", + "order": 3280, + "unicode": "🧑🏿‍❤️‍🧑🏽" + }, + { + "group": 1, + "hexcode": "1F9D1-1F3FF-200D-2764-FE0F-200D-1F9D1-1F3FE", + "label": "couple with heart: person, person, dark skin tone, medium-dark skin tone", + "order": 3282, + "unicode": "🧑🏿‍❤️‍🧑🏾" + } + ] + }, + { + "group": 1, + "hexcode": "1F469-200D-2764-FE0F-200D-1F468", + "label": "couple with heart: woman, man", + "order": 3284, + "tags": [ + "anniversary", + "babe", + "bae", + "couple", + "dating", + "heart", + "kiss", + "love", + "man", + "person", + "relationship", + "romance", + "together", + "woman", + "you" + ], + "unicode": "👩‍❤️‍👨", + "skins": [ + { + "group": 1, + "hexcode": "1F469-1F3FB-200D-2764-FE0F-200D-1F468-1F3FB", + "label": "couple with heart: woman, man, light skin tone", + "order": 3286, + "unicode": "👩🏻‍❤️‍👨🏻" + }, + { + "group": 1, + "hexcode": "1F469-1F3FB-200D-2764-FE0F-200D-1F468-1F3FC", + "label": "couple with heart: woman, man, light skin tone, medium-light skin tone", + "order": 3288, + "unicode": "👩🏻‍❤️‍👨🏼" + }, + { + "group": 1, + "hexcode": "1F469-1F3FB-200D-2764-FE0F-200D-1F468-1F3FD", + "label": "couple with heart: woman, man, light skin tone, medium skin tone", + "order": 3290, + "unicode": "👩🏻‍❤️‍👨🏽" + }, + { + "group": 1, + "hexcode": "1F469-1F3FB-200D-2764-FE0F-200D-1F468-1F3FE", + "label": "couple with heart: woman, man, light skin tone, medium-dark skin tone", + "order": 3292, + "unicode": "👩🏻‍❤️‍👨🏾" + }, + { + "group": 1, + "hexcode": "1F469-1F3FB-200D-2764-FE0F-200D-1F468-1F3FF", + "label": "couple with heart: woman, man, light skin tone, dark skin tone", + "order": 3294, + "unicode": "👩🏻‍❤️‍👨🏿" + }, + { + "group": 1, + "hexcode": "1F469-1F3FC-200D-2764-FE0F-200D-1F468-1F3FB", + "label": "couple with heart: woman, man, medium-light skin tone, light skin tone", + "order": 3296, + "unicode": "👩🏼‍❤️‍👨🏻" + }, + { + "group": 1, + "hexcode": "1F469-1F3FC-200D-2764-FE0F-200D-1F468-1F3FC", + "label": "couple with heart: woman, man, medium-light skin tone", + "order": 3298, + "unicode": "👩🏼‍❤️‍👨🏼" + }, + { + "group": 1, + "hexcode": "1F469-1F3FC-200D-2764-FE0F-200D-1F468-1F3FD", + "label": "couple with heart: woman, man, medium-light skin tone, medium skin tone", + "order": 3300, + "unicode": "👩🏼‍❤️‍👨🏽" + }, + { + "group": 1, + "hexcode": "1F469-1F3FC-200D-2764-FE0F-200D-1F468-1F3FE", + "label": "couple with heart: woman, man, medium-light skin tone, medium-dark skin tone", + "order": 3302, + "unicode": "👩🏼‍❤️‍👨🏾" + }, + { + "group": 1, + "hexcode": "1F469-1F3FC-200D-2764-FE0F-200D-1F468-1F3FF", + "label": "couple with heart: woman, man, medium-light skin tone, dark skin tone", + "order": 3304, + "unicode": "👩🏼‍❤️‍👨🏿" + }, + { + "group": 1, + "hexcode": "1F469-1F3FD-200D-2764-FE0F-200D-1F468-1F3FB", + "label": "couple with heart: woman, man, medium skin tone, light skin tone", + "order": 3306, + "unicode": "👩🏽‍❤️‍👨🏻" + }, + { + "group": 1, + "hexcode": "1F469-1F3FD-200D-2764-FE0F-200D-1F468-1F3FC", + "label": "couple with heart: woman, man, medium skin tone, medium-light skin tone", + "order": 3308, + "unicode": "👩🏽‍❤️‍👨🏼" + }, + { + "group": 1, + "hexcode": "1F469-1F3FD-200D-2764-FE0F-200D-1F468-1F3FD", + "label": "couple with heart: woman, man, medium skin tone", + "order": 3310, + "unicode": "👩🏽‍❤️‍👨🏽" + }, + { + "group": 1, + "hexcode": "1F469-1F3FD-200D-2764-FE0F-200D-1F468-1F3FE", + "label": "couple with heart: woman, man, medium skin tone, medium-dark skin tone", + "order": 3312, + "unicode": "👩🏽‍❤️‍👨🏾" + }, + { + "group": 1, + "hexcode": "1F469-1F3FD-200D-2764-FE0F-200D-1F468-1F3FF", + "label": "couple with heart: woman, man, medium skin tone, dark skin tone", + "order": 3314, + "unicode": "👩🏽‍❤️‍👨🏿" + }, + { + "group": 1, + "hexcode": "1F469-1F3FE-200D-2764-FE0F-200D-1F468-1F3FB", + "label": "couple with heart: woman, man, medium-dark skin tone, light skin tone", + "order": 3316, + "unicode": "👩🏾‍❤️‍👨🏻" + }, + { + "group": 1, + "hexcode": "1F469-1F3FE-200D-2764-FE0F-200D-1F468-1F3FC", + "label": "couple with heart: woman, man, medium-dark skin tone, medium-light skin tone", + "order": 3318, + "unicode": "👩🏾‍❤️‍👨🏼" + }, + { + "group": 1, + "hexcode": "1F469-1F3FE-200D-2764-FE0F-200D-1F468-1F3FD", + "label": "couple with heart: woman, man, medium-dark skin tone, medium skin tone", + "order": 3320, + "unicode": "👩🏾‍❤️‍👨🏽" + }, + { + "group": 1, + "hexcode": "1F469-1F3FE-200D-2764-FE0F-200D-1F468-1F3FE", + "label": "couple with heart: woman, man, medium-dark skin tone", + "order": 3322, + "unicode": "👩🏾‍❤️‍👨🏾" + }, + { + "group": 1, + "hexcode": "1F469-1F3FE-200D-2764-FE0F-200D-1F468-1F3FF", + "label": "couple with heart: woman, man, medium-dark skin tone, dark skin tone", + "order": 3324, + "unicode": "👩🏾‍❤️‍👨🏿" + }, + { + "group": 1, + "hexcode": "1F469-1F3FF-200D-2764-FE0F-200D-1F468-1F3FB", + "label": "couple with heart: woman, man, dark skin tone, light skin tone", + "order": 3326, + "unicode": "👩🏿‍❤️‍👨🏻" + }, + { + "group": 1, + "hexcode": "1F469-1F3FF-200D-2764-FE0F-200D-1F468-1F3FC", + "label": "couple with heart: woman, man, dark skin tone, medium-light skin tone", + "order": 3328, + "unicode": "👩🏿‍❤️‍👨🏼" + }, + { + "group": 1, + "hexcode": "1F469-1F3FF-200D-2764-FE0F-200D-1F468-1F3FD", + "label": "couple with heart: woman, man, dark skin tone, medium skin tone", + "order": 3330, + "unicode": "👩🏿‍❤️‍👨🏽" + }, + { + "group": 1, + "hexcode": "1F469-1F3FF-200D-2764-FE0F-200D-1F468-1F3FE", + "label": "couple with heart: woman, man, dark skin tone, medium-dark skin tone", + "order": 3332, + "unicode": "👩🏿‍❤️‍👨🏾" + }, + { + "group": 1, + "hexcode": "1F469-1F3FF-200D-2764-FE0F-200D-1F468-1F3FF", + "label": "couple with heart: woman, man, dark skin tone", + "order": 3334, + "unicode": "👩🏿‍❤️‍👨🏿" + } + ] + }, + { + "group": 1, + "hexcode": "1F468-200D-2764-FE0F-200D-1F468", + "label": "couple with heart: man, man", + "order": 3336, + "tags": [ + "anniversary", + "babe", + "bae", + "couple", + "dating", + "heart", + "kiss", + "love", + "man", + "person", + "relationship", + "romance", + "together", + "you" + ], + "unicode": "👨‍❤️‍👨", + "skins": [ + { + "group": 1, + "hexcode": "1F468-1F3FB-200D-2764-FE0F-200D-1F468-1F3FB", + "label": "couple with heart: man, man, light skin tone", + "order": 3338, + "unicode": "👨🏻‍❤️‍👨🏻" + }, + { + "group": 1, + "hexcode": "1F468-1F3FB-200D-2764-FE0F-200D-1F468-1F3FC", + "label": "couple with heart: man, man, light skin tone, medium-light skin tone", + "order": 3340, + "unicode": "👨🏻‍❤️‍👨🏼" + }, + { + "group": 1, + "hexcode": "1F468-1F3FB-200D-2764-FE0F-200D-1F468-1F3FD", + "label": "couple with heart: man, man, light skin tone, medium skin tone", + "order": 3342, + "unicode": "👨🏻‍❤️‍👨🏽" + }, + { + "group": 1, + "hexcode": "1F468-1F3FB-200D-2764-FE0F-200D-1F468-1F3FE", + "label": "couple with heart: man, man, light skin tone, medium-dark skin tone", + "order": 3344, + "unicode": "👨🏻‍❤️‍👨🏾" + }, + { + "group": 1, + "hexcode": "1F468-1F3FB-200D-2764-FE0F-200D-1F468-1F3FF", + "label": "couple with heart: man, man, light skin tone, dark skin tone", + "order": 3346, + "unicode": "👨🏻‍❤️‍👨🏿" + }, + { + "group": 1, + "hexcode": "1F468-1F3FC-200D-2764-FE0F-200D-1F468-1F3FB", + "label": "couple with heart: man, man, medium-light skin tone, light skin tone", + "order": 3348, + "unicode": "👨🏼‍❤️‍👨🏻" + }, + { + "group": 1, + "hexcode": "1F468-1F3FC-200D-2764-FE0F-200D-1F468-1F3FC", + "label": "couple with heart: man, man, medium-light skin tone", + "order": 3350, + "unicode": "👨🏼‍❤️‍👨🏼" + }, + { + "group": 1, + "hexcode": "1F468-1F3FC-200D-2764-FE0F-200D-1F468-1F3FD", + "label": "couple with heart: man, man, medium-light skin tone, medium skin tone", + "order": 3352, + "unicode": "👨🏼‍❤️‍👨🏽" + }, + { + "group": 1, + "hexcode": "1F468-1F3FC-200D-2764-FE0F-200D-1F468-1F3FE", + "label": "couple with heart: man, man, medium-light skin tone, medium-dark skin tone", + "order": 3354, + "unicode": "👨🏼‍❤️‍👨🏾" + }, + { + "group": 1, + "hexcode": "1F468-1F3FC-200D-2764-FE0F-200D-1F468-1F3FF", + "label": "couple with heart: man, man, medium-light skin tone, dark skin tone", + "order": 3356, + "unicode": "👨🏼‍❤️‍👨🏿" + }, + { + "group": 1, + "hexcode": "1F468-1F3FD-200D-2764-FE0F-200D-1F468-1F3FB", + "label": "couple with heart: man, man, medium skin tone, light skin tone", + "order": 3358, + "unicode": "👨🏽‍❤️‍👨🏻" + }, + { + "group": 1, + "hexcode": "1F468-1F3FD-200D-2764-FE0F-200D-1F468-1F3FC", + "label": "couple with heart: man, man, medium skin tone, medium-light skin tone", + "order": 3360, + "unicode": "👨🏽‍❤️‍👨🏼" + }, + { + "group": 1, + "hexcode": "1F468-1F3FD-200D-2764-FE0F-200D-1F468-1F3FD", + "label": "couple with heart: man, man, medium skin tone", + "order": 3362, + "unicode": "👨🏽‍❤️‍👨🏽" + }, + { + "group": 1, + "hexcode": "1F468-1F3FD-200D-2764-FE0F-200D-1F468-1F3FE", + "label": "couple with heart: man, man, medium skin tone, medium-dark skin tone", + "order": 3364, + "unicode": "👨🏽‍❤️‍👨🏾" + }, + { + "group": 1, + "hexcode": "1F468-1F3FD-200D-2764-FE0F-200D-1F468-1F3FF", + "label": "couple with heart: man, man, medium skin tone, dark skin tone", + "order": 3366, + "unicode": "👨🏽‍❤️‍👨🏿" + }, + { + "group": 1, + "hexcode": "1F468-1F3FE-200D-2764-FE0F-200D-1F468-1F3FB", + "label": "couple with heart: man, man, medium-dark skin tone, light skin tone", + "order": 3368, + "unicode": "👨🏾‍❤️‍👨🏻" + }, + { + "group": 1, + "hexcode": "1F468-1F3FE-200D-2764-FE0F-200D-1F468-1F3FC", + "label": "couple with heart: man, man, medium-dark skin tone, medium-light skin tone", + "order": 3370, + "unicode": "👨🏾‍❤️‍👨🏼" + }, + { + "group": 1, + "hexcode": "1F468-1F3FE-200D-2764-FE0F-200D-1F468-1F3FD", + "label": "couple with heart: man, man, medium-dark skin tone, medium skin tone", + "order": 3372, + "unicode": "👨🏾‍❤️‍👨🏽" + }, + { + "group": 1, + "hexcode": "1F468-1F3FE-200D-2764-FE0F-200D-1F468-1F3FE", + "label": "couple with heart: man, man, medium-dark skin tone", + "order": 3374, + "unicode": "👨🏾‍❤️‍👨🏾" + }, + { + "group": 1, + "hexcode": "1F468-1F3FE-200D-2764-FE0F-200D-1F468-1F3FF", + "label": "couple with heart: man, man, medium-dark skin tone, dark skin tone", + "order": 3376, + "unicode": "👨🏾‍❤️‍👨🏿" + }, + { + "group": 1, + "hexcode": "1F468-1F3FF-200D-2764-FE0F-200D-1F468-1F3FB", + "label": "couple with heart: man, man, dark skin tone, light skin tone", + "order": 3378, + "unicode": "👨🏿‍❤️‍👨🏻" + }, + { + "group": 1, + "hexcode": "1F468-1F3FF-200D-2764-FE0F-200D-1F468-1F3FC", + "label": "couple with heart: man, man, dark skin tone, medium-light skin tone", + "order": 3380, + "unicode": "👨🏿‍❤️‍👨🏼" + }, + { + "group": 1, + "hexcode": "1F468-1F3FF-200D-2764-FE0F-200D-1F468-1F3FD", + "label": "couple with heart: man, man, dark skin tone, medium skin tone", + "order": 3382, + "unicode": "👨🏿‍❤️‍👨🏽" + }, + { + "group": 1, + "hexcode": "1F468-1F3FF-200D-2764-FE0F-200D-1F468-1F3FE", + "label": "couple with heart: man, man, dark skin tone, medium-dark skin tone", + "order": 3384, + "unicode": "👨🏿‍❤️‍👨🏾" + }, + { + "group": 1, + "hexcode": "1F468-1F3FF-200D-2764-FE0F-200D-1F468-1F3FF", + "label": "couple with heart: man, man, dark skin tone", + "order": 3386, + "unicode": "👨🏿‍❤️‍👨🏿" + } + ] + }, + { + "group": 1, + "hexcode": "1F469-200D-2764-FE0F-200D-1F469", + "label": "couple with heart: woman, woman", + "order": 3388, + "tags": [ + "anniversary", + "babe", + "bae", + "couple", + "dating", + "heart", + "kiss", + "love", + "person", + "relationship", + "romance", + "together", + "woman", + "you" + ], + "unicode": "👩‍❤️‍👩", + "skins": [ + { + "group": 1, + "hexcode": "1F469-1F3FB-200D-2764-FE0F-200D-1F469-1F3FB", + "label": "couple with heart: woman, woman, light skin tone", + "order": 3390, + "unicode": "👩🏻‍❤️‍👩🏻" + }, + { + "group": 1, + "hexcode": "1F469-1F3FB-200D-2764-FE0F-200D-1F469-1F3FC", + "label": "couple with heart: woman, woman, light skin tone, medium-light skin tone", + "order": 3392, + "unicode": "👩🏻‍❤️‍👩🏼" + }, + { + "group": 1, + "hexcode": "1F469-1F3FB-200D-2764-FE0F-200D-1F469-1F3FD", + "label": "couple with heart: woman, woman, light skin tone, medium skin tone", + "order": 3394, + "unicode": "👩🏻‍❤️‍👩🏽" + }, + { + "group": 1, + "hexcode": "1F469-1F3FB-200D-2764-FE0F-200D-1F469-1F3FE", + "label": "couple with heart: woman, woman, light skin tone, medium-dark skin tone", + "order": 3396, + "unicode": "👩🏻‍❤️‍👩🏾" + }, + { + "group": 1, + "hexcode": "1F469-1F3FB-200D-2764-FE0F-200D-1F469-1F3FF", + "label": "couple with heart: woman, woman, light skin tone, dark skin tone", + "order": 3398, + "unicode": "👩🏻‍❤️‍👩🏿" + }, + { + "group": 1, + "hexcode": "1F469-1F3FC-200D-2764-FE0F-200D-1F469-1F3FB", + "label": "couple with heart: woman, woman, medium-light skin tone, light skin tone", + "order": 3400, + "unicode": "👩🏼‍❤️‍👩🏻" + }, + { + "group": 1, + "hexcode": "1F469-1F3FC-200D-2764-FE0F-200D-1F469-1F3FC", + "label": "couple with heart: woman, woman, medium-light skin tone", + "order": 3402, + "unicode": "👩🏼‍❤️‍👩🏼" + }, + { + "group": 1, + "hexcode": "1F469-1F3FC-200D-2764-FE0F-200D-1F469-1F3FD", + "label": "couple with heart: woman, woman, medium-light skin tone, medium skin tone", + "order": 3404, + "unicode": "👩🏼‍❤️‍👩🏽" + }, + { + "group": 1, + "hexcode": "1F469-1F3FC-200D-2764-FE0F-200D-1F469-1F3FE", + "label": "couple with heart: woman, woman, medium-light skin tone, medium-dark skin tone", + "order": 3406, + "unicode": "👩🏼‍❤️‍👩🏾" + }, + { + "group": 1, + "hexcode": "1F469-1F3FC-200D-2764-FE0F-200D-1F469-1F3FF", + "label": "couple with heart: woman, woman, medium-light skin tone, dark skin tone", + "order": 3408, + "unicode": "👩🏼‍❤️‍👩🏿" + }, + { + "group": 1, + "hexcode": "1F469-1F3FD-200D-2764-FE0F-200D-1F469-1F3FB", + "label": "couple with heart: woman, woman, medium skin tone, light skin tone", + "order": 3410, + "unicode": "👩🏽‍❤️‍👩🏻" + }, + { + "group": 1, + "hexcode": "1F469-1F3FD-200D-2764-FE0F-200D-1F469-1F3FC", + "label": "couple with heart: woman, woman, medium skin tone, medium-light skin tone", + "order": 3412, + "unicode": "👩🏽‍❤️‍👩🏼" + }, + { + "group": 1, + "hexcode": "1F469-1F3FD-200D-2764-FE0F-200D-1F469-1F3FD", + "label": "couple with heart: woman, woman, medium skin tone", + "order": 3414, + "unicode": "👩🏽‍❤️‍👩🏽" + }, + { + "group": 1, + "hexcode": "1F469-1F3FD-200D-2764-FE0F-200D-1F469-1F3FE", + "label": "couple with heart: woman, woman, medium skin tone, medium-dark skin tone", + "order": 3416, + "unicode": "👩🏽‍❤️‍👩🏾" + }, + { + "group": 1, + "hexcode": "1F469-1F3FD-200D-2764-FE0F-200D-1F469-1F3FF", + "label": "couple with heart: woman, woman, medium skin tone, dark skin tone", + "order": 3418, + "unicode": "👩🏽‍❤️‍👩🏿" + }, + { + "group": 1, + "hexcode": "1F469-1F3FE-200D-2764-FE0F-200D-1F469-1F3FB", + "label": "couple with heart: woman, woman, medium-dark skin tone, light skin tone", + "order": 3420, + "unicode": "👩🏾‍❤️‍👩🏻" + }, + { + "group": 1, + "hexcode": "1F469-1F3FE-200D-2764-FE0F-200D-1F469-1F3FC", + "label": "couple with heart: woman, woman, medium-dark skin tone, medium-light skin tone", + "order": 3422, + "unicode": "👩🏾‍❤️‍👩🏼" + }, + { + "group": 1, + "hexcode": "1F469-1F3FE-200D-2764-FE0F-200D-1F469-1F3FD", + "label": "couple with heart: woman, woman, medium-dark skin tone, medium skin tone", + "order": 3424, + "unicode": "👩🏾‍❤️‍👩🏽" + }, + { + "group": 1, + "hexcode": "1F469-1F3FE-200D-2764-FE0F-200D-1F469-1F3FE", + "label": "couple with heart: woman, woman, medium-dark skin tone", + "order": 3426, + "unicode": "👩🏾‍❤️‍👩🏾" + }, + { + "group": 1, + "hexcode": "1F469-1F3FE-200D-2764-FE0F-200D-1F469-1F3FF", + "label": "couple with heart: woman, woman, medium-dark skin tone, dark skin tone", + "order": 3428, + "unicode": "👩🏾‍❤️‍👩🏿" + }, + { + "group": 1, + "hexcode": "1F469-1F3FF-200D-2764-FE0F-200D-1F469-1F3FB", + "label": "couple with heart: woman, woman, dark skin tone, light skin tone", + "order": 3430, + "unicode": "👩🏿‍❤️‍👩🏻" + }, + { + "group": 1, + "hexcode": "1F469-1F3FF-200D-2764-FE0F-200D-1F469-1F3FC", + "label": "couple with heart: woman, woman, dark skin tone, medium-light skin tone", + "order": 3432, + "unicode": "👩🏿‍❤️‍👩🏼" + }, + { + "group": 1, + "hexcode": "1F469-1F3FF-200D-2764-FE0F-200D-1F469-1F3FD", + "label": "couple with heart: woman, woman, dark skin tone, medium skin tone", + "order": 3434, + "unicode": "👩🏿‍❤️‍👩🏽" + }, + { + "group": 1, + "hexcode": "1F469-1F3FF-200D-2764-FE0F-200D-1F469-1F3FE", + "label": "couple with heart: woman, woman, dark skin tone, medium-dark skin tone", + "order": 3436, + "unicode": "👩🏿‍❤️‍👩🏾" + }, + { + "group": 1, + "hexcode": "1F469-1F3FF-200D-2764-FE0F-200D-1F469-1F3FF", + "label": "couple with heart: woman, woman, dark skin tone", + "order": 3438, + "unicode": "👩🏿‍❤️‍👩🏿" + } + ] + }, + { + "group": 1, + "hexcode": "1F468-200D-1F469-200D-1F466", + "label": "family: man, woman, boy", + "order": 3440, + "tags": ["boy", "child", "family", "man", "woman"], + "unicode": "👨‍👩‍👦" + }, + { + "group": 1, + "hexcode": "1F468-200D-1F469-200D-1F467", + "label": "family: man, woman, girl", + "order": 3441, + "tags": ["child", "family", "girl", "man", "woman"], + "unicode": "👨‍👩‍👧" + }, + { + "group": 1, + "hexcode": "1F468-200D-1F469-200D-1F467-200D-1F466", + "label": "family: man, woman, girl, boy", + "order": 3442, + "tags": ["boy", "child", "family", "girl", "man", "woman"], + "unicode": "👨‍👩‍👧‍👦" + }, + { + "group": 1, + "hexcode": "1F468-200D-1F469-200D-1F466-200D-1F466", + "label": "family: man, woman, boy, boy", + "order": 3443, + "tags": ["boy", "child", "family", "man", "woman"], + "unicode": "👨‍👩‍👦‍👦" + }, + { + "group": 1, + "hexcode": "1F468-200D-1F469-200D-1F467-200D-1F467", + "label": "family: man, woman, girl, girl", + "order": 3444, + "tags": ["child", "family", "girl", "man", "woman"], + "unicode": "👨‍👩‍👧‍👧" + }, + { + "group": 1, + "hexcode": "1F468-200D-1F468-200D-1F466", + "label": "family: man, man, boy", + "order": 3445, + "tags": ["boy", "child", "family", "man"], + "unicode": "👨‍👨‍👦" + }, + { + "group": 1, + "hexcode": "1F468-200D-1F468-200D-1F467", + "label": "family: man, man, girl", + "order": 3446, + "tags": ["child", "family", "girl", "man"], + "unicode": "👨‍👨‍👧" + }, + { + "group": 1, + "hexcode": "1F468-200D-1F468-200D-1F467-200D-1F466", + "label": "family: man, man, girl, boy", + "order": 3447, + "tags": ["boy", "child", "family", "girl", "man"], + "unicode": "👨‍👨‍👧‍👦" + }, + { + "group": 1, + "hexcode": "1F468-200D-1F468-200D-1F466-200D-1F466", + "label": "family: man, man, boy, boy", + "order": 3448, + "tags": ["boy", "child", "family", "man"], + "unicode": "👨‍👨‍👦‍👦" + }, + { + "group": 1, + "hexcode": "1F468-200D-1F468-200D-1F467-200D-1F467", + "label": "family: man, man, girl, girl", + "order": 3449, + "tags": ["child", "family", "girl", "man"], + "unicode": "👨‍👨‍👧‍👧" + }, + { + "group": 1, + "hexcode": "1F469-200D-1F469-200D-1F466", + "label": "family: woman, woman, boy", + "order": 3450, + "tags": ["boy", "child", "family", "woman"], + "unicode": "👩‍👩‍👦" + }, + { + "group": 1, + "hexcode": "1F469-200D-1F469-200D-1F467", + "label": "family: woman, woman, girl", + "order": 3451, + "tags": ["child", "family", "girl", "woman"], + "unicode": "👩‍👩‍👧" + }, + { + "group": 1, + "hexcode": "1F469-200D-1F469-200D-1F467-200D-1F466", + "label": "family: woman, woman, girl, boy", + "order": 3452, + "tags": ["boy", "child", "family", "girl", "woman"], + "unicode": "👩‍👩‍👧‍👦" + }, + { + "group": 1, + "hexcode": "1F469-200D-1F469-200D-1F466-200D-1F466", + "label": "family: woman, woman, boy, boy", + "order": 3453, + "tags": ["boy", "child", "family", "woman"], + "unicode": "👩‍👩‍👦‍👦" + }, + { + "group": 1, + "hexcode": "1F469-200D-1F469-200D-1F467-200D-1F467", + "label": "family: woman, woman, girl, girl", + "order": 3454, + "tags": ["child", "family", "girl", "woman"], + "unicode": "👩‍👩‍👧‍👧" + }, + { + "group": 1, + "hexcode": "1F468-200D-1F466", + "label": "family: man, boy", + "order": 3455, + "tags": ["boy", "child", "family", "man"], + "unicode": "👨‍👦" + }, + { + "group": 1, + "hexcode": "1F468-200D-1F466-200D-1F466", + "label": "family: man, boy, boy", + "order": 3456, + "tags": ["boy", "child", "family", "man"], + "unicode": "👨‍👦‍👦" + }, + { + "group": 1, + "hexcode": "1F468-200D-1F467", + "label": "family: man, girl", + "order": 3457, + "tags": ["child", "family", "girl", "man"], + "unicode": "👨‍👧" + }, + { + "group": 1, + "hexcode": "1F468-200D-1F467-200D-1F466", + "label": "family: man, girl, boy", + "order": 3458, + "tags": ["boy", "child", "family", "girl", "man"], + "unicode": "👨‍👧‍👦" + }, + { + "group": 1, + "hexcode": "1F468-200D-1F467-200D-1F467", + "label": "family: man, girl, girl", + "order": 3459, + "tags": ["child", "family", "girl", "man"], + "unicode": "👨‍👧‍👧" + }, + { + "group": 1, + "hexcode": "1F469-200D-1F466", + "label": "family: woman, boy", + "order": 3460, + "tags": ["boy", "child", "family", "woman"], + "unicode": "👩‍👦" + }, + { + "group": 1, + "hexcode": "1F469-200D-1F466-200D-1F466", + "label": "family: woman, boy, boy", + "order": 3461, + "tags": ["boy", "child", "family", "woman"], + "unicode": "👩‍👦‍👦" + }, + { + "group": 1, + "hexcode": "1F469-200D-1F467", + "label": "family: woman, girl", + "order": 3462, + "tags": ["child", "family", "girl", "woman"], + "unicode": "👩‍👧" + }, + { + "group": 1, + "hexcode": "1F469-200D-1F467-200D-1F466", + "label": "family: woman, girl, boy", + "order": 3463, + "tags": ["boy", "child", "family", "girl", "woman"], + "unicode": "👩‍👧‍👦" + }, + { + "group": 1, + "hexcode": "1F469-200D-1F467-200D-1F467", + "label": "family: woman, girl, girl", + "order": 3464, + "tags": ["child", "family", "girl", "woman"], + "unicode": "👩‍👧‍👧" + }, + { + "group": 1, + "hexcode": "1F5E3", + "label": "speaking head", + "order": 3466, + "tags": ["face", "head", "silhouette", "speak", "speaking"], + "unicode": "🗣️" + }, + { + "group": 1, + "hexcode": "1F464", + "label": "bust in silhouette", + "order": 3467, + "tags": ["bust", "mysterious", "shadow", "silhouette"], + "unicode": "👤" + }, + { + "group": 1, + "hexcode": "1F465", + "label": "busts in silhouette", + "order": 3468, + "tags": [ + "bff", + "bust", + "busts", + "everyone", + "friend", + "friends", + "people", + "silhouette" + ], + "unicode": "👥" + }, + { + "group": 1, + "hexcode": "1FAC2", + "label": "people hugging", + "order": 3469, + "tags": [ + "comfort", + "embrace", + "farewell", + "friendship", + "goodbye", + "hello", + "hug", + "hugging", + "love", + "people", + "thanks" + ], + "unicode": "🫂" + }, + { + "group": 1, + "hexcode": "1F46A", + "label": "family", + "order": 3470, + "tags": ["child"], + "unicode": "👪️" + }, + { + "group": 1, + "hexcode": "1F9D1-200D-1F9D1-200D-1F9D2", + "label": "family: adult, adult, child", + "order": 3471, + "tags": ["adult", "child", "family"], + "unicode": "🧑‍🧑‍🧒" + }, + { + "group": 1, + "hexcode": "1F9D1-200D-1F9D1-200D-1F9D2-200D-1F9D2", + "label": "family: adult, adult, child, child", + "order": 3472, + "tags": ["adult", "child", "family"], + "unicode": "🧑‍🧑‍🧒‍🧒" + }, + { + "group": 1, + "hexcode": "1F9D1-200D-1F9D2", + "label": "family: adult, child", + "order": 3473, + "tags": ["adult", "child", "family"], + "unicode": "🧑‍🧒" + }, + { + "group": 1, + "hexcode": "1F9D1-200D-1F9D2-200D-1F9D2", + "label": "family: adult, child, child", + "order": 3474, + "tags": ["adult", "child", "family"], + "unicode": "🧑‍🧒‍🧒" + }, + { + "group": 1, + "hexcode": "1F463", + "label": "footprints", + "order": 3475, + "tags": ["barefoot", "clothing", "footprint", "omw", "print", "walk"], + "unicode": "👣" + }, + { + "group": 1, + "hexcode": "1FAC6", + "label": "fingerprint", + "order": 3476, + "tags": [ + "clue", + "crime", + "detective", + "forensics", + "identity", + "mystery", + "print", + "safety", + "trace" + ], + "unicode": "🫆" + } + ] + }, + { + "name": "Animals & Nature", + "emojis": [ + { + "group": 3, + "hexcode": "1F435", + "label": "monkey face", + "order": 3486, + "tags": ["animal", "banana", "face", "monkey"], + "unicode": "🐵" + }, + { + "group": 3, + "hexcode": "1F412", + "label": "monkey", + "order": 3487, + "tags": ["animal", "banana"], + "unicode": "🐒" + }, + { + "group": 3, + "hexcode": "1F98D", + "label": "gorilla", + "order": 3488, + "tags": ["animal"], + "unicode": "🦍" + }, + { + "group": 3, + "hexcode": "1F9A7", + "label": "orangutan", + "order": 3489, + "tags": ["animal", "ape", "monkey"], + "unicode": "🦧" + }, + { + "group": 3, + "hexcode": "1F436", + "label": "dog face", + "order": 3490, + "tags": ["adorbs", "animal", "dog", "face", "pet", "puppies", "puppy"], + "unicode": "🐶" + }, + { + "group": 3, + "hexcode": "1F415", + "label": "dog", + "order": 3491, + "tags": ["animal", "animals", "dogs", "pet"], + "unicode": "🐕️" + }, + { + "group": 3, + "hexcode": "1F9AE", + "label": "guide dog", + "order": 3492, + "tags": ["accessibility", "animal", "blind", "dog", "guide"], + "unicode": "🦮" + }, + { + "group": 3, + "hexcode": "1F415-200D-1F9BA", + "label": "service dog", + "order": 3493, + "tags": ["accessibility", "animal", "assistance", "dog", "service"], + "unicode": "🐕‍🦺" + }, + { + "group": 3, + "hexcode": "1F429", + "label": "poodle", + "order": 3494, + "tags": ["animal", "dog", "fluffy"], + "unicode": "🐩" + }, + { + "group": 3, + "hexcode": "1F43A", + "label": "wolf", + "order": 3495, + "tags": ["animal", "face"], + "unicode": "🐺" + }, + { + "group": 3, + "hexcode": "1F98A", + "label": "fox", + "order": 3496, + "tags": ["animal", "face"], + "unicode": "🦊" + }, + { + "group": 3, + "hexcode": "1F99D", + "label": "raccoon", + "order": 3497, + "tags": ["animal", "curious", "sly"], + "unicode": "🦝" + }, + { + "group": 3, + "hexcode": "1F431", + "label": "cat face", + "order": 3498, + "tags": ["animal", "cat", "face", "kitten", "kitty", "pet"], + "unicode": "🐱" + }, + { + "group": 3, + "hexcode": "1F408", + "label": "cat", + "order": 3499, + "tags": ["animal", "animals", "cats", "kitten", "pet"], + "unicode": "🐈️" + }, + { + "group": 3, + "hexcode": "1F408-200D-2B1B", + "label": "black cat", + "order": 3500, + "tags": [ + "animal", + "black", + "cat", + "feline", + "halloween", + "meow", + "unlucky" + ], + "unicode": "🐈‍⬛" + }, + { + "group": 3, + "hexcode": "1F981", + "label": "lion", + "order": 3501, + "tags": [ + "alpha", + "animal", + "face", + "leo", + "mane", + "order", + "rawr", + "roar", + "safari", + "strong", + "zodiac" + ], + "unicode": "🦁" + }, + { + "group": 3, + "hexcode": "1F42F", + "label": "tiger face", + "order": 3502, + "tags": ["animal", "big", "cat", "face", "predator", "tiger"], + "unicode": "🐯" + }, + { + "group": 3, + "hexcode": "1F405", + "label": "tiger", + "order": 3503, + "tags": ["animal", "big", "cat", "predator", "zoo"], + "unicode": "🐅" + }, + { + "group": 3, + "hexcode": "1F406", + "label": "leopard", + "order": 3504, + "tags": ["animal", "big", "cat", "predator", "zoo"], + "unicode": "🐆" + }, + { + "group": 3, + "hexcode": "1F434", + "label": "horse face", + "order": 3505, + "tags": [ + "animal", + "dressage", + "equine", + "face", + "farm", + "horse", + "horses" + ], + "unicode": "🐴" + }, + { + "group": 3, + "hexcode": "1FACE", + "label": "moose", + "order": 3506, + "tags": ["alces", "animal", "antlers", "elk", "mammal"], + "unicode": "🫎" + }, + { + "group": 3, + "hexcode": "1FACF", + "label": "donkey", + "order": 3507, + "tags": [ + "animal", + "ass", + "burro", + "hinny", + "mammal", + "mule", + "stubborn" + ], + "unicode": "🫏" + }, + { + "group": 3, + "hexcode": "1F40E", + "label": "horse", + "order": 3508, + "tags": ["animal", "equestrian", "farm", "racehorse", "racing"], + "unicode": "🐎" + }, + { + "group": 3, + "hexcode": "1F984", + "label": "unicorn", + "order": 3509, + "tags": ["face"], + "unicode": "🦄" + }, + { + "group": 3, + "hexcode": "1F993", + "label": "zebra", + "order": 3510, + "tags": ["animal", "stripe"], + "unicode": "🦓" + }, + { + "group": 3, + "hexcode": "1F98C", + "label": "deer", + "order": 3511, + "tags": ["animal"], + "unicode": "🦌" + }, + { + "group": 3, + "hexcode": "1F9AC", + "label": "bison", + "order": 3512, + "tags": ["animal", "buffalo", "herd", "wisent"], + "unicode": "🦬" + }, + { + "group": 3, + "hexcode": "1F42E", + "label": "cow face", + "order": 3513, + "tags": ["animal", "cow", "face", "farm", "milk", "moo"], + "unicode": "🐮" + }, + { + "group": 3, + "hexcode": "1F402", + "label": "ox", + "order": 3514, + "tags": ["animal", "animals", "bull", "farm", "taurus", "zodiac"], + "unicode": "🐂" + }, + { + "group": 3, + "hexcode": "1F403", + "label": "water buffalo", + "order": 3515, + "tags": ["animal", "buffalo", "water", "zoo"], + "unicode": "🐃" + }, + { + "group": 3, + "hexcode": "1F404", + "label": "cow", + "order": 3516, + "tags": ["animal", "animals", "farm", "milk", "moo"], + "unicode": "🐄" + }, + { + "group": 3, + "hexcode": "1F437", + "label": "pig face", + "order": 3517, + "tags": ["animal", "bacon", "face", "farm", "pig", "pork"], + "unicode": "🐷" + }, + { + "group": 3, + "hexcode": "1F416", + "label": "pig", + "order": 3518, + "tags": ["animal", "bacon", "farm", "pork", "sow"], + "unicode": "🐖" + }, + { + "group": 3, + "hexcode": "1F417", + "label": "boar", + "order": 3519, + "tags": ["animal", "pig"], + "unicode": "🐗" + }, + { + "group": 3, + "hexcode": "1F43D", + "label": "pig nose", + "order": 3520, + "tags": ["animal", "face", "farm", "nose", "pig", "smell", "snout"], + "unicode": "🐽" + }, + { + "group": 3, + "hexcode": "1F40F", + "label": "ram", + "order": 3521, + "tags": ["animal", "aries", "horns", "male", "sheep", "zodiac", "zoo"], + "unicode": "🐏" + }, + { + "group": 3, + "hexcode": "1F411", + "label": "ewe", + "order": 3522, + "tags": [ + "animal", + "baa", + "farm", + "female", + "fluffy", + "lamb", + "sheep", + "wool" + ], + "unicode": "🐑" + }, + { + "group": 3, + "hexcode": "1F410", + "label": "goat", + "order": 3523, + "tags": ["animal", "capricorn", "farm", "milk", "zodiac"], + "unicode": "🐐" + }, + { + "group": 3, + "hexcode": "1F42A", + "label": "camel", + "order": 3524, + "tags": ["animal", "desert", "dromedary", "hump", "one"], + "unicode": "🐪" + }, + { + "group": 3, + "hexcode": "1F42B", + "label": "two-hump camel", + "order": 3525, + "tags": [ + "animal", + "bactrian", + "camel", + "desert", + "hump", + "two", + "two-hump" + ], + "unicode": "🐫" + }, + { + "group": 3, + "hexcode": "1F999", + "label": "llama", + "order": 3526, + "tags": ["alpaca", "animal", "guanaco", "vicuña", "wool"], + "unicode": "🦙" + }, + { + "group": 3, + "hexcode": "1F992", + "label": "giraffe", + "order": 3527, + "tags": ["animal", "spots"], + "unicode": "🦒" + }, + { + "group": 3, + "hexcode": "1F418", + "label": "elephant", + "order": 3528, + "tags": ["animal"], + "unicode": "🐘" + }, + { + "group": 3, + "hexcode": "1F9A3", + "label": "mammoth", + "order": 3529, + "tags": ["animal", "extinction", "large", "tusk", "wooly"], + "unicode": "🦣" + }, + { + "group": 3, + "hexcode": "1F98F", + "label": "rhinoceros", + "order": 3530, + "tags": ["animal"], + "unicode": "🦏" + }, + { + "group": 3, + "hexcode": "1F99B", + "label": "hippopotamus", + "order": 3531, + "tags": ["animal", "hippo"], + "unicode": "🦛" + }, + { + "group": 3, + "hexcode": "1F42D", + "label": "mouse face", + "order": 3532, + "tags": ["animal", "face", "mouse"], + "unicode": "🐭" + }, + { + "group": 3, + "hexcode": "1F401", + "label": "mouse", + "order": 3533, + "tags": ["animal", "animals"], + "unicode": "🐁" + }, + { + "group": 3, + "hexcode": "1F400", + "label": "rat", + "order": 3534, + "tags": ["animal"], + "unicode": "🐀" + }, + { + "group": 3, + "hexcode": "1F439", + "label": "hamster", + "order": 3535, + "tags": ["animal", "face", "pet"], + "unicode": "🐹" + }, + { + "group": 3, + "hexcode": "1F430", + "label": "rabbit face", + "order": 3536, + "tags": ["animal", "bunny", "face", "pet", "rabbit"], + "unicode": "🐰" + }, + { + "group": 3, + "hexcode": "1F407", + "label": "rabbit", + "order": 3537, + "tags": ["animal", "bunny", "pet"], + "unicode": "🐇" + }, + { + "group": 3, + "hexcode": "1F43F", + "label": "chipmunk", + "order": 3539, + "tags": ["animal", "squirrel"], + "unicode": "🐿️" + }, + { + "group": 3, + "hexcode": "1F9AB", + "label": "beaver", + "order": 3540, + "tags": ["animal", "dam", "teeth"], + "unicode": "🦫" + }, + { + "group": 3, + "hexcode": "1F994", + "label": "hedgehog", + "order": 3541, + "tags": ["animal", "spiny"], + "unicode": "🦔" + }, + { + "group": 3, + "hexcode": "1F987", + "label": "bat", + "order": 3542, + "tags": ["animal", "vampire"], + "unicode": "🦇" + }, + { + "group": 3, + "hexcode": "1F43B", + "label": "bear", + "order": 3543, + "tags": ["animal", "face", "grizzly", "growl", "honey"], + "unicode": "🐻" + }, + { + "group": 3, + "hexcode": "1F43B-200D-2744-FE0F", + "label": "polar bear", + "order": 3544, + "tags": ["animal", "arctic", "bear", "polar", "white"], + "unicode": "🐻‍❄️" + }, + { + "group": 3, + "hexcode": "1F428", + "label": "koala", + "order": 3546, + "tags": [ + "animal", + "australia", + "bear", + "down", + "face", + "marsupial", + "under" + ], + "unicode": "🐨" + }, + { + "group": 3, + "hexcode": "1F43C", + "label": "panda", + "order": 3547, + "tags": ["animal", "bamboo", "face"], + "unicode": "🐼" + }, + { + "group": 3, + "hexcode": "1F9A5", + "label": "sloth", + "order": 3548, + "tags": ["lazy", "slow"], + "unicode": "🦥" + }, + { + "group": 3, + "hexcode": "1F9A6", + "label": "otter", + "order": 3549, + "tags": ["animal", "fishing", "playful"], + "unicode": "🦦" + }, + { + "group": 3, + "hexcode": "1F9A8", + "label": "skunk", + "order": 3550, + "tags": ["animal", "stink"], + "unicode": "🦨" + }, + { + "group": 3, + "hexcode": "1F998", + "label": "kangaroo", + "order": 3551, + "tags": ["animal", "joey", "jump", "marsupial"], + "unicode": "🦘" + }, + { + "group": 3, + "hexcode": "1F9A1", + "label": "badger", + "order": 3552, + "tags": ["animal", "honey", "pester"], + "unicode": "🦡" + }, + { + "group": 3, + "hexcode": "1F43E", + "label": "paw prints", + "order": 3553, + "tags": ["feet", "paw", "paws", "print", "prints"], + "unicode": "🐾" + }, + { + "group": 3, + "hexcode": "1F983", + "label": "turkey", + "order": 3554, + "tags": ["bird", "gobble", "thanksgiving"], + "unicode": "🦃" + }, + { + "group": 3, + "hexcode": "1F414", + "label": "chicken", + "order": 3555, + "tags": ["animal", "bird", "ornithology"], + "unicode": "🐔" + }, + { + "group": 3, + "hexcode": "1F413", + "label": "rooster", + "order": 3556, + "tags": ["animal", "bird", "ornithology"], + "unicode": "🐓" + }, + { + "group": 3, + "hexcode": "1F423", + "label": "hatching chick", + "order": 3557, + "tags": ["animal", "baby", "bird", "chick", "egg", "hatching"], + "unicode": "🐣" + }, + { + "group": 3, + "hexcode": "1F424", + "label": "baby chick", + "order": 3558, + "tags": ["animal", "baby", "bird", "chick", "ornithology"], + "unicode": "🐤" + }, + { + "group": 3, + "hexcode": "1F425", + "label": "front-facing baby chick", + "order": 3559, + "tags": [ + "animal", + "baby", + "bird", + "chick", + "front-facing", + "newborn", + "ornithology" + ], + "unicode": "🐥" + }, + { + "group": 3, + "hexcode": "1F426", + "label": "bird", + "order": 3560, + "tags": ["animal", "ornithology"], + "unicode": "🐦️" + }, + { + "group": 3, + "hexcode": "1F427", + "label": "penguin", + "order": 3561, + "tags": ["animal", "antarctica", "bird", "ornithology"], + "unicode": "🐧" + }, + { + "group": 3, + "hexcode": "1F54A", + "label": "dove", + "order": 3563, + "tags": ["bird", "fly", "ornithology", "peace"], + "unicode": "🕊️" + }, + { + "group": 3, + "hexcode": "1F985", + "label": "eagle", + "order": 3564, + "tags": ["animal", "bird", "ornithology"], + "unicode": "🦅" + }, + { + "group": 3, + "hexcode": "1F986", + "label": "duck", + "order": 3565, + "tags": ["animal", "bird", "ornithology"], + "unicode": "🦆" + }, + { + "group": 3, + "hexcode": "1F9A2", + "label": "swan", + "order": 3566, + "tags": ["animal", "bird", "cygnet", "duckling", "ornithology", "ugly"], + "unicode": "🦢" + }, + { + "group": 3, + "hexcode": "1F989", + "label": "owl", + "order": 3567, + "tags": ["animal", "bird", "ornithology", "wise"], + "unicode": "🦉" + }, + { + "group": 3, + "hexcode": "1F9A4", + "label": "dodo", + "order": 3568, + "tags": ["animal", "bird", "extinction", "large", "ornithology"], + "unicode": "🦤" + }, + { + "group": 3, + "hexcode": "1FAB6", + "label": "feather", + "order": 3569, + "tags": ["bird", "flight", "light", "plumage"], + "unicode": "🪶" + }, + { + "group": 3, + "hexcode": "1F9A9", + "label": "flamingo", + "order": 3570, + "tags": ["animal", "bird", "flamboyant", "ornithology", "tropical"], + "unicode": "🦩" + }, + { + "group": 3, + "hexcode": "1F99A", + "label": "peacock", + "order": 3571, + "tags": [ + "animal", + "bird", + "colorful", + "ornithology", + "ostentatious", + "peahen", + "pretty", + "proud" + ], + "unicode": "🦚" + }, + { + "group": 3, + "hexcode": "1F99C", + "label": "parrot", + "order": 3572, + "tags": ["animal", "bird", "ornithology", "pirate", "talk"], + "unicode": "🦜" + }, + { + "group": 3, + "hexcode": "1FABD", + "label": "wing", + "order": 3573, + "tags": [ + "angelic", + "ascend", + "aviation", + "bird", + "fly", + "flying", + "heavenly", + "mythology", + "soar" + ], + "unicode": "🪽" + }, + { + "group": 3, + "hexcode": "1F426-200D-2B1B", + "label": "black bird", + "order": 3574, + "tags": [ + "animal", + "beak", + "bird", + "black", + "caw", + "corvid", + "crow", + "ornithology", + "raven", + "rook" + ], + "unicode": "🐦‍⬛" + }, + { + "group": 3, + "hexcode": "1FABF", + "label": "goose", + "order": 3575, + "tags": [ + "animal", + "bird", + "duck", + "flock", + "fowl", + "gaggle", + "gander", + "geese", + "honk", + "ornithology", + "silly" + ], + "unicode": "🪿" + }, + { + "group": 3, + "hexcode": "1F426-200D-1F525", + "label": "phoenix", + "order": 3576, + "tags": [ + "ascend", + "ascension", + "emerge", + "fantasy", + "firebird", + "glory", + "immortal", + "rebirth", + "reincarnation", + "reinvent", + "renewal", + "revival", + "revive", + "rise", + "transform" + ], + "unicode": "🐦‍🔥" + }, + { + "group": 3, + "hexcode": "1F438", + "label": "frog", + "order": 3577, + "tags": ["animal", "face"], + "unicode": "🐸" + }, + { + "group": 3, + "hexcode": "1F40A", + "label": "crocodile", + "order": 3578, + "tags": ["animal", "zoo"], + "unicode": "🐊" + }, + { + "group": 3, + "hexcode": "1F422", + "label": "turtle", + "order": 3579, + "tags": ["animal", "terrapin", "tortoise"], + "unicode": "🐢" + }, + { + "group": 3, + "hexcode": "1F98E", + "label": "lizard", + "order": 3580, + "tags": ["animal", "reptile"], + "unicode": "🦎" + }, + { + "group": 3, + "hexcode": "1F40D", + "label": "snake", + "order": 3581, + "tags": ["animal", "bearer", "ophiuchus", "serpent", "zodiac"], + "unicode": "🐍" + }, + { + "group": 3, + "hexcode": "1F432", + "label": "dragon face", + "order": 3582, + "tags": ["animal", "dragon", "face", "fairy", "fairytale", "tale"], + "unicode": "🐲" + }, + { + "group": 3, + "hexcode": "1F409", + "label": "dragon", + "order": 3583, + "tags": ["animal", "fairy", "fairytale", "knights", "tale"], + "unicode": "🐉" + }, + { + "group": 3, + "hexcode": "1F995", + "label": "sauropod", + "order": 3584, + "tags": ["brachiosaurus", "brontosaurus", "dinosaur", "diplodocus"], + "unicode": "🦕" + }, + { + "group": 3, + "hexcode": "1F996", + "label": "T-Rex", + "order": 3585, + "tags": ["dinosaur", "rex", "t", "t-rex", "tyrannosaurus"], + "unicode": "🦖" + }, + { + "group": 3, + "hexcode": "1F433", + "label": "spouting whale", + "order": 3586, + "tags": ["animal", "beach", "face", "ocean", "spouting", "whale"], + "unicode": "🐳" + }, + { + "group": 3, + "hexcode": "1F40B", + "label": "whale", + "order": 3587, + "tags": ["animal", "beach", "ocean"], + "unicode": "🐋" + }, + { + "group": 3, + "hexcode": "1F42C", + "label": "dolphin", + "order": 3588, + "tags": ["animal", "beach", "flipper", "ocean"], + "unicode": "🐬" + }, + { + "group": 3, + "hexcode": "1F9AD", + "label": "seal", + "order": 3589, + "tags": ["animal", "lion", "ocean", "sea"], + "unicode": "🦭" + }, + { + "group": 3, + "hexcode": "1F41F", + "label": "fish", + "order": 3590, + "tags": ["animal", "dinner", "fishes", "fishing", "pisces", "zodiac"], + "unicode": "🐟️" + }, + { + "group": 3, + "hexcode": "1F420", + "label": "tropical fish", + "order": 3591, + "tags": ["animal", "fish", "fishes", "tropical"], + "unicode": "🐠" + }, + { + "group": 3, + "hexcode": "1F421", + "label": "blowfish", + "order": 3592, + "tags": ["animal", "fish"], + "unicode": "🐡" + }, + { + "group": 3, + "hexcode": "1F988", + "label": "shark", + "order": 3593, + "tags": ["animal", "fish"], + "unicode": "🦈" + }, + { + "group": 3, + "hexcode": "1F419", + "label": "octopus", + "order": 3594, + "tags": ["animal", "creature", "ocean"], + "unicode": "🐙" + }, + { + "group": 3, + "hexcode": "1F41A", + "label": "spiral shell", + "order": 3595, + "tags": ["animal", "beach", "conch", "sea", "shell", "spiral"], + "unicode": "🐚" + }, + { + "group": 3, + "hexcode": "1FAB8", + "label": "coral", + "order": 3596, + "tags": ["change", "climate", "ocean", "reef", "sea"], + "unicode": "🪸" + }, + { + "group": 3, + "hexcode": "1FABC", + "label": "jellyfish", + "order": 3597, + "tags": [ + "animal", + "aquarium", + "burn", + "invertebrate", + "jelly", + "life", + "marine", + "ocean", + "ouch", + "plankton", + "sea", + "sting", + "stinger", + "tentacles" + ], + "unicode": "🪼" + }, + { + "group": 3, + "hexcode": "1F980", + "label": "crab", + "order": 3598, + "tags": ["cancer", "zodiac"], + "unicode": "🦀" + }, + { + "group": 3, + "hexcode": "1F99E", + "label": "lobster", + "order": 3599, + "tags": ["animal", "bisque", "claws", "seafood"], + "unicode": "🦞" + }, + { + "group": 3, + "hexcode": "1F990", + "label": "shrimp", + "order": 3600, + "tags": ["food", "shellfish", "small"], + "unicode": "🦐" + }, + { + "group": 3, + "hexcode": "1F991", + "label": "squid", + "order": 3601, + "tags": ["animal", "food", "mollusk"], + "unicode": "🦑" + }, + { + "group": 3, + "hexcode": "1F9AA", + "label": "oyster", + "order": 3602, + "tags": ["diving", "pearl"], + "unicode": "🦪" + }, + { + "group": 3, + "hexcode": "1F40C", + "label": "snail", + "order": 3603, + "tags": ["animal", "escargot", "garden", "nature", "slug"], + "unicode": "🐌" + }, + { + "group": 3, + "hexcode": "1F98B", + "label": "butterfly", + "order": 3604, + "tags": ["insect", "pretty"], + "unicode": "🦋" + }, + { + "group": 3, + "hexcode": "1F41B", + "label": "bug", + "order": 3605, + "tags": ["animal", "garden", "insect"], + "unicode": "🐛" + }, + { + "group": 3, + "hexcode": "1F41C", + "label": "ant", + "order": 3606, + "tags": ["animal", "garden", "insect"], + "unicode": "🐜" + }, + { + "group": 3, + "hexcode": "1F41D", + "label": "honeybee", + "order": 3607, + "tags": [ + "animal", + "bee", + "bumblebee", + "honey", + "insect", + "nature", + "spring" + ], + "unicode": "🐝" + }, + { + "group": 3, + "hexcode": "1FAB2", + "label": "beetle", + "order": 3608, + "tags": ["animal", "bug", "insect"], + "unicode": "🪲" + }, + { + "group": 3, + "hexcode": "1F41E", + "label": "lady beetle", + "order": 3609, + "tags": [ + "animal", + "beetle", + "garden", + "insect", + "lady", + "ladybird", + "ladybug", + "nature" + ], + "unicode": "🐞" + }, + { + "group": 3, + "hexcode": "1F997", + "label": "cricket", + "order": 3610, + "tags": ["animal", "bug", "grasshopper", "insect", "orthoptera"], + "unicode": "🦗" + }, + { + "group": 3, + "hexcode": "1FAB3", + "label": "cockroach", + "order": 3611, + "tags": ["animal", "insect", "pest", "roach"], + "unicode": "🪳" + }, + { + "group": 3, + "hexcode": "1F577", + "label": "spider", + "order": 3613, + "tags": ["animal", "insect"], + "unicode": "🕷️" + }, + { + "group": 3, + "hexcode": "1F578", + "label": "spider web", + "order": 3615, + "tags": ["spider", "web"], + "unicode": "🕸️" + }, + { + "group": 3, + "hexcode": "1F982", + "label": "scorpion", + "order": 3616, + "tags": ["scorpio", "scorpius", "zodiac"], + "unicode": "🦂" + }, + { + "group": 3, + "hexcode": "1F99F", + "label": "mosquito", + "order": 3617, + "tags": [ + "bite", + "disease", + "fever", + "insect", + "malaria", + "pest", + "virus" + ], + "unicode": "🦟" + }, + { + "group": 3, + "hexcode": "1FAB0", + "label": "fly", + "order": 3618, + "tags": ["animal", "disease", "insect", "maggot", "pest", "rotting"], + "unicode": "🪰" + }, + { + "group": 3, + "hexcode": "1FAB1", + "label": "worm", + "order": 3619, + "tags": ["animal", "annelid", "earthworm", "parasite"], + "unicode": "🪱" + }, + { + "group": 3, + "hexcode": "1F9A0", + "label": "microbe", + "order": 3620, + "tags": ["amoeba", "bacteria", "science", "virus"], + "unicode": "🦠" + }, + { + "group": 3, + "hexcode": "1F490", + "label": "bouquet", + "order": 3621, + "tags": [ + "anniversary", + "birthday", + "date", + "flower", + "love", + "plant", + "romance" + ], + "unicode": "💐" + }, + { + "group": 3, + "hexcode": "1F338", + "label": "cherry blossom", + "order": 3622, + "tags": [ + "blossom", + "cherry", + "flower", + "plant", + "spring", + "springtime" + ], + "unicode": "🌸" + }, + { + "group": 3, + "hexcode": "1F4AE", + "label": "white flower", + "order": 3623, + "tags": ["flower", "white"], + "unicode": "💮" + }, + { + "group": 3, + "hexcode": "1FAB7", + "label": "lotus", + "order": 3624, + "tags": [ + "beauty", + "buddhism", + "calm", + "flower", + "hinduism", + "peace", + "purity", + "serenity" + ], + "unicode": "🪷" + }, + { + "group": 3, + "hexcode": "1F3F5", + "label": "rosette", + "order": 3626, + "tags": ["plant"], + "unicode": "🏵️" + }, + { + "group": 3, + "hexcode": "1F339", + "label": "rose", + "order": 3627, + "tags": [ + "beauty", + "elegant", + "flower", + "love", + "plant", + "red", + "valentine" + ], + "unicode": "🌹" + }, + { + "group": 3, + "hexcode": "1F940", + "label": "wilted flower", + "order": 3628, + "tags": ["dying", "flower", "wilted"], + "unicode": "🥀" + }, + { + "group": 3, + "hexcode": "1F33A", + "label": "hibiscus", + "order": 3629, + "tags": ["flower", "plant"], + "unicode": "🌺" + }, + { + "group": 3, + "hexcode": "1F33B", + "label": "sunflower", + "order": 3630, + "tags": ["flower", "outdoors", "plant", "sun"], + "unicode": "🌻" + }, + { + "group": 3, + "hexcode": "1F33C", + "label": "blossom", + "order": 3631, + "tags": ["buttercup", "dandelion", "flower", "plant"], + "unicode": "🌼" + }, + { + "group": 3, + "hexcode": "1F337", + "label": "tulip", + "order": 3632, + "tags": ["blossom", "flower", "growth", "plant"], + "unicode": "🌷" + }, + { + "group": 3, + "hexcode": "1FABB", + "label": "hyacinth", + "order": 3633, + "tags": [ + "bloom", + "bluebonnet", + "flower", + "indigo", + "lavender", + "lilac", + "lupine", + "plant", + "purple", + "shrub", + "snapdragon", + "spring", + "violet" + ], + "unicode": "🪻" + }, + { + "group": 3, + "hexcode": "1F331", + "label": "seedling", + "order": 3634, + "tags": ["plant", "sapling", "sprout", "young"], + "unicode": "🌱" + }, + { + "group": 3, + "hexcode": "1FAB4", + "label": "potted plant", + "order": 3635, + "tags": [ + "decor", + "grow", + "house", + "nurturing", + "plant", + "pot", + "potted" + ], + "unicode": "🪴" + }, + { + "group": 3, + "hexcode": "1F332", + "label": "evergreen tree", + "order": 3636, + "tags": ["christmas", "evergreen", "forest", "pine", "tree"], + "unicode": "🌲" + }, + { + "group": 3, + "hexcode": "1F333", + "label": "deciduous tree", + "order": 3637, + "tags": ["deciduous", "forest", "green", "habitat", "shedding", "tree"], + "unicode": "🌳" + }, + { + "group": 3, + "hexcode": "1F334", + "label": "palm tree", + "order": 3638, + "tags": ["beach", "palm", "plant", "tree", "tropical"], + "unicode": "🌴" + }, + { + "group": 3, + "hexcode": "1F335", + "label": "cactus", + "order": 3639, + "tags": ["desert", "drought", "nature", "plant"], + "unicode": "🌵" + }, + { + "group": 3, + "hexcode": "1F33E", + "label": "sheaf of rice", + "order": 3640, + "tags": ["ear", "grain", "grains", "plant", "rice", "sheaf"], + "unicode": "🌾" + }, + { + "group": 3, + "hexcode": "1F33F", + "label": "herb", + "order": 3641, + "tags": ["leaf", "plant"], + "unicode": "🌿" + }, + { + "group": 3, + "hexcode": "2618", + "label": "shamrock", + "order": 3643, + "tags": ["irish", "plant"], + "unicode": "☘️" + }, + { + "group": 3, + "hexcode": "1F340", + "label": "four leaf clover", + "order": 3644, + "tags": [ + "4", + "clover", + "four", + "four-leaf", + "irish", + "leaf", + "lucky", + "plant" + ], + "unicode": "🍀" + }, + { + "group": 3, + "hexcode": "1F341", + "label": "maple leaf", + "order": 3645, + "tags": ["falling", "leaf", "maple"], + "unicode": "🍁" + }, + { + "group": 3, + "hexcode": "1F342", + "label": "fallen leaf", + "order": 3646, + "tags": ["autumn", "fall", "fallen", "falling", "leaf"], + "unicode": "🍂" + }, + { + "group": 3, + "hexcode": "1F343", + "label": "leaf fluttering in wind", + "order": 3647, + "tags": ["blow", "flutter", "fluttering", "leaf", "wind"], + "unicode": "🍃" + }, + { + "group": 3, + "hexcode": "1FAB9", + "label": "empty nest", + "order": 3648, + "tags": ["branch", "empty", "home", "nest", "nesting"], + "unicode": "🪹" + }, + { + "group": 3, + "hexcode": "1FABA", + "label": "nest with eggs", + "order": 3649, + "tags": ["bird", "branch", "egg", "eggs", "nest", "nesting"], + "unicode": "🪺" + }, + { + "group": 3, + "hexcode": "1F344", + "label": "mushroom", + "order": 3650, + "tags": ["fungus", "toadstool"], + "unicode": "🍄" + }, + { + "group": 3, + "hexcode": "1FABE", + "label": "leafless tree", + "order": 3651, + "tags": [ + "bare", + "barren", + "branches", + "dead", + "drought", + "leafless", + "tree", + "trunk", + "winter", + "wood" + ], + "unicode": "🪾" + } + ] + }, + { + "name": "Food & Drink", + "emojis": [ + { + "group": 4, + "hexcode": "1F347", + "label": "grapes", + "order": 3652, + "tags": ["dionysus", "fruit", "grape"], + "unicode": "🍇" + }, + { + "group": 4, + "hexcode": "1F348", + "label": "melon", + "order": 3653, + "tags": ["cantaloupe", "fruit"], + "unicode": "🍈" + }, + { + "group": 4, + "hexcode": "1F349", + "label": "watermelon", + "order": 3654, + "tags": ["fruit"], + "unicode": "🍉" + }, + { + "group": 4, + "hexcode": "1F34A", + "label": "tangerine", + "order": 3655, + "tags": ["c", "citrus", "fruit", "nectarine", "orange", "vitamin"], + "unicode": "🍊" + }, + { + "group": 4, + "hexcode": "1F34B", + "label": "lemon", + "order": 3656, + "tags": ["citrus", "fruit", "sour"], + "unicode": "🍋" + }, + { + "group": 4, + "hexcode": "1F34B-200D-1F7E9", + "label": "lime", + "order": 3657, + "tags": [ + "acidity", + "citrus", + "cocktail", + "fruit", + "garnish", + "key", + "margarita", + "mojito", + "refreshing", + "salsa", + "sour", + "tangy", + "tequila", + "tropical", + "zest" + ], + "unicode": "🍋‍🟩" + }, + { + "group": 4, + "hexcode": "1F34C", + "label": "banana", + "order": 3658, + "tags": ["fruit", "potassium"], + "unicode": "🍌" + }, + { + "group": 4, + "hexcode": "1F34D", + "label": "pineapple", + "order": 3659, + "tags": ["colada", "fruit", "pina", "tropical"], + "unicode": "🍍" + }, + { + "group": 4, + "hexcode": "1F96D", + "label": "mango", + "order": 3660, + "tags": ["food", "fruit", "tropical"], + "unicode": "🥭" + }, + { + "group": 4, + "hexcode": "1F34E", + "label": "red apple", + "order": 3661, + "tags": ["apple", "diet", "food", "fruit", "health", "red", "ripe"], + "unicode": "🍎" + }, + { + "group": 4, + "hexcode": "1F34F", + "label": "green apple", + "order": 3662, + "tags": ["apple", "fruit", "green"], + "unicode": "🍏" + }, + { + "group": 4, + "hexcode": "1F350", + "label": "pear", + "order": 3663, + "tags": ["fruit"], + "unicode": "🍐" + }, + { + "group": 4, + "hexcode": "1F351", + "label": "peach", + "order": 3664, + "tags": ["fruit"], + "unicode": "🍑" + }, + { + "group": 4, + "hexcode": "1F352", + "label": "cherries", + "order": 3665, + "tags": ["berries", "cherry", "fruit", "red"], + "unicode": "🍒" + }, + { + "group": 4, + "hexcode": "1F353", + "label": "strawberry", + "order": 3666, + "tags": ["berry", "fruit"], + "unicode": "🍓" + }, + { + "group": 4, + "hexcode": "1FAD0", + "label": "blueberries", + "order": 3667, + "tags": [ + "berries", + "berry", + "bilberry", + "blue", + "blueberry", + "food", + "fruit" + ], + "unicode": "🫐" + }, + { + "group": 4, + "hexcode": "1F95D", + "label": "kiwi fruit", + "order": 3668, + "tags": ["food", "fruit", "kiwi"], + "unicode": "🥝" + }, + { + "group": 4, + "hexcode": "1F345", + "label": "tomato", + "order": 3669, + "tags": ["food", "fruit", "vegetable"], + "unicode": "🍅" + }, + { + "group": 4, + "hexcode": "1FAD2", + "label": "olive", + "order": 3670, + "tags": ["food"], + "unicode": "🫒" + }, + { + "group": 4, + "hexcode": "1F965", + "label": "coconut", + "order": 3671, + "tags": ["colada", "palm", "piña"], + "unicode": "🥥" + }, + { + "group": 4, + "hexcode": "1F951", + "label": "avocado", + "order": 3672, + "tags": ["food", "fruit"], + "unicode": "🥑" + }, + { + "group": 4, + "hexcode": "1F346", + "label": "eggplant", + "order": 3673, + "tags": ["aubergine", "vegetable"], + "unicode": "🍆" + }, + { + "group": 4, + "hexcode": "1F954", + "label": "potato", + "order": 3674, + "tags": ["food", "vegetable"], + "unicode": "🥔" + }, + { + "group": 4, + "hexcode": "1F955", + "label": "carrot", + "order": 3675, + "tags": ["food", "vegetable"], + "unicode": "🥕" + }, + { + "group": 4, + "hexcode": "1F33D", + "label": "ear of corn", + "order": 3676, + "tags": ["corn", "crops", "ear", "farm", "maize", "maze"], + "unicode": "🌽" + }, + { + "group": 4, + "hexcode": "1F336", + "label": "hot pepper", + "order": 3678, + "tags": ["hot", "pepper"], + "unicode": "🌶️" + }, + { + "group": 4, + "hexcode": "1FAD1", + "label": "bell pepper", + "order": 3679, + "tags": ["bell", "capsicum", "food", "pepper", "vegetable"], + "unicode": "🫑" + }, + { + "group": 4, + "hexcode": "1F952", + "label": "cucumber", + "order": 3680, + "tags": ["food", "pickle", "vegetable"], + "unicode": "🥒" + }, + { + "group": 4, + "hexcode": "1F96C", + "label": "leafy green", + "order": 3681, + "tags": [ + "bok", + "burgers", + "cabbage", + "choy", + "green", + "kale", + "leafy", + "lettuce", + "salad" + ], + "unicode": "🥬" + }, + { + "group": 4, + "hexcode": "1F966", + "label": "broccoli", + "order": 3682, + "tags": ["cabbage", "wild"], + "unicode": "🥦" + }, + { + "group": 4, + "hexcode": "1F9C4", + "label": "garlic", + "order": 3683, + "tags": ["flavoring"], + "unicode": "🧄" + }, + { + "group": 4, + "hexcode": "1F9C5", + "label": "onion", + "order": 3684, + "tags": ["flavoring"], + "unicode": "🧅" + }, + { + "group": 4, + "hexcode": "1F95C", + "label": "peanuts", + "order": 3685, + "tags": ["food", "nut", "peanut", "vegetable"], + "unicode": "🥜" + }, + { + "group": 4, + "hexcode": "1FAD8", + "label": "beans", + "order": 3686, + "tags": ["food", "kidney", "legume", "small"], + "unicode": "🫘" + }, + { + "group": 4, + "hexcode": "1F330", + "label": "chestnut", + "order": 3687, + "tags": ["almond", "plant"], + "unicode": "🌰" + }, + { + "group": 4, + "hexcode": "1FADA", + "label": "ginger root", + "order": 3688, + "tags": [ + "beer", + "ginger", + "health", + "herb", + "natural", + "root", + "spice" + ], + "unicode": "🫚" + }, + { + "group": 4, + "hexcode": "1FADB", + "label": "pea pod", + "order": 3689, + "tags": [ + "beans", + "beanstalk", + "edamame", + "legume", + "pea", + "pod", + "soybean", + "vegetable", + "veggie" + ], + "unicode": "🫛" + }, + { + "group": 4, + "hexcode": "1F344-200D-1F7EB", + "label": "brown mushroom", + "order": 3690, + "tags": [ + "food", + "fungi", + "fungus", + "mushroom", + "nature", + "pizza", + "portobello", + "shiitake", + "shroom", + "spore", + "sprout", + "toppings", + "truffle", + "vegetable", + "vegetarian", + "veggie" + ], + "unicode": "🍄‍🟫" + }, + { + "group": 4, + "hexcode": "1FADC", + "label": "root vegetable", + "order": 3691, + "tags": [ + "beet", + "food", + "garden", + "radish", + "root", + "salad", + "turnip", + "vegetable", + "vegetarian" + ], + "unicode": "🫜" + }, + { + "group": 4, + "hexcode": "1F35E", + "label": "bread", + "order": 3692, + "tags": [ + "carbs", + "food", + "grain", + "loaf", + "restaurant", + "toast", + "wheat" + ], + "unicode": "🍞" + }, + { + "group": 4, + "hexcode": "1F950", + "label": "croissant", + "order": 3693, + "tags": ["bread", "breakfast", "crescent", "food", "french", "roll"], + "unicode": "🥐" + }, + { + "group": 4, + "hexcode": "1F956", + "label": "baguette bread", + "order": 3694, + "tags": ["baguette", "bread", "food", "french"], + "unicode": "🥖" + }, + { + "group": 4, + "hexcode": "1FAD3", + "label": "flatbread", + "order": 3695, + "tags": ["arepa", "bread", "food", "gordita", "lavash", "naan", "pita"], + "unicode": "🫓" + }, + { + "group": 4, + "hexcode": "1F968", + "label": "pretzel", + "order": 3696, + "tags": ["convoluted", "twisted"], + "unicode": "🥨" + }, + { + "group": 4, + "hexcode": "1F96F", + "label": "bagel", + "order": 3697, + "tags": ["bakery", "bread", "breakfast", "schmear"], + "unicode": "🥯" + }, + { + "group": 4, + "hexcode": "1F95E", + "label": "pancakes", + "order": 3698, + "tags": ["breakfast", "crêpe", "food", "hotcake", "pancake"], + "unicode": "🥞" + }, + { + "group": 4, + "hexcode": "1F9C7", + "label": "waffle", + "order": 3699, + "tags": ["breakfast", "indecisive", "iron"], + "unicode": "🧇" + }, + { + "group": 4, + "hexcode": "1F9C0", + "label": "cheese wedge", + "order": 3700, + "tags": ["cheese", "wedge"], + "unicode": "🧀" + }, + { + "group": 4, + "hexcode": "1F356", + "label": "meat on bone", + "order": 3701, + "tags": ["bone", "meat"], + "unicode": "🍖" + }, + { + "group": 4, + "hexcode": "1F357", + "label": "poultry leg", + "order": 3702, + "tags": [ + "bone", + "chicken", + "drumstick", + "hungry", + "leg", + "poultry", + "turkey" + ], + "unicode": "🍗" + }, + { + "group": 4, + "hexcode": "1F969", + "label": "cut of meat", + "order": 3703, + "tags": ["chop", "cut", "lambchop", "meat", "porkchop", "red", "steak"], + "unicode": "🥩" + }, + { + "group": 4, + "hexcode": "1F953", + "label": "bacon", + "order": 3704, + "tags": ["breakfast", "food", "meat"], + "unicode": "🥓" + }, + { + "group": 4, + "hexcode": "1F354", + "label": "hamburger", + "order": 3705, + "tags": ["burger", "eat", "fast", "food", "hungry"], + "unicode": "🍔" + }, + { + "group": 4, + "hexcode": "1F35F", + "label": "french fries", + "order": 3706, + "tags": ["fast", "food", "french", "fries"], + "unicode": "🍟" + }, + { + "group": 4, + "hexcode": "1F355", + "label": "pizza", + "order": 3707, + "tags": ["cheese", "food", "hungry", "pepperoni", "slice"], + "unicode": "🍕" + }, + { + "group": 4, + "hexcode": "1F32D", + "label": "hot dog", + "order": 3708, + "tags": ["dog", "frankfurter", "hot", "hotdog", "sausage"], + "unicode": "🌭" + }, + { + "group": 4, + "hexcode": "1F96A", + "label": "sandwich", + "order": 3709, + "tags": ["bread"], + "unicode": "🥪" + }, + { + "group": 4, + "hexcode": "1F32E", + "label": "taco", + "order": 3710, + "tags": ["mexican"], + "unicode": "🌮" + }, + { + "group": 4, + "hexcode": "1F32F", + "label": "burrito", + "order": 3711, + "tags": ["mexican", "wrap"], + "unicode": "🌯" + }, + { + "group": 4, + "hexcode": "1FAD4", + "label": "tamale", + "order": 3712, + "tags": ["food", "mexican", "pamonha", "wrapped"], + "unicode": "🫔" + }, + { + "group": 4, + "hexcode": "1F959", + "label": "stuffed flatbread", + "order": 3713, + "tags": ["falafel", "flatbread", "food", "gyro", "kebab", "stuffed"], + "unicode": "🥙" + }, + { + "group": 4, + "hexcode": "1F9C6", + "label": "falafel", + "order": 3714, + "tags": ["chickpea", "meatball"], + "unicode": "🧆" + }, + { + "group": 4, + "hexcode": "1F95A", + "label": "egg", + "order": 3715, + "tags": ["breakfast", "food"], + "unicode": "🥚" + }, + { + "group": 4, + "hexcode": "1F373", + "label": "cooking", + "order": 3716, + "tags": [ + "breakfast", + "easy", + "egg", + "fry", + "frying", + "over", + "pan", + "restaurant", + "side", + "sunny", + "up" + ], + "unicode": "🍳" + }, + { + "group": 4, + "hexcode": "1F958", + "label": "shallow pan of food", + "order": 3717, + "tags": ["casserole", "food", "paella", "pan", "shallow"], + "unicode": "🥘" + }, + { + "group": 4, + "hexcode": "1F372", + "label": "pot of food", + "order": 3718, + "tags": ["food", "pot", "soup", "stew"], + "unicode": "🍲" + }, + { + "group": 4, + "hexcode": "1FAD5", + "label": "fondue", + "order": 3719, + "tags": ["cheese", "chocolate", "food", "melted", "pot", "ski"], + "unicode": "🫕" + }, + { + "group": 4, + "hexcode": "1F963", + "label": "bowl with spoon", + "order": 3720, + "tags": [ + "bowl", + "breakfast", + "cereal", + "congee", + "oatmeal", + "porridge", + "spoon" + ], + "unicode": "🥣" + }, + { + "group": 4, + "hexcode": "1F957", + "label": "green salad", + "order": 3721, + "tags": ["food", "green", "salad"], + "unicode": "🥗" + }, + { + "group": 4, + "hexcode": "1F37F", + "label": "popcorn", + "order": 3722, + "tags": ["corn", "movie", "pop"], + "unicode": "🍿" + }, + { + "group": 4, + "hexcode": "1F9C8", + "label": "butter", + "order": 3723, + "tags": ["dairy"], + "unicode": "🧈" + }, + { + "group": 4, + "hexcode": "1F9C2", + "label": "salt", + "order": 3724, + "tags": [ + "condiment", + "flavor", + "mad", + "salty", + "shaker", + "taste", + "upset" + ], + "unicode": "🧂" + }, + { + "group": 4, + "hexcode": "1F96B", + "label": "canned food", + "order": 3725, + "tags": ["can", "canned", "food"], + "unicode": "🥫" + }, + { + "group": 4, + "hexcode": "1F371", + "label": "bento box", + "order": 3726, + "tags": ["bento", "box", "food"], + "unicode": "🍱" + }, + { + "group": 4, + "hexcode": "1F358", + "label": "rice cracker", + "order": 3727, + "tags": ["cracker", "food", "rice"], + "unicode": "🍘" + }, + { + "group": 4, + "hexcode": "1F359", + "label": "rice ball", + "order": 3728, + "tags": ["ball", "food", "japanese", "rice"], + "unicode": "🍙" + }, + { + "group": 4, + "hexcode": "1F35A", + "label": "cooked rice", + "order": 3729, + "tags": ["cooked", "food", "rice"], + "unicode": "🍚" + }, + { + "group": 4, + "hexcode": "1F35B", + "label": "curry rice", + "order": 3730, + "tags": ["curry", "food", "rice"], + "unicode": "🍛" + }, + { + "group": 4, + "hexcode": "1F35C", + "label": "steaming bowl", + "order": 3731, + "tags": [ + "bowl", + "chopsticks", + "food", + "noodle", + "pho", + "ramen", + "soup", + "steaming" + ], + "unicode": "🍜" + }, + { + "group": 4, + "hexcode": "1F35D", + "label": "spaghetti", + "order": 3732, + "tags": ["food", "meatballs", "pasta", "restaurant"], + "unicode": "🍝" + }, + { + "group": 4, + "hexcode": "1F360", + "label": "roasted sweet potato", + "order": 3733, + "tags": ["food", "potato", "roasted", "sweet"], + "unicode": "🍠" + }, + { + "group": 4, + "hexcode": "1F362", + "label": "oden", + "order": 3734, + "tags": ["food", "kebab", "restaurant", "seafood", "skewer", "stick"], + "unicode": "🍢" + }, + { + "group": 4, + "hexcode": "1F363", + "label": "sushi", + "order": 3735, + "tags": ["food"], + "unicode": "🍣" + }, + { + "group": 4, + "hexcode": "1F364", + "label": "fried shrimp", + "order": 3736, + "tags": ["fried", "prawn", "shrimp", "tempura"], + "unicode": "🍤" + }, + { + "group": 4, + "hexcode": "1F365", + "label": "fish cake with swirl", + "order": 3737, + "tags": ["cake", "fish", "food", "pastry", "restaurant", "swirl"], + "unicode": "🍥" + }, + { + "group": 4, + "hexcode": "1F96E", + "label": "moon cake", + "order": 3738, + "tags": ["autumn", "cake", "festival", "moon", "yuèbǐng"], + "unicode": "🥮" + }, + { + "group": 4, + "hexcode": "1F361", + "label": "dango", + "order": 3739, + "tags": ["dessert", "japanese", "skewer", "stick", "sweet"], + "unicode": "🍡" + }, + { + "group": 4, + "hexcode": "1F95F", + "label": "dumpling", + "order": 3740, + "tags": ["empanada", "gyōza", "jiaozi", "pierogi", "potsticker"], + "unicode": "🥟" + }, + { + "group": 4, + "hexcode": "1F960", + "label": "fortune cookie", + "order": 3741, + "tags": ["cookie", "fortune", "prophecy"], + "unicode": "🥠" + }, + { + "group": 4, + "hexcode": "1F961", + "label": "takeout box", + "order": 3742, + "tags": [ + "box", + "chopsticks", + "delivery", + "food", + "oyster", + "pail", + "takeout" + ], + "unicode": "🥡" + }, + { + "group": 4, + "hexcode": "1F366", + "label": "soft ice cream", + "order": 3743, + "tags": [ + "cream", + "dessert", + "food", + "ice", + "icecream", + "restaurant", + "serve", + "soft", + "sweet" + ], + "unicode": "🍦" + }, + { + "group": 4, + "hexcode": "1F367", + "label": "shaved ice", + "order": 3744, + "tags": ["dessert", "ice", "restaurant", "shaved", "sweet"], + "unicode": "🍧" + }, + { + "group": 4, + "hexcode": "1F368", + "label": "ice cream", + "order": 3745, + "tags": ["cream", "dessert", "food", "ice", "restaurant", "sweet"], + "unicode": "🍨" + }, + { + "group": 4, + "hexcode": "1F369", + "label": "doughnut", + "order": 3746, + "tags": ["breakfast", "dessert", "donut", "food", "sweet"], + "unicode": "🍩" + }, + { + "group": 4, + "hexcode": "1F36A", + "label": "cookie", + "order": 3747, + "tags": ["chip", "chocolate", "dessert", "sweet"], + "unicode": "🍪" + }, + { + "group": 4, + "hexcode": "1F382", + "label": "birthday cake", + "order": 3748, + "tags": [ + "bday", + "birthday", + "cake", + "celebration", + "dessert", + "happy", + "pastry", + "sweet" + ], + "unicode": "🎂" + }, + { + "group": 4, + "hexcode": "1F370", + "label": "shortcake", + "order": 3749, + "tags": ["cake", "dessert", "pastry", "slice", "sweet"], + "unicode": "🍰" + }, + { + "group": 4, + "hexcode": "1F9C1", + "label": "cupcake", + "order": 3750, + "tags": ["bakery", "dessert", "sprinkles", "sugar", "sweet", "treat"], + "unicode": "🧁" + }, + { + "group": 4, + "hexcode": "1F967", + "label": "pie", + "order": 3751, + "tags": [ + "apple", + "filling", + "fruit", + "meat", + "pastry", + "pumpkin", + "slice" + ], + "unicode": "🥧" + }, + { + "group": 4, + "hexcode": "1F36B", + "label": "chocolate bar", + "order": 3752, + "tags": [ + "bar", + "candy", + "chocolate", + "dessert", + "halloween", + "sweet", + "tooth" + ], + "unicode": "🍫" + }, + { + "group": 4, + "hexcode": "1F36C", + "label": "candy", + "order": 3753, + "tags": [ + "cavities", + "dessert", + "halloween", + "restaurant", + "sweet", + "tooth", + "wrapper" + ], + "unicode": "🍬" + }, + { + "group": 4, + "hexcode": "1F36D", + "label": "lollipop", + "order": 3754, + "tags": ["candy", "dessert", "food", "restaurant", "sweet"], + "unicode": "🍭" + }, + { + "group": 4, + "hexcode": "1F36E", + "label": "custard", + "order": 3755, + "tags": ["dessert", "pudding", "sweet"], + "unicode": "🍮" + }, + { + "group": 4, + "hexcode": "1F36F", + "label": "honey pot", + "order": 3756, + "tags": [ + "barrel", + "bear", + "food", + "honey", + "honeypot", + "jar", + "pot", + "sweet" + ], + "unicode": "🍯" + }, + { + "group": 4, + "hexcode": "1F37C", + "label": "baby bottle", + "order": 3757, + "tags": [ + "babies", + "baby", + "birth", + "born", + "bottle", + "drink", + "infant", + "milk", + "newborn" + ], + "unicode": "🍼" + }, + { + "group": 4, + "hexcode": "1F95B", + "label": "glass of milk", + "order": 3758, + "tags": ["drink", "glass", "milk"], + "unicode": "🥛" + }, + { + "group": 4, + "hexcode": "2615", + "label": "hot beverage", + "order": 3759, + "tags": [ + "beverage", + "cafe", + "caffeine", + "chai", + "coffee", + "drink", + "hot", + "morning", + "steaming", + "tea" + ], + "unicode": "☕️" + }, + { + "group": 4, + "hexcode": "1FAD6", + "label": "teapot", + "order": 3760, + "tags": ["brew", "drink", "food", "pot", "tea"], + "unicode": "🫖" + }, + { + "group": 4, + "hexcode": "1F375", + "label": "teacup without handle", + "order": 3761, + "tags": [ + "beverage", + "cup", + "drink", + "handle", + "oolong", + "tea", + "teacup" + ], + "unicode": "🍵" + }, + { + "group": 4, + "hexcode": "1F376", + "label": "sake", + "order": 3762, + "tags": ["bar", "beverage", "bottle", "cup", "drink", "restaurant"], + "unicode": "🍶" + }, + { + "group": 4, + "hexcode": "1F37E", + "label": "bottle with popping cork", + "order": 3763, + "tags": ["bar", "bottle", "cork", "drink", "popping"], + "unicode": "🍾" + }, + { + "group": 4, + "hexcode": "1F377", + "label": "wine glass", + "order": 3764, + "tags": [ + "alcohol", + "bar", + "beverage", + "booze", + "club", + "drink", + "drinking", + "drinks", + "glass", + "restaurant", + "wine" + ], + "unicode": "🍷" + }, + { + "group": 4, + "hexcode": "1F378", + "label": "cocktail glass", + "order": 3765, + "tags": [ + "alcohol", + "bar", + "booze", + "club", + "cocktail", + "drink", + "drinking", + "drinks", + "glass", + "mad", + "martini", + "men" + ], + "unicode": "🍸️" + }, + { + "group": 4, + "hexcode": "1F379", + "label": "tropical drink", + "order": 3766, + "tags": [ + "alcohol", + "bar", + "booze", + "club", + "cocktail", + "drink", + "drinking", + "drinks", + "drunk", + "mai", + "party", + "tai", + "tropical", + "tropics" + ], + "unicode": "🍹" + }, + { + "group": 4, + "hexcode": "1F37A", + "label": "beer mug", + "order": 3767, + "tags": [ + "alcohol", + "ale", + "bar", + "beer", + "booze", + "drink", + "drinking", + "drinks", + "mug", + "octoberfest", + "oktoberfest", + "pint", + "stein", + "summer" + ], + "unicode": "🍺" + }, + { + "group": 4, + "hexcode": "1F37B", + "label": "clinking beer mugs", + "order": 3768, + "tags": [ + "alcohol", + "bar", + "beer", + "booze", + "bottoms", + "cheers", + "clink", + "clinking", + "drinking", + "drinks", + "mugs" + ], + "unicode": "🍻" + }, + { + "group": 4, + "hexcode": "1F942", + "label": "clinking glasses", + "order": 3769, + "tags": ["celebrate", "clink", "clinking", "drink", "glass", "glasses"], + "unicode": "🥂" + }, + { + "group": 4, + "hexcode": "1F943", + "label": "tumbler glass", + "order": 3770, + "tags": [ + "glass", + "liquor", + "scotch", + "shot", + "tumbler", + "whiskey", + "whisky" + ], + "unicode": "🥃" + }, + { + "group": 4, + "hexcode": "1FAD7", + "label": "pouring liquid", + "order": 3771, + "tags": [ + "accident", + "drink", + "empty", + "glass", + "liquid", + "oops", + "pour", + "pouring", + "spill", + "water" + ], + "unicode": "🫗" + }, + { + "group": 4, + "hexcode": "1F964", + "label": "cup with straw", + "order": 3772, + "tags": [ + "cup", + "drink", + "juice", + "malt", + "soda", + "soft", + "straw", + "water" + ], + "unicode": "🥤" + }, + { + "group": 4, + "hexcode": "1F9CB", + "label": "bubble tea", + "order": 3773, + "tags": ["boba", "bubble", "food", "milk", "pearl", "tea"], + "unicode": "🧋" + }, + { + "group": 4, + "hexcode": "1F9C3", + "label": "beverage box", + "order": 3774, + "tags": ["beverage", "box", "juice", "straw", "sweet"], + "unicode": "🧃" + }, + { + "group": 4, + "hexcode": "1F9C9", + "label": "mate", + "order": 3775, + "tags": ["drink"], + "unicode": "🧉" + }, + { + "group": 4, + "hexcode": "1F9CA", + "label": "ice", + "order": 3776, + "tags": ["cold", "cube", "iceberg"], + "unicode": "🧊" + }, + { + "group": 4, + "hexcode": "1F962", + "label": "chopsticks", + "order": 3777, + "tags": ["hashi", "jeotgarak", "kuaizi"], + "unicode": "🥢" + }, + { + "group": 4, + "hexcode": "1F37D", + "label": "fork and knife with plate", + "order": 3779, + "tags": ["cooking", "dinner", "eat", "fork", "knife", "plate"], + "unicode": "🍽️" + }, + { + "group": 4, + "hexcode": "1F374", + "label": "fork and knife", + "order": 3780, + "tags": [ + "breakfast", + "breaky", + "cooking", + "cutlery", + "delicious", + "dinner", + "eat", + "feed", + "food", + "fork", + "hungry", + "knife", + "lunch", + "restaurant", + "yum", + "yummy" + ], + "unicode": "🍴" + }, + { + "group": 4, + "hexcode": "1F944", + "label": "spoon", + "order": 3781, + "tags": ["eat", "tableware"], + "unicode": "🥄" + }, + { + "group": 4, + "hexcode": "1F52A", + "label": "kitchen knife", + "order": 3782, + "tags": [ + "chef", + "cooking", + "hocho", + "kitchen", + "knife", + "tool", + "weapon" + ], + "unicode": "🔪" + }, + { + "group": 4, + "hexcode": "1FAD9", + "label": "jar", + "order": 3783, + "tags": [ + "condiment", + "container", + "empty", + "nothing", + "sauce", + "store" + ], + "unicode": "🫙" + }, + { + "group": 4, + "hexcode": "1F3FA", + "label": "amphora", + "order": 3784, + "tags": [ + "aquarius", + "cooking", + "drink", + "jug", + "tool", + "weapon", + "zodiac" + ], + "unicode": "🏺" + } + ] + }, + { + "name": "Activity", + "emojis": [ + { + "group": 6, + "hexcode": "1F383", + "label": "jack-o-lantern", + "order": 4052, + "tags": ["celebration", "halloween", "jack", "lantern", "pumpkin"], + "unicode": "🎃" + }, + { + "group": 6, + "hexcode": "1F384", + "label": "Christmas tree", + "order": 4053, + "tags": ["celebration", "christmas", "tree"], + "unicode": "🎄" + }, + { + "group": 6, + "hexcode": "1F386", + "label": "fireworks", + "order": 4054, + "tags": ["boom", "celebration", "entertainment", "yolo"], + "unicode": "🎆" + }, + { + "group": 6, + "hexcode": "1F387", + "label": "sparkler", + "order": 4055, + "tags": ["boom", "celebration", "fireworks", "sparkle"], + "unicode": "🎇" + }, + { + "group": 6, + "hexcode": "1F9E8", + "label": "firecracker", + "order": 4056, + "tags": [ + "dynamite", + "explosive", + "fire", + "fireworks", + "light", + "pop", + "popping", + "spark" + ], + "unicode": "🧨" + }, + { + "group": 6, + "hexcode": "2728", + "label": "sparkles", + "order": 4057, + "tags": ["*", "magic", "sparkle", "star"], + "unicode": "✨️" + }, + { + "group": 6, + "hexcode": "1F388", + "label": "balloon", + "order": 4058, + "tags": ["birthday", "celebrate", "celebration"], + "unicode": "🎈" + }, + { + "group": 6, + "hexcode": "1F389", + "label": "party popper", + "order": 4059, + "tags": [ + "awesome", + "birthday", + "celebrate", + "celebration", + "excited", + "hooray", + "party", + "popper", + "tada", + "woohoo" + ], + "unicode": "🎉" + }, + { + "group": 6, + "hexcode": "1F38A", + "label": "confetti ball", + "order": 4060, + "tags": [ + "ball", + "celebrate", + "celebration", + "confetti", + "party", + "woohoo" + ], + "unicode": "🎊" + }, + { + "group": 6, + "hexcode": "1F38B", + "label": "tanabata tree", + "order": 4061, + "tags": ["banner", "celebration", "japanese", "tanabata", "tree"], + "unicode": "🎋" + }, + { + "group": 6, + "hexcode": "1F38D", + "label": "pine decoration", + "order": 4062, + "tags": [ + "bamboo", + "celebration", + "decoration", + "japanese", + "pine", + "plant" + ], + "unicode": "🎍" + }, + { + "group": 6, + "hexcode": "1F38E", + "label": "Japanese dolls", + "order": 4063, + "tags": ["celebration", "doll", "dolls", "festival", "japanese"], + "unicode": "🎎" + }, + { + "group": 6, + "hexcode": "1F38F", + "label": "carp streamer", + "order": 4064, + "tags": ["carp", "celebration", "streamer"], + "unicode": "🎏" + }, + { + "group": 6, + "hexcode": "1F390", + "label": "wind chime", + "order": 4065, + "tags": ["bell", "celebration", "chime", "wind"], + "unicode": "🎐" + }, + { + "group": 6, + "hexcode": "1F391", + "label": "moon viewing ceremony", + "order": 4066, + "tags": ["celebration", "ceremony", "moon", "viewing"], + "unicode": "🎑" + }, + { + "group": 6, + "hexcode": "1F9E7", + "label": "red envelope", + "order": 4067, + "tags": [ + "envelope", + "gift", + "good", + "hóngbāo", + "lai", + "luck", + "money", + "red", + "see" + ], + "unicode": "🧧" + }, + { + "group": 6, + "hexcode": "1F380", + "label": "ribbon", + "order": 4068, + "tags": ["celebration"], + "unicode": "🎀" + }, + { + "group": 6, + "hexcode": "1F381", + "label": "wrapped gift", + "order": 4069, + "tags": [ + "birthday", + "bow", + "box", + "celebration", + "christmas", + "gift", + "present", + "surprise", + "wrapped" + ], + "unicode": "🎁" + }, + { + "group": 6, + "hexcode": "1F397", + "label": "reminder ribbon", + "order": 4071, + "tags": ["celebration", "reminder", "ribbon"], + "unicode": "🎗️" + }, + { + "group": 6, + "hexcode": "1F39F", + "label": "admission tickets", + "order": 4073, + "tags": ["admission", "ticket", "tickets"], + "unicode": "🎟️" + }, + { + "group": 6, + "hexcode": "1F3AB", + "label": "ticket", + "order": 4074, + "tags": ["admission", "stub"], + "unicode": "🎫" + }, + { + "group": 6, + "hexcode": "1F396", + "label": "military medal", + "order": 4076, + "tags": ["award", "celebration", "medal", "military"], + "unicode": "🎖️" + }, + { + "group": 6, + "hexcode": "1F3C6", + "label": "trophy", + "order": 4077, + "tags": [ + "champion", + "champs", + "prize", + "slay", + "sport", + "victory", + "win", + "winning" + ], + "unicode": "🏆️" + }, + { + "group": 6, + "hexcode": "1F3C5", + "label": "sports medal", + "order": 4078, + "tags": ["award", "gold", "medal", "sports", "winner"], + "unicode": "🏅" + }, + { + "group": 6, + "hexcode": "1F947", + "label": "1st place medal", + "order": 4079, + "tags": ["1st", "first", "gold", "medal", "place"], + "unicode": "🥇" + }, + { + "group": 6, + "hexcode": "1F948", + "label": "2nd place medal", + "order": 4080, + "tags": ["2nd", "medal", "place", "second", "silver"], + "unicode": "🥈" + }, + { + "group": 6, + "hexcode": "1F949", + "label": "3rd place medal", + "order": 4081, + "tags": ["3rd", "bronze", "medal", "place", "third"], + "unicode": "🥉" + }, + { + "group": 6, + "hexcode": "26BD", + "label": "soccer ball", + "order": 4082, + "tags": ["ball", "football", "futbol", "soccer", "sport"], + "unicode": "⚽️" + }, + { + "group": 6, + "hexcode": "26BE", + "label": "baseball", + "order": 4083, + "tags": ["ball", "sport"], + "unicode": "⚾️" + }, + { + "group": 6, + "hexcode": "1F94E", + "label": "softball", + "order": 4084, + "tags": ["ball", "glove", "sports", "underarm"], + "unicode": "🥎" + }, + { + "group": 6, + "hexcode": "1F3C0", + "label": "basketball", + "order": 4085, + "tags": ["ball", "hoop", "sport"], + "unicode": "🏀" + }, + { + "group": 6, + "hexcode": "1F3D0", + "label": "volleyball", + "order": 4086, + "tags": ["ball", "game"], + "unicode": "🏐" + }, + { + "group": 6, + "hexcode": "1F3C8", + "label": "american football", + "order": 4087, + "tags": ["american", "ball", "bowl", "football", "sport", "super"], + "unicode": "🏈" + }, + { + "group": 6, + "hexcode": "1F3C9", + "label": "rugby football", + "order": 4088, + "tags": ["ball", "football", "rugby", "sport"], + "unicode": "🏉" + }, + { + "group": 6, + "hexcode": "1F3BE", + "label": "tennis", + "order": 4089, + "tags": ["ball", "racquet", "sport"], + "unicode": "🎾" + }, + { + "group": 6, + "hexcode": "1F94F", + "label": "flying disc", + "order": 4090, + "tags": ["disc", "flying", "ultimate"], + "unicode": "🥏" + }, + { + "group": 6, + "hexcode": "1F3B3", + "label": "bowling", + "order": 4091, + "tags": ["ball", "game", "sport", "strike"], + "unicode": "🎳" + }, + { + "group": 6, + "hexcode": "1F3CF", + "label": "cricket game", + "order": 4092, + "tags": ["ball", "bat", "cricket", "game"], + "unicode": "🏏" + }, + { + "group": 6, + "hexcode": "1F3D1", + "label": "field hockey", + "order": 4093, + "tags": ["ball", "field", "game", "hockey", "stick"], + "unicode": "🏑" + }, + { + "group": 6, + "hexcode": "1F3D2", + "label": "ice hockey", + "order": 4094, + "tags": ["game", "hockey", "ice", "puck", "stick"], + "unicode": "🏒" + }, + { + "group": 6, + "hexcode": "1F94D", + "label": "lacrosse", + "order": 4095, + "tags": ["ball", "goal", "sports", "stick"], + "unicode": "🥍" + }, + { + "group": 6, + "hexcode": "1F3D3", + "label": "ping pong", + "order": 4096, + "tags": [ + "ball", + "bat", + "game", + "paddle", + "ping", + "pingpong", + "pong", + "table", + "tennis" + ], + "unicode": "🏓" + }, + { + "group": 6, + "hexcode": "1F3F8", + "label": "badminton", + "order": 4097, + "tags": ["birdie", "game", "racquet", "shuttlecock"], + "unicode": "🏸" + }, + { + "group": 6, + "hexcode": "1F94A", + "label": "boxing glove", + "order": 4098, + "tags": ["boxing", "glove"], + "unicode": "🥊" + }, + { + "group": 6, + "hexcode": "1F94B", + "label": "martial arts uniform", + "order": 4099, + "tags": ["arts", "judo", "karate", "martial", "taekwondo", "uniform"], + "unicode": "🥋" + }, + { + "group": 6, + "hexcode": "1F945", + "label": "goal net", + "order": 4100, + "tags": ["goal", "net"], + "unicode": "🥅" + }, + { + "group": 6, + "hexcode": "26F3", + "label": "flag in hole", + "order": 4101, + "tags": ["flag", "golf", "hole", "sport"], + "unicode": "⛳️" + }, + { + "group": 6, + "hexcode": "26F8", + "label": "ice skate", + "order": 4103, + "tags": ["ice", "skate", "skating"], + "unicode": "⛸️" + }, + { + "group": 6, + "hexcode": "1F3A3", + "label": "fishing pole", + "order": 4104, + "tags": ["entertainment", "fish", "fishing", "pole", "sport"], + "unicode": "🎣" + }, + { + "group": 6, + "hexcode": "1F93F", + "label": "diving mask", + "order": 4105, + "tags": ["diving", "mask", "scuba", "snorkeling"], + "unicode": "🤿" + }, + { + "group": 6, + "hexcode": "1F3BD", + "label": "running shirt", + "order": 4106, + "tags": ["athletics", "running", "sash", "shirt"], + "unicode": "🎽" + }, + { + "group": 6, + "hexcode": "1F3BF", + "label": "skis", + "order": 4107, + "tags": ["ski", "snow", "sport"], + "unicode": "🎿" + }, + { + "group": 6, + "hexcode": "1F6F7", + "label": "sled", + "order": 4108, + "tags": ["luge", "sledge", "sleigh", "snow", "toboggan"], + "unicode": "🛷" + }, + { + "group": 6, + "hexcode": "1F94C", + "label": "curling stone", + "order": 4109, + "tags": ["curling", "game", "rock", "stone"], + "unicode": "🥌" + }, + { + "group": 6, + "hexcode": "1F3AF", + "label": "bullseye", + "order": 4110, + "tags": [ + "bull", + "dart", + "direct", + "entertainment", + "game", + "hit", + "target" + ], + "unicode": "🎯" + }, + { + "group": 6, + "hexcode": "1FA80", + "label": "yo-yo", + "order": 4111, + "tags": ["fluctuate", "toy"], + "unicode": "🪀" + }, + { + "group": 6, + "hexcode": "1FA81", + "label": "kite", + "order": 4112, + "tags": ["fly", "soar"], + "unicode": "🪁" + }, + { + "group": 6, + "hexcode": "1F52B", + "label": "water pistol", + "order": 4113, + "tags": [ + "gun", + "handgun", + "pistol", + "revolver", + "tool", + "water", + "weapon" + ], + "unicode": "🔫" + }, + { + "group": 6, + "hexcode": "1F3B1", + "label": "pool 8 ball", + "order": 4114, + "tags": ["8", "8ball", "ball", "billiard", "eight", "game", "pool"], + "unicode": "🎱" + }, + { + "group": 6, + "hexcode": "1F52E", + "label": "crystal ball", + "order": 4115, + "tags": [ + "ball", + "crystal", + "fairy", + "fairytale", + "fantasy", + "fortune", + "future", + "magic", + "tale", + "tool" + ], + "unicode": "🔮" + }, + { + "group": 6, + "hexcode": "1FA84", + "label": "magic wand", + "order": 4116, + "tags": ["magic", "magician", "wand", "witch", "wizard"], + "unicode": "🪄" + }, + { + "group": 6, + "hexcode": "1F3AE", + "label": "video game", + "order": 4117, + "tags": ["controller", "entertainment", "game", "video"], + "unicode": "🎮️" + }, + { + "group": 6, + "hexcode": "1F579", + "label": "joystick", + "order": 4119, + "tags": ["game", "video", "videogame"], + "unicode": "🕹️" + }, + { + "group": 6, + "hexcode": "1F3B0", + "label": "slot machine", + "order": 4120, + "tags": [ + "casino", + "gamble", + "gambling", + "game", + "machine", + "slot", + "slots" + ], + "unicode": "🎰" + }, + { + "group": 6, + "hexcode": "1F3B2", + "label": "game die", + "order": 4121, + "tags": ["dice", "die", "entertainment", "game"], + "unicode": "🎲" + }, + { + "group": 6, + "hexcode": "1F9E9", + "label": "puzzle piece", + "order": 4122, + "tags": ["clue", "interlocking", "jigsaw", "piece", "puzzle"], + "unicode": "🧩" + }, + { + "group": 6, + "hexcode": "1F9F8", + "label": "teddy bear", + "order": 4123, + "tags": ["bear", "plaything", "plush", "stuffed", "teddy", "toy"], + "unicode": "🧸" + }, + { + "group": 6, + "hexcode": "1FA85", + "label": "piñata", + "order": 4124, + "tags": [ + "candy", + "celebrate", + "celebration", + "cinco", + "de", + "festive", + "mayo", + "party", + "pinada", + "pinata" + ], + "unicode": "🪅" + }, + { + "group": 6, + "hexcode": "1FAA9", + "label": "mirror ball", + "order": 4125, + "tags": ["ball", "dance", "disco", "glitter", "mirror", "party"], + "unicode": "🪩" + }, + { + "group": 6, + "hexcode": "1FA86", + "label": "nesting dolls", + "order": 4126, + "tags": [ + "babooshka", + "baboushka", + "babushka", + "doll", + "dolls", + "matryoshka", + "nesting", + "russia" + ], + "unicode": "🪆" + }, + { + "group": 6, + "hexcode": "2660", + "label": "spade suit", + "order": 4128, + "tags": ["card", "game", "spade", "suit"], + "unicode": "♠️" + }, + { + "group": 6, + "hexcode": "2665", + "label": "heart suit", + "order": 4130, + "tags": ["card", "emotion", "game", "heart", "hearts", "suit"], + "unicode": "♥️" + }, + { + "group": 6, + "hexcode": "2666", + "label": "diamond suit", + "order": 4132, + "tags": ["card", "diamond", "game", "suit"], + "unicode": "♦️" + }, + { + "group": 6, + "hexcode": "2663", + "label": "club suit", + "order": 4134, + "tags": ["card", "club", "clubs", "game", "suit"], + "unicode": "♣️" + }, + { + "group": 6, + "hexcode": "265F", + "label": "chess pawn", + "order": 4136, + "tags": ["chess", "dupe", "expendable", "pawn"], + "unicode": "♟️" + }, + { + "group": 6, + "hexcode": "1F0CF", + "label": "joker", + "order": 4137, + "tags": ["card", "game", "wildcard"], + "unicode": "🃏" + }, + { + "group": 6, + "hexcode": "1F004", + "label": "mahjong red dragon", + "order": 4138, + "tags": ["dragon", "game", "mahjong", "red"], + "unicode": "🀄️" + }, + { + "group": 6, + "hexcode": "1F3B4", + "label": "flower playing cards", + "order": 4139, + "tags": ["card", "cards", "flower", "game", "japanese", "playing"], + "unicode": "🎴" + }, + { + "group": 6, + "hexcode": "1F3AD", + "label": "performing arts", + "order": 4140, + "tags": [ + "actor", + "actress", + "art", + "arts", + "entertainment", + "mask", + "performing", + "theater", + "theatre", + "thespian" + ], + "unicode": "🎭️" + }, + { + "group": 6, + "hexcode": "1F5BC", + "label": "framed picture", + "order": 4142, + "tags": ["art", "frame", "framed", "museum", "painting", "picture"], + "unicode": "🖼️" + }, + { + "group": 6, + "hexcode": "1F3A8", + "label": "artist palette", + "order": 4143, + "tags": [ + "art", + "artist", + "artsy", + "arty", + "colorful", + "creative", + "entertainment", + "museum", + "painter", + "painting", + "palette" + ], + "unicode": "🎨" + }, + { + "group": 6, + "hexcode": "1F9F5", + "label": "thread", + "order": 4144, + "tags": ["needle", "sewing", "spool", "string"], + "unicode": "🧵" + }, + { + "group": 6, + "hexcode": "1FAA1", + "label": "sewing needle", + "order": 4145, + "tags": [ + "embroidery", + "needle", + "sew", + "sewing", + "stitches", + "sutures", + "tailoring", + "thread" + ], + "unicode": "🪡" + }, + { + "group": 6, + "hexcode": "1F9F6", + "label": "yarn", + "order": 4146, + "tags": ["ball", "crochet", "knit"], + "unicode": "🧶" + }, + { + "group": 6, + "hexcode": "1FAA2", + "label": "knot", + "order": 4147, + "tags": ["cord", "rope", "tangled", "tie", "twine", "twist"], + "unicode": "🪢" + } + ] + }, + { + "name": "Travel & Places", + "emojis": [ + { + "group": 5, + "hexcode": "1F30D", + "label": "globe showing Europe-Africa", + "order": 3785, + "tags": [ + "africa", + "earth", + "europe", + "europe-africa", + "globe", + "showing", + "world" + ], + "unicode": "🌍️" + }, + { + "group": 5, + "hexcode": "1F30E", + "label": "globe showing Americas", + "order": 3786, + "tags": ["americas", "earth", "globe", "showing", "world"], + "unicode": "🌎️" + }, + { + "group": 5, + "hexcode": "1F30F", + "label": "globe showing Asia-Australia", + "order": 3787, + "tags": [ + "asia", + "asia-australia", + "australia", + "earth", + "globe", + "showing", + "world" + ], + "unicode": "🌏️" + }, + { + "group": 5, + "hexcode": "1F310", + "label": "globe with meridians", + "order": 3788, + "tags": [ + "earth", + "globe", + "internet", + "meridians", + "web", + "world", + "worldwide" + ], + "unicode": "🌐" + }, + { + "group": 5, + "hexcode": "1F5FA", + "label": "world map", + "order": 3790, + "tags": ["map", "world"], + "unicode": "🗺️" + }, + { + "group": 5, + "hexcode": "1F5FE", + "label": "map of Japan", + "order": 3791, + "tags": ["japan", "map"], + "unicode": "🗾" + }, + { + "group": 5, + "hexcode": "1F9ED", + "label": "compass", + "order": 3792, + "tags": ["direction", "magnetic", "navigation", "orienteering"], + "unicode": "🧭" + }, + { + "group": 5, + "hexcode": "1F3D4", + "label": "snow-capped mountain", + "order": 3794, + "tags": ["cold", "mountain", "snow", "snow-capped"], + "unicode": "🏔️" + }, + { + "group": 5, + "hexcode": "26F0", + "label": "mountain", + "order": 3796, + "tags": ["mountain"], + "unicode": "⛰️" + }, + { + "group": 5, + "hexcode": "1F30B", + "label": "volcano", + "order": 3797, + "tags": ["eruption", "mountain", "nature"], + "unicode": "🌋" + }, + { + "group": 5, + "hexcode": "1F5FB", + "label": "mount fuji", + "order": 3798, + "tags": ["fuji", "mount", "mountain", "nature"], + "unicode": "🗻" + }, + { + "group": 5, + "hexcode": "1F3D5", + "label": "camping", + "order": 3800, + "tags": ["camping"], + "unicode": "🏕️" + }, + { + "group": 5, + "hexcode": "1F3D6", + "label": "beach with umbrella", + "order": 3802, + "tags": ["beach", "umbrella"], + "unicode": "🏖️" + }, + { + "group": 5, + "hexcode": "1F3DC", + "label": "desert", + "order": 3804, + "tags": ["desert"], + "unicode": "🏜️" + }, + { + "group": 5, + "hexcode": "1F3DD", + "label": "desert island", + "order": 3806, + "tags": ["desert", "island"], + "unicode": "🏝️" + }, + { + "group": 5, + "hexcode": "1F3DE", + "label": "national park", + "order": 3808, + "tags": ["national", "park"], + "unicode": "🏞️" + }, + { + "group": 5, + "hexcode": "1F3DF", + "label": "stadium", + "order": 3810, + "tags": ["stadium"], + "unicode": "🏟️" + }, + { + "group": 5, + "hexcode": "1F3DB", + "label": "classical building", + "order": 3812, + "tags": ["building", "classical"], + "unicode": "🏛️" + }, + { + "group": 5, + "hexcode": "1F3D7", + "label": "building construction", + "order": 3814, + "tags": ["building", "construction", "crane"], + "unicode": "🏗️" + }, + { + "group": 5, + "hexcode": "1F9F1", + "label": "brick", + "order": 3815, + "tags": ["bricks", "clay", "mortar", "wall"], + "unicode": "🧱" + }, + { + "group": 5, + "hexcode": "1FAA8", + "label": "rock", + "order": 3816, + "tags": ["boulder", "heavy", "solid", "stone", "tough"], + "unicode": "🪨" + }, + { + "group": 5, + "hexcode": "1FAB5", + "label": "wood", + "order": 3817, + "tags": ["log", "lumber", "timber"], + "unicode": "🪵" + }, + { + "group": 5, + "hexcode": "1F6D6", + "label": "hut", + "order": 3818, + "tags": ["home", "house", "roundhouse", "shelter", "yurt"], + "unicode": "🛖" + }, + { + "group": 5, + "hexcode": "1F3D8", + "label": "houses", + "order": 3820, + "tags": ["house"], + "unicode": "🏘️" + }, + { + "group": 5, + "hexcode": "1F3DA", + "label": "derelict house", + "order": 3822, + "tags": ["derelict", "home", "house"], + "unicode": "🏚️" + }, + { + "group": 5, + "hexcode": "1F3E0", + "label": "house", + "order": 3823, + "tags": [ + "building", + "country", + "heart", + "home", + "ranch", + "settle", + "simple", + "suburban", + "suburbia", + "where" + ], + "unicode": "🏠️" + }, + { + "group": 5, + "hexcode": "1F3E1", + "label": "house with garden", + "order": 3824, + "tags": [ + "building", + "country", + "garden", + "heart", + "home", + "house", + "ranch", + "settle", + "simple", + "suburban", + "suburbia", + "where" + ], + "unicode": "🏡" + }, + { + "group": 5, + "hexcode": "1F3E2", + "label": "office building", + "order": 3825, + "tags": ["building", "city", "cubical", "job", "office"], + "unicode": "🏢" + }, + { + "group": 5, + "hexcode": "1F3E3", + "label": "Japanese post office", + "order": 3826, + "tags": ["building", "japanese", "office", "post"], + "unicode": "🏣" + }, + { + "group": 5, + "hexcode": "1F3E4", + "label": "post office", + "order": 3827, + "tags": ["building", "european", "office", "post"], + "unicode": "🏤" + }, + { + "group": 5, + "hexcode": "1F3E5", + "label": "hospital", + "order": 3828, + "tags": ["building", "doctor", "medicine"], + "unicode": "🏥" + }, + { + "group": 5, + "hexcode": "1F3E6", + "label": "bank", + "order": 3829, + "tags": ["building"], + "unicode": "🏦" + }, + { + "group": 5, + "hexcode": "1F3E8", + "label": "hotel", + "order": 3830, + "tags": ["building"], + "unicode": "🏨" + }, + { + "group": 5, + "hexcode": "1F3E9", + "label": "love hotel", + "order": 3831, + "tags": ["building", "hotel", "love"], + "unicode": "🏩" + }, + { + "group": 5, + "hexcode": "1F3EA", + "label": "convenience store", + "order": 3832, + "tags": ["24", "building", "convenience", "hours", "store"], + "unicode": "🏪" + }, + { + "group": 5, + "hexcode": "1F3EB", + "label": "school", + "order": 3833, + "tags": ["building"], + "unicode": "🏫" + }, + { + "group": 5, + "hexcode": "1F3EC", + "label": "department store", + "order": 3834, + "tags": ["building", "department", "store"], + "unicode": "🏬" + }, + { + "group": 5, + "hexcode": "1F3ED", + "label": "factory", + "order": 3835, + "tags": ["building"], + "unicode": "🏭️" + }, + { + "group": 5, + "hexcode": "1F3EF", + "label": "Japanese castle", + "order": 3836, + "tags": ["building", "castle", "japanese"], + "unicode": "🏯" + }, + { + "group": 5, + "hexcode": "1F3F0", + "label": "castle", + "order": 3837, + "tags": ["building", "european"], + "unicode": "🏰" + }, + { + "group": 5, + "hexcode": "1F492", + "label": "wedding", + "order": 3838, + "tags": ["chapel", "hitched", "nuptials", "romance"], + "unicode": "💒" + }, + { + "group": 5, + "hexcode": "1F5FC", + "label": "Tokyo tower", + "order": 3839, + "tags": ["tokyo", "tower"], + "unicode": "🗼" + }, + { + "group": 5, + "hexcode": "1F5FD", + "label": "Statue of Liberty", + "order": 3840, + "tags": ["liberty", "new", "ny", "nyc", "statue", "york"], + "unicode": "🗽" + }, + { + "group": 5, + "hexcode": "26EA", + "label": "church", + "order": 3841, + "tags": ["bless", "chapel", "christian", "cross", "religion"], + "unicode": "⛪️" + }, + { + "group": 5, + "hexcode": "1F54C", + "label": "mosque", + "order": 3842, + "tags": ["islam", "masjid", "muslim", "religion"], + "unicode": "🕌" + }, + { + "group": 5, + "hexcode": "1F6D5", + "label": "hindu temple", + "order": 3843, + "tags": ["hindu", "temple"], + "unicode": "🛕" + }, + { + "group": 5, + "hexcode": "1F54D", + "label": "synagogue", + "order": 3844, + "tags": ["jew", "jewish", "judaism", "religion", "temple"], + "unicode": "🕍" + }, + { + "group": 5, + "hexcode": "26E9", + "label": "shinto shrine", + "order": 3846, + "tags": ["religion", "shinto", "shrine"], + "unicode": "⛩️" + }, + { + "group": 5, + "hexcode": "1F54B", + "label": "kaaba", + "order": 3847, + "tags": ["hajj", "islam", "muslim", "religion", "umrah"], + "unicode": "🕋" + }, + { + "group": 5, + "hexcode": "26F2", + "label": "fountain", + "order": 3848, + "tags": ["fountain"], + "unicode": "⛲️" + }, + { + "group": 5, + "hexcode": "26FA", + "label": "tent", + "order": 3849, + "tags": ["camping"], + "unicode": "⛺️" + }, + { + "group": 5, + "hexcode": "1F301", + "label": "foggy", + "order": 3850, + "tags": ["fog"], + "unicode": "🌁" + }, + { + "group": 5, + "hexcode": "1F303", + "label": "night with stars", + "order": 3851, + "tags": ["night", "star", "stars"], + "unicode": "🌃" + }, + { + "group": 5, + "hexcode": "1F3D9", + "label": "cityscape", + "order": 3853, + "tags": ["city"], + "unicode": "🏙️" + }, + { + "group": 5, + "hexcode": "1F304", + "label": "sunrise over mountains", + "order": 3854, + "tags": ["morning", "mountains", "over", "sun", "sunrise"], + "unicode": "🌄" + }, + { + "group": 5, + "hexcode": "1F305", + "label": "sunrise", + "order": 3855, + "tags": ["morning", "nature", "sun"], + "unicode": "🌅" + }, + { + "group": 5, + "hexcode": "1F306", + "label": "cityscape at dusk", + "order": 3856, + "tags": [ + "at", + "building", + "city", + "cityscape", + "dusk", + "evening", + "landscape", + "sun", + "sunset" + ], + "unicode": "🌆" + }, + { + "group": 5, + "hexcode": "1F307", + "label": "sunset", + "order": 3857, + "tags": ["building", "dusk", "sun"], + "unicode": "🌇" + }, + { + "group": 5, + "hexcode": "1F309", + "label": "bridge at night", + "order": 3858, + "tags": ["at", "bridge", "night"], + "unicode": "🌉" + }, + { + "group": 5, + "hexcode": "2668", + "label": "hot springs", + "order": 3860, + "tags": ["hot", "hotsprings", "springs", "steaming"], + "unicode": "♨️" + }, + { + "group": 5, + "hexcode": "1F3A0", + "label": "carousel horse", + "order": 3861, + "tags": ["carousel", "entertainment", "horse"], + "unicode": "🎠" + }, + { + "group": 5, + "hexcode": "1F6DD", + "label": "playground slide", + "order": 3862, + "tags": [ + "amusement", + "park", + "play", + "playground", + "playing", + "slide", + "sliding", + "theme" + ], + "unicode": "🛝" + }, + { + "group": 5, + "hexcode": "1F3A1", + "label": "ferris wheel", + "order": 3863, + "tags": ["amusement", "ferris", "park", "theme", "wheel"], + "unicode": "🎡" + }, + { + "group": 5, + "hexcode": "1F3A2", + "label": "roller coaster", + "order": 3864, + "tags": ["amusement", "coaster", "park", "roller", "theme"], + "unicode": "🎢" + }, + { + "group": 5, + "hexcode": "1F488", + "label": "barber pole", + "order": 3865, + "tags": ["barber", "cut", "fresh", "haircut", "pole", "shave"], + "unicode": "💈" + }, + { + "group": 5, + "hexcode": "1F3AA", + "label": "circus tent", + "order": 3866, + "tags": ["circus", "tent"], + "unicode": "🎪" + }, + { + "group": 5, + "hexcode": "1F682", + "label": "locomotive", + "order": 3867, + "tags": [ + "caboose", + "engine", + "railway", + "steam", + "train", + "trains", + "travel" + ], + "unicode": "🚂" + }, + { + "group": 5, + "hexcode": "1F683", + "label": "railway car", + "order": 3868, + "tags": [ + "car", + "electric", + "railway", + "train", + "tram", + "travel", + "trolleybus" + ], + "unicode": "🚃" + }, + { + "group": 5, + "hexcode": "1F684", + "label": "high-speed train", + "order": 3869, + "tags": ["high-speed", "railway", "shinkansen", "speed", "train"], + "unicode": "🚄" + }, + { + "group": 5, + "hexcode": "1F685", + "label": "bullet train", + "order": 3870, + "tags": [ + "bullet", + "high-speed", + "nose", + "railway", + "shinkansen", + "speed", + "train", + "travel" + ], + "unicode": "🚅" + }, + { + "group": 5, + "hexcode": "1F686", + "label": "train", + "order": 3871, + "tags": ["arrived", "choo", "railway"], + "unicode": "🚆" + }, + { + "group": 5, + "hexcode": "1F687", + "label": "metro", + "order": 3872, + "tags": ["subway", "travel"], + "unicode": "🚇️" + }, + { + "group": 5, + "hexcode": "1F688", + "label": "light rail", + "order": 3873, + "tags": ["arrived", "light", "monorail", "rail", "railway"], + "unicode": "🚈" + }, + { + "group": 5, + "hexcode": "1F689", + "label": "station", + "order": 3874, + "tags": ["railway", "train"], + "unicode": "🚉" + }, + { + "group": 5, + "hexcode": "1F68A", + "label": "tram", + "order": 3875, + "tags": ["trolleybus"], + "unicode": "🚊" + }, + { + "group": 5, + "hexcode": "1F69D", + "label": "monorail", + "order": 3876, + "tags": ["vehicle"], + "unicode": "🚝" + }, + { + "group": 5, + "hexcode": "1F69E", + "label": "mountain railway", + "order": 3877, + "tags": ["car", "mountain", "railway", "trip"], + "unicode": "🚞" + }, + { + "group": 5, + "hexcode": "1F68B", + "label": "tram car", + "order": 3878, + "tags": ["bus", "car", "tram", "trolley", "trolleybus"], + "unicode": "🚋" + }, + { + "group": 5, + "hexcode": "1F68C", + "label": "bus", + "order": 3879, + "tags": ["school", "vehicle"], + "unicode": "🚌" + }, + { + "group": 5, + "hexcode": "1F68D", + "label": "oncoming bus", + "order": 3880, + "tags": ["bus", "cars", "oncoming"], + "unicode": "🚍️" + }, + { + "group": 5, + "hexcode": "1F68E", + "label": "trolleybus", + "order": 3881, + "tags": ["bus", "tram", "trolley"], + "unicode": "🚎" + }, + { + "group": 5, + "hexcode": "1F690", + "label": "minibus", + "order": 3882, + "tags": ["bus", "drive", "van", "vehicle"], + "unicode": "🚐" + }, + { + "group": 5, + "hexcode": "1F691", + "label": "ambulance", + "order": 3883, + "tags": ["emergency", "vehicle"], + "unicode": "🚑️" + }, + { + "group": 5, + "hexcode": "1F692", + "label": "fire engine", + "order": 3884, + "tags": ["engine", "fire", "truck"], + "unicode": "🚒" + }, + { + "group": 5, + "hexcode": "1F693", + "label": "police car", + "order": 3885, + "tags": ["5–0", "car", "cops", "patrol", "police"], + "unicode": "🚓" + }, + { + "group": 5, + "hexcode": "1F694", + "label": "oncoming police car", + "order": 3886, + "tags": ["car", "oncoming", "police"], + "unicode": "🚔️" + }, + { + "group": 5, + "hexcode": "1F695", + "label": "taxi", + "order": 3887, + "tags": ["cab", "cabbie", "car", "drive", "vehicle", "yellow"], + "unicode": "🚕" + }, + { + "group": 5, + "hexcode": "1F696", + "label": "oncoming taxi", + "order": 3888, + "tags": [ + "cab", + "cabbie", + "cars", + "drove", + "hail", + "oncoming", + "taxi", + "yellow" + ], + "unicode": "🚖" + }, + { + "group": 5, + "hexcode": "1F697", + "label": "automobile", + "order": 3889, + "tags": ["car", "driving", "vehicle"], + "unicode": "🚗" + }, + { + "group": 5, + "hexcode": "1F698", + "label": "oncoming automobile", + "order": 3890, + "tags": ["automobile", "car", "cars", "drove", "oncoming", "vehicle"], + "unicode": "🚘️" + }, + { + "group": 5, + "hexcode": "1F699", + "label": "sport utility vehicle", + "order": 3891, + "tags": [ + "car", + "drive", + "recreational", + "sport", + "sportutility", + "utility", + "vehicle" + ], + "unicode": "🚙" + }, + { + "group": 5, + "hexcode": "1F6FB", + "label": "pickup truck", + "order": 3892, + "tags": [ + "automobile", + "car", + "flatbed", + "pick-up", + "pickup", + "transportation", + "truck" + ], + "unicode": "🛻" + }, + { + "group": 5, + "hexcode": "1F69A", + "label": "delivery truck", + "order": 3893, + "tags": ["car", "delivery", "drive", "truck", "vehicle"], + "unicode": "🚚" + }, + { + "group": 5, + "hexcode": "1F69B", + "label": "articulated lorry", + "order": 3894, + "tags": [ + "articulated", + "car", + "drive", + "lorry", + "move", + "semi", + "truck", + "vehicle" + ], + "unicode": "🚛" + }, + { + "group": 5, + "hexcode": "1F69C", + "label": "tractor", + "order": 3895, + "tags": ["vehicle"], + "unicode": "🚜" + }, + { + "group": 5, + "hexcode": "1F3CE", + "label": "racing car", + "order": 3897, + "tags": ["car", "racing", "zoom"], + "unicode": "🏎️" + }, + { + "group": 5, + "hexcode": "1F3CD", + "label": "motorcycle", + "order": 3899, + "tags": ["racing"], + "unicode": "🏍️" + }, + { + "group": 5, + "hexcode": "1F6F5", + "label": "motor scooter", + "order": 3900, + "tags": ["motor", "scooter"], + "unicode": "🛵" + }, + { + "group": 5, + "hexcode": "1F9BD", + "label": "manual wheelchair", + "order": 3901, + "tags": ["accessibility", "manual", "wheelchair"], + "unicode": "🦽" + }, + { + "group": 5, + "hexcode": "1F9BC", + "label": "motorized wheelchair", + "order": 3902, + "tags": ["accessibility", "motorized", "wheelchair"], + "unicode": "🦼" + }, + { + "group": 5, + "hexcode": "1F6FA", + "label": "auto rickshaw", + "order": 3903, + "tags": ["auto", "rickshaw", "tuk"], + "unicode": "🛺" + }, + { + "group": 5, + "hexcode": "1F6B2", + "label": "bicycle", + "order": 3904, + "tags": [ + "bike", + "class", + "cycle", + "cycling", + "cyclist", + "gang", + "ride", + "spin", + "spinning" + ], + "unicode": "🚲️" + }, + { + "group": 5, + "hexcode": "1F6F4", + "label": "kick scooter", + "order": 3905, + "tags": ["kick", "scooter"], + "unicode": "🛴" + }, + { + "group": 5, + "hexcode": "1F6F9", + "label": "skateboard", + "order": 3906, + "tags": ["board", "skate", "skater", "wheels"], + "unicode": "🛹" + }, + { + "group": 5, + "hexcode": "1F6FC", + "label": "roller skate", + "order": 3907, + "tags": ["blades", "roller", "skate", "skates", "sport"], + "unicode": "🛼" + }, + { + "group": 5, + "hexcode": "1F68F", + "label": "bus stop", + "order": 3908, + "tags": ["bus", "busstop", "stop"], + "unicode": "🚏" + }, + { + "group": 5, + "hexcode": "1F6E3", + "label": "motorway", + "order": 3910, + "tags": ["highway", "road"], + "unicode": "🛣️" + }, + { + "group": 5, + "hexcode": "1F6E4", + "label": "railway track", + "order": 3912, + "tags": ["railway", "track", "train"], + "unicode": "🛤️" + }, + { + "group": 5, + "hexcode": "1F6E2", + "label": "oil drum", + "order": 3914, + "tags": ["drum", "oil"], + "unicode": "🛢️" + }, + { + "group": 5, + "hexcode": "26FD", + "label": "fuel pump", + "order": 3915, + "tags": [ + "diesel", + "fuel", + "fuelpump", + "gas", + "gasoline", + "pump", + "station" + ], + "unicode": "⛽️" + }, + { + "group": 5, + "hexcode": "1F6DE", + "label": "wheel", + "order": 3916, + "tags": ["car", "circle", "tire", "turn", "vehicle"], + "unicode": "🛞" + }, + { + "group": 5, + "hexcode": "1F6A8", + "label": "police car light", + "order": 3917, + "tags": [ + "alarm", + "alert", + "beacon", + "car", + "emergency", + "light", + "police", + "revolving", + "siren" + ], + "unicode": "🚨" + }, + { + "group": 5, + "hexcode": "1F6A5", + "label": "horizontal traffic light", + "order": 3918, + "tags": [ + "horizontal", + "intersection", + "light", + "signal", + "stop", + "stoplight", + "traffic" + ], + "unicode": "🚥" + }, + { + "group": 5, + "hexcode": "1F6A6", + "label": "vertical traffic light", + "order": 3919, + "tags": [ + "drove", + "intersection", + "light", + "signal", + "stop", + "stoplight", + "traffic", + "vertical" + ], + "unicode": "🚦" + }, + { + "group": 5, + "hexcode": "1F6D1", + "label": "stop sign", + "order": 3920, + "tags": ["octagonal", "sign", "stop"], + "unicode": "🛑" + }, + { + "group": 5, + "hexcode": "1F6A7", + "label": "construction", + "order": 3921, + "tags": ["barrier"], + "unicode": "🚧" + }, + { + "group": 5, + "hexcode": "2693", + "label": "anchor", + "order": 3922, + "tags": ["ship", "tool"], + "unicode": "⚓️" + }, + { + "group": 5, + "hexcode": "1F6DF", + "label": "ring buoy", + "order": 3923, + "tags": [ + "buoy", + "float", + "life", + "lifesaver", + "preserver", + "rescue", + "ring", + "safety", + "save", + "saver", + "swim" + ], + "unicode": "🛟" + }, + { + "group": 5, + "hexcode": "26F5", + "label": "sailboat", + "order": 3924, + "tags": ["boat", "resort", "sailing", "sea", "yacht"], + "unicode": "⛵️" + }, + { + "group": 5, + "hexcode": "1F6F6", + "label": "canoe", + "order": 3925, + "tags": ["boat"], + "unicode": "🛶" + }, + { + "group": 5, + "hexcode": "1F6A4", + "label": "speedboat", + "order": 3926, + "tags": [ + "billionaire", + "boat", + "lake", + "luxury", + "millionaire", + "summer", + "travel" + ], + "unicode": "🚤" + }, + { + "group": 5, + "hexcode": "1F6F3", + "label": "passenger ship", + "order": 3928, + "tags": ["passenger", "ship"], + "unicode": "🛳️" + }, + { + "group": 5, + "hexcode": "26F4", + "label": "ferry", + "order": 3930, + "tags": ["boat", "passenger"], + "unicode": "⛴️" + }, + { + "group": 5, + "hexcode": "1F6E5", + "label": "motor boat", + "order": 3932, + "tags": ["boat", "motor", "motorboat"], + "unicode": "🛥️" + }, + { + "group": 5, + "hexcode": "1F6A2", + "label": "ship", + "order": 3933, + "tags": ["boat", "passenger", "travel"], + "unicode": "🚢" + }, + { + "group": 5, + "hexcode": "2708", + "label": "airplane", + "order": 3935, + "tags": ["aeroplane", "fly", "flying", "jet", "plane", "travel"], + "unicode": "✈️" + }, + { + "group": 5, + "hexcode": "1F6E9", + "label": "small airplane", + "order": 3937, + "tags": ["aeroplane", "airplane", "plane", "small"], + "unicode": "🛩️" + }, + { + "group": 5, + "hexcode": "1F6EB", + "label": "airplane departure", + "order": 3938, + "tags": [ + "aeroplane", + "airplane", + "check-in", + "departure", + "departures", + "plane" + ], + "unicode": "🛫" + }, + { + "group": 5, + "hexcode": "1F6EC", + "label": "airplane arrival", + "order": 3939, + "tags": [ + "aeroplane", + "airplane", + "arrival", + "arrivals", + "arriving", + "landing", + "plane" + ], + "unicode": "🛬" + }, + { + "group": 5, + "hexcode": "1FA82", + "label": "parachute", + "order": 3940, + "tags": ["hang-glide", "parasail", "skydive"], + "unicode": "🪂" + }, + { + "group": 5, + "hexcode": "1F4BA", + "label": "seat", + "order": 3941, + "tags": ["chair"], + "unicode": "💺" + }, + { + "group": 5, + "hexcode": "1F681", + "label": "helicopter", + "order": 3942, + "tags": ["copter", "roflcopter", "travel", "vehicle"], + "unicode": "🚁" + }, + { + "group": 5, + "hexcode": "1F69F", + "label": "suspension railway", + "order": 3943, + "tags": ["railway", "suspension"], + "unicode": "🚟" + }, + { + "group": 5, + "hexcode": "1F6A0", + "label": "mountain cableway", + "order": 3944, + "tags": ["cable", "cableway", "gondola", "lift", "mountain", "ski"], + "unicode": "🚠" + }, + { + "group": 5, + "hexcode": "1F6A1", + "label": "aerial tramway", + "order": 3945, + "tags": ["aerial", "cable", "car", "gondola", "ropeway", "tramway"], + "unicode": "🚡" + }, + { + "group": 5, + "hexcode": "1F6F0", + "label": "satellite", + "order": 3947, + "tags": ["space"], + "unicode": "🛰️" + }, + { + "group": 5, + "hexcode": "1F680", + "label": "rocket", + "order": 3948, + "tags": ["launch", "rockets", "space", "travel"], + "unicode": "🚀" + }, + { + "group": 5, + "hexcode": "1F6F8", + "label": "flying saucer", + "order": 3949, + "tags": ["aliens", "extra", "flying", "saucer", "terrestrial", "ufo"], + "unicode": "🛸" + }, + { + "group": 5, + "hexcode": "1F6CE", + "label": "bellhop bell", + "order": 3951, + "tags": ["bell", "bellhop", "hotel"], + "unicode": "🛎️" + }, + { + "group": 5, + "hexcode": "1F9F3", + "label": "luggage", + "order": 3952, + "tags": ["bag", "packing", "roller", "suitcase", "travel"], + "unicode": "🧳" + }, + { + "group": 5, + "hexcode": "231B", + "label": "hourglass done", + "order": 3953, + "tags": ["done", "hourglass", "sand", "time", "timer"], + "unicode": "⌛️" + }, + { + "group": 5, + "hexcode": "23F3", + "label": "hourglass not done", + "order": 3954, + "tags": [ + "done", + "flowing", + "hourglass", + "hours", + "not", + "sand", + "timer", + "waiting", + "yolo" + ], + "unicode": "⏳️" + }, + { + "group": 5, + "hexcode": "231A", + "label": "watch", + "order": 3955, + "tags": ["clock", "time"], + "unicode": "⌚️" + }, + { + "group": 5, + "hexcode": "23F0", + "label": "alarm clock", + "order": 3956, + "tags": ["alarm", "clock", "hours", "hrs", "late", "time", "waiting"], + "unicode": "⏰️" + }, + { + "group": 5, + "hexcode": "23F1", + "label": "stopwatch", + "order": 3958, + "tags": ["clock", "time"], + "unicode": "⏱️" + }, + { + "group": 5, + "hexcode": "23F2", + "label": "timer clock", + "order": 3960, + "tags": ["clock", "timer"], + "unicode": "⏲️" + }, + { + "group": 5, + "hexcode": "1F570", + "label": "mantelpiece clock", + "order": 3962, + "tags": ["clock", "mantelpiece", "time"], + "unicode": "🕰️" + }, + { + "group": 5, + "hexcode": "1F55B", + "label": "twelve o’clock", + "order": 3963, + "tags": ["12", "12:00", "clock", "o’clock", "time", "twelve"], + "unicode": "🕛️" + }, + { + "group": 5, + "hexcode": "1F567", + "label": "twelve-thirty", + "order": 3964, + "tags": ["12", "12:30", "30", "clock", "thirty", "time", "twelve"], + "unicode": "🕧️" + }, + { + "group": 5, + "hexcode": "1F550", + "label": "one o’clock", + "order": 3965, + "tags": ["1", "1:00", "clock", "one", "o’clock", "time"], + "unicode": "🕐️" + }, + { + "group": 5, + "hexcode": "1F55C", + "label": "one-thirty", + "order": 3966, + "tags": ["1", "1:30", "30", "clock", "one", "thirty", "time"], + "unicode": "🕜️" + }, + { + "group": 5, + "hexcode": "1F551", + "label": "two o’clock", + "order": 3967, + "tags": ["2", "2:00", "clock", "o’clock", "time", "two"], + "unicode": "🕑️" + }, + { + "group": 5, + "hexcode": "1F55D", + "label": "two-thirty", + "order": 3968, + "tags": ["2", "2:30", "30", "clock", "thirty", "time", "two"], + "unicode": "🕝️" + }, + { + "group": 5, + "hexcode": "1F552", + "label": "three o’clock", + "order": 3969, + "tags": ["3", "3:00", "clock", "o’clock", "three", "time"], + "unicode": "🕒️" + }, + { + "group": 5, + "hexcode": "1F55E", + "label": "three-thirty", + "order": 3970, + "tags": ["3", "30", "3:30", "clock", "thirty", "three", "time"], + "unicode": "🕞️" + }, + { + "group": 5, + "hexcode": "1F553", + "label": "four o’clock", + "order": 3971, + "tags": ["4", "4:00", "clock", "four", "o’clock", "time"], + "unicode": "🕓️" + }, + { + "group": 5, + "hexcode": "1F55F", + "label": "four-thirty", + "order": 3972, + "tags": ["30", "4", "4:30", "clock", "four", "thirty", "time"], + "unicode": "🕟️" + }, + { + "group": 5, + "hexcode": "1F554", + "label": "five o’clock", + "order": 3973, + "tags": ["5", "5:00", "clock", "five", "o’clock", "time"], + "unicode": "🕔️" + }, + { + "group": 5, + "hexcode": "1F560", + "label": "five-thirty", + "order": 3974, + "tags": ["30", "5", "5:30", "clock", "five", "thirty", "time"], + "unicode": "🕠️" + }, + { + "group": 5, + "hexcode": "1F555", + "label": "six o’clock", + "order": 3975, + "tags": ["6", "6:00", "clock", "o’clock", "six", "time"], + "unicode": "🕕️" + }, + { + "group": 5, + "hexcode": "1F561", + "label": "six-thirty", + "order": 3976, + "tags": ["30", "6", "6:30", "clock", "six", "thirty"], + "unicode": "🕡️" + }, + { + "group": 5, + "hexcode": "1F556", + "label": "seven o’clock", + "order": 3977, + "tags": ["0", "7", "7:00", "clock", "o’clock", "seven"], + "unicode": "🕖️" + }, + { + "group": 5, + "hexcode": "1F562", + "label": "seven-thirty", + "order": 3978, + "tags": ["30", "7", "7:30", "clock", "seven", "thirty"], + "unicode": "🕢️" + }, + { + "group": 5, + "hexcode": "1F557", + "label": "eight o’clock", + "order": 3979, + "tags": ["8", "8:00", "clock", "eight", "o’clock", "time"], + "unicode": "🕗️" + }, + { + "group": 5, + "hexcode": "1F563", + "label": "eight-thirty", + "order": 3980, + "tags": ["30", "8", "8:30", "clock", "eight", "thirty", "time"], + "unicode": "🕣️" + }, + { + "group": 5, + "hexcode": "1F558", + "label": "nine o’clock", + "order": 3981, + "tags": ["9", "9:00", "clock", "nine", "o’clock", "time"], + "unicode": "🕘️" + }, + { + "group": 5, + "hexcode": "1F564", + "label": "nine-thirty", + "order": 3982, + "tags": ["30", "9", "9:30", "clock", "nine", "thirty", "time"], + "unicode": "🕤️" + }, + { + "group": 5, + "hexcode": "1F559", + "label": "ten o’clock", + "order": 3983, + "tags": ["0", "10", "10:00", "clock", "o’clock", "ten"], + "unicode": "🕙️" + }, + { + "group": 5, + "hexcode": "1F565", + "label": "ten-thirty", + "order": 3984, + "tags": ["10", "10:30", "30", "clock", "ten", "thirty", "time"], + "unicode": "🕥️" + }, + { + "group": 5, + "hexcode": "1F55A", + "label": "eleven o’clock", + "order": 3985, + "tags": ["11", "11:00", "clock", "eleven", "o’clock", "time"], + "unicode": "🕚️" + }, + { + "group": 5, + "hexcode": "1F566", + "label": "eleven-thirty", + "order": 3986, + "tags": ["11", "11:30", "30", "clock", "eleven", "thirty", "time"], + "unicode": "🕦️" + }, + { + "group": 5, + "hexcode": "1F311", + "label": "new moon", + "order": 3987, + "tags": ["dark", "moon", "new", "space"], + "unicode": "🌑" + }, + { + "group": 5, + "hexcode": "1F312", + "label": "waxing crescent moon", + "order": 3988, + "tags": ["crescent", "dreams", "moon", "space", "waxing"], + "unicode": "🌒" + }, + { + "group": 5, + "hexcode": "1F313", + "label": "first quarter moon", + "order": 3989, + "tags": ["first", "moon", "quarter", "space"], + "unicode": "🌓" + }, + { + "group": 5, + "hexcode": "1F314", + "label": "waxing gibbous moon", + "order": 3990, + "tags": ["gibbous", "moon", "space", "waxing"], + "unicode": "🌔" + }, + { + "group": 5, + "hexcode": "1F315", + "label": "full moon", + "order": 3991, + "tags": ["full", "moon", "space"], + "unicode": "🌕️" + }, + { + "group": 5, + "hexcode": "1F316", + "label": "waning gibbous moon", + "order": 3992, + "tags": ["gibbous", "moon", "space", "waning"], + "unicode": "🌖" + }, + { + "group": 5, + "hexcode": "1F317", + "label": "last quarter moon", + "order": 3993, + "tags": ["last", "moon", "quarter", "space"], + "unicode": "🌗" + }, + { + "group": 5, + "hexcode": "1F318", + "label": "waning crescent moon", + "order": 3994, + "tags": ["crescent", "moon", "space", "waning"], + "unicode": "🌘" + }, + { + "group": 5, + "hexcode": "1F319", + "label": "crescent moon", + "order": 3995, + "tags": ["crescent", "moon", "ramadan", "space"], + "unicode": "🌙" + }, + { + "group": 5, + "hexcode": "1F31A", + "label": "new moon face", + "order": 3996, + "tags": ["face", "moon", "new", "space"], + "unicode": "🌚" + }, + { + "group": 5, + "hexcode": "1F31B", + "label": "first quarter moon face", + "order": 3997, + "tags": ["face", "first", "moon", "quarter", "space"], + "unicode": "🌛" + }, + { + "group": 5, + "hexcode": "1F31C", + "label": "last quarter moon face", + "order": 3998, + "tags": ["dreams", "face", "last", "moon", "quarter"], + "unicode": "🌜️" + }, + { + "group": 5, + "hexcode": "1F321", + "label": "thermometer", + "order": 4000, + "tags": ["weather"], + "unicode": "🌡️" + }, + { + "group": 5, + "hexcode": "2600", + "label": "sun", + "order": 4002, + "tags": ["bright", "rays", "space", "sunny", "weather"], + "unicode": "☀️" + }, + { + "group": 5, + "hexcode": "1F31D", + "label": "full moon face", + "order": 4003, + "tags": ["bright", "face", "full", "moon"], + "unicode": "🌝" + }, + { + "group": 5, + "hexcode": "1F31E", + "label": "sun with face", + "order": 4004, + "tags": [ + "beach", + "bright", + "day", + "face", + "heat", + "shine", + "sun", + "sunny", + "sunshine", + "weather" + ], + "unicode": "🌞" + }, + { + "group": 5, + "hexcode": "1FA90", + "label": "ringed planet", + "order": 4005, + "tags": ["planet", "ringed", "saturn", "saturnine"], + "unicode": "🪐" + }, + { + "group": 5, + "hexcode": "2B50", + "label": "star", + "order": 4006, + "tags": ["astronomy", "medium", "stars", "white"], + "unicode": "⭐️" + }, + { + "group": 5, + "hexcode": "1F31F", + "label": "glowing star", + "order": 4007, + "tags": [ + "glittery", + "glow", + "glowing", + "night", + "shining", + "sparkle", + "star", + "win" + ], + "unicode": "🌟" + }, + { + "group": 5, + "hexcode": "1F320", + "label": "shooting star", + "order": 4008, + "tags": ["falling", "night", "shooting", "space", "star"], + "unicode": "🌠" + }, + { + "group": 5, + "hexcode": "1F30C", + "label": "milky way", + "order": 4009, + "tags": ["milky", "space", "way"], + "unicode": "🌌" + }, + { + "group": 5, + "hexcode": "2601", + "label": "cloud", + "order": 4011, + "tags": ["weather"], + "unicode": "☁️" + }, + { + "group": 5, + "hexcode": "26C5", + "label": "sun behind cloud", + "order": 4012, + "tags": ["behind", "cloud", "cloudy", "sun", "weather"], + "unicode": "⛅️" + }, + { + "group": 5, + "hexcode": "26C8", + "label": "cloud with lightning and rain", + "order": 4014, + "tags": ["cloud", "lightning", "rain", "thunder", "thunderstorm"], + "unicode": "⛈️" + }, + { + "group": 5, + "hexcode": "1F324", + "label": "sun behind small cloud", + "order": 4016, + "tags": ["behind", "cloud", "sun", "weather"], + "unicode": "🌤️" + }, + { + "group": 5, + "hexcode": "1F325", + "label": "sun behind large cloud", + "order": 4018, + "tags": ["behind", "cloud", "sun", "weather"], + "unicode": "🌥️" + }, + { + "group": 5, + "hexcode": "1F326", + "label": "sun behind rain cloud", + "order": 4020, + "tags": ["behind", "cloud", "rain", "sun", "weather"], + "unicode": "🌦️" + }, + { + "group": 5, + "hexcode": "1F327", + "label": "cloud with rain", + "order": 4022, + "tags": ["cloud", "rain", "weather"], + "unicode": "🌧️" + }, + { + "group": 5, + "hexcode": "1F328", + "label": "cloud with snow", + "order": 4024, + "tags": ["cloud", "cold", "snow", "weather"], + "unicode": "🌨️" + }, + { + "group": 5, + "hexcode": "1F329", + "label": "cloud with lightning", + "order": 4026, + "tags": ["cloud", "lightning", "weather"], + "unicode": "🌩️" + }, + { + "group": 5, + "hexcode": "1F32A", + "label": "tornado", + "order": 4028, + "tags": ["cloud", "weather", "whirlwind"], + "unicode": "🌪️" + }, + { + "group": 5, + "hexcode": "1F32B", + "label": "fog", + "order": 4030, + "tags": ["cloud", "weather"], + "unicode": "🌫️" + }, + { + "group": 5, + "hexcode": "1F32C", + "label": "wind face", + "order": 4032, + "tags": ["blow", "cloud", "face", "wind"], + "unicode": "🌬️" + }, + { + "group": 5, + "hexcode": "1F300", + "label": "cyclone", + "order": 4033, + "tags": ["dizzy", "hurricane", "twister", "typhoon", "weather"], + "unicode": "🌀" + }, + { + "group": 5, + "hexcode": "1F308", + "label": "rainbow", + "order": 4034, + "tags": [ + "gay", + "genderqueer", + "glbt", + "glbtq", + "lesbian", + "lgbt", + "lgbtq", + "lgbtqia", + "nature", + "pride", + "queer", + "rain", + "trans", + "transgender", + "weather" + ], + "unicode": "🌈" + }, + { + "group": 5, + "hexcode": "1F302", + "label": "closed umbrella", + "order": 4035, + "tags": ["closed", "clothing", "rain", "umbrella"], + "unicode": "🌂" + }, + { + "group": 5, + "hexcode": "2602", + "label": "umbrella", + "order": 4037, + "tags": ["clothing", "rain"], + "unicode": "☂️" + }, + { + "group": 5, + "hexcode": "2614", + "label": "umbrella with rain drops", + "order": 4038, + "tags": ["clothing", "drop", "drops", "rain", "umbrella", "weather"], + "unicode": "☔️" + }, + { + "group": 5, + "hexcode": "26F1", + "label": "umbrella on ground", + "order": 4040, + "tags": ["ground", "rain", "sun", "umbrella"], + "unicode": "⛱️" + }, + { + "group": 5, + "hexcode": "26A1", + "label": "high voltage", + "order": 4041, + "tags": [ + "danger", + "electric", + "electricity", + "high", + "lightning", + "nature", + "thunder", + "thunderbolt", + "voltage", + "zap" + ], + "unicode": "⚡️" + }, + { + "group": 5, + "hexcode": "2744", + "label": "snowflake", + "order": 4043, + "tags": ["cold", "snow", "weather"], + "unicode": "❄️" + }, + { + "group": 5, + "hexcode": "2603", + "label": "snowman", + "order": 4045, + "tags": ["cold", "man", "snow"], + "unicode": "☃️" + }, + { + "group": 5, + "hexcode": "26C4", + "label": "snowman without snow", + "order": 4046, + "tags": ["cold", "man", "snow", "snowman"], + "unicode": "⛄️" + }, + { + "group": 5, + "hexcode": "2604", + "label": "comet", + "order": 4048, + "tags": ["space"], + "unicode": "☄️" + }, + { + "group": 5, + "hexcode": "1F525", + "label": "fire", + "order": 4049, + "tags": ["af", "burn", "flame", "hot", "lit", "litaf", "tool"], + "unicode": "🔥" + }, + { + "group": 5, + "hexcode": "1F4A7", + "label": "droplet", + "order": 4050, + "tags": [ + "cold", + "comic", + "drop", + "nature", + "sad", + "sweat", + "tear", + "water", + "weather" + ], + "unicode": "💧" + }, + { + "group": 5, + "hexcode": "1F30A", + "label": "water wave", + "order": 4051, + "tags": [ + "nature", + "ocean", + "surf", + "surfer", + "surfing", + "water", + "wave" + ], + "unicode": "🌊" + } + ] + }, + { + "name": "Objects", + "emojis": [ + { + "group": 7, + "hexcode": "1F453", + "label": "glasses", + "order": 4148, + "tags": ["clothing", "eye", "eyeglasses", "eyewear"], + "unicode": "👓️" + }, + { + "group": 7, + "hexcode": "1F576", + "label": "sunglasses", + "order": 4150, + "tags": ["dark", "eye", "eyewear", "glasses"], + "unicode": "🕶️" + }, + { + "group": 7, + "hexcode": "1F97D", + "label": "goggles", + "order": 4151, + "tags": ["dive", "eye", "protection", "scuba", "swimming", "welding"], + "unicode": "🥽" + }, + { + "group": 7, + "hexcode": "1F97C", + "label": "lab coat", + "order": 4152, + "tags": [ + "clothes", + "coat", + "doctor", + "dr", + "experiment", + "jacket", + "lab", + "scientist", + "white" + ], + "unicode": "🥼" + }, + { + "group": 7, + "hexcode": "1F9BA", + "label": "safety vest", + "order": 4153, + "tags": ["emergency", "safety", "vest"], + "unicode": "🦺" + }, + { + "group": 7, + "hexcode": "1F454", + "label": "necktie", + "order": 4154, + "tags": ["clothing", "employed", "serious", "shirt", "tie"], + "unicode": "👔" + }, + { + "group": 7, + "hexcode": "1F455", + "label": "t-shirt", + "order": 4155, + "tags": [ + "blue", + "casual", + "clothes", + "clothing", + "collar", + "dressed", + "shirt", + "shopping", + "tshirt", + "weekend" + ], + "unicode": "👕" + }, + { + "group": 7, + "hexcode": "1F456", + "label": "jeans", + "order": 4156, + "tags": [ + "blue", + "casual", + "clothes", + "clothing", + "denim", + "dressed", + "pants", + "shopping", + "trousers", + "weekend" + ], + "unicode": "👖" + }, + { + "group": 7, + "hexcode": "1F9E3", + "label": "scarf", + "order": 4157, + "tags": ["bundle", "cold", "neck", "up"], + "unicode": "🧣" + }, + { + "group": 7, + "hexcode": "1F9E4", + "label": "gloves", + "order": 4158, + "tags": ["hand"], + "unicode": "🧤" + }, + { + "group": 7, + "hexcode": "1F9E5", + "label": "coat", + "order": 4159, + "tags": ["brr", "bundle", "cold", "jacket", "up"], + "unicode": "🧥" + }, + { + "group": 7, + "hexcode": "1F9E6", + "label": "socks", + "order": 4160, + "tags": ["stocking"], + "unicode": "🧦" + }, + { + "group": 7, + "hexcode": "1F457", + "label": "dress", + "order": 4161, + "tags": ["clothes", "clothing", "dressed", "fancy", "shopping"], + "unicode": "👗" + }, + { + "group": 7, + "hexcode": "1F458", + "label": "kimono", + "order": 4162, + "tags": ["clothing", "comfortable"], + "unicode": "👘" + }, + { + "group": 7, + "hexcode": "1F97B", + "label": "sari", + "order": 4163, + "tags": ["clothing", "dress"], + "unicode": "🥻" + }, + { + "group": 7, + "hexcode": "1FA71", + "label": "one-piece swimsuit", + "order": 4164, + "tags": ["bathing", "one-piece", "suit", "swimsuit"], + "unicode": "🩱" + }, + { + "group": 7, + "hexcode": "1FA72", + "label": "briefs", + "order": 4165, + "tags": ["bathing", "one-piece", "suit", "swimsuit", "underwear"], + "unicode": "🩲" + }, + { + "group": 7, + "hexcode": "1FA73", + "label": "shorts", + "order": 4166, + "tags": ["bathing", "pants", "suit", "swimsuit", "underwear"], + "unicode": "🩳" + }, + { + "group": 7, + "hexcode": "1F459", + "label": "bikini", + "order": 4167, + "tags": ["bathing", "beach", "clothing", "pool", "suit", "swim"], + "unicode": "👙" + }, + { + "group": 7, + "hexcode": "1F45A", + "label": "woman’s clothes", + "order": 4168, + "tags": [ + "blouse", + "clothes", + "clothing", + "collar", + "dress", + "dressed", + "lady", + "shirt", + "shopping", + "woman", + "woman’s" + ], + "unicode": "👚" + }, + { + "group": 7, + "hexcode": "1FAAD", + "label": "folding hand fan", + "order": 4169, + "tags": [ + "clack", + "clap", + "cool", + "cooling", + "dance", + "fan", + "flirt", + "flutter", + "folding", + "hand", + "hot", + "shy" + ], + "unicode": "🪭" + }, + { + "group": 7, + "hexcode": "1F45B", + "label": "purse", + "order": 4170, + "tags": [ + "clothes", + "clothing", + "coin", + "dress", + "fancy", + "handbag", + "shopping" + ], + "unicode": "👛" + }, + { + "group": 7, + "hexcode": "1F45C", + "label": "handbag", + "order": 4171, + "tags": [ + "bag", + "clothes", + "clothing", + "dress", + "lady", + "purse", + "shopping" + ], + "unicode": "👜" + }, + { + "group": 7, + "hexcode": "1F45D", + "label": "clutch bag", + "order": 4172, + "tags": [ + "bag", + "clothes", + "clothing", + "clutch", + "dress", + "handbag", + "pouch", + "purse" + ], + "unicode": "👝" + }, + { + "group": 7, + "hexcode": "1F6CD", + "label": "shopping bags", + "order": 4174, + "tags": ["bag", "bags", "hotel", "shopping"], + "unicode": "🛍️" + }, + { + "group": 7, + "hexcode": "1F392", + "label": "backpack", + "order": 4175, + "tags": [ + "backpacking", + "bag", + "bookbag", + "education", + "rucksack", + "satchel", + "school" + ], + "unicode": "🎒" + }, + { + "group": 7, + "hexcode": "1FA74", + "label": "thong sandal", + "order": 4176, + "tags": [ + "beach", + "flip", + "flop", + "sandal", + "sandals", + "shoe", + "thong", + "thongs", + "zōri" + ], + "unicode": "🩴" + }, + { + "group": 7, + "hexcode": "1F45E", + "label": "man’s shoe", + "order": 4177, + "tags": [ + "brown", + "clothes", + "clothing", + "feet", + "foot", + "kick", + "man", + "man’s", + "shoe", + "shoes", + "shopping" + ], + "unicode": "👞" + }, + { + "group": 7, + "hexcode": "1F45F", + "label": "running shoe", + "order": 4178, + "tags": [ + "athletic", + "clothes", + "clothing", + "fast", + "kick", + "running", + "shoe", + "shoes", + "shopping", + "sneaker", + "tennis" + ], + "unicode": "👟" + }, + { + "group": 7, + "hexcode": "1F97E", + "label": "hiking boot", + "order": 4179, + "tags": [ + "backpacking", + "boot", + "brown", + "camping", + "hiking", + "outdoors", + "shoe" + ], + "unicode": "🥾" + }, + { + "group": 7, + "hexcode": "1F97F", + "label": "flat shoe", + "order": 4180, + "tags": [ + "ballet", + "comfy", + "flat", + "flats", + "shoe", + "slip-on", + "slipper" + ], + "unicode": "🥿" + }, + { + "group": 7, + "hexcode": "1F460", + "label": "high-heeled shoe", + "order": 4181, + "tags": [ + "clothes", + "clothing", + "dress", + "fashion", + "heel", + "heels", + "high-heeled", + "shoe", + "shoes", + "shopping", + "stiletto", + "woman" + ], + "unicode": "👠" + }, + { + "group": 7, + "hexcode": "1F461", + "label": "woman’s sandal", + "order": 4182, + "tags": ["clothing", "sandal", "shoe", "woman", "woman’s"], + "unicode": "👡" + }, + { + "group": 7, + "hexcode": "1FA70", + "label": "ballet shoes", + "order": 4183, + "tags": ["ballet", "dance", "shoes"], + "unicode": "🩰" + }, + { + "group": 7, + "hexcode": "1F462", + "label": "woman’s boot", + "order": 4184, + "tags": [ + "boot", + "clothes", + "clothing", + "dress", + "shoe", + "shoes", + "shopping", + "woman", + "woman’s" + ], + "unicode": "👢" + }, + { + "group": 7, + "hexcode": "1FAAE", + "label": "hair pick", + "order": 4185, + "tags": ["afro", "comb", "groom", "hair", "pick"], + "unicode": "🪮" + }, + { + "group": 7, + "hexcode": "1F451", + "label": "crown", + "order": 4186, + "tags": [ + "clothing", + "family", + "king", + "medieval", + "queen", + "royal", + "royalty", + "win" + ], + "unicode": "👑" + }, + { + "group": 7, + "hexcode": "1F452", + "label": "woman’s hat", + "order": 4187, + "tags": [ + "clothes", + "clothing", + "garden", + "hat", + "hats", + "party", + "woman", + "woman’s" + ], + "unicode": "👒" + }, + { + "group": 7, + "hexcode": "1F3A9", + "label": "top hat", + "order": 4188, + "tags": [ + "clothes", + "clothing", + "fancy", + "formal", + "hat", + "magic", + "top", + "tophat" + ], + "unicode": "🎩" + }, + { + "group": 7, + "hexcode": "1F393", + "label": "graduation cap", + "order": 4189, + "tags": [ + "cap", + "celebration", + "clothing", + "education", + "graduation", + "hat", + "scholar" + ], + "unicode": "🎓️" + }, + { + "group": 7, + "hexcode": "1F9E2", + "label": "billed cap", + "order": 4190, + "tags": ["baseball", "bent", "billed", "cap", "dad", "hat"], + "unicode": "🧢" + }, + { + "group": 7, + "hexcode": "1FA96", + "label": "military helmet", + "order": 4191, + "tags": ["army", "helmet", "military", "soldier", "war", "warrior"], + "unicode": "🪖" + }, + { + "group": 7, + "hexcode": "26D1", + "label": "rescue worker’s helmet", + "order": 4193, + "tags": ["aid", "cross", "face", "hat", "helmet", "rescue", "worker’s"], + "unicode": "⛑️" + }, + { + "group": 7, + "hexcode": "1F4FF", + "label": "prayer beads", + "order": 4194, + "tags": ["beads", "clothing", "necklace", "prayer", "religion"], + "unicode": "📿" + }, + { + "group": 7, + "hexcode": "1F484", + "label": "lipstick", + "order": 4195, + "tags": ["cosmetics", "date", "makeup"], + "unicode": "💄" + }, + { + "group": 7, + "hexcode": "1F48D", + "label": "ring", + "order": 4196, + "tags": [ + "diamond", + "engaged", + "engagement", + "married", + "romance", + "shiny", + "sparkling", + "wedding" + ], + "unicode": "💍" + }, + { + "group": 7, + "hexcode": "1F48E", + "label": "gem stone", + "order": 4197, + "tags": [ + "diamond", + "engagement", + "gem", + "jewel", + "money", + "romance", + "stone", + "wedding" + ], + "unicode": "💎" + }, + { + "group": 7, + "hexcode": "1F507", + "label": "muted speaker", + "order": 4198, + "tags": ["mute", "muted", "quiet", "silent", "sound", "speaker"], + "unicode": "🔇" + }, + { + "group": 7, + "hexcode": "1F508", + "label": "speaker low volume", + "order": 4199, + "tags": ["low", "soft", "sound", "speaker", "volume"], + "unicode": "🔈️" + }, + { + "group": 7, + "hexcode": "1F509", + "label": "speaker medium volume", + "order": 4200, + "tags": ["medium", "sound", "speaker", "volume"], + "unicode": "🔉" + }, + { + "group": 7, + "hexcode": "1F50A", + "label": "speaker high volume", + "order": 4201, + "tags": ["high", "loud", "music", "sound", "speaker", "volume"], + "unicode": "🔊" + }, + { + "group": 7, + "hexcode": "1F4E2", + "label": "loudspeaker", + "order": 4202, + "tags": ["address", "communication", "loud", "public", "sound"], + "unicode": "📢" + }, + { + "group": 7, + "hexcode": "1F4E3", + "label": "megaphone", + "order": 4203, + "tags": ["cheering", "sound"], + "unicode": "📣" + }, + { + "group": 7, + "hexcode": "1F4EF", + "label": "postal horn", + "order": 4204, + "tags": ["horn", "post", "postal"], + "unicode": "📯" + }, + { + "group": 7, + "hexcode": "1F514", + "label": "bell", + "order": 4205, + "tags": ["break", "church", "sound"], + "unicode": "🔔" + }, + { + "group": 7, + "hexcode": "1F515", + "label": "bell with slash", + "order": 4206, + "tags": [ + "bell", + "forbidden", + "mute", + "no", + "not", + "prohibited", + "quiet", + "silent", + "slash", + "sound" + ], + "unicode": "🔕" + }, + { + "group": 7, + "hexcode": "1F3BC", + "label": "musical score", + "order": 4207, + "tags": ["music", "musical", "note", "score"], + "unicode": "🎼" + }, + { + "group": 7, + "hexcode": "1F3B5", + "label": "musical note", + "order": 4208, + "tags": ["music", "musical", "note", "sound"], + "unicode": "🎵" + }, + { + "group": 7, + "hexcode": "1F3B6", + "label": "musical notes", + "order": 4209, + "tags": ["music", "musical", "note", "notes", "sound"], + "unicode": "🎶" + }, + { + "group": 7, + "hexcode": "1F399", + "label": "studio microphone", + "order": 4211, + "tags": ["mic", "microphone", "music", "studio"], + "unicode": "🎙️" + }, + { + "group": 7, + "hexcode": "1F39A", + "label": "level slider", + "order": 4213, + "tags": ["level", "music", "slider"], + "unicode": "🎚️" + }, + { + "group": 7, + "hexcode": "1F39B", + "label": "control knobs", + "order": 4215, + "tags": ["control", "knobs", "music"], + "unicode": "🎛️" + }, + { + "group": 7, + "hexcode": "1F3A4", + "label": "microphone", + "order": 4216, + "tags": ["karaoke", "mic", "music", "sing", "sound"], + "unicode": "🎤" + }, + { + "group": 7, + "hexcode": "1F3A7", + "label": "headphone", + "order": 4217, + "tags": ["earbud", "sound"], + "unicode": "🎧️" + }, + { + "group": 7, + "hexcode": "1F4FB", + "label": "radio", + "order": 4218, + "tags": ["entertainment", "tbt", "video"], + "unicode": "📻️" + }, + { + "group": 7, + "hexcode": "1F3B7", + "label": "saxophone", + "order": 4219, + "tags": ["instrument", "music", "sax"], + "unicode": "🎷" + }, + { + "group": 7, + "hexcode": "1FA97", + "label": "accordion", + "order": 4220, + "tags": [ + "box", + "concertina", + "instrument", + "music", + "squeeze", + "squeezebox" + ], + "unicode": "🪗" + }, + { + "group": 7, + "hexcode": "1F3B8", + "label": "guitar", + "order": 4221, + "tags": ["instrument", "music", "strat"], + "unicode": "🎸" + }, + { + "group": 7, + "hexcode": "1F3B9", + "label": "musical keyboard", + "order": 4222, + "tags": ["instrument", "keyboard", "music", "musical", "piano"], + "unicode": "🎹" + }, + { + "group": 7, + "hexcode": "1F3BA", + "label": "trumpet", + "order": 4223, + "tags": ["instrument", "music"], + "unicode": "🎺" + }, + { + "group": 7, + "hexcode": "1F3BB", + "label": "violin", + "order": 4224, + "tags": ["instrument", "music"], + "unicode": "🎻" + }, + { + "group": 7, + "hexcode": "1FA95", + "label": "banjo", + "order": 4225, + "tags": ["music", "stringed"], + "unicode": "🪕" + }, + { + "group": 7, + "hexcode": "1F941", + "label": "drum", + "order": 4226, + "tags": ["drumsticks", "music"], + "unicode": "🥁" + }, + { + "group": 7, + "hexcode": "1FA98", + "label": "long drum", + "order": 4227, + "tags": ["beat", "conga", "drum", "instrument", "long", "rhythm"], + "unicode": "🪘" + }, + { + "group": 7, + "hexcode": "1FA87", + "label": "maracas", + "order": 4228, + "tags": [ + "cha", + "dance", + "instrument", + "music", + "party", + "percussion", + "rattle", + "shake", + "shaker" + ], + "unicode": "🪇" + }, + { + "group": 7, + "hexcode": "1FA88", + "label": "flute", + "order": 4229, + "tags": [ + "band", + "fife", + "flautist", + "instrument", + "marching", + "music", + "orchestra", + "piccolo", + "pipe", + "recorder", + "woodwind" + ], + "unicode": "🪈" + }, + { + "group": 7, + "hexcode": "1FA89", + "label": "harp", + "order": 4230, + "tags": ["cupid", "instrument", "love", "music", "orchestra"], + "unicode": "🪉" + }, + { + "group": 7, + "hexcode": "1F4F1", + "label": "mobile phone", + "order": 4231, + "tags": ["cell", "communication", "mobile", "phone", "telephone"], + "unicode": "📱" + }, + { + "group": 7, + "hexcode": "1F4F2", + "label": "mobile phone with arrow", + "order": 4232, + "tags": [ + "arrow", + "build", + "call", + "cell", + "communication", + "mobile", + "phone", + "receive", + "telephone" + ], + "unicode": "📲" + }, + { + "group": 7, + "hexcode": "260E", + "label": "telephone", + "order": 4234, + "tags": ["phone"], + "unicode": "☎️" + }, + { + "group": 7, + "hexcode": "1F4DE", + "label": "telephone receiver", + "order": 4235, + "tags": ["communication", "phone", "receiver", "telephone", "voip"], + "unicode": "📞" + }, + { + "group": 7, + "hexcode": "1F4DF", + "label": "pager", + "order": 4236, + "tags": ["communication"], + "unicode": "📟️" + }, + { + "group": 7, + "hexcode": "1F4E0", + "label": "fax machine", + "order": 4237, + "tags": ["communication", "fax", "machine"], + "unicode": "📠" + }, + { + "group": 7, + "hexcode": "1F50B", + "label": "battery", + "order": 4238, + "tags": ["battery"], + "unicode": "🔋" + }, + { + "group": 7, + "hexcode": "1FAAB", + "label": "low battery", + "order": 4239, + "tags": ["battery", "drained", "electronic", "energy", "low", "power"], + "unicode": "🪫" + }, + { + "group": 7, + "hexcode": "1F50C", + "label": "electric plug", + "order": 4240, + "tags": ["electric", "electricity", "plug"], + "unicode": "🔌" + }, + { + "group": 7, + "hexcode": "1F4BB", + "label": "laptop", + "order": 4241, + "tags": ["computer", "office", "pc", "personal"], + "unicode": "💻️" + }, + { + "group": 7, + "hexcode": "1F5A5", + "label": "desktop computer", + "order": 4243, + "tags": ["computer", "desktop", "monitor"], + "unicode": "🖥️" + }, + { + "group": 7, + "hexcode": "1F5A8", + "label": "printer", + "order": 4245, + "tags": ["computer"], + "unicode": "🖨️" + }, + { + "group": 7, + "hexcode": "2328", + "label": "keyboard", + "order": 4247, + "tags": ["computer"], + "unicode": "⌨️" + }, + { + "group": 7, + "hexcode": "1F5B1", + "label": "computer mouse", + "order": 4249, + "tags": ["computer", "mouse"], + "unicode": "🖱️" + }, + { + "group": 7, + "hexcode": "1F5B2", + "label": "trackball", + "order": 4251, + "tags": ["computer"], + "unicode": "🖲️" + }, + { + "group": 7, + "hexcode": "1F4BD", + "label": "computer disk", + "order": 4252, + "tags": ["computer", "disk", "minidisk", "optical"], + "unicode": "💽" + }, + { + "group": 7, + "hexcode": "1F4BE", + "label": "floppy disk", + "order": 4253, + "tags": ["computer", "disk", "floppy"], + "unicode": "💾" + }, + { + "group": 7, + "hexcode": "1F4BF", + "label": "optical disk", + "order": 4254, + "tags": ["blu-ray", "cd", "computer", "disk", "dvd", "optical"], + "unicode": "💿️" + }, + { + "group": 7, + "hexcode": "1F4C0", + "label": "dvd", + "order": 4255, + "tags": ["blu-ray", "cd", "computer", "disk", "optical"], + "unicode": "📀" + }, + { + "group": 7, + "hexcode": "1F9EE", + "label": "abacus", + "order": 4256, + "tags": ["calculation", "calculator"], + "unicode": "🧮" + }, + { + "group": 7, + "hexcode": "1F3A5", + "label": "movie camera", + "order": 4257, + "tags": [ + "bollywood", + "camera", + "cinema", + "film", + "hollywood", + "movie", + "record" + ], + "unicode": "🎥" + }, + { + "group": 7, + "hexcode": "1F39E", + "label": "film frames", + "order": 4259, + "tags": ["cinema", "film", "frames", "movie"], + "unicode": "🎞️" + }, + { + "group": 7, + "hexcode": "1F4FD", + "label": "film projector", + "order": 4261, + "tags": ["cinema", "film", "movie", "projector", "video"], + "unicode": "📽️" + }, + { + "group": 7, + "hexcode": "1F3AC", + "label": "clapper board", + "order": 4262, + "tags": ["action", "board", "clapper", "movie"], + "unicode": "🎬️" + }, + { + "group": 7, + "hexcode": "1F4FA", + "label": "television", + "order": 4263, + "tags": ["tv", "video"], + "unicode": "📺️" + }, + { + "group": 7, + "hexcode": "1F4F7", + "label": "camera", + "order": 4264, + "tags": ["photo", "selfie", "snap", "tbt", "trip", "video"], + "unicode": "📷️" + }, + { + "group": 7, + "hexcode": "1F4F8", + "label": "camera with flash", + "order": 4265, + "tags": ["camera", "flash", "video"], + "unicode": "📸" + }, + { + "group": 7, + "hexcode": "1F4F9", + "label": "video camera", + "order": 4266, + "tags": ["camcorder", "camera", "tbt", "video"], + "unicode": "📹️" + }, + { + "group": 7, + "hexcode": "1F4FC", + "label": "videocassette", + "order": 4267, + "tags": ["old", "school", "tape", "vcr", "vhs", "video"], + "unicode": "📼" + }, + { + "group": 7, + "hexcode": "1F50D", + "label": "magnifying glass tilted left", + "order": 4268, + "tags": [ + "glass", + "lab", + "left", + "left-pointing", + "magnifying", + "science", + "search", + "tilted", + "tool" + ], + "unicode": "🔍️" + }, + { + "group": 7, + "hexcode": "1F50E", + "label": "magnifying glass tilted right", + "order": 4269, + "tags": [ + "contact", + "glass", + "lab", + "magnifying", + "right", + "right-pointing", + "science", + "search", + "tilted", + "tool" + ], + "unicode": "🔎" + }, + { + "group": 7, + "hexcode": "1F56F", + "label": "candle", + "order": 4271, + "tags": ["light"], + "unicode": "🕯️" + }, + { + "group": 7, + "hexcode": "1F4A1", + "label": "light bulb", + "order": 4272, + "tags": ["bulb", "comic", "electric", "idea", "light"], + "unicode": "💡" + }, + { + "group": 7, + "hexcode": "1F526", + "label": "flashlight", + "order": 4273, + "tags": ["electric", "light", "tool", "torch"], + "unicode": "🔦" + }, + { + "group": 7, + "hexcode": "1F3EE", + "label": "red paper lantern", + "order": 4274, + "tags": ["bar", "lantern", "light", "paper", "red", "restaurant"], + "unicode": "🏮" + }, + { + "group": 7, + "hexcode": "1FA94", + "label": "diya lamp", + "order": 4275, + "tags": ["diya", "lamp", "light", "oil"], + "unicode": "🪔" + }, + { + "group": 7, + "hexcode": "1F4D4", + "label": "notebook with decorative cover", + "order": 4276, + "tags": [ + "book", + "cover", + "decorated", + "decorative", + "education", + "notebook", + "school", + "writing" + ], + "unicode": "📔" + }, + { + "group": 7, + "hexcode": "1F4D5", + "label": "closed book", + "order": 4277, + "tags": ["book", "closed", "education"], + "unicode": "📕" + }, + { + "group": 7, + "hexcode": "1F4D6", + "label": "open book", + "order": 4278, + "tags": [ + "book", + "education", + "fantasy", + "knowledge", + "library", + "novels", + "open", + "reading" + ], + "unicode": "📖" + }, + { + "group": 7, + "hexcode": "1F4D7", + "label": "green book", + "order": 4279, + "tags": ["book", "education", "fantasy", "green", "library", "reading"], + "unicode": "📗" + }, + { + "group": 7, + "hexcode": "1F4D8", + "label": "blue book", + "order": 4280, + "tags": ["blue", "book", "education", "fantasy", "library", "reading"], + "unicode": "📘" + }, + { + "group": 7, + "hexcode": "1F4D9", + "label": "orange book", + "order": 4281, + "tags": [ + "book", + "education", + "fantasy", + "library", + "orange", + "reading" + ], + "unicode": "📙" + }, + { + "group": 7, + "hexcode": "1F4DA", + "label": "books", + "order": 4282, + "tags": [ + "book", + "education", + "fantasy", + "knowledge", + "library", + "novels", + "reading", + "school", + "study" + ], + "unicode": "📚️" + }, + { + "group": 7, + "hexcode": "1F4D3", + "label": "notebook", + "order": 4283, + "tags": ["notebook"], + "unicode": "📓" + }, + { + "group": 7, + "hexcode": "1F4D2", + "label": "ledger", + "order": 4284, + "tags": ["notebook"], + "unicode": "📒" + }, + { + "group": 7, + "hexcode": "1F4C3", + "label": "page with curl", + "order": 4285, + "tags": ["curl", "document", "page", "paper"], + "unicode": "📃" + }, + { + "group": 7, + "hexcode": "1F4DC", + "label": "scroll", + "order": 4286, + "tags": ["paper"], + "unicode": "📜" + }, + { + "group": 7, + "hexcode": "1F4C4", + "label": "page facing up", + "order": 4287, + "tags": ["document", "facing", "page", "paper", "up"], + "unicode": "📄" + }, + { + "group": 7, + "hexcode": "1F4F0", + "label": "newspaper", + "order": 4288, + "tags": ["communication", "news", "paper"], + "unicode": "📰" + }, + { + "group": 7, + "hexcode": "1F5DE", + "label": "rolled-up newspaper", + "order": 4290, + "tags": ["news", "newspaper", "paper", "rolled", "rolled-up"], + "unicode": "🗞️" + }, + { + "group": 7, + "hexcode": "1F4D1", + "label": "bookmark tabs", + "order": 4291, + "tags": ["bookmark", "mark", "marker", "tabs"], + "unicode": "📑" + }, + { + "group": 7, + "hexcode": "1F516", + "label": "bookmark", + "order": 4292, + "tags": ["mark"], + "unicode": "🔖" + }, + { + "group": 7, + "hexcode": "1F3F7", + "label": "label", + "order": 4294, + "tags": ["tag"], + "unicode": "🏷️" + }, + { + "group": 7, + "hexcode": "1F4B0", + "label": "money bag", + "order": 4295, + "tags": [ + "bag", + "bank", + "bet", + "billion", + "cash", + "cost", + "dollar", + "gold", + "million", + "money", + "moneybag", + "paid", + "paying", + "pot", + "rich", + "win" + ], + "unicode": "💰️" + }, + { + "group": 7, + "hexcode": "1FA99", + "label": "coin", + "order": 4296, + "tags": [ + "dollar", + "euro", + "gold", + "metal", + "money", + "rich", + "silver", + "treasure" + ], + "unicode": "🪙" + }, + { + "group": 7, + "hexcode": "1F4B4", + "label": "yen banknote", + "order": 4297, + "tags": [ + "bank", + "banknote", + "bill", + "currency", + "money", + "note", + "yen" + ], + "unicode": "💴" + }, + { + "group": 7, + "hexcode": "1F4B5", + "label": "dollar banknote", + "order": 4298, + "tags": [ + "bank", + "banknote", + "bill", + "currency", + "dollar", + "money", + "note" + ], + "unicode": "💵" + }, + { + "group": 7, + "hexcode": "1F4B6", + "label": "euro banknote", + "order": 4299, + "tags": [ + "100", + "bank", + "banknote", + "bill", + "currency", + "euro", + "money", + "note", + "rich" + ], + "unicode": "💶" + }, + { + "group": 7, + "hexcode": "1F4B7", + "label": "pound banknote", + "order": 4300, + "tags": [ + "bank", + "banknote", + "bill", + "billion", + "cash", + "currency", + "money", + "note", + "pound", + "pounds" + ], + "unicode": "💷" + }, + { + "group": 7, + "hexcode": "1F4B8", + "label": "money with wings", + "order": 4301, + "tags": [ + "bank", + "banknote", + "bill", + "billion", + "cash", + "dollar", + "fly", + "million", + "money", + "note", + "pay", + "wings" + ], + "unicode": "💸" + }, + { + "group": 7, + "hexcode": "1F4B3", + "label": "credit card", + "order": 4302, + "tags": ["bank", "card", "cash", "charge", "credit", "money", "pay"], + "unicode": "💳️" + }, + { + "group": 7, + "hexcode": "1F9FE", + "label": "receipt", + "order": 4303, + "tags": ["accounting", "bookkeeping", "evidence", "invoice", "proof"], + "unicode": "🧾" + }, + { + "group": 7, + "hexcode": "1F4B9", + "label": "chart increasing with yen", + "order": 4304, + "tags": [ + "bank", + "chart", + "currency", + "graph", + "growth", + "increasing", + "market", + "money", + "rise", + "trend", + "upward", + "yen" + ], + "unicode": "💹" + }, + { + "group": 7, + "hexcode": "2709", + "label": "envelope", + "order": 4306, + "tags": ["e-mail", "email", "letter"], + "unicode": "✉️" + }, + { + "group": 7, + "hexcode": "1F4E7", + "label": "e-mail", + "order": 4307, + "tags": ["email", "letter", "mail"], + "unicode": "📧" + }, + { + "group": 7, + "hexcode": "1F4E8", + "label": "incoming envelope", + "order": 4308, + "tags": [ + "delivering", + "e-mail", + "email", + "envelope", + "incoming", + "letter", + "mail", + "receive", + "sent" + ], + "unicode": "📨" + }, + { + "group": 7, + "hexcode": "1F4E9", + "label": "envelope with arrow", + "order": 4309, + "tags": [ + "arrow", + "communication", + "down", + "e-mail", + "email", + "envelope", + "letter", + "mail", + "outgoing", + "send", + "sent" + ], + "unicode": "📩" + }, + { + "group": 7, + "hexcode": "1F4E4", + "label": "outbox tray", + "order": 4310, + "tags": ["box", "email", "letter", "mail", "outbox", "sent", "tray"], + "unicode": "📤️" + }, + { + "group": 7, + "hexcode": "1F4E5", + "label": "inbox tray", + "order": 4311, + "tags": [ + "box", + "email", + "inbox", + "letter", + "mail", + "receive", + "tray", + "zero" + ], + "unicode": "📥️" + }, + { + "group": 7, + "hexcode": "1F4E6", + "label": "package", + "order": 4312, + "tags": ["box", "communication", "delivery", "parcel", "shipping"], + "unicode": "📦️" + }, + { + "group": 7, + "hexcode": "1F4EB", + "label": "closed mailbox with raised flag", + "order": 4313, + "tags": [ + "closed", + "communication", + "flag", + "mail", + "mailbox", + "postbox", + "raised" + ], + "unicode": "📫️" + }, + { + "group": 7, + "hexcode": "1F4EA", + "label": "closed mailbox with lowered flag", + "order": 4314, + "tags": ["closed", "flag", "lowered", "mail", "mailbox", "postbox"], + "unicode": "📪️" + }, + { + "group": 7, + "hexcode": "1F4EC", + "label": "open mailbox with raised flag", + "order": 4315, + "tags": ["flag", "mail", "mailbox", "open", "postbox", "raised"], + "unicode": "📬️" + }, + { + "group": 7, + "hexcode": "1F4ED", + "label": "open mailbox with lowered flag", + "order": 4316, + "tags": ["flag", "lowered", "mail", "mailbox", "open", "postbox"], + "unicode": "📭️" + }, + { + "group": 7, + "hexcode": "1F4EE", + "label": "postbox", + "order": 4317, + "tags": ["mail", "mailbox"], + "unicode": "📮" + }, + { + "group": 7, + "hexcode": "1F5F3", + "label": "ballot box with ballot", + "order": 4319, + "tags": ["ballot", "box"], + "unicode": "🗳️" + }, + { + "group": 7, + "hexcode": "270F", + "label": "pencil", + "order": 4321, + "tags": ["pencil"], + "unicode": "✏️" + }, + { + "group": 7, + "hexcode": "2712", + "label": "black nib", + "order": 4323, + "tags": ["black", "nib", "pen"], + "unicode": "✒️" + }, + { + "group": 7, + "hexcode": "1F58B", + "label": "fountain pen", + "order": 4325, + "tags": ["fountain", "pen"], + "unicode": "🖋️" + }, + { + "group": 7, + "hexcode": "1F58A", + "label": "pen", + "order": 4327, + "tags": ["ballpoint"], + "unicode": "🖊️" + }, + { + "group": 7, + "hexcode": "1F58C", + "label": "paintbrush", + "order": 4329, + "tags": ["painting"], + "unicode": "🖌️" + }, + { + "group": 7, + "hexcode": "1F58D", + "label": "crayon", + "order": 4331, + "tags": ["crayon"], + "unicode": "🖍️" + }, + { + "group": 7, + "hexcode": "1F4DD", + "label": "memo", + "order": 4332, + "tags": ["communication", "media", "notes", "pencil"], + "unicode": "📝" + }, + { + "group": 7, + "hexcode": "1F4BC", + "label": "briefcase", + "order": 4333, + "tags": ["office"], + "unicode": "💼" + }, + { + "group": 7, + "hexcode": "1F4C1", + "label": "file folder", + "order": 4334, + "tags": ["file", "folder"], + "unicode": "📁" + }, + { + "group": 7, + "hexcode": "1F4C2", + "label": "open file folder", + "order": 4335, + "tags": ["file", "folder", "open"], + "unicode": "📂" + }, + { + "group": 7, + "hexcode": "1F5C2", + "label": "card index dividers", + "order": 4337, + "tags": ["card", "dividers", "index"], + "unicode": "🗂️" + }, + { + "group": 7, + "hexcode": "1F4C5", + "label": "calendar", + "order": 4338, + "tags": ["date"], + "unicode": "📅" + }, + { + "group": 7, + "hexcode": "1F4C6", + "label": "tear-off calendar", + "order": 4339, + "tags": ["calendar", "tear-off"], + "unicode": "📆" + }, + { + "group": 7, + "hexcode": "1F5D2", + "label": "spiral notepad", + "order": 4341, + "tags": ["note", "notepad", "pad", "spiral"], + "unicode": "🗒️" + }, + { + "group": 7, + "hexcode": "1F5D3", + "label": "spiral calendar", + "order": 4343, + "tags": ["calendar", "pad", "spiral"], + "unicode": "🗓️" + }, + { + "group": 7, + "hexcode": "1F4C7", + "label": "card index", + "order": 4344, + "tags": ["card", "index", "old", "rolodex", "school"], + "unicode": "📇" + }, + { + "group": 7, + "hexcode": "1F4C8", + "label": "chart increasing", + "order": 4345, + "tags": [ + "chart", + "data", + "graph", + "growth", + "increasing", + "right", + "trend", + "up", + "upward" + ], + "unicode": "📈" + }, + { + "group": 7, + "hexcode": "1F4C9", + "label": "chart decreasing", + "order": 4346, + "tags": [ + "chart", + "data", + "decreasing", + "down", + "downward", + "graph", + "negative", + "trend" + ], + "unicode": "📉" + }, + { + "group": 7, + "hexcode": "1F4CA", + "label": "bar chart", + "order": 4347, + "tags": ["bar", "chart", "data", "graph"], + "unicode": "📊" + }, + { + "group": 7, + "hexcode": "1F4CB", + "label": "clipboard", + "order": 4348, + "tags": ["do", "list", "notes"], + "unicode": "📋️" + }, + { + "group": 7, + "hexcode": "1F4CC", + "label": "pushpin", + "order": 4349, + "tags": ["collage", "pin"], + "unicode": "📌" + }, + { + "group": 7, + "hexcode": "1F4CD", + "label": "round pushpin", + "order": 4350, + "tags": ["location", "map", "pin", "pushpin", "round"], + "unicode": "📍" + }, + { + "group": 7, + "hexcode": "1F4CE", + "label": "paperclip", + "order": 4351, + "tags": ["paperclip"], + "unicode": "📎" + }, + { + "group": 7, + "hexcode": "1F587", + "label": "linked paperclips", + "order": 4353, + "tags": ["link", "linked", "paperclip", "paperclips"], + "unicode": "🖇️" + }, + { + "group": 7, + "hexcode": "1F4CF", + "label": "straight ruler", + "order": 4354, + "tags": ["angle", "edge", "math", "ruler", "straight", "straightedge"], + "unicode": "📏" + }, + { + "group": 7, + "hexcode": "1F4D0", + "label": "triangular ruler", + "order": 4355, + "tags": [ + "angle", + "math", + "rule", + "ruler", + "set", + "slide", + "triangle", + "triangular" + ], + "unicode": "📐" + }, + { + "group": 7, + "hexcode": "2702", + "label": "scissors", + "order": 4357, + "tags": ["cut", "cutting", "paper", "tool"], + "unicode": "✂️" + }, + { + "group": 7, + "hexcode": "1F5C3", + "label": "card file box", + "order": 4359, + "tags": ["box", "card", "file"], + "unicode": "🗃️" + }, + { + "group": 7, + "hexcode": "1F5C4", + "label": "file cabinet", + "order": 4361, + "tags": ["cabinet", "file", "filing", "paper"], + "unicode": "🗄️" + }, + { + "group": 7, + "hexcode": "1F5D1", + "label": "wastebasket", + "order": 4363, + "tags": ["can", "garbage", "trash", "waste"], + "unicode": "🗑️" + }, + { + "group": 7, + "hexcode": "1F512", + "label": "locked", + "order": 4364, + "tags": ["closed", "lock", "private"], + "unicode": "🔒️" + }, + { + "group": 7, + "hexcode": "1F513", + "label": "unlocked", + "order": 4365, + "tags": ["cracked", "lock", "open", "unlock"], + "unicode": "🔓️" + }, + { + "group": 7, + "hexcode": "1F50F", + "label": "locked with pen", + "order": 4366, + "tags": ["ink", "lock", "locked", "nib", "pen", "privacy"], + "unicode": "🔏" + }, + { + "group": 7, + "hexcode": "1F510", + "label": "locked with key", + "order": 4367, + "tags": ["bike", "closed", "key", "lock", "locked", "secure"], + "unicode": "🔐" + }, + { + "group": 7, + "hexcode": "1F511", + "label": "key", + "order": 4368, + "tags": ["keys", "lock", "major", "password", "unlock"], + "unicode": "🔑" + }, + { + "group": 7, + "hexcode": "1F5DD", + "label": "old key", + "order": 4370, + "tags": ["clue", "key", "lock", "old"], + "unicode": "🗝️" + }, + { + "group": 7, + "hexcode": "1F528", + "label": "hammer", + "order": 4371, + "tags": ["home", "improvement", "repairs", "tool"], + "unicode": "🔨" + }, + { + "group": 7, + "hexcode": "1FA93", + "label": "axe", + "order": 4372, + "tags": ["ax", "chop", "hatchet", "split", "wood"], + "unicode": "🪓" + }, + { + "group": 7, + "hexcode": "26CF", + "label": "pick", + "order": 4374, + "tags": ["hammer", "mining", "tool"], + "unicode": "⛏️" + }, + { + "group": 7, + "hexcode": "2692", + "label": "hammer and pick", + "order": 4376, + "tags": ["hammer", "pick", "tool"], + "unicode": "⚒️" + }, + { + "group": 7, + "hexcode": "1F6E0", + "label": "hammer and wrench", + "order": 4378, + "tags": ["hammer", "spanner", "tool", "wrench"], + "unicode": "🛠️" + }, + { + "group": 7, + "hexcode": "1F5E1", + "label": "dagger", + "order": 4380, + "tags": ["knife", "weapon"], + "unicode": "🗡️" + }, + { + "group": 7, + "hexcode": "2694", + "label": "crossed swords", + "order": 4382, + "tags": ["crossed", "swords", "weapon"], + "unicode": "⚔️" + }, + { + "group": 7, + "hexcode": "1F4A3", + "label": "bomb", + "order": 4383, + "tags": ["boom", "comic", "dangerous", "explosion", "hot"], + "unicode": "💣️" + }, + { + "group": 7, + "hexcode": "1FA83", + "label": "boomerang", + "order": 4384, + "tags": ["rebound", "repercussion", "weapon"], + "unicode": "🪃" + }, + { + "group": 7, + "hexcode": "1F3F9", + "label": "bow and arrow", + "order": 4385, + "tags": [ + "archer", + "archery", + "arrow", + "bow", + "sagittarius", + "tool", + "weapon", + "zodiac" + ], + "unicode": "🏹" + }, + { + "group": 7, + "hexcode": "1F6E1", + "label": "shield", + "order": 4387, + "tags": ["weapon"], + "unicode": "🛡️" + }, + { + "group": 7, + "hexcode": "1FA9A", + "label": "carpentry saw", + "order": 4388, + "tags": [ + "carpenter", + "carpentry", + "cut", + "lumber", + "saw", + "tool", + "trim" + ], + "unicode": "🪚" + }, + { + "group": 7, + "hexcode": "1F527", + "label": "wrench", + "order": 4389, + "tags": ["home", "improvement", "spanner", "tool"], + "unicode": "🔧" + }, + { + "group": 7, + "hexcode": "1FA9B", + "label": "screwdriver", + "order": 4390, + "tags": ["flathead", "handy", "screw", "tool"], + "unicode": "🪛" + }, + { + "group": 7, + "hexcode": "1F529", + "label": "nut and bolt", + "order": 4391, + "tags": ["bolt", "home", "improvement", "nut", "tool"], + "unicode": "🔩" + }, + { + "group": 7, + "hexcode": "2699", + "label": "gear", + "order": 4393, + "tags": ["cog", "cogwheel", "tool"], + "unicode": "⚙️" + }, + { + "group": 7, + "hexcode": "1F5DC", + "label": "clamp", + "order": 4395, + "tags": ["compress", "tool", "vice"], + "unicode": "🗜️" + }, + { + "group": 7, + "hexcode": "2696", + "label": "balance scale", + "order": 4397, + "tags": [ + "balance", + "justice", + "libra", + "scale", + "scales", + "tool", + "weight", + "zodiac" + ], + "unicode": "⚖️" + }, + { + "group": 7, + "hexcode": "1F9AF", + "label": "white cane", + "order": 4398, + "tags": ["accessibility", "blind", "cane", "probing", "white"], + "unicode": "🦯" + }, + { + "group": 7, + "hexcode": "1F517", + "label": "link", + "order": 4399, + "tags": ["links"], + "unicode": "🔗" + }, + { + "group": 7, + "hexcode": "26D3-FE0F-200D-1F4A5", + "label": "broken chain", + "order": 4400, + "tags": ["break", "breaking", "broken", "chain", "cuffs", "freedom"], + "unicode": "⛓️‍💥" + }, + { + "group": 7, + "hexcode": "26D3", + "label": "chains", + "order": 4403, + "tags": ["chain"], + "unicode": "⛓️" + }, + { + "group": 7, + "hexcode": "1FA9D", + "label": "hook", + "order": 4404, + "tags": ["catch", "crook", "curve", "ensnare", "point", "selling"], + "unicode": "🪝" + }, + { + "group": 7, + "hexcode": "1F9F0", + "label": "toolbox", + "order": 4405, + "tags": ["box", "chest", "mechanic", "red", "tool"], + "unicode": "🧰" + }, + { + "group": 7, + "hexcode": "1F9F2", + "label": "magnet", + "order": 4406, + "tags": [ + "attraction", + "horseshoe", + "magnetic", + "negative", + "positive", + "shape", + "u" + ], + "unicode": "🧲" + }, + { + "group": 7, + "hexcode": "1FA9C", + "label": "ladder", + "order": 4407, + "tags": ["climb", "rung", "step"], + "unicode": "🪜" + }, + { + "group": 7, + "hexcode": "1FA8F", + "label": "shovel", + "order": 4408, + "tags": [ + "bury", + "dig", + "garden", + "hole", + "plant", + "scoop", + "snow", + "spade" + ], + "unicode": "🪏" + }, + { + "group": 7, + "hexcode": "2697", + "label": "alembic", + "order": 4410, + "tags": ["chemistry", "tool"], + "unicode": "⚗️" + }, + { + "group": 7, + "hexcode": "1F9EA", + "label": "test tube", + "order": 4411, + "tags": [ + "chemist", + "chemistry", + "experiment", + "lab", + "science", + "test", + "tube" + ], + "unicode": "🧪" + }, + { + "group": 7, + "hexcode": "1F9EB", + "label": "petri dish", + "order": 4412, + "tags": [ + "bacteria", + "biologist", + "biology", + "culture", + "dish", + "lab", + "petri" + ], + "unicode": "🧫" + }, + { + "group": 7, + "hexcode": "1F9EC", + "label": "dna", + "order": 4413, + "tags": ["biologist", "evolution", "gene", "genetics", "life"], + "unicode": "🧬" + }, + { + "group": 7, + "hexcode": "1F52C", + "label": "microscope", + "order": 4414, + "tags": ["experiment", "lab", "science", "tool"], + "unicode": "🔬" + }, + { + "group": 7, + "hexcode": "1F52D", + "label": "telescope", + "order": 4415, + "tags": ["contact", "extraterrestrial", "science", "tool"], + "unicode": "🔭" + }, + { + "group": 7, + "hexcode": "1F4E1", + "label": "satellite antenna", + "order": 4416, + "tags": [ + "aliens", + "antenna", + "contact", + "dish", + "satellite", + "science" + ], + "unicode": "📡" + }, + { + "group": 7, + "hexcode": "1F489", + "label": "syringe", + "order": 4417, + "tags": [ + "doctor", + "flu", + "medicine", + "needle", + "shot", + "sick", + "tool", + "vaccination" + ], + "unicode": "💉" + }, + { + "group": 7, + "hexcode": "1FA78", + "label": "drop of blood", + "order": 4418, + "tags": [ + "bleed", + "blood", + "donation", + "drop", + "injury", + "medicine", + "menstruation" + ], + "unicode": "🩸" + }, + { + "group": 7, + "hexcode": "1F48A", + "label": "pill", + "order": 4419, + "tags": [ + "doctor", + "drugs", + "medicated", + "medicine", + "pills", + "sick", + "vitamin" + ], + "unicode": "💊" + }, + { + "group": 7, + "hexcode": "1FA79", + "label": "adhesive bandage", + "order": 4420, + "tags": ["adhesive", "bandage"], + "unicode": "🩹" + }, + { + "group": 7, + "hexcode": "1FA7C", + "label": "crutch", + "order": 4421, + "tags": [ + "aid", + "cane", + "disability", + "help", + "hurt", + "injured", + "mobility", + "stick" + ], + "unicode": "🩼" + }, + { + "group": 7, + "hexcode": "1FA7A", + "label": "stethoscope", + "order": 4422, + "tags": ["doctor", "heart", "medicine"], + "unicode": "🩺" + }, + { + "group": 7, + "hexcode": "1FA7B", + "label": "x-ray", + "order": 4423, + "tags": ["bones", "doctor", "medical", "skeleton", "skull", "xray"], + "unicode": "🩻" + }, + { + "group": 7, + "hexcode": "1F6AA", + "label": "door", + "order": 4424, + "tags": ["back", "closet", "front"], + "unicode": "🚪" + }, + { + "group": 7, + "hexcode": "1F6D7", + "label": "elevator", + "order": 4425, + "tags": ["accessibility", "hoist", "lift"], + "unicode": "🛗" + }, + { + "group": 7, + "hexcode": "1FA9E", + "label": "mirror", + "order": 4426, + "tags": ["makeup", "reflection", "reflector", "speculum"], + "unicode": "🪞" + }, + { + "group": 7, + "hexcode": "1FA9F", + "label": "window", + "order": 4427, + "tags": ["air", "frame", "fresh", "opening", "transparent", "view"], + "unicode": "🪟" + }, + { + "group": 7, + "hexcode": "1F6CF", + "label": "bed", + "order": 4429, + "tags": ["hotel", "sleep"], + "unicode": "🛏️" + }, + { + "group": 7, + "hexcode": "1F6CB", + "label": "couch and lamp", + "order": 4431, + "tags": ["couch", "hotel", "lamp"], + "unicode": "🛋️" + }, + { + "group": 7, + "hexcode": "1FA91", + "label": "chair", + "order": 4432, + "tags": ["seat", "sit"], + "unicode": "🪑" + }, + { + "group": 7, + "hexcode": "1F6BD", + "label": "toilet", + "order": 4433, + "tags": ["bathroom"], + "unicode": "🚽" + }, + { + "group": 7, + "hexcode": "1FAA0", + "label": "plunger", + "order": 4434, + "tags": ["cup", "force", "plumber", "poop", "suction", "toilet"], + "unicode": "🪠" + }, + { + "group": 7, + "hexcode": "1F6BF", + "label": "shower", + "order": 4435, + "tags": ["water"], + "unicode": "🚿" + }, + { + "group": 7, + "hexcode": "1F6C1", + "label": "bathtub", + "order": 4436, + "tags": ["bath"], + "unicode": "🛁" + }, + { + "group": 7, + "hexcode": "1FAA4", + "label": "mouse trap", + "order": 4437, + "tags": [ + "bait", + "cheese", + "lure", + "mouse", + "mousetrap", + "snare", + "trap" + ], + "unicode": "🪤" + }, + { + "group": 7, + "hexcode": "1FA92", + "label": "razor", + "order": 4438, + "tags": ["sharp", "shave"], + "unicode": "🪒" + }, + { + "group": 7, + "hexcode": "1F9F4", + "label": "lotion bottle", + "order": 4439, + "tags": ["bottle", "lotion", "moisturizer", "shampoo", "sunscreen"], + "unicode": "🧴" + }, + { + "group": 7, + "hexcode": "1F9F7", + "label": "safety pin", + "order": 4440, + "tags": ["diaper", "pin", "punk", "rock", "safety"], + "unicode": "🧷" + }, + { + "group": 7, + "hexcode": "1F9F9", + "label": "broom", + "order": 4441, + "tags": ["cleaning", "sweeping", "witch"], + "unicode": "🧹" + }, + { + "group": 7, + "hexcode": "1F9FA", + "label": "basket", + "order": 4442, + "tags": ["farming", "laundry", "picnic"], + "unicode": "🧺" + }, + { + "group": 7, + "hexcode": "1F9FB", + "label": "roll of paper", + "order": 4443, + "tags": ["paper", "roll", "toilet", "towels"], + "unicode": "🧻" + }, + { + "group": 7, + "hexcode": "1FAA3", + "label": "bucket", + "order": 4444, + "tags": ["cask", "pail", "vat"], + "unicode": "🪣" + }, + { + "group": 7, + "hexcode": "1F9FC", + "label": "soap", + "order": 4445, + "tags": ["bar", "bathing", "clean", "cleaning", "lather", "soapdish"], + "unicode": "🧼" + }, + { + "group": 7, + "hexcode": "1FAE7", + "label": "bubbles", + "order": 4446, + "tags": [ + "bubble", + "burp", + "clean", + "floating", + "pearl", + "soap", + "underwater" + ], + "unicode": "🫧" + }, + { + "group": 7, + "hexcode": "1FAA5", + "label": "toothbrush", + "order": 4447, + "tags": [ + "bathroom", + "brush", + "clean", + "dental", + "hygiene", + "teeth", + "toiletry" + ], + "unicode": "🪥" + }, + { + "group": 7, + "hexcode": "1F9FD", + "label": "sponge", + "order": 4448, + "tags": ["absorbing", "cleaning", "porous", "soak"], + "unicode": "🧽" + }, + { + "group": 7, + "hexcode": "1F9EF", + "label": "fire extinguisher", + "order": 4449, + "tags": ["extinguish", "extinguisher", "fire", "quench"], + "unicode": "🧯" + }, + { + "group": 7, + "hexcode": "1F6D2", + "label": "shopping cart", + "order": 4450, + "tags": ["cart", "shopping", "trolley"], + "unicode": "🛒" + }, + { + "group": 7, + "hexcode": "1F6AC", + "label": "cigarette", + "order": 4451, + "tags": ["smoking"], + "unicode": "🚬" + }, + { + "group": 7, + "hexcode": "26B0", + "label": "coffin", + "order": 4453, + "tags": ["dead", "death", "vampire"], + "unicode": "⚰️" + }, + { + "group": 7, + "hexcode": "1FAA6", + "label": "headstone", + "order": 4454, + "tags": [ + "cemetery", + "dead", + "grave", + "graveyard", + "memorial", + "rip", + "tomb", + "tombstone" + ], + "unicode": "🪦" + }, + { + "group": 7, + "hexcode": "26B1", + "label": "funeral urn", + "order": 4456, + "tags": ["ashes", "death", "funeral", "urn"], + "unicode": "⚱️" + }, + { + "group": 7, + "hexcode": "1F9FF", + "label": "nazar amulet", + "order": 4457, + "tags": [ + "amulet", + "bead", + "blue", + "charm", + "evil-eye", + "nazar", + "talisman" + ], + "unicode": "🧿" + }, + { + "group": 7, + "hexcode": "1FAAC", + "label": "hamsa", + "order": 4458, + "tags": [ + "amulet", + "fatima", + "fortune", + "guide", + "hand", + "mary", + "miriam", + "palm", + "protect", + "protection" + ], + "unicode": "🪬" + }, + { + "group": 7, + "hexcode": "1F5FF", + "label": "moai", + "order": 4459, + "tags": ["face", "moyai", "statue", "stoneface", "travel"], + "unicode": "🗿" + }, + { + "group": 7, + "hexcode": "1FAA7", + "label": "placard", + "order": 4460, + "tags": [ + "card", + "demonstration", + "notice", + "picket", + "plaque", + "protest", + "sign" + ], + "unicode": "🪧" + }, + { + "group": 7, + "hexcode": "1FAAA", + "label": "identification card", + "order": 4461, + "tags": [ + "card", + "credentials", + "document", + "id", + "identification", + "license", + "security" + ], + "unicode": "🪪" + } + ] + }, + { + "name": "Symbols", + "emojis": [ + { + "group": 8, + "hexcode": "1F3E7", + "label": "ATM sign", + "order": 4462, + "tags": ["atm", "automated", "bank", "cash", "money", "sign", "teller"], + "unicode": "🏧" + }, + { + "group": 8, + "hexcode": "1F6AE", + "label": "litter in bin sign", + "order": 4463, + "tags": ["bin", "litter", "litterbin", "sign"], + "unicode": "🚮" + }, + { + "group": 8, + "hexcode": "1F6B0", + "label": "potable water", + "order": 4464, + "tags": ["drinking", "potable", "water"], + "unicode": "🚰" + }, + { + "group": 8, + "hexcode": "267F", + "label": "wheelchair symbol", + "order": 4465, + "tags": ["access", "handicap", "symbol", "wheelchair"], + "unicode": "♿️" + }, + { + "group": 8, + "hexcode": "1F6B9", + "label": "men’s room", + "order": 4466, + "tags": [ + "bathroom", + "lavatory", + "man", + "men’s", + "restroom", + "room", + "toilet", + "wc" + ], + "unicode": "🚹️" + }, + { + "group": 8, + "hexcode": "1F6BA", + "label": "women’s room", + "order": 4467, + "tags": [ + "bathroom", + "lavatory", + "restroom", + "room", + "toilet", + "wc", + "woman", + "women’s" + ], + "unicode": "🚺️" + }, + { + "group": 8, + "hexcode": "1F6BB", + "label": "restroom", + "order": 4468, + "tags": ["bathroom", "lavatory", "toilet", "wc"], + "unicode": "🚻" + }, + { + "group": 8, + "hexcode": "1F6BC", + "label": "baby symbol", + "order": 4469, + "tags": ["baby", "changing", "symbol"], + "unicode": "🚼️" + }, + { + "group": 8, + "hexcode": "1F6BE", + "label": "water closet", + "order": 4470, + "tags": [ + "bathroom", + "closet", + "lavatory", + "restroom", + "toilet", + "water", + "wc" + ], + "unicode": "🚾" + }, + { + "group": 8, + "hexcode": "1F6C2", + "label": "passport control", + "order": 4471, + "tags": ["control", "passport"], + "unicode": "🛂" + }, + { + "group": 8, + "hexcode": "1F6C3", + "label": "customs", + "order": 4472, + "tags": ["packing"], + "unicode": "🛃" + }, + { + "group": 8, + "hexcode": "1F6C4", + "label": "baggage claim", + "order": 4473, + "tags": [ + "arrived", + "baggage", + "bags", + "case", + "checked", + "claim", + "journey", + "packing", + "plane", + "ready", + "travel", + "trip" + ], + "unicode": "🛄" + }, + { + "group": 8, + "hexcode": "1F6C5", + "label": "left luggage", + "order": 4474, + "tags": ["baggage", "case", "left", "locker", "luggage"], + "unicode": "🛅" + }, + { + "group": 8, + "hexcode": "26A0", + "label": "warning", + "order": 4476, + "tags": ["caution"], + "unicode": "⚠️" + }, + { + "group": 8, + "hexcode": "1F6B8", + "label": "children crossing", + "order": 4477, + "tags": ["child", "children", "crossing", "pedestrian", "traffic"], + "unicode": "🚸" + }, + { + "group": 8, + "hexcode": "26D4", + "label": "no entry", + "order": 4478, + "tags": [ + "do", + "entry", + "fail", + "forbidden", + "no", + "not", + "pass", + "prohibited", + "traffic" + ], + "unicode": "⛔️" + }, + { + "group": 8, + "hexcode": "1F6AB", + "label": "prohibited", + "order": 4479, + "tags": ["entry", "forbidden", "no", "not", "smoke"], + "unicode": "🚫" + }, + { + "group": 8, + "hexcode": "1F6B3", + "label": "no bicycles", + "order": 4480, + "tags": [ + "bicycle", + "bicycles", + "bike", + "forbidden", + "no", + "not", + "prohibited" + ], + "unicode": "🚳" + }, + { + "group": 8, + "hexcode": "1F6AD", + "label": "no smoking", + "order": 4481, + "tags": ["forbidden", "no", "not", "prohibited", "smoke", "smoking"], + "unicode": "🚭️" + }, + { + "group": 8, + "hexcode": "1F6AF", + "label": "no littering", + "order": 4482, + "tags": ["forbidden", "litter", "littering", "no", "not", "prohibited"], + "unicode": "🚯" + }, + { + "group": 8, + "hexcode": "1F6B1", + "label": "non-potable water", + "order": 4483, + "tags": ["dry", "non-drinking", "non-potable", "prohibited", "water"], + "unicode": "🚱" + }, + { + "group": 8, + "hexcode": "1F6B7", + "label": "no pedestrians", + "order": 4484, + "tags": [ + "forbidden", + "no", + "not", + "pedestrian", + "pedestrians", + "prohibited" + ], + "unicode": "🚷" + }, + { + "group": 8, + "hexcode": "1F4F5", + "label": "no mobile phones", + "order": 4485, + "tags": [ + "cell", + "forbidden", + "mobile", + "no", + "not", + "phone", + "phones", + "prohibited", + "telephone" + ], + "unicode": "📵" + }, + { + "group": 8, + "hexcode": "1F51E", + "label": "no one under eighteen", + "order": 4486, + "tags": [ + "18", + "age", + "eighteen", + "forbidden", + "no", + "not", + "one", + "prohibited", + "restriction", + "underage" + ], + "unicode": "🔞" + }, + { + "group": 8, + "hexcode": "2622", + "label": "radioactive", + "order": 4488, + "tags": ["sign"], + "unicode": "☢️" + }, + { + "group": 8, + "hexcode": "2623", + "label": "biohazard", + "order": 4490, + "tags": ["sign"], + "unicode": "☣️" + }, + { + "group": 8, + "hexcode": "2B06", + "label": "up arrow", + "order": 4492, + "tags": ["arrow", "cardinal", "direction", "north", "up"], + "unicode": "⬆️" + }, + { + "group": 8, + "hexcode": "2197", + "label": "up-right arrow", + "order": 4494, + "tags": [ + "arrow", + "direction", + "intercardinal", + "northeast", + "up-right" + ], + "unicode": "↗️" + }, + { + "group": 8, + "hexcode": "27A1", + "label": "right arrow", + "order": 4496, + "tags": ["arrow", "cardinal", "direction", "east", "right"], + "unicode": "➡️" + }, + { + "group": 8, + "hexcode": "2198", + "label": "down-right arrow", + "order": 4498, + "tags": [ + "arrow", + "direction", + "down-right", + "intercardinal", + "southeast" + ], + "unicode": "↘️" + }, + { + "group": 8, + "hexcode": "2B07", + "label": "down arrow", + "order": 4500, + "tags": ["arrow", "cardinal", "direction", "down", "south"], + "unicode": "⬇️" + }, + { + "group": 8, + "hexcode": "2199", + "label": "down-left arrow", + "order": 4502, + "tags": [ + "arrow", + "direction", + "down-left", + "intercardinal", + "southwest" + ], + "unicode": "↙️" + }, + { + "group": 8, + "hexcode": "2B05", + "label": "left arrow", + "order": 4504, + "tags": ["arrow", "cardinal", "direction", "left", "west"], + "unicode": "⬅️" + }, + { + "group": 8, + "hexcode": "2196", + "label": "up-left arrow", + "order": 4506, + "tags": ["arrow", "direction", "intercardinal", "northwest", "up-left"], + "unicode": "↖️" + }, + { + "group": 8, + "hexcode": "2195", + "label": "up-down arrow", + "order": 4508, + "tags": ["arrow", "up-down"], + "unicode": "↕️" + }, + { + "group": 8, + "hexcode": "2194", + "label": "left-right arrow", + "order": 4510, + "tags": ["arrow", "left-right"], + "unicode": "↔️" + }, + { + "group": 8, + "hexcode": "21A9", + "label": "right arrow curving left", + "order": 4512, + "tags": ["arrow", "curving", "left", "right"], + "unicode": "↩️" + }, + { + "group": 8, + "hexcode": "21AA", + "label": "left arrow curving right", + "order": 4514, + "tags": ["arrow", "curving", "left", "right"], + "unicode": "↪️" + }, + { + "group": 8, + "hexcode": "2934", + "label": "right arrow curving up", + "order": 4516, + "tags": ["arrow", "curving", "right", "up"], + "unicode": "⤴️" + }, + { + "group": 8, + "hexcode": "2935", + "label": "right arrow curving down", + "order": 4518, + "tags": ["arrow", "curving", "down", "right"], + "unicode": "⤵️" + }, + { + "group": 8, + "hexcode": "1F503", + "label": "clockwise vertical arrows", + "order": 4519, + "tags": [ + "arrow", + "arrows", + "clockwise", + "refresh", + "reload", + "vertical" + ], + "unicode": "🔃" + }, + { + "group": 8, + "hexcode": "1F504", + "label": "counterclockwise arrows button", + "order": 4520, + "tags": [ + "again", + "anticlockwise", + "arrow", + "arrows", + "button", + "counterclockwise", + "deja", + "refresh", + "rewindershins", + "vu" + ], + "unicode": "🔄" + }, + { + "group": 8, + "hexcode": "1F519", + "label": "BACK arrow", + "order": 4521, + "tags": ["arrow", "back"], + "unicode": "🔙" + }, + { + "group": 8, + "hexcode": "1F51A", + "label": "END arrow", + "order": 4522, + "tags": ["arrow", "end"], + "unicode": "🔚" + }, + { + "group": 8, + "hexcode": "1F51B", + "label": "ON! arrow", + "order": 4523, + "tags": ["arrow", "mark", "on!"], + "unicode": "🔛" + }, + { + "group": 8, + "hexcode": "1F51C", + "label": "SOON arrow", + "order": 4524, + "tags": ["arrow", "brb", "omw", "soon"], + "unicode": "🔜" + }, + { + "group": 8, + "hexcode": "1F51D", + "label": "TOP arrow", + "order": 4525, + "tags": ["arrow", "homie", "top", "up"], + "unicode": "🔝" + }, + { + "group": 8, + "hexcode": "1F6D0", + "label": "place of worship", + "order": 4526, + "tags": ["place", "pray", "religion", "worship"], + "unicode": "🛐" + }, + { + "group": 8, + "hexcode": "269B", + "label": "atom symbol", + "order": 4528, + "tags": ["atheist", "atom", "symbol"], + "unicode": "⚛️" + }, + { + "group": 8, + "hexcode": "1F549", + "label": "om", + "order": 4530, + "tags": ["hindu", "religion"], + "unicode": "🕉️" + }, + { + "group": 8, + "hexcode": "2721", + "label": "star of David", + "order": 4532, + "tags": ["david", "jew", "jewish", "judaism", "religion", "star"], + "unicode": "✡️" + }, + { + "group": 8, + "hexcode": "2638", + "label": "wheel of dharma", + "order": 4534, + "tags": ["buddhist", "dharma", "religion", "wheel"], + "unicode": "☸️" + }, + { + "group": 8, + "hexcode": "262F", + "label": "yin yang", + "order": 4536, + "tags": [ + "difficult", + "lives", + "religion", + "tao", + "taoist", + "total", + "yang", + "yin", + "yinyang" + ], + "unicode": "☯️" + }, + { + "group": 8, + "hexcode": "271D", + "label": "latin cross", + "order": 4538, + "tags": ["christ", "christian", "cross", "latin", "religion"], + "unicode": "✝️" + }, + { + "group": 8, + "hexcode": "2626", + "label": "orthodox cross", + "order": 4540, + "tags": ["christian", "cross", "orthodox", "religion"], + "unicode": "☦️" + }, + { + "group": 8, + "hexcode": "262A", + "label": "star and crescent", + "order": 4542, + "tags": ["crescent", "islam", "muslim", "ramadan", "religion", "star"], + "unicode": "☪️" + }, + { + "group": 8, + "hexcode": "262E", + "label": "peace symbol", + "order": 4544, + "tags": ["healing", "peace", "peaceful", "symbol"], + "unicode": "☮️" + }, + { + "group": 8, + "hexcode": "1F54E", + "label": "menorah", + "order": 4545, + "tags": [ + "candelabrum", + "candlestick", + "hanukkah", + "jewish", + "judaism", + "religion" + ], + "unicode": "🕎" + }, + { + "group": 8, + "hexcode": "1F52F", + "label": "dotted six-pointed star", + "order": 4546, + "tags": [ + "dotted", + "fortune", + "jewish", + "judaism", + "six-pointed", + "star" + ], + "unicode": "🔯" + }, + { + "group": 8, + "hexcode": "1FAAF", + "label": "khanda", + "order": 4547, + "tags": [ + "deg", + "fateh", + "khalsa", + "religion", + "sikh", + "sikhism", + "tegh" + ], + "unicode": "🪯" + }, + { + "group": 8, + "hexcode": "2648", + "label": "Aries", + "order": 4548, + "tags": ["aries", "horoscope", "ram", "zodiac"], + "unicode": "♈️" + }, + { + "group": 8, + "hexcode": "2649", + "label": "Taurus", + "order": 4549, + "tags": ["bull", "horoscope", "ox", "taurus", "zodiac"], + "unicode": "♉️" + }, + { + "group": 8, + "hexcode": "264A", + "label": "Gemini", + "order": 4550, + "tags": ["gemini", "horoscope", "twins", "zodiac"], + "unicode": "♊️" + }, + { + "group": 8, + "hexcode": "264B", + "label": "Cancer", + "order": 4551, + "tags": ["cancer", "crab", "horoscope", "zodiac"], + "unicode": "♋️" + }, + { + "group": 8, + "hexcode": "264C", + "label": "Leo", + "order": 4552, + "tags": ["horoscope", "leo", "lion", "zodiac"], + "unicode": "♌️" + }, + { + "group": 8, + "hexcode": "264D", + "label": "Virgo", + "order": 4553, + "tags": ["horoscope", "virgo", "zodiac"], + "unicode": "♍️" + }, + { + "group": 8, + "hexcode": "264E", + "label": "Libra", + "order": 4554, + "tags": [ + "balance", + "horoscope", + "justice", + "libra", + "scales", + "zodiac" + ], + "unicode": "♎️" + }, + { + "group": 8, + "hexcode": "264F", + "label": "Scorpio", + "order": 4555, + "tags": ["horoscope", "scorpio", "scorpion", "scorpius", "zodiac"], + "unicode": "♏️" + }, + { + "group": 8, + "hexcode": "2650", + "label": "Sagittarius", + "order": 4556, + "tags": ["archer", "horoscope", "sagittarius", "zodiac"], + "unicode": "♐️" + }, + { + "group": 8, + "hexcode": "2651", + "label": "Capricorn", + "order": 4557, + "tags": ["capricorn", "goat", "horoscope", "zodiac"], + "unicode": "♑️" + }, + { + "group": 8, + "hexcode": "2652", + "label": "Aquarius", + "order": 4558, + "tags": ["aquarius", "bearer", "horoscope", "water", "zodiac"], + "unicode": "♒️" + }, + { + "group": 8, + "hexcode": "2653", + "label": "Pisces", + "order": 4559, + "tags": ["fish", "horoscope", "pisces", "zodiac"], + "unicode": "♓️" + }, + { + "group": 8, + "hexcode": "26CE", + "label": "Ophiuchus", + "order": 4560, + "tags": ["bearer", "ophiuchus", "serpent", "snake", "zodiac"], + "unicode": "⛎️" + }, + { + "group": 8, + "hexcode": "1F500", + "label": "shuffle tracks button", + "order": 4561, + "tags": ["arrow", "button", "crossed", "shuffle", "tracks"], + "unicode": "🔀" + }, + { + "group": 8, + "hexcode": "1F501", + "label": "repeat button", + "order": 4562, + "tags": ["arrow", "button", "clockwise", "repeat"], + "unicode": "🔁" + }, + { + "group": 8, + "hexcode": "1F502", + "label": "repeat single button", + "order": 4563, + "tags": ["arrow", "button", "clockwise", "once", "repeat", "single"], + "unicode": "🔂" + }, + { + "group": 8, + "hexcode": "25B6", + "label": "play button", + "order": 4565, + "tags": ["arrow", "button", "play", "right", "triangle"], + "unicode": "▶️" + }, + { + "group": 8, + "hexcode": "23E9", + "label": "fast-forward button", + "order": 4566, + "tags": [ + "arrow", + "button", + "double", + "fast", + "fast-forward", + "forward" + ], + "unicode": "⏩️" + }, + { + "group": 8, + "hexcode": "23ED", + "label": "next track button", + "order": 4568, + "tags": ["arrow", "button", "next", "scene", "track", "triangle"], + "unicode": "⏭️" + }, + { + "group": 8, + "hexcode": "23EF", + "label": "play or pause button", + "order": 4570, + "tags": ["arrow", "button", "pause", "play", "right", "triangle"], + "unicode": "⏯️" + }, + { + "group": 8, + "hexcode": "25C0", + "label": "reverse button", + "order": 4572, + "tags": ["arrow", "button", "left", "reverse", "triangle"], + "unicode": "◀️" + }, + { + "group": 8, + "hexcode": "23EA", + "label": "fast reverse button", + "order": 4573, + "tags": ["arrow", "button", "double", "fast", "reverse", "rewind"], + "unicode": "⏪️" + }, + { + "group": 8, + "hexcode": "23EE", + "label": "last track button", + "order": 4575, + "tags": [ + "arrow", + "button", + "last", + "previous", + "scene", + "track", + "triangle" + ], + "unicode": "⏮️" + }, + { + "group": 8, + "hexcode": "1F53C", + "label": "upwards button", + "order": 4576, + "tags": ["arrow", "button", "red", "up", "upwards"], + "unicode": "🔼" + }, + { + "group": 8, + "hexcode": "23EB", + "label": "fast up button", + "order": 4577, + "tags": ["arrow", "button", "double", "fast", "up"], + "unicode": "⏫️" + }, + { + "group": 8, + "hexcode": "1F53D", + "label": "downwards button", + "order": 4578, + "tags": ["arrow", "button", "down", "downwards", "red"], + "unicode": "🔽" + }, + { + "group": 8, + "hexcode": "23EC", + "label": "fast down button", + "order": 4579, + "tags": ["arrow", "button", "double", "down", "fast"], + "unicode": "⏬️" + }, + { + "group": 8, + "hexcode": "23F8", + "label": "pause button", + "order": 4581, + "tags": ["bar", "button", "double", "pause", "vertical"], + "unicode": "⏸️" + }, + { + "group": 8, + "hexcode": "23F9", + "label": "stop button", + "order": 4583, + "tags": ["button", "square", "stop"], + "unicode": "⏹️" + }, + { + "group": 8, + "hexcode": "23FA", + "label": "record button", + "order": 4585, + "tags": ["button", "circle", "record"], + "unicode": "⏺️" + }, + { + "group": 8, + "hexcode": "23CF", + "label": "eject button", + "order": 4587, + "tags": ["button", "eject"], + "unicode": "⏏️" + }, + { + "group": 8, + "hexcode": "1F3A6", + "label": "cinema", + "order": 4588, + "tags": ["camera", "film", "movie"], + "unicode": "🎦" + }, + { + "group": 8, + "hexcode": "1F505", + "label": "dim button", + "order": 4589, + "tags": ["brightness", "button", "dim", "low"], + "unicode": "🔅" + }, + { + "group": 8, + "hexcode": "1F506", + "label": "bright button", + "order": 4590, + "tags": ["bright", "brightness", "button", "light"], + "unicode": "🔆" + }, + { + "group": 8, + "hexcode": "1F4F6", + "label": "antenna bars", + "order": 4591, + "tags": [ + "antenna", + "bar", + "bars", + "cell", + "communication", + "mobile", + "phone", + "signal", + "telephone" + ], + "unicode": "📶" + }, + { + "group": 8, + "hexcode": "1F6DC", + "label": "wireless", + "order": 4592, + "tags": [ + "broadband", + "computer", + "connectivity", + "hotspot", + "internet", + "network", + "router", + "smartphone", + "wi-fi", + "wifi", + "wlan" + ], + "unicode": "🛜" + }, + { + "group": 8, + "hexcode": "1F4F3", + "label": "vibration mode", + "order": 4593, + "tags": [ + "cell", + "communication", + "mobile", + "mode", + "phone", + "telephone", + "vibration" + ], + "unicode": "📳" + }, + { + "group": 8, + "hexcode": "1F4F4", + "label": "mobile phone off", + "order": 4594, + "tags": ["cell", "mobile", "off", "phone", "telephone"], + "unicode": "📴" + }, + { + "group": 8, + "hexcode": "2640", + "label": "female sign", + "order": 4596, + "tags": ["female", "sign", "woman"], + "unicode": "♀️" + }, + { + "group": 8, + "hexcode": "2642", + "label": "male sign", + "order": 4598, + "tags": ["male", "man", "sign"], + "unicode": "♂️" + }, + { + "group": 8, + "hexcode": "26A7", + "label": "transgender symbol", + "order": 4600, + "tags": ["symbol", "transgender"], + "unicode": "⚧️" + }, + { + "group": 8, + "hexcode": "2716", + "label": "multiply", + "order": 4602, + "tags": ["cancel", "multiplication", "sign", "x", "×"], + "unicode": "✖️" + }, + { + "group": 8, + "hexcode": "2795", + "label": "plus", + "order": 4603, + "tags": ["+"], + "unicode": "➕️" + }, + { + "group": 8, + "hexcode": "2796", + "label": "minus", + "order": 4604, + "tags": ["-", "heavy", "math", "sign", "−"], + "unicode": "➖️" + }, + { + "group": 8, + "hexcode": "2797", + "label": "divide", + "order": 4605, + "tags": ["division", "heavy", "math", "sign", "÷"], + "unicode": "➗️" + }, + { + "group": 8, + "hexcode": "1F7F0", + "label": "heavy equals sign", + "order": 4606, + "tags": [ + "answer", + "equal", + "equality", + "equals", + "heavy", + "math", + "sign" + ], + "unicode": "🟰" + }, + { + "group": 8, + "hexcode": "267E", + "label": "infinity", + "order": 4608, + "tags": ["forever", "unbounded", "universal"], + "unicode": "♾️" + }, + { + "group": 8, + "hexcode": "203C", + "label": "double exclamation mark", + "order": 4610, + "tags": [ + "!", + "!!", + "bangbang", + "double", + "exclamation", + "mark", + "punctuation" + ], + "unicode": "‼️" + }, + { + "group": 8, + "hexcode": "2049", + "label": "exclamation question mark", + "order": 4612, + "tags": [ + "!", + "!?", + "?", + "exclamation", + "interrobang", + "mark", + "punctuation", + "question" + ], + "unicode": "⁉️" + }, + { + "group": 8, + "hexcode": "2753", + "label": "red question mark", + "order": 4613, + "tags": ["?", "mark", "punctuation", "question", "red"], + "unicode": "❓️" + }, + { + "group": 8, + "hexcode": "2754", + "label": "white question mark", + "order": 4614, + "tags": ["?", "mark", "outlined", "punctuation", "question", "white"], + "unicode": "❔️" + }, + { + "group": 8, + "hexcode": "2755", + "label": "white exclamation mark", + "order": 4615, + "tags": [ + "!", + "exclamation", + "mark", + "outlined", + "punctuation", + "white" + ], + "unicode": "❕️" + }, + { + "group": 8, + "hexcode": "2757", + "label": "red exclamation mark", + "order": 4616, + "tags": ["!", "exclamation", "mark", "punctuation", "red"], + "unicode": "❗️" + }, + { + "group": 8, + "hexcode": "3030", + "label": "wavy dash", + "order": 4618, + "tags": ["dash", "punctuation", "wavy"], + "unicode": "〰️" + }, + { + "group": 8, + "hexcode": "1F4B1", + "label": "currency exchange", + "order": 4619, + "tags": ["bank", "currency", "exchange", "money"], + "unicode": "💱" + }, + { + "group": 8, + "hexcode": "1F4B2", + "label": "heavy dollar sign", + "order": 4620, + "tags": [ + "billion", + "cash", + "charge", + "currency", + "dollar", + "heavy", + "million", + "money", + "pay", + "sign" + ], + "unicode": "💲" + }, + { + "group": 8, + "hexcode": "2695", + "label": "medical symbol", + "order": 4622, + "tags": ["aesculapius", "medical", "medicine", "staff", "symbol"], + "unicode": "⚕️" + }, + { + "group": 8, + "hexcode": "267B", + "label": "recycling symbol", + "order": 4624, + "tags": ["recycle", "recycling", "symbol"], + "unicode": "♻️" + }, + { + "group": 8, + "hexcode": "269C", + "label": "fleur-de-lis", + "order": 4626, + "tags": ["knights"], + "unicode": "⚜️" + }, + { + "group": 8, + "hexcode": "1F531", + "label": "trident emblem", + "order": 4627, + "tags": ["anchor", "emblem", "poseidon", "ship", "tool", "trident"], + "unicode": "🔱" + }, + { + "group": 8, + "hexcode": "1F4DB", + "label": "name badge", + "order": 4628, + "tags": ["badge", "name"], + "unicode": "📛" + }, + { + "group": 8, + "hexcode": "1F530", + "label": "Japanese symbol for beginner", + "order": 4629, + "tags": [ + "beginner", + "chevron", + "green", + "japanese", + "leaf", + "symbol", + "tool", + "yellow" + ], + "unicode": "🔰" + }, + { + "group": 8, + "hexcode": "2B55", + "label": "hollow red circle", + "order": 4630, + "tags": ["circle", "heavy", "hollow", "large", "o", "red"], + "unicode": "⭕️" + }, + { + "group": 8, + "hexcode": "2705", + "label": "check mark button", + "order": 4631, + "tags": [ + "button", + "check", + "checked", + "checkmark", + "complete", + "completed", + "done", + "fixed", + "mark", + "tick", + "✓" + ], + "unicode": "✅️" + }, + { + "group": 8, + "hexcode": "2611", + "label": "check box with check", + "order": 4633, + "tags": [ + "ballot", + "box", + "check", + "checked", + "done", + "off", + "tick", + "✓" + ], + "unicode": "☑️" + }, + { + "group": 8, + "hexcode": "2714", + "label": "check mark", + "order": 4635, + "tags": [ + "check", + "checked", + "checkmark", + "done", + "heavy", + "mark", + "tick", + "✓" + ], + "unicode": "✔️" + }, + { + "group": 8, + "hexcode": "274C", + "label": "cross mark", + "order": 4636, + "tags": [ + "cancel", + "cross", + "mark", + "multiplication", + "multiply", + "x", + "×" + ], + "unicode": "❌️" + }, + { + "group": 8, + "hexcode": "274E", + "label": "cross mark button", + "order": 4637, + "tags": [ + "button", + "cross", + "mark", + "multiplication", + "multiply", + "square", + "x", + "×" + ], + "unicode": "❎️" + }, + { + "group": 8, + "hexcode": "27B0", + "label": "curly loop", + "order": 4638, + "tags": ["curl", "curly", "loop"], + "unicode": "➰️" + }, + { + "group": 8, + "hexcode": "27BF", + "label": "double curly loop", + "order": 4639, + "tags": ["curl", "curly", "double", "loop"], + "unicode": "➿️" + }, + { + "group": 8, + "hexcode": "303D", + "label": "part alternation mark", + "order": 4641, + "tags": ["alternation", "mark", "part"], + "unicode": "〽️" + }, + { + "group": 8, + "hexcode": "2733", + "label": "eight-spoked asterisk", + "order": 4643, + "tags": ["*", "asterisk", "eight-spoked"], + "unicode": "✳️" + }, + { + "group": 8, + "hexcode": "2734", + "label": "eight-pointed star", + "order": 4645, + "tags": ["*", "eight-pointed", "star"], + "unicode": "✴️" + }, + { + "group": 8, + "hexcode": "2747", + "label": "sparkle", + "order": 4647, + "tags": ["*"], + "unicode": "❇️" + }, + { + "group": 8, + "hexcode": "00A9", + "label": "copyright", + "order": 4649, + "tags": ["c"], + "unicode": "©️" + }, + { + "group": 8, + "hexcode": "00AE", + "label": "registered", + "order": 4651, + "tags": ["r"], + "unicode": "®️" + }, + { + "group": 8, + "hexcode": "2122", + "label": "trade mark", + "order": 4653, + "tags": ["mark", "tm", "trade", "trademark"], + "unicode": "™️" + }, + { + "group": 8, + "hexcode": "1FADF", + "label": "splatter", + "order": 4654, + "tags": [ + "drip", + "holi", + "ink", + "liquid", + "mess", + "paint", + "spill", + "stain" + ], + "unicode": "🫟" + }, + { + "group": 8, + "hexcode": "0023-FE0F-20E3", + "label": "keycap: #", + "order": 4655, + "tags": ["keycap"], + "unicode": "#️⃣" + }, + { + "group": 8, + "hexcode": "002A-FE0F-20E3", + "label": "keycap: *", + "order": 4657, + "tags": ["keycap"], + "unicode": "*️⃣" + }, + { + "group": 8, + "hexcode": "0030-FE0F-20E3", + "label": "keycap: 0", + "order": 4659, + "tags": ["keycap"], + "unicode": "0️⃣" + }, + { + "group": 8, + "hexcode": "0031-FE0F-20E3", + "label": "keycap: 1", + "order": 4661, + "tags": ["keycap"], + "unicode": "1️⃣" + }, + { + "group": 8, + "hexcode": "0032-FE0F-20E3", + "label": "keycap: 2", + "order": 4663, + "tags": ["keycap"], + "unicode": "2️⃣" + }, + { + "group": 8, + "hexcode": "0033-FE0F-20E3", + "label": "keycap: 3", + "order": 4665, + "tags": ["keycap"], + "unicode": "3️⃣" + }, + { + "group": 8, + "hexcode": "0034-FE0F-20E3", + "label": "keycap: 4", + "order": 4667, + "tags": ["keycap"], + "unicode": "4️⃣" + }, + { + "group": 8, + "hexcode": "0035-FE0F-20E3", + "label": "keycap: 5", + "order": 4669, + "tags": ["keycap"], + "unicode": "5️⃣" + }, + { + "group": 8, + "hexcode": "0036-FE0F-20E3", + "label": "keycap: 6", + "order": 4671, + "tags": ["keycap"], + "unicode": "6️⃣" + }, + { + "group": 8, + "hexcode": "0037-FE0F-20E3", + "label": "keycap: 7", + "order": 4673, + "tags": ["keycap"], + "unicode": "7️⃣" + }, + { + "group": 8, + "hexcode": "0038-FE0F-20E3", + "label": "keycap: 8", + "order": 4675, + "tags": ["keycap"], + "unicode": "8️⃣" + }, + { + "group": 8, + "hexcode": "0039-FE0F-20E3", + "label": "keycap: 9", + "order": 4677, + "tags": ["keycap"], + "unicode": "9️⃣" + }, + { + "group": 8, + "hexcode": "1F51F", + "label": "keycap: 10", + "order": 4679, + "tags": ["keycap"], + "unicode": "🔟" + }, + { + "group": 8, + "hexcode": "1F520", + "label": "input latin uppercase", + "order": 4680, + "tags": ["abcd", "input", "latin", "letters", "uppercase"], + "unicode": "🔠" + }, + { + "group": 8, + "hexcode": "1F521", + "label": "input latin lowercase", + "order": 4681, + "tags": ["abcd", "input", "latin", "letters", "lowercase"], + "unicode": "🔡" + }, + { + "group": 8, + "hexcode": "1F522", + "label": "input numbers", + "order": 4682, + "tags": ["1234", "input", "numbers"], + "unicode": "🔢" + }, + { + "group": 8, + "hexcode": "1F523", + "label": "input symbols", + "order": 4683, + "tags": ["%", "&", "input", "symbols", "♪", "〒"], + "unicode": "🔣" + }, + { + "group": 8, + "hexcode": "1F524", + "label": "input latin letters", + "order": 4684, + "tags": ["abc", "alphabet", "input", "latin", "letters"], + "unicode": "🔤" + }, + { + "group": 8, + "hexcode": "1F170", + "label": "A button (blood type)", + "order": 4686, + "tags": ["blood", "button", "type"], + "unicode": "🅰️" + }, + { + "group": 8, + "hexcode": "1F18E", + "label": "AB button (blood type)", + "order": 4687, + "tags": ["ab", "blood", "button", "type"], + "unicode": "🆎" + }, + { + "group": 8, + "hexcode": "1F171", + "label": "B button (blood type)", + "order": 4689, + "tags": ["b", "blood", "button", "type"], + "unicode": "🅱️" + }, + { + "group": 8, + "hexcode": "1F191", + "label": "CL button", + "order": 4690, + "tags": ["button", "cl"], + "unicode": "🆑" + }, + { + "group": 8, + "hexcode": "1F192", + "label": "COOL button", + "order": 4691, + "tags": ["button", "cool"], + "unicode": "🆒" + }, + { + "group": 8, + "hexcode": "1F193", + "label": "FREE button", + "order": 4692, + "tags": ["button", "free"], + "unicode": "🆓" + }, + { + "group": 8, + "hexcode": "2139", + "label": "information", + "order": 4694, + "tags": ["i"], + "unicode": "ℹ️" + }, + { + "group": 8, + "hexcode": "1F194", + "label": "ID button", + "order": 4695, + "tags": ["button", "id", "identity"], + "unicode": "🆔" + }, + { + "group": 8, + "hexcode": "24C2", + "label": "circled M", + "order": 4697, + "tags": ["circle", "circled", "m"], + "unicode": "Ⓜ️" + }, + { + "group": 8, + "hexcode": "1F195", + "label": "NEW button", + "order": 4698, + "tags": ["button", "new"], + "unicode": "🆕" + }, + { + "group": 8, + "hexcode": "1F196", + "label": "NG button", + "order": 4699, + "tags": ["button", "ng"], + "unicode": "🆖" + }, + { + "group": 8, + "hexcode": "1F17E", + "label": "O button (blood type)", + "order": 4701, + "tags": ["blood", "button", "o", "type"], + "unicode": "🅾️" + }, + { + "group": 8, + "hexcode": "1F197", + "label": "OK button", + "order": 4702, + "tags": ["button", "ok", "okay"], + "unicode": "🆗" + }, + { + "group": 8, + "hexcode": "1F17F", + "label": "P button", + "order": 4704, + "tags": ["button", "p", "parking"], + "unicode": "🅿️" + }, + { + "group": 8, + "hexcode": "1F198", + "label": "SOS button", + "order": 4705, + "tags": ["button", "help", "sos"], + "unicode": "🆘" + }, + { + "group": 8, + "hexcode": "1F199", + "label": "UP! button", + "order": 4706, + "tags": ["button", "mark", "up", "up!"], + "unicode": "🆙" + }, + { + "group": 8, + "hexcode": "1F19A", + "label": "VS button", + "order": 4707, + "tags": ["button", "versus", "vs"], + "unicode": "🆚" + }, + { + "group": 8, + "hexcode": "1F201", + "label": "Japanese “here” button", + "order": 4708, + "tags": ["button", "here", "japanese", "katakana"], + "unicode": "🈁" + }, + { + "group": 8, + "hexcode": "1F202", + "label": "Japanese “service charge” button", + "order": 4710, + "tags": ["button", "charge", "japanese", "katakana", "service"], + "unicode": "🈂️" + }, + { + "group": 8, + "hexcode": "1F237", + "label": "Japanese “monthly amount” button", + "order": 4712, + "tags": ["amount", "button", "ideograph", "japanese", "monthly"], + "unicode": "🈷️" + }, + { + "group": 8, + "hexcode": "1F236", + "label": "Japanese “not free of charge” button", + "order": 4713, + "tags": ["button", "charge", "free", "ideograph", "japanese", "not"], + "unicode": "🈶" + }, + { + "group": 8, + "hexcode": "1F22F", + "label": "Japanese “reserved” button", + "order": 4714, + "tags": ["button", "ideograph", "japanese", "reserved"], + "unicode": "🈯️" + }, + { + "group": 8, + "hexcode": "1F250", + "label": "Japanese “bargain” button", + "order": 4715, + "tags": ["bargain", "button", "ideograph", "japanese"], + "unicode": "🉐" + }, + { + "group": 8, + "hexcode": "1F239", + "label": "Japanese “discount” button", + "order": 4716, + "tags": ["button", "discount", "ideograph", "japanese"], + "unicode": "🈹" + }, + { + "group": 8, + "hexcode": "1F21A", + "label": "Japanese “free of charge” button", + "order": 4717, + "tags": ["button", "charge", "free", "ideograph", "japanese"], + "unicode": "🈚️" + }, + { + "group": 8, + "hexcode": "1F232", + "label": "Japanese “prohibited” button", + "order": 4718, + "tags": ["button", "ideograph", "japanese", "prohibited"], + "unicode": "🈲" + }, + { + "group": 8, + "hexcode": "1F251", + "label": "Japanese “acceptable” button", + "order": 4719, + "tags": ["acceptable", "button", "ideograph", "japanese"], + "unicode": "🉑" + }, + { + "group": 8, + "hexcode": "1F238", + "label": "Japanese “application” button", + "order": 4720, + "tags": ["application", "button", "ideograph", "japanese"], + "unicode": "🈸" + }, + { + "group": 8, + "hexcode": "1F234", + "label": "Japanese “passing grade” button", + "order": 4721, + "tags": ["button", "grade", "ideograph", "japanese", "passing"], + "unicode": "🈴" + }, + { + "group": 8, + "hexcode": "1F233", + "label": "Japanese “vacancy” button", + "order": 4722, + "tags": ["button", "ideograph", "japanese", "vacancy"], + "unicode": "🈳" + }, + { + "group": 8, + "hexcode": "3297", + "label": "Japanese “congratulations” button", + "order": 4724, + "tags": ["button", "congratulations", "ideograph", "japanese"], + "unicode": "㊗️" + }, + { + "group": 8, + "hexcode": "3299", + "label": "Japanese “secret” button", + "order": 4726, + "tags": ["button", "ideograph", "japanese", "secret"], + "unicode": "㊙️" + }, + { + "group": 8, + "hexcode": "1F23A", + "label": "Japanese “open for business” button", + "order": 4727, + "tags": ["business", "button", "ideograph", "japanese", "open"], + "unicode": "🈺" + }, + { + "group": 8, + "hexcode": "1F235", + "label": "Japanese “no vacancy” button", + "order": 4728, + "tags": ["button", "ideograph", "japanese", "no", "vacancy"], + "unicode": "🈵" + }, + { + "group": 8, + "hexcode": "1F534", + "label": "red circle", + "order": 4729, + "tags": ["circle", "geometric", "red"], + "unicode": "🔴" + }, + { + "group": 8, + "hexcode": "1F7E0", + "label": "orange circle", + "order": 4730, + "tags": ["circle", "orange"], + "unicode": "🟠" + }, + { + "group": 8, + "hexcode": "1F7E1", + "label": "yellow circle", + "order": 4731, + "tags": ["circle", "yellow"], + "unicode": "🟡" + }, + { + "group": 8, + "hexcode": "1F7E2", + "label": "green circle", + "order": 4732, + "tags": ["circle", "green"], + "unicode": "🟢" + }, + { + "group": 8, + "hexcode": "1F535", + "label": "blue circle", + "order": 4733, + "tags": ["blue", "circle", "geometric"], + "unicode": "🔵" + }, + { + "group": 8, + "hexcode": "1F7E3", + "label": "purple circle", + "order": 4734, + "tags": ["circle", "purple"], + "unicode": "🟣" + }, + { + "group": 8, + "hexcode": "1F7E4", + "label": "brown circle", + "order": 4735, + "tags": ["brown", "circle"], + "unicode": "🟤" + }, + { + "group": 8, + "hexcode": "26AB", + "label": "black circle", + "order": 4736, + "tags": ["black", "circle", "geometric"], + "unicode": "⚫️" + }, + { + "group": 8, + "hexcode": "26AA", + "label": "white circle", + "order": 4737, + "tags": ["circle", "geometric", "white"], + "unicode": "⚪️" + }, + { + "group": 8, + "hexcode": "1F7E5", + "label": "red square", + "order": 4738, + "tags": ["card", "penalty", "red", "square"], + "unicode": "🟥" + }, + { + "group": 8, + "hexcode": "1F7E7", + "label": "orange square", + "order": 4739, + "tags": ["orange", "square"], + "unicode": "🟧" + }, + { + "group": 8, + "hexcode": "1F7E8", + "label": "yellow square", + "order": 4740, + "tags": ["card", "penalty", "square", "yellow"], + "unicode": "🟨" + }, + { + "group": 8, + "hexcode": "1F7E9", + "label": "green square", + "order": 4741, + "tags": ["green", "square"], + "unicode": "🟩" + }, + { + "group": 8, + "hexcode": "1F7E6", + "label": "blue square", + "order": 4742, + "tags": ["blue", "square"], + "unicode": "🟦" + }, + { + "group": 8, + "hexcode": "1F7EA", + "label": "purple square", + "order": 4743, + "tags": ["purple", "square"], + "unicode": "🟪" + }, + { + "group": 8, + "hexcode": "1F7EB", + "label": "brown square", + "order": 4744, + "tags": ["brown", "square"], + "unicode": "🟫" + }, + { + "group": 8, + "hexcode": "2B1B", + "label": "black large square", + "order": 4745, + "tags": ["black", "geometric", "large", "square"], + "unicode": "⬛️" + }, + { + "group": 8, + "hexcode": "2B1C", + "label": "white large square", + "order": 4746, + "tags": ["geometric", "large", "square", "white"], + "unicode": "⬜️" + }, + { + "group": 8, + "hexcode": "25FC", + "label": "black medium square", + "order": 4748, + "tags": ["black", "geometric", "medium", "square"], + "unicode": "◼️" + }, + { + "group": 8, + "hexcode": "25FB", + "label": "white medium square", + "order": 4750, + "tags": ["geometric", "medium", "square", "white"], + "unicode": "◻️" + }, + { + "group": 8, + "hexcode": "25FE", + "label": "black medium-small square", + "order": 4751, + "tags": ["black", "geometric", "medium-small", "square"], + "unicode": "◾️" + }, + { + "group": 8, + "hexcode": "25FD", + "label": "white medium-small square", + "order": 4752, + "tags": ["geometric", "medium-small", "square", "white"], + "unicode": "◽️" + }, + { + "group": 8, + "hexcode": "25AA", + "label": "black small square", + "order": 4754, + "tags": ["black", "geometric", "small", "square"], + "unicode": "▪️" + }, + { + "group": 8, + "hexcode": "25AB", + "label": "white small square", + "order": 4756, + "tags": ["geometric", "small", "square", "white"], + "unicode": "▫️" + }, + { + "group": 8, + "hexcode": "1F536", + "label": "large orange diamond", + "order": 4757, + "tags": ["diamond", "geometric", "large", "orange"], + "unicode": "🔶" + }, + { + "group": 8, + "hexcode": "1F537", + "label": "large blue diamond", + "order": 4758, + "tags": ["blue", "diamond", "geometric", "large"], + "unicode": "🔷" + }, + { + "group": 8, + "hexcode": "1F538", + "label": "small orange diamond", + "order": 4759, + "tags": ["diamond", "geometric", "orange", "small"], + "unicode": "🔸" + }, + { + "group": 8, + "hexcode": "1F539", + "label": "small blue diamond", + "order": 4760, + "tags": ["blue", "diamond", "geometric", "small"], + "unicode": "🔹" + }, + { + "group": 8, + "hexcode": "1F53A", + "label": "red triangle pointed up", + "order": 4761, + "tags": ["geometric", "pointed", "red", "triangle", "up"], + "unicode": "🔺" + }, + { + "group": 8, + "hexcode": "1F53B", + "label": "red triangle pointed down", + "order": 4762, + "tags": ["down", "geometric", "pointed", "red", "triangle"], + "unicode": "🔻" + }, + { + "group": 8, + "hexcode": "1F4A0", + "label": "diamond with a dot", + "order": 4763, + "tags": ["comic", "diamond", "dot", "geometric"], + "unicode": "💠" + }, + { + "group": 8, + "hexcode": "1F518", + "label": "radio button", + "order": 4764, + "tags": ["button", "geometric", "radio"], + "unicode": "🔘" + }, + { + "group": 8, + "hexcode": "1F533", + "label": "white square button", + "order": 4765, + "tags": ["button", "geometric", "outlined", "square", "white"], + "unicode": "🔳" + }, + { + "group": 8, + "hexcode": "1F532", + "label": "black square button", + "order": 4766, + "tags": ["black", "button", "geometric", "square"], + "unicode": "🔲" + } + ] + }, + { + "name": "Flags", + "emojis": [ + { + "group": 9, + "hexcode": "1F3C1", + "label": "chequered flag", + "order": 4767, + "tags": [ + "checkered", + "chequered", + "finish", + "flag", + "flags", + "game", + "race", + "racing", + "sport", + "win" + ], + "unicode": "🏁" + }, + { + "group": 9, + "hexcode": "1F6A9", + "label": "triangular flag", + "order": 4768, + "tags": ["construction", "flag", "golf", "post", "triangular"], + "unicode": "🚩" + }, + { + "group": 9, + "hexcode": "1F38C", + "label": "crossed flags", + "order": 4769, + "tags": ["celebration", "cross", "crossed", "flags", "japanese"], + "unicode": "🎌" + }, + { + "group": 9, + "hexcode": "1F3F4", + "label": "black flag", + "order": 4770, + "tags": ["black", "flag", "waving"], + "unicode": "🏴" + }, + { + "group": 9, + "hexcode": "1F3F3", + "label": "white flag", + "order": 4772, + "tags": ["flag", "waving", "white"], + "unicode": "🏳️" + }, + { + "group": 9, + "hexcode": "1F3F3-FE0F-200D-1F308", + "label": "rainbow flag", + "order": 4773, + "tags": [ + "bisexual", + "flag", + "gay", + "genderqueer", + "glbt", + "glbtq", + "lesbian", + "lgbt", + "lgbtq", + "lgbtqia", + "pride", + "queer", + "rainbow", + "trans", + "transgender" + ], + "unicode": "🏳️‍🌈" + }, + { + "group": 9, + "hexcode": "1F3F3-FE0F-200D-26A7-FE0F", + "label": "transgender flag", + "order": 4775, + "tags": ["blue", "flag", "light", "pink", "transgender", "white"], + "unicode": "🏳️‍⚧️" + }, + { + "group": 9, + "hexcode": "1F3F4-200D-2620-FE0F", + "label": "pirate flag", + "order": 4779, + "tags": ["flag", "jolly", "pirate", "plunder", "roger", "treasure"], + "unicode": "🏴‍☠️" + }, + { + "group": 9, + "hexcode": "1F1E6-1F1E8", + "label": "flag: Ascension Island", + "order": 4781, + "tags": ["AC", "flag"], + "unicode": "🇦🇨" + }, + { + "group": 9, + "hexcode": "1F1E6-1F1E9", + "label": "flag: Andorra", + "order": 4782, + "tags": ["AD", "flag"], + "unicode": "🇦🇩" + }, + { + "group": 9, + "hexcode": "1F1E6-1F1EA", + "label": "flag: United Arab Emirates", + "order": 4783, + "tags": ["AE", "flag"], + "unicode": "🇦🇪" + }, + { + "group": 9, + "hexcode": "1F1E6-1F1EB", + "label": "flag: Afghanistan", + "order": 4784, + "tags": ["AF", "flag"], + "unicode": "🇦🇫" + }, + { + "group": 9, + "hexcode": "1F1E6-1F1EC", + "label": "flag: Antigua & Barbuda", + "order": 4785, + "tags": ["AG", "flag"], + "unicode": "🇦🇬" + }, + { + "group": 9, + "hexcode": "1F1E6-1F1EE", + "label": "flag: Anguilla", + "order": 4786, + "tags": ["AI", "flag"], + "unicode": "🇦🇮" + }, + { + "group": 9, + "hexcode": "1F1E6-1F1F1", + "label": "flag: Albania", + "order": 4787, + "tags": ["AL", "flag"], + "unicode": "🇦🇱" + }, + { + "group": 9, + "hexcode": "1F1E6-1F1F2", + "label": "flag: Armenia", + "order": 4788, + "tags": ["AM", "flag"], + "unicode": "🇦🇲" + }, + { + "group": 9, + "hexcode": "1F1E6-1F1F4", + "label": "flag: Angola", + "order": 4789, + "tags": ["AO", "flag"], + "unicode": "🇦🇴" + }, + { + "group": 9, + "hexcode": "1F1E6-1F1F6", + "label": "flag: Antarctica", + "order": 4790, + "tags": ["AQ", "flag"], + "unicode": "🇦🇶" + }, + { + "group": 9, + "hexcode": "1F1E6-1F1F7", + "label": "flag: Argentina", + "order": 4791, + "tags": ["AR", "flag"], + "unicode": "🇦🇷" + }, + { + "group": 9, + "hexcode": "1F1E6-1F1F8", + "label": "flag: American Samoa", + "order": 4792, + "tags": ["AS", "flag"], + "unicode": "🇦🇸" + }, + { + "group": 9, + "hexcode": "1F1E6-1F1F9", + "label": "flag: Austria", + "order": 4793, + "tags": ["AT", "flag"], + "unicode": "🇦🇹" + }, + { + "group": 9, + "hexcode": "1F1E6-1F1FA", + "label": "flag: Australia", + "order": 4794, + "tags": ["AU", "flag"], + "unicode": "🇦🇺" + }, + { + "group": 9, + "hexcode": "1F1E6-1F1FC", + "label": "flag: Aruba", + "order": 4795, + "tags": ["AW", "flag"], + "unicode": "🇦🇼" + }, + { + "group": 9, + "hexcode": "1F1E6-1F1FD", + "label": "flag: Åland Islands", + "order": 4796, + "tags": ["AX", "flag"], + "unicode": "🇦🇽" + }, + { + "group": 9, + "hexcode": "1F1E6-1F1FF", + "label": "flag: Azerbaijan", + "order": 4797, + "tags": ["AZ", "flag"], + "unicode": "🇦🇿" + }, + { + "group": 9, + "hexcode": "1F1E7-1F1E6", + "label": "flag: Bosnia & Herzegovina", + "order": 4798, + "tags": ["BA", "flag"], + "unicode": "🇧🇦" + }, + { + "group": 9, + "hexcode": "1F1E7-1F1E7", + "label": "flag: Barbados", + "order": 4799, + "tags": ["BB", "flag"], + "unicode": "🇧🇧" + }, + { + "group": 9, + "hexcode": "1F1E7-1F1E9", + "label": "flag: Bangladesh", + "order": 4800, + "tags": ["BD", "flag"], + "unicode": "🇧🇩" + }, + { + "group": 9, + "hexcode": "1F1E7-1F1EA", + "label": "flag: Belgium", + "order": 4801, + "tags": ["BE", "flag"], + "unicode": "🇧🇪" + }, + { + "group": 9, + "hexcode": "1F1E7-1F1EB", + "label": "flag: Burkina Faso", + "order": 4802, + "tags": ["BF", "flag"], + "unicode": "🇧🇫" + }, + { + "group": 9, + "hexcode": "1F1E7-1F1EC", + "label": "flag: Bulgaria", + "order": 4803, + "tags": ["BG", "flag"], + "unicode": "🇧🇬" + }, + { + "group": 9, + "hexcode": "1F1E7-1F1ED", + "label": "flag: Bahrain", + "order": 4804, + "tags": ["BH", "flag"], + "unicode": "🇧🇭" + }, + { + "group": 9, + "hexcode": "1F1E7-1F1EE", + "label": "flag: Burundi", + "order": 4805, + "tags": ["BI", "flag"], + "unicode": "🇧🇮" + }, + { + "group": 9, + "hexcode": "1F1E7-1F1EF", + "label": "flag: Benin", + "order": 4806, + "tags": ["BJ", "flag"], + "unicode": "🇧🇯" + }, + { + "group": 9, + "hexcode": "1F1E7-1F1F1", + "label": "flag: St. Barthélemy", + "order": 4807, + "tags": ["BL", "flag"], + "unicode": "🇧🇱" + }, + { + "group": 9, + "hexcode": "1F1E7-1F1F2", + "label": "flag: Bermuda", + "order": 4808, + "tags": ["BM", "flag"], + "unicode": "🇧🇲" + }, + { + "group": 9, + "hexcode": "1F1E7-1F1F3", + "label": "flag: Brunei", + "order": 4809, + "tags": ["BN", "flag"], + "unicode": "🇧🇳" + }, + { + "group": 9, + "hexcode": "1F1E7-1F1F4", + "label": "flag: Bolivia", + "order": 4810, + "tags": ["BO", "flag"], + "unicode": "🇧🇴" + }, + { + "group": 9, + "hexcode": "1F1E7-1F1F6", + "label": "flag: Caribbean Netherlands", + "order": 4811, + "tags": ["BQ", "flag"], + "unicode": "🇧🇶" + }, + { + "group": 9, + "hexcode": "1F1E7-1F1F7", + "label": "flag: Brazil", + "order": 4812, + "tags": ["BR", "flag"], + "unicode": "🇧🇷" + }, + { + "group": 9, + "hexcode": "1F1E7-1F1F8", + "label": "flag: Bahamas", + "order": 4813, + "tags": ["BS", "flag"], + "unicode": "🇧🇸" + }, + { + "group": 9, + "hexcode": "1F1E7-1F1F9", + "label": "flag: Bhutan", + "order": 4814, + "tags": ["BT", "flag"], + "unicode": "🇧🇹" + }, + { + "group": 9, + "hexcode": "1F1E7-1F1FB", + "label": "flag: Bouvet Island", + "order": 4815, + "tags": ["BV", "flag"], + "unicode": "🇧🇻" + }, + { + "group": 9, + "hexcode": "1F1E7-1F1FC", + "label": "flag: Botswana", + "order": 4816, + "tags": ["BW", "flag"], + "unicode": "🇧🇼" + }, + { + "group": 9, + "hexcode": "1F1E7-1F1FE", + "label": "flag: Belarus", + "order": 4817, + "tags": ["BY", "flag"], + "unicode": "🇧🇾" + }, + { + "group": 9, + "hexcode": "1F1E7-1F1FF", + "label": "flag: Belize", + "order": 4818, + "tags": ["BZ", "flag"], + "unicode": "🇧🇿" + }, + { + "group": 9, + "hexcode": "1F1E8-1F1E6", + "label": "flag: Canada", + "order": 4819, + "tags": ["CA", "flag"], + "unicode": "🇨🇦" + }, + { + "group": 9, + "hexcode": "1F1E8-1F1E8", + "label": "flag: Cocos (Keeling) Islands", + "order": 4820, + "tags": ["CC", "flag"], + "unicode": "🇨🇨" + }, + { + "group": 9, + "hexcode": "1F1E8-1F1E9", + "label": "flag: Congo - Kinshasa", + "order": 4821, + "tags": ["CD", "flag"], + "unicode": "🇨🇩" + }, + { + "group": 9, + "hexcode": "1F1E8-1F1EB", + "label": "flag: Central African Republic", + "order": 4822, + "tags": ["CF", "flag"], + "unicode": "🇨🇫" + }, + { + "group": 9, + "hexcode": "1F1E8-1F1EC", + "label": "flag: Congo - Brazzaville", + "order": 4823, + "tags": ["CG", "flag"], + "unicode": "🇨🇬" + }, + { + "group": 9, + "hexcode": "1F1E8-1F1ED", + "label": "flag: Switzerland", + "order": 4824, + "tags": ["CH", "flag"], + "unicode": "🇨🇭" + }, + { + "group": 9, + "hexcode": "1F1E8-1F1EE", + "label": "flag: Côte d’Ivoire", + "order": 4825, + "tags": ["CI", "flag"], + "unicode": "🇨🇮" + }, + { + "group": 9, + "hexcode": "1F1E8-1F1F0", + "label": "flag: Cook Islands", + "order": 4826, + "tags": ["CK", "flag"], + "unicode": "🇨🇰" + }, + { + "group": 9, + "hexcode": "1F1E8-1F1F1", + "label": "flag: Chile", + "order": 4827, + "tags": ["CL", "flag"], + "unicode": "🇨🇱" + }, + { + "group": 9, + "hexcode": "1F1E8-1F1F2", + "label": "flag: Cameroon", + "order": 4828, + "tags": ["CM", "flag"], + "unicode": "🇨🇲" + }, + { + "group": 9, + "hexcode": "1F1E8-1F1F3", + "label": "flag: China", + "order": 4829, + "tags": ["CN", "flag"], + "unicode": "🇨🇳" + }, + { + "group": 9, + "hexcode": "1F1E8-1F1F4", + "label": "flag: Colombia", + "order": 4830, + "tags": ["CO", "flag"], + "unicode": "🇨🇴" + }, + { + "group": 9, + "hexcode": "1F1E8-1F1F5", + "label": "flag: Clipperton Island", + "order": 4831, + "tags": ["CP", "flag"], + "unicode": "🇨🇵" + }, + { + "group": 9, + "hexcode": "1F1E8-1F1F6", + "label": "flag: Sark", + "order": 4832, + "tags": ["CQ", "flag"], + "unicode": "🇨🇶" + }, + { + "group": 9, + "hexcode": "1F1E8-1F1F7", + "label": "flag: Costa Rica", + "order": 4833, + "tags": ["CR", "flag"], + "unicode": "🇨🇷" + }, + { + "group": 9, + "hexcode": "1F1E8-1F1FA", + "label": "flag: Cuba", + "order": 4834, + "tags": ["CU", "flag"], + "unicode": "🇨🇺" + }, + { + "group": 9, + "hexcode": "1F1E8-1F1FB", + "label": "flag: Cape Verde", + "order": 4835, + "tags": ["CV", "flag"], + "unicode": "🇨🇻" + }, + { + "group": 9, + "hexcode": "1F1E8-1F1FC", + "label": "flag: Curaçao", + "order": 4836, + "tags": ["CW", "flag"], + "unicode": "🇨🇼" + }, + { + "group": 9, + "hexcode": "1F1E8-1F1FD", + "label": "flag: Christmas Island", + "order": 4837, + "tags": ["CX", "flag"], + "unicode": "🇨🇽" + }, + { + "group": 9, + "hexcode": "1F1E8-1F1FE", + "label": "flag: Cyprus", + "order": 4838, + "tags": ["CY", "flag"], + "unicode": "🇨🇾" + }, + { + "group": 9, + "hexcode": "1F1E8-1F1FF", + "label": "flag: Czechia", + "order": 4839, + "tags": ["CZ", "flag"], + "unicode": "🇨🇿" + }, + { + "group": 9, + "hexcode": "1F1E9-1F1EA", + "label": "flag: Germany", + "order": 4840, + "tags": ["DE", "flag"], + "unicode": "🇩🇪" + }, + { + "group": 9, + "hexcode": "1F1E9-1F1EC", + "label": "flag: Diego Garcia", + "order": 4841, + "tags": ["DG", "flag"], + "unicode": "🇩🇬" + }, + { + "group": 9, + "hexcode": "1F1E9-1F1EF", + "label": "flag: Djibouti", + "order": 4842, + "tags": ["DJ", "flag"], + "unicode": "🇩🇯" + }, + { + "group": 9, + "hexcode": "1F1E9-1F1F0", + "label": "flag: Denmark", + "order": 4843, + "tags": ["DK", "flag"], + "unicode": "🇩🇰" + }, + { + "group": 9, + "hexcode": "1F1E9-1F1F2", + "label": "flag: Dominica", + "order": 4844, + "tags": ["DM", "flag"], + "unicode": "🇩🇲" + }, + { + "group": 9, + "hexcode": "1F1E9-1F1F4", + "label": "flag: Dominican Republic", + "order": 4845, + "tags": ["DO", "flag"], + "unicode": "🇩🇴" + }, + { + "group": 9, + "hexcode": "1F1E9-1F1FF", + "label": "flag: Algeria", + "order": 4846, + "tags": ["DZ", "flag"], + "unicode": "🇩🇿" + }, + { + "group": 9, + "hexcode": "1F1EA-1F1E6", + "label": "flag: Ceuta & Melilla", + "order": 4847, + "tags": ["EA", "flag"], + "unicode": "🇪🇦" + }, + { + "group": 9, + "hexcode": "1F1EA-1F1E8", + "label": "flag: Ecuador", + "order": 4848, + "tags": ["EC", "flag"], + "unicode": "🇪🇨" + }, + { + "group": 9, + "hexcode": "1F1EA-1F1EA", + "label": "flag: Estonia", + "order": 4849, + "tags": ["EE", "flag"], + "unicode": "🇪🇪" + }, + { + "group": 9, + "hexcode": "1F1EA-1F1EC", + "label": "flag: Egypt", + "order": 4850, + "tags": ["EG", "flag"], + "unicode": "🇪🇬" + }, + { + "group": 9, + "hexcode": "1F1EA-1F1ED", + "label": "flag: Western Sahara", + "order": 4851, + "tags": ["EH", "flag"], + "unicode": "🇪🇭" + }, + { + "group": 9, + "hexcode": "1F1EA-1F1F7", + "label": "flag: Eritrea", + "order": 4852, + "tags": ["ER", "flag"], + "unicode": "🇪🇷" + }, + { + "group": 9, + "hexcode": "1F1EA-1F1F8", + "label": "flag: Spain", + "order": 4853, + "tags": ["ES", "flag"], + "unicode": "🇪🇸" + }, + { + "group": 9, + "hexcode": "1F1EA-1F1F9", + "label": "flag: Ethiopia", + "order": 4854, + "tags": ["ET", "flag"], + "unicode": "🇪🇹" + }, + { + "group": 9, + "hexcode": "1F1EA-1F1FA", + "label": "flag: European Union", + "order": 4855, + "tags": ["EU", "flag"], + "unicode": "🇪🇺" + }, + { + "group": 9, + "hexcode": "1F1EB-1F1EE", + "label": "flag: Finland", + "order": 4856, + "tags": ["FI", "flag"], + "unicode": "🇫🇮" + }, + { + "group": 9, + "hexcode": "1F1EB-1F1EF", + "label": "flag: Fiji", + "order": 4857, + "tags": ["FJ", "flag"], + "unicode": "🇫🇯" + }, + { + "group": 9, + "hexcode": "1F1EB-1F1F0", + "label": "flag: Falkland Islands", + "order": 4858, + "tags": ["FK", "flag"], + "unicode": "🇫🇰" + }, + { + "group": 9, + "hexcode": "1F1EB-1F1F2", + "label": "flag: Micronesia", + "order": 4859, + "tags": ["FM", "flag"], + "unicode": "🇫🇲" + }, + { + "group": 9, + "hexcode": "1F1EB-1F1F4", + "label": "flag: Faroe Islands", + "order": 4860, + "tags": ["FO", "flag"], + "unicode": "🇫🇴" + }, + { + "group": 9, + "hexcode": "1F1EB-1F1F7", + "label": "flag: France", + "order": 4861, + "tags": ["FR", "flag"], + "unicode": "🇫🇷" + }, + { + "group": 9, + "hexcode": "1F1EC-1F1E6", + "label": "flag: Gabon", + "order": 4862, + "tags": ["GA", "flag"], + "unicode": "🇬🇦" + }, + { + "group": 9, + "hexcode": "1F1EC-1F1E7", + "label": "flag: United Kingdom", + "order": 4863, + "tags": ["GB", "flag"], + "unicode": "🇬🇧" + }, + { + "group": 9, + "hexcode": "1F1EC-1F1E9", + "label": "flag: Grenada", + "order": 4864, + "tags": ["GD", "flag"], + "unicode": "🇬🇩" + }, + { + "group": 9, + "hexcode": "1F1EC-1F1EA", + "label": "flag: Georgia", + "order": 4865, + "tags": ["GE", "flag"], + "unicode": "🇬🇪" + }, + { + "group": 9, + "hexcode": "1F1EC-1F1EB", + "label": "flag: French Guiana", + "order": 4866, + "tags": ["GF", "flag"], + "unicode": "🇬🇫" + }, + { + "group": 9, + "hexcode": "1F1EC-1F1EC", + "label": "flag: Guernsey", + "order": 4867, + "tags": ["GG", "flag"], + "unicode": "🇬🇬" + }, + { + "group": 9, + "hexcode": "1F1EC-1F1ED", + "label": "flag: Ghana", + "order": 4868, + "tags": ["GH", "flag"], + "unicode": "🇬🇭" + }, + { + "group": 9, + "hexcode": "1F1EC-1F1EE", + "label": "flag: Gibraltar", + "order": 4869, + "tags": ["GI", "flag"], + "unicode": "🇬🇮" + }, + { + "group": 9, + "hexcode": "1F1EC-1F1F1", + "label": "flag: Greenland", + "order": 4870, + "tags": ["GL", "flag"], + "unicode": "🇬🇱" + }, + { + "group": 9, + "hexcode": "1F1EC-1F1F2", + "label": "flag: Gambia", + "order": 4871, + "tags": ["GM", "flag"], + "unicode": "🇬🇲" + }, + { + "group": 9, + "hexcode": "1F1EC-1F1F3", + "label": "flag: Guinea", + "order": 4872, + "tags": ["GN", "flag"], + "unicode": "🇬🇳" + }, + { + "group": 9, + "hexcode": "1F1EC-1F1F5", + "label": "flag: Guadeloupe", + "order": 4873, + "tags": ["GP", "flag"], + "unicode": "🇬🇵" + }, + { + "group": 9, + "hexcode": "1F1EC-1F1F6", + "label": "flag: Equatorial Guinea", + "order": 4874, + "tags": ["GQ", "flag"], + "unicode": "🇬🇶" + }, + { + "group": 9, + "hexcode": "1F1EC-1F1F7", + "label": "flag: Greece", + "order": 4875, + "tags": ["GR", "flag"], + "unicode": "🇬🇷" + }, + { + "group": 9, + "hexcode": "1F1EC-1F1F8", + "label": "flag: South Georgia & South Sandwich Islands", + "order": 4876, + "tags": ["GS", "flag"], + "unicode": "🇬🇸" + }, + { + "group": 9, + "hexcode": "1F1EC-1F1F9", + "label": "flag: Guatemala", + "order": 4877, + "tags": ["GT", "flag"], + "unicode": "🇬🇹" + }, + { + "group": 9, + "hexcode": "1F1EC-1F1FA", + "label": "flag: Guam", + "order": 4878, + "tags": ["GU", "flag"], + "unicode": "🇬🇺" + }, + { + "group": 9, + "hexcode": "1F1EC-1F1FC", + "label": "flag: Guinea-Bissau", + "order": 4879, + "tags": ["GW", "flag"], + "unicode": "🇬🇼" + }, + { + "group": 9, + "hexcode": "1F1EC-1F1FE", + "label": "flag: Guyana", + "order": 4880, + "tags": ["GY", "flag"], + "unicode": "🇬🇾" + }, + { + "group": 9, + "hexcode": "1F1ED-1F1F0", + "label": "flag: Hong Kong SAR China", + "order": 4881, + "tags": ["HK", "flag"], + "unicode": "🇭🇰" + }, + { + "group": 9, + "hexcode": "1F1ED-1F1F2", + "label": "flag: Heard & McDonald Islands", + "order": 4882, + "tags": ["HM", "flag"], + "unicode": "🇭🇲" + }, + { + "group": 9, + "hexcode": "1F1ED-1F1F3", + "label": "flag: Honduras", + "order": 4883, + "tags": ["HN", "flag"], + "unicode": "🇭🇳" + }, + { + "group": 9, + "hexcode": "1F1ED-1F1F7", + "label": "flag: Croatia", + "order": 4884, + "tags": ["HR", "flag"], + "unicode": "🇭🇷" + }, + { + "group": 9, + "hexcode": "1F1ED-1F1F9", + "label": "flag: Haiti", + "order": 4885, + "tags": ["HT", "flag"], + "unicode": "🇭🇹" + }, + { + "group": 9, + "hexcode": "1F1ED-1F1FA", + "label": "flag: Hungary", + "order": 4886, + "tags": ["HU", "flag"], + "unicode": "🇭🇺" + }, + { + "group": 9, + "hexcode": "1F1EE-1F1E8", + "label": "flag: Canary Islands", + "order": 4887, + "tags": ["IC", "flag"], + "unicode": "🇮🇨" + }, + { + "group": 9, + "hexcode": "1F1EE-1F1E9", + "label": "flag: Indonesia", + "order": 4888, + "tags": ["ID", "flag"], + "unicode": "🇮🇩" + }, + { + "group": 9, + "hexcode": "1F1EE-1F1EA", + "label": "flag: Ireland", + "order": 4889, + "tags": ["IE", "flag"], + "unicode": "🇮🇪" + }, + { + "group": 9, + "hexcode": "1F1EE-1F1F1", + "label": "flag: Israel", + "order": 4890, + "tags": ["IL", "flag"], + "unicode": "🇮🇱" + }, + { + "group": 9, + "hexcode": "1F1EE-1F1F2", + "label": "flag: Isle of Man", + "order": 4891, + "tags": ["IM", "flag"], + "unicode": "🇮🇲" + }, + { + "group": 9, + "hexcode": "1F1EE-1F1F3", + "label": "flag: India", + "order": 4892, + "tags": ["IN", "flag"], + "unicode": "🇮🇳" + }, + { + "group": 9, + "hexcode": "1F1EE-1F1F4", + "label": "flag: British Indian Ocean Territory", + "order": 4893, + "tags": ["IO", "flag"], + "unicode": "🇮🇴" + }, + { + "group": 9, + "hexcode": "1F1EE-1F1F6", + "label": "flag: Iraq", + "order": 4894, + "tags": ["IQ", "flag"], + "unicode": "🇮🇶" + }, + { + "group": 9, + "hexcode": "1F1EE-1F1F7", + "label": "flag: Iran", + "order": 4895, + "tags": ["IR", "flag"], + "unicode": "🇮🇷" + }, + { + "group": 9, + "hexcode": "1F1EE-1F1F8", + "label": "flag: Iceland", + "order": 4896, + "tags": ["IS", "flag"], + "unicode": "🇮🇸" + }, + { + "group": 9, + "hexcode": "1F1EE-1F1F9", + "label": "flag: Italy", + "order": 4897, + "tags": ["IT", "flag"], + "unicode": "🇮🇹" + }, + { + "group": 9, + "hexcode": "1F1EF-1F1EA", + "label": "flag: Jersey", + "order": 4898, + "tags": ["JE", "flag"], + "unicode": "🇯🇪" + }, + { + "group": 9, + "hexcode": "1F1EF-1F1F2", + "label": "flag: Jamaica", + "order": 4899, + "tags": ["JM", "flag"], + "unicode": "🇯🇲" + }, + { + "group": 9, + "hexcode": "1F1EF-1F1F4", + "label": "flag: Jordan", + "order": 4900, + "tags": ["JO", "flag"], + "unicode": "🇯🇴" + }, + { + "group": 9, + "hexcode": "1F1EF-1F1F5", + "label": "flag: Japan", + "order": 4901, + "tags": ["JP", "flag"], + "unicode": "🇯🇵" + }, + { + "group": 9, + "hexcode": "1F1F0-1F1EA", + "label": "flag: Kenya", + "order": 4902, + "tags": ["KE", "flag"], + "unicode": "🇰🇪" + }, + { + "group": 9, + "hexcode": "1F1F0-1F1EC", + "label": "flag: Kyrgyzstan", + "order": 4903, + "tags": ["KG", "flag"], + "unicode": "🇰🇬" + }, + { + "group": 9, + "hexcode": "1F1F0-1F1ED", + "label": "flag: Cambodia", + "order": 4904, + "tags": ["KH", "flag"], + "unicode": "🇰🇭" + }, + { + "group": 9, + "hexcode": "1F1F0-1F1EE", + "label": "flag: Kiribati", + "order": 4905, + "tags": ["KI", "flag"], + "unicode": "🇰🇮" + }, + { + "group": 9, + "hexcode": "1F1F0-1F1F2", + "label": "flag: Comoros", + "order": 4906, + "tags": ["KM", "flag"], + "unicode": "🇰🇲" + }, + { + "group": 9, + "hexcode": "1F1F0-1F1F3", + "label": "flag: St. Kitts & Nevis", + "order": 4907, + "tags": ["KN", "flag"], + "unicode": "🇰🇳" + }, + { + "group": 9, + "hexcode": "1F1F0-1F1F5", + "label": "flag: North Korea", + "order": 4908, + "tags": ["KP", "flag"], + "unicode": "🇰🇵" + }, + { + "group": 9, + "hexcode": "1F1F0-1F1F7", + "label": "flag: South Korea", + "order": 4909, + "tags": ["KR", "flag"], + "unicode": "🇰🇷" + }, + { + "group": 9, + "hexcode": "1F1F0-1F1FC", + "label": "flag: Kuwait", + "order": 4910, + "tags": ["KW", "flag"], + "unicode": "🇰🇼" + }, + { + "group": 9, + "hexcode": "1F1F0-1F1FE", + "label": "flag: Cayman Islands", + "order": 4911, + "tags": ["KY", "flag"], + "unicode": "🇰🇾" + }, + { + "group": 9, + "hexcode": "1F1F0-1F1FF", + "label": "flag: Kazakhstan", + "order": 4912, + "tags": ["KZ", "flag"], + "unicode": "🇰🇿" + }, + { + "group": 9, + "hexcode": "1F1F1-1F1E6", + "label": "flag: Laos", + "order": 4913, + "tags": ["LA", "flag"], + "unicode": "🇱🇦" + }, + { + "group": 9, + "hexcode": "1F1F1-1F1E7", + "label": "flag: Lebanon", + "order": 4914, + "tags": ["LB", "flag"], + "unicode": "🇱🇧" + }, + { + "group": 9, + "hexcode": "1F1F1-1F1E8", + "label": "flag: St. Lucia", + "order": 4915, + "tags": ["LC", "flag"], + "unicode": "🇱🇨" + }, + { + "group": 9, + "hexcode": "1F1F1-1F1EE", + "label": "flag: Liechtenstein", + "order": 4916, + "tags": ["LI", "flag"], + "unicode": "🇱🇮" + }, + { + "group": 9, + "hexcode": "1F1F1-1F1F0", + "label": "flag: Sri Lanka", + "order": 4917, + "tags": ["LK", "flag"], + "unicode": "🇱🇰" + }, + { + "group": 9, + "hexcode": "1F1F1-1F1F7", + "label": "flag: Liberia", + "order": 4918, + "tags": ["LR", "flag"], + "unicode": "🇱🇷" + }, + { + "group": 9, + "hexcode": "1F1F1-1F1F8", + "label": "flag: Lesotho", + "order": 4919, + "tags": ["LS", "flag"], + "unicode": "🇱🇸" + }, + { + "group": 9, + "hexcode": "1F1F1-1F1F9", + "label": "flag: Lithuania", + "order": 4920, + "tags": ["LT", "flag"], + "unicode": "🇱🇹" + }, + { + "group": 9, + "hexcode": "1F1F1-1F1FA", + "label": "flag: Luxembourg", + "order": 4921, + "tags": ["LU", "flag"], + "unicode": "🇱🇺" + }, + { + "group": 9, + "hexcode": "1F1F1-1F1FB", + "label": "flag: Latvia", + "order": 4922, + "tags": ["LV", "flag"], + "unicode": "🇱🇻" + }, + { + "group": 9, + "hexcode": "1F1F1-1F1FE", + "label": "flag: Libya", + "order": 4923, + "tags": ["LY", "flag"], + "unicode": "🇱🇾" + }, + { + "group": 9, + "hexcode": "1F1F2-1F1E6", + "label": "flag: Morocco", + "order": 4924, + "tags": ["MA", "flag"], + "unicode": "🇲🇦" + }, + { + "group": 9, + "hexcode": "1F1F2-1F1E8", + "label": "flag: Monaco", + "order": 4925, + "tags": ["MC", "flag"], + "unicode": "🇲🇨" + }, + { + "group": 9, + "hexcode": "1F1F2-1F1E9", + "label": "flag: Moldova", + "order": 4926, + "tags": ["MD", "flag"], + "unicode": "🇲🇩" + }, + { + "group": 9, + "hexcode": "1F1F2-1F1EA", + "label": "flag: Montenegro", + "order": 4927, + "tags": ["ME", "flag"], + "unicode": "🇲🇪" + }, + { + "group": 9, + "hexcode": "1F1F2-1F1EB", + "label": "flag: St. Martin", + "order": 4928, + "tags": ["MF", "flag"], + "unicode": "🇲🇫" + }, + { + "group": 9, + "hexcode": "1F1F2-1F1EC", + "label": "flag: Madagascar", + "order": 4929, + "tags": ["MG", "flag"], + "unicode": "🇲🇬" + }, + { + "group": 9, + "hexcode": "1F1F2-1F1ED", + "label": "flag: Marshall Islands", + "order": 4930, + "tags": ["MH", "flag"], + "unicode": "🇲🇭" + }, + { + "group": 9, + "hexcode": "1F1F2-1F1F0", + "label": "flag: North Macedonia", + "order": 4931, + "tags": ["MK", "flag"], + "unicode": "🇲🇰" + }, + { + "group": 9, + "hexcode": "1F1F2-1F1F1", + "label": "flag: Mali", + "order": 4932, + "tags": ["ML", "flag"], + "unicode": "🇲🇱" + }, + { + "group": 9, + "hexcode": "1F1F2-1F1F2", + "label": "flag: Myanmar (Burma)", + "order": 4933, + "tags": ["MM", "flag"], + "unicode": "🇲🇲" + }, + { + "group": 9, + "hexcode": "1F1F2-1F1F3", + "label": "flag: Mongolia", + "order": 4934, + "tags": ["MN", "flag"], + "unicode": "🇲🇳" + }, + { + "group": 9, + "hexcode": "1F1F2-1F1F4", + "label": "flag: Macao SAR China", + "order": 4935, + "tags": ["MO", "flag"], + "unicode": "🇲🇴" + }, + { + "group": 9, + "hexcode": "1F1F2-1F1F5", + "label": "flag: Northern Mariana Islands", + "order": 4936, + "tags": ["MP", "flag"], + "unicode": "🇲🇵" + }, + { + "group": 9, + "hexcode": "1F1F2-1F1F6", + "label": "flag: Martinique", + "order": 4937, + "tags": ["MQ", "flag"], + "unicode": "🇲🇶" + }, + { + "group": 9, + "hexcode": "1F1F2-1F1F7", + "label": "flag: Mauritania", + "order": 4938, + "tags": ["MR", "flag"], + "unicode": "🇲🇷" + }, + { + "group": 9, + "hexcode": "1F1F2-1F1F8", + "label": "flag: Montserrat", + "order": 4939, + "tags": ["MS", "flag"], + "unicode": "🇲🇸" + }, + { + "group": 9, + "hexcode": "1F1F2-1F1F9", + "label": "flag: Malta", + "order": 4940, + "tags": ["MT", "flag"], + "unicode": "🇲🇹" + }, + { + "group": 9, + "hexcode": "1F1F2-1F1FA", + "label": "flag: Mauritius", + "order": 4941, + "tags": ["MU", "flag"], + "unicode": "🇲🇺" + }, + { + "group": 9, + "hexcode": "1F1F2-1F1FB", + "label": "flag: Maldives", + "order": 4942, + "tags": ["MV", "flag"], + "unicode": "🇲🇻" + }, + { + "group": 9, + "hexcode": "1F1F2-1F1FC", + "label": "flag: Malawi", + "order": 4943, + "tags": ["MW", "flag"], + "unicode": "🇲🇼" + }, + { + "group": 9, + "hexcode": "1F1F2-1F1FD", + "label": "flag: Mexico", + "order": 4944, + "tags": ["MX", "flag"], + "unicode": "🇲🇽" + }, + { + "group": 9, + "hexcode": "1F1F2-1F1FE", + "label": "flag: Malaysia", + "order": 4945, + "tags": ["MY", "flag"], + "unicode": "🇲🇾" + }, + { + "group": 9, + "hexcode": "1F1F2-1F1FF", + "label": "flag: Mozambique", + "order": 4946, + "tags": ["MZ", "flag"], + "unicode": "🇲🇿" + }, + { + "group": 9, + "hexcode": "1F1F3-1F1E6", + "label": "flag: Namibia", + "order": 4947, + "tags": ["NA", "flag"], + "unicode": "🇳🇦" + }, + { + "group": 9, + "hexcode": "1F1F3-1F1E8", + "label": "flag: New Caledonia", + "order": 4948, + "tags": ["NC", "flag"], + "unicode": "🇳🇨" + }, + { + "group": 9, + "hexcode": "1F1F3-1F1EA", + "label": "flag: Niger", + "order": 4949, + "tags": ["NE", "flag"], + "unicode": "🇳🇪" + }, + { + "group": 9, + "hexcode": "1F1F3-1F1EB", + "label": "flag: Norfolk Island", + "order": 4950, + "tags": ["NF", "flag"], + "unicode": "🇳🇫" + }, + { + "group": 9, + "hexcode": "1F1F3-1F1EC", + "label": "flag: Nigeria", + "order": 4951, + "tags": ["NG", "flag"], + "unicode": "🇳🇬" + }, + { + "group": 9, + "hexcode": "1F1F3-1F1EE", + "label": "flag: Nicaragua", + "order": 4952, + "tags": ["NI", "flag"], + "unicode": "🇳🇮" + }, + { + "group": 9, + "hexcode": "1F1F3-1F1F1", + "label": "flag: Netherlands", + "order": 4953, + "tags": ["NL", "flag"], + "unicode": "🇳🇱" + }, + { + "group": 9, + "hexcode": "1F1F3-1F1F4", + "label": "flag: Norway", + "order": 4954, + "tags": ["NO", "flag"], + "unicode": "🇳🇴" + }, + { + "group": 9, + "hexcode": "1F1F3-1F1F5", + "label": "flag: Nepal", + "order": 4955, + "tags": ["NP", "flag"], + "unicode": "🇳🇵" + }, + { + "group": 9, + "hexcode": "1F1F3-1F1F7", + "label": "flag: Nauru", + "order": 4956, + "tags": ["NR", "flag"], + "unicode": "🇳🇷" + }, + { + "group": 9, + "hexcode": "1F1F3-1F1FA", + "label": "flag: Niue", + "order": 4957, + "tags": ["NU", "flag"], + "unicode": "🇳🇺" + }, + { + "group": 9, + "hexcode": "1F1F3-1F1FF", + "label": "flag: New Zealand", + "order": 4958, + "tags": ["NZ", "flag"], + "unicode": "🇳🇿" + }, + { + "group": 9, + "hexcode": "1F1F4-1F1F2", + "label": "flag: Oman", + "order": 4959, + "tags": ["OM", "flag"], + "unicode": "🇴🇲" + }, + { + "group": 9, + "hexcode": "1F1F5-1F1E6", + "label": "flag: Panama", + "order": 4960, + "tags": ["PA", "flag"], + "unicode": "🇵🇦" + }, + { + "group": 9, + "hexcode": "1F1F5-1F1EA", + "label": "flag: Peru", + "order": 4961, + "tags": ["PE", "flag"], + "unicode": "🇵🇪" + }, + { + "group": 9, + "hexcode": "1F1F5-1F1EB", + "label": "flag: French Polynesia", + "order": 4962, + "tags": ["PF", "flag"], + "unicode": "🇵🇫" + }, + { + "group": 9, + "hexcode": "1F1F5-1F1EC", + "label": "flag: Papua New Guinea", + "order": 4963, + "tags": ["PG", "flag"], + "unicode": "🇵🇬" + }, + { + "group": 9, + "hexcode": "1F1F5-1F1ED", + "label": "flag: Philippines", + "order": 4964, + "tags": ["PH", "flag"], + "unicode": "🇵🇭" + }, + { + "group": 9, + "hexcode": "1F1F5-1F1F0", + "label": "flag: Pakistan", + "order": 4965, + "tags": ["PK", "flag"], + "unicode": "🇵🇰" + }, + { + "group": 9, + "hexcode": "1F1F5-1F1F1", + "label": "flag: Poland", + "order": 4966, + "tags": ["PL", "flag"], + "unicode": "🇵🇱" + }, + { + "group": 9, + "hexcode": "1F1F5-1F1F2", + "label": "flag: St. Pierre & Miquelon", + "order": 4967, + "tags": ["PM", "flag"], + "unicode": "🇵🇲" + }, + { + "group": 9, + "hexcode": "1F1F5-1F1F3", + "label": "flag: Pitcairn Islands", + "order": 4968, + "tags": ["PN", "flag"], + "unicode": "🇵🇳" + }, + { + "group": 9, + "hexcode": "1F1F5-1F1F7", + "label": "flag: Puerto Rico", + "order": 4969, + "tags": ["PR", "flag"], + "unicode": "🇵🇷" + }, + { + "group": 9, + "hexcode": "1F1F5-1F1F8", + "label": "flag: Palestinian Territories", + "order": 4970, + "tags": ["PS", "flag"], + "unicode": "🇵🇸" + }, + { + "group": 9, + "hexcode": "1F1F5-1F1F9", + "label": "flag: Portugal", + "order": 4971, + "tags": ["PT", "flag"], + "unicode": "🇵🇹" + }, + { + "group": 9, + "hexcode": "1F1F5-1F1FC", + "label": "flag: Palau", + "order": 4972, + "tags": ["PW", "flag"], + "unicode": "🇵🇼" + }, + { + "group": 9, + "hexcode": "1F1F5-1F1FE", + "label": "flag: Paraguay", + "order": 4973, + "tags": ["PY", "flag"], + "unicode": "🇵🇾" + }, + { + "group": 9, + "hexcode": "1F1F6-1F1E6", + "label": "flag: Qatar", + "order": 4974, + "tags": ["QA", "flag"], + "unicode": "🇶🇦" + }, + { + "group": 9, + "hexcode": "1F1F7-1F1EA", + "label": "flag: Réunion", + "order": 4975, + "tags": ["RE", "flag"], + "unicode": "🇷🇪" + }, + { + "group": 9, + "hexcode": "1F1F7-1F1F4", + "label": "flag: Romania", + "order": 4976, + "tags": ["RO", "flag"], + "unicode": "🇷🇴" + }, + { + "group": 9, + "hexcode": "1F1F7-1F1F8", + "label": "flag: Serbia", + "order": 4977, + "tags": ["RS", "flag"], + "unicode": "🇷🇸" + }, + { + "group": 9, + "hexcode": "1F1F7-1F1FA", + "label": "flag: Russia", + "order": 4978, + "tags": ["RU", "flag"], + "unicode": "🇷🇺" + }, + { + "group": 9, + "hexcode": "1F1F7-1F1FC", + "label": "flag: Rwanda", + "order": 4979, + "tags": ["RW", "flag"], + "unicode": "🇷🇼" + }, + { + "group": 9, + "hexcode": "1F1F8-1F1E6", + "label": "flag: Saudi Arabia", + "order": 4980, + "tags": ["SA", "flag"], + "unicode": "🇸🇦" + }, + { + "group": 9, + "hexcode": "1F1F8-1F1E7", + "label": "flag: Solomon Islands", + "order": 4981, + "tags": ["SB", "flag"], + "unicode": "🇸🇧" + }, + { + "group": 9, + "hexcode": "1F1F8-1F1E8", + "label": "flag: Seychelles", + "order": 4982, + "tags": ["SC", "flag"], + "unicode": "🇸🇨" + }, + { + "group": 9, + "hexcode": "1F1F8-1F1E9", + "label": "flag: Sudan", + "order": 4983, + "tags": ["SD", "flag"], + "unicode": "🇸🇩" + }, + { + "group": 9, + "hexcode": "1F1F8-1F1EA", + "label": "flag: Sweden", + "order": 4984, + "tags": ["SE", "flag"], + "unicode": "🇸🇪" + }, + { + "group": 9, + "hexcode": "1F1F8-1F1EC", + "label": "flag: Singapore", + "order": 4985, + "tags": ["SG", "flag"], + "unicode": "🇸🇬" + }, + { + "group": 9, + "hexcode": "1F1F8-1F1ED", + "label": "flag: St. Helena", + "order": 4986, + "tags": ["SH", "flag"], + "unicode": "🇸🇭" + }, + { + "group": 9, + "hexcode": "1F1F8-1F1EE", + "label": "flag: Slovenia", + "order": 4987, + "tags": ["SI", "flag"], + "unicode": "🇸🇮" + }, + { + "group": 9, + "hexcode": "1F1F8-1F1EF", + "label": "flag: Svalbard & Jan Mayen", + "order": 4988, + "tags": ["SJ", "flag"], + "unicode": "🇸🇯" + }, + { + "group": 9, + "hexcode": "1F1F8-1F1F0", + "label": "flag: Slovakia", + "order": 4989, + "tags": ["SK", "flag"], + "unicode": "🇸🇰" + }, + { + "group": 9, + "hexcode": "1F1F8-1F1F1", + "label": "flag: Sierra Leone", + "order": 4990, + "tags": ["SL", "flag"], + "unicode": "🇸🇱" + }, + { + "group": 9, + "hexcode": "1F1F8-1F1F2", + "label": "flag: San Marino", + "order": 4991, + "tags": ["SM", "flag"], + "unicode": "🇸🇲" + }, + { + "group": 9, + "hexcode": "1F1F8-1F1F3", + "label": "flag: Senegal", + "order": 4992, + "tags": ["SN", "flag"], + "unicode": "🇸🇳" + }, + { + "group": 9, + "hexcode": "1F1F8-1F1F4", + "label": "flag: Somalia", + "order": 4993, + "tags": ["SO", "flag"], + "unicode": "🇸🇴" + }, + { + "group": 9, + "hexcode": "1F1F8-1F1F7", + "label": "flag: Suriname", + "order": 4994, + "tags": ["SR", "flag"], + "unicode": "🇸🇷" + }, + { + "group": 9, + "hexcode": "1F1F8-1F1F8", + "label": "flag: South Sudan", + "order": 4995, + "tags": ["SS", "flag"], + "unicode": "🇸🇸" + }, + { + "group": 9, + "hexcode": "1F1F8-1F1F9", + "label": "flag: São Tomé & Príncipe", + "order": 4996, + "tags": ["ST", "flag"], + "unicode": "🇸🇹" + }, + { + "group": 9, + "hexcode": "1F1F8-1F1FB", + "label": "flag: El Salvador", + "order": 4997, + "tags": ["SV", "flag"], + "unicode": "🇸🇻" + }, + { + "group": 9, + "hexcode": "1F1F8-1F1FD", + "label": "flag: Sint Maarten", + "order": 4998, + "tags": ["SX", "flag"], + "unicode": "🇸🇽" + }, + { + "group": 9, + "hexcode": "1F1F8-1F1FE", + "label": "flag: Syria", + "order": 4999, + "tags": ["SY", "flag"], + "unicode": "🇸🇾" + }, + { + "group": 9, + "hexcode": "1F1F8-1F1FF", + "label": "flag: Eswatini", + "order": 5000, + "tags": ["SZ", "flag"], + "unicode": "🇸🇿" + }, + { + "group": 9, + "hexcode": "1F1F9-1F1E6", + "label": "flag: Tristan da Cunha", + "order": 5001, + "tags": ["TA", "flag"], + "unicode": "🇹🇦" + }, + { + "group": 9, + "hexcode": "1F1F9-1F1E8", + "label": "flag: Turks & Caicos Islands", + "order": 5002, + "tags": ["TC", "flag"], + "unicode": "🇹🇨" + }, + { + "group": 9, + "hexcode": "1F1F9-1F1E9", + "label": "flag: Chad", + "order": 5003, + "tags": ["TD", "flag"], + "unicode": "🇹🇩" + }, + { + "group": 9, + "hexcode": "1F1F9-1F1EB", + "label": "flag: French Southern Territories", + "order": 5004, + "tags": ["TF", "flag"], + "unicode": "🇹🇫" + }, + { + "group": 9, + "hexcode": "1F1F9-1F1EC", + "label": "flag: Togo", + "order": 5005, + "tags": ["TG", "flag"], + "unicode": "🇹🇬" + }, + { + "group": 9, + "hexcode": "1F1F9-1F1ED", + "label": "flag: Thailand", + "order": 5006, + "tags": ["TH", "flag"], + "unicode": "🇹🇭" + }, + { + "group": 9, + "hexcode": "1F1F9-1F1EF", + "label": "flag: Tajikistan", + "order": 5007, + "tags": ["TJ", "flag"], + "unicode": "🇹🇯" + }, + { + "group": 9, + "hexcode": "1F1F9-1F1F0", + "label": "flag: Tokelau", + "order": 5008, + "tags": ["TK", "flag"], + "unicode": "🇹🇰" + }, + { + "group": 9, + "hexcode": "1F1F9-1F1F1", + "label": "flag: Timor-Leste", + "order": 5009, + "tags": ["TL", "flag"], + "unicode": "🇹🇱" + }, + { + "group": 9, + "hexcode": "1F1F9-1F1F2", + "label": "flag: Turkmenistan", + "order": 5010, + "tags": ["TM", "flag"], + "unicode": "🇹🇲" + }, + { + "group": 9, + "hexcode": "1F1F9-1F1F3", + "label": "flag: Tunisia", + "order": 5011, + "tags": ["TN", "flag"], + "unicode": "🇹🇳" + }, + { + "group": 9, + "hexcode": "1F1F9-1F1F4", + "label": "flag: Tonga", + "order": 5012, + "tags": ["TO", "flag"], + "unicode": "🇹🇴" + }, + { + "group": 9, + "hexcode": "1F1F9-1F1F7", + "label": "flag: Türkiye", + "order": 5013, + "tags": ["TR", "flag"], + "unicode": "🇹🇷" + }, + { + "group": 9, + "hexcode": "1F1F9-1F1F9", + "label": "flag: Trinidad & Tobago", + "order": 5014, + "tags": ["TT", "flag"], + "unicode": "🇹🇹" + }, + { + "group": 9, + "hexcode": "1F1F9-1F1FB", + "label": "flag: Tuvalu", + "order": 5015, + "tags": ["TV", "flag"], + "unicode": "🇹🇻" + }, + { + "group": 9, + "hexcode": "1F1F9-1F1FC", + "label": "flag: Taiwan", + "order": 5016, + "tags": ["TW", "flag"], + "unicode": "🇹🇼" + }, + { + "group": 9, + "hexcode": "1F1F9-1F1FF", + "label": "flag: Tanzania", + "order": 5017, + "tags": ["TZ", "flag"], + "unicode": "🇹🇿" + }, + { + "group": 9, + "hexcode": "1F1FA-1F1E6", + "label": "flag: Ukraine", + "order": 5018, + "tags": ["UA", "flag"], + "unicode": "🇺🇦" + }, + { + "group": 9, + "hexcode": "1F1FA-1F1EC", + "label": "flag: Uganda", + "order": 5019, + "tags": ["UG", "flag"], + "unicode": "🇺🇬" + }, + { + "group": 9, + "hexcode": "1F1FA-1F1F2", + "label": "flag: U.S. Outlying Islands", + "order": 5020, + "tags": ["UM", "flag"], + "unicode": "🇺🇲" + }, + { + "group": 9, + "hexcode": "1F1FA-1F1F3", + "label": "flag: United Nations", + "order": 5021, + "tags": ["UN", "flag"], + "unicode": "🇺🇳" + }, + { + "group": 9, + "hexcode": "1F1FA-1F1F8", + "label": "flag: United States", + "order": 5022, + "tags": ["US", "flag"], + "unicode": "🇺🇸" + }, + { + "group": 9, + "hexcode": "1F1FA-1F1FE", + "label": "flag: Uruguay", + "order": 5023, + "tags": ["UY", "flag"], + "unicode": "🇺🇾" + }, + { + "group": 9, + "hexcode": "1F1FA-1F1FF", + "label": "flag: Uzbekistan", + "order": 5024, + "tags": ["UZ", "flag"], + "unicode": "🇺🇿" + }, + { + "group": 9, + "hexcode": "1F1FB-1F1E6", + "label": "flag: Vatican City", + "order": 5025, + "tags": ["VA", "flag"], + "unicode": "🇻🇦" + }, + { + "group": 9, + "hexcode": "1F1FB-1F1E8", + "label": "flag: St. Vincent & Grenadines", + "order": 5026, + "tags": ["VC", "flag"], + "unicode": "🇻🇨" + }, + { + "group": 9, + "hexcode": "1F1FB-1F1EA", + "label": "flag: Venezuela", + "order": 5027, + "tags": ["VE", "flag"], + "unicode": "🇻🇪" + }, + { + "group": 9, + "hexcode": "1F1FB-1F1EC", + "label": "flag: British Virgin Islands", + "order": 5028, + "tags": ["VG", "flag"], + "unicode": "🇻🇬" + }, + { + "group": 9, + "hexcode": "1F1FB-1F1EE", + "label": "flag: U.S. Virgin Islands", + "order": 5029, + "tags": ["VI", "flag"], + "unicode": "🇻🇮" + }, + { + "group": 9, + "hexcode": "1F1FB-1F1F3", + "label": "flag: Vietnam", + "order": 5030, + "tags": ["VN", "flag"], + "unicode": "🇻🇳" + }, + { + "group": 9, + "hexcode": "1F1FB-1F1FA", + "label": "flag: Vanuatu", + "order": 5031, + "tags": ["VU", "flag"], + "unicode": "🇻🇺" + }, + { + "group": 9, + "hexcode": "1F1FC-1F1EB", + "label": "flag: Wallis & Futuna", + "order": 5032, + "tags": ["WF", "flag"], + "unicode": "🇼🇫" + }, + { + "group": 9, + "hexcode": "1F1FC-1F1F8", + "label": "flag: Samoa", + "order": 5033, + "tags": ["WS", "flag"], + "unicode": "🇼🇸" + }, + { + "group": 9, + "hexcode": "1F1FD-1F1F0", + "label": "flag: Kosovo", + "order": 5034, + "tags": ["XK", "flag"], + "unicode": "🇽🇰" + }, + { + "group": 9, + "hexcode": "1F1FE-1F1EA", + "label": "flag: Yemen", + "order": 5035, + "tags": ["YE", "flag"], + "unicode": "🇾🇪" + }, + { + "group": 9, + "hexcode": "1F1FE-1F1F9", + "label": "flag: Mayotte", + "order": 5036, + "tags": ["YT", "flag"], + "unicode": "🇾🇹" + }, + { + "group": 9, + "hexcode": "1F1FF-1F1E6", + "label": "flag: South Africa", + "order": 5037, + "tags": ["ZA", "flag"], + "unicode": "🇿🇦" + }, + { + "group": 9, + "hexcode": "1F1FF-1F1F2", + "label": "flag: Zambia", + "order": 5038, + "tags": ["ZM", "flag"], + "unicode": "🇿🇲" + }, + { + "group": 9, + "hexcode": "1F1FF-1F1FC", + "label": "flag: Zimbabwe", + "order": 5039, + "tags": ["ZW", "flag"], + "unicode": "🇿🇼" + }, + { + "group": 9, + "hexcode": "1F3F4-E0067-E0062-E0065-E006E-E0067-E007F", + "label": "flag: England", + "order": 5040, + "tags": ["flag", "gbeng"], + "unicode": "🏴󠁧󠁢󠁥󠁮󠁧󠁿" + }, + { + "group": 9, + "hexcode": "1F3F4-E0067-E0062-E0073-E0063-E0074-E007F", + "label": "flag: Scotland", + "order": 5041, + "tags": ["flag", "gbsct"], + "unicode": "🏴󠁧󠁢󠁳󠁣󠁴󠁿" + }, + { + "group": 9, + "hexcode": "1F3F4-E0067-E0062-E0077-E006C-E0073-E007F", + "label": "flag: Wales", + "order": 5042, + "tags": ["flag", "gbwls"], + "unicode": "🏴󠁧󠁢󠁷󠁬󠁳󠁿" + } + ] + } +] diff --git a/packages/frontend/component/src/ui/icon-picker/picker/emoji/emoji-picker.css.ts b/packages/frontend/component/src/ui/icon-picker/picker/emoji/emoji-picker.css.ts new file mode 100644 index 0000000000..e560f99e59 --- /dev/null +++ b/packages/frontend/component/src/ui/icon-picker/picker/emoji/emoji-picker.css.ts @@ -0,0 +1,42 @@ +import { cssVarV2 } from '@toeverything/theme/v2'; +import { style } from '@vanilla-extract/css'; + +export const skinTrigger = style({}); + +// header skin +export const skinList = style({ + display: 'flex', + flexDirection: 'row', + gap: 2, +}); +export const skinItem = style({}); + +// content +export const loadingWrapper = style({ + height: 120, + flexGrow: 1, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + fontSize: 12, + color: cssVarV2.text.secondary, +}); + +export const footer = style({ + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + padding: '8px 16px', + borderTop: `0.5px solid ${cssVarV2.layer.insideBorder.border}`, +}); + +export const footerButton = style({}); +export const footerButtonActive = style({ + backgroundColor: cssVarV2.layer.background.hoverOverlay, +}); +export const footerIcon = style({ + color: cssVarV2.icon.secondary, +}); +export const footerIconActive = style({ + color: cssVarV2.icon.primary, +}); diff --git a/packages/frontend/component/src/ui/icon-picker/picker/emoji/emoji-picker.tsx b/packages/frontend/component/src/ui/icon-picker/picker/emoji/emoji-picker.tsx new file mode 100644 index 0000000000..ace2ffddbf --- /dev/null +++ b/packages/frontend/component/src/ui/icon-picker/picker/emoji/emoji-picker.tsx @@ -0,0 +1,332 @@ +import { RecentIcon, SearchIcon } from '@blocksuite/icons/rc'; +import { cssVarV2 } from '@toeverything/theme/v2'; +import clsx from 'clsx'; +import { + memo, + startTransition, + useCallback, + useEffect, + useRef, + useState, +} from 'react'; + +import { IconButton } from '../../../button'; +import Input from '../../../input'; +import { Loading } from '../../../loading'; +import { Menu } from '../../../menu'; +import { Scrollable } from '../../../scrollbar'; +import * as pickerStyles from '../picker.css'; +import { GROUP_ICON_MAP, type GroupName, GROUPS } from './constants'; +import rawData from './data/en.json'; +// import { emojiGroupList } from './gen-data'; +import * as styles from './emoji-picker.css'; +import type { CompactEmoji } from './type'; + +type EmojiGroup = { + name: string; + emojis: Array; +}; +const emojiGroupList = rawData as EmojiGroup[]; + +const useRecentEmojis = () => { + const [recentEmojis, setRecentEmojis] = useState>([]); + + useEffect(() => { + const recentEmojis = localStorage.getItem('recentEmojis'); + setRecentEmojis(recentEmojis ? recentEmojis.split(',') : []); + }, []); + + const add = useCallback((emoji: string) => { + setRecentEmojis(prevRecentEmojis => { + const newRecentEmojis = [ + emoji, + ...prevRecentEmojis.filter(e => e !== emoji), + ].slice(0, 10); + localStorage.setItem('recentEmojis', newRecentEmojis.join(',')); + return newRecentEmojis; + }); + }, []); + + return { + recentEmojis, + add, + }; +}; + +// Memoized individual emoji button to prevent unnecessary re-renders +const EmojiButton = memo(function EmojiButton({ + emoji, + onSelect, +}: { + emoji: string; + onSelect: (emoji: string) => void; +}) { + const handleClick = useCallback(() => { + onSelect(emoji); + }, [emoji, onSelect]); + + return ( + {emoji}} + onClick={handleClick} + /> + ); +}); + +// Memoized emoji groups to prevent unnecessary re-renders +const EmojiGroups = memo(function EmojiGroups({ + onSelect, + keyword, + skin, +}: { + onSelect: (emoji: string) => void; + keyword?: string; + skin?: number; +}) { + const [groups, setGroups] = useState([]); + + const loading = !keyword && !groups.length; + + useEffect(() => { + startTransition(() => { + if (!keyword) { + setGroups(emojiGroupList); + return; + } + + setGroups( + emojiGroupList + .map(group => ({ + ...group, + emojis: group.emojis.filter(emoji => + emoji.tags?.some(tag => tag.includes(keyword.toLowerCase())) + ), + })) + .filter(group => group.emojis.length > 0) + ); + }); + }, [keyword]); + + if (loading) { + return ( +
+ + Loading emojis... +
+ ); + } + + return groups.map(group => ( +
+
+ {group.name} +
+
+ {group.emojis.map(emoji => ( + + ))} +
+
+ )); +}); + +const skinList = [ + { unicode: '👋', value: undefined }, + { unicode: '👋🏻', value: 0 }, + { unicode: '👋🏼', value: 1 }, + { unicode: '👋🏽', value: 2 }, + { unicode: '👋🏾', value: 3 }, + { unicode: '👋🏿', value: 4 }, +]; + +export const EmojiPicker = ({ + onSelect, +}: { + onSelect?: (emoji: string) => void; +}) => { + const scrollableRef = useRef(null); + + const [keyword, setKeyword] = useState(''); + const [activeGroupId, setActiveGroupId] = useState( + undefined + ); + const [skin, setSkin] = useState(undefined); + const { recentEmojis, add: addRecent } = useRecentEmojis(); + + const checkActiveGroup = useCallback(() => { + const scrollable = scrollableRef.current; + if (!scrollable) return; + + // get actual scrollable element + const viewport = scrollable.querySelector( + '[data-radix-scroll-area-viewport]' + ) as HTMLElement; + if (!viewport) return; + + const scrollTop = viewport.scrollTop; + + // find the first group that is at the top of the scrollable element + for (let i = emojiGroupList.length - 1; i >= 0; i--) { + const group = emojiGroupList[i]; + const groupElement = viewport.querySelector( + `[data-group-name="${group.name}"]` + ) as HTMLElement; + if (!groupElement) continue; + + // use offsetTop to get the position of the element relative to the scrollable element + const elementTop = groupElement.offsetTop; + + if (elementTop <= scrollTop + 50) { + setActiveGroupId(group.name); + return; + } + } + }, []); + + const jumpToGroup = useCallback((groupName: string) => { + const groupElement = scrollableRef.current?.querySelector( + `[data-group-name="${groupName}"]` + ) as HTMLElement; + if (!groupElement) return; + + setActiveGroupId(groupName); + groupElement.scrollIntoView({ behavior: 'smooth' }); + }, []); + + const handleEmojiSelect = useCallback( + (emoji: string) => { + addRecent(emoji); + onSelect?.(emoji); + }, + [addRecent, onSelect] + ); + + useEffect(() => { + checkActiveGroup(); + }, [checkActiveGroup]); + + return ( +
+
+ + +
+ } + placeholder="Filter..." + /> + + {skinList.map(skin => ( + {skin.unicode}} + onClick={() => setSkin(skin.value)} + /> + ))} + + } + > + + {skin !== undefined + ? skinList[skin + 1].unicode + : skinList[0].unicode} + + } + /> + + + + + {/* Recent */} + {recentEmojis.length ? ( +
+
+ Recent +
+
+ {recentEmojis.map(emoji => ( + + ))} +
+
+ ) : null} + + {/* Groups */} + +
+ +
+
+ {['Recent', ...GROUPS].map(group => { + const Icon = GROUP_ICON_MAP[group as GroupName] ?? RecentIcon; + const active = activeGroupId === group; + return ( + + } + className={clsx( + active ? styles.footerButtonActive : styles.footerButton + )} + onClick={() => jumpToGroup(group)} + /> + ); + })} +
+ + ); +}; diff --git a/packages/frontend/component/src/ui/icon-picker/picker/emoji/gen-data.ts b/packages/frontend/component/src/ui/icon-picker/picker/emoji/gen-data.ts new file mode 100644 index 0000000000..0f84989a0c --- /dev/null +++ b/packages/frontend/component/src/ui/icon-picker/picker/emoji/gen-data.ts @@ -0,0 +1,59 @@ +import compactEmojis from 'emojibase-data/en/compact.json'; + +import { type GroupName, GROUPS } from './constants'; +import type { CompactEmoji } from './type'; + +const emojiGroupObj = GROUPS.reduce( + (acc, name) => ({ + ...acc, + [name]: { + emojis: [] as CompactEmoji[], + }, + }), + {} as Record +); +compactEmojis.forEach((emoji: CompactEmoji) => { + const group = emoji.group; + if (group === undefined) return; + + switch (group) { + case 0: + case 1: + emojiGroupObj['Smileys & People'].emojis.push(emoji); + break; + case 2: + // 🏻🏼🏽🏾🏿🦰🦱🦳🦲 + break; + case 3: + emojiGroupObj['Animals & Nature'].emojis.push(emoji); + break; + case 4: + emojiGroupObj['Food & Drink'].emojis.push(emoji); + break; + case 5: + emojiGroupObj['Travel & Places'].emojis.push(emoji); + break; + case 6: + emojiGroupObj['Activity'].emojis.push(emoji); + break; + case 7: + emojiGroupObj['Objects'].emojis.push(emoji); + break; + case 8: + emojiGroupObj['Symbols'].emojis.push(emoji); + break; + case 9: + emojiGroupObj['Flags'].emojis.push(emoji); + break; + } +}); + +export const emojiGroupList = GROUPS.map(name => ({ + name, + emojis: emojiGroupObj[name].emojis.sort((a, b) => a.order - b.order), +})); + +// it may take 300~500ms to group and sort the emojis in runtime +// so for now we just generate the data in build time +// and save it to ./data +console.log(JSON.stringify(emojiGroupList)); diff --git a/packages/frontend/component/src/ui/icon-picker/picker/emoji/icons/activity.tsx b/packages/frontend/component/src/ui/icon-picker/picker/emoji/icons/activity.tsx new file mode 100644 index 0000000000..23d6797c21 --- /dev/null +++ b/packages/frontend/component/src/ui/icon-picker/picker/emoji/icons/activity.tsx @@ -0,0 +1,21 @@ +import type { SVGProps } from 'react'; + +export const ActivityIcon = (attrs: SVGProps) => { + return ( + + + + ); +}; diff --git a/packages/frontend/component/src/ui/icon-picker/picker/emoji/icons/animal.tsx b/packages/frontend/component/src/ui/icon-picker/picker/emoji/icons/animal.tsx new file mode 100644 index 0000000000..ae637af839 --- /dev/null +++ b/packages/frontend/component/src/ui/icon-picker/picker/emoji/icons/animal.tsx @@ -0,0 +1,21 @@ +import type { SVGProps } from 'react'; + +export const AnimalIcon = ({ ...attrs }: SVGProps) => { + return ( + + + + ); +}; diff --git a/packages/frontend/component/src/ui/icon-picker/picker/emoji/icons/flag.tsx b/packages/frontend/component/src/ui/icon-picker/picker/emoji/icons/flag.tsx new file mode 100644 index 0000000000..3c1e28049b --- /dev/null +++ b/packages/frontend/component/src/ui/icon-picker/picker/emoji/icons/flag.tsx @@ -0,0 +1,21 @@ +import type { SVGProps } from 'react'; + +export const FlagIcon = (attrs: SVGProps) => { + return ( + + + + ); +}; diff --git a/packages/frontend/component/src/ui/icon-picker/picker/emoji/icons/food.tsx b/packages/frontend/component/src/ui/icon-picker/picker/emoji/icons/food.tsx new file mode 100644 index 0000000000..84cadfceb4 --- /dev/null +++ b/packages/frontend/component/src/ui/icon-picker/picker/emoji/icons/food.tsx @@ -0,0 +1,21 @@ +import type { SVGProps } from 'react'; + +export const FoodIcon = ({ ...attrs }: SVGProps) => { + return ( + + + + ); +}; diff --git a/packages/frontend/component/src/ui/icon-picker/picker/emoji/icons/object.tsx b/packages/frontend/component/src/ui/icon-picker/picker/emoji/icons/object.tsx new file mode 100644 index 0000000000..291e4be119 --- /dev/null +++ b/packages/frontend/component/src/ui/icon-picker/picker/emoji/icons/object.tsx @@ -0,0 +1,21 @@ +import type { HTMLAttributes } from 'react'; + +export const ObjectIcon = (attrs: HTMLAttributes) => { + return ( + + + + ); +}; diff --git a/packages/frontend/component/src/ui/icon-picker/picker/emoji/icons/smile.tsx b/packages/frontend/component/src/ui/icon-picker/picker/emoji/icons/smile.tsx new file mode 100644 index 0000000000..8fdab11e9d --- /dev/null +++ b/packages/frontend/component/src/ui/icon-picker/picker/emoji/icons/smile.tsx @@ -0,0 +1,21 @@ +import type { SVGProps } from 'react'; + +export const SmileIcon = ({ ...attrs }: SVGProps) => { + return ( + + + + ); +}; diff --git a/packages/frontend/component/src/ui/icon-picker/picker/emoji/icons/symbol.tsx b/packages/frontend/component/src/ui/icon-picker/picker/emoji/icons/symbol.tsx new file mode 100644 index 0000000000..9a89f8ceb4 --- /dev/null +++ b/packages/frontend/component/src/ui/icon-picker/picker/emoji/icons/symbol.tsx @@ -0,0 +1,21 @@ +import type { HTMLAttributes } from 'react'; + +export const SymbolIcon = (attrs: HTMLAttributes) => { + return ( + + + + ); +}; diff --git a/packages/frontend/component/src/ui/icon-picker/picker/emoji/icons/travel.tsx b/packages/frontend/component/src/ui/icon-picker/picker/emoji/icons/travel.tsx new file mode 100644 index 0000000000..19b167c039 --- /dev/null +++ b/packages/frontend/component/src/ui/icon-picker/picker/emoji/icons/travel.tsx @@ -0,0 +1,21 @@ +import type { SVGProps } from 'react'; + +export const TravelIcon = (attrs: SVGProps) => { + return ( + + + + ); +}; diff --git a/packages/frontend/component/src/ui/icon-picker/picker/emoji/type.ts b/packages/frontend/component/src/ui/icon-picker/picker/emoji/type.ts new file mode 100644 index 0000000000..8668aa3ac4 --- /dev/null +++ b/packages/frontend/component/src/ui/icon-picker/picker/emoji/type.ts @@ -0,0 +1,11 @@ +export type CompactEmoji = { + label: string; + annotation?: string; + group: number; + hexcode: string; + order: number; + shortcodes: string[]; + tags: string[]; + unicode: string; + skins?: Array>; +}; diff --git a/packages/frontend/component/src/ui/icon-picker/picker/picker.css.ts b/packages/frontend/component/src/ui/icon-picker/picker/picker.css.ts new file mode 100644 index 0000000000..8c47b76c75 --- /dev/null +++ b/packages/frontend/component/src/ui/icon-picker/picker/picker.css.ts @@ -0,0 +1,69 @@ +import { cssVarV2 } from '@toeverything/theme/v2'; +import { style } from '@vanilla-extract/css'; + +export const root = style({ + height: '100%', + display: 'flex', + flexDirection: 'column', +}); + +// search + +export const searchContainer = style({ + padding: '16px 12px 8px 12px', + display: 'flex', + gap: 8, +}); + +export const searchInput = style({ + width: 0, + flexGrow: 1, + height: `32px !important`, + borderRadius: `4px !important`, + gap: `0px !important`, +}); + +// content scroll +export const scrollRoot = style({ + height: 0, + flexGrow: 1, + padding: '0px 12px', +}); + +export const scrollViewport = style({ + padding: '8px 0px', +}); + +// group +export const group = style({ + selectors: { + '&:not(:last-child)': { + marginBottom: 8, + }, + }, +}); + +export const groupName = style({ + fontSize: 12, + fontWeight: 500, + lineHeight: '18px', + color: cssVarV2.text.secondary, + height: 30, + display: 'flex', + alignItems: 'center', + padding: '0px 4px', +}); + +export const groupGrid = style({ + display: 'flex', + flexWrap: 'wrap', + justifyContent: 'space-between', + gap: 4, + + // fill the last row with empty space + '::after': { + content: '""', + flex: 'auto', + minWidth: '24px', + }, +}); diff --git a/packages/frontend/component/src/ui/icon-picker/renderer.tsx b/packages/frontend/component/src/ui/icon-picker/renderer.tsx new file mode 100644 index 0000000000..3410798565 --- /dev/null +++ b/packages/frontend/component/src/ui/icon-picker/renderer.tsx @@ -0,0 +1,29 @@ +import type { ReactNode } from 'react'; + +import { AffineIconRenderer } from './renderer/affine-icon'; +import { type IconData, IconType } from './type'; + +export const IconRenderer = ({ + data, + fallback, +}: { + data?: IconData; + fallback?: ReactNode; +}) => { + if (!data) { + return fallback ?? null; + } + + if (data.type === IconType.Emoji && data.unicode) { + return data.unicode; + } + if (data.type === IconType.AffineIcon && data.name) { + return ; + } + if (data.type === IconType.Blob) { + // Not supported yet + return null; + } + + return fallback ?? null; +}; diff --git a/packages/frontend/component/src/ui/icon-picker/renderer/affine-icon.tsx b/packages/frontend/component/src/ui/icon-picker/renderer/affine-icon.tsx new file mode 100644 index 0000000000..0e59476bdb --- /dev/null +++ b/packages/frontend/component/src/ui/icon-picker/renderer/affine-icon.tsx @@ -0,0 +1,19 @@ +import * as allIcons from '@blocksuite/icons/rc'; +import type { SVGProps } from 'react'; + +export const AffineIconRenderer = ({ + name, + ...props +}: { + name: string; +} & SVGProps) => { + const Icon = allIcons[ + `${name}Icon` as keyof typeof allIcons + ] as React.ComponentType>; + + if (!Icon) { + return null; + } + + return ; +}; diff --git a/packages/frontend/component/src/ui/icon-picker/type.ts b/packages/frontend/component/src/ui/icon-picker/type.ts new file mode 100644 index 0000000000..ba089983a7 --- /dev/null +++ b/packages/frontend/component/src/ui/icon-picker/type.ts @@ -0,0 +1,20 @@ +export enum IconType { + Emoji = 'emoji', + AffineIcon = 'affine-icon', + Blob = 'blob', +} + +export type IconData = + | { + type: IconType.Emoji; + unicode: string; + } + | { + type: IconType.AffineIcon; + name: string; + color: string; + } + | { + type: IconType.Blob; + blob: Blob; + }; diff --git a/packages/frontend/core/package.json b/packages/frontend/core/package.json index 2f1d20bcb6..7312a843a1 100644 --- a/packages/frontend/core/package.json +++ b/packages/frontend/core/package.json @@ -24,7 +24,7 @@ "@blocksuite/affine-components": "workspace:*", "@blocksuite/affine-shared": "workspace:*", "@blocksuite/global": "workspace:*", - "@blocksuite/icons": "^2.2.13", + "@blocksuite/icons": "^2.2.17", "@blocksuite/std": "workspace:*", "@dotlottie/player-component": "^2.7.12", "@emotion/cache": "^11.14.0", diff --git a/packages/frontend/core/src/blocksuite/ai/chat-panel/index.ts b/packages/frontend/core/src/blocksuite/ai/chat-panel/index.ts index c9689b017b..50ffdbbe5c 100644 --- a/packages/frontend/core/src/blocksuite/ai/chat-panel/index.ts +++ b/packages/frontend/core/src/blocksuite/ai/chat-panel/index.ts @@ -2,6 +2,8 @@ import type { AIDraftService, AIToolsConfigService, } from '@affine/core/modules/ai-button'; +import type { AIModelService } from '@affine/core/modules/ai-button/services/models'; +import type { SubscriptionService } from '@affine/core/modules/cloud'; import type { WorkspaceDialogService } from '@affine/core/modules/dialogs'; import type { FeatureFlagService } from '@affine/core/modules/feature-flag'; import type { PeekViewService } from '@affine/core/modules/peek-view'; @@ -129,6 +131,12 @@ export class ChatPanel extends SignalWatcher( @property({ attribute: false }) accessor peekViewService!: PeekViewService; + @property({ attribute: false }) + accessor subscriptionService!: SubscriptionService; + + @property({ attribute: false }) + accessor aiModelService!: AIModelService; + @state() accessor session: CopilotChatHistoryFragment | null | undefined; @@ -426,6 +434,8 @@ export class ChatPanel extends SignalWatcher( .aiDraftService=${this.aiDraftService} .aiToolsConfigService=${this.aiToolsConfigService} .peekViewService=${this.peekViewService} + .subscriptionService=${this.subscriptionService} + .aiModelService=${this.aiModelService} .onEmbeddingProgressChange=${this.onEmbeddingProgressChange} .onContextChange=${this.onContextChange} .width=${this.sidebarWidth} diff --git a/packages/frontend/core/src/blocksuite/ai/components/ai-chat-composer/ai-chat-composer.ts b/packages/frontend/core/src/blocksuite/ai/components/ai-chat-composer/ai-chat-composer.ts index 2ec606390a..c40825251d 100644 --- a/packages/frontend/core/src/blocksuite/ai/components/ai-chat-composer/ai-chat-composer.ts +++ b/packages/frontend/core/src/blocksuite/ai/components/ai-chat-composer/ai-chat-composer.ts @@ -4,6 +4,8 @@ import type { AIDraftService, AIToolsConfigService, } from '@affine/core/modules/ai-button'; +import type { AIModelService } from '@affine/core/modules/ai-button/services/models'; +import type { SubscriptionService } from '@affine/core/modules/cloud'; import type { WorkspaceDialogService } from '@affine/core/modules/dialogs'; import type { ContextEmbedStatus, @@ -141,6 +143,12 @@ export class AIChatComposer extends SignalWatcher( @property({ attribute: false }) accessor affineFeatureFlagService!: FeatureFlagService; + @property({ attribute: false }) + accessor subscriptionService!: SubscriptionService; + + @property({ attribute: false }) + accessor aiModelService!: AIModelService; + @state() accessor chips: ChatChip[] = []; @@ -189,6 +197,9 @@ export class AIChatComposer extends SignalWatcher( .affineFeatureFlagService=${this.affineFeatureFlagService} .aiDraftService=${this.aiDraftService} .aiToolsConfigService=${this.aiToolsConfigService} + .notificationService=${this.notificationService} + .subscriptionService=${this.subscriptionService} + .aiModelService=${this.aiModelService} .portalContainer=${this.portalContainer} .onChatSuccess=${this.onChatSuccess} .trackOptions=${this.trackOptions} diff --git a/packages/frontend/core/src/blocksuite/ai/components/ai-chat-content/ai-chat-content.ts b/packages/frontend/core/src/blocksuite/ai/components/ai-chat-content/ai-chat-content.ts index 2e51e465d8..bf7e40e562 100644 --- a/packages/frontend/core/src/blocksuite/ai/components/ai-chat-content/ai-chat-content.ts +++ b/packages/frontend/core/src/blocksuite/ai/components/ai-chat-content/ai-chat-content.ts @@ -3,6 +3,8 @@ import type { AIToolsConfigService, } from '@affine/core/modules/ai-button'; import type { AIDraftState } from '@affine/core/modules/ai-button/services/ai-draft'; +import type { AIModelService } from '@affine/core/modules/ai-button/services/models'; +import type { SubscriptionService } from '@affine/core/modules/cloud'; import type { WorkspaceDialogService } from '@affine/core/modules/dialogs'; import type { FeatureFlagService } from '@affine/core/modules/feature-flag'; import type { PeekViewService } from '@affine/core/modules/peek-view'; @@ -167,6 +169,9 @@ export class AIChatContent extends SignalWatcher( @property({ attribute: false }) accessor aiToolsConfigService!: AIToolsConfigService; + @property({ attribute: false }) + accessor aiModelService!: AIModelService; + @property({ attribute: false }) accessor onEmbeddingProgressChange: | ((count: Record) => void) @@ -184,6 +189,9 @@ export class AIChatContent extends SignalWatcher( @property({ attribute: false }) accessor peekViewService!: PeekViewService; + @property({ attribute: false }) + accessor subscriptionService!: SubscriptionService; + @state() accessor chatContextValue: ChatContextValue = DEFAULT_CHAT_CONTEXT_VALUE; @@ -462,6 +470,8 @@ export class AIChatContent extends SignalWatcher( .notificationService=${this.notificationService} .aiDraftService=${this.aiDraftService} .aiToolsConfigService=${this.aiToolsConfigService} + .subscriptionService=${this.subscriptionService} + .aiModelService=${this.aiModelService} .trackOptions=${{ where: 'chat-panel', control: 'chat-send', diff --git a/packages/frontend/core/src/blocksuite/ai/components/ai-chat-input/ai-chat-input.ts b/packages/frontend/core/src/blocksuite/ai/components/ai-chat-input/ai-chat-input.ts index 1a4134c0bd..51e546d427 100644 --- a/packages/frontend/core/src/blocksuite/ai/components/ai-chat-input/ai-chat-input.ts +++ b/packages/frontend/core/src/blocksuite/ai/components/ai-chat-input/ai-chat-input.ts @@ -2,12 +2,15 @@ import type { AIDraftService, AIToolsConfigService, } from '@affine/core/modules/ai-button'; +import type { AIModelService } from '@affine/core/modules/ai-button/services/models'; +import type { SubscriptionService } from '@affine/core/modules/cloud'; import type { FeatureFlagService } from '@affine/core/modules/feature-flag'; import type { CopilotChatHistoryFragment } from '@affine/graphql'; import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit'; import { unsafeCSSVar, unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme'; import type { EditorHost } from '@blocksuite/affine/std'; import { ShadowlessElement } from '@blocksuite/affine/std'; +import type { NotificationService } from '@blocksuite/affine-shared/services'; import { ArrowUpBigIcon, CloseIcon } from '@blocksuite/icons/lit'; import { css, html, nothing, type PropertyValues } from 'lit'; import { property, query, state } from 'lit/decorators.js'; @@ -324,9 +327,6 @@ export class AIChatInput extends SignalWatcher( @state() accessor focused = false; - @state() - accessor modelId: string | undefined = undefined; - @property({ attribute: false }) accessor chatContextValue!: AIChatInputContext; @@ -368,6 +368,15 @@ export class AIChatInput extends SignalWatcher( @property({ attribute: false }) accessor affineFeatureFlagService!: FeatureFlagService; + @property({ attribute: false }) + accessor notificationService!: NotificationService; + + @property({ attribute: false }) + accessor subscriptionService!: SubscriptionService; + + @property({ attribute: false }) + accessor aiModelService!: AIModelService; + @property({ attribute: false }) accessor isRootSession: boolean = true; @@ -516,14 +525,15 @@ export class AIChatInput extends SignalWatcher( ${status === 'transmitting' || status === 'loading' ? html` + + ${DownloadIcon({ width: '20', height: '20' })} + ${CopyIcon({ width: '20', height: '20' })} diff --git a/packages/frontend/core/src/blocksuite/block-suite-editor/doc-icon-picker.css.ts b/packages/frontend/core/src/blocksuite/block-suite-editor/doc-icon-picker.css.ts new file mode 100644 index 0000000000..6711032de6 --- /dev/null +++ b/packages/frontend/core/src/blocksuite/block-suite-editor/doc-icon-picker.css.ts @@ -0,0 +1,31 @@ +import { cssVarV2 } from '@toeverything/theme/v2'; +import { style } from '@vanilla-extract/css'; + +export const docIconPickerTrigger = style({ + width: 64, + height: 64, + padding: 2, + selectors: { + '&[data-icon-type="emoji"], &[data-icon-type="affine-icon"]': { + fontSize: 60, + lineHeight: 1, + }, + }, +}); + +export const placeholder = style({ + padding: '4px', +}); +export const placeholderContent = style({ + display: 'flex', + alignItems: 'center', + gap: 4, +}); +export const placeholderContentIcon = style({ + color: cssVarV2.icon.secondary, + fontSize: 16, +}); +export const placeholderContentText = style({ + color: cssVarV2.text.secondary, + fontSize: 12, +}); diff --git a/packages/frontend/core/src/blocksuite/block-suite-editor/doc-icon-picker.tsx b/packages/frontend/core/src/blocksuite/block-suite-editor/doc-icon-picker.tsx new file mode 100644 index 0000000000..4ced934af2 --- /dev/null +++ b/packages/frontend/core/src/blocksuite/block-suite-editor/doc-icon-picker.tsx @@ -0,0 +1,83 @@ +import { IconEditor, IconRenderer } from '@affine/component'; +import { ExplorerIconService } from '@affine/core/modules/explorer-icon/services/explorer-icon'; +import { useI18n } from '@affine/i18n'; +import { SmileSolidIcon } from '@blocksuite/icons/rc'; +import { useLiveData, useService } from '@toeverything/infra'; + +import * as styles from './doc-icon-picker.css'; + +const TitleContainer = ({ + children, + isPlaceholder, +}: { + children: React.ReactNode; + isPlaceholder: boolean; +}) => { + return ( +
+ {children} +
+ ); +}; + +export const DocIconPicker = ({ + docId, + readonly, +}: { + docId: string; + readonly?: boolean; +}) => { + const t = useI18n(); + const explorerIconService = useService(ExplorerIconService); + + const icon = useLiveData(explorerIconService.icon$('doc', docId)); + + const isPlaceholder = !icon?.icon; + + if (readonly) { + return isPlaceholder ? null : ( +
+ +
+ ); + } + + return ( + + { + explorerIconService.setIcon({ + where: 'doc', + id: docId, + icon: data, + }); + }} + closeAfterSelect={true} + triggerVariant="plain" + triggerClassName={ + isPlaceholder ? styles.placeholder : styles.docIconPickerTrigger + } + iconPlaceholder={ +
+ + + {t['com.affine.docIconPicker.placeholder']()} + +
+ } + /> +
+ ); +}; diff --git a/packages/frontend/core/src/blocksuite/block-suite-editor/lit-adaper.tsx b/packages/frontend/core/src/blocksuite/block-suite-editor/lit-adaper.tsx index c4b0ebb13d..23cb69b181 100644 --- a/packages/frontend/core/src/blocksuite/block-suite-editor/lit-adaper.tsx +++ b/packages/frontend/core/src/blocksuite/block-suite-editor/lit-adaper.tsx @@ -48,6 +48,7 @@ import { WorkspacePropertiesTable, } from '../../components/properties'; import { BiDirectionalLinkPanel } from './bi-directional-link-panel'; +import { DocIconPicker } from './doc-icon-picker'; import { BlocksuiteEditorJournalDocTitle } from './journal-doc-title'; import { StarterBar } from './starter-bar'; import * as styles from './styles.css'; @@ -254,6 +255,9 @@ export const BlocksuiteDocEditor = forwardRef< return ( <>
+ {!BUILD_CONFIG.isMobileEdition ? ( + + ) : null} {!isJournal ? ( ) : ( diff --git a/packages/frontend/core/src/desktop/components/navigation-panel/nodes/collection/index.tsx b/packages/frontend/core/src/desktop/components/navigation-panel/nodes/collection/index.tsx index a89cc62f22..bae6c66b08 100644 --- a/packages/frontend/core/src/desktop/components/navigation-panel/nodes/collection/index.tsx +++ b/packages/frontend/core/src/desktop/components/navigation-panel/nodes/collection/index.tsx @@ -229,6 +229,10 @@ export const NavigationPanelCollectionNode = ({ operations={finalOperations} dropEffect={handleDropEffectOnCollection} data-testid={`navigation-panel-collection-${collectionId}`} + explorerIconConfig={{ + where: 'collection', + id: collectionId, + }} > } onClick={handleShowEdit}> {t['com.affine.collection.menu.edit']()} @@ -136,7 +136,7 @@ export const useNavigationPanelCollectionNodeOperations = ( ), }, { - index: 99, + index: 102, view: ( } @@ -147,7 +147,7 @@ export const useNavigationPanelCollectionNodeOperations = ( ), }, { - index: 99, + index: 101, view: ( } @@ -160,7 +160,7 @@ export const useNavigationPanelCollectionNodeOperations = ( ), }, { - index: 99, + index: 100, view: ( } onClick={handleOpenInNewTab}> {t['com.affine.workbench.tab.page-menu-open']()} diff --git a/packages/frontend/core/src/desktop/components/navigation-panel/nodes/doc/index.tsx b/packages/frontend/core/src/desktop/components/navigation-panel/nodes/doc/index.tsx index c8800bd75a..25fa7a882f 100644 --- a/packages/frontend/core/src/desktop/components/navigation-panel/nodes/doc/index.tsx +++ b/packages/frontend/core/src/desktop/components/navigation-panel/nodes/doc/index.tsx @@ -314,6 +314,10 @@ export const NavigationPanelDocNode = ({ operations={finalOperations} dropEffect={handleDropEffectOnDoc} data-testid={`navigation-panel-doc-${docId}`} + explorerIconConfig={{ + where: 'doc', + id: docId, + }} > {appSettings.showLinkedDocInSidebar ? ( diff --git a/packages/frontend/core/src/desktop/components/navigation-panel/nodes/folder/index.tsx b/packages/frontend/core/src/desktop/components/navigation-panel/nodes/folder/index.tsx index 9387652e96..43f23c7358 100644 --- a/packages/frontend/core/src/desktop/components/navigation-panel/nodes/folder/index.tsx +++ b/packages/frontend/core/src/desktop/components/navigation-panel/nodes/folder/index.tsx @@ -811,6 +811,7 @@ const NavigationPanelFolderNodeFolder = ({ } dropEffect={handleDropEffect} data-testid={`navigation-panel-folder-${node.id}`} + explorerIconConfig={node.id ? { where: 'folder', id: node.id } : null} > {children.map(child => ( diff --git a/packages/frontend/core/src/desktop/components/navigation-panel/sections/tags/index.tsx b/packages/frontend/core/src/desktop/components/navigation-panel/sections/tags/index.tsx index 3ee2549ba4..1fccc265b2 100644 --- a/packages/frontend/core/src/desktop/components/navigation-panel/sections/tags/index.tsx +++ b/packages/frontend/core/src/desktop/components/navigation-panel/sections/tags/index.tsx @@ -1,4 +1,5 @@ import { IconButton } from '@affine/component'; +import { RenameModal } from '@affine/component/rename-modal'; import { NavigationPanelService } from '@affine/core/modules/navigation-panel'; import { TagService } from '@affine/core/modules/tag'; import { useI18n } from '@affine/i18n'; @@ -10,7 +11,6 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import { CollapsibleSection } from '../../layouts/collapsible-section'; import { NavigationPanelTagNode } from '../../nodes/tag'; import { NavigationPanelTreeRoot } from '../../tree'; -import { NavigationPanelTreeNodeRenameModal as CreateTagModal } from '../../tree/node'; import { RootEmpty } from './empty'; import * as styles from './styles.css'; @@ -62,11 +62,11 @@ export const NavigationPanelTags = () => { {creating && ( - )}
diff --git a/packages/frontend/core/src/desktop/components/navigation-panel/tree/node.tsx b/packages/frontend/core/src/desktop/components/navigation-panel/tree/node.tsx index 260ef4ba03..299bfcc828 100644 --- a/packages/frontend/core/src/desktop/components/navigation-panel/tree/node.tsx +++ b/packages/frontend/core/src/desktop/components/navigation-panel/tree/node.tsx @@ -4,19 +4,22 @@ import { type DropTargetDropEvent, type DropTargetOptions, type DropTargetTreeInstruction, + IconAndNameEditorMenu, IconButton, + type IconData, + IconRenderer, Menu, MenuItem, useDraggable, useDropTarget, } from '@affine/component'; -import { RenameModal } from '@affine/component/rename-modal'; import { Guard } from '@affine/core/components/guard'; import { AppSidebarService } from '@affine/core/modules/app-sidebar'; +import { ExplorerIconService } from '@affine/core/modules/explorer-icon/services/explorer-icon'; +import type { ExplorerType } from '@affine/core/modules/explorer-icon/store/explorer-icon'; import type { DocPermissionActions } from '@affine/core/modules/permissions'; import { WorkbenchLink } from '@affine/core/modules/workbench'; import type { AffineDNDData } from '@affine/core/types/dnd'; -import { extractEmojiIcon } from '@affine/core/utils'; import { useI18n } from '@affine/i18n'; import { ArrowDownSmallIcon, @@ -83,6 +86,10 @@ export interface BaseNavigationPanelTreeNodeProps { [key: `data-${string}`]: any; } +type ExplorerIconConfig = { + where: ExplorerType; + id: string; +}; interface WebNavigationPanelTreeNodeProps extends BaseNavigationPanelTreeNodeProps { renameable?: boolean; @@ -90,6 +97,8 @@ interface WebNavigationPanelTreeNodeProps renameableGuard?: { docId: string; action: DocPermissionActions }; defaultRenaming?: boolean; + explorerIconConfig?: ExplorerIconConfig | null; + canDrop?: DropTargetOptions['canDrop']; reorderable?: boolean; dndData?: AffineDNDData; @@ -105,25 +114,63 @@ export const NavigationPanelTreeNodeRenameModal = ({ setRenaming, handleRename, rawName, + explorerIconConfig, className, + fallbackIcon, }: { setRenaming: (renaming: boolean) => void; handleRename: (newName: string) => void; rawName: string | undefined; className?: string; + explorerIconConfig?: ExplorerIconConfig | null; + fallbackIcon?: React.ReactNode; }) => { + const explorerIconService = useService(ExplorerIconService); const appSidebarService = useService(AppSidebarService).sidebar; const sidebarWidth = useLiveData(appSidebarService.width$); + + const explorerIcon = useLiveData( + useMemo( + () => + explorerIconConfig + ? explorerIconService.icon$( + explorerIconConfig.where, + explorerIconConfig.id + ) + : null, + [explorerIconConfig, explorerIconService] + ) + ); + + const onIconChange = useCallback( + (data?: IconData) => { + if (!explorerIconConfig) return; + explorerIconService.setIcon({ + where: explorerIconConfig.where, + id: explorerIconConfig.id, + icon: data, + }); + }, + [explorerIconConfig, explorerIconService] + ); + return ( -
- + ); }; @@ -140,7 +187,6 @@ export const NavigationPanelTreeNode = ({ onRename, disabled, collapsed, - extractEmojiAsIcon, setCollapsed, collapsible = true, canDrop, @@ -151,10 +197,12 @@ export const NavigationPanelTreeNode = ({ childrenPlaceholder, linkComponent: LinkComponent = WorkbenchLink, dndData, + explorerIconConfig, onDrop, dropEffect, ...otherProps }: WebNavigationPanelTreeNodeProps) => { + const explorerIconService = useService(ExplorerIconService); const t = useI18n(); const cid = useId(); const context = useContext(NavigationPanelTreeContext); @@ -165,20 +213,19 @@ export const NavigationPanelTreeNode = ({ const [renaming, setRenaming] = useState(defaultRenaming); const [lastInGroup, setLastInGroup] = useState(false); const rootRef = useRef(null); + const explorerIcon = useLiveData( + useMemo( + () => + explorerIconConfig + ? explorerIconService.icon$( + explorerIconConfig?.where, + explorerIconConfig?.id + ) + : null, + [explorerIconConfig, explorerIconService] + ) + ); - const { emoji, name } = useMemo(() => { - if (!extractEmojiAsIcon || !rawName) { - return { - emoji: null, - name: rawName, - }; - } - const { emoji, rest } = extractEmojiIcon(rawName); - return { - emoji, - name: rest, - }; - }, [extractEmojiAsIcon, rawName]); const { dragRef, dragging, CustomDragPreview } = useDraggable< AffineDNDData & { draggable: { __cid: string } } >( @@ -384,6 +431,14 @@ export const NavigationPanelTreeNode = ({ [clickForCollapse, collapsed, collapsible, onClick, setCollapsed] ); + const fallbackIcon = Icon && ( + + ); + const content = (
- {emoji ?? - (Icon && ( - - ))} +
-
{name}
+
{rawName}
{postfix}
)}
diff --git a/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/integration/constants.tsx b/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/integration/constants.tsx index 619b5eb1bb..23a7e32d6b 100644 --- a/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/integration/constants.tsx +++ b/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/integration/constants.tsx @@ -1,11 +1,11 @@ -import type { FeatureFlagService } from '@affine/core/modules/feature-flag'; import { IntegrationTypeIcon } from '@affine/core/modules/integration'; import type { I18nString } from '@affine/i18n'; import { Logo1Icon, TodayIcon } from '@blocksuite/icons/rc'; -import { LiveData } from '@toeverything/infra'; import type { ReactNode } from 'react'; import { CalendarSettingPanel } from './calendar/setting-panel'; +import MCPIcon from './mcp-server/MCP.inline.svg'; +import { McpServerSettingPanel } from './mcp-server/setting-panel'; import { ReadwiseSettingPanel } from './readwise/setting-panel'; type IntegrationCard = { @@ -13,6 +13,7 @@ type IntegrationCard = { name: I18nString; desc: I18nString; icon: ReactNode; + cloud?: boolean; } & ( | { setting: ReactNode; @@ -37,6 +38,14 @@ const INTEGRATION_LIST = [ icon: , setting: , }, + { + id: 'mcp-server' as const, + name: 'com.affine.integration.mcp-server.name', + desc: 'com.affine.integration.mcp-server.desc', + icon: , + setting: , + cloud: true, + }, { id: 'web-clipper' as const, name: 'com.affine.integration.web-clipper.name', @@ -55,13 +64,11 @@ export type IntegrationItem = Exclude & { id: IntegrationId; }; -export function getAllowedIntegrationList$( - _featureFlagService: FeatureFlagService -) { - return LiveData.computed(() => { - return INTEGRATION_LIST.filter(item => { - if (!item) return false; - return true; - }) as IntegrationItem[]; - }); +export function getAllowedIntegrationList(isCloudWorkspace: boolean) { + return INTEGRATION_LIST.filter(item => { + if (!item) return false; + const requiredCloud = 'cloud' in item && item.cloud; + if (requiredCloud && !isCloudWorkspace) return false; + return true; + }) as IntegrationItem[]; } diff --git a/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/integration/index.tsx b/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/integration/index.tsx index 5d6e1080e6..7dc9a187c2 100644 --- a/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/integration/index.tsx +++ b/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/integration/index.tsx @@ -1,7 +1,7 @@ import { SettingHeader } from '@affine/component/setting-components'; -import { FeatureFlagService } from '@affine/core/modules/feature-flag'; +import { WorkspaceService } from '@affine/core/modules/workspace'; import { useI18n } from '@affine/i18n'; -import { useLiveData, useService } from '@toeverything/infra'; +import { useService } from '@toeverything/infra'; import { type ReactNode, useCallback, useMemo, useState } from 'react'; import { SubPageProvider, useSubPageIsland } from '../../sub-page'; @@ -10,22 +10,21 @@ import { IntegrationCardContent, IntegrationCardHeader, } from './card'; -import { getAllowedIntegrationList$ } from './constants'; +import { getAllowedIntegrationList } from './constants'; import { type IntegrationItem } from './constants'; import { list } from './index.css'; export const IntegrationSetting = () => { const t = useI18n(); const [opened, setOpened] = useState(null); - const featureFlagService = useService(FeatureFlagService); + const workspaceService = useService(WorkspaceService); + const isCloudWorkspace = workspaceService.workspace.flavour !== 'local'; + console.log('isCloudWorkspace', isCloudWorkspace); - const integrationList = useLiveData( - useMemo( - () => getAllowedIntegrationList$(featureFlagService), - [featureFlagService] - ) + const integrationList = useMemo( + () => getAllowedIntegrationList(isCloudWorkspace), + [isCloudWorkspace] ); - const handleCardClick = useCallback((card: IntegrationItem) => { if ('setting' in card && card.setting) { setOpened(card.id); diff --git a/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/integration/mcp-server/MCP.inline.svg b/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/integration/mcp-server/MCP.inline.svg new file mode 100644 index 0000000000..0b857d323c --- /dev/null +++ b/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/integration/mcp-server/MCP.inline.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/integration/mcp-server/setting-panel.css.ts b/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/integration/mcp-server/setting-panel.css.ts new file mode 100644 index 0000000000..1cea68d35d --- /dev/null +++ b/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/integration/mcp-server/setting-panel.css.ts @@ -0,0 +1,48 @@ +import { cssVar } from '@toeverything/theme'; +import { cssVarV2 } from '@toeverything/theme/v2'; +import { style } from '@vanilla-extract/css'; + +export const connectButton = style({ + width: '100%', + marginTop: '24px', +}); + +export const section = style({ + display: 'flex', + flexDirection: 'column', + border: `1px solid ${cssVar('borderColor')}`, + borderRadius: '8px', + padding: '8px 16px', + gap: '0px', + marginBottom: '16px', +}); + +export const sectionHeader = style({ + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + gap: '8px', +}); + +export const preArea = style({ + backgroundColor: cssVarV2('layer/background/secondary'), + padding: '16px 16px', + borderRadius: '8px', + margin: '8px 0', + fontFamily: cssVar('fontMonoFamily'), + overflowX: 'auto', +}); + +export const sectionDescription = style({ + fontSize: 13, + lineHeight: '22px', + color: cssVarV2('text/secondary'), +}); + +export const sectionTitle = style({ + fontSize: 14, + fontWeight: 500, + lineHeight: 1.25, + color: cssVarV2('text/primary'), +}); diff --git a/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/integration/mcp-server/setting-panel.tsx b/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/integration/mcp-server/setting-panel.tsx new file mode 100644 index 0000000000..a7de24543e --- /dev/null +++ b/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/integration/mcp-server/setting-panel.tsx @@ -0,0 +1,227 @@ +import { Button, ErrorMessage, notify, Skeleton } from '@affine/component'; +import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks'; +import { AccessTokenService, ServerService } from '@affine/core/modules/cloud'; +import { WorkspaceService } from '@affine/core/modules/workspace'; +import { UserFriendlyError } from '@affine/error'; +import { useI18n } from '@affine/i18n'; +import { useLiveData, useService } from '@toeverything/infra'; +import { type ReactNode, useEffect, useMemo, useState } from 'react'; + +import { IntegrationSettingHeader } from '../setting'; +import MCPIcon from './MCP.inline.svg'; +import * as styles from './setting-panel.css'; + +export const McpServerSettingPanel = () => { + return ; +}; + +const McpServerSettingHeader = ({ action }: { action?: ReactNode }) => { + const t = useI18n(); + + return ( + } + name={t['com.affine.integration.mcp-server.name']()} + desc={t['com.affine.integration.mcp-server.desc']()} + action={action} + /> + ); +}; + +const McpServerSetting = () => { + const workspaceService = useService(WorkspaceService); + const serverService = useService(ServerService); + const workspaceName = useLiveData(workspaceService.workspace.name$); + const accessTokenService = useService(AccessTokenService); + const accessTokens = useLiveData(accessTokenService.accessTokens$); + const isRevalidating = useLiveData(accessTokenService.isRevalidating$); + const error = useLiveData(accessTokenService.error$); + const [mutating, setMutating] = useState(false); + const t = useI18n(); + + const mcpAccessToken = useMemo(() => { + return accessTokens?.find(token => token.name === 'mcp'); + }, [accessTokens]); + + const code = useMemo(() => { + return mcpAccessToken + ? JSON.stringify( + { + mcpServers: { + [`${workspaceName} - AFFiNE`]: { + type: 'streamable-http', + url: `${serverService.server.baseUrl}/api/workspaces/${workspaceService.workspace.id}/mcp`, + note: 'Read docs from AFFiNE workspace', + headers: { + Authorization: `Bearer ${mcpAccessToken.token}`, + }, + }, + }, + }, + null, + 2 + ) + : null; + }, [mcpAccessToken, workspaceName, workspaceService, serverService]); + + const showLoading = accessTokens === null && isRevalidating; + const showError = accessTokens === null && error !== null; + + useEffect(() => { + accessTokenService.revalidate(); + }, [accessTokenService]); + + const handleGenerateAccessToken = useAsyncCallback(async () => { + setMutating(true); + try { + if (mcpAccessToken) { + await accessTokenService.revokeUserAccessToken(mcpAccessToken.id); + } + await accessTokenService.generateUserAccessToken('mcp'); + } catch (err) { + notify.error({ + error: UserFriendlyError.fromAny(err), + }); + } finally { + setMutating(false); + } + }, [accessTokenService, mcpAccessToken]); + + const handleRevokeAccessToken = useAsyncCallback(async () => { + setMutating(true); + try { + if (mcpAccessToken) { + await accessTokenService.revokeUserAccessToken(mcpAccessToken.id); + } + } catch (err) { + notify.error({ + error: UserFriendlyError.fromAny(err), + }); + } finally { + setMutating(false); + } + }, [accessTokenService, mcpAccessToken]); + + if (showLoading) { + return ( +
+ + +
+ ); + } + + if (showError) { + return ( +
+ + {error} +
+ ); + } + + return ( +
+ + +
+
+
Personal access token
+ {!mcpAccessToken ? ( + + ) : ( + + )} +
+

+ This access token is used for the MCP service, please keep this + information secure. Deleting it will invalidate the access token. +

+
+ +
+
+
Server Config
+ +
+ {code ? ( +
{code}
+ ) : ( +

+ No access token found, please generate one first. +

+ )} +
+ +
+
+
Support tools
+
+
+ +
+
+
doc-read
+
+
+ Return the complete text and basic metadata of a single document + identified by docId; use this when the user needs the full content + of a specific file rather than a search result. +
+
+ +
+
+
doc-semantic-search
+
+
+ Retrieve conceptually related passages by performing vector-based + semantic similarity search across embedded documents; use this tool + only when exact keyword search fails or the user explicitly needs + meaning-level matches (e.g., paraphrases, synonyms, broader + concepts, recent documents). +
+
+ +
+
+
doc-keyword-search
+
+
+ Fuzzy search all workspace documents for the exact keyword or phrase + supplied and return passages ranked by textual match. Use this tool + by default whenever a straightforward term-based or keyword-base + lookup is sufficient. +
+
+
+
+ ); +}; diff --git a/packages/frontend/core/src/desktop/pages/workspace/chat/index.tsx b/packages/frontend/core/src/desktop/pages/workspace/chat/index.tsx index 17a39d0214..fa293f69ff 100644 --- a/packages/frontend/core/src/desktop/pages/workspace/chat/index.tsx +++ b/packages/frontend/core/src/desktop/pages/workspace/chat/index.tsx @@ -15,10 +15,12 @@ import { AIDraftService, AIToolsConfigService, } from '@affine/core/modules/ai-button'; +import { AIModelService } from '@affine/core/modules/ai-button/services/models'; import { EventSourceService, FetchService, GraphQLService, + SubscriptionService, } from '@affine/core/modules/cloud'; import { WorkspaceDialogService } from '@affine/core/modules/dialogs'; import { FeatureFlagService } from '@affine/core/modules/feature-flag'; @@ -229,6 +231,8 @@ export const Component = () => { ); content.aiDraftService = framework.get(AIDraftService); content.aiToolsConfigService = framework.get(AIToolsConfigService); + content.subscriptionService = framework.get(SubscriptionService); + content.aiModelService = framework.get(AIModelService); content.createSession = createSession; content.onOpenDoc = onOpenDoc; diff --git a/packages/frontend/core/src/desktop/pages/workspace/detail-page/tabs/chat.tsx b/packages/frontend/core/src/desktop/pages/workspace/detail-page/tabs/chat.tsx index e59291b80e..0f28f04ed6 100644 --- a/packages/frontend/core/src/desktop/pages/workspace/detail-page/tabs/chat.tsx +++ b/packages/frontend/core/src/desktop/pages/workspace/detail-page/tabs/chat.tsx @@ -8,6 +8,8 @@ import { AIDraftService, AIToolsConfigService, } from '@affine/core/modules/ai-button'; +import { AIModelService } from '@affine/core/modules/ai-button/services/models'; +import { SubscriptionService } from '@affine/core/modules/cloud'; import { WorkspaceDialogService } from '@affine/core/modules/dialogs'; import { FeatureFlagService } from '@affine/core/modules/feature-flag'; import { PeekViewService } from '@affine/core/modules/peek-view'; @@ -104,6 +106,9 @@ export const EditorChatPanel = forwardRef(function EditorChatPanel( chatPanelRef.current.aiDraftService = framework.get(AIDraftService); chatPanelRef.current.aiToolsConfigService = framework.get(AIToolsConfigService); + chatPanelRef.current.subscriptionService = + framework.get(SubscriptionService); + chatPanelRef.current.aiModelService = framework.get(AIModelService); containerRef.current?.append(chatPanelRef.current); } else { diff --git a/packages/frontend/core/src/mobile/dialogs/setting/index.tsx b/packages/frontend/core/src/mobile/dialogs/setting/index.tsx index 410c94571c..4db02b137c 100644 --- a/packages/frontend/core/src/mobile/dialogs/setting/index.tsx +++ b/packages/frontend/core/src/mobile/dialogs/setting/index.tsx @@ -12,6 +12,7 @@ import { AppearanceGroup } from './appearance'; import { ExperimentalFeatureSetting } from './experimental'; import { OthersGroup } from './others'; import * as styles from './style.css'; +import { UserSubscription } from './subscription'; import { SwipeDialog } from './swipe-dialog'; import { UserProfile } from './user-profile'; import { UserUsage } from './user-usage'; @@ -23,6 +24,7 @@ const MobileSetting = () => { return (
+ diff --git a/packages/frontend/core/src/mobile/dialogs/setting/subscription/index.tsx b/packages/frontend/core/src/mobile/dialogs/setting/subscription/index.tsx new file mode 100644 index 0000000000..e9b46b98a4 --- /dev/null +++ b/packages/frontend/core/src/mobile/dialogs/setting/subscription/index.tsx @@ -0,0 +1,56 @@ +import { Button } from '@affine/component'; +import { AuthService, ServerService } from '@affine/core/modules/cloud'; +import { NativePaywallService } from '@affine/core/modules/paywall'; +import { useI18n } from '@affine/i18n'; +import { useLiveData, useService } from '@toeverything/infra'; + +import * as styles from './styles.css'; + +export const UserSubscription = () => { + const serverService = useService(ServerService); + const authService = useService(AuthService); + const nativePaywallProvider = + useService(NativePaywallService).getNativePaywallProvider(); + const t = useI18n(); + + const supported = useLiveData( + serverService.server.features$.map(f => f.payment) + ); + + const loggedIn = useLiveData(authService.session.status$) === 'authenticated'; + + if (!loggedIn) { + return null; + } + + if (!supported) { + // TODO: enable this + // return null; + } + + if (!nativePaywallProvider) { + return null; + } + + return ( +
+
+
+ {t['com.affine.payment.subscription.title']()} +
+
+ {t['com.affine.payment.subscription.description']()} +
+
+ +
+ ); +}; diff --git a/packages/frontend/core/src/mobile/dialogs/setting/subscription/styles.css.ts b/packages/frontend/core/src/mobile/dialogs/setting/subscription/styles.css.ts new file mode 100644 index 0000000000..f18bab4f59 --- /dev/null +++ b/packages/frontend/core/src/mobile/dialogs/setting/subscription/styles.css.ts @@ -0,0 +1,37 @@ +import { cssVarV2 } from '@toeverything/theme/v2'; +import { style } from '@vanilla-extract/css'; + +export const root = style({ + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + gap: 16, + border: `1px solid ${cssVarV2('database/border')}`, + borderRadius: '12px', + padding: '10px 16px', + backgroundColor: cssVarV2('edgeless/selection/selectionMarqueeBackground'), +}); + +export const content = style({ + display: 'flex', + flexDirection: 'column', + gap: 4, +}); + +export const title = style({ + fontSize: '17px', + lineHeight: '22px', + fontWeight: 600, + color: cssVarV2('text/primary'), +}); + +export const description = style({ + fontSize: '13px', + lineHeight: '18px', + fontWeight: 400, + color: cssVarV2('text/secondary'), +}); + +export const button = style({ + fontSize: '15px', +}); diff --git a/packages/frontend/core/src/modules/ai-button/index.ts b/packages/frontend/core/src/modules/ai-button/index.ts index d77f788624..08f541f813 100644 --- a/packages/frontend/core/src/modules/ai-button/index.ts +++ b/packages/frontend/core/src/modules/ai-button/index.ts @@ -8,12 +8,14 @@ export { import type { Framework } from '@toeverything/infra'; +import { GraphQLService, ServerScope, SubscriptionService } from '../cloud'; import { FeatureFlagService } from '../feature-flag'; import { CacheStorage, GlobalStateService } from '../storage'; import { WorkspaceScope } from '../workspace'; import { AIButtonProvider } from './provider/ai-button'; import { AIButtonService } from './services/ai-button'; import { AIDraftService } from './services/ai-draft'; +import { AIModelService } from './services/models'; import { AINetworkSearchService } from './services/network-search'; import { AIPlaygroundService } from './services/playground'; import { AIReasoningService } from './services/reasoning'; @@ -49,3 +51,13 @@ export function configureAIDraftModule(framework: Framework) { export function configureAIToolsConfigModule(framework: Framework) { framework.service(AIToolsConfigService, [GlobalStateService]); } + +export function configureAIModelModule(framework: Framework) { + framework + .scope(ServerScope) + .service(AIModelService, [ + GlobalStateService, + GraphQLService, + SubscriptionService, + ]); +} diff --git a/packages/frontend/core/src/modules/ai-button/services/models.ts b/packages/frontend/core/src/modules/ai-button/services/models.ts new file mode 100644 index 0000000000..b50b76c40c --- /dev/null +++ b/packages/frontend/core/src/modules/ai-button/services/models.ts @@ -0,0 +1,112 @@ +import { getPromptModelsQuery, SubscriptionStatus } from '@affine/graphql'; +import { + createSignalFromObservable, + type Signal, +} from '@blocksuite/affine/shared/utils'; +import { signal } from '@preact/signals-core'; +import { LiveData, Service } from '@toeverything/infra'; + +import type { GraphQLService, SubscriptionService } from '../../cloud'; +import type { GlobalStateService } from '../../storage'; + +const AI_MODEL_ID_KEY = 'AIModelId'; + +export interface AIModel { + name: string; + id: string; + version: string; + category: string; + isPro: boolean; + isDefault: boolean; +} + +export class AIModelService extends Service { + modelId: Signal; + + models: Signal = signal([]); + + private readonly modelId$ = LiveData.from( + this.globalStateService.globalState.watch(AI_MODEL_ID_KEY), + undefined + ); + + constructor( + private readonly globalStateService: GlobalStateService, + private readonly gqlService: GraphQLService, + private readonly subscriptionService: SubscriptionService + ) { + super(); + + const { signal: modelId, cleanup } = createSignalFromObservable< + string | undefined + >(this.modelId$, undefined); + this.modelId = modelId; + this.disposables.push(cleanup); + + this.init().catch(err => { + console.error(err); + }); + } + + resetModel = () => { + this.globalStateService.globalState.set(AI_MODEL_ID_KEY, undefined); + }; + + setModel = (modelId: string) => { + const isSubscribed = + this.subscriptionService.subscription.ai$.value?.status === + SubscriptionStatus.Active; + const model = this.models.value.find(model => model.id === modelId); + if (!isSubscribed && model?.isPro) { + return; + } + this.globalStateService.globalState.set(AI_MODEL_ID_KEY, modelId); + }; + + private readonly init = async () => { + await this.initModels(); + + // subscribe to ai purchase status + const sub = this.subscriptionService.subscription.ai$.subscribe( + subscription => { + const isSubscribed = subscription?.status === SubscriptionStatus.Active; + const model = this.models.value.find( + model => model.id === this.modelId.value + ); + if (!isSubscribed && model?.isPro) { + this.resetModel(); + } + } + ); + this.disposables.push(() => sub.unsubscribe()); + }; + + private readonly initModels = async (prompt?: string) => { + const promptName = prompt || 'Chat With AFFiNE AI'; + const models = await this.getModelsByPrompt(promptName); + if (models) { + const { defaultModel, optionalModels, proModels } = models; + this.models.value = optionalModels.map(model => { + const [category] = model.name.split(' '); + const version = model.name.slice(category.length + 1); + return { + name: model.name, + id: model.id, + version, + category, + isPro: proModels.some(proModel => proModel.id === model.id), + isDefault: model.id === defaultModel, + }; + }); + } + }; + + private readonly getModelsByPrompt = async (promptName: string) => { + return this.gqlService + .gql({ + query: getPromptModelsQuery, + variables: { promptName }, + }) + .then(res => res.currentUser?.copilot?.models); + }; +} diff --git a/packages/frontend/core/src/modules/cloud/index.ts b/packages/frontend/core/src/modules/cloud/index.ts index 04a6d0ab59..adedec6f7c 100644 --- a/packages/frontend/core/src/modules/cloud/index.ts +++ b/packages/frontend/core/src/modules/cloud/index.ts @@ -7,6 +7,7 @@ export { AccountLoggedOut } from './events/account-logged-out'; export { AuthProvider } from './provider/auth'; export { ValidatorProvider } from './provider/validator'; export { ServerScope } from './scopes/server'; +export { AccessTokenService } from './services/access-token'; export { AuthService } from './services/auth'; export { CaptchaService } from './services/captcha'; export { DefaultServerService } from './services/default-server'; @@ -102,6 +103,8 @@ import { WorkspacePermissionService } from '../permissions'; import { DocScope, DocService, DocsService } from '../doc'; import { DocCreatedByUpdatedBySyncStore } from './stores/doc-created-by-updated-by-sync'; import { GlobalDialogService } from '../dialogs'; +import { AccessTokenService } from './services/access-token'; +import { AccessTokenStore } from './stores/access-token'; export function configureCloudModule(framework: Framework) { configureDefaultAuthProvider(framework); @@ -171,7 +174,9 @@ export function configureCloudModule(framework: Framework) { .service(PublicUserService, [PublicUserStore]) .store(PublicUserStore, [GraphQLService]) .service(UserSettingsService, [UserSettingsStore]) - .store(UserSettingsStore, [GraphQLService]); + .store(UserSettingsStore, [GraphQLService]) + .service(AccessTokenService, [AccessTokenStore]) + .store(AccessTokenStore, [GraphQLService]); framework .scope(WorkspaceScope) diff --git a/packages/frontend/core/src/modules/cloud/services/access-token.ts b/packages/frontend/core/src/modules/cloud/services/access-token.ts new file mode 100644 index 0000000000..2ed4b124ff --- /dev/null +++ b/packages/frontend/core/src/modules/cloud/services/access-token.ts @@ -0,0 +1,80 @@ +import { + effect, + exhaustMapWithTrailing, + fromPromise, + LiveData, + onComplete, + OnEvent, + onStart, + Service, + smartRetry, +} from '@toeverything/infra'; +import { catchError, EMPTY, tap } from 'rxjs'; + +import { AccountChanged } from '../events/account-changed'; +import type { AccessToken, AccessTokenStore } from '../stores/access-token'; + +@OnEvent(AccountChanged, e => e.onAccountChanged) +export class AccessTokenService extends Service { + constructor(private readonly accessTokenStore: AccessTokenStore) { + super(); + } + + accessTokens$ = new LiveData(null); + isRevalidating$ = new LiveData(false); + error$ = new LiveData(null); + + async generateUserAccessToken(name: string) { + const accessToken = + await this.accessTokenStore.generateUserAccessToken(name); + this.accessTokens$.value = [ + ...(this.accessTokens$.value || []), + accessToken as AccessToken, + ]; + + await this.waitForRevalidation(); + } + + async revokeUserAccessToken(id: string) { + await this.accessTokenStore.revokeUserAccessToken(id); + this.accessTokens$.value = + this.accessTokens$.value?.filter(token => token.id !== id) ?? null; + await this.waitForRevalidation(); + } + + revalidate = effect( + exhaustMapWithTrailing(() => { + return fromPromise(() => { + return this.accessTokenStore.listUserAccessTokens(); + }).pipe( + smartRetry(), + tap(accessTokens => { + this.accessTokens$.value = accessTokens; + }), + catchError(error => { + this.error$.value = error; + return EMPTY; + }), + onStart(() => { + this.isRevalidating$.value = true; + }), + onComplete(() => { + this.isRevalidating$.value = false; + }) + ); + }) + ); + + private onAccountChanged() { + this.accessTokens$.value = null; + this.revalidate(); + } + + async waitForRevalidation(signal?: AbortSignal) { + this.revalidate(); + await this.isRevalidating$.waitFor( + isRevalidating => !isRevalidating, + signal + ); + } +} diff --git a/packages/frontend/core/src/modules/cloud/stores/access-token.ts b/packages/frontend/core/src/modules/cloud/stores/access-token.ts new file mode 100644 index 0000000000..d1d5d95f3d --- /dev/null +++ b/packages/frontend/core/src/modules/cloud/stores/access-token.ts @@ -0,0 +1,51 @@ +import { + generateUserAccessTokenMutation, + type ListUserAccessTokensQuery, + listUserAccessTokensQuery, + revokeUserAccessTokenMutation, +} from '@affine/graphql'; +import { Store } from '@toeverything/infra'; + +import type { GraphQLService } from '../services/graphql'; + +export type AccessToken = + ListUserAccessTokensQuery['revealedAccessTokens'][number]; + +export class AccessTokenStore extends Store { + constructor(private readonly gqlService: GraphQLService) { + super(); + } + + async listUserAccessTokens(signal?: AbortSignal): Promise { + const data = await this.gqlService.gql({ + query: listUserAccessTokensQuery, + context: { signal }, + }); + + return data.revealedAccessTokens; + } + + async generateUserAccessToken( + name: string, + expiresAt?: string, + signal?: AbortSignal + ) { + const data = await this.gqlService.gql({ + query: generateUserAccessTokenMutation, + variables: { input: { name, expiresAt } }, + context: { signal }, + }); + + return data.generateUserAccessToken; + } + + async revokeUserAccessToken(id: string, signal?: AbortSignal) { + const data = await this.gqlService.gql({ + query: revokeUserAccessTokenMutation, + variables: { id }, + context: { signal }, + }); + + return data.revokeUserAccessToken; + } +} diff --git a/packages/frontend/core/src/modules/db/schema/schema.ts b/packages/frontend/core/src/modules/db/schema/schema.ts index c323fd71b5..eff152b2cb 100644 --- a/packages/frontend/core/src/modules/db/schema/schema.ts +++ b/packages/frontend/core/src/modules/db/schema/schema.ts @@ -1,3 +1,4 @@ +import type { IconData } from '@affine/component'; import { type DBSchemaBuilder, f, @@ -46,6 +47,13 @@ export const AFFiNE_WORKSPACE_DB_SCHEMA = { collectionId: f.string().primaryKey(), index: f.string(), }, + explorerIcon: { + /** + * ${doc|collection|folder|tag}:${id} + */ + id: f.string().primaryKey(), + icon: f.json(), + }, } as const satisfies DBSchemaBuilder; export type AFFiNEWorkspaceDbSchema = typeof AFFiNE_WORKSPACE_DB_SCHEMA; diff --git a/packages/frontend/core/src/modules/doc-display-meta/index.ts b/packages/frontend/core/src/modules/doc-display-meta/index.ts index ad1832a8ee..bdcbcdc3b2 100644 --- a/packages/frontend/core/src/modules/doc-display-meta/index.ts +++ b/packages/frontend/core/src/modules/doc-display-meta/index.ts @@ -1,7 +1,7 @@ import { type Framework } from '@toeverything/infra'; import { DocsService } from '../doc'; -import { FeatureFlagService } from '../feature-flag'; +import { ExplorerIconService } from '../explorer-icon/services/explorer-icon'; import { I18nService } from '../i18n'; import { JournalService } from '../journal'; import { WorkspaceScope } from '../workspace'; @@ -15,7 +15,7 @@ export function configureDocDisplayMetaModule(framework: Framework) { .service(DocDisplayMetaService, [ JournalService, DocsService, - FeatureFlagService, I18nService, + ExplorerIconService, ]); } diff --git a/packages/frontend/core/src/modules/doc-display-meta/services/doc-display-meta.ts b/packages/frontend/core/src/modules/doc-display-meta/services/doc-display-meta.ts index f5df3756bb..f0fca2a56a 100644 --- a/packages/frontend/core/src/modules/doc-display-meta/services/doc-display-meta.ts +++ b/packages/frontend/core/src/modules/doc-display-meta/services/doc-display-meta.ts @@ -1,4 +1,3 @@ -import { extractEmojiIcon } from '@affine/core/utils'; import { i18nTime } from '@affine/i18n'; import { AliasIcon as LitAliasIcon, @@ -27,9 +26,10 @@ import type { Dayjs } from 'dayjs'; import dayjs from 'dayjs'; import type { DocRecord, DocsService } from '../../doc'; -import type { FeatureFlagService } from '../../feature-flag'; +import type { ExplorerIconService } from '../../explorer-icon/services/explorer-icon'; import type { I18nService } from '../../i18n'; import type { JournalService } from '../../journal'; +import { getDocIconComponent } from './icon'; type IconType = 'rc' | 'lit'; interface DocDisplayIconOptions { @@ -87,8 +87,8 @@ export class DocDisplayMetaService extends Service { constructor( private readonly journalService: JournalService, private readonly docsService: DocsService, - private readonly featureFlagService: FeatureFlagService, - private readonly i18nService: I18nService + private readonly i18nService: I18nService, + private readonly explorerIconService: ExplorerIconService ) { super(); } @@ -128,30 +128,32 @@ export class DocDisplayMetaService extends Service { const iconSet = icons[options?.type ?? 'rc']; return LiveData.computed(get => { - const enableEmojiIcon = - get(this.featureFlagService.flags.enable_emoji_doc_icon.$) && - options?.enableEmojiIcon !== false; + const enableEmojiIcon = options?.enableEmojiIcon !== false; const doc = get(this.docsService.list.doc$(docId)); const referenced = !!options?.reference; const titleAlias = referenced ? options?.title : undefined; - const originalTitle = doc ? get(doc.title$) : ''; + // const originalTitle = doc ? get(doc.title$) : ''; // link to journal doc const journalDateString = get(this.journalService.journalDate$(docId)); const journalIcon = journalDateString ? this.getJournalIcon(journalDateString, options) : undefined; - const journalTitle = journalDateString - ? i18nTime(journalDateString, { absolute: { accuracy: 'day' } }) - : undefined; - const title = titleAlias ?? journalTitle ?? originalTitle; + // const journalTitle = journalDateString + // ? i18nTime(journalDateString, { absolute: { accuracy: 'day' } }) + // : undefined; + // const title = titleAlias ?? journalTitle ?? originalTitle; const mode = doc ? get(doc.primaryMode$) : undefined; const finalMode = options?.mode ?? mode ?? 'page'; const referenceToNode = !!(referenced && options.referenceToNode); // emoji title - if (enableEmojiIcon && title) { - const { emoji } = extractEmojiIcon(title); - if (emoji) return () => emoji; + if (enableEmojiIcon) { + // const { emoji } = extractEmojiIcon(title); + // if (emoji) return () => emoji; + const icon = get(this.explorerIconService.icon$('doc', docId))?.icon; + if (icon) { + return getDocIconComponent(icon); + } } // title alias @@ -176,10 +178,6 @@ export class DocDisplayMetaService extends Service { title$(docId: string, options?: DocDisplayTitleOptions) { return LiveData.computed(get => { - const enableEmojiIcon = - get(this.featureFlagService.flags.enable_emoji_doc_icon.$) && - options?.enableEmojiIcon !== false; - const lng = get(this.i18nService.i18n.currentLanguageKey$); const doc = get(this.docsService.list.doc$(docId)); const referenced = !!options?.reference; @@ -190,20 +188,17 @@ export class DocDisplayMetaService extends Service { const journalTitle = journalDateString ? i18nTime(journalDateString, { absolute: { accuracy: 'day' } }) : undefined; - const title = titleAlias ?? journalTitle ?? originalTitle; + // const title = titleAlias ?? journalTitle ?? originalTitle; // emoji title - if (enableEmojiIcon && title) { - const { rest } = extractEmojiIcon(title); - if (rest) return rest; - - // When the title has only one emoji character, - // if it's a journal document, the date should be displayed. - const journalDateString = get(this.journalService.journalDate$(docId)); - if (journalDateString) { - return i18nTime(journalDateString, { absolute: { accuracy: 'day' } }); - } - } + // if (enableEmojiIcon && title) { + // // When the title has only one emoji character, + // // if it's a journal document, the date should be displayed. + // const journalDateString = get(this.journalService.journalDate$(docId)); + // if (journalDateString) { + // return i18nTime(journalDateString, { absolute: { accuracy: 'day' } }); + // } + // } // title alias if (titleAlias) return titleAlias; diff --git a/packages/frontend/core/src/modules/doc-display-meta/services/icon.tsx b/packages/frontend/core/src/modules/doc-display-meta/services/icon.tsx new file mode 100644 index 0000000000..f65367056e --- /dev/null +++ b/packages/frontend/core/src/modules/doc-display-meta/services/icon.tsx @@ -0,0 +1,7 @@ +import { type IconData, IconRenderer } from '@affine/component'; + +export const getDocIconComponent = (icon: IconData) => { + const Icon = () => ; + Icon.displayName = 'DocIcon'; + return Icon; +}; diff --git a/packages/frontend/core/src/modules/explorer-icon/index.ts b/packages/frontend/core/src/modules/explorer-icon/index.ts new file mode 100644 index 0000000000..2301712525 --- /dev/null +++ b/packages/frontend/core/src/modules/explorer-icon/index.ts @@ -0,0 +1,13 @@ +import type { Framework } from '@toeverything/infra'; + +import { WorkspaceDBService } from '../db'; +import { WorkspaceScope } from '../workspace'; +import { ExplorerIconService } from './services/explorer-icon'; +import { ExplorerIconStore } from './store/explorer-icon'; + +export function configureExplorerIconModule(framework: Framework) { + framework + .scope(WorkspaceScope) + .store(ExplorerIconStore, [WorkspaceDBService]) + .service(ExplorerIconService, [ExplorerIconStore]); +} diff --git a/packages/frontend/core/src/modules/explorer-icon/services/explorer-icon.ts b/packages/frontend/core/src/modules/explorer-icon/services/explorer-icon.ts new file mode 100644 index 0000000000..4e8a357c87 --- /dev/null +++ b/packages/frontend/core/src/modules/explorer-icon/services/explorer-icon.ts @@ -0,0 +1,21 @@ +import { LiveData, Service } from '@toeverything/infra'; + +import type { ExplorerIconStore, ExplorerType } from '../store/explorer-icon'; + +export class ExplorerIconService extends Service { + constructor(private readonly store: ExplorerIconStore) { + super(); + } + + getIcon(type: ExplorerType, id: string) { + return this.store.getIcon(type, id); + } + + setIcon(options: Parameters[0]) { + return this.store.setIcon(options); + } + + icon$(type: ExplorerType, id: string) { + return LiveData.from(this.store.watchIcon(type, id), null); + } +} diff --git a/packages/frontend/core/src/modules/explorer-icon/store/explorer-icon.ts b/packages/frontend/core/src/modules/explorer-icon/store/explorer-icon.ts new file mode 100644 index 0000000000..db46883924 --- /dev/null +++ b/packages/frontend/core/src/modules/explorer-icon/store/explorer-icon.ts @@ -0,0 +1,33 @@ +import type { IconData } from '@affine/component'; +import { Store } from '@toeverything/infra'; + +import type { WorkspaceDBService } from '../../db'; + +export type ExplorerType = 'doc' | 'collection' | 'folder' | 'tag'; + +export class ExplorerIconStore extends Store { + constructor(private readonly dbService: WorkspaceDBService) { + super(); + } + + watchIcon(type: ExplorerType, id: string) { + return this.dbService.db.explorerIcon.get$(`${type}:${id}`); + } + + getIcon(type: ExplorerType, id: string) { + return this.dbService.db.explorerIcon.get(`${type}:${id}`); + } + + setIcon(options: { where: ExplorerType; id: string; icon?: IconData }) { + const { where, id, icon } = options; + // remove icon + if (!icon) { + return this.dbService.db.explorerIcon.delete(`${where}:${id}`); + } + // upsert icon + return this.dbService.db.explorerIcon.create({ + id: `${where}:${id}`, + icon, + }); + } +} diff --git a/packages/frontend/core/src/modules/index.ts b/packages/frontend/core/src/modules/index.ts index f4d87570a4..c09aa994ee 100644 --- a/packages/frontend/core/src/modules/index.ts +++ b/packages/frontend/core/src/modules/index.ts @@ -4,6 +4,7 @@ import { type Framework } from '@toeverything/infra'; import { configureAIButtonModule, configureAIDraftModule, + configureAIModelModule, configureAINetworkSearchModule, configureAIPlaygroundModule, configureAIReasoningModule, @@ -27,6 +28,7 @@ import { configureDocSummaryModule } from './doc-summary'; import { configureDocsSearchModule } from './docs-search'; import { configureEditorModule } from './editor'; import { configureEditorSettingModule } from './editor-setting'; +import { configureExplorerIconModule } from './explorer-icon'; import { configureFavoriteModule } from './favorite'; import { configureFeatureFlagModule } from './feature-flag'; import { configureGlobalContextModule } from './global-context'; @@ -42,6 +44,7 @@ import { configureNavigationPanelModule } from './navigation-panel'; import { configureNotificationModule } from './notification'; import { configureOpenInApp } from './open-in-app'; import { configureOrganizeModule } from './organize'; +import { configurePaywallModule } from './paywall'; import { configurePDFModule } from './pdf'; import { configurePeekViewModule } from './peek-view'; import { configurePermissionsModule } from './permissions'; @@ -85,6 +88,7 @@ export function configureCommonModules(framework: Framework) { configureTelemetryModule(framework); configurePDFModule(framework); configurePeekViewModule(framework); + configureExplorerIconModule(framework); configureDocDisplayMetaModule(framework); configureQuickSearchModule(framework); configureDocsSearchModule(framework); @@ -115,6 +119,7 @@ export function configureCommonModules(framework: Framework) { configureAIButtonModule(framework); configureAIDraftModule(framework); configureAIToolsConfigModule(framework); + configureAIModelModule(framework); configureTemplateDocModule(framework); configureBlobManagementModule(framework); configureMediaModule(framework); @@ -126,4 +131,5 @@ export function configureCommonModules(framework: Framework) { configureIndexerEmbeddingModule(framework); configureCommentModule(framework); configureDocSummaryModule(framework); + configurePaywallModule(framework); } diff --git a/packages/frontend/core/src/modules/paywall/index.ts b/packages/frontend/core/src/modules/paywall/index.ts new file mode 100644 index 0000000000..785e81cffe --- /dev/null +++ b/packages/frontend/core/src/modules/paywall/index.ts @@ -0,0 +1,10 @@ +import type { Framework } from '@toeverything/infra'; + +import { NativePaywallService } from './services/native-paywall'; + +export { NativePaywallProvider } from './providers/native-paywall'; +export { NativePaywallService } from './services/native-paywall'; + +export function configurePaywallModule(framework: Framework) { + framework.service(NativePaywallService); +} diff --git a/packages/frontend/core/src/modules/paywall/providers/native-paywall.ts b/packages/frontend/core/src/modules/paywall/providers/native-paywall.ts new file mode 100644 index 0000000000..b8a1ad066b --- /dev/null +++ b/packages/frontend/core/src/modules/paywall/providers/native-paywall.ts @@ -0,0 +1,9 @@ +import { createIdentifier } from '@toeverything/infra'; + +export interface NativePaywallProvider { + showPaywall(type: 'Pro' | 'AI'): Promise; +} + +export const NativePaywallProvider = createIdentifier( + 'NativePaywallProvider' +); diff --git a/packages/frontend/core/src/modules/paywall/services/native-paywall.ts b/packages/frontend/core/src/modules/paywall/services/native-paywall.ts new file mode 100644 index 0000000000..418a3dbdf6 --- /dev/null +++ b/packages/frontend/core/src/modules/paywall/services/native-paywall.ts @@ -0,0 +1,13 @@ +import { Service } from '@toeverything/infra'; + +import { NativePaywallProvider } from '../providers/native-paywall'; + +export class NativePaywallService extends Service { + constructor() { + super(); + } + + getNativePaywallProvider() { + return this.framework.getOptional(NativePaywallProvider); + } +} diff --git a/packages/frontend/core/src/utils/__tests__/extract-emoji-icon.spec.ts b/packages/frontend/core/src/utils/__tests__/extract-emoji-icon.spec.ts index 3828b7e09a..a4031767fc 100644 --- a/packages/frontend/core/src/utils/__tests__/extract-emoji-icon.spec.ts +++ b/packages/frontend/core/src/utils/__tests__/extract-emoji-icon.spec.ts @@ -9,7 +9,22 @@ test('extract-emoji-icon', () => { }); expect(extractEmojiIcon('❤️123')).toEqual({ + emoji: '❤️', + rest: '123', + }); + + expect(extractEmojiIcon('➡️456')).toEqual({ + emoji: '➡️', + rest: '456', + }); + + expect(extractEmojiIcon('✈️789')).toEqual({ + emoji: '✈️', + rest: '789', + }); + + expect(extractEmojiIcon('plain text')).toEqual({ emoji: null, - rest: '❤️123', + rest: 'plain text', }); }); diff --git a/packages/frontend/core/src/utils/extract-emoji-icon.ts b/packages/frontend/core/src/utils/extract-emoji-icon.ts index 8fb7b1a1b3..7837335261 100644 --- a/packages/frontend/core/src/utils/extract-emoji-icon.ts +++ b/packages/frontend/core/src/utils/extract-emoji-icon.ts @@ -1,13 +1,19 @@ import Graphemer from 'graphemer'; +const emojiRe = + /(\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])/g; +/** + * @deprecated use ExplorerIconService instead + */ export function extractEmojiIcon(text: string) { - const isStartsWithEmoji = /^(\p{Emoji_Presentation})/u.test(text); - if (isStartsWithEmoji) { + emojiRe.lastIndex = 0; + const match = emojiRe.exec(text); + if (match && match.index === 0) { // emoji like "👨🏻‍❤️‍💋‍👨🏻" are combined. Graphemer can handle these. const emojiEnd = Graphemer.nextBreak(text, 0); return { - emoji: text.substring(0, emojiEnd), - rest: text.substring(emojiEnd), + emoji: text.slice(0, emojiEnd), + rest: text.slice(emojiEnd), }; } return { diff --git a/packages/frontend/i18n/src/i18n-completenesses.json b/packages/frontend/i18n/src/i18n-completenesses.json index 88fc0ffc18..1ff79f6d80 100644 --- a/packages/frontend/i18n/src/i18n-completenesses.json +++ b/packages/frontend/i18n/src/i18n-completenesses.json @@ -14,7 +14,7 @@ "it-IT": 100, "it": 1, "ja": 100, - "ko": 53, + "ko": 52, "pl": 100, "pt-BR": 100, "ru": 100, diff --git a/packages/frontend/i18n/src/i18n.gen.ts b/packages/frontend/i18n/src/i18n.gen.ts index fd271b7544..52539d1265 100644 --- a/packages/frontend/i18n/src/i18n.gen.ts +++ b/packages/frontend/i18n/src/i18n.gen.ts @@ -99,6 +99,10 @@ export function useAFFiNEI18N(): { * `Copied link to clipboard` */ ["Copied link to clipboard"](): string; + /** + * `Copied to clipboard` + */ + ["Copied to clipboard"](): string; /** * `Copy` */ @@ -8180,6 +8184,14 @@ export function useAFFiNEI18N(): { ["com.affine.integration.calendar.unsubscribe-content"](options: { readonly name: string; }): string; + /** + * `MCP Server` + */ + ["com.affine.integration.mcp-server.name"](): string; + /** + * `Enable other MCP Client to search and read the doc of AFFiNE.` + */ + ["com.affine.integration.mcp-server.desc"](): string; /** * `Notes` */ @@ -8294,6 +8306,18 @@ export function useAFFiNEI18N(): { * `Only current mode` */ ["com.affine.comment.filter.only-current-mode"](): string; + /** + * `Unlock more features` + */ + ["com.affine.payment.subscription.title"](): string; + /** + * `The universal editor that lets you work, play, present or create just about anything.` + */ + ["com.affine.payment.subscription.description"](): string; + /** + * `Upgrade` + */ + ["com.affine.payment.subscription.button"](): string; /** * `Reply` */ @@ -8314,6 +8338,10 @@ export function useAFFiNEI18N(): { * `Cut` */ ["com.affine.context-menu.cut"](): string; + /** + * `Add icon` + */ + ["com.affine.docIconPicker.placeholder"](): string; /** * `An internal error occurred.` */ @@ -8719,6 +8747,10 @@ export function useAFFiNEI18N(): { * `Workspace id is required to update team subscription.` */ ["error.WORKSPACE_ID_REQUIRED_TO_UPDATE_TEAM_SUBSCRIPTION"](): string; + /** + * `This subscription is managed by App Store or Google Play. Please manage it in the corresponding store.` + */ + ["error.MANAGED_BY_APP_STORE_OR_PLAY"](): string; /** * `Copilot session not found.` */ diff --git a/packages/frontend/i18n/src/resources/en.json b/packages/frontend/i18n/src/resources/en.json index d4f94e617e..46689d1a6a 100644 --- a/packages/frontend/i18n/src/resources/en.json +++ b/packages/frontend/i18n/src/resources/en.json @@ -15,6 +15,7 @@ "Continue": "Continue", "Convert to ": "Convert to ", "Copied link to clipboard": "Copied link to clipboard", + "Copied to clipboard": "Copied to clipboard", "Copy": "Copy", "Create": "Create", "Created": "Created", @@ -2053,6 +2054,8 @@ "com.affine.integration.calendar.show-events-desc": "Enabling this setting allows you to connect your calendar events to your Journal in AFFiNE", "com.affine.integration.calendar.show-all-day-events": "Show all day event", "com.affine.integration.calendar.unsubscribe-content": "Are you sure you want to unsubscribe \"{{name}}\"? Unsubscribing this account will remove its data from Journal.", + "com.affine.integration.mcp-server.name": "MCP Server", + "com.affine.integration.mcp-server.desc": "Enable other MCP Client to search and read the doc of AFFiNE.", "com.affine.audio.notes": "Notes", "com.affine.audio.transcribing": "Transcribing", "com.affine.audio.transcribe.non-owner.confirm.title": "Unable to retrieve AI results for others", @@ -2081,11 +2084,15 @@ "com.affine.comment.filter.show-resolved": "Show resolved comments", "com.affine.comment.filter.only-my-replies": "Only my replies and mentions", "com.affine.comment.filter.only-current-mode": "Only current mode", + "com.affine.payment.subscription.title": "Unlock more features", + "com.affine.payment.subscription.description": "The universal editor that lets you work, play, present or create just about anything.", + "com.affine.payment.subscription.button": "Upgrade", "com.affine.comment.reply": "Reply", "com.affine.comment.copy-link": "Copy link", "com.affine.context-menu.copy": "Copy", "com.affine.context-menu.paste": "Paste", "com.affine.context-menu.cut": "Cut", + "com.affine.docIconPicker.placeholder": "Add icon", "error.INTERNAL_SERVER_ERROR": "An internal error occurred.", "error.NETWORK_ERROR": "Network error.", "error.TOO_MANY_REQUEST": "Too many requests.", @@ -2168,6 +2175,7 @@ "error.CANT_UPDATE_ONETIME_PAYMENT_SUBSCRIPTION": "You cannot update an onetime payment subscription.", "error.WORKSPACE_ID_REQUIRED_FOR_TEAM_SUBSCRIPTION": "A workspace is required to checkout for team subscription.", "error.WORKSPACE_ID_REQUIRED_TO_UPDATE_TEAM_SUBSCRIPTION": "Workspace id is required to update team subscription.", + "error.MANAGED_BY_APP_STORE_OR_PLAY": "This subscription is managed by App Store or Google Play. Please manage it in the corresponding store.", "error.COPILOT_SESSION_NOT_FOUND": "Copilot session not found.", "error.COPILOT_SESSION_INVALID_INPUT": "Copilot session input is invalid.", "error.COPILOT_SESSION_DELETED": "Copilot session has been deleted.", diff --git a/tests/affine-local/e2e/links.spec.ts b/tests/affine-local/e2e/links.spec.ts index 397c865009..a6134fefba 100644 --- a/tests/affine-local/e2e/links.spec.ts +++ b/tests/affine-local/e2e/links.spec.ts @@ -821,64 +821,64 @@ test.describe('Customize linked doc title and description', () => { await expect(cardDescription).toBeHidden(); }); - test('should show emoji doc icon in normal document', async ({ page }) => { - await enableEmojiDocIcon(page); + // test('should show emoji doc icon in normal document', async ({ page }) => { + // await enableEmojiDocIcon(page); - await clickNewPageButton(page); - const title = getBlockSuiteEditorTitle(page); - await title.click(); + // await clickNewPageButton(page); + // const title = getBlockSuiteEditorTitle(page); + // await title.click(); - await page.keyboard.press('Enter'); - await createLinkedPage(page, 'Test Page'); + // await page.keyboard.press('Enter'); + // await createLinkedPage(page, 'Test Page'); - const toolbar = page.locator('affine-toolbar-widget editor-toolbar'); + // const toolbar = page.locator('affine-toolbar-widget editor-toolbar'); - const inlineLink = page.locator('affine-reference'); - await inlineLink.hover(); + // const inlineLink = page.locator('affine-reference'); + // await inlineLink.hover(); - // Edits title - await toolbar.getByRole('button', { name: 'Edit' }).click(); + // // Edits title + // await toolbar.getByRole('button', { name: 'Edit' }).click(); - // Title alias - await page.keyboard.type('🦀hello'); - await page.keyboard.press('Enter'); + // // Title alias + // await page.keyboard.type('🦀hello'); + // await page.keyboard.press('Enter'); - const a = inlineLink.locator('a'); + // const a = inlineLink.locator('a'); - await expect(a).toHaveText('🦀hello'); - await expect(a.locator('svg')).toBeHidden(); - await expect(a.locator('.affine-reference-title')).toHaveText('hello'); - }); + // await expect(a).toHaveText('🦀hello'); + // await expect(a.locator('svg')).toBeHidden(); + // await expect(a.locator('.affine-reference-title')).toHaveText('hello'); + // }); - test('should show emoji doc icon in journal document', async ({ page }) => { - await enableEmojiDocIcon(page); + // test('should show emoji doc icon in journal document', async ({ page }) => { + // await enableEmojiDocIcon(page); - await clickNewPageButton(page); - const title = getBlockSuiteEditorTitle(page); - await title.click(); + // await clickNewPageButton(page); + // const title = getBlockSuiteEditorTitle(page); + // await title.click(); - await page.keyboard.press('Enter'); - await createTodayPage(page); + // await page.keyboard.press('Enter'); + // await createTodayPage(page); - const toolbar = page.locator('affine-toolbar-widget editor-toolbar'); + // const toolbar = page.locator('affine-toolbar-widget editor-toolbar'); - const inlineLink = page.locator('affine-reference'); - await inlineLink.hover(); + // const inlineLink = page.locator('affine-reference'); + // await inlineLink.hover(); - // Edits title - await toolbar.getByRole('button', { name: 'Edit' }).click(); + // // Edits title + // await toolbar.getByRole('button', { name: 'Edit' }).click(); - // Title alias - await page.keyboard.type('🦀'); - await page.keyboard.press('Enter'); + // // Title alias + // await page.keyboard.type('🦀'); + // await page.keyboard.press('Enter'); - const a = inlineLink.locator('a'); + // const a = inlineLink.locator('a'); - const year = String(new Date().getFullYear()); - await expect(a).toContainText('🦀'); - await expect(a.locator('svg')).toBeHidden(); - await expect(a.locator('.affine-reference-title')).toContainText(year); - }); + // const year = String(new Date().getFullYear()); + // await expect(a).toContainText('🦀'); + // await expect(a.locator('svg')).toBeHidden(); + // await expect(a.locator('.affine-reference-title')).toContainText(year); + // }); }); test('should save open doc mode of internal links', async ({ page }) => { diff --git a/yarn.lock b/yarn.lock index b85ea49dfc..e57230b70c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -184,7 +184,7 @@ __metadata: "@affine/error": "workspace:*" "@affine/graphql": "workspace:*" "@affine/routes": "workspace:*" - "@blocksuite/icons": "npm:^2.2.12" + "@blocksuite/icons": "npm:^2.2.17" "@radix-ui/react-accordion": "npm:^1.2.2" "@radix-ui/react-alert-dialog": "npm:^1.1.3" "@radix-ui/react-aspect-ratio": "npm:^1.1.1" @@ -253,7 +253,7 @@ __metadata: "@affine/i18n": "workspace:*" "@affine/nbstore": "workspace:*" "@blocksuite/affine": "workspace:*" - "@blocksuite/icons": "npm:^2.2.12" + "@blocksuite/icons": "npm:^2.2.17" "@capacitor/android": "npm:^7.0.0" "@capacitor/app": "npm:^7.0.0" "@capacitor/cli": "npm:^7.0.0" @@ -312,10 +312,8 @@ __metadata: "@atlaskit/pragmatic-drag-and-drop": "npm:^1.4.0" "@atlaskit/pragmatic-drag-and-drop-hitbox": "npm:^1.0.3" "@blocksuite/affine": "workspace:*" - "@blocksuite/icons": "npm:^2.2.12" + "@blocksuite/icons": "npm:^2.2.17" "@chromatic-com/storybook": "npm:^4.0.0" - "@emoji-mart/data": "npm:^1.2.1" - "@emoji-mart/react": "npm:^1.1.1" "@emotion/react": "npm:^11.14.0" "@emotion/styled": "npm:^11.14.0" "@radix-ui/react-avatar": "npm:^1.1.2" @@ -348,6 +346,7 @@ __metadata: clsx: "npm:^2.1.1" dayjs: "npm:^1.11.13" emoji-mart: "npm:^5.6.0" + emojibase-data: "npm:^16.0.3" foxact: "npm:^0.2.45" jotai: "npm:^2.10.3" lit: "npm:^3.2.1" @@ -409,7 +408,7 @@ __metadata: "@blocksuite/affine-ext-loader": "workspace:*" "@blocksuite/affine-shared": "workspace:*" "@blocksuite/global": "workspace:*" - "@blocksuite/icons": "npm:^2.2.13" + "@blocksuite/icons": "npm:^2.2.17" "@blocksuite/std": "workspace:*" "@dotlottie/player-component": "npm:^2.7.12" "@emotion/cache": "npm:^11.14.0" @@ -687,7 +686,7 @@ __metadata: "@affine/native": "workspace:*" "@affine/nbstore": "workspace:*" "@blocksuite/affine": "workspace:*" - "@blocksuite/icons": "npm:^2.2.12" + "@blocksuite/icons": "npm:^2.2.17" "@capacitor/app": "npm:^7.0.0" "@capacitor/browser": "npm:^7.0.0" "@capacitor/cli": "npm:^7.0.0" @@ -754,7 +753,7 @@ __metadata: "@affine/i18n": "workspace:*" "@affine/nbstore": "workspace:*" "@blocksuite/affine": "workspace:*" - "@blocksuite/icons": "npm:^2.2.12" + "@blocksuite/icons": "npm:^2.2.17" "@sentry/react": "npm:^9.2.0" "@toeverything/infra": "workspace:*" "@types/react": "npm:^19.0.1" @@ -774,7 +773,7 @@ __metadata: "@affine-tools/cli": "workspace:*" "@capacitor/cli": "npm:^7.0.0" "@eslint/js": "npm:^9.16.0" - "@faker-js/faker": "npm:^9.3.0" + "@faker-js/faker": "npm:^10.0.0" "@istanbuljs/schema": "npm:^0.1.3" "@magic-works/i18n-codegen": "npm:^0.6.1" "@playwright/test": "npm:=1.52.0" @@ -930,7 +929,7 @@ __metadata: "@apollo/server": "npm:^4.11.3" "@aws-sdk/client-s3": "npm:^3.779.0" "@aws-sdk/s3-request-presigner": "npm:^3.779.0" - "@faker-js/faker": "npm:^9.6.0" + "@faker-js/faker": "npm:^10.0.0" "@fal-ai/serverless-client": "npm:^0.15.0" "@google-cloud/opentelemetry-cloud-trace-exporter": "npm:^2.4.1" "@google-cloud/opentelemetry-resource-util": "npm:^2.4.0" @@ -945,6 +944,7 @@ __metadata: "@nestjs/platform-express": "npm:^11.0.12" "@nestjs/platform-socket.io": "npm:^11.0.12" "@nestjs/schedule": "npm:^6.0.0" + "@nestjs/swagger": "npm:^11.2.0" "@nestjs/testing": "patch:@nestjs/testing@npm%3A10.4.15#~/.yarn/patches/@nestjs-testing-npm-10.4.15-d591a1705a.patch" "@nestjs/throttler": "npm:^6.4.0" "@nestjs/websockets": "npm:^11.0.12" @@ -2433,7 +2433,7 @@ __metadata: "@blocksuite/affine-shared": "workspace:*" "@blocksuite/affine-widget-slash-menu": "workspace:*" "@blocksuite/global": "workspace:*" - "@blocksuite/icons": "npm:^2.2.12" + "@blocksuite/icons": "npm:^2.2.17" "@blocksuite/std": "workspace:*" "@blocksuite/store": "workspace:*" "@floating-ui/dom": "npm:^1.6.13" @@ -2461,7 +2461,7 @@ __metadata: "@blocksuite/affine-shared": "workspace:*" "@blocksuite/affine-widget-slash-menu": "workspace:*" "@blocksuite/global": "workspace:*" - "@blocksuite/icons": "npm:^2.2.12" + "@blocksuite/icons": "npm:^2.2.17" "@blocksuite/std": "workspace:*" "@blocksuite/store": "workspace:*" "@lit/context": "npm:^1.1.2" @@ -2488,7 +2488,7 @@ __metadata: "@blocksuite/affine-shared": "workspace:*" "@blocksuite/affine-widget-slash-menu": "workspace:*" "@blocksuite/global": "workspace:*" - "@blocksuite/icons": "npm:^2.2.12" + "@blocksuite/icons": "npm:^2.2.17" "@blocksuite/std": "workspace:*" "@blocksuite/store": "workspace:*" "@emoji-mart/data": "npm:^1.2.1" @@ -2521,7 +2521,7 @@ __metadata: "@blocksuite/affine-shared": "workspace:*" "@blocksuite/affine-widget-slash-menu": "workspace:*" "@blocksuite/global": "workspace:*" - "@blocksuite/icons": "npm:^2.2.12" + "@blocksuite/icons": "npm:^2.2.17" "@blocksuite/std": "workspace:*" "@blocksuite/store": "workspace:*" "@floating-ui/dom": "npm:^1.6.13" @@ -2549,7 +2549,7 @@ __metadata: "@blocksuite/affine-widget-slash-menu": "workspace:*" "@blocksuite/data-view": "workspace:*" "@blocksuite/global": "workspace:*" - "@blocksuite/icons": "npm:^2.2.12" + "@blocksuite/icons": "npm:^2.2.17" "@blocksuite/std": "workspace:*" "@blocksuite/store": "workspace:*" "@floating-ui/dom": "npm:^1.6.13" @@ -2579,7 +2579,7 @@ __metadata: "@blocksuite/affine-widget-slash-menu": "workspace:*" "@blocksuite/data-view": "workspace:*" "@blocksuite/global": "workspace:*" - "@blocksuite/icons": "npm:^2.2.12" + "@blocksuite/icons": "npm:^2.2.17" "@blocksuite/std": "workspace:*" "@blocksuite/store": "workspace:*" "@emotion/css": "npm:^11.13.5" @@ -2635,7 +2635,7 @@ __metadata: "@blocksuite/affine-shared": "workspace:*" "@blocksuite/affine-widget-edgeless-toolbar": "workspace:*" "@blocksuite/global": "workspace:*" - "@blocksuite/icons": "npm:^2.2.12" + "@blocksuite/icons": "npm:^2.2.17" "@blocksuite/std": "workspace:*" "@blocksuite/store": "workspace:*" "@floating-ui/dom": "npm:^1.6.13" @@ -2663,7 +2663,7 @@ __metadata: "@blocksuite/affine-shared": "workspace:*" "@blocksuite/affine-widget-slash-menu": "workspace:*" "@blocksuite/global": "workspace:*" - "@blocksuite/icons": "npm:^2.2.12" + "@blocksuite/icons": "npm:^2.2.17" "@blocksuite/std": "workspace:*" "@blocksuite/store": "workspace:*" "@floating-ui/dom": "npm:^1.6.13" @@ -2695,7 +2695,7 @@ __metadata: "@blocksuite/affine-shared": "workspace:*" "@blocksuite/affine-widget-slash-menu": "workspace:*" "@blocksuite/global": "workspace:*" - "@blocksuite/icons": "npm:^2.2.12" + "@blocksuite/icons": "npm:^2.2.17" "@blocksuite/std": "workspace:*" "@blocksuite/store": "workspace:*" "@floating-ui/dom": "npm:^1.6.13" @@ -2726,7 +2726,7 @@ __metadata: "@blocksuite/affine-widget-edgeless-toolbar": "workspace:*" "@blocksuite/affine-widget-frame-title": "workspace:*" "@blocksuite/global": "workspace:*" - "@blocksuite/icons": "npm:^2.2.12" + "@blocksuite/icons": "npm:^2.2.17" "@blocksuite/std": "workspace:*" "@blocksuite/store": "workspace:*" "@floating-ui/dom": "npm:^1.6.13" @@ -2755,7 +2755,7 @@ __metadata: "@blocksuite/affine-shared": "workspace:*" "@blocksuite/affine-widget-slash-menu": "workspace:*" "@blocksuite/global": "workspace:*" - "@blocksuite/icons": "npm:^2.2.12" + "@blocksuite/icons": "npm:^2.2.17" "@blocksuite/std": "workspace:*" "@blocksuite/store": "workspace:*" "@floating-ui/dom": "npm:^1.6.13" @@ -2783,7 +2783,7 @@ __metadata: "@blocksuite/affine-shared": "workspace:*" "@blocksuite/affine-widget-slash-menu": "workspace:*" "@blocksuite/global": "workspace:*" - "@blocksuite/icons": "npm:^2.2.12" + "@blocksuite/icons": "npm:^2.2.17" "@blocksuite/std": "workspace:*" "@blocksuite/store": "workspace:*" "@floating-ui/dom": "npm:^1.6.13" @@ -2813,7 +2813,7 @@ __metadata: "@blocksuite/affine-rich-text": "workspace:*" "@blocksuite/affine-shared": "workspace:*" "@blocksuite/global": "workspace:*" - "@blocksuite/icons": "npm:^2.2.12" + "@blocksuite/icons": "npm:^2.2.17" "@blocksuite/std": "workspace:*" "@blocksuite/store": "workspace:*" "@floating-ui/dom": "npm:^1.6.13" @@ -2845,7 +2845,7 @@ __metadata: "@blocksuite/affine-shared": "workspace:*" "@blocksuite/affine-widget-slash-menu": "workspace:*" "@blocksuite/global": "workspace:*" - "@blocksuite/icons": "npm:^2.2.12" + "@blocksuite/icons": "npm:^2.2.17" "@blocksuite/std": "workspace:*" "@blocksuite/store": "workspace:*" "@lit/context": "npm:^1.1.2" @@ -2920,7 +2920,7 @@ __metadata: "@blocksuite/affine-widget-edgeless-toolbar": "workspace:*" "@blocksuite/data-view": "workspace:*" "@blocksuite/global": "workspace:*" - "@blocksuite/icons": "npm:^2.2.12" + "@blocksuite/icons": "npm:^2.2.17" "@blocksuite/std": "workspace:*" "@blocksuite/store": "workspace:*" "@floating-ui/dom": "npm:^1.6.13" @@ -2952,7 +2952,7 @@ __metadata: "@blocksuite/affine-shared": "workspace:*" "@blocksuite/affine-widget-slash-menu": "workspace:*" "@blocksuite/global": "workspace:*" - "@blocksuite/icons": "npm:^2.2.12" + "@blocksuite/icons": "npm:^2.2.17" "@blocksuite/std": "workspace:*" "@blocksuite/store": "workspace:*" "@floating-ui/dom": "npm:^1.6.13" @@ -3012,7 +3012,7 @@ __metadata: "@blocksuite/affine-widget-slash-menu": "workspace:*" "@blocksuite/data-view": "workspace:*" "@blocksuite/global": "workspace:*" - "@blocksuite/icons": "npm:^2.2.12" + "@blocksuite/icons": "npm:^2.2.17" "@blocksuite/std": "workspace:*" "@blocksuite/store": "workspace:*" "@emotion/css": "npm:^11.13.5" @@ -3032,7 +3032,7 @@ __metadata: "@blocksuite/affine-model": "workspace:*" "@blocksuite/affine-shared": "workspace:*" "@blocksuite/global": "workspace:*" - "@blocksuite/icons": "npm:^2.2.12" + "@blocksuite/icons": "npm:^2.2.17" "@blocksuite/std": "workspace:*" "@blocksuite/store": "workspace:*" "@blocksuite/sync": "workspace:*" @@ -3080,7 +3080,7 @@ __metadata: "@blocksuite/affine-shared": "workspace:*" "@blocksuite/data-view": "workspace:*" "@blocksuite/global": "workspace:*" - "@blocksuite/icons": "npm:^2.2.12" + "@blocksuite/icons": "npm:^2.2.17" "@blocksuite/std": "workspace:*" "@blocksuite/store": "workspace:*" "@floating-ui/dom": "npm:^1.6.13" @@ -3105,7 +3105,7 @@ __metadata: "@blocksuite/affine-model": "workspace:*" "@blocksuite/affine-shared": "workspace:*" "@blocksuite/global": "workspace:*" - "@blocksuite/icons": "npm:^2.2.12" + "@blocksuite/icons": "npm:^2.2.17" "@blocksuite/std": "workspace:*" "@blocksuite/store": "workspace:*" "@floating-ui/dom": "npm:^1.6.13" @@ -3129,7 +3129,7 @@ __metadata: "@blocksuite/affine-rich-text": "workspace:*" "@blocksuite/affine-shared": "workspace:*" "@blocksuite/global": "workspace:*" - "@blocksuite/icons": "npm:^2.2.12" + "@blocksuite/icons": "npm:^2.2.17" "@blocksuite/std": "workspace:*" "@blocksuite/store": "workspace:*" "@floating-ui/dom": "npm:^1.6.13" @@ -3155,7 +3155,7 @@ __metadata: "@blocksuite/affine-rich-text": "workspace:*" "@blocksuite/affine-shared": "workspace:*" "@blocksuite/global": "workspace:*" - "@blocksuite/icons": "npm:^2.2.12" + "@blocksuite/icons": "npm:^2.2.17" "@blocksuite/std": "workspace:*" "@blocksuite/store": "workspace:*" "@floating-ui/dom": "npm:^1.6.13" @@ -3183,7 +3183,7 @@ __metadata: "@blocksuite/affine-rich-text": "workspace:*" "@blocksuite/affine-shared": "workspace:*" "@blocksuite/global": "workspace:*" - "@blocksuite/icons": "npm:^2.2.12" + "@blocksuite/icons": "npm:^2.2.17" "@blocksuite/std": "workspace:*" "@blocksuite/store": "workspace:*" "@floating-ui/dom": "npm:^1.6.13" @@ -3210,7 +3210,7 @@ __metadata: "@blocksuite/affine-shared": "workspace:*" "@blocksuite/affine-widget-edgeless-toolbar": "workspace:*" "@blocksuite/global": "workspace:*" - "@blocksuite/icons": "npm:^2.2.12" + "@blocksuite/icons": "npm:^2.2.17" "@blocksuite/std": "workspace:*" "@blocksuite/store": "workspace:*" "@lit/context": "npm:^1.1.2" @@ -3239,7 +3239,7 @@ __metadata: "@blocksuite/affine-shared": "workspace:*" "@blocksuite/affine-widget-edgeless-toolbar": "workspace:*" "@blocksuite/global": "workspace:*" - "@blocksuite/icons": "npm:^2.2.12" + "@blocksuite/icons": "npm:^2.2.17" "@blocksuite/std": "workspace:*" "@blocksuite/store": "workspace:*" "@lit/context": "npm:^1.1.2" @@ -3268,7 +3268,7 @@ __metadata: "@blocksuite/affine-shared": "workspace:*" "@blocksuite/affine-widget-edgeless-toolbar": "workspace:*" "@blocksuite/global": "workspace:*" - "@blocksuite/icons": "npm:^2.2.12" + "@blocksuite/icons": "npm:^2.2.17" "@blocksuite/std": "workspace:*" "@blocksuite/store": "workspace:*" "@lit/context": "npm:^1.1.2" @@ -3299,7 +3299,7 @@ __metadata: "@blocksuite/affine-shared": "workspace:*" "@blocksuite/affine-widget-edgeless-toolbar": "workspace:*" "@blocksuite/global": "workspace:*" - "@blocksuite/icons": "npm:^2.2.12" + "@blocksuite/icons": "npm:^2.2.17" "@blocksuite/std": "workspace:*" "@blocksuite/store": "workspace:*" "@lit/context": "npm:^1.1.2" @@ -3334,7 +3334,7 @@ __metadata: "@blocksuite/affine-shared": "workspace:*" "@blocksuite/affine-widget-edgeless-toolbar": "workspace:*" "@blocksuite/global": "workspace:*" - "@blocksuite/icons": "npm:^2.2.12" + "@blocksuite/icons": "npm:^2.2.17" "@blocksuite/std": "workspace:*" "@blocksuite/store": "workspace:*" "@lit/context": "npm:^1.1.2" @@ -3366,7 +3366,7 @@ __metadata: "@blocksuite/affine-shared": "workspace:*" "@blocksuite/affine-widget-edgeless-toolbar": "workspace:*" "@blocksuite/global": "workspace:*" - "@blocksuite/icons": "npm:^2.2.12" + "@blocksuite/icons": "npm:^2.2.17" "@blocksuite/std": "workspace:*" "@blocksuite/store": "workspace:*" "@lit/context": "npm:^1.1.2" @@ -3394,7 +3394,7 @@ __metadata: "@blocksuite/affine-shared": "workspace:*" "@blocksuite/affine-widget-edgeless-toolbar": "workspace:*" "@blocksuite/global": "workspace:*" - "@blocksuite/icons": "npm:^2.2.12" + "@blocksuite/icons": "npm:^2.2.17" "@blocksuite/std": "workspace:*" "@blocksuite/store": "workspace:*" "@lit/context": "npm:^1.1.2" @@ -3423,7 +3423,7 @@ __metadata: "@blocksuite/affine-shared": "workspace:*" "@blocksuite/affine-widget-edgeless-toolbar": "workspace:*" "@blocksuite/global": "workspace:*" - "@blocksuite/icons": "npm:^2.2.12" + "@blocksuite/icons": "npm:^2.2.17" "@blocksuite/std": "workspace:*" "@blocksuite/store": "workspace:*" "@lit/context": "npm:^1.1.2" @@ -3452,7 +3452,7 @@ __metadata: "@blocksuite/affine-shared": "workspace:*" "@blocksuite/affine-widget-edgeless-toolbar": "workspace:*" "@blocksuite/global": "workspace:*" - "@blocksuite/icons": "npm:^2.2.12" + "@blocksuite/icons": "npm:^2.2.17" "@blocksuite/std": "workspace:*" "@blocksuite/store": "workspace:*" "@floating-ui/dom": "npm:^1.6.13" @@ -3481,7 +3481,7 @@ __metadata: "@blocksuite/affine-shared": "workspace:*" "@blocksuite/affine-widget-edgeless-toolbar": "workspace:*" "@blocksuite/global": "workspace:*" - "@blocksuite/icons": "npm:^2.2.12" + "@blocksuite/icons": "npm:^2.2.17" "@blocksuite/std": "workspace:*" "@blocksuite/store": "workspace:*" "@lit/context": "npm:^1.1.2" @@ -3543,7 +3543,7 @@ __metadata: "@blocksuite/affine-model": "workspace:*" "@blocksuite/affine-shared": "workspace:*" "@blocksuite/global": "workspace:*" - "@blocksuite/icons": "npm:^2.2.12" + "@blocksuite/icons": "npm:^2.2.17" "@blocksuite/std": "workspace:*" "@blocksuite/store": "workspace:*" "@floating-ui/dom": "npm:^1.6.13" @@ -3574,7 +3574,7 @@ __metadata: "@blocksuite/affine-rich-text": "workspace:*" "@blocksuite/affine-shared": "workspace:*" "@blocksuite/global": "workspace:*" - "@blocksuite/icons": "npm:^2.2.12" + "@blocksuite/icons": "npm:^2.2.17" "@blocksuite/std": "workspace:*" "@blocksuite/store": "workspace:*" "@floating-ui/dom": "npm:^1.6.13" @@ -3608,7 +3608,7 @@ __metadata: "@blocksuite/affine-model": "workspace:*" "@blocksuite/affine-shared": "workspace:*" "@blocksuite/global": "workspace:*" - "@blocksuite/icons": "npm:^2.2.12" + "@blocksuite/icons": "npm:^2.2.17" "@blocksuite/std": "workspace:*" "@blocksuite/store": "workspace:*" "@floating-ui/dom": "npm:^1.6.13" @@ -3636,7 +3636,7 @@ __metadata: "@blocksuite/affine-model": "workspace:*" "@blocksuite/affine-shared": "workspace:*" "@blocksuite/global": "workspace:*" - "@blocksuite/icons": "npm:^2.2.12" + "@blocksuite/icons": "npm:^2.2.17" "@blocksuite/std": "workspace:*" "@blocksuite/store": "workspace:*" "@floating-ui/dom": "npm:^1.6.13" @@ -3671,7 +3671,7 @@ __metadata: "@blocksuite/affine-rich-text": "workspace:*" "@blocksuite/affine-shared": "workspace:*" "@blocksuite/global": "workspace:*" - "@blocksuite/icons": "npm:^2.2.12" + "@blocksuite/icons": "npm:^2.2.17" "@blocksuite/std": "workspace:*" "@blocksuite/store": "workspace:*" "@floating-ui/dom": "npm:^1.6.13" @@ -3705,7 +3705,7 @@ __metadata: "@blocksuite/affine-model": "workspace:*" "@blocksuite/affine-shared": "workspace:*" "@blocksuite/global": "workspace:*" - "@blocksuite/icons": "npm:^2.2.12" + "@blocksuite/icons": "npm:^2.2.17" "@blocksuite/std": "workspace:*" "@blocksuite/store": "workspace:*" "@floating-ui/dom": "npm:^1.6.13" @@ -3749,7 +3749,7 @@ __metadata: "@blocksuite/affine-model": "workspace:*" "@blocksuite/affine-shared": "workspace:*" "@blocksuite/global": "workspace:*" - "@blocksuite/icons": "npm:^2.2.12" + "@blocksuite/icons": "npm:^2.2.17" "@blocksuite/std": "workspace:*" "@blocksuite/store": "workspace:*" "@floating-ui/dom": "npm:^1.6.13" @@ -3774,7 +3774,7 @@ __metadata: dependencies: "@blocksuite/affine-model": "workspace:*" "@blocksuite/global": "workspace:*" - "@blocksuite/icons": "npm:^2.2.12" + "@blocksuite/icons": "npm:^2.2.17" "@blocksuite/std": "workspace:*" "@blocksuite/store": "workspace:*" "@floating-ui/dom": "npm:^1.6.13" @@ -3833,7 +3833,7 @@ __metadata: "@blocksuite/affine-model": "workspace:*" "@blocksuite/affine-shared": "workspace:*" "@blocksuite/global": "workspace:*" - "@blocksuite/icons": "npm:^2.2.12" + "@blocksuite/icons": "npm:^2.2.17" "@blocksuite/std": "workspace:*" "@blocksuite/store": "workspace:*" "@floating-ui/dom": "npm:^1.6.13" @@ -3860,7 +3860,7 @@ __metadata: "@blocksuite/affine-model": "workspace:*" "@blocksuite/affine-shared": "workspace:*" "@blocksuite/global": "workspace:*" - "@blocksuite/icons": "npm:^2.2.12" + "@blocksuite/icons": "npm:^2.2.17" "@blocksuite/std": "workspace:*" "@preact/signals-core": "npm:^1.8.0" "@toeverything/theme": "npm:^1.1.16" @@ -3880,7 +3880,7 @@ __metadata: "@blocksuite/affine-model": "workspace:*" "@blocksuite/affine-shared": "workspace:*" "@blocksuite/global": "workspace:*" - "@blocksuite/icons": "npm:^2.2.12" + "@blocksuite/icons": "npm:^2.2.17" "@blocksuite/std": "workspace:*" "@lit/context": "npm:^1.1.2" "@preact/signals-core": "npm:^1.8.0" @@ -3906,7 +3906,7 @@ __metadata: "@blocksuite/affine-model": "workspace:*" "@blocksuite/affine-shared": "workspace:*" "@blocksuite/global": "workspace:*" - "@blocksuite/icons": "npm:^2.2.12" + "@blocksuite/icons": "npm:^2.2.17" "@blocksuite/std": "workspace:*" "@lit/context": "npm:^1.1.2" "@preact/signals-core": "npm:^1.8.0" @@ -3928,7 +3928,7 @@ __metadata: "@blocksuite/affine-rich-text": "workspace:*" "@blocksuite/affine-shared": "workspace:*" "@blocksuite/global": "workspace:*" - "@blocksuite/icons": "npm:^2.2.12" + "@blocksuite/icons": "npm:^2.2.17" "@blocksuite/std": "workspace:*" "@floating-ui/dom": "npm:^1.6.13" "@lit/context": "npm:^1.1.2" @@ -3951,7 +3951,7 @@ __metadata: "@blocksuite/affine-model": "workspace:*" "@blocksuite/affine-shared": "workspace:*" "@blocksuite/global": "workspace:*" - "@blocksuite/icons": "npm:^2.2.12" + "@blocksuite/icons": "npm:^2.2.17" "@blocksuite/std": "workspace:*" "@floating-ui/dom": "npm:^1.6.13" "@preact/signals-core": "npm:^1.8.0" @@ -4010,7 +4010,7 @@ __metadata: "@blocksuite/affine-shared": "workspace:*" "@blocksuite/data-view": "workspace:*" "@blocksuite/global": "workspace:*" - "@blocksuite/icons": "npm:^2.2.12" + "@blocksuite/icons": "npm:^2.2.17" "@blocksuite/std": "workspace:*" "@blocksuite/store": "workspace:*" "@lit/context": "npm:^1.1.2" @@ -4036,7 +4036,7 @@ __metadata: "@blocksuite/affine-rich-text": "workspace:*" "@blocksuite/affine-shared": "workspace:*" "@blocksuite/global": "workspace:*" - "@blocksuite/icons": "npm:^2.2.12" + "@blocksuite/icons": "npm:^2.2.17" "@blocksuite/std": "workspace:*" "@blocksuite/store": "workspace:*" "@lit/context": "npm:^1.1.2" @@ -4063,7 +4063,7 @@ __metadata: "@blocksuite/affine-shared": "workspace:*" "@blocksuite/affine-widget-edgeless-selected-rect": "workspace:*" "@blocksuite/global": "workspace:*" - "@blocksuite/icons": "npm:^2.2.12" + "@blocksuite/icons": "npm:^2.2.17" "@blocksuite/std": "workspace:*" "@lit/context": "npm:^1.1.2" "@preact/signals-core": "npm:^1.8.0" @@ -4083,7 +4083,7 @@ __metadata: "@blocksuite/affine-model": "workspace:*" "@blocksuite/affine-shared": "workspace:*" "@blocksuite/global": "workspace:*" - "@blocksuite/icons": "npm:^2.2.12" + "@blocksuite/icons": "npm:^2.2.17" "@blocksuite/std": "workspace:*" "@blocksuite/store": "workspace:*" "@lit/context": "npm:^1.1.2" @@ -4107,7 +4107,7 @@ __metadata: "@blocksuite/affine-model": "workspace:*" "@blocksuite/affine-shared": "workspace:*" "@blocksuite/global": "workspace:*" - "@blocksuite/icons": "npm:^2.2.12" + "@blocksuite/icons": "npm:^2.2.17" "@blocksuite/std": "workspace:*" "@preact/signals-core": "npm:^1.8.0" "@toeverything/theme": "npm:^1.1.16" @@ -4143,7 +4143,7 @@ __metadata: "@blocksuite/affine-rich-text": "workspace:*" "@blocksuite/affine-shared": "workspace:*" "@blocksuite/global": "workspace:*" - "@blocksuite/icons": "npm:^2.2.12" + "@blocksuite/icons": "npm:^2.2.17" "@blocksuite/std": "workspace:*" "@blocksuite/store": "workspace:*" "@floating-ui/dom": "npm:^1.6.13" @@ -4168,7 +4168,7 @@ __metadata: "@blocksuite/affine-model": "workspace:*" "@blocksuite/affine-shared": "workspace:*" "@blocksuite/global": "workspace:*" - "@blocksuite/icons": "npm:^2.2.12" + "@blocksuite/icons": "npm:^2.2.17" "@blocksuite/std": "workspace:*" "@floating-ui/dom": "npm:^1.6.13" "@preact/signals-core": "npm:^1.8.0" @@ -4189,7 +4189,7 @@ __metadata: "@blocksuite/affine-model": "workspace:*" "@blocksuite/affine-shared": "workspace:*" "@blocksuite/global": "workspace:*" - "@blocksuite/icons": "npm:^2.2.12" + "@blocksuite/icons": "npm:^2.2.17" "@blocksuite/std": "workspace:*" "@floating-ui/dom": "npm:^1.6.13" "@preact/signals-core": "npm:^1.8.0" @@ -4298,7 +4298,7 @@ __metadata: "@blocksuite/affine-components": "workspace:*" "@blocksuite/affine-shared": "workspace:*" "@blocksuite/global": "workspace:*" - "@blocksuite/icons": "npm:^2.2.12" + "@blocksuite/icons": "npm:^2.2.17" "@blocksuite/std": "workspace:*" "@blocksuite/store": "workspace:*" "@emotion/css": "npm:^11.13.5" @@ -4332,9 +4332,9 @@ __metadata: languageName: unknown linkType: soft -"@blocksuite/icons@npm:^2.2.12, @blocksuite/icons@npm:^2.2.13": - version: 2.2.15 - resolution: "@blocksuite/icons@npm:2.2.15" +"@blocksuite/icons@npm:^2.2.17": + version: 2.2.17 + resolution: "@blocksuite/icons@npm:2.2.17" peerDependencies: "@types/react": ^18.0.25 change-case: 4.1.2 @@ -4345,7 +4345,7 @@ __metadata: optional: true react: optional: true - checksum: 10/891c646b5a1c5ee8f4e42752053a7aab7e55ed8f6f6a08e0fe9371172c92ad5b861355d017175426871a422f6364a8d18aa88a1f96aa33029bf2a48d33f70c40 + checksum: 10/a9f0f96507bb1df537479d59693f6468f230e5b18bcab9fcfb62458650ee9ccf7d0972fb21029ad9594f7f312b5f92692ad5acff9a7281bc5288351769d73409 languageName: node linkType: hard @@ -4354,7 +4354,7 @@ __metadata: resolution: "@blocksuite/integration-test@workspace:blocksuite/integration-test" dependencies: "@blocksuite/affine": "workspace:*" - "@blocksuite/icons": "npm:^2.2.12" + "@blocksuite/icons": "npm:^2.2.17" "@floating-ui/dom": "npm:^1.6.13" "@lit/context": "npm:^1.1.3" "@lottiefiles/dotlottie-wc": "npm:^0.5.0" @@ -4382,7 +4382,7 @@ __metadata: "@blocksuite/affine-model": "workspace:*" "@blocksuite/affine-shared": "workspace:*" "@blocksuite/data-view": "workspace:*" - "@blocksuite/icons": "npm:^2.2.12" + "@blocksuite/icons": "npm:^2.2.17" "@blocksuite/integration-test": "workspace:*" "@preact/signals-core": "npm:^1.8.0" "@shoelace-style/shoelace": "npm:2.20.1" @@ -5530,16 +5530,6 @@ __metadata: languageName: node linkType: hard -"@emoji-mart/react@npm:^1.1.1": - version: 1.1.1 - resolution: "@emoji-mart/react@npm:1.1.1" - peerDependencies: - emoji-mart: ^5.2 - react: ^16.8 || ^17 || ^18 - checksum: 10/def4dddaa01ce88c396510d84d1878b881fe0f9c484a836a50e3db784a91ab98edf94816cff503b4d1cab7b00400a01c87e32d19dee2d950f64ef9243f79101e - languageName: node - linkType: hard - "@emotion/babel-plugin@npm:^11.13.5": version: 11.13.5 resolution: "@emotion/babel-plugin@npm:11.13.5" @@ -6013,10 +6003,10 @@ __metadata: languageName: node linkType: hard -"@faker-js/faker@npm:^9.3.0, @faker-js/faker@npm:^9.6.0": - version: 9.8.0 - resolution: "@faker-js/faker@npm:9.8.0" - checksum: 10/d7d7940ba7f2bd0d266d507bb7a33ec5099654da624dc8647d7cfd2e908b75ba06ed7dd0ec3b771a18600a734f420370b3cecd1ee5d4799993b1ee3505f866d8 +"@faker-js/faker@npm:^10.0.0": + version: 10.0.0 + resolution: "@faker-js/faker@npm:10.0.0" + checksum: 10/14e558e66bf7d8ffb6c00f241c4763dc8295baee5d7be56a2b40c7150cd6b7404c46d82a5558144fa7635c4391eb412fb10bad12c87b9ba844d12d904fe55d73 languageName: node linkType: hard @@ -7963,6 +7953,13 @@ __metadata: languageName: node linkType: hard +"@microsoft/tsdoc@npm:0.15.1": + version: 0.15.1 + resolution: "@microsoft/tsdoc@npm:0.15.1" + checksum: 10/1a92612883088fe184dba596e7ba7a0daef0e6981caeca22bad6ad551d2247294f12e368537d0d8192525cf5743f7f15fcc2ad7b3b849f26a09a15ffdd89fd0c + languageName: node + linkType: hard + "@modelcontextprotocol/sdk@npm:^1.16.0, @modelcontextprotocol/sdk@npm:^1.8.0": version: 1.16.0 resolution: "@modelcontextprotocol/sdk@npm:1.16.0" @@ -9110,35 +9107,35 @@ __metadata: languageName: node linkType: hard -"@nestjs/bull-shared@npm:^11.0.2": - version: 11.0.2 - resolution: "@nestjs/bull-shared@npm:11.0.2" +"@nestjs/bull-shared@npm:^11.0.3": + version: 11.0.3 + resolution: "@nestjs/bull-shared@npm:11.0.3" dependencies: tslib: "npm:2.8.1" peerDependencies: "@nestjs/common": ^10.0.0 || ^11.0.0 "@nestjs/core": ^10.0.0 || ^11.0.0 - checksum: 10/5ddd639104241c3d7cfef0366fda3b9900dcb250b1a251566bc866a1dab1ada84ce5118efc1455d27a261e86f4b802893237a00e5876748ffb0759086aa02a13 + checksum: 10/3e24f66f4673bf9213fc20c495ef6c62860a839ce4f385453c190fe57840b82adb76b3244d97168d1ea0b557cbb861e739980117422daddd0692ba1013882ae2 languageName: node linkType: hard "@nestjs/bullmq@npm:^11.0.2": - version: 11.0.2 - resolution: "@nestjs/bullmq@npm:11.0.2" + version: 11.0.3 + resolution: "@nestjs/bullmq@npm:11.0.3" dependencies: - "@nestjs/bull-shared": "npm:^11.0.2" + "@nestjs/bull-shared": "npm:^11.0.3" tslib: "npm:2.8.1" peerDependencies: "@nestjs/common": ^10.0.0 || ^11.0.0 "@nestjs/core": ^10.0.0 || ^11.0.0 bullmq: ^3.0.0 || ^4.0.0 || ^5.0.0 - checksum: 10/eac510862401d06898e3222e5979c0b16c0026e0048858d64365a714538bd4838d3d030c0cc18b396a6eab978aab0cc33b82dd3fb340757fcb813073b0cae370 + checksum: 10/3f5fc685b5ea7d1d9590ea6b3594df3e684a208a115e637b1366963af0167276d4eee35b2085324ed6be4d751d249466ccf707a28120c090063f0d6afc8c2015 languageName: node linkType: hard "@nestjs/common@npm:^11.0.12": - version: 11.1.5 - resolution: "@nestjs/common@npm:11.1.5" + version: 11.1.6 + resolution: "@nestjs/common@npm:11.1.6" dependencies: file-type: "npm:21.0.0" iterare: "npm:1.2.1" @@ -9155,13 +9152,13 @@ __metadata: optional: true class-validator: optional: true - checksum: 10/419ccee948e6113751d6d2e949daff2fd00d255150414f29c5164e7393f0944a53e2366a387c9751afec73cab5fe019a9b1e2ef940cdd44876222b8b262643fe + checksum: 10/bb8a8f040ecaad5ea4938d166afa5e077060372001def3694999bdd8695dd6d69845c4569ade94aecb4f6b625198d3e848270ecc29be7ba5d73aefe5edd76984 languageName: node linkType: hard "@nestjs/core@npm:^11.0.12": - version: 11.1.5 - resolution: "@nestjs/core@npm:11.1.5" + version: 11.1.6 + resolution: "@nestjs/core@npm:11.1.6" dependencies: "@nuxt/opencollective": "npm:0.4.1" fast-safe-stringify: "npm:2.1.1" @@ -9183,7 +9180,7 @@ __metadata: optional: true "@nestjs/websockets": optional: true - checksum: 10/0086f5a8080bec35a53b4332dd1a786ed02e2e36d0fb5cd75ca1b19340b0a40d1a381a49817c494e445dc92243d9ba1d97746a5262ba721df327114d66d86422 + checksum: 10/d38c32eea96f9a475907e352b1f9f0441bf1bd4de2fa479836930e9670072deb1c1986253da324eb85bbcb884515d90ad2f10e88e49e491ac888b6893cc1faf3 languageName: node linkType: hard @@ -9244,8 +9241,8 @@ __metadata: linkType: hard "@nestjs/platform-express@npm:^11.0.12": - version: 11.1.5 - resolution: "@nestjs/platform-express@npm:11.1.5" + version: 11.1.6 + resolution: "@nestjs/platform-express@npm:11.1.6" dependencies: cors: "npm:2.8.5" express: "npm:5.1.0" @@ -9255,13 +9252,13 @@ __metadata: peerDependencies: "@nestjs/common": ^11.0.0 "@nestjs/core": ^11.0.0 - checksum: 10/b82cf1b80078fb15757448c279ff51e24cf210d7ceeaae2c2569553b772931ef396cd48d0b77045e63b78df632589d3b8ec433cddafc64f51df59f8fd41b8507 + checksum: 10/06e14cbfd587ba2ce51c2336d2ad4c5990e95f95ddf829b75125b998449a4186532ad0e65d148a566a49e3503d9e7dab476c3a4720d053ef430a243e70650d69 languageName: node linkType: hard "@nestjs/platform-socket.io@npm:^11.0.12": - version: 11.1.5 - resolution: "@nestjs/platform-socket.io@npm:11.1.5" + version: 11.1.6 + resolution: "@nestjs/platform-socket.io@npm:11.1.6" dependencies: socket.io: "npm:4.8.1" tslib: "npm:2.8.1" @@ -9269,19 +9266,47 @@ __metadata: "@nestjs/common": ^11.0.0 "@nestjs/websockets": ^11.0.0 rxjs: ^7.1.0 - checksum: 10/c6c3d96d98d095fde1766e8c294f4e1760816f337828e09293bede7747117ccf728b1f96f4ac3a2c1f7be0582824858b5ec0c89343fe96cb047d83e4dd2c3222 + checksum: 10/8d36586b2cdc66f19e35b55fbb47019871f6acf8d7d30330fe1965a35cbeb7f9b60424b35a6c61733410896d58b831c56eb31b62d16d4b8eb96b020fd416c3ff languageName: node linkType: hard "@nestjs/schedule@npm:^6.0.0": - version: 6.0.0 - resolution: "@nestjs/schedule@npm:6.0.0" + version: 6.0.1 + resolution: "@nestjs/schedule@npm:6.0.1" dependencies: - cron: "npm:4.3.0" + cron: "npm:4.3.3" peerDependencies: "@nestjs/common": ^10.0.0 || ^11.0.0 "@nestjs/core": ^10.0.0 || ^11.0.0 - checksum: 10/84f4c11f9fe3aa6d5e7b3c4d0e27f610e47394e87b2974bf0b7a169832b52343daf9ca1c5717caf642d9b134d8a0ae2ce5ab3dca5562d41781b92ddc96a91043 + checksum: 10/7c9e892c1e495ddd49531e783171a9a38b991d69d2060caa25709f886f458b034811bddebbb80fa7cdffd0f4578b0f81d4f0fbfe27e0ae11b6a358775fac8e6f + languageName: node + linkType: hard + +"@nestjs/swagger@npm:^11.2.0": + version: 11.2.0 + resolution: "@nestjs/swagger@npm:11.2.0" + dependencies: + "@microsoft/tsdoc": "npm:0.15.1" + "@nestjs/mapped-types": "npm:2.1.0" + js-yaml: "npm:4.1.0" + lodash: "npm:4.17.21" + path-to-regexp: "npm:8.2.0" + swagger-ui-dist: "npm:5.21.0" + peerDependencies: + "@fastify/static": ^8.0.0 + "@nestjs/common": ^11.0.1 + "@nestjs/core": ^11.0.1 + class-transformer: "*" + class-validator: "*" + reflect-metadata: ^0.1.12 || ^0.2.0 + peerDependenciesMeta: + "@fastify/static": + optional: true + class-transformer: + optional: true + class-validator: + optional: true + checksum: 10/2f8cee1f58ea5d887939d0d6f230f272a2526900057515d95253fb6d6c96dcd2d110396d70e9b74e6342cf83f4575b4dbe58ae97ba6252270b8318dcaa5fc8cd languageName: node linkType: hard @@ -9335,8 +9360,8 @@ __metadata: linkType: hard "@nestjs/websockets@npm:^11.0.12": - version: 11.1.5 - resolution: "@nestjs/websockets@npm:11.1.5" + version: 11.1.6 + resolution: "@nestjs/websockets@npm:11.1.6" dependencies: iterare: "npm:1.2.1" object-hash: "npm:3.0.0" @@ -9350,7 +9375,7 @@ __metadata: peerDependenciesMeta: "@nestjs/platform-socket.io": optional: true - checksum: 10/2a916dcd8bb75fff561b4d5c9a18304d33e0525de6162c24deef4908cd4a062261d4d8ac729b35eef0aedcc5837c9b344591ba9310784920f014811790a1999c + checksum: 10/b4ebae17afbfc5c70ee5d66dd0b7b23d3d6d12fb98678f5a00069e0f009916c0119583810b92bba878a99d5488fc3f6134ce84bf5f8cdfd73ff0d54ec0e4f33a languageName: node linkType: hard @@ -12711,6 +12736,13 @@ __metadata: languageName: node linkType: hard +"@scarf/scarf@npm:=1.4.0": + version: 1.4.0 + resolution: "@scarf/scarf@npm:1.4.0" + checksum: 10/1b39a18fa29e91cfbc134c588e20c5f01a1b21ec4473614123801155b48378e9c3bf72adaca8c67e433ae951ab653268e9502cc5733230d8927532f74a6b89c9 + languageName: node + linkType: hard + "@sec-ant/readable-stream@npm:^0.4.1": version: 0.4.1 resolution: "@sec-ant/readable-stream@npm:0.4.1" @@ -15152,10 +15184,10 @@ __metadata: languageName: node linkType: hard -"@types/luxon@npm:~3.6.0": - version: 3.6.2 - resolution: "@types/luxon@npm:3.6.2" - checksum: 10/73ca30059e0b1e352ce3a208837bc042e0bae9cf6e5b42f63de9ddfe15348a9e9bf9fcde3d4034038be24cb24adc579ae984cadff3bf70432e54fed1ad249d12 +"@types/luxon@npm:~3.7.0": + version: 3.7.1 + resolution: "@types/luxon@npm:3.7.1" + checksum: 10/c7bc164c278393ea0be938f986c74b4cddfab9013b1aff4495b016f771ded1d5b7b7b4825b2c7f0b8799edce19c5f531c28ff434ab3dedf994ac2d99a20fd4c4 languageName: node linkType: hard @@ -17299,13 +17331,13 @@ __metadata: linkType: hard "axios@npm:^1.8.3": - version: 1.9.0 - resolution: "axios@npm:1.9.0" + version: 1.12.2 + resolution: "axios@npm:1.12.2" dependencies: follow-redirects: "npm:^1.15.6" - form-data: "npm:^4.0.0" + form-data: "npm:^4.0.4" proxy-from-env: "npm:^1.1.0" - checksum: 10/a2f90bba56820883879f32a237e2b9ff25c250365dcafd41cec41b3406a3df334a148f90010182dfdadb4b41dc59f6f0b3e8898ff41b666d1157b5f3f4523497 + checksum: 10/886a79770594eaad76493fecf90344b567bd956240609b5dcd09bd0afe8d3e6f1ad6d3257a93a483b6192b409d4b673d9515a34619e3e3ed1b2c0ec2a83b20ba languageName: node linkType: hard @@ -19302,13 +19334,13 @@ __metadata: languageName: node linkType: hard -"cron@npm:4.3.0": - version: 4.3.0 - resolution: "cron@npm:4.3.0" +"cron@npm:4.3.3": + version: 4.3.3 + resolution: "cron@npm:4.3.3" dependencies: - "@types/luxon": "npm:~3.6.0" - luxon: "npm:~3.6.0" - checksum: 10/202d9eef7da7c3cae548ef0e0fd20a0ee2f41ca36cca1486ba4ecd16783d02ebc6c116c84572aef0ce6c85c9f290eba9ff00e1a8eda2053123cf2c9d68a4702a + "@types/luxon": "npm:~3.7.0" + luxon: "npm:~3.7.0" + checksum: 10/14019a639e08a8cd3eeb25dcc0541ee5cdae706ef925ac97a964ab7045284b3d3e7058f122dde6e9ff6931c836ea49c41675235d96e361365f6dd1e67bf0d347 languageName: node linkType: hard @@ -20509,14 +20541,14 @@ __metadata: linkType: hard "dompurify@npm:^3.2.4": - version: 3.2.6 - resolution: "dompurify@npm:3.2.6" + version: 3.2.7 + resolution: "dompurify@npm:3.2.7" dependencies: "@types/trusted-types": "npm:^2.0.7" dependenciesMeta: "@types/trusted-types": optional: true - checksum: 10/b91631ed0e4d17fae950ef53613cc009ed7e73adc43ac94a41dd52f35483f7538d13caebdafa7626e0da145fc8184e7ac7935f14f25b7e841b32fda777e40447 + checksum: 10/51b7866fb834ee62d6c415f41ece5ce11db7b463f60a822932a1f832573a40b98be7715550298690e7647988fbe086db1098bda9b10548b3166fc975eb9bd849 languageName: node linkType: hard @@ -20804,15 +20836,15 @@ __metadata: linkType: hard "electron@npm:^35.0.0": - version: 35.5.1 - resolution: "electron@npm:35.5.1" + version: 35.7.5 + resolution: "electron@npm:35.7.5" dependencies: "@electron/get": "npm:^2.0.0" "@types/node": "npm:^22.7.7" extract-zip: "npm:^2.0.1" bin: electron: cli.js - checksum: 10/765752c94c3bea799f62fb0a75049a10452ff97e791a96111f36b97293ffed9240b27dbc083c01f7a2c7172e84b6b394b07c9aaf91d57783958b2adb4e570579 + checksum: 10/f3757f0b6f21ddd5ebf8b83becdaa3e47dc13473e5375e6e6aee638d7029e73d08d6a4170b37ad8c594057da139e7cdf4c8b82b70c2c1ad0306abdcc274c5fee languageName: node linkType: hard @@ -20907,6 +20939,15 @@ __metadata: languageName: node linkType: hard +"emojibase-data@npm:^16.0.3": + version: 16.0.3 + resolution: "emojibase-data@npm:16.0.3" + peerDependencies: + emojibase: "*" + checksum: 10/d9cd4b961ea5bca483b857bc84e670e77c3dcbeac41ce3b672086a9256a43bc027b34d4adc9f9a2be85e2b76c85a2b5e91280b6518335432bdbd4c671eeba1ec + languageName: node + linkType: hard + "emojis-list@npm:^3.0.0": version: 3.0.0 resolution: "emojis-list@npm:3.0.0" @@ -22311,7 +22352,7 @@ __metadata: languageName: node linkType: hard -"form-data@npm:^4.0.0": +"form-data@npm:^4.0.0, form-data@npm:^4.0.4": version: 4.0.4 resolution: "form-data@npm:4.0.4" dependencies: @@ -24904,6 +24945,17 @@ __metadata: languageName: node linkType: hard +"js-yaml@npm:4.1.0, js-yaml@npm:^4.0.0, js-yaml@npm:^4.1.0": + version: 4.1.0 + resolution: "js-yaml@npm:4.1.0" + dependencies: + argparse: "npm:^2.0.1" + bin: + js-yaml: bin/js-yaml.js + checksum: 10/c138a34a3fd0d08ebaf71273ad4465569a483b8a639e0b118ff65698d257c2791d3199e3f303631f2cb98213fa7b5f5d6a4621fd0fff819421b990d30d967140 + languageName: node + linkType: hard + "js-yaml@npm:^3.13.1, js-yaml@npm:^3.14.1": version: 3.14.1 resolution: "js-yaml@npm:3.14.1" @@ -24916,17 +24968,6 @@ __metadata: languageName: node linkType: hard -"js-yaml@npm:^4.0.0, js-yaml@npm:^4.1.0": - version: 4.1.0 - resolution: "js-yaml@npm:4.1.0" - dependencies: - argparse: "npm:^2.0.1" - bin: - js-yaml: bin/js-yaml.js - checksum: 10/c138a34a3fd0d08ebaf71273ad4465569a483b8a639e0b118ff65698d257c2791d3199e3f303631f2cb98213fa7b5f5d6a4621fd0fff819421b990d30d967140 - languageName: node - linkType: hard - "jsbn@npm:1.1.0": version: 1.1.0 resolution: "jsbn@npm:1.1.0" @@ -26094,10 +26135,10 @@ __metadata: languageName: node linkType: hard -"luxon@npm:^3.2.1, luxon@npm:~3.6.0": - version: 3.6.1 - resolution: "luxon@npm:3.6.1" - checksum: 10/35aad425607708c87af110a52c949190bc35b987770079ec8007ef2365cd29639413db3360d2883777aa01cb3ca5bdb37f42ee3e8e5a0dd277fe22e90cc8a786 +"luxon@npm:^3.2.1, luxon@npm:~3.7.0": + version: 3.7.2 + resolution: "luxon@npm:3.7.2" + checksum: 10/b24cd205ed306ce7415991687897dcc4027921ae413c9116590bc33a95f93b86ce52cf74ba72b4f5c5ab1c10090517f54ac8edfb127c049e0bf55b90dc2260be languageName: node linkType: hard @@ -33132,6 +33173,15 @@ __metadata: languageName: node linkType: hard +"swagger-ui-dist@npm:5.21.0": + version: 5.21.0 + resolution: "swagger-ui-dist@npm:5.21.0" + dependencies: + "@scarf/scarf": "npm:=1.4.0" + checksum: 10/5e34f4337b06d64ee6bbd64940cc63ace7c457deb98a1de2a387833c6d919543bfed1da662827191fd6c6c6762cc2f52b7ca3f316932a9bc3b12d51b718c5424 + languageName: node + linkType: hard + "swap-case@npm:^2.0.2": version: 2.0.2 resolution: "swap-case@npm:2.0.2"