From b1d68ac307ed87d63e83820cbdf843fff0fd9f7f Mon Sep 17 00:00:00 2001 From: polwex Date: Thu, 11 Sep 2025 01:48:14 +0700 Subject: init --- front/src/App.tsx | 43 + front/src/Router.tsx | 29 + front/src/assets/crowspinner.gif | Bin 0 -> 83062 bytes front/src/assets/icons/bell.svg | 3 + front/src/assets/icons/comet.svg | 23 + front/src/assets/icons/copy.svg | 5 + front/src/assets/icons/crow.svg | 29 + front/src/assets/icons/emoji.svg | 5 + front/src/assets/icons/home.svg | 3 + front/src/assets/icons/key.png | Bin 0 -> 16383 bytes front/src/assets/icons/key.svg | 5 + front/src/assets/icons/logo.png | Bin 0 -> 33892 bytes front/src/assets/icons/logo.svg | 8 + front/src/assets/icons/messages.svg | 8 + front/src/assets/icons/nostr.svg | 3 + front/src/assets/icons/pals.svg | 3 + front/src/assets/icons/profile.svg | 3 + front/src/assets/icons/quote.svg | 3 + front/src/assets/icons/radio.svg | 3 + front/src/assets/icons/reply.svg | 3 + front/src/assets/icons/rt.svg | 3 + front/src/assets/icons/rumors.svg | 3 + front/src/assets/icons/settings.svg | 3 + front/src/assets/icons/youtube.svg | 1 + front/src/assets/reacts/chad.png | Bin 0 -> 20099 bytes front/src/assets/reacts/cringe.png | Bin 0 -> 21923 bytes front/src/assets/reacts/cry.png | Bin 0 -> 18471 bytes front/src/assets/reacts/doom.png | Bin 0 -> 14749 bytes front/src/assets/reacts/facepalm.png | Bin 0 -> 188497 bytes front/src/assets/reacts/galaxy.png | Bin 0 -> 43137 bytes front/src/assets/reacts/gigachad.png | Bin 0 -> 19087 bytes front/src/assets/reacts/pepechin.png | Bin 0 -> 22389 bytes front/src/assets/reacts/pepeeyes.png | Bin 0 -> 18480 bytes front/src/assets/reacts/pepegmi.png | Bin 0 -> 23024 bytes front/src/assets/reacts/pepesad.png | Bin 0 -> 18323 bytes front/src/assets/reacts/pika.png | Bin 0 -> 11475 bytes front/src/assets/reacts/pink.png | Bin 0 -> 26288 bytes front/src/assets/reacts/soy.png | Bin 0 -> 12655 bytes front/src/assets/reacts/yeschad.png | Bin 0 -> 29394 bytes front/src/assets/triangles.svg | 18 + front/src/components/Avatar.tsx | 59 + front/src/components/Sigil.tsx | 50 + front/src/components/feed/Body.tsx | 174 ++ front/src/components/feed/Card.tsx | 9 + front/src/components/feed/Composer.tsx | 52 + front/src/components/feed/External.tsx | 41 + front/src/components/feed/Footer.tsx | 237 ++ front/src/components/feed/Header.tsx | 33 + front/src/components/feed/Media.tsx | 35 + front/src/components/feed/NostrIcon.tsx | 22 + front/src/components/feed/Post.tsx | 79 + front/src/components/feed/PostData.tsx | 160 ++ front/src/components/feed/PostList.tsx | 32 + front/src/components/feed/Quote.tsx | 37 + front/src/components/feed/RP.tsx | 47 + front/src/components/feed/Reactions.tsx | 118 + front/src/components/feed/StatsModal.tsx | 106 + front/src/components/layout/Sidebar.tsx | 81 + front/src/components/modals/Modal.tsx | 72 + front/src/components/modals/ShipModal.tsx | 45 + front/src/components/snippets/Snippets.tsx | 395 +++ front/src/logic/api.ts | 15 + front/src/logic/bunts.ts | 51 + front/src/logic/constants.ts | 36 + front/src/logic/emojis.json | 3613 ++++++++++++++++++++++++++++ front/src/logic/nostril.ts | 36 + front/src/logic/requests/nostril.ts | 139 ++ front/src/logic/utils.ts | 459 ++++ front/src/main.tsx | 9 + front/src/pages/Feed.tsx | 104 + front/src/pages/Settings.tsx | 92 + front/src/pages/User.tsx | 18 + front/src/state/state.ts | 64 + front/src/styles/ThemeProvider.tsx | 302 +++ front/src/styles/ThemeSwitcher.css | 249 ++ front/src/styles/ThemeSwitcher.tsx | 131 + front/src/styles/styles.css | 438 ++++ front/src/styles/trill.css | 612 +++++ front/src/types/nostr.ts | 11 + front/src/types/nostril.ts | 6 + front/src/types/trill.ts | 420 ++++ front/src/types/twatter.ts | 336 +++ front/src/types/ui.ts | 49 + front/src/types/urbit.ts | 8 + front/src/vite-env.d.ts | 1 + 85 files changed, 9290 insertions(+) create mode 100644 front/src/App.tsx create mode 100644 front/src/Router.tsx create mode 100644 front/src/assets/crowspinner.gif create mode 100644 front/src/assets/icons/bell.svg create mode 100644 front/src/assets/icons/comet.svg create mode 100644 front/src/assets/icons/copy.svg create mode 100644 front/src/assets/icons/crow.svg create mode 100644 front/src/assets/icons/emoji.svg create mode 100644 front/src/assets/icons/home.svg create mode 100644 front/src/assets/icons/key.png create mode 100644 front/src/assets/icons/key.svg create mode 100644 front/src/assets/icons/logo.png create mode 100644 front/src/assets/icons/logo.svg create mode 100644 front/src/assets/icons/messages.svg create mode 100644 front/src/assets/icons/nostr.svg create mode 100644 front/src/assets/icons/pals.svg create mode 100644 front/src/assets/icons/profile.svg create mode 100644 front/src/assets/icons/quote.svg create mode 100644 front/src/assets/icons/radio.svg create mode 100644 front/src/assets/icons/reply.svg create mode 100644 front/src/assets/icons/rt.svg create mode 100644 front/src/assets/icons/rumors.svg create mode 100644 front/src/assets/icons/settings.svg create mode 100644 front/src/assets/icons/youtube.svg create mode 100644 front/src/assets/reacts/chad.png create mode 100644 front/src/assets/reacts/cringe.png create mode 100644 front/src/assets/reacts/cry.png create mode 100644 front/src/assets/reacts/doom.png create mode 100644 front/src/assets/reacts/facepalm.png create mode 100644 front/src/assets/reacts/galaxy.png create mode 100644 front/src/assets/reacts/gigachad.png create mode 100644 front/src/assets/reacts/pepechin.png create mode 100644 front/src/assets/reacts/pepeeyes.png create mode 100644 front/src/assets/reacts/pepegmi.png create mode 100644 front/src/assets/reacts/pepesad.png create mode 100644 front/src/assets/reacts/pika.png create mode 100644 front/src/assets/reacts/pink.png create mode 100644 front/src/assets/reacts/soy.png create mode 100644 front/src/assets/reacts/yeschad.png create mode 100644 front/src/assets/triangles.svg create mode 100644 front/src/components/Avatar.tsx create mode 100644 front/src/components/Sigil.tsx create mode 100644 front/src/components/feed/Body.tsx create mode 100644 front/src/components/feed/Card.tsx create mode 100644 front/src/components/feed/Composer.tsx create mode 100644 front/src/components/feed/External.tsx create mode 100644 front/src/components/feed/Footer.tsx create mode 100644 front/src/components/feed/Header.tsx create mode 100644 front/src/components/feed/Media.tsx create mode 100644 front/src/components/feed/NostrIcon.tsx create mode 100644 front/src/components/feed/Post.tsx create mode 100644 front/src/components/feed/PostData.tsx create mode 100644 front/src/components/feed/PostList.tsx create mode 100644 front/src/components/feed/Quote.tsx create mode 100644 front/src/components/feed/RP.tsx create mode 100644 front/src/components/feed/Reactions.tsx create mode 100644 front/src/components/feed/StatsModal.tsx create mode 100644 front/src/components/layout/Sidebar.tsx create mode 100644 front/src/components/modals/Modal.tsx create mode 100644 front/src/components/modals/ShipModal.tsx create mode 100644 front/src/components/snippets/Snippets.tsx create mode 100644 front/src/logic/api.ts create mode 100644 front/src/logic/bunts.ts create mode 100644 front/src/logic/constants.ts create mode 100644 front/src/logic/emojis.json create mode 100644 front/src/logic/nostril.ts create mode 100644 front/src/logic/requests/nostril.ts create mode 100644 front/src/logic/utils.ts create mode 100644 front/src/main.tsx create mode 100644 front/src/pages/Feed.tsx create mode 100644 front/src/pages/Settings.tsx create mode 100644 front/src/pages/User.tsx create mode 100644 front/src/state/state.ts create mode 100644 front/src/styles/ThemeProvider.tsx create mode 100644 front/src/styles/ThemeSwitcher.css create mode 100644 front/src/styles/ThemeSwitcher.tsx create mode 100644 front/src/styles/styles.css create mode 100644 front/src/styles/trill.css create mode 100644 front/src/types/nostr.ts create mode 100644 front/src/types/nostril.ts create mode 100644 front/src/types/trill.ts create mode 100644 front/src/types/twatter.ts create mode 100644 front/src/types/ui.ts create mode 100644 front/src/types/urbit.ts create mode 100644 front/src/vite-env.d.ts (limited to 'front/src') diff --git a/front/src/App.tsx b/front/src/App.tsx new file mode 100644 index 0000000..60ca66a --- /dev/null +++ b/front/src/App.tsx @@ -0,0 +1,43 @@ +import { useEffect, useState } from "react"; +import useLocalState from "@/state/state"; +import Router from "./Router"; +import "@/styles/styles.css"; +import { ThemeProvider } from "@/styles/ThemeProvider"; +import spinner from "@/assets/crowspinner.gif"; + +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { Toaster } from "react-hot-toast"; +const queryClient = new QueryClient(); + +// const isMobile = MOBILE_BROWSER_REGEX.test(navigator.userAgent); + +function App() { + const [loading, setLoading] = useState(true); + console.log("NOSTRIL INIT"); + const { init, modal } = useLocalState(); + useEffect(() => { + init().then((_res: any) => { + setLoading(false); + }); + }, []); + if (loading) + return ( +
+ +

Syncing with your Urbit...

+
+ ); + else + return ( + + + {/* {isMobile ? : } */} + + {modal && modal} + + + + ); +} + +export default App; diff --git a/front/src/Router.tsx b/front/src/Router.tsx new file mode 100644 index 0000000..b7b033e --- /dev/null +++ b/front/src/Router.tsx @@ -0,0 +1,29 @@ +import Sidebar from "@/components/layout/Sidebar"; + +// new +import Feed from "@/pages/Feed"; +import Settings from "@/pages/Settings"; +import { Switch, Router, Redirect, Route } from "wouter"; + +export default function r() { + return ( + + + +
+ + + +
+
+ +
+ ); +} +function toGlobal() { + return ; +} + +export function P404() { + return

404

; +} diff --git a/front/src/assets/crowspinner.gif b/front/src/assets/crowspinner.gif new file mode 100644 index 0000000..d0033d3 Binary files /dev/null and b/front/src/assets/crowspinner.gif differ diff --git a/front/src/assets/icons/bell.svg b/front/src/assets/icons/bell.svg new file mode 100644 index 0000000..98e88cd --- /dev/null +++ b/front/src/assets/icons/bell.svg @@ -0,0 +1,3 @@ + + + diff --git a/front/src/assets/icons/comet.svg b/front/src/assets/icons/comet.svg new file mode 100644 index 0000000..2d5c3f5 --- /dev/null +++ b/front/src/assets/icons/comet.svg @@ -0,0 +1,23 @@ + + + + + diff --git a/front/src/assets/icons/copy.svg b/front/src/assets/icons/copy.svg new file mode 100644 index 0000000..714e9f5 --- /dev/null +++ b/front/src/assets/icons/copy.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/front/src/assets/icons/crow.svg b/front/src/assets/icons/crow.svg new file mode 100644 index 0000000..e967970 --- /dev/null +++ b/front/src/assets/icons/crow.svg @@ -0,0 +1,29 @@ + + + + + diff --git a/front/src/assets/icons/emoji.svg b/front/src/assets/icons/emoji.svg new file mode 100644 index 0000000..7a957fd --- /dev/null +++ b/front/src/assets/icons/emoji.svg @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/front/src/assets/icons/home.svg b/front/src/assets/icons/home.svg new file mode 100644 index 0000000..64d7984 --- /dev/null +++ b/front/src/assets/icons/home.svg @@ -0,0 +1,3 @@ + + + diff --git a/front/src/assets/icons/key.png b/front/src/assets/icons/key.png new file mode 100644 index 0000000..2efe10b Binary files /dev/null and b/front/src/assets/icons/key.png differ diff --git a/front/src/assets/icons/key.svg b/front/src/assets/icons/key.svg new file mode 100644 index 0000000..c2ac4b9 --- /dev/null +++ b/front/src/assets/icons/key.svg @@ -0,0 +1,5 @@ + + + key + + diff --git a/front/src/assets/icons/logo.png b/front/src/assets/icons/logo.png new file mode 100644 index 0000000..fdb3f22 Binary files /dev/null and b/front/src/assets/icons/logo.png differ diff --git a/front/src/assets/icons/logo.svg b/front/src/assets/icons/logo.svg new file mode 100644 index 0000000..7cbac7c --- /dev/null +++ b/front/src/assets/icons/logo.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/front/src/assets/icons/messages.svg b/front/src/assets/icons/messages.svg new file mode 100644 index 0000000..8a0b9c3 --- /dev/null +++ b/front/src/assets/icons/messages.svg @@ -0,0 +1,8 @@ + + + + message + + + + diff --git a/front/src/assets/icons/nostr.svg b/front/src/assets/icons/nostr.svg new file mode 100644 index 0000000..80760a8 --- /dev/null +++ b/front/src/assets/icons/nostr.svg @@ -0,0 +1,3 @@ + + + diff --git a/front/src/assets/icons/pals.svg b/front/src/assets/icons/pals.svg new file mode 100644 index 0000000..04b17a3 --- /dev/null +++ b/front/src/assets/icons/pals.svg @@ -0,0 +1,3 @@ + + + diff --git a/front/src/assets/icons/profile.svg b/front/src/assets/icons/profile.svg new file mode 100644 index 0000000..f94e63d --- /dev/null +++ b/front/src/assets/icons/profile.svg @@ -0,0 +1,3 @@ + + + diff --git a/front/src/assets/icons/quote.svg b/front/src/assets/icons/quote.svg new file mode 100644 index 0000000..5b847e3 --- /dev/null +++ b/front/src/assets/icons/quote.svg @@ -0,0 +1,3 @@ + + + diff --git a/front/src/assets/icons/radio.svg b/front/src/assets/icons/radio.svg new file mode 100644 index 0000000..5c98c15 --- /dev/null +++ b/front/src/assets/icons/radio.svg @@ -0,0 +1,3 @@ + + + diff --git a/front/src/assets/icons/reply.svg b/front/src/assets/icons/reply.svg new file mode 100644 index 0000000..db86cfd --- /dev/null +++ b/front/src/assets/icons/reply.svg @@ -0,0 +1,3 @@ + + + diff --git a/front/src/assets/icons/rt.svg b/front/src/assets/icons/rt.svg new file mode 100644 index 0000000..43b4a36 --- /dev/null +++ b/front/src/assets/icons/rt.svg @@ -0,0 +1,3 @@ + + + diff --git a/front/src/assets/icons/rumors.svg b/front/src/assets/icons/rumors.svg new file mode 100644 index 0000000..2df5165 --- /dev/null +++ b/front/src/assets/icons/rumors.svg @@ -0,0 +1,3 @@ + + + diff --git a/front/src/assets/icons/settings.svg b/front/src/assets/icons/settings.svg new file mode 100644 index 0000000..5c5e400 --- /dev/null +++ b/front/src/assets/icons/settings.svg @@ -0,0 +1,3 @@ + + + diff --git a/front/src/assets/icons/youtube.svg b/front/src/assets/icons/youtube.svg new file mode 100644 index 0000000..46d7db9 --- /dev/null +++ b/front/src/assets/icons/youtube.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/front/src/assets/reacts/chad.png b/front/src/assets/reacts/chad.png new file mode 100644 index 0000000..51cfa0d Binary files /dev/null and b/front/src/assets/reacts/chad.png differ diff --git a/front/src/assets/reacts/cringe.png b/front/src/assets/reacts/cringe.png new file mode 100644 index 0000000..d39a50c Binary files /dev/null and b/front/src/assets/reacts/cringe.png differ diff --git a/front/src/assets/reacts/cry.png b/front/src/assets/reacts/cry.png new file mode 100644 index 0000000..f70fb28 Binary files /dev/null and b/front/src/assets/reacts/cry.png differ diff --git a/front/src/assets/reacts/doom.png b/front/src/assets/reacts/doom.png new file mode 100644 index 0000000..e6df1f4 Binary files /dev/null and b/front/src/assets/reacts/doom.png differ diff --git a/front/src/assets/reacts/facepalm.png b/front/src/assets/reacts/facepalm.png new file mode 100644 index 0000000..a03def9 Binary files /dev/null and b/front/src/assets/reacts/facepalm.png differ diff --git a/front/src/assets/reacts/galaxy.png b/front/src/assets/reacts/galaxy.png new file mode 100644 index 0000000..3c496d3 Binary files /dev/null and b/front/src/assets/reacts/galaxy.png differ diff --git a/front/src/assets/reacts/gigachad.png b/front/src/assets/reacts/gigachad.png new file mode 100644 index 0000000..5f3c2e1 Binary files /dev/null and b/front/src/assets/reacts/gigachad.png differ diff --git a/front/src/assets/reacts/pepechin.png b/front/src/assets/reacts/pepechin.png new file mode 100644 index 0000000..dafd907 Binary files /dev/null and b/front/src/assets/reacts/pepechin.png differ diff --git a/front/src/assets/reacts/pepeeyes.png b/front/src/assets/reacts/pepeeyes.png new file mode 100644 index 0000000..e57d5e6 Binary files /dev/null and b/front/src/assets/reacts/pepeeyes.png differ diff --git a/front/src/assets/reacts/pepegmi.png b/front/src/assets/reacts/pepegmi.png new file mode 100644 index 0000000..7c3cae4 Binary files /dev/null and b/front/src/assets/reacts/pepegmi.png differ diff --git a/front/src/assets/reacts/pepesad.png b/front/src/assets/reacts/pepesad.png new file mode 100644 index 0000000..51891fd Binary files /dev/null and b/front/src/assets/reacts/pepesad.png differ diff --git a/front/src/assets/reacts/pika.png b/front/src/assets/reacts/pika.png new file mode 100644 index 0000000..791594b Binary files /dev/null and b/front/src/assets/reacts/pika.png differ diff --git a/front/src/assets/reacts/pink.png b/front/src/assets/reacts/pink.png new file mode 100644 index 0000000..59fdc6a Binary files /dev/null and b/front/src/assets/reacts/pink.png differ diff --git a/front/src/assets/reacts/soy.png b/front/src/assets/reacts/soy.png new file mode 100644 index 0000000..33dbe33 Binary files /dev/null and b/front/src/assets/reacts/soy.png differ diff --git a/front/src/assets/reacts/yeschad.png b/front/src/assets/reacts/yeschad.png new file mode 100644 index 0000000..e001332 Binary files /dev/null and b/front/src/assets/reacts/yeschad.png differ diff --git a/front/src/assets/triangles.svg b/front/src/assets/triangles.svg new file mode 100644 index 0000000..0b45c01 --- /dev/null +++ b/front/src/assets/triangles.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/front/src/components/Avatar.tsx b/front/src/components/Avatar.tsx new file mode 100644 index 0000000..35b4386 --- /dev/null +++ b/front/src/components/Avatar.tsx @@ -0,0 +1,59 @@ +import useLocalState from "@/state/state"; +import type { Ship } from "@/types/urbit"; +import Sigil from "./Sigil"; +import ShipModal from "./modals/ShipModal"; + +export default function ({ + p, + size, + color, + noClickOnName, +}: { + p: Ship; + size: number; + color?: string; + noClickOnName?: boolean; +}) { + const { setModal } = useLocalState(); + // TODO revisit this when %whom updates + const avatar = ( +
+ +
+ ); + const tooLong = (s: string) => (s.length > 15 ? " too-long" : ""); + function openModal(e: React.MouseEvent) { + if (noClickOnName) return; + e.stopPropagation(); + setModal(); + } + const name = ( +
+

{p.length > 28 ? "Anon" : p}

+
+ ); + return ( +
+ {avatar} + {name} +
+ ); +} + +export function SigilOnly({ p, size, color }: any) { + const { setModal } = useLocalState(); + function openModal(e: React.MouseEvent) { + e.stopPropagation(); + setModal(); + } + return ( +
+ +
+ ); +} diff --git a/front/src/components/Sigil.tsx b/front/src/components/Sigil.tsx new file mode 100644 index 0000000..4978a72 --- /dev/null +++ b/front/src/components/Sigil.tsx @@ -0,0 +1,50 @@ +import comet from "@/assets/icons/comet.svg"; +import { auraToHex } from "@/logic/utils"; +import { isValidPatp } from "urbit-ob"; +import { sigil } from "urbit-sigils"; +import { reactRenderer } from "urbit-sigils"; + +interface SigilProps { + patp: string; + size: number; + color?: string; +} + +const Sigil = (props: SigilProps) => { + const color = props.color ? auraToHex(props.color) : "black"; + if (!isValidPatp(props.patp)) return
X
; + else if (props.patp.length > 28) + return ( + + ); + else if (props.patp.length > 15) + // moons + return ( + <> + {sigil({ + patp: props.patp.substring(props.patp.length - 13), + renderer: reactRenderer, + size: props.size, + colors: ["grey", "white"], + })} + + ); + else + return ( + <> + {sigil({ + patp: props.patp, + renderer: reactRenderer, + size: props.size, + colors: [color, "white"], + })} + + ); +}; + +export default Sigil; diff --git a/front/src/components/feed/Body.tsx b/front/src/components/feed/Body.tsx new file mode 100644 index 0000000..2f11962 --- /dev/null +++ b/front/src/components/feed/Body.tsx @@ -0,0 +1,174 @@ +import type { + // TODO ref backend fetching!! + Reference, + Block, + Inline, + Media as MediaType, + ExternalContent, +} from "@/types/trill"; +import crow from "@/assets/icons/crow.svg"; +import type { PostProps } from "./Post"; +import Media from "./Media"; +import JSONContent, { YoutubeSnippet } from "./External"; +import { useLocation } from "wouter"; +import Quote from "./Quote"; +import PostData from "./PostData"; +import Card from "./Card.tsx"; +import type { Ship } from "@/types/urbit.ts"; + +function Body(props: PostProps) { + const text = props.poast.contents.filter((c) => { + return ( + "paragraph" in c || + "blockquote" in c || + "heading" in c || + "codeblock" in c || + "list" in c + ); + }); + + const media: MediaType[] = props.poast.contents.filter( + (c): c is MediaType => "media" in c, + ); + + const refs = props.poast.contents.filter((c): c is Reference => "ref" in c); + const json = props.poast.contents.filter( + (c): c is ExternalContent => "json" in c, + ); + + return ( +
+
+ {text.map((b, i) => ( + + ))} +
+ {media.length > 0 && } + {refs.map((r, i) => ( + + ))} + +
+ ); +} +export default Body; + +function TextBlock({ block }: { block: Block }) { + const key = JSON.stringify(block); + return "paragraph" in block ? ( +
+ {block.paragraph.map((i, ind) => ( + + ))} +
+ ) : "blockquote" in block ? ( +
+ {block.blockquote.map((i, ind) => ( + + ))} +
+ ) : "heading" in block ? ( + + ) : "codeblock" in block ? ( +
+      
+        {block.codeblock.code}
+      
+    
+ ) : "list" in block ? ( + block.list.ordered ? ( +
    + {block.list.text.map((i, ind) => ( +
  1. + +
  2. + ))} +
+ ) : ( +
    + {block.list.text.map((i, ind) => ( +
  • + +
  • + ))} +
+ ) + ) : null; +} +function Inlin({ i }: { i: Inline }) { + const [_, navigate] = useLocation(); + function gotoShip(e: React.MouseEvent, ship: Ship) { + e.stopPropagation(); + navigate(`/feed/${ship}`); + } + return "text" in i ? ( + {i.text} + ) : "italic" in i ? ( + {i.italic} + ) : "bold" in i ? ( + {i.bold} + ) : "strike" in i ? ( + {i.strike} + ) : "underline" in i ? ( + {i.underline} + ) : "sup" in i ? ( + {i.sup} + ) : "sub" in i ? ( + {i.sub} + ) : "ship" in i ? ( + gotoShip(e, i.ship)} + > + {i.ship} + + ) : "codespan" in i ? ( + {i.codespan} + ) : "link" in i ? ( + + ) : "break" in i ? ( +
+ ) : null; +} + +function LinkParser({ href, show }: { href: string; show: string }) { + const YOUTUBE_REGEX_1 = /(youtube\.com\/watch\?v=)(\w+)/; + const YOUTUBE_REGEX_2 = /(youtu\.be\/)([a-zA-Z0-9-_]+)/; + const m1 = href.match(YOUTUBE_REGEX_1); + const m2 = href.match(YOUTUBE_REGEX_2); + const ytb = m1 && m1[2] ? m1[2] : m2 && m2[2] ? m2[2] : ""; + return ytb ? ( + + ) : ( + {show} + ); +} +function Heading({ string, num }: { string: string; num: number }) { + return num === 1 ? ( +

{string}

+ ) : num === 2 ? ( +

{string}

+ ) : num === 3 ? ( +

{string}

+ ) : num === 4 ? ( +

{string}

+ ) : num === 5 ? ( +
{string}
+ ) : num === 6 ? ( +
{string}
+ ) : null; +} + +function Ref({ r, nest }: { r: Reference; nest: number }) { + if (r.ref.type === "nostril") { + const comp = PostData({ + host: r.ref.ship, + id: r.ref.path.slice(1), + nest: nest + 1, + className: "quote-in-post", + })(Quote); + return {comp}; + } + return <>; +} diff --git a/front/src/components/feed/Card.tsx b/front/src/components/feed/Card.tsx new file mode 100644 index 0000000..37f4911 --- /dev/null +++ b/front/src/components/feed/Card.tsx @@ -0,0 +1,9 @@ +export default function ({ children, logo, cn}: { cn?: string; logo: string; children: any }) { + const className = "trill-post-card" + (cn ? ` ${cn}`: "") + return ( +
+ + {children} +
+ ); +} diff --git a/front/src/components/feed/Composer.tsx b/front/src/components/feed/Composer.tsx new file mode 100644 index 0000000..27da392 --- /dev/null +++ b/front/src/components/feed/Composer.tsx @@ -0,0 +1,52 @@ +import { openLock } from "@/logic/bunts"; +import { HASHTAGS_REGEX } from "@/logic/constants"; +import useLocalState from "@/state/state"; +import type { Poast, SentPoast } from "@/types/trill"; +import Sigil from "@/components/Sigil"; +import { useState } from "react"; + +function Composer({ + isAnon, + replying, +}: { + isAnon?: boolean; + replying?: Poast; +}) { + const { api, keys } = useLocalState(); + const our = api!.airlock.our!; + const [input, setInput] = useState(replying ? `${replying}: ` : ""); + async function poast() { + // TODO + // const parent = replying ? replying : null; + // const tokens = tokenize(input); + // const post: SentPoast = { + // host: parent ? parent.host : our, + // author: our, + // thread: parent ? parent.thread : null, + // parent: parent ? parent.id : null, + // contents: input, + // read: openLock, + // write: openLock, + // tags: input.match(HASHTAGS_REGEX) || [], + // }; + // TODO make it user choosable + const pubkey = keys[0]!; + await api!.addPost(pubkey, input); + } + const placeHolder = isAnon ? "> be me" : "What's going on in Urbit"; + return ( +
+
+ +
+ setInput(e.currentTarget.value)} + placeholder={placeHolder} + /> + +
+ ); +} + +export default Composer; diff --git a/front/src/components/feed/External.tsx b/front/src/components/feed/External.tsx new file mode 100644 index 0000000..0ea1500 --- /dev/null +++ b/front/src/components/feed/External.tsx @@ -0,0 +1,41 @@ +import type { ExternalContent } from "@/types/trill"; +import youtube from "@/assets/icons/youtube.svg"; +import Card from "./Card"; + +interface JSONProps { + content: ExternalContent[]; +} + +function JSONContent({ content }: JSONProps) { + return ( + <> + {content.map((c, i) => { + if (!JSON.parse(c.json.content)) return

Error

; + else + return ( +

+ External content from "{c.json.origin}", use + UFA + to display. +

+ ); + })} + + ); +} +export default JSONContent; + +export function YoutubeSnippet({ href, id }: { href: string; id: string }) { + const thumbnail = `https://i.ytimg.com/vi/${id}/hqdefault.jpg`; + // todo styiling + return ( + + + + + + ); +} diff --git a/front/src/components/feed/Footer.tsx b/front/src/components/feed/Footer.tsx new file mode 100644 index 0000000..938a8c7 --- /dev/null +++ b/front/src/components/feed/Footer.tsx @@ -0,0 +1,237 @@ +import type { PostProps } from "./Post"; +import reply from "@/assets/icons/reply.svg"; +import quote from "@/assets/icons/quote.svg"; +import repost from "@/assets/icons/rt.svg"; +import { useState } from "react"; +import useLocalState from "@/state/state"; +import { useLocation } from "wouter"; +import { displayCount } from "@/logic/utils"; +import { TrillReactModal, stringToReact } from "./Reactions"; +import toast from "react-hot-toast"; +import NostrIcon from "./NostrIcon"; +function Footer({ poast, refetch }: PostProps) { + const [_showMenu, setShowMenu] = useState(false); + const [location, navigate] = useLocation(); + const [reposting, _setReposting] = useState(false); + const { api, setComposerData, setModal } = useLocalState(); + const our = api!.airlock.our!; + function doReply(e: React.MouseEvent) { + e.stopPropagation(); + setComposerData({ type: "reply", post: { service: "trill", post: poast } }); + navigate("/composer"); + } + function doQuote(e: React.MouseEvent) { + e.stopPropagation(); + setComposerData({ + type: "quote", + post: { service: "trill", post: poast }, + }); + navigate("/composer"); + } + const childrenCount = poast.children + ? poast.children.length + ? poast.children.length + : Object.keys(poast.children).length + : 0; + const myRP = poast.engagement.shared.find((r) => r.pid.ship === our); + async function cancelRP(e: React.MouseEvent) { + e.stopPropagation(); + const r = await api!.deletePost(our); + if (r) toast.success("Repost deleted"); + refetch(); + if (location.includes(poast.id)) navigate("/"); + } + async function sendRP(e: React.MouseEvent) { + // TODO update backend because contents are only markdown now + e.stopPropagation(); + // const c = [ + // { + // ref: { + // type: "trill", + // ship: poast.host, + // path: `/${poast.id}`, + // }, + // }, + // ]; + // const post: SentPoast = { + // host: our, + // author: our, + // thread: null, + // parent: null, + // contents: input, + // read: openLock, + // write: openLock, + // tags: [], // TODO + // }; + // const r = await api!.addPost(post, false); + // setReposting(true); + // if (r) { + // setReposting(false); + // toast.success("Your post was published"); + // } + } + function doReact(e: React.MouseEvent) { + e.stopPropagation(); + const modal = ; + setModal(modal); + } + function showReplyCount() { + if (poast.children[0]) fetchAndShow(); // Flatpoast + // else { + // const authors = Object.keys(poast.children).map( + // (i) => poast.children[i].post.author + // ); + // setEngagement({ type: "replies", ships: authors }, poast); + // } + } + async function fetchAndShow() { + // let authors = []; + // for (let i of poast.children as string[]) { + // const res = await scrypoastFull(poast.host, i); + // if (res) + // authors.push(res.post.author || "deleter"); + // } + // setEngagement({ type: "replies", ships: authors }, poast); + } + function showRepostCount() { + // const ships = poast.engagement.shared.map((entry) => entry.host); + // setEngagement({ type: "reposts", ships: ships }, poast); + } + function showQuoteCount() { + // setEngagement({ type: "quotes", quotes: poast.engagement.quoted }, poast); + } + function showReactCount() { + // setEngagement({ type: "reacts", reacts: poast.engagement.reacts }, poast); + } + + const mostCommonReact = Object.values(poast.engagement.reacts).reduce( + (acc: any, item) => { + if (!acc.counts[item]) acc.counts[item] = 0; + acc.counts[item] += 1; + if (!acc.winner || acc.counts[item] > acc.counts[acc.winner]) + acc.winner = item; + return acc; + }, + { counts: {}, winner: "" }, + ).winner; + const reactIcon = stringToReact(mostCommonReact); + + // TODO round up all helpers + + return ( +
+
+
+ + {displayCount(childrenCount)} + + +
+
+ + {displayCount(poast.engagement.quoted.length)} + + +
+
+ + {displayCount(poast.engagement.shared.length)} + + {reposting ? ( +

...

+ ) : myRP ? ( + + ) : ( + + )} +
+
+ + {displayCount(Object.keys(poast.engagement.reacts).length)} + + {reactIcon} +
+ +
+
+ ); +} +export default Footer; + +// function Menu({ +// poast, +// setShowMenu, +// refetch, +// }: { +// poast: Poast; +// setShowMenu: Function; +// refetch: Function; +// }) { +// const ref = useRef(null); +// const [location, navigate] = useLocation(); +// // TODO this is a mess and the event still propagates +// useEffect(() => { +// const checkIfClickedOutside = (e: any) => { +// e.stopPropagation(); +// if (ref && ref.current && !ref.current.contains(e.target)) +// setShowMenu(false); +// }; +// document.addEventListener("mousedown", checkIfClickedOutside); +// return () => { +// document.removeEventListener("mousedown", checkIfClickedOutside); +// }; +// }, []); +// const { our, setModal, setAlert } = useLocalState(); +// const mine = our === poast.host || our === poast.author; +// async function doDelete(e: React.MouseEvent) { +// e.stopPropagation(); +// deletePost(poast.host, poast.id); +// setAlert("Post deleted"); +// setShowMenu(false); +// refetch(); +// if (location.includes(poast.id)) navigate("/"); +// } +// async function copyLink(e: React.MouseEvent) { +// e.stopPropagation(); +// const link = trillPermalink(poast); +// await navigator.clipboard.writeText(link); +// // some alert +// setShowMenu(false); +// } +// function openStats(e: React.MouseEvent) { +// e.stopPropagation(); +// e.preventDefault(); +// const m = setModal(null)} />; +// setModal(m); +// } +// return ( +//
+// {/*

Share to Groups

*/} +//

+// See Stats +//

+//

+// Permalink +//

+// {mine && ( +//

+// Delete Post +//

+// )} +//
+// ); +// } diff --git a/front/src/components/feed/Header.tsx b/front/src/components/feed/Header.tsx new file mode 100644 index 0000000..7658bfb --- /dev/null +++ b/front/src/components/feed/Header.tsx @@ -0,0 +1,33 @@ +import { date_diff } from "@/logic/utils"; +import type { PostProps } from "./Post"; +import { useLocation } from "wouter"; +function Header(props: PostProps) { + const [_, navigate] = useLocation(); + function go(e: React.MouseEvent) { + e.stopPropagation(); + } + function openThread(e: React.MouseEvent) { + e.stopPropagation(); + const sel = window.getSelection()?.toString(); + if (!sel) navigate(`/feed/${poast.host}/${poast.id}`); + } + const { poast } = props; + const name = ( +
+

{poast.author}

+
+ ); + return ( +
+
+ {name} +
+
+

+ {date_diff(poast.time, "short")} +

+
+
+ ); +} +export default Header; diff --git a/front/src/components/feed/Media.tsx b/front/src/components/feed/Media.tsx new file mode 100644 index 0000000..04ea156 --- /dev/null +++ b/front/src/components/feed/Media.tsx @@ -0,0 +1,35 @@ +import type { Media } from "@/types/trill"; +interface Props { + media: Media[]; +} +function M({ media }: Props) { + return ( +
+ {media.map((m, i) => { + return "video" in m.media ? ( +
+ ); +} +export default M; + +function Images({ urls }: { urls: string[] }) { + return ( + <> + {urls.map((u, i) => ( + + ))} + + ); +} diff --git a/front/src/components/feed/NostrIcon.tsx b/front/src/components/feed/NostrIcon.tsx new file mode 100644 index 0000000..0c368fb --- /dev/null +++ b/front/src/components/feed/NostrIcon.tsx @@ -0,0 +1,22 @@ +import nostrIcon from "@/assets/icons/nostr.svg"; +import useLocalState from "@/state/state"; +import toast from "react-hot-toast"; +import type { Poast } from "@/types/trill"; +export default function ({ poast }: { poast: Poast }) { + const { relays, api, keys } = useLocalState(); + + async function sendToRelay(e: React.MouseEvent) { + e.stopPropagation(); + // + const urls = Object.keys(relays); + await api!.relayPost(poast.host, poast.id, urls); + toast.success("Post relayed"); + } + // TODO round up all helpers + + return ( +
+ +
+ ); +} diff --git a/front/src/components/feed/Post.tsx b/front/src/components/feed/Post.tsx new file mode 100644 index 0000000..1211a97 --- /dev/null +++ b/front/src/components/feed/Post.tsx @@ -0,0 +1,79 @@ +import type { PostID, Poast, Reference } from "@/types/trill"; + +import Header from "./Header"; +import Body from "./Body"; +import Footer from "./Footer"; +import { useLocation } from "wouter"; +import useLocalState from "@/state/state"; +import RP from "./RP"; +import ShipModal from "../modals/ShipModal"; +import type { Ship } from "@/types/urbit"; +import Sigil from "../Sigil"; + +export interface PostProps { + poast: Poast; + fake?: boolean; + rter?: Ship; + rtat?: number; + rtid?: PostID; + nest?: number; + refetch: Function; +} +function Post(props: PostProps) { + const { poast } = props; + console.log({ poast }); + if (!poast || poast.contents === null) { + return null; + } + const isRP = + poast.contents.length === 1 && + "ref" in poast.contents[0] && + poast.contents[0].ref.type === "trill"; + if (isRP) { + const ref = (poast.contents[0] as Reference).ref; + return ( + + ); + } else return ; +} +export default Post; + +function TrillPost(props: PostProps) { + const { poast, fake } = props; + const { setModal } = useLocalState(); + const [_, navigate] = useLocation(); + function openThread(_e: React.MouseEvent) { + const sel = window.getSelection()?.toString(); + if (!sel) navigate(`/feed/${poast.host}/${poast.id}`); + } + + function openModal(e: React.MouseEvent) { + e.stopPropagation(); + setModal(); + } + const avatar = ( +
+ +
+ ); + return ( +
+
{avatar}
+
+
+ + {!fake &&
} +
+
+ ); +} diff --git a/front/src/components/feed/PostData.tsx b/front/src/components/feed/PostData.tsx new file mode 100644 index 0000000..f3c4715 --- /dev/null +++ b/front/src/components/feed/PostData.tsx @@ -0,0 +1,160 @@ +import { useQuery, useQueryClient } from "@tanstack/react-query"; +import spinner from "@/assets/triangles.svg"; +import { useEffect, useRef, useState } from "react"; +import useLocalState from "@/state/state"; +import type { PostID } from "@/types/trill"; +import type { Ship } from "@/types/urbit"; + +function PostData(props: { + host: Ship; + id: PostID; + rter?: Ship; + rtat?: number; + rtid?: PostID; + nest?: number; // nested quotes + className?: string; +}) { + const { api } = useLocalState(); + const { host, id, nest } = props; + const [enest, setEnest] = useState(nest); + useEffect(() => { + setEnest(nest); + }, [nest]); + + return function (Component: React.ElementType) { + // const [showNested, setShowNested] = useState(nest <= 3); + const handleShowNested = (e: React.MouseEvent) => { + e.stopPropagation(); + setEnest(enest! - 3); + }; + const [dead, setDead] = useState(false); + const [denied, setDenied] = useState(false); + const { isLoading, isError, data, refetch } = useQuery({ + queryKey: ["trill-thread", host, id], + queryFn: fetchNode, + }); + const queryClient = useQueryClient(); + const dataRef = useRef(data); + useEffect(() => { + dataRef.current = data; + }, [data]); + + async function fetchNode(): Promise { + const res = await api!.scryPost(host, id, null, null); + if ("fpost" in res) return res; + else { + const existing = queryClient.getQueryData(["trill-thread", host, id]); + const existingData = existing || data; + if ("bugen" in res) { + // we peek for the actual node + peekTheNode(); + // if we have a cache we don't invalidate it + if (existingData && "fpost" in existingData) return existingData; + // if we don't have a cache then we show the loading screen + else return res; + } + if ("no-node" in res) { + if (existingData && "fpost" in existingData) return existingData; + else return res; + } + } + } + function peekTheNode() { + let timer; + peekNode({ ship: host, id }); + timer = setTimeout(() => { + const gotPost = dataRef.current && "fpost" in dataRef.current; + setDead(!gotPost); + // clearTimeout(timer); + }, 10_000); + } + + useEffect(() => { + const path = `${host}/${id}`; + if (path in peekedPosts) { + queryClient.setQueryData(["trill-thread", host, id], { + fpost: peekedPosts[path], + }); + } else if (path in deniedPosts) { + setDenied(true); + } + }, [peekedPosts]); + useEffect(() => { + const path = `${host}/${id}`; + if (path in deniedPosts) setDenied(true); + }, [deniedPosts]); + + useEffect(() => { + const l = lastThread; + if (l && l.thread == id) { + queryClient.setQueryData(["trill-thread", host, id], { fpost: l }); + } + }, [lastThread]); + function retryPeek(e: React.MouseEvent) { + e.stopPropagation(); + setDead(false); + peekTheNode(); + } + if (enest > 3) + return ( +
+
+ +
+
+ ); + else + return data ? ( + dead ? ( +
+
+

{host} did not respond

+ +
+
+ ) : denied ? ( +
+

+ {host} denied you access to this post +

+
+ ) : "no-node" in data || "bucun" in data ? ( +
+

Post not found

+
+ ) : "bugen" in data ? ( +
+
+

Post not found, requesting...

+ +
+
+ ) : "fpost" in data && data.fpost.contents === null ? ( +
+

Post deleted

+
+ ) : ( + + ) + ) : // no data + isLoading || isError ? ( +
+ +
+ ) : ( +
+

...

+
+ ); + }; +} +export default PostData; diff --git a/front/src/components/feed/PostList.tsx b/front/src/components/feed/PostList.tsx new file mode 100644 index 0000000..3d41ff8 --- /dev/null +++ b/front/src/components/feed/PostList.tsx @@ -0,0 +1,32 @@ +import TrillPost from "./Post"; +import type { FC } from "@/types/trill"; +// import { useEffect } from "react"; +// import { useQueryClient } from "@tanstack/react-query"; +// import { toFull } from "../thread/helpers"; + +function TrillFeed({ data, refetch }: { data: FC; refetch: Function }) { + // const qc = useQueryClient(); + // useEffect(() => { + // Object.values(data.feed).forEach((poast) => { + // const queryKey = ["trill-thread", poast.host, poast.id]; + // const existing = qc.getQueryData(queryKey); + // if (!existing || !("fpost" in (existing as any))) { + // qc.setQueryData(queryKey, { + // fpost: toFull(poast), + // }); + // } + // }); + // }, [data]); + return ( + <> + {Object.keys(data.feed) + .sort() + .reverse() + .map((i) => ( + + ))} + + ); +} + +export default TrillFeed; diff --git a/front/src/components/feed/Quote.tsx b/front/src/components/feed/Quote.tsx new file mode 100644 index 0000000..d71be40 --- /dev/null +++ b/front/src/components/feed/Quote.tsx @@ -0,0 +1,37 @@ +import type { FullNode } from "@/types/trill"; +import { date_diff } from "@/logic/utils"; +import { useLocation } from "wouter"; +import Body from "./Body"; +import Sigil from "../Sigil"; +import { toFlat } from "./RP"; + +function Quote({ + data, + refetch, + nest, +}: { + data: FullNode; + refetch?: Function; + nest: number; +}) { + const [_, navigate] = useLocation(); + function gotoQuote(e: React.MouseEvent) { + e.stopPropagation(); + navigate(`/feed/${data.host}/${data.id}`); + } + return ( +
+
+ ( +
+ + {data.author} +
+ ){date_diff(data.time, "short")} +
+ +
+ ); +} + +export default Quote; diff --git a/front/src/components/feed/RP.tsx b/front/src/components/feed/RP.tsx new file mode 100644 index 0000000..dc733cc --- /dev/null +++ b/front/src/components/feed/RP.tsx @@ -0,0 +1,47 @@ +import Post from "./Post"; +import type { Ship } from "@/types/urbit"; +import type { Poast, FullNode, ID } from "@/types/trill"; +import PostData from "./PostData"; +export default function (props: { + host: string; + id: string; + rter: Ship; + rtat: number; + rtid: ID; + refetch?: Function; +}) { + return PostData(props)(RP); +} + +function RP({ + data, + refetch, + rter, + rtat, + rtid, +}: { + data: FullNode; + refetch: Function; + rter: Ship; + rtat: number; + rtid: ID; +}) { + return ( + + ); +} + +export function toFlat(n: FullNode): Poast { + return { + ...n, + children: !n.children + ? [] + : Object.keys(n.children).map((c) => n.children[c].id), + }; +} diff --git a/front/src/components/feed/Reactions.tsx b/front/src/components/feed/Reactions.tsx new file mode 100644 index 0000000..58662cd --- /dev/null +++ b/front/src/components/feed/Reactions.tsx @@ -0,0 +1,118 @@ +import type { Poast } from "@/types/trill"; +import yeschad from "@/assets/reacts/yeschad.png"; +import cringe from "@/assets/reacts/cringe.png"; +import cry from "@/assets/reacts/cry.png"; +import doom from "@/assets/reacts/doom.png"; +import galaxy from "@/assets/reacts/galaxy.png"; +import gigachad from "@/assets/reacts/gigachad.png"; +import pepechin from "@/assets/reacts/pepechin.png"; +import pepeeyes from "@/assets/reacts/pepeeyes.png"; +import pepegmi from "@/assets/reacts/pepegmi.png"; +import pepesad from "@/assets/reacts/pepesad.png"; +import pink from "@/assets/reacts/pink.png"; +import soy from "@/assets/reacts/soy.png"; +import chad from "@/assets/reacts/chad.png"; +import pika from "@/assets/reacts/pika.png"; +import facepalm from "@/assets/reacts/facepalm.png"; +import emoji from "@/assets/icons/emoji.svg"; +import emojis from "@/logic/emojis.json"; +import Modal from "../modals/Modal"; +import useLocalState from "@/state/state"; + +export function ReactModal({ send }: { send: (s: string) => Promise }) { + const { setModal } = useLocalState(); + async function sendReact(e: React.MouseEvent, s: string) { + e.stopPropagation(); + const res = await send(s); + if (res) setModal(null); + } + // todo one more meme + return ( + +
+ sendReact(e, "❤️")}>️️❤️ + sendReact(e, "🤔")}>🤔 + sendReact(e, "😅")}>😅 + sendReact(e, "🤬")}>🤬 + sendReact(e, "😂")}>😂️ + sendReact(e, "🫡")}>🫡️ + sendReact(e, "🤢")}>🤢 + sendReact(e, "😭")}>😭 + sendReact(e, "😱")}>😱 + sendReact(e, "facepalm")} + src={facepalm} + alt="" + /> + sendReact(e, "👍")}>👍️ + sendReact(e, "👎")}>👎️ + sendReact(e, "☝")}>☝️ + sendReact(e, "🤝")}>🤝️ + sendReact(e, "🙏")}>🙏 + sendReact(e, "🤡")}>🤡 + sendReact(e, "👀")}>👀 + sendReact(e, "🎤")}>🎤 + sendReact(e, "💯")}>💯 + sendReact(e, "🔥")}>🔥 + sendReact(e, "yeschad")} src={yeschad} alt="" /> + sendReact(e, "gigachad")} + src={gigachad} + alt="" + /> + sendReact(e, "pika")} src={pika} alt="" /> + sendReact(e, "cringe")} src={cringe} alt="" /> + sendReact(e, "pepegmi")} src={pepegmi} alt="" /> + sendReact(e, "pepesad")} src={pepesad} alt="" /> + sendReact(e, "galaxy")} src={galaxy} alt="" /> + sendReact(e, "pink")} src={pink} alt="" /> + sendReact(e, "soy")} src={soy} alt="" /> + sendReact(e, "cry")} src={cry} alt="" /> + sendReact(e, "doom")} src={doom} alt="" /> +
+
+ ); +} + +export function stringToReact(s: string) { + const em = (emojis as Record)[s.replace(/\:/g, "")]; + if (s === "yeschad") + return ; + if (s === "facepalm") + return ; + if (s === "yes.jpg") + return ; + if (s === "gigachad") + return ; + if (s === "pepechin") + return ; + if (s === "pepeeyes") + return ; + if (s === "pepegmi") + return ; + if (s === "pepesad") + return ; + if (s === "") + return ; + if (s === "cringe") return ; + if (s === "cry") return ; + if (s === "crywojak") return ; + if (s === "doom") return ; + if (s === "galaxy") return ; + if (s === "pink") return ; + if (s === "pinkwojak") return ; + if (s === "soy") return ; + if (s === "chad") return ; + if (s === "pika") return ; + if (em) return {em}; + else if (s.length > 2) return ; + else return {s}; +} + +export function TrillReactModal({ poast }: { poast: Poast }) { + const { api } = useLocalState(); + async function sendReact(s: string) { + return await api!.addReact(poast.host, poast.id, s); + } + return ; +} diff --git a/front/src/components/feed/StatsModal.tsx b/front/src/components/feed/StatsModal.tsx new file mode 100644 index 0000000..4720b2a --- /dev/null +++ b/front/src/components/feed/StatsModal.tsx @@ -0,0 +1,106 @@ +import type { Poast } from "@/types/trill"; +import Modal from "../modals/Modal"; +import { useState } from "react"; +import Post from "./Post"; +import RP from "./RP"; +import Avatar from "../Avatar"; +import { stringToReact } from "./Reactions"; + +function StatsModal({ poast, close }: { close: any; poast: Poast }) { + const [tab, setTab] = useState("replies"); + const replies = poast.children || []; + const quotes = poast.engagement.quoted; + const reposts = poast.engagement.shared; + const reacts = poast.engagement.reacts; + function set(e: React.MouseEvent, s: string) { + e.stopPropagation(); + setTab(s); + } + // TODO revise the global thingy here + return ( + +
+ {}} /> +
+
set(e, "replies")} + > +

Replies

+
+
set(e, "quotes")} + > +

Quotes

+
+
set(e, "reposts")} + > +

Reposts

+
+
set(e, "reacts")} + > +

Reacts

+
+
+
+ {tab === "replies" ? ( +
+ {replies.map((p) => ( +
+ +
+ ))} +
+ ) : tab === "quotes" ? ( +
+ {quotes.map((p) => ( +
+ +
+ ))} +
+ ) : tab === "reposts" ? ( +
+ {reposts.map((p) => ( +
+ +
+ ))} +
+ ) : tab === "reacts" ? ( +
+ {Object.keys(reacts).map((p) => ( +
+ + {stringToReact(reacts[p])} +
+ ))} +
+ ) : null} +
+
+
+ ); +} +export default StatsModal; diff --git a/front/src/components/layout/Sidebar.tsx b/front/src/components/layout/Sidebar.tsx new file mode 100644 index 0000000..1568421 --- /dev/null +++ b/front/src/components/layout/Sidebar.tsx @@ -0,0 +1,81 @@ +import { RADIO, versionNum } from "@/logic/constants"; +import { useLocation } from "wouter"; +import useLocalState from "@/state/state"; +import key from "@/assets/icons/key.svg"; +import logo from "@/assets/icons/logo.png"; +import home from "@/assets/icons/home.svg"; +import bell from "@/assets/icons/bell.svg"; +import settings from "@/assets/icons/settings.svg"; +import messages from "@/assets/icons/messages.svg"; +import profile from "@/assets/icons/profile.svg"; +import pals from "@/assets/icons/pals.svg"; +import rumors from "@/assets/icons/rumors.svg"; +import { ThemeSwitcher } from "@/styles/ThemeSwitcher"; + +function SlidingMenu() { + const [_, navigate] = useLocation(); + const { api } = useLocalState(); + function goto(to: string) { + navigate(to); + } + return ( +
+ +

Feeds

+
goto(`/feed/global`)}> + +
Home
+
+
goto(`/hark`)}> + +
Activity
+
+
+ +
goto("/chat")}> + +
Messages
+
+
goto("/pals")}> + +
Pals
+
+
+
goto(`/feed/${api!.airlock.our}`)} + > + +
Profile
+
+
goto("/feed/anon")}> + +
Rumors
+
+
+
goto("/radio")}> +
{RADIO}
+
Radio
+
+
+
(window.location.href = "/cookies")} + > + +
Logins
+
+
goto("/sets")}> + +
Settings
+
+ +
+ ); +} +export default SlidingMenu; diff --git a/front/src/components/modals/Modal.tsx b/front/src/components/modals/Modal.tsx new file mode 100644 index 0000000..7dd688c --- /dev/null +++ b/front/src/components/modals/Modal.tsx @@ -0,0 +1,72 @@ +import useLocalState from "@/state/state"; +import { useEffect, useRef, useState } from "react"; + +function Modal({ children }: any) { + const { setModal } = useLocalState(); + function onKey(event: any) { + if (event.key === "Escape") setModal(null); + } + useEffect(() => { + document.addEventListener("keyup", onKey); + return () => { + document.removeEventListener("keyup", onKey); + }; + }, [children]); + + function clickAway(e: React.MouseEvent) { + console.log("clicked away"); + e.stopPropagation(); + if (!modalRef.current || !modalRef.current.contains(e.target)) + setModal(null); + } + const modalRef = useRef(null); + return ( + + ); +} +export default Modal; + +export function Welcome() { + return ( + +
+

Welcome to Nostril!

+

+ Trill is the world's only truly free and sovereign social media + platform, powered by Urbit. +

+

+ Click on the crow icon on the top left to see all available feeds. +

+

The Global feed should be populated by default.

+

Follow people soon so your Global feed doesn't go stale.

+

+ Trill is still on beta. The UI is Mobile only, we recommend you use + your phone or the browser dev tools. Desktop UI is on the works. +

+

+ If you have any feedback please reach out to us on Groups at + ~hoster-dozzod-sortug/trill or here at ~polwex +

+
+
+ ); +} + +export function Tooltip({ children, text, className }: any) { + const [show, toggle] = useState(false); + return ( +
toggle(true)} + onMouseOut={() => toggle(false)} + > + {children} + {show &&
{text}
} +
+ ); +} diff --git a/front/src/components/modals/ShipModal.tsx b/front/src/components/modals/ShipModal.tsx new file mode 100644 index 0000000..86bffbb --- /dev/null +++ b/front/src/components/modals/ShipModal.tsx @@ -0,0 +1,45 @@ +import type { Ship } from "@/types/urbit"; +import Modal from "./Modal"; +import Avatar from "../Avatar"; +import copyIcon from "@/assets/icons/copy.svg"; +import useLocalState from "@/state/state"; +import { useLocation } from "wouter"; +import toast from "react-hot-toast"; + +export default function ({ ship }: { ship: Ship }) { + const { setModal, api } = useLocalState(); + const [_, navigate] = useLocation(); + function close() { + setModal(null); + } + async function copy(e: React.MouseEvent) { + e.stopPropagation(); + await navigator.clipboard.writeText(ship); + toast.success("Copied to clipboard"); + } + return ( + +
+
+ + +
+
+ + + {ship !== api!.airlock.our && ( + <> + + + )} +
+
+
+ ); +} diff --git a/front/src/components/snippets/Snippets.tsx b/front/src/components/snippets/Snippets.tsx new file mode 100644 index 0000000..68f5446 --- /dev/null +++ b/front/src/components/snippets/Snippets.tsx @@ -0,0 +1,395 @@ +import { fetchTweet, lurkTweet } from "@/logic/twatter/calls"; +import { pokeDister, scryDister, scryGangs } from "@/logic/requests/tlon"; +import { useEffect, useState } from "react"; +import Tweet from "@/sections/twatter/Tweet"; +import { toFlat } from "@/sections/feed/thread/helpers"; +import PostData from "@/sections/feed/PostData"; +import Post from "@/sections/feed/post/Post"; +import { FullNode, SortugRef } from "@/types/trill"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { subscribe, unsub } from "@/logic/requests/generic"; +import { AppData, GroupMetadata } from "@/types/tlon"; +import comet from "@/assets/icons/comet.svg"; +import Sigil from "@/ui/Sigil"; +import { PollLoader } from "@/sections/feed/poll/Show"; +import { parseThread, parseTweet } from "@/logic/twatter/parser"; +import { Tweet as TweetType } from "@/types/twatter"; +import { scryRadio } from "@/logic/requests/nostril"; +import useLocalState from "@/state/state"; +import { RadioTower, ScheduledRadio, radioLink } from "@/logic/requests/radio"; +import { Ship } from "@/types/urbit"; +import { RADIO } from "@/logic/constants"; +import { SigilOnly } from "../Avatar"; +import { date_diff } from "@/logic/utils"; +import ShipsModal from "../modals/ShipsModal"; + +export function TrillSnippet({ r }: { r: SortugRef }) { + const { ship, path } = r; + return PostData({ host: ship, id: path.slice(1) })(TrillSnippetMarkup); +} +function TrillSnippetMarkup({ + data, + refetch, +}: { + data: FullNode; + refetch: Function; +}) { + return ( +
+ +
+ ); +} +//
{ +// if (pop) pop(link); +// }} +// className="chat-snippet trill-snippet" +// > +// Post not found +//
+// ); + +export function TweetSnippet({ + link, + giveBack, +}: { + link: string; + giveBack?: Function; +}) { + const id = link.split("/")[5]; + const { isLoading, isError, data } = useQuery({ + queryKey: ["twatter-thread", id], + queryFn: () => lurkTweet(id), + }); + const [tw, setTw] = useState(); + useEffect(() => { + if (data && "thread-lurk" in data) { + const js = JSON.parse(data["thread-lurk"]).data.tweetResult; + if (JSON.stringify(js) === "{}") return; + if (giveBack) giveBack(JSON.stringify(parseTweet(js.result))); + } + }, [data]); + if (isLoading || isError) + return ( +
+

Fetching Tweet from your Urbit...

+
+ ); + else { + if ("no-coki" in data) + return ( + + ); + if ("fail" in data) + return ( +

+ Bad request. Please send some feedback (here) of what you were trying + to fetch. +

+ ); + if ("thread-lurk" in data) { + const js = JSON.parse(data["thread-lurk"]).data.tweetResult; + if (JSON.stringify(js) === "{}") + return null; // TODO wtf + else + return ( +
+ +
+ ); + } + // else { + // const head = parseThread(JSON.parse(data.thread)); + // const tweet = head.thread.tweets[0] + // giveBack(JSON.stringify(tweet)) + // return ( + //
+ // + //
+ // ); + // } + } +} + +export function AppSnippet({ r }: { r: SortugRef }) { + async function sub() { + if (!subn) { + const s = await subscribe( + "treaty", + "/treaties", + (data: { add: AppData }) => { + if ("ini" in data) { + const app = Object.values(data.ini).find((d) => d.desk === name); + setApp(app); + } + if ("add" in data && data.add.desk === name) setApp(data.add); + if (appData) unsub(subn); + }, + ); + setSub(s); + const res = await pokeDister(ship); + } + } + const { ship, path } = r; + const name = path.slice(1); + const [appData, setApp] = useState(); + const [subn, setSub] = useState(); + const { isLoading, data, isError } = useQuery({ + queryKey: ["dister", ship], + queryFn: () => scryDister(ship), + }); + if (isLoading || isError) return
...
; + else { + const app = Object.values(data.ini).find((d) => d.desk === name); + if (!app && !appData) sub(); + const a = app + ? app + : appData + ? appData + : { title: name, image: comet, info: "", ship }; + return ( +
+ +
+ ); + } +} +function AppDiv({ app }: { app: Partial }) { + return ( + <> + +
+

{app.title}

+

{app.info}

+

App from {app.ship}

+
+

+ +

+ + ); +} + +export function TlonSnippet({ r }: { r: SortugRef }) { + if (r.type === "app") return ; + if (r.type === "groups") return ; +} +export function GroupSnippet({ r }: { r: SortugRef }) { + const queryClient = useQueryClient(); + async function sub() { + if (!subn) { + const path = `/gangs/index/${ship}`; + const s = await subscribe("groups", path, (data: any) => { + const key = `${ship}/${name}`; + const val = data[key]; + queryClient.setQueryData(["gangs"], (old: any) => { + return { ...old, [key]: { preview: val } }; + }); + }); + setSub(s); + } + } + const { ship, path } = r; + const name = path.slice(1); + const [groupData, setGroup] = useState(); + const [subn, setSub] = useState(); + const { isLoading, data, isError } = useQuery({ + queryKey: ["gangs"], + queryFn: scryGangs, + }); + if (isLoading || isError) return
...
; + else { + const group = data[`${ship}/${name}`]; + if (!group && !groupData) sub(); + const a = + group && group.preview + ? group.preview.meta + : groupData + ? groupData + : { title: name, image: comet, cover: "", description: "" }; + return ( +
+ {a.image.startsWith("#") ? ( +
+ ) : ( + + )} +
+

{a.title}

+

+ {a.description.length > 25 + ? a.description.substring(0, 25) + "..." + : a.description} +

+

Group by {ship}

+
+ {/*

+ +

*/} +
+ ); + } +} + +export function PollSnippet({ r }: { r: SortugRef }) { + return ( +
+ +
+ ); +} + +export function SnippetHandler(props: { r: SortugRef }) { + if (props.r.type === "trill") return ; + if (props.r.type === "trill-polls") return ; + if (props.r.type === "app") return ; + if (props.r.type === "groups") return ; +} + +export function RadioSnippet({ ship }: { ship: Ship }) { + const { our } = useLocalState(); + return ship === our ? : ; +} + +function DudesRadio({ ship }: { ship }) { + function onc() { + radioLink(ship); + } + const { radioTowers } = useLocalState(); + const tower = radioTowers.find((t) => t.location === ship); + if (!tower) + return ( +
+

{RADIO}

+
+

Radio data not published. Click and check.

; +
+
+ ); + else + return ( +
+

{RADIO}

+
+

Radio Session. Playing: {tower.description}

+

Started {new Date(tower.time).toLocaleString()}

+
+
+ + + {tower.viewers} + 👀 + +
+
+ ); +} + +function OwnRadio() { + const { currentRadio, our, setModal, radioTowers } = useLocalState(); + const [scheduled, setS] = useState(null); + function onc() { + radioLink(our); + } + useEffect(() => { + scryRadio().then((r) => { + if (r) setS(r.radio); + }); + }, []); + function showViewers() { + const modal = ( + + ); + setModal(modal); + } + if (scheduled && scheduled.time > Date.now()) + return ( +
+

{RADIO}

+
+

+ Radio Session. Playing: + + {scheduled.desc} + +

+

Starting at {new Date(scheduled.time).toLocaleString()}

+
+
+ +
+
+ ); + else if (!currentRadio) + return ( +
+

{RADIO}

+
+

Radio unavailable

+
+
+ ); + else + return ( +
+

{RADIO}

+
+

+ Radio Session. Playing: + + {currentRadio.description} + +

+ {/*

Started {date_diff(currentRadio.time, "long")}

*/} +
+
+ + + {currentRadio?.viewers?.length || ""} + 👀 + +
+
+ ); + + // return ( + // {scheduled > Date.now() + // ? (<> + //

+ // Radio Session. Playing: + // + // {currentRadio.description} + // + //

+ + //

Starting at {new Date(scheduled).toLocaleString()}

+ // + + // ): scheduled !== 0() + + // } + //

+ // Radio Session. Playing: + // + // {currentRadio.description} + // + //

+ // {scheduled && scheduled > Date.now() ? ( + //

Starting at {new Date(scheduled).toLocaleString()}

+ // ) : scheduled !== 0 ? ( + //

Started {date_diff(new Date(scheduled), "long")}. Click to join.

+ // ) : ( + //

Unscheduled session. Click to join.

+ // )} + // ); +} diff --git a/front/src/logic/api.ts b/front/src/logic/api.ts new file mode 100644 index 0000000..b8acba2 --- /dev/null +++ b/front/src/logic/api.ts @@ -0,0 +1,15 @@ +import Urbit from "urbit-api"; + +export const URL = import.meta.env.PROD ? "" : "http://localhost:8080"; + +export async function start(): Promise { + const airlock = new Urbit(URL, ""); + const res = await fetch(URL + "/~/host"); + const ship = await res.text(); + airlock.ship = ship.slice(1); + airlock.our = ship; + airlock.desk = "nostril"; + await airlock.poke({ app: "hood", mark: "helm-hi", json: "opening airlock" }); + await airlock.eventSource(); + return airlock; +} diff --git a/front/src/logic/bunts.ts b/front/src/logic/bunts.ts new file mode 100644 index 0000000..dfa70e3 --- /dev/null +++ b/front/src/logic/bunts.ts @@ -0,0 +1,51 @@ +import type { Engagement, List, Lock } from "@/types/trill"; + +export const openLock: Lock = { + rank: { caveats: [], locked: false, public: true }, + luk: { caveats: [], locked: false, public: true }, + ship: { caveats: [], locked: false, public: true }, + tags: { caveats: [], locked: false, public: true }, + custom: { fn: null, public: false }, +}; + +export const engagementBunt: Engagement = { + reacts: {}, + quoted: [], + shared: [], +}; + +export const pushStateBunt = { + followers: [], + gate: { + lock: openLock, + mute: openLock, + begs: [], + "post-begs": [], + backlog: 0, + }, +}; + +export const harkStateBunt = { + unread: {}, + engagement: [], +}; + +export const pullStateBunt = { + following: [], + begs: [], + "post-begs": [], +}; +export const listBunt: List = { + symbol: "", + name: "", + desc: "", + icon: "", + cover: "", + members: [], + public: true, +}; + +// export const palsBunt: Pals = { +// incoming: {}, +// outgoing: {} +// } diff --git a/front/src/logic/constants.ts b/front/src/logic/constants.ts new file mode 100644 index 0000000..fcf5573 --- /dev/null +++ b/front/src/logic/constants.ts @@ -0,0 +1,36 @@ +import type { Poast } from "@/types/trill"; + +export const versionNum = "0.1.0"; +export const TIMEOUT = 15_000; + +export const ChatPostCount = 50; +export const FeedPostCount = 50; +export const RumorShip = "~londev-dozzod-sortug"; +export const RumorShip2 = "~paldev"; + +export function isRumor(poast: Poast) { + return poast.author === RumorShip || poast.author === RumorShip2; +} + +export const MOBILE_BROWSER_REGEX = + /Android|webOS|iPhone|iPad|iPod|BlackBerry/i; +export const AUDIO_REGEX = new RegExp(/https:\/\/.+\.(mp3|wav|ogg)\b/gim); +export const VIDEO_REGEX = new RegExp(/https:\/\/.+\.(mov|mp4|ogv)\b/gim); +export const TWITTER_REGEX = new RegExp( + /https:\/\/(twitter|x)\.com\/.+\/status\/\d+/gim, +); + +export const REF_REGEX = new RegExp( + /urbit:\/\/[a-z0-9-]+\/~[a-z-_]+\/[a-z0-9-_]+/gim, +); +export const RADIO_REGEX = new RegExp(/urbit:\/\/radio\/~[a-z-_]+/gim); + +export const IMAGE_REGEX = new RegExp( + /https:\/\/.+\.(jpg|img|png|gif|tiff|jpeg|webp|webm|svg)\b/gim, +); + +export const SHIP_REGEX = new RegExp(/\B~[a-z-]+/); +export const HASHTAGS_REGEX = new RegExp(/#[a-z-]+/g); + +export const DEFAULT_DATE = { year: 1970, month: 1, day: 1 }; +export const RADIO = "📻"; diff --git a/front/src/logic/emojis.json b/front/src/logic/emojis.json new file mode 100644 index 0000000..599f707 --- /dev/null +++ b/front/src/logic/emojis.json @@ -0,0 +1,3613 @@ +{ + "100": "💯", + "1000": "1000", + "1234": "🔢", + "white_heavy_check_mark": "✅", + "done": "✅", + "pants": "👖", + "squirrel": "🐿️", + "partyparrot": "🦜", + "party-parrot": "🦜", + "60fps_parrot": "🦜", + "charmander": "🔥", + "java": "☕", + "slack": "S", + "discord": "D", + "github": "G", + "jira": "J", + "shame": "😳", + "airbyte-100": "💯", + "airbyte-fire": "🔥", + "firee": "🔥", + "meow_party": "🎉", + "meowparty": "🎉", + "partyblob": "🎉", + "party-blob": "🎉", + "party_blob": "🎉", + "octavia-hmm": "🤔", + "octavia-hello": "👋", + "ory_love": "❤️", + "grinning": "😀", + "smiley": "😃", + "smile": "😄", + "grin": "😁", + "laughing": "😆", + "satisfied": "😆", + "sweat_smile": "😅", + "rolling_on_the_floor_laughing": "🤣", + "joy": "😂", + "slightly_smiling_face": "🙂", + "upside_down_face": "🙃", + "melting_face": "🫠", + "wink": "😉", + "blush": "😊", + "innocent": "😇", + "smiling_face_with_3_hearts": "🥰", + "heart_eyes": "😍", + "star-struck": "🤩", + "grinning_face_with_star_eyes": "🤩", + "kissing_heart": "😘", + "kissing": "😗", + "relaxed": "☺️", + "kissing_closed_eyes": "😚", + "kissing_smiling_eyes": "😙", + "smiling_face_with_tear": "🥲", + "yum": "😋", + "stuck_out_tongue": "😛", + "stuck_out_tongue_winking_eye": "😜", + "zany_face": "🤪", + "grinning_face_with_one_large_and_one_small_eye": "🤪", + "stuck_out_tongue_closed_eyes": "😝", + "money_mouth_face": "🤑", + "hugging_face": "🤗", + "face_with_hand_over_mouth": "🤭", + "smiling_face_with_smiling_eyes_and_hand_covering_mouth": "🤭", + "face_with_open_eyes_and_hand_over_mouth": "🫢", + "face_with_peeking_eye": "🫣", + "shushing_face": "🤫", + "face_with_finger_covering_closed_lips": "🤫", + "thinking_face": "🤔", + "saluting_face": "🫡", + "zipper_mouth_face": "🤐", + "face_with_raised_eyebrow": "🤨", + "face_with_one_eyebrow_raised": "🤨", + "neutral_face": "😐", + "expressionless": "😑", + "no_mouth": "😶", + "dotted_line_face": "🫥", + "face_in_clouds": "😶‍🌫️", + "smirk": "😏", + "unamused": "😒", + "face_with_rolling_eyes": "🙄", + "grimacing": "😬", + "face_exhaling": "😮‍💨", + "lying_face": "🤥", + "relieved": "😌", + "pensive": "😔", + "sleepy": "😪", + "drooling_face": "🤤", + "sleeping": "😴", + "mask": "😷", + "face_with_thermometer": "🤒", + "face_with_head_bandage": "🤕", + "nauseated_face": "🤢", + "face_vomiting": "🤮", + "face_with_open_mouth_vomiting": "🤮", + "sneezing_face": "🤧", + "hot_face": "🥵", + "cold_face": "🥶", + "woozy_face": "🥴", + "dizzy_face": "😵", + "face_with_spiral_eyes": "😵‍💫", + "exploding_head": "🤯", + "wow_fb": "🤯", + "shocked_face_with_exploding_head": "🤯", + "face_with_cowboy_hat": "🤠", + "partying_face": "🥳", + "disguised_face": "🥸", + "sunglasses": "😎", + "nerd_face": "🤓", + "face_with_monocle": "🧐", + "confused": "😕", + "face_with_diagonal_mouth": "🫤", + "worried": "😟", + "slightly_frowning_face": "🙁", + "white_frowning_face": "☹️", + "open_mouth": "😮", + "hushed": "😯", + "astonished": "😲", + "flushed": "😳", + "pleading_face": "🥺", + "face_holding_back_tears": "🥹", + "frowning": "😦", + "anguished": "😧", + "fearful": "😨", + "cold_sweat": "😰", + "disappointed_relieved": "😥", + "cry": "😢", + "sob": "😭", + "scream": "😱", + "confounded": "😖", + "persevere": "😣", + "disappointed": "😞", + "sweat": "😓", + "weary": "😩", + "tired_face": "😫", + "yawning_face": "🥱", + "triumph": "😤", + "rage": "😡", + "jakesidsmithmadness": "😡", + "angry": "😠", + "face_with_symbols_on_mouth": "🤬", + "serious_face_with_symbols_covering_mouth": "🤬", + "smiling_imp": "😈", + "imp": "👿", + "skull": "💀", + "skull_and_crossbones": "☠️", + "hankey": "💩", + "shit": "💩", + "clown_face": "🤡", + "japanese_ogre": "👹", + "japanese_goblin": "👺", + "ghost": "👻", + "alien": "👽", + "space_invader": "👾", + "robot_face": "🤖", + "android": "🤖", + "smiley_cat": "😺", + "smile_cat": "😸", + "joy_cat": "😹", + "heart_eyes_cat": "😻", + "smirk_cat": "😼", + "kissing_cat": "😽", + "scream_cat": "🙀", + "crying_cat_face": "😿", + "pouting_cat": "😾", + "see_no_evil": "🙈", + "hear_no_evil": "🙉", + "speak_no_evil": "🙊", + "kiss": "💋", + "love_letter": "💌", + "cupid": "💘", + "gift_heart": "💝", + "sparkling_heart": "💖", + "heartpulse": "💗", + "heartbeat": "💓", + "revolving_hearts": "💞", + "two_hearts": "💕", + "heart_decoration": "💟", + "heavy_heart_exclamation_mark_ornament": "❣️", + "broken_heart": "💔", + "heart_on_fire": "❤️‍🔥", + "mending_heart": "❤️‍🩹", + "heart": "❤️", + "kodee_love": "❤️", + "kodee-love": "❤️", + "orange_heart": "🧡", + "yellow_heart": "💛", + "green_heart": "💚", + "blue_heart": "💙", + "purple_heart": "💜", + "brown_heart": "🤎", + "black_heart": "🖤", + "white_heart": "🤍", + "anger": "💢", + "boom": "💥", + "collision": "💥", + "dizzy": "💫", + "sweat_drops": "💦", + "dash": "💨", + "hole": "🕳️", + "bomb": "💣", + "speech_balloon": "💬", + "eye-in-speech-bubble": "👁️‍🗨️", + "left_speech_bubble": "🗨️", + "right_anger_bubble": "🗯️", + "thought_balloon": "💭", + "zzz": "💤", + "wave_animated": "👋", + "wave": "👋", + "wave::skin-tone-2": "👋🏻", + "wave::skin-tone-3": "👋🏼", + "wave::skin-tone-4": "👋🏽", + "wave::skin-tone-5": "👋🏾", + "wave::skin-tone-6": "👋🏿", + "raised_back_of_hand": "🤚", + "raised_back_of_hand::skin-tone-2": "🤚🏻", + "raised_back_of_hand::skin-tone-3": "🤚🏼", + "raised_back_of_hand::skin-tone-4": "🤚🏽", + "raised_back_of_hand::skin-tone-5": "🤚🏾", + "raised_back_of_hand::skin-tone-6": "🤚🏿", + "raised_hand_with_fingers_splayed": "🖐️", + "raised_hand_with_fingers_splayed::skin-tone-2": "🖐🏻", + "raised_hand_with_fingers_splayed::skin-tone-3": "🖐🏼", + "raised_hand_with_fingers_splayed::skin-tone-4": "🖐🏽", + "raised_hand_with_fingers_splayed::skin-tone-5": "🖐🏾", + "raised_hand_with_fingers_splayed::skin-tone-6": "🖐🏿", + "hand": "✋", + "raised_hand": "✋", + "hand::skin-tone-2": "✋🏻", + "raised_hand::skin-tone-2": "✋🏻", + "hand::skin-tone-3": "✋🏼", + "raised_hand::skin-tone-3": "✋🏼", + "hand::skin-tone-4": "✋🏽", + "raised_hand::skin-tone-4": "✋🏽", + "hand::skin-tone-5": "✋🏾", + "raised_hand::skin-tone-5": "✋🏾", + "hand::skin-tone-6": "✋🏿", + "raised_hand::skin-tone-6": "✋🏿", + "spock-hand": "🖖", + "spock-hand::skin-tone-2": "🖖🏻", + "spock-hand::skin-tone-3": "🖖🏼", + "spock-hand::skin-tone-4": "🖖🏽", + "spock-hand::skin-tone-5": "🖖🏾", + "spock-hand::skin-tone-6": "🖖🏿", + "rightwards_hand": "🫱", + "rightwards_hand::skin-tone-2": "🫱🏻", + "rightwards_hand::skin-tone-3": "🫱🏼", + "rightwards_hand::skin-tone-4": "🫱🏽", + "rightwards_hand::skin-tone-5": "🫱🏾", + "rightwards_hand::skin-tone-6": "🫱🏿", + "leftwards_hand": "🫲", + "leftwards_hand::skin-tone-2": "🫲🏻", + "leftwards_hand::skin-tone-3": "🫲🏼", + "leftwards_hand::skin-tone-4": "🫲🏽", + "leftwards_hand::skin-tone-5": "🫲🏾", + "leftwards_hand::skin-tone-6": "🫲🏿", + "palm_down_hand": "🫳", + "palm_down_hand::skin-tone-2": "🫳🏻", + "palm_down_hand::skin-tone-3": "🫳🏼", + "palm_down_hand::skin-tone-4": "🫳🏽", + "palm_down_hand::skin-tone-5": "🫳🏾", + "palm_down_hand::skin-tone-6": "🫳🏿", + "palm_up_hand": "🫴", + "palm_up_hand::skin-tone-2": "🫴🏻", + "palm_up_hand::skin-tone-3": "🫴🏼", + "palm_up_hand::skin-tone-4": "🫴🏽", + "palm_up_hand::skin-tone-5": "🫴🏾", + "palm_up_hand::skin-tone-6": "🫴🏿", + "ok_hand": "👌", + "ok_hand::skin-tone-2": "👌🏻", + "ok_hand::skin-tone-3": "👌🏼", + "ok_hand::skin-tone-4": "👌🏽", + "ok_hand::skin-tone-5": "👌🏾", + "ok_hand::skin-tone-6": "👌🏿", + "nice": "👌", + "nice::skin-tone-2": "👌🏻", + "nice::skin-tone-3": "👌🏼", + "nice::skin-tone-4": "👌🏽", + "nice::skin-tone-5": "👌🏾", + "nice::skin-tone-6": "👌🏿", + "pinched_fingers": "🤌", + "pinched_fingers::skin-tone-2": "🤌🏻", + "pinched_fingers::skin-tone-3": "🤌🏼", + "pinched_fingers::skin-tone-4": "🤌🏽", + "pinched_fingers::skin-tone-5": "🤌🏾", + "pinched_fingers::skin-tone-6": "🤌🏿", + "pinching_hand": "🤏", + "pinching_hand::skin-tone-2": "🤏🏻", + "pinching_hand::skin-tone-3": "🤏🏼", + "pinching_hand::skin-tone-4": "🤏🏽", + "pinching_hand::skin-tone-5": "🤏🏾", + "pinching_hand::skin-tone-6": "🤏🏿", + "v": "✌️", + "v::skin-tone-2": "✌🏻", + "v::skin-tone-3": "✌🏼", + "v::skin-tone-4": "✌🏽", + "v::skin-tone-5": "✌🏾", + "v::skin-tone-6": "✌🏿", + "crossed_fingers": "🤞", + "hand_with_index_and_middle_fingers_crossed": "🤞", + "crossed_fingers::skin-tone-2": "🤞🏻", + "hand_with_index_and_middle_fingers_crossed::skin-tone-2": "🤞🏻", + "crossed_fingers::skin-tone-3": "🤞🏼", + "hand_with_index_and_middle_fingers_crossed::skin-tone-3": "🤞🏼", + "crossed_fingers::skin-tone-4": "🤞🏽", + "hand_with_index_and_middle_fingers_crossed::skin-tone-4": "🤞🏽", + "crossed_fingers::skin-tone-5": "🤞🏾", + "hand_with_index_and_middle_fingers_crossed::skin-tone-5": "🤞🏾", + "crossed_fingers::skin-tone-6": "🤞🏿", + "hand_with_index_and_middle_fingers_crossed::skin-tone-6": "🤞🏿", + "hand_with_index_finger_and_thumb_crossed": "🫰", + "hand_with_index_finger_and_thumb_crossed::skin-tone-2": "🫰🏻", + "hand_with_index_finger_and_thumb_crossed::skin-tone-3": "🫰🏼", + "hand_with_index_finger_and_thumb_crossed::skin-tone-4": "🫰🏽", + "hand_with_index_finger_and_thumb_crossed::skin-tone-5": "🫰🏾", + "hand_with_index_finger_and_thumb_crossed::skin-tone-6": "🫰🏿", + "i_love_you_hand_sign": "🤟", + "i_love_you_hand_sign::skin-tone-2": "🤟🏻", + "i_love_you_hand_sign::skin-tone-3": "🤟🏼", + "i_love_you_hand_sign::skin-tone-4": "🤟🏽", + "i_love_you_hand_sign::skin-tone-5": "🤟🏾", + "i_love_you_hand_sign::skin-tone-6": "🤟🏿", + "the_horns": "🤘", + "sign_of_the_horns": "🤘", + "the_horns::skin-tone-2": "🤘🏻", + "sign_of_the_horns::skin-tone-2": "🤘🏻", + "the_horns::skin-tone-3": "🤘🏼", + "sign_of_the_horns::skin-tone-3": "🤘🏼", + "the_horns::skin-tone-4": "🤘🏽", + "sign_of_the_horns::skin-tone-4": "🤘🏽", + "the_horns::skin-tone-5": "🤘🏾", + "sign_of_the_horns::skin-tone-5": "🤘🏾", + "the_horns::skin-tone-6": "🤘🏿", + "sign_of_the_horns::skin-tone-6": "🤘🏿", + "call_me_hand": "🤙", + "call_me_hand::skin-tone-2": "🤙🏻", + "call_me_hand::skin-tone-3": "🤙🏼", + "call_me_hand::skin-tone-4": "🤙🏽", + "call_me_hand::skin-tone-5": "🤙🏾", + "call_me_hand::skin-tone-6": "🤙🏿", + "point_left": "👈", + "point_left::skin-tone-2": "👈🏻", + "point_left::skin-tone-3": "👈🏼", + "point_left::skin-tone-4": "👈🏽", + "point_left::skin-tone-5": "👈🏾", + "point_left::skin-tone-6": "👈🏿", + "point_right": "👉", + "point_right::skin-tone-2": "👉🏻", + "point_right::skin-tone-3": "👉🏼", + "point_right::skin-tone-4": "👉🏽", + "point_right::skin-tone-5": "👉🏾", + "point_right::skin-tone-6": "👉🏿", + "point_up_2": "👆", + "point_up_2::skin-tone-2": "👆🏻", + "point_up_2::skin-tone-3": "👆🏼", + "point_up_2::skin-tone-4": "👆🏽", + "point_up_2::skin-tone-5": "👆🏾", + "point_up_2::skin-tone-6": "👆🏿", + "middle_finger": "🖕", + "reversed_hand_with_middle_finger_extended": "🖕", + "middle_finger::skin-tone-2": "🖕🏻", + "reversed_hand_with_middle_finger_extended::skin-tone-2": "🖕🏻", + "middle_finger::skin-tone-3": "🖕🏼", + "reversed_hand_with_middle_finger_extended::skin-tone-3": "🖕🏼", + "middle_finger::skin-tone-4": "🖕🏽", + "reversed_hand_with_middle_finger_extended::skin-tone-4": "🖕🏽", + "middle_finger::skin-tone-5": "🖕🏾", + "reversed_hand_with_middle_finger_extended::skin-tone-5": "🖕🏾", + "middle_finger::skin-tone-6": "🖕🏿", + "reversed_hand_with_middle_finger_extended::skin-tone-6": "🖕🏿", + "point_down": "👇", + "point_down::skin-tone-2": "👇🏻", + "point_down::skin-tone-3": "👇🏼", + "point_down::skin-tone-4": "👇🏽", + "point_down::skin-tone-5": "👇🏾", + "point_down::skin-tone-6": "👇🏿", + "point_up": "☝️", + "point_up::skin-tone-2": "☝🏻", + "point_up::skin-tone-3": "☝🏼", + "point_up::skin-tone-4": "☝🏽", + "point_up::skin-tone-5": "☝🏾", + "point_up::skin-tone-6": "☝🏿", + "index_pointing_at_the_viewer": "🫵", + "index_pointing_at_the_viewer::skin-tone-2": "🫵🏻", + "index_pointing_at_the_viewer::skin-tone-3": "🫵🏼", + "index_pointing_at_the_viewer::skin-tone-4": "🫵🏽", + "index_pointing_at_the_viewer::skin-tone-5": "🫵🏾", + "index_pointing_at_the_viewer::skin-tone-6": "🫵🏿", + "+1": "👍", + "thumbsup": "👍", + "+1::skin-tone-2": "👍🏻", + "thumbsup::skin-tone-2": "👍🏻", + "+1::skin-tone-3": "👍🏼", + "thumbsup::skin-tone-3": "👍🏼", + "+1::skin-tone-4": "👍🏽", + "thumbsup::skin-tone-4": "👍🏽", + "+1::skin-tone-5": "👍🏾", + "thumbsup::skin-tone-5": "👍🏾", + "+1::skin-tone-6": "👍🏿", + "thumbsup::skin-tone-6": "👍🏿", + "-1": "👎", + "thumbsdown": "👎", + "-1::skin-tone-2": "👎🏻", + "thumbsdown::skin-tone-2": "👎🏻", + "-1::skin-tone-3": "👎🏼", + "thumbsdown::skin-tone-3": "👎🏼", + "-1::skin-tone-4": "👎🏽", + "thumbsdown::skin-tone-4": "👎🏽", + "-1::skin-tone-5": "👎🏾", + "thumbsdown::skin-tone-5": "👎🏾", + "-1::skin-tone-6": "👎🏿", + "thumbsdown::skin-tone-6": "👎🏿", + "fist": "✊", + "fist::skin-tone-2": "✊🏻", + "fist::skin-tone-3": "✊🏼", + "fist::skin-tone-4": "✊🏽", + "fist::skin-tone-5": "✊🏾", + "fist::skin-tone-6": "✊🏿", + "facepunch": "👊", + "punch": "👊", + "facepunch::skin-tone-2": "👊🏻", + "punch::skin-tone-2": "👊🏻", + "facepunch::skin-tone-3": "👊🏼", + "punch::skin-tone-3": "👊🏼", + "facepunch::skin-tone-4": "👊🏽", + "punch::skin-tone-4": "👊🏽", + "facepunch::skin-tone-5": "👊🏾", + "punch::skin-tone-5": "👊🏾", + "facepunch::skin-tone-6": "👊🏿", + "punch::skin-tone-6": "👊🏿", + "left-facing_fist": "🤛", + "left-facing_fist::skin-tone-2": "🤛🏻", + "left-facing_fist::skin-tone-3": "🤛🏼", + "left-facing_fist::skin-tone-4": "🤛🏽", + "left-facing_fist::skin-tone-5": "🤛🏾", + "left-facing_fist::skin-tone-6": "🤛🏿", + "right-facing_fist": "🤜", + "right-facing_fist::skin-tone-2": "🤜🏻", + "right-facing_fist::skin-tone-3": "🤜🏼", + "right-facing_fist::skin-tone-4": "🤜🏽", + "right-facing_fist::skin-tone-5": "🤜🏾", + "right-facing_fist::skin-tone-6": "🤜🏿", + "clap": "👏", + "clap::skin-tone-2": "👏🏻", + "clap::skin-tone-3": "👏🏼", + "clap::skin-tone-4": "👏🏽", + "clap::skin-tone-5": "👏🏾", + "clap::skin-tone-6": "👏🏿", + "raised_hands": "🙌", + "raised_hands::skin-tone-2": "🙌🏻", + "raised_hands::skin-tone-3": "🙌🏼", + "raised_hands::skin-tone-4": "🙌🏽", + "raised_hands::skin-tone-5": "🙌🏾", + "raised_hands::skin-tone-6": "🙌🏿", + "heart_hands": "🫶", + "heart_hands::skin-tone-2": "🫶🏻", + "heart_hands::skin-tone-3": "🫶🏼", + "heart_hands::skin-tone-4": "🫶🏽", + "heart_hands::skin-tone-5": "🫶🏾", + "heart_hands::skin-tone-6": "🫶🏿", + "open_hands": "👐", + "open_hands::skin-tone-2": "👐🏻", + "open_hands::skin-tone-3": "👐🏼", + "open_hands::skin-tone-4": "👐🏽", + "open_hands::skin-tone-5": "👐🏾", + "open_hands::skin-tone-6": "👐🏿", + "palms_up_together": "🤲", + "palms_up_together::skin-tone-2": "🤲🏻", + "palms_up_together::skin-tone-3": "🤲🏼", + "palms_up_together::skin-tone-4": "🤲🏽", + "palms_up_together::skin-tone-5": "🤲🏾", + "palms_up_together::skin-tone-6": "🤲🏿", + "handshake": "🤝", + "handshake::skin-tone-2": "🤝🏻", + "handshake::skin-tone-3": "🤝🏼", + "handshake::skin-tone-4": "🤝🏽", + "handshake::skin-tone-5": "🤝🏾", + "handshake::skin-tone-6": "🤝🏿", + "pray": "🙏", + "pray::skin-tone-2": "🙏🏻", + "pray::skin-tone-3": "🙏🏼", + "pray::skin-tone-4": "🙏🏽", + "pray::skin-tone-5": "🙏🏾", + "pray::skin-tone-6": "🙏🏿", + "writing_hand": "✍️", + "writing_hand::skin-tone-2": "✍🏻", + "writing_hand::skin-tone-3": "✍🏼", + "writing_hand::skin-tone-4": "✍🏽", + "writing_hand::skin-tone-5": "✍🏾", + "writing_hand::skin-tone-6": "✍🏿", + "nail_care": "💅", + "nail_care::skin-tone-2": "💅🏻", + "nail_care::skin-tone-3": "💅🏼", + "nail_care::skin-tone-4": "💅🏽", + "nail_care::skin-tone-5": "💅🏾", + "nail_care::skin-tone-6": "💅🏿", + "selfie": "🤳", + "selfie::skin-tone-2": "🤳🏻", + "selfie::skin-tone-3": "🤳🏼", + "selfie::skin-tone-4": "🤳🏽", + "selfie::skin-tone-5": "🤳🏾", + "selfie::skin-tone-6": "🤳🏿", + "muscle": "💪", + "muscle::skin-tone-2": "💪🏻", + "muscle::skin-tone-3": "💪🏼", + "muscle::skin-tone-4": "💪🏽", + "muscle::skin-tone-5": "💪🏾", + "muscle::skin-tone-6": "💪🏿", + "mechanical_arm": "🦾", + "mechanical_leg": "🦿", + "leg": "🦵", + "leg::skin-tone-2": "🦵🏻", + "leg::skin-tone-3": "🦵🏼", + "leg::skin-tone-4": "🦵🏽", + "leg::skin-tone-5": "🦵🏾", + "leg::skin-tone-6": "🦵🏿", + "foot": "🦶", + "foot::skin-tone-2": "🦶🏻", + "foot::skin-tone-3": "🦶🏼", + "foot::skin-tone-4": "🦶🏽", + "foot::skin-tone-5": "🦶🏾", + "foot::skin-tone-6": "🦶🏿", + "ear": "👂", + "ear::skin-tone-2": "👂🏻", + "ear::skin-tone-3": "👂🏼", + "ear::skin-tone-4": "👂🏽", + "ear::skin-tone-5": "👂🏾", + "ear::skin-tone-6": "👂🏿", + "ear_with_hearing_aid": "🦻", + "ear_with_hearing_aid::skin-tone-2": "🦻🏻", + "ear_with_hearing_aid::skin-tone-3": "🦻🏼", + "ear_with_hearing_aid::skin-tone-4": "🦻🏽", + "ear_with_hearing_aid::skin-tone-5": "🦻🏾", + "ear_with_hearing_aid::skin-tone-6": "🦻🏿", + "nose": "👃", + "nose::skin-tone-2": "👃🏻", + "nose::skin-tone-3": "👃🏼", + "nose::skin-tone-4": "👃🏽", + "nose::skin-tone-5": "👃🏾", + "nose::skin-tone-6": "👃🏿", + "brain": "🧠", + "anatomical_heart": "🫀", + "lungs": "🫁", + "tooth": "🦷", + "bone": "🦴", + "eyes": "👀", + "dag-eyes": "👀", + "eye": "👁️", + "tongue": "👅", + "lips": "👄", + "biting_lip": "🫦", + "baby": "👶", + "baby::skin-tone-2": "👶🏻", + "baby::skin-tone-3": "👶🏼", + "baby::skin-tone-4": "👶🏽", + "baby::skin-tone-5": "👶🏾", + "baby::skin-tone-6": "👶🏿", + "child": "🧒", + "child::skin-tone-2": "🧒🏻", + "child::skin-tone-3": "🧒🏼", + "child::skin-tone-4": "🧒🏽", + "child::skin-tone-5": "🧒🏾", + "child::skin-tone-6": "🧒🏿", + "boy": "👦", + "boy::skin-tone-2": "👦🏻", + "boy::skin-tone-3": "👦🏼", + "boy::skin-tone-4": "👦🏽", + "boy::skin-tone-5": "👦🏾", + "boy::skin-tone-6": "👦🏿", + "girl": "👧", + "girl::skin-tone-2": "👧🏻", + "girl::skin-tone-3": "👧🏼", + "girl::skin-tone-4": "👧🏽", + "girl::skin-tone-5": "👧🏾", + "girl::skin-tone-6": "👧🏿", + "adult": "🧑", + "adult::skin-tone-2": "🧑🏻", + "adult::skin-tone-3": "🧑🏼", + "adult::skin-tone-4": "🧑🏽", + "adult::skin-tone-5": "🧑🏾", + "adult::skin-tone-6": "🧑🏿", + "person_with_blond_hair": "👱", + "person_with_blond_hair::skin-tone-2": "👱🏻", + "person_with_blond_hair::skin-tone-3": "👱🏼", + "person_with_blond_hair::skin-tone-4": "👱🏽", + "person_with_blond_hair::skin-tone-5": "👱🏾", + "person_with_blond_hair::skin-tone-6": "👱🏿", + "man": "👨", + "man::skin-tone-2": "👨🏻", + "man::skin-tone-3": "👨🏼", + "man::skin-tone-4": "👨🏽", + "man::skin-tone-5": "👨🏾", + "man::skin-tone-6": "👨🏿", + "bearded_person": "🧔", + "bearded_person::skin-tone-2": "🧔🏻", + "bearded_person::skin-tone-3": "🧔🏼", + "bearded_person::skin-tone-4": "🧔🏽", + "bearded_person::skin-tone-5": "🧔🏾", + "bearded_person::skin-tone-6": "🧔🏿", + "man_with_beard": "🧔‍♂️", + "man_with_beard::skin-tone-2": "🧔🏻‍♂️", + "man_with_beard::skin-tone-3": "🧔🏼‍♂️", + "man_with_beard::skin-tone-4": "🧔🏽‍♂️", + "man_with_beard::skin-tone-5": "🧔🏾‍♂️", + "man_with_beard::skin-tone-6": "🧔🏿‍♂️", + "woman_with_beard": "🧔‍♀️", + "woman_with_beard::skin-tone-2": "🧔🏻‍♀️", + "woman_with_beard::skin-tone-3": "🧔🏼‍♀️", + "woman_with_beard::skin-tone-4": "🧔🏽‍♀️", + "woman_with_beard::skin-tone-5": "🧔🏾‍♀️", + "woman_with_beard::skin-tone-6": "🧔🏿‍♀️", + "red_haired_man": "👨‍🦰", + "red_haired_man::skin-tone-2": "👨🏻‍🦰", + "red_haired_man::skin-tone-3": "👨🏼‍🦰", + "red_haired_man::skin-tone-4": "👨🏽‍🦰", + "red_haired_man::skin-tone-5": "👨🏾‍🦰", + "red_haired_man::skin-tone-6": "👨🏿‍🦰", + "curly_haired_man": "👨‍🦱", + "curly_haired_man::skin-tone-2": "👨🏻‍🦱", + "curly_haired_man::skin-tone-3": "👨🏼‍🦱", + "curly_haired_man::skin-tone-4": "👨🏽‍🦱", + "curly_haired_man::skin-tone-5": "👨🏾‍🦱", + "curly_haired_man::skin-tone-6": "👨🏿‍🦱", + "white_haired_man": "👨‍🦳", + "white_haired_man::skin-tone-2": "👨🏻‍🦳", + "white_haired_man::skin-tone-3": "👨🏼‍🦳", + "white_haired_man::skin-tone-4": "👨🏽‍🦳", + "white_haired_man::skin-tone-5": "👨🏾‍🦳", + "white_haired_man::skin-tone-6": "👨🏿‍🦳", + "bald_man": "👨‍🦲", + "bald_man::skin-tone-2": "👨🏻‍🦲", + "bald_man::skin-tone-3": "👨🏼‍🦲", + "bald_man::skin-tone-4": "👨🏽‍🦲", + "bald_man::skin-tone-5": "👨🏾‍🦲", + "bald_man::skin-tone-6": "👨🏿‍🦲", + "woman": "👩", + "woman::skin-tone-2": "👩🏻", + "woman::skin-tone-3": "👩🏼", + "woman::skin-tone-4": "👩🏽", + "woman::skin-tone-5": "👩🏾", + "woman::skin-tone-6": "👩🏿", + "red_haired_woman": "👩‍🦰", + "red_haired_woman::skin-tone-2": "👩🏻‍🦰", + "red_haired_woman::skin-tone-3": "👩🏼‍🦰", + "red_haired_woman::skin-tone-4": "👩🏽‍🦰", + "red_haired_woman::skin-tone-5": "👩🏾‍🦰", + "red_haired_woman::skin-tone-6": "👩🏿‍🦰", + "red_haired_person": "🧑‍🦰", + "red_haired_person::skin-tone-2": "🧑🏻‍🦰", + "red_haired_person::skin-tone-3": "🧑🏼‍🦰", + "red_haired_person::skin-tone-4": "🧑🏽‍🦰", + "red_haired_person::skin-tone-5": "🧑🏾‍🦰", + "red_haired_person::skin-tone-6": "🧑🏿‍🦰", + "curly_haired_woman": "👩‍🦱", + "curly_haired_woman::skin-tone-2": "👩🏻‍🦱", + "curly_haired_woman::skin-tone-3": "👩🏼‍🦱", + "curly_haired_woman::skin-tone-4": "👩🏽‍🦱", + "curly_haired_woman::skin-tone-5": "👩🏾‍🦱", + "curly_haired_woman::skin-tone-6": "👩🏿‍🦱", + "curly_haired_person": "🧑‍🦱", + "curly_haired_person::skin-tone-2": "🧑🏻‍🦱", + "curly_haired_person::skin-tone-3": "🧑🏼‍🦱", + "curly_haired_person::skin-tone-4": "🧑🏽‍🦱", + "curly_haired_person::skin-tone-5": "🧑🏾‍🦱", + "curly_haired_person::skin-tone-6": "🧑🏿‍🦱", + "white_haired_woman": "👩‍🦳", + "white_haired_woman::skin-tone-2": "👩🏻‍🦳", + "white_haired_woman::skin-tone-3": "👩🏼‍🦳", + "white_haired_woman::skin-tone-4": "👩🏽‍🦳", + "white_haired_woman::skin-tone-5": "👩🏾‍🦳", + "white_haired_woman::skin-tone-6": "👩🏿‍🦳", + "white_haired_person": "🧑‍🦳", + "white_haired_person::skin-tone-2": "🧑🏻‍🦳", + "white_haired_person::skin-tone-3": "🧑🏼‍🦳", + "white_haired_person::skin-tone-4": "🧑🏽‍🦳", + "white_haired_person::skin-tone-5": "🧑🏾‍🦳", + "white_haired_person::skin-tone-6": "🧑🏿‍🦳", + "bald_woman": "👩‍🦲", + "bald_woman::skin-tone-2": "👩🏻‍🦲", + "bald_woman::skin-tone-3": "👩🏼‍🦲", + "bald_woman::skin-tone-4": "👩🏽‍🦲", + "bald_woman::skin-tone-5": "👩🏾‍🦲", + "bald_woman::skin-tone-6": "👩🏿‍🦲", + "bald_person": "🧑‍🦲", + "bald_person::skin-tone-2": "🧑🏻‍🦲", + "bald_person::skin-tone-3": "🧑🏼‍🦲", + "bald_person::skin-tone-4": "🧑🏽‍🦲", + "bald_person::skin-tone-5": "🧑🏾‍🦲", + "bald_person::skin-tone-6": "🧑🏿‍🦲", + "blond-haired-woman": "👱‍♀️", + "blond-haired-woman::skin-tone-2": "👱🏻‍♀️", + "blond-haired-woman::skin-tone-3": "👱🏼‍♀️", + "blond-haired-woman::skin-tone-4": "👱🏽‍♀️", + "blond-haired-woman::skin-tone-5": "👱🏾‍♀️", + "blond-haired-woman::skin-tone-6": "👱🏿‍♀️", + "blond-haired-man": "👱‍♂️", + "blond-haired-man::skin-tone-2": "👱🏻‍♂️", + "blond-haired-man::skin-tone-3": "👱🏼‍♂️", + "blond-haired-man::skin-tone-4": "👱🏽‍♂️", + "blond-haired-man::skin-tone-5": "👱🏾‍♂️", + "blond-haired-man::skin-tone-6": "👱🏿‍♂️", + "older_adult": "🧓", + "older_adult::skin-tone-2": "🧓🏻", + "older_adult::skin-tone-3": "🧓🏼", + "older_adult::skin-tone-4": "🧓🏽", + "older_adult::skin-tone-5": "🧓🏾", + "older_adult::skin-tone-6": "🧓🏿", + "older_man": "👴", + "older_man::skin-tone-2": "👴🏻", + "older_man::skin-tone-3": "👴🏼", + "older_man::skin-tone-4": "👴🏽", + "older_man::skin-tone-5": "👴🏾", + "older_man::skin-tone-6": "👴🏿", + "older_woman": "👵", + "older_woman::skin-tone-2": "👵🏻", + "older_woman::skin-tone-3": "👵🏼", + "older_woman::skin-tone-4": "👵🏽", + "older_woman::skin-tone-5": "👵🏾", + "older_woman::skin-tone-6": "👵🏿", + "person_frowning": "🙍", + "person_frowning::skin-tone-2": "🙍🏻", + "person_frowning::skin-tone-3": "🙍🏼", + "person_frowning::skin-tone-4": "🙍🏽", + "person_frowning::skin-tone-5": "🙍🏾", + "person_frowning::skin-tone-6": "🙍🏿", + "man-frowning": "🙍‍♂️", + "man-frowning::skin-tone-2": "🙍🏻‍♂️", + "man-frowning::skin-tone-3": "🙍🏼‍♂️", + "man-frowning::skin-tone-4": "🙍🏽‍♂️", + "man-frowning::skin-tone-5": "🙍🏾‍♂️", + "man-frowning::skin-tone-6": "🙍🏿‍♂️", + "woman-frowning": "🙍‍♀️", + "woman-frowning::skin-tone-2": "🙍🏻‍♀️", + "woman-frowning::skin-tone-3": "🙍🏼‍♀️", + "woman-frowning::skin-tone-4": "🙍🏽‍♀️", + "woman-frowning::skin-tone-5": "🙍🏾‍♀️", + "woman-frowning::skin-tone-6": "🙍🏿‍♀️", + "person_with_pouting_face": "🙎", + "person_with_pouting_face::skin-tone-2": "🙎🏻", + "person_with_pouting_face::skin-tone-3": "🙎🏼", + "person_with_pouting_face::skin-tone-4": "🙎🏽", + "person_with_pouting_face::skin-tone-5": "🙎🏾", + "person_with_pouting_face::skin-tone-6": "🙎🏿", + "man-pouting": "🙎‍♂️", + "man-pouting::skin-tone-2": "🙎🏻‍♂️", + "man-pouting::skin-tone-3": "🙎🏼‍♂️", + "man-pouting::skin-tone-4": "🙎🏽‍♂️", + "man-pouting::skin-tone-5": "🙎🏾‍♂️", + "man-pouting::skin-tone-6": "🙎🏿‍♂️", + "woman-pouting": "🙎‍♀️", + "woman-pouting::skin-tone-2": "🙎🏻‍♀️", + "woman-pouting::skin-tone-3": "🙎🏼‍♀️", + "woman-pouting::skin-tone-4": "🙎🏽‍♀️", + "woman-pouting::skin-tone-5": "🙎🏾‍♀️", + "woman-pouting::skin-tone-6": "🙎🏿‍♀️", + "no_good": "🙅", + "no_good::skin-tone-2": "🙅🏻", + "no_good::skin-tone-3": "🙅🏼", + "no_good::skin-tone-4": "🙅🏽", + "no_good::skin-tone-5": "🙅🏾", + "no_good::skin-tone-6": "🙅🏿", + "man-gesturing-no": "🙅‍♂️", + "man-gesturing-no::skin-tone-2": "🙅🏻‍♂️", + "man-gesturing-no::skin-tone-3": "🙅🏼‍♂️", + "man-gesturing-no::skin-tone-4": "🙅🏽‍♂️", + "man-gesturing-no::skin-tone-5": "🙅🏾‍♂️", + "man-gesturing-no::skin-tone-6": "🙅🏿‍♂️", + "woman-gesturing-no": "🙅‍♀️", + "woman-gesturing-no::skin-tone-2": "🙅🏻‍♀️", + "woman-gesturing-no::skin-tone-3": "🙅🏼‍♀️", + "woman-gesturing-no::skin-tone-4": "🙅🏽‍♀️", + "woman-gesturing-no::skin-tone-5": "🙅🏾‍♀️", + "woman-gesturing-no::skin-tone-6": "🙅🏿‍♀️", + "ok_woman": "🙆", + "ok_woman::skin-tone-2": "🙆🏻", + "ok_woman::skin-tone-3": "🙆🏼", + "ok_woman::skin-tone-4": "🙆🏽", + "ok_woman::skin-tone-5": "🙆🏾", + "ok_woman::skin-tone-6": "🙆🏿", + "man-gesturing-ok": "🙆‍♂️", + "man-gesturing-ok::skin-tone-2": "🙆🏻‍♂️", + "man-gesturing-ok::skin-tone-3": "🙆🏼‍♂️", + "man-gesturing-ok::skin-tone-4": "🙆🏽‍♂️", + "man-gesturing-ok::skin-tone-5": "🙆🏾‍♂️", + "man-gesturing-ok::skin-tone-6": "🙆🏿‍♂️", + "woman-gesturing-ok": "🙆‍♀️", + "woman-gesturing-ok::skin-tone-2": "🙆🏻‍♀️", + "woman-gesturing-ok::skin-tone-3": "🙆🏼‍♀️", + "woman-gesturing-ok::skin-tone-4": "🙆🏽‍♀️", + "woman-gesturing-ok::skin-tone-5": "🙆🏾‍♀️", + "woman-gesturing-ok::skin-tone-6": "🙆🏿‍♀️", + "information_desk_person": "💁", + "information_desk_person::skin-tone-2": "💁🏻", + "information_desk_person::skin-tone-3": "💁🏼", + "information_desk_person::skin-tone-4": "💁🏽", + "information_desk_person::skin-tone-5": "💁🏾", + "information_desk_person::skin-tone-6": "💁🏿", + "man-tipping-hand": "💁‍♂️", + "man-tipping-hand::skin-tone-2": "💁🏻‍♂️", + "man-tipping-hand::skin-tone-3": "💁🏼‍♂️", + "man-tipping-hand::skin-tone-4": "💁🏽‍♂️", + "man-tipping-hand::skin-tone-5": "💁🏾‍♂️", + "man-tipping-hand::skin-tone-6": "💁🏿‍♂️", + "woman-tipping-hand": "💁‍♀️", + "woman-tipping-hand::skin-tone-2": "💁🏻‍♀️", + "woman-tipping-hand::skin-tone-3": "💁🏼‍♀️", + "woman-tipping-hand::skin-tone-4": "💁🏽‍♀️", + "woman-tipping-hand::skin-tone-5": "💁🏾‍♀️", + "woman-tipping-hand::skin-tone-6": "💁🏿‍♀️", + "raising_hand": "🙋", + "raising_hand::skin-tone-2": "🙋🏻", + "raising_hand::skin-tone-3": "🙋🏼", + "raising_hand::skin-tone-4": "🙋🏽", + "raising_hand::skin-tone-5": "🙋🏾", + "raising_hand::skin-tone-6": "🙋🏿", + "man-raising-hand": "🙋‍♂️", + "man-raising-hand::skin-tone-2": "🙋🏻‍♂️", + "man-raising-hand::skin-tone-3": "🙋🏼‍♂️", + "man-raising-hand::skin-tone-4": "🙋🏽‍♂️", + "man-raising-hand::skin-tone-5": "🙋🏾‍♂️", + "man-raising-hand::skin-tone-6": "🙋🏿‍♂️", + "woman-raising-hand": "🙋‍♀️", + "woman-raising-hand::skin-tone-2": "🙋🏻‍♀️", + "woman-raising-hand::skin-tone-3": "🙋🏼‍♀️", + "woman-raising-hand::skin-tone-4": "🙋🏽‍♀️", + "woman-raising-hand::skin-tone-5": "🙋🏾‍♀️", + "woman-raising-hand::skin-tone-6": "🙋🏿‍♀️", + "deaf_person": "🧏", + "deaf_person::skin-tone-2": "🧏🏻", + "deaf_person::skin-tone-3": "🧏🏼", + "deaf_person::skin-tone-4": "🧏🏽", + "deaf_person::skin-tone-5": "🧏🏾", + "deaf_person::skin-tone-6": "🧏🏿", + "deaf_man": "🧏‍♂️", + "deaf_man::skin-tone-2": "🧏🏻‍♂️", + "deaf_man::skin-tone-3": "🧏🏼‍♂️", + "deaf_man::skin-tone-4": "🧏🏽‍♂️", + "deaf_man::skin-tone-5": "🧏🏾‍♂️", + "deaf_man::skin-tone-6": "🧏🏿‍♂️", + "deaf_woman": "🧏‍♀️", + "deaf_woman::skin-tone-2": "🧏🏻‍♀️", + "deaf_woman::skin-tone-3": "🧏🏼‍♀️", + "deaf_woman::skin-tone-4": "🧏🏽‍♀️", + "deaf_woman::skin-tone-5": "🧏🏾‍♀️", + "deaf_woman::skin-tone-6": "🧏🏿‍♀️", + "nod-nicholson": "🙇", + "daggy-celebrate": "🎉", + "bow": "🙇", + "bow::skin-tone-2": "🙇🏻", + "bow::skin-tone-3": "🙇🏼", + "bow::skin-tone-4": "🙇🏽", + "bow::skin-tone-5": "🙇🏾", + "bow::skin-tone-6": "🙇🏿", + "man-bowing": "🙇‍♂️", + "man-bowing::skin-tone-2": "🙇🏻‍♂️", + "man-bowing::skin-tone-3": "🙇🏼‍♂️", + "man-bowing::skin-tone-4": "🙇🏽‍♂️", + "man-bowing::skin-tone-5": "🙇🏾‍♂️", + "man-bowing::skin-tone-6": "🙇🏿‍♂️", + "woman-bowing": "🙇‍♀️", + "woman-bowing::skin-tone-2": "🙇🏻‍♀️", + "woman-bowing::skin-tone-3": "🙇🏼‍♀️", + "woman-bowing::skin-tone-4": "🙇🏽‍♀️", + "woman-bowing::skin-tone-5": "🙇🏾‍♀️", + "woman-bowing::skin-tone-6": "🙇🏿‍♀️", + "face_palm": "🤦", + "face_palm::skin-tone-2": "🤦🏻", + "face_palm::skin-tone-3": "🤦🏼", + "face_palm::skin-tone-4": "🤦🏽", + "face_palm::skin-tone-5": "🤦🏾", + "face_palm::skin-tone-6": "🤦🏿", + "man-facepalming": "🤦‍♂️", + "man-facepalming::skin-tone-2": "🤦🏻‍♂️", + "man-facepalming::skin-tone-3": "🤦🏼‍♂️", + "man-facepalming::skin-tone-4": "🤦🏽‍♂️", + "man-facepalming::skin-tone-5": "🤦🏾‍♂️", + "man-facepalming::skin-tone-6": "🤦🏿‍♂️", + "woman-facepalming": "🤦‍♀️", + "woman-facepalming::skin-tone-2": "🤦🏻‍♀️", + "woman-facepalming::skin-tone-3": "🤦🏼‍♀️", + "woman-facepalming::skin-tone-4": "🤦🏽‍♀️", + "woman-facepalming::skin-tone-5": "🤦🏾‍♀️", + "woman-facepalming::skin-tone-6": "🤦🏿‍♀️", + "shrug": "🤷", + "shrug::skin-tone-2": "🤷🏻", + "shrug::skin-tone-3": "🤷🏼", + "shrug::skin-tone-4": "🤷🏽", + "shrug::skin-tone-5": "🤷🏾", + "shrug::skin-tone-6": "🤷🏿", + "man-shrugging": "🤷‍♂️", + "man-shrugging::skin-tone-2": "🤷🏻‍♂️", + "man-shrugging::skin-tone-3": "🤷🏼‍♂️", + "man-shrugging::skin-tone-4": "🤷🏽‍♂️", + "man-shrugging::skin-tone-5": "🤷🏾‍♂️", + "man-shrugging::skin-tone-6": "🤷🏿‍♂️", + "woman-shrugging": "🤷‍♀️", + "woman-shrugging::skin-tone-2": "🤷🏻‍♀️", + "woman-shrugging::skin-tone-3": "🤷🏼‍♀️", + "woman-shrugging::skin-tone-4": "🤷🏽‍♀️", + "woman-shrugging::skin-tone-5": "🤷🏾‍♀️", + "woman-shrugging::skin-tone-6": "🤷🏿‍♀️", + "health_worker": "🧑‍⚕️", + "health_worker::skin-tone-2": "🧑🏻‍⚕️", + "health_worker::skin-tone-3": "🧑🏼‍⚕️", + "health_worker::skin-tone-4": "🧑🏽‍⚕️", + "health_worker::skin-tone-5": "🧑🏾‍⚕️", + "health_worker::skin-tone-6": "🧑🏿‍⚕️", + "male-doctor": "👨‍⚕️", + "male-doctor::skin-tone-2": "👨🏻‍⚕️", + "male-doctor::skin-tone-3": "👨🏼‍⚕️", + "male-doctor::skin-tone-4": "👨🏽‍⚕️", + "male-doctor::skin-tone-5": "👨🏾‍⚕️", + "male-doctor::skin-tone-6": "👨🏿‍⚕️", + "female-doctor": "👩‍⚕️", + "female-doctor::skin-tone-2": "👩🏻‍⚕️", + "female-doctor::skin-tone-3": "👩🏼‍⚕️", + "female-doctor::skin-tone-4": "👩🏽‍⚕️", + "female-doctor::skin-tone-5": "👩🏾‍⚕️", + "female-doctor::skin-tone-6": "👩🏿‍⚕️", + "student": "🧑‍🎓", + "student::skin-tone-2": "🧑🏻‍🎓", + "student::skin-tone-3": "🧑🏼‍🎓", + "student::skin-tone-4": "🧑🏽‍🎓", + "student::skin-tone-5": "🧑🏾‍🎓", + "student::skin-tone-6": "🧑🏿‍🎓", + "male-student": "👨‍🎓", + "male-student::skin-tone-2": "👨🏻‍🎓", + "male-student::skin-tone-3": "👨🏼‍🎓", + "male-student::skin-tone-4": "👨🏽‍🎓", + "male-student::skin-tone-5": "👨🏾‍🎓", + "male-student::skin-tone-6": "👨🏿‍🎓", + "female-student": "👩‍🎓", + "female-student::skin-tone-2": "👩🏻‍🎓", + "female-student::skin-tone-3": "👩🏼‍🎓", + "female-student::skin-tone-4": "👩🏽‍🎓", + "female-student::skin-tone-5": "👩🏾‍🎓", + "female-student::skin-tone-6": "👩🏿‍🎓", + "teacher": "🧑‍🏫", + "teacher::skin-tone-2": "🧑🏻‍🏫", + "teacher::skin-tone-3": "🧑🏼‍🏫", + "teacher::skin-tone-4": "🧑🏽‍🏫", + "teacher::skin-tone-5": "🧑🏾‍🏫", + "teacher::skin-tone-6": "🧑🏿‍🏫", + "male-teacher": "👨‍🏫", + "male-teacher::skin-tone-2": "👨🏻‍🏫", + "male-teacher::skin-tone-3": "👨🏼‍🏫", + "male-teacher::skin-tone-4": "👨🏽‍🏫", + "male-teacher::skin-tone-5": "👨🏾‍🏫", + "male-teacher::skin-tone-6": "👨🏿‍🏫", + "female-teacher": "👩‍🏫", + "female-teacher::skin-tone-2": "👩🏻‍🏫", + "female-teacher::skin-tone-3": "👩🏼‍🏫", + "female-teacher::skin-tone-4": "👩🏽‍🏫", + "female-teacher::skin-tone-5": "👩🏾‍🏫", + "female-teacher::skin-tone-6": "👩🏿‍🏫", + "judge": "🧑‍⚖️", + "judge::skin-tone-2": "🧑🏻‍⚖️", + "judge::skin-tone-3": "🧑🏼‍⚖️", + "judge::skin-tone-4": "🧑🏽‍⚖️", + "judge::skin-tone-5": "🧑🏾‍⚖️", + "judge::skin-tone-6": "🧑🏿‍⚖️", + "male-judge": "👨‍⚖️", + "male-judge::skin-tone-2": "👨🏻‍⚖️", + "male-judge::skin-tone-3": "👨🏼‍⚖️", + "male-judge::skin-tone-4": "👨🏽‍⚖️", + "male-judge::skin-tone-5": "👨🏾‍⚖️", + "male-judge::skin-tone-6": "👨🏿‍⚖️", + "female-judge": "👩‍⚖️", + "female-judge::skin-tone-2": "👩🏻‍⚖️", + "female-judge::skin-tone-3": "👩🏼‍⚖️", + "female-judge::skin-tone-4": "👩🏽‍⚖️", + "female-judge::skin-tone-5": "👩🏾‍⚖️", + "female-judge::skin-tone-6": "👩🏿‍⚖️", + "farmer": "🧑‍🌾", + "farmer::skin-tone-2": "🧑🏻‍🌾", + "farmer::skin-tone-3": "🧑🏼‍🌾", + "farmer::skin-tone-4": "🧑🏽‍🌾", + "farmer::skin-tone-5": "🧑🏾‍🌾", + "farmer::skin-tone-6": "🧑🏿‍🌾", + "male-farmer": "👨‍🌾", + "male-farmer::skin-tone-2": "👨🏻‍🌾", + "male-farmer::skin-tone-3": "👨🏼‍🌾", + "male-farmer::skin-tone-4": "👨🏽‍🌾", + "male-farmer::skin-tone-5": "👨🏾‍🌾", + "male-farmer::skin-tone-6": "👨🏿‍🌾", + "female-farmer": "👩‍🌾", + "female-farmer::skin-tone-2": "👩🏻‍🌾", + "female-farmer::skin-tone-3": "👩🏼‍🌾", + "female-farmer::skin-tone-4": "👩🏽‍🌾", + "female-farmer::skin-tone-5": "👩🏾‍🌾", + "female-farmer::skin-tone-6": "👩🏿‍🌾", + "cook": "🧑‍🍳", + "cook::skin-tone-2": "🧑🏻‍🍳", + "cook::skin-tone-3": "🧑🏼‍🍳", + "cook::skin-tone-4": "🧑🏽‍🍳", + "cook::skin-tone-5": "🧑🏾‍🍳", + "cook::skin-tone-6": "🧑🏿‍🍳", + "male-cook": "👨‍🍳", + "male-cook::skin-tone-2": "👨🏻‍🍳", + "male-cook::skin-tone-3": "👨🏼‍🍳", + "male-cook::skin-tone-4": "👨🏽‍🍳", + "male-cook::skin-tone-5": "👨🏾‍🍳", + "male-cook::skin-tone-6": "👨🏿‍🍳", + "female-cook": "👩‍🍳", + "female-cook::skin-tone-2": "👩🏻‍🍳", + "female-cook::skin-tone-3": "👩🏼‍🍳", + "female-cook::skin-tone-4": "👩🏽‍🍳", + "female-cook::skin-tone-5": "👩🏾‍🍳", + "female-cook::skin-tone-6": "👩🏿‍🍳", + "mechanic": "🧑‍🔧", + "mechanic::skin-tone-2": "🧑🏻‍🔧", + "mechanic::skin-tone-3": "🧑🏼‍🔧", + "mechanic::skin-tone-4": "🧑🏽‍🔧", + "mechanic::skin-tone-5": "🧑🏾‍🔧", + "mechanic::skin-tone-6": "🧑🏿‍🔧", + "male-mechanic": "👨‍🔧", + "male-mechanic::skin-tone-2": "👨🏻‍🔧", + "male-mechanic::skin-tone-3": "👨🏼‍🔧", + "male-mechanic::skin-tone-4": "👨🏽‍🔧", + "male-mechanic::skin-tone-5": "👨🏾‍🔧", + "male-mechanic::skin-tone-6": "👨🏿‍🔧", + "female-mechanic": "👩‍🔧", + "female-mechanic::skin-tone-2": "👩🏻‍🔧", + "female-mechanic::skin-tone-3": "👩🏼‍🔧", + "female-mechanic::skin-tone-4": "👩🏽‍🔧", + "female-mechanic::skin-tone-5": "👩🏾‍🔧", + "female-mechanic::skin-tone-6": "👩🏿‍🔧", + "factory_worker": "🧑‍🏭", + "factory_worker::skin-tone-2": "🧑🏻‍🏭", + "factory_worker::skin-tone-3": "🧑🏼‍🏭", + "factory_worker::skin-tone-4": "🧑🏽‍🏭", + "factory_worker::skin-tone-5": "🧑🏾‍🏭", + "factory_worker::skin-tone-6": "🧑🏿‍🏭", + "male-factory-worker": "👨‍🏭", + "male-factory-worker::skin-tone-2": "👨🏻‍🏭", + "male-factory-worker::skin-tone-3": "👨🏼‍🏭", + "male-factory-worker::skin-tone-4": "👨🏽‍🏭", + "male-factory-worker::skin-tone-5": "👨🏾‍🏭", + "male-factory-worker::skin-tone-6": "👨🏿‍🏭", + "female-factory-worker": "👩‍🏭", + "female-factory-worker::skin-tone-2": "👩🏻‍🏭", + "female-factory-worker::skin-tone-3": "👩🏼‍🏭", + "female-factory-worker::skin-tone-4": "👩🏽‍🏭", + "female-factory-worker::skin-tone-5": "👩🏾‍🏭", + "female-factory-worker::skin-tone-6": "👩🏿‍🏭", + "office_worker": "🧑‍💼", + "office_worker::skin-tone-2": "🧑🏻‍💼", + "office_worker::skin-tone-3": "🧑🏼‍💼", + "office_worker::skin-tone-4": "🧑🏽‍💼", + "office_worker::skin-tone-5": "🧑🏾‍💼", + "office_worker::skin-tone-6": "🧑🏿‍💼", + "male-office-worker": "👨‍💼", + "male-office-worker::skin-tone-2": "👨🏻‍💼", + "male-office-worker::skin-tone-3": "👨🏼‍💼", + "male-office-worker::skin-tone-4": "👨🏽‍💼", + "male-office-worker::skin-tone-5": "👨🏾‍💼", + "male-office-worker::skin-tone-6": "👨🏿‍💼", + "female-office-worker": "👩‍💼", + "female-office-worker::skin-tone-2": "👩🏻‍💼", + "female-office-worker::skin-tone-3": "👩🏼‍💼", + "female-office-worker::skin-tone-4": "👩🏽‍💼", + "female-office-worker::skin-tone-5": "👩🏾‍💼", + "female-office-worker::skin-tone-6": "👩🏿‍💼", + "scientist": "🧑‍🔬", + "scientist::skin-tone-2": "🧑🏻‍🔬", + "scientist::skin-tone-3": "🧑🏼‍🔬", + "scientist::skin-tone-4": "🧑🏽‍🔬", + "scientist::skin-tone-5": "🧑🏾‍🔬", + "scientist::skin-tone-6": "🧑🏿‍🔬", + "male-scientist": "👨‍🔬", + "male-scientist::skin-tone-2": "👨🏻‍🔬", + "male-scientist::skin-tone-3": "👨🏼‍🔬", + "male-scientist::skin-tone-4": "👨🏽‍🔬", + "male-scientist::skin-tone-5": "👨🏾‍🔬", + "male-scientist::skin-tone-6": "👨🏿‍🔬", + "female-scientist": "👩‍🔬", + "female-scientist::skin-tone-2": "👩🏻‍🔬", + "female-scientist::skin-tone-3": "👩🏼‍🔬", + "female-scientist::skin-tone-4": "👩🏽‍🔬", + "female-scientist::skin-tone-5": "👩🏾‍🔬", + "female-scientist::skin-tone-6": "👩🏿‍🔬", + "technologist": "🧑‍💻", + "technologist::skin-tone-2": "🧑🏻‍💻", + "technologist::skin-tone-3": "🧑🏼‍💻", + "technologist::skin-tone-4": "🧑🏽‍💻", + "technologist::skin-tone-5": "🧑🏾‍💻", + "technologist::skin-tone-6": "🧑🏿‍💻", + "male-technologist": "👨‍💻", + "male-technologist::skin-tone-2": "👨🏻‍💻", + "male-technologist::skin-tone-3": "👨🏼‍💻", + "male-technologist::skin-tone-4": "👨🏽‍💻", + "male-technologist::skin-tone-5": "👨🏾‍💻", + "male-technologist::skin-tone-6": "👨🏿‍💻", + "female-technologist": "👩‍💻", + "female-technologist::skin-tone-2": "👩🏻‍💻", + "female-technologist::skin-tone-3": "👩🏼‍💻", + "female-technologist::skin-tone-4": "👩🏽‍💻", + "female-technologist::skin-tone-5": "👩🏾‍💻", + "female-technologist::skin-tone-6": "👩🏿‍💻", + "singer": "🧑‍🎤", + "singer::skin-tone-2": "🧑🏻‍🎤", + "singer::skin-tone-3": "🧑🏼‍🎤", + "singer::skin-tone-4": "🧑🏽‍🎤", + "singer::skin-tone-5": "🧑🏾‍🎤", + "singer::skin-tone-6": "🧑🏿‍🎤", + "male-singer": "👨‍🎤", + "male-singer::skin-tone-2": "👨🏻‍🎤", + "male-singer::skin-tone-3": "👨🏼‍🎤", + "male-singer::skin-tone-4": "👨🏽‍🎤", + "male-singer::skin-tone-5": "👨🏾‍🎤", + "male-singer::skin-tone-6": "👨🏿‍🎤", + "female-singer": "👩‍🎤", + "female-singer::skin-tone-2": "👩🏻‍🎤", + "female-singer::skin-tone-3": "👩🏼‍🎤", + "female-singer::skin-tone-4": "👩🏽‍🎤", + "female-singer::skin-tone-5": "👩🏾‍🎤", + "female-singer::skin-tone-6": "👩🏿‍🎤", + "artist": "🧑‍🎨", + "artist::skin-tone-2": "🧑🏻‍🎨", + "artist::skin-tone-3": "🧑🏼‍🎨", + "artist::skin-tone-4": "🧑🏽‍🎨", + "artist::skin-tone-5": "🧑🏾‍🎨", + "artist::skin-tone-6": "🧑🏿‍🎨", + "male-artist": "👨‍🎨", + "male-artist::skin-tone-2": "👨🏻‍🎨", + "male-artist::skin-tone-3": "👨🏼‍🎨", + "male-artist::skin-tone-4": "👨🏽‍🎨", + "male-artist::skin-tone-5": "👨🏾‍🎨", + "male-artist::skin-tone-6": "👨🏿‍🎨", + "female-artist": "👩‍🎨", + "female-artist::skin-tone-2": "👩🏻‍🎨", + "female-artist::skin-tone-3": "👩🏼‍🎨", + "female-artist::skin-tone-4": "👩🏽‍🎨", + "female-artist::skin-tone-5": "👩🏾‍🎨", + "female-artist::skin-tone-6": "👩🏿‍🎨", + "pilot": "🧑‍✈️", + "pilot::skin-tone-2": "🧑🏻‍✈️", + "pilot::skin-tone-3": "🧑🏼‍✈️", + "pilot::skin-tone-4": "🧑🏽‍✈️", + "pilot::skin-tone-5": "🧑🏾‍✈️", + "pilot::skin-tone-6": "🧑🏿‍✈️", + "male-pilot": "👨‍✈️", + "male-pilot::skin-tone-2": "👨🏻‍✈️", + "male-pilot::skin-tone-3": "👨🏼‍✈️", + "male-pilot::skin-tone-4": "👨🏽‍✈️", + "male-pilot::skin-tone-5": "👨🏾‍✈️", + "male-pilot::skin-tone-6": "👨🏿‍✈️", + "female-pilot": "👩‍✈️", + "female-pilot::skin-tone-2": "👩🏻‍✈️", + "female-pilot::skin-tone-3": "👩🏼‍✈️", + "female-pilot::skin-tone-4": "👩🏽‍✈️", + "female-pilot::skin-tone-5": "👩🏾‍✈️", + "female-pilot::skin-tone-6": "👩🏿‍✈️", + "astronaut": "🧑‍🚀", + "astronaut::skin-tone-2": "🧑🏻‍🚀", + "astronaut::skin-tone-3": "🧑🏼‍🚀", + "astronaut::skin-tone-4": "🧑🏽‍🚀", + "astronaut::skin-tone-5": "🧑🏾‍🚀", + "astronaut::skin-tone-6": "🧑🏿‍🚀", + "male-astronaut": "👨‍🚀", + "male-astronaut::skin-tone-2": "👨🏻‍🚀", + "male-astronaut::skin-tone-3": "👨🏼‍🚀", + "male-astronaut::skin-tone-4": "👨🏽‍🚀", + "male-astronaut::skin-tone-5": "👨🏾‍🚀", + "male-astronaut::skin-tone-6": "👨🏿‍🚀", + "female-astronaut": "👩‍🚀", + "female-astronaut::skin-tone-2": "👩🏻‍🚀", + "female-astronaut::skin-tone-3": "👩🏼‍🚀", + "female-astronaut::skin-tone-4": "👩🏽‍🚀", + "female-astronaut::skin-tone-5": "👩🏾‍🚀", + "female-astronaut::skin-tone-6": "👩🏿‍🚀", + "firefighter": "🧑‍🚒", + "firefighter::skin-tone-2": "🧑🏻‍🚒", + "firefighter::skin-tone-3": "🧑🏼‍🚒", + "firefighter::skin-tone-4": "🧑🏽‍🚒", + "firefighter::skin-tone-5": "🧑🏾‍🚒", + "firefighter::skin-tone-6": "🧑🏿‍🚒", + "male-firefighter": "👨‍🚒", + "male-firefighter::skin-tone-2": "👨🏻‍🚒", + "male-firefighter::skin-tone-3": "👨🏼‍🚒", + "male-firefighter::skin-tone-4": "👨🏽‍🚒", + "male-firefighter::skin-tone-5": "👨🏾‍🚒", + "male-firefighter::skin-tone-6": "👨🏿‍🚒", + "female-firefighter": "👩‍🚒", + "female-firefighter::skin-tone-2": "👩🏻‍🚒", + "female-firefighter::skin-tone-3": "👩🏼‍🚒", + "female-firefighter::skin-tone-4": "👩🏽‍🚒", + "female-firefighter::skin-tone-5": "👩🏾‍🚒", + "female-firefighter::skin-tone-6": "👩🏿‍🚒", + "cop": "👮", + "cop::skin-tone-2": "👮🏻", + "cop::skin-tone-3": "👮🏼", + "cop::skin-tone-4": "👮🏽", + "cop::skin-tone-5": "👮🏾", + "cop::skin-tone-6": "👮🏿", + "male-police-officer": "👮‍♂️", + "male-police-officer::skin-tone-2": "👮🏻‍♂️", + "male-police-officer::skin-tone-3": "👮🏼‍♂️", + "male-police-officer::skin-tone-4": "👮🏽‍♂️", + "male-police-officer::skin-tone-5": "👮🏾‍♂️", + "male-police-officer::skin-tone-6": "👮🏿‍♂️", + "female-police-officer": "👮‍♀️", + "female-police-officer::skin-tone-2": "👮🏻‍♀️", + "female-police-officer::skin-tone-3": "👮🏼‍♀️", + "female-police-officer::skin-tone-4": "👮🏽‍♀️", + "female-police-officer::skin-tone-5": "👮🏾‍♀️", + "female-police-officer::skin-tone-6": "👮🏿‍♀️", + "sleuth_or_spy": "🕵️", + "sleuth_or_spy::skin-tone-2": "🕵🏻", + "sleuth_or_spy::skin-tone-3": "🕵🏼", + "sleuth_or_spy::skin-tone-4": "🕵🏽", + "sleuth_or_spy::skin-tone-5": "🕵🏾", + "sleuth_or_spy::skin-tone-6": "🕵🏿", + "male-detective": "🕵️‍♂️", + "male-detective::skin-tone-2": "🕵🏻‍♂️", + "male-detective::skin-tone-3": "🕵🏼‍♂️", + "male-detective::skin-tone-4": "🕵🏽‍♂️", + "male-detective::skin-tone-5": "🕵🏾‍♂️", + "male-detective::skin-tone-6": "🕵🏿‍♂️", + "female-detective": "🕵️‍♀️", + "female-detective::skin-tone-2": "🕵🏻‍♀️", + "female-detective::skin-tone-3": "🕵🏼‍♀️", + "female-detective::skin-tone-4": "🕵🏽‍♀️", + "female-detective::skin-tone-5": "🕵🏾‍♀️", + "female-detective::skin-tone-6": "🕵🏿‍♀️", + "guardsman": "💂", + "guardsman::skin-tone-2": "💂🏻", + "guardsman::skin-tone-3": "💂🏼", + "guardsman::skin-tone-4": "💂🏽", + "guardsman::skin-tone-5": "💂🏾", + "guardsman::skin-tone-6": "💂🏿", + "male-guard": "💂‍♂️", + "male-guard::skin-tone-2": "💂🏻‍♂️", + "male-guard::skin-tone-3": "💂🏼‍♂️", + "male-guard::skin-tone-4": "💂🏽‍♂️", + "male-guard::skin-tone-5": "💂🏾‍♂️", + "male-guard::skin-tone-6": "💂🏿‍♂️", + "female-guard": "💂‍♀️", + "female-guard::skin-tone-2": "💂🏻‍♀️", + "female-guard::skin-tone-3": "💂🏼‍♀️", + "female-guard::skin-tone-4": "💂🏽‍♀️", + "female-guard::skin-tone-5": "💂🏾‍♀️", + "female-guard::skin-tone-6": "💂🏿‍♀️", + "ninja": "🥷", + "ninja::skin-tone-2": "🥷🏻", + "ninja::skin-tone-3": "🥷🏼", + "ninja::skin-tone-4": "🥷🏽", + "ninja::skin-tone-5": "🥷🏾", + "ninja::skin-tone-6": "🥷🏿", + "construction_worker": "👷", + "construction_worker::skin-tone-2": "👷🏻", + "construction_worker::skin-tone-3": "👷🏼", + "construction_worker::skin-tone-4": "👷🏽", + "construction_worker::skin-tone-5": "👷🏾", + "construction_worker::skin-tone-6": "👷🏿", + "male-construction-worker": "👷‍♂️", + "male-construction-worker::skin-tone-2": "👷🏻‍♂️", + "male-construction-worker::skin-tone-3": "👷🏼‍♂️", + "male-construction-worker::skin-tone-4": "👷🏽‍♂️", + "male-construction-worker::skin-tone-5": "👷🏾‍♂️", + "male-construction-worker::skin-tone-6": "👷🏿‍♂️", + "female-construction-worker": "👷‍♀️", + "female-construction-worker::skin-tone-2": "👷🏻‍♀️", + "female-construction-worker::skin-tone-3": "👷🏼‍♀️", + "female-construction-worker::skin-tone-4": "👷🏽‍♀️", + "female-construction-worker::skin-tone-5": "👷🏾‍♀️", + "female-construction-worker::skin-tone-6": "👷🏿‍♀️", + "person_with_crown": "🫅", + "person_with_crown::skin-tone-2": "🫅🏻", + "person_with_crown::skin-tone-3": "🫅🏼", + "person_with_crown::skin-tone-4": "🫅🏽", + "person_with_crown::skin-tone-5": "🫅🏾", + "person_with_crown::skin-tone-6": "🫅🏿", + "prince": "🤴", + "prince::skin-tone-2": "🤴🏻", + "prince::skin-tone-3": "🤴🏼", + "prince::skin-tone-4": "🤴🏽", + "prince::skin-tone-5": "🤴🏾", + "prince::skin-tone-6": "🤴🏿", + "princess": "👸", + "princess::skin-tone-2": "👸🏻", + "princess::skin-tone-3": "👸🏼", + "princess::skin-tone-4": "👸🏽", + "princess::skin-tone-5": "👸🏾", + "princess::skin-tone-6": "👸🏿", + "man_with_turban": "👳", + "man_with_turban::skin-tone-2": "👳🏻", + "man_with_turban::skin-tone-3": "👳🏼", + "man_with_turban::skin-tone-4": "👳🏽", + "man_with_turban::skin-tone-5": "👳🏾", + "man_with_turban::skin-tone-6": "👳🏿", + "man-wearing-turban": "👳‍♂️", + "man-wearing-turban::skin-tone-2": "👳🏻‍♂️", + "man-wearing-turban::skin-tone-3": "👳🏼‍♂️", + "man-wearing-turban::skin-tone-4": "👳🏽‍♂️", + "man-wearing-turban::skin-tone-5": "👳🏾‍♂️", + "man-wearing-turban::skin-tone-6": "👳🏿‍♂️", + "woman-wearing-turban": "👳‍♀️", + "woman-wearing-turban::skin-tone-2": "👳🏻‍♀️", + "woman-wearing-turban::skin-tone-3": "👳🏼‍♀️", + "woman-wearing-turban::skin-tone-4": "👳🏽‍♀️", + "woman-wearing-turban::skin-tone-5": "👳🏾‍♀️", + "woman-wearing-turban::skin-tone-6": "👳🏿‍♀️", + "man_with_gua_pi_mao": "👲", + "man_with_gua_pi_mao::skin-tone-2": "👲🏻", + "man_with_gua_pi_mao::skin-tone-3": "👲🏼", + "man_with_gua_pi_mao::skin-tone-4": "👲🏽", + "man_with_gua_pi_mao::skin-tone-5": "👲🏾", + "man_with_gua_pi_mao::skin-tone-6": "👲🏿", + "person_with_headscarf": "🧕", + "person_with_headscarf::skin-tone-2": "🧕🏻", + "person_with_headscarf::skin-tone-3": "🧕🏼", + "person_with_headscarf::skin-tone-4": "🧕🏽", + "person_with_headscarf::skin-tone-5": "🧕🏾", + "person_with_headscarf::skin-tone-6": "🧕🏿", + "person_in_tuxedo": "🤵", + "person_in_tuxedo::skin-tone-2": "🤵🏻", + "person_in_tuxedo::skin-tone-3": "🤵🏼", + "person_in_tuxedo::skin-tone-4": "🤵🏽", + "person_in_tuxedo::skin-tone-5": "🤵🏾", + "person_in_tuxedo::skin-tone-6": "🤵🏿", + "man_in_tuxedo": "🤵‍♂️", + "man_in_tuxedo::skin-tone-2": "🤵🏻‍♂️", + "man_in_tuxedo::skin-tone-3": "🤵🏼‍♂️", + "man_in_tuxedo::skin-tone-4": "🤵🏽‍♂️", + "man_in_tuxedo::skin-tone-5": "🤵🏾‍♂️", + "man_in_tuxedo::skin-tone-6": "🤵🏿‍♂️", + "woman_in_tuxedo": "🤵‍♀️", + "woman_in_tuxedo::skin-tone-2": "🤵🏻‍♀️", + "woman_in_tuxedo::skin-tone-3": "🤵🏼‍♀️", + "woman_in_tuxedo::skin-tone-4": "🤵🏽‍♀️", + "woman_in_tuxedo::skin-tone-5": "🤵🏾‍♀️", + "woman_in_tuxedo::skin-tone-6": "🤵🏿‍♀️", + "bride_with_veil": "👰", + "bride_with_veil::skin-tone-2": "👰🏻", + "bride_with_veil::skin-tone-3": "👰🏼", + "bride_with_veil::skin-tone-4": "👰🏽", + "bride_with_veil::skin-tone-5": "👰🏾", + "bride_with_veil::skin-tone-6": "👰🏿", + "man_with_veil": "👰‍♂️", + "man_with_veil::skin-tone-2": "👰🏻‍♂️", + "man_with_veil::skin-tone-3": "👰🏼‍♂️", + "man_with_veil::skin-tone-4": "👰🏽‍♂️", + "man_with_veil::skin-tone-5": "👰🏾‍♂️", + "man_with_veil::skin-tone-6": "👰🏿‍♂️", + "woman_with_veil": "👰‍♀️", + "woman_with_veil::skin-tone-2": "👰🏻‍♀️", + "woman_with_veil::skin-tone-3": "👰🏼‍♀️", + "woman_with_veil::skin-tone-4": "👰🏽‍♀️", + "woman_with_veil::skin-tone-5": "👰🏾‍♀️", + "woman_with_veil::skin-tone-6": "👰🏿‍♀️", + "pregnant_woman": "🤰", + "pregnant_woman::skin-tone-2": "🤰🏻", + "pregnant_woman::skin-tone-3": "🤰🏼", + "pregnant_woman::skin-tone-4": "🤰🏽", + "pregnant_woman::skin-tone-5": "🤰🏾", + "pregnant_woman::skin-tone-6": "🤰🏿", + "pregnant_man": "🫃", + "pregnant_man::skin-tone-2": "🫃🏻", + "pregnant_man::skin-tone-3": "🫃🏼", + "pregnant_man::skin-tone-4": "🫃🏽", + "pregnant_man::skin-tone-5": "🫃🏾", + "pregnant_man::skin-tone-6": "🫃🏿", + "pregnant_person": "🫄", + "pregnant_person::skin-tone-2": "🫄🏻", + "pregnant_person::skin-tone-3": "🫄🏼", + "pregnant_person::skin-tone-4": "🫄🏽", + "pregnant_person::skin-tone-5": "🫄🏾", + "pregnant_person::skin-tone-6": "🫄🏿", + "breast-feeding": "🤱", + "breast-feeding::skin-tone-2": "🤱🏻", + "breast-feeding::skin-tone-3": "🤱🏼", + "breast-feeding::skin-tone-4": "🤱🏽", + "breast-feeding::skin-tone-5": "🤱🏾", + "breast-feeding::skin-tone-6": "🤱🏿", + "woman_feeding_baby": "👩‍🍼", + "woman_feeding_baby::skin-tone-2": "👩🏻‍🍼", + "woman_feeding_baby::skin-tone-3": "👩🏼‍🍼", + "woman_feeding_baby::skin-tone-4": "👩🏽‍🍼", + "woman_feeding_baby::skin-tone-5": "👩🏾‍🍼", + "woman_feeding_baby::skin-tone-6": "👩🏿‍🍼", + "man_feeding_baby": "👨‍🍼", + "man_feeding_baby::skin-tone-2": "👨🏻‍🍼", + "man_feeding_baby::skin-tone-3": "👨🏼‍🍼", + "man_feeding_baby::skin-tone-4": "👨🏽‍🍼", + "man_feeding_baby::skin-tone-5": "👨🏾‍🍼", + "man_feeding_baby::skin-tone-6": "👨🏿‍🍼", + "person_feeding_baby": "🧑‍🍼", + "person_feeding_baby::skin-tone-2": "🧑🏻‍🍼", + "person_feeding_baby::skin-tone-3": "🧑🏼‍🍼", + "person_feeding_baby::skin-tone-4": "🧑🏽‍🍼", + "person_feeding_baby::skin-tone-5": "🧑🏾‍🍼", + "person_feeding_baby::skin-tone-6": "🧑🏿‍🍼", + "angel": "👼", + "angel::skin-tone-2": "👼🏻", + "angel::skin-tone-3": "👼🏼", + "angel::skin-tone-4": "👼🏽", + "angel::skin-tone-5": "👼🏾", + "angel::skin-tone-6": "👼🏿", + "dagsanta": "🎅", + "santa": "🎅", + "santa::skin-tone-2": "🎅🏻", + "santa::skin-tone-3": "🎅🏼", + "santa::skin-tone-4": "🎅🏽", + "santa::skin-tone-5": "🎅🏾", + "santa::skin-tone-6": "🎅🏿", + "mrs_claus": "🤶", + "mother_christmas": "🤶", + "mrs_claus::skin-tone-2": "🤶🏻", + "mother_christmas::skin-tone-2": "🤶🏻", + "mrs_claus::skin-tone-3": "🤶🏼", + "mother_christmas::skin-tone-3": "🤶🏼", + "mrs_claus::skin-tone-4": "🤶🏽", + "mother_christmas::skin-tone-4": "🤶🏽", + "mrs_claus::skin-tone-5": "🤶🏾", + "mother_christmas::skin-tone-5": "🤶🏾", + "mrs_claus::skin-tone-6": "🤶🏿", + "mother_christmas::skin-tone-6": "🤶🏿", + "mx_claus": "🧑‍🎄", + "mx_claus::skin-tone-2": "🧑🏻‍🎄", + "mx_claus::skin-tone-3": "🧑🏼‍🎄", + "mx_claus::skin-tone-4": "🧑🏽‍🎄", + "mx_claus::skin-tone-5": "🧑🏾‍🎄", + "mx_claus::skin-tone-6": "🧑🏿‍🎄", + "superhero": "🦸", + "superhero::skin-tone-2": "🦸🏻", + "superhero::skin-tone-3": "🦸🏼", + "superhero::skin-tone-4": "🦸🏽", + "superhero::skin-tone-5": "🦸🏾", + "superhero::skin-tone-6": "🦸🏿", + "male_superhero": "🦸‍♂️", + "male_superhero::skin-tone-2": "🦸🏻‍♂️", + "male_superhero::skin-tone-3": "🦸🏼‍♂️", + "male_superhero::skin-tone-4": "🦸🏽‍♂️", + "male_superhero::skin-tone-5": "🦸🏾‍♂️", + "male_superhero::skin-tone-6": "🦸🏿‍♂️", + "female_superhero": "🦸‍♀️", + "female_superhero::skin-tone-2": "🦸🏻‍♀️", + "female_superhero::skin-tone-3": "🦸🏼‍♀️", + "female_superhero::skin-tone-4": "🦸🏽‍♀️", + "female_superhero::skin-tone-5": "🦸🏾‍♀️", + "female_superhero::skin-tone-6": "🦸🏿‍♀️", + "supervillain": "🦹", + "supervillain::skin-tone-2": "🦹🏻", + "supervillain::skin-tone-3": "🦹🏼", + "supervillain::skin-tone-4": "🦹🏽", + "supervillain::skin-tone-5": "🦹🏾", + "supervillain::skin-tone-6": "🦹🏿", + "male_supervillain": "🦹‍♂️", + "male_supervillain::skin-tone-2": "🦹🏻‍♂️", + "male_supervillain::skin-tone-3": "🦹🏼‍♂️", + "male_supervillain::skin-tone-4": "🦹🏽‍♂️", + "male_supervillain::skin-tone-5": "🦹🏾‍♂️", + "male_supervillain::skin-tone-6": "🦹🏿‍♂️", + "female_supervillain": "🦹‍♀️", + "female_supervillain::skin-tone-2": "🦹🏻‍♀️", + "female_supervillain::skin-tone-3": "🦹🏼‍♀️", + "female_supervillain::skin-tone-4": "🦹🏽‍♀️", + "female_supervillain::skin-tone-5": "🦹🏾‍♀️", + "female_supervillain::skin-tone-6": "🦹🏿‍♀️", + "mage": "🧙", + "mage_ai": "🧙", + "mage::skin-tone-2": "🧙🏻", + "mage::skin-tone-3": "🧙🏼", + "mage::skin-tone-4": "🧙🏽", + "mage::skin-tone-5": "🧙🏾", + "mage::skin-tone-6": "🧙🏿", + "male_mage": "🧙‍♂️", + "male_mage::skin-tone-2": "🧙🏻‍♂️", + "male_mage::skin-tone-3": "🧙🏼‍♂️", + "male_mage::skin-tone-4": "🧙🏽‍♂️", + "male_mage::skin-tone-5": "🧙🏾‍♂️", + "male_mage::skin-tone-6": "🧙🏿‍♂️", + "female_mage": "🧙‍♀️", + "female_mage::skin-tone-2": "🧙🏻‍♀️", + "female_mage::skin-tone-3": "🧙🏼‍♀️", + "female_mage::skin-tone-4": "🧙🏽‍♀️", + "female_mage::skin-tone-5": "🧙🏾‍♀️", + "female_mage::skin-tone-6": "🧙🏿‍♀️", + "airflow": "A", + "fairy": "🧚", + "fairy::skin-tone-2": "🧚🏻", + "fairy::skin-tone-3": "🧚🏼", + "fairy::skin-tone-4": "🧚🏽", + "fairy::skin-tone-5": "🧚🏾", + "fairy::skin-tone-6": "🧚🏿", + "male_fairy": "🧚‍♂️", + "male_fairy::skin-tone-2": "🧚🏻‍♂️", + "male_fairy::skin-tone-3": "🧚🏼‍♂️", + "male_fairy::skin-tone-4": "🧚🏽‍♂️", + "male_fairy::skin-tone-5": "🧚🏾‍♂️", + "male_fairy::skin-tone-6": "🧚🏿‍♂️", + "female_fairy": "🧚‍♀️", + "female_fairy::skin-tone-2": "🧚🏻‍♀️", + "female_fairy::skin-tone-3": "🧚🏼‍♀️", + "female_fairy::skin-tone-4": "🧚🏽‍♀️", + "female_fairy::skin-tone-5": "🧚🏾‍♀️", + "female_fairy::skin-tone-6": "🧚🏿‍♀️", + "vampire": "🧛", + "vampire::skin-tone-2": "🧛🏻", + "vampire::skin-tone-3": "🧛🏼", + "vampire::skin-tone-4": "🧛🏽", + "vampire::skin-tone-5": "🧛🏾", + "vampire::skin-tone-6": "🧛🏿", + "male_vampire": "🧛‍♂️", + "male_vampire::skin-tone-2": "🧛🏻‍♂️", + "male_vampire::skin-tone-3": "🧛🏼‍♂️", + "male_vampire::skin-tone-4": "🧛🏽‍♂️", + "male_vampire::skin-tone-5": "🧛🏾‍♂️", + "male_vampire::skin-tone-6": "🧛🏿‍♂️", + "female_vampire": "🧛‍♀️", + "female_vampire::skin-tone-2": "🧛🏻‍♀️", + "female_vampire::skin-tone-3": "🧛🏼‍♀️", + "female_vampire::skin-tone-4": "🧛🏽‍♀️", + "female_vampire::skin-tone-5": "🧛🏾‍♀️", + "female_vampire::skin-tone-6": "🧛🏿‍♀️", + "merperson": "🧜", + "merperson::skin-tone-2": "🧜🏻", + "merperson::skin-tone-3": "🧜🏼", + "merperson::skin-tone-4": "🧜🏽", + "merperson::skin-tone-5": "🧜🏾", + "merperson::skin-tone-6": "🧜🏿", + "merman": "🧜‍♂️", + "merman::skin-tone-2": "🧜🏻‍♂️", + "merman::skin-tone-3": "🧜🏼‍♂️", + "merman::skin-tone-4": "🧜🏽‍♂️", + "merman::skin-tone-5": "🧜🏾‍♂️", + "merman::skin-tone-6": "🧜🏿‍♂️", + "mermaid": "🧜‍♀️", + "mermaid::skin-tone-2": "🧜🏻‍♀️", + "mermaid::skin-tone-3": "🧜🏼‍♀️", + "mermaid::skin-tone-4": "🧜🏽‍♀️", + "mermaid::skin-tone-5": "🧜🏾‍♀️", + "mermaid::skin-tone-6": "🧜🏿‍♀️", + "elf": "🧝", + "elf::skin-tone-2": "🧝🏻", + "elf::skin-tone-3": "🧝🏼", + "elf::skin-tone-4": "🧝🏽", + "elf::skin-tone-5": "🧝🏾", + "elf::skin-tone-6": "🧝🏿", + "male_elf": "🧝‍♂️", + "male_elf::skin-tone-2": "🧝🏻‍♂️", + "male_elf::skin-tone-3": "🧝🏼‍♂️", + "male_elf::skin-tone-4": "🧝🏽‍♂️", + "male_elf::skin-tone-5": "🧝🏾‍♂️", + "male_elf::skin-tone-6": "🧝🏿‍♂️", + "female_elf": "🧝‍♀️", + "female_elf::skin-tone-2": "🧝🏻‍♀️", + "female_elf::skin-tone-3": "🧝🏼‍♀️", + "female_elf::skin-tone-4": "🧝🏽‍♀️", + "female_elf::skin-tone-5": "🧝🏾‍♀️", + "female_elf::skin-tone-6": "🧝🏿‍♀️", + "genie": "🧞", + "male_genie": "🧞‍♂️", + "female_genie": "🧞‍♀️", + "zombie": "🧟", + "male_zombie": "🧟‍♂️", + "female_zombie": "🧟‍♀️", + "troll": "🧌", + "massage": "💆", + "massage::skin-tone-2": "💆🏻", + "massage::skin-tone-3": "💆🏼", + "massage::skin-tone-4": "💆🏽", + "massage::skin-tone-5": "💆🏾", + "massage::skin-tone-6": "💆🏿", + "man-getting-massage": "💆‍♂️", + "man-getting-massage::skin-tone-2": "💆🏻‍♂️", + "man-getting-massage::skin-tone-3": "💆🏼‍♂️", + "man-getting-massage::skin-tone-4": "💆🏽‍♂️", + "man-getting-massage::skin-tone-5": "💆🏾‍♂️", + "man-getting-massage::skin-tone-6": "💆🏿‍♂️", + "woman-getting-massage": "💆‍♀️", + "woman-getting-massage::skin-tone-2": "💆🏻‍♀️", + "woman-getting-massage::skin-tone-3": "💆🏼‍♀️", + "woman-getting-massage::skin-tone-4": "💆🏽‍♀️", + "woman-getting-massage::skin-tone-5": "💆🏾‍♀️", + "woman-getting-massage::skin-tone-6": "💆🏿‍♀️", + "haircut": "💇", + "haircut::skin-tone-2": "💇🏻", + "haircut::skin-tone-3": "💇🏼", + "haircut::skin-tone-4": "💇🏽", + "haircut::skin-tone-5": "💇🏾", + "haircut::skin-tone-6": "💇🏿", + "man-getting-haircut": "💇‍♂️", + "man-getting-haircut::skin-tone-2": "💇🏻‍♂️", + "man-getting-haircut::skin-tone-3": "💇🏼‍♂️", + "man-getting-haircut::skin-tone-4": "💇🏽‍♂️", + "man-getting-haircut::skin-tone-5": "💇🏾‍♂️", + "man-getting-haircut::skin-tone-6": "💇🏿‍♂️", + "woman-getting-haircut": "💇‍♀️", + "woman-getting-haircut::skin-tone-2": "💇🏻‍♀️", + "woman-getting-haircut::skin-tone-3": "💇🏼‍♀️", + "woman-getting-haircut::skin-tone-4": "💇🏽‍♀️", + "woman-getting-haircut::skin-tone-5": "💇🏾‍♀️", + "woman-getting-haircut::skin-tone-6": "💇🏿‍♀️", + "lfg": "🚶", + "walking": "🚶", + "walking::skin-tone-2": "🚶🏻", + "walking::skin-tone-3": "🚶🏼", + "walking::skin-tone-4": "🚶🏽", + "walking::skin-tone-5": "🚶🏾", + "walking::skin-tone-6": "🚶🏿", + "man-walking": "🚶‍♂️", + "man-walking::skin-tone-2": "🚶🏻‍♂️", + "man-walking::skin-tone-3": "🚶🏼‍♂️", + "man-walking::skin-tone-4": "🚶🏽‍♂️", + "man-walking::skin-tone-5": "🚶🏾‍♂️", + "man-walking::skin-tone-6": "🚶🏿‍♂️", + "woman-walking": "🚶‍♀️", + "woman-walking::skin-tone-2": "🚶🏻‍♀️", + "woman-walking::skin-tone-3": "🚶🏼‍♀️", + "woman-walking::skin-tone-4": "🚶🏽‍♀️", + "woman-walking::skin-tone-5": "🚶🏾‍♀️", + "woman-walking::skin-tone-6": "🚶🏿‍♀️", + "standing_person": "🧍", + "standing_person::skin-tone-2": "🧍🏻", + "standing_person::skin-tone-3": "🧍🏼", + "standing_person::skin-tone-4": "🧍🏽", + "standing_person::skin-tone-5": "🧍🏾", + "standing_person::skin-tone-6": "🧍🏿", + "man_standing": "🧍‍♂️", + "man_standing::skin-tone-2": "🧍🏻‍♂️", + "man_standing::skin-tone-3": "🧍🏼‍♂️", + "man_standing::skin-tone-4": "🧍🏽‍♂️", + "man_standing::skin-tone-5": "🧍🏾‍♂️", + "man_standing::skin-tone-6": "🧍🏿‍♂️", + "woman_standing": "🧍‍♀️", + "woman_standing::skin-tone-2": "🧍🏻‍♀️", + "woman_standing::skin-tone-3": "🧍🏼‍♀️", + "woman_standing::skin-tone-4": "🧍🏽‍♀️", + "woman_standing::skin-tone-5": "🧍🏾‍♀️", + "woman_standing::skin-tone-6": "🧍🏿‍♀️", + "kneeling_person": "🧎", + "kneeling_person::skin-tone-2": "🧎🏻", + "kneeling_person::skin-tone-3": "🧎🏼", + "kneeling_person::skin-tone-4": "🧎🏽", + "kneeling_person::skin-tone-5": "🧎🏾", + "kneeling_person::skin-tone-6": "🧎🏿", + "man_kneeling": "🧎‍♂️", + "man_kneeling::skin-tone-2": "🧎🏻‍♂️", + "man_kneeling::skin-tone-3": "🧎🏼‍♂️", + "man_kneeling::skin-tone-4": "🧎🏽‍♂️", + "man_kneeling::skin-tone-5": "🧎🏾‍♂️", + "man_kneeling::skin-tone-6": "🧎🏿‍♂️", + "woman_kneeling": "🧎‍♀️", + "woman_kneeling::skin-tone-2": "🧎🏻‍♀️", + "woman_kneeling::skin-tone-3": "🧎🏼‍♀️", + "woman_kneeling::skin-tone-4": "🧎🏽‍♀️", + "woman_kneeling::skin-tone-5": "🧎🏾‍♀️", + "woman_kneeling::skin-tone-6": "🧎🏿‍♀️", + "person_with_probing_cane": "🧑‍🦯", + "person_with_probing_cane::skin-tone-2": "🧑🏻‍🦯", + "person_with_probing_cane::skin-tone-3": "🧑🏼‍🦯", + "person_with_probing_cane::skin-tone-4": "🧑🏽‍🦯", + "person_with_probing_cane::skin-tone-5": "🧑🏾‍🦯", + "person_with_probing_cane::skin-tone-6": "🧑🏿‍🦯", + "man_with_probing_cane": "👨‍🦯", + "man_with_probing_cane::skin-tone-2": "👨🏻‍🦯", + "man_with_probing_cane::skin-tone-3": "👨🏼‍🦯", + "man_with_probing_cane::skin-tone-4": "👨🏽‍🦯", + "man_with_probing_cane::skin-tone-5": "👨🏾‍🦯", + "man_with_probing_cane::skin-tone-6": "👨🏿‍🦯", + "woman_with_probing_cane": "👩‍🦯", + "woman_with_probing_cane::skin-tone-2": "👩🏻‍🦯", + "woman_with_probing_cane::skin-tone-3": "👩🏼‍🦯", + "woman_with_probing_cane::skin-tone-4": "👩🏽‍🦯", + "woman_with_probing_cane::skin-tone-5": "👩🏾‍🦯", + "woman_with_probing_cane::skin-tone-6": "👩🏿‍🦯", + "person_in_motorized_wheelchair": "🧑‍🦼", + "person_in_motorized_wheelchair::skin-tone-2": "🧑🏻‍🦼", + "person_in_motorized_wheelchair::skin-tone-3": "🧑🏼‍🦼", + "person_in_motorized_wheelchair::skin-tone-4": "🧑🏽‍🦼", + "person_in_motorized_wheelchair::skin-tone-5": "🧑🏾‍🦼", + "person_in_motorized_wheelchair::skin-tone-6": "🧑🏿‍🦼", + "man_in_motorized_wheelchair": "👨‍🦼", + "man_in_motorized_wheelchair::skin-tone-2": "👨🏻‍🦼", + "man_in_motorized_wheelchair::skin-tone-3": "👨🏼‍🦼", + "man_in_motorized_wheelchair::skin-tone-4": "👨🏽‍🦼", + "man_in_motorized_wheelchair::skin-tone-5": "👨🏾‍🦼", + "man_in_motorized_wheelchair::skin-tone-6": "👨🏿‍🦼", + "woman_in_motorized_wheelchair": "👩‍🦼", + "woman_in_motorized_wheelchair::skin-tone-2": "👩🏻‍🦼", + "woman_in_motorized_wheelchair::skin-tone-3": "👩🏼‍🦼", + "woman_in_motorized_wheelchair::skin-tone-4": "👩🏽‍🦼", + "woman_in_motorized_wheelchair::skin-tone-5": "👩🏾‍🦼", + "woman_in_motorized_wheelchair::skin-tone-6": "👩🏿‍🦼", + "person_in_manual_wheelchair": "🧑‍🦽", + "person_in_manual_wheelchair::skin-tone-2": "🧑🏻‍🦽", + "person_in_manual_wheelchair::skin-tone-3": "🧑🏼‍🦽", + "person_in_manual_wheelchair::skin-tone-4": "🧑🏽‍🦽", + "person_in_manual_wheelchair::skin-tone-5": "🧑🏾‍🦽", + "person_in_manual_wheelchair::skin-tone-6": "🧑🏿‍🦽", + "man_in_manual_wheelchair": "👨‍🦽", + "man_in_manual_wheelchair::skin-tone-2": "👨🏻‍🦽", + "man_in_manual_wheelchair::skin-tone-3": "👨🏼‍🦽", + "man_in_manual_wheelchair::skin-tone-4": "👨🏽‍🦽", + "man_in_manual_wheelchair::skin-tone-5": "👨🏾‍🦽", + "man_in_manual_wheelchair::skin-tone-6": "👨🏿‍🦽", + "woman_in_manual_wheelchair": "👩‍🦽", + "woman_in_manual_wheelchair::skin-tone-2": "👩🏻‍🦽", + "woman_in_manual_wheelchair::skin-tone-3": "👩🏼‍🦽", + "woman_in_manual_wheelchair::skin-tone-4": "👩🏽‍🦽", + "woman_in_manual_wheelchair::skin-tone-5": "👩🏾‍🦽", + "woman_in_manual_wheelchair::skin-tone-6": "👩🏿‍🦽", + "runner": "🏃", + "running": "🏃", + "runner::skin-tone-2": "🏃🏻", + "running::skin-tone-2": "🏃🏻", + "runner::skin-tone-3": "🏃🏼", + "running::skin-tone-3": "🏃🏼", + "runner::skin-tone-4": "🏃🏽", + "running::skin-tone-4": "🏃🏽", + "runner::skin-tone-5": "🏃🏾", + "running::skin-tone-5": "🏃🏾", + "runner::skin-tone-6": "🏃🏿", + "running::skin-tone-6": "🏃🏿", + "man-running": "🏃‍♂️", + "man-running::skin-tone-2": "🏃🏻‍♂️", + "man-running::skin-tone-3": "🏃🏼‍♂️", + "man-running::skin-tone-4": "🏃🏽‍♂️", + "man-running::skin-tone-5": "🏃🏾‍♂️", + "man-running::skin-tone-6": "🏃🏿‍♂️", + "woman-running": "🏃‍♀️", + "woman-running::skin-tone-2": "🏃🏻‍♀️", + "woman-running::skin-tone-3": "🏃🏼‍♀️", + "woman-running::skin-tone-4": "🏃🏽‍♀️", + "woman-running::skin-tone-5": "🏃🏾‍♀️", + "woman-running::skin-tone-6": "🏃🏿‍♀️", + "dancer": "💃", + "dancer::skin-tone-2": "💃🏻", + "dancer::skin-tone-3": "💃🏼", + "dancer::skin-tone-4": "💃🏽", + "dancer::skin-tone-5": "💃🏾", + "dancer::skin-tone-6": "💃🏿", + "man_dancing": "🕺", + "man_dancing::skin-tone-2": "🕺🏻", + "man_dancing::skin-tone-3": "🕺🏼", + "man_dancing::skin-tone-4": "🕺🏽", + "man_dancing::skin-tone-5": "🕺🏾", + "man_dancing::skin-tone-6": "🕺🏿", + "man_in_business_suit_levitating": "🕴️", + "man_in_business_suit_levitating::skin-tone-2": "🕴🏻", + "man_in_business_suit_levitating::skin-tone-3": "🕴🏼", + "man_in_business_suit_levitating::skin-tone-4": "🕴🏽", + "man_in_business_suit_levitating::skin-tone-5": "🕴🏾", + "man_in_business_suit_levitating::skin-tone-6": "🕴🏿", + "dancers": "👯", + "men-with-bunny-ears-partying": "👯‍♂️", + "man-with-bunny-ears-partying": "👯‍♂️", + "women-with-bunny-ears-partying": "👯‍♀️", + "woman-with-bunny-ears-partying": "👯‍♀️", + "person_in_steamy_room": "🧖", + "person_in_steamy_room::skin-tone-2": "🧖🏻", + "person_in_steamy_room::skin-tone-3": "🧖🏼", + "person_in_steamy_room::skin-tone-4": "🧖🏽", + "person_in_steamy_room::skin-tone-5": "🧖🏾", + "person_in_steamy_room::skin-tone-6": "🧖🏿", + "man_in_steamy_room": "🧖‍♂️", + "man_in_steamy_room::skin-tone-2": "🧖🏻‍♂️", + "man_in_steamy_room::skin-tone-3": "🧖🏼‍♂️", + "man_in_steamy_room::skin-tone-4": "🧖🏽‍♂️", + "man_in_steamy_room::skin-tone-5": "🧖🏾‍♂️", + "man_in_steamy_room::skin-tone-6": "🧖🏿‍♂️", + "woman_in_steamy_room": "🧖‍♀️", + "woman_in_steamy_room::skin-tone-2": "🧖🏻‍♀️", + "woman_in_steamy_room::skin-tone-3": "🧖🏼‍♀️", + "woman_in_steamy_room::skin-tone-4": "🧖🏽‍♀️", + "woman_in_steamy_room::skin-tone-5": "🧖🏾‍♀️", + "woman_in_steamy_room::skin-tone-6": "🧖🏿‍♀️", + "person_climbing": "🧗", + "person_climbing::skin-tone-2": "🧗🏻", + "person_climbing::skin-tone-3": "🧗🏼", + "person_climbing::skin-tone-4": "🧗🏽", + "person_climbing::skin-tone-5": "🧗🏾", + "person_climbing::skin-tone-6": "🧗🏿", + "man_climbing": "🧗‍♂️", + "man_climbing::skin-tone-2": "🧗🏻‍♂️", + "man_climbing::skin-tone-3": "🧗🏼‍♂️", + "man_climbing::skin-tone-4": "🧗🏽‍♂️", + "man_climbing::skin-tone-5": "🧗🏾‍♂️", + "man_climbing::skin-tone-6": "🧗🏿‍♂️", + "woman_climbing": "🧗‍♀️", + "woman_climbing::skin-tone-2": "🧗🏻‍♀️", + "woman_climbing::skin-tone-3": "🧗🏼‍♀️", + "woman_climbing::skin-tone-4": "🧗🏽‍♀️", + "woman_climbing::skin-tone-5": "🧗🏾‍♀️", + "woman_climbing::skin-tone-6": "🧗🏿‍♀️", + "fencer": "🤺", + "horse_racing": "🏇", + "horse_racing::skin-tone-2": "🏇🏻", + "horse_racing::skin-tone-3": "🏇🏼", + "horse_racing::skin-tone-4": "🏇🏽", + "horse_racing::skin-tone-5": "🏇🏾", + "horse_racing::skin-tone-6": "🏇🏿", + "skier": "⛷️", + "snowboarder": "🏂", + "snowboarder::skin-tone-2": "🏂🏻", + "snowboarder::skin-tone-3": "🏂🏼", + "snowboarder::skin-tone-4": "🏂🏽", + "snowboarder::skin-tone-5": "🏂🏾", + "snowboarder::skin-tone-6": "🏂🏿", + "golfer": "🏌️", + "golfer::skin-tone-2": "🏌🏻", + "golfer::skin-tone-3": "🏌🏼", + "golfer::skin-tone-4": "🏌🏽", + "golfer::skin-tone-5": "🏌🏾", + "golfer::skin-tone-6": "🏌🏿", + "man-golfing": "🏌️‍♂️", + "man-golfing::skin-tone-2": "🏌🏻‍♂️", + "man-golfing::skin-tone-3": "🏌🏼‍♂️", + "man-golfing::skin-tone-4": "🏌🏽‍♂️", + "man-golfing::skin-tone-5": "🏌🏾‍♂️", + "man-golfing::skin-tone-6": "🏌🏿‍♂️", + "woman-golfing": "🏌️‍♀️", + "woman-golfing::skin-tone-2": "🏌🏻‍♀️", + "woman-golfing::skin-tone-3": "🏌🏼‍♀️", + "woman-golfing::skin-tone-4": "🏌🏽‍♀️", + "woman-golfing::skin-tone-5": "🏌🏾‍♀️", + "woman-golfing::skin-tone-6": "🏌🏿‍♀️", + "surfer": "🏄", + "surfer::skin-tone-2": "🏄🏻", + "surfer::skin-tone-3": "🏄🏼", + "surfer::skin-tone-4": "🏄🏽", + "surfer::skin-tone-5": "🏄🏾", + "surfer::skin-tone-6": "🏄🏿", + "man-surfing": "🏄‍♂️", + "man-surfing::skin-tone-2": "🏄🏻‍♂️", + "man-surfing::skin-tone-3": "🏄🏼‍♂️", + "man-surfing::skin-tone-4": "🏄🏽‍♂️", + "man-surfing::skin-tone-5": "🏄🏾‍♂️", + "man-surfing::skin-tone-6": "🏄🏿‍♂️", + "woman-surfing": "🏄‍♀️", + "woman-surfing::skin-tone-2": "🏄🏻‍♀️", + "woman-surfing::skin-tone-3": "🏄🏼‍♀️", + "woman-surfing::skin-tone-4": "🏄🏽‍♀️", + "woman-surfing::skin-tone-5": "🏄🏾‍♀️", + "woman-surfing::skin-tone-6": "🏄🏿‍♀️", + "rowboat": "🚣", + "rowboat::skin-tone-2": "🚣🏻", + "rowboat::skin-tone-3": "🚣🏼", + "rowboat::skin-tone-4": "🚣🏽", + "rowboat::skin-tone-5": "🚣🏾", + "rowboat::skin-tone-6": "🚣🏿", + "man-rowing-boat": "🚣‍♂️", + "man-rowing-boat::skin-tone-2": "🚣🏻‍♂️", + "man-rowing-boat::skin-tone-3": "🚣🏼‍♂️", + "man-rowing-boat::skin-tone-4": "🚣🏽‍♂️", + "man-rowing-boat::skin-tone-5": "🚣🏾‍♂️", + "man-rowing-boat::skin-tone-6": "🚣🏿‍♂️", + "woman-rowing-boat": "🚣‍♀️", + "woman-rowing-boat::skin-tone-2": "🚣🏻‍♀️", + "woman-rowing-boat::skin-tone-3": "🚣🏼‍♀️", + "woman-rowing-boat::skin-tone-4": "🚣🏽‍♀️", + "woman-rowing-boat::skin-tone-5": "🚣🏾‍♀️", + "woman-rowing-boat::skin-tone-6": "🚣🏿‍♀️", + "swimmer": "🏊", + "swimmer::skin-tone-2": "🏊🏻", + "swimmer::skin-tone-3": "🏊🏼", + "swimmer::skin-tone-4": "🏊🏽", + "swimmer::skin-tone-5": "🏊🏾", + "swimmer::skin-tone-6": "🏊🏿", + "man-swimming": "🏊‍♂️", + "man-swimming::skin-tone-2": "🏊🏻‍♂️", + "man-swimming::skin-tone-3": "🏊🏼‍♂️", + "man-swimming::skin-tone-4": "🏊🏽‍♂️", + "man-swimming::skin-tone-5": "🏊🏾‍♂️", + "man-swimming::skin-tone-6": "🏊🏿‍♂️", + "woman-swimming": "🏊‍♀️", + "woman-swimming::skin-tone-2": "🏊🏻‍♀️", + "woman-swimming::skin-tone-3": "🏊🏼‍♀️", + "woman-swimming::skin-tone-4": "🏊🏽‍♀️", + "woman-swimming::skin-tone-5": "🏊🏾‍♀️", + "woman-swimming::skin-tone-6": "🏊🏿‍♀️", + "person_with_ball": "⛹️", + "person_with_ball::skin-tone-2": "⛹🏻", + "person_with_ball::skin-tone-3": "⛹🏼", + "person_with_ball::skin-tone-4": "⛹🏽", + "person_with_ball::skin-tone-5": "⛹🏾", + "person_with_ball::skin-tone-6": "⛹🏿", + "man-bouncing-ball": "⛹️‍♂️", + "man-bouncing-ball::skin-tone-2": "⛹🏻‍♂️", + "man-bouncing-ball::skin-tone-3": "⛹🏼‍♂️", + "man-bouncing-ball::skin-tone-4": "⛹🏽‍♂️", + "man-bouncing-ball::skin-tone-5": "⛹🏾‍♂️", + "man-bouncing-ball::skin-tone-6": "⛹🏿‍♂️", + "woman-bouncing-ball": "⛹️‍♀️", + "woman-bouncing-ball::skin-tone-2": "⛹🏻‍♀️", + "woman-bouncing-ball::skin-tone-3": "⛹🏼‍♀️", + "woman-bouncing-ball::skin-tone-4": "⛹🏽‍♀️", + "woman-bouncing-ball::skin-tone-5": "⛹🏾‍♀️", + "woman-bouncing-ball::skin-tone-6": "⛹🏿‍♀️", + "weight_lifter": "🏋️", + "weight_lifter::skin-tone-2": "🏋🏻", + "weight_lifter::skin-tone-3": "🏋🏼", + "weight_lifter::skin-tone-4": "🏋🏽", + "weight_lifter::skin-tone-5": "🏋🏾", + "weight_lifter::skin-tone-6": "🏋🏿", + "man-lifting-weights": "🏋️‍♂️", + "man-lifting-weights::skin-tone-2": "🏋🏻‍♂️", + "man-lifting-weights::skin-tone-3": "🏋🏼‍♂️", + "man-lifting-weights::skin-tone-4": "🏋🏽‍♂️", + "man-lifting-weights::skin-tone-5": "🏋🏾‍♂️", + "man-lifting-weights::skin-tone-6": "🏋🏿‍♂️", + "woman-lifting-weights": "🏋️‍♀️", + "woman-lifting-weights::skin-tone-2": "🏋🏻‍♀️", + "woman-lifting-weights::skin-tone-3": "🏋🏼‍♀️", + "woman-lifting-weights::skin-tone-4": "🏋🏽‍♀️", + "woman-lifting-weights::skin-tone-5": "🏋🏾‍♀️", + "woman-lifting-weights::skin-tone-6": "🏋🏿‍♀️", + "bicyclist": "🚴", + "bicyclist::skin-tone-2": "🚴🏻", + "bicyclist::skin-tone-3": "🚴🏼", + "bicyclist::skin-tone-4": "🚴🏽", + "bicyclist::skin-tone-5": "🚴🏾", + "bicyclist::skin-tone-6": "🚴🏿", + "man-biking": "🚴‍♂️", + "man-biking::skin-tone-2": "🚴🏻‍♂️", + "man-biking::skin-tone-3": "🚴🏼‍♂️", + "man-biking::skin-tone-4": "🚴🏽‍♂️", + "man-biking::skin-tone-5": "🚴🏾‍♂️", + "man-biking::skin-tone-6": "🚴🏿‍♂️", + "woman-biking": "🚴‍♀️", + "woman-biking::skin-tone-2": "🚴🏻‍♀️", + "woman-biking::skin-tone-3": "🚴🏼‍♀️", + "woman-biking::skin-tone-4": "🚴🏽‍♀️", + "woman-biking::skin-tone-5": "🚴🏾‍♀️", + "woman-biking::skin-tone-6": "🚴🏿‍♀️", + "mountain_bicyclist": "🚵", + "mountain_bicyclist::skin-tone-2": "🚵🏻", + "mountain_bicyclist::skin-tone-3": "🚵🏼", + "mountain_bicyclist::skin-tone-4": "🚵🏽", + "mountain_bicyclist::skin-tone-5": "🚵🏾", + "mountain_bicyclist::skin-tone-6": "🚵🏿", + "man-mountain-biking": "🚵‍♂️", + "man-mountain-biking::skin-tone-2": "🚵🏻‍♂️", + "man-mountain-biking::skin-tone-3": "🚵🏼‍♂️", + "man-mountain-biking::skin-tone-4": "🚵🏽‍♂️", + "man-mountain-biking::skin-tone-5": "🚵🏾‍♂️", + "man-mountain-biking::skin-tone-6": "🚵🏿‍♂️", + "woman-mountain-biking": "🚵‍♀️", + "woman-mountain-biking::skin-tone-2": "🚵🏻‍♀️", + "woman-mountain-biking::skin-tone-3": "🚵🏼‍♀️", + "woman-mountain-biking::skin-tone-4": "🚵🏽‍♀️", + "woman-mountain-biking::skin-tone-5": "🚵🏾‍♀️", + "woman-mountain-biking::skin-tone-6": "🚵🏿‍♀️", + "person_doing_cartwheel": "🤸", + "person_doing_cartwheel::skin-tone-2": "🤸🏻", + "person_doing_cartwheel::skin-tone-3": "🤸🏼", + "person_doing_cartwheel::skin-tone-4": "🤸🏽", + "person_doing_cartwheel::skin-tone-5": "🤸🏾", + "person_doing_cartwheel::skin-tone-6": "🤸🏿", + "man-cartwheeling": "🤸‍♂️", + "man-cartwheeling::skin-tone-2": "🤸🏻‍♂️", + "man-cartwheeling::skin-tone-3": "🤸🏼‍♂️", + "man-cartwheeling::skin-tone-4": "🤸🏽‍♂️", + "man-cartwheeling::skin-tone-5": "🤸🏾‍♂️", + "man-cartwheeling::skin-tone-6": "🤸🏿‍♂️", + "woman-cartwheeling": "🤸‍♀️", + "woman-cartwheeling::skin-tone-2": "🤸🏻‍♀️", + "woman-cartwheeling::skin-tone-3": "🤸🏼‍♀️", + "woman-cartwheeling::skin-tone-4": "🤸🏽‍♀️", + "woman-cartwheeling::skin-tone-5": "🤸🏾‍♀️", + "woman-cartwheeling::skin-tone-6": "🤸🏿‍♀️", + "wrestlers": "🤼", + "man-wrestling": "🤼‍♂️", + "woman-wrestling": "🤼‍♀️", + "water_polo": "🤽", + "water_polo::skin-tone-2": "🤽🏻", + "water_polo::skin-tone-3": "🤽🏼", + "water_polo::skin-tone-4": "🤽🏽", + "water_polo::skin-tone-5": "🤽🏾", + "water_polo::skin-tone-6": "🤽🏿", + "man-playing-water-polo": "🤽‍♂️", + "man-playing-water-polo::skin-tone-2": "🤽🏻‍♂️", + "man-playing-water-polo::skin-tone-3": "🤽🏼‍♂️", + "man-playing-water-polo::skin-tone-4": "🤽🏽‍♂️", + "man-playing-water-polo::skin-tone-5": "🤽🏾‍♂️", + "man-playing-water-polo::skin-tone-6": "🤽🏿‍♂️", + "woman-playing-water-polo": "🤽‍♀️", + "woman-playing-water-polo::skin-tone-2": "🤽🏻‍♀️", + "woman-playing-water-polo::skin-tone-3": "🤽🏼‍♀️", + "woman-playing-water-polo::skin-tone-4": "🤽🏽‍♀️", + "woman-playing-water-polo::skin-tone-5": "🤽🏾‍♀️", + "woman-playing-water-polo::skin-tone-6": "🤽🏿‍♀️", + "handball": "🤾", + "handball::skin-tone-2": "🤾🏻", + "handball::skin-tone-3": "🤾🏼", + "handball::skin-tone-4": "🤾🏽", + "handball::skin-tone-5": "🤾🏾", + "handball::skin-tone-6": "🤾🏿", + "man-playing-handball": "🤾‍♂️", + "man-playing-handball::skin-tone-2": "🤾🏻‍♂️", + "man-playing-handball::skin-tone-3": "🤾🏼‍♂️", + "man-playing-handball::skin-tone-4": "🤾🏽‍♂️", + "man-playing-handball::skin-tone-5": "🤾🏾‍♂️", + "man-playing-handball::skin-tone-6": "🤾🏿‍♂️", + "woman-playing-handball": "🤾‍♀️", + "woman-playing-handball::skin-tone-2": "🤾🏻‍♀️", + "woman-playing-handball::skin-tone-3": "🤾🏼‍♀️", + "woman-playing-handball::skin-tone-4": "🤾🏽‍♀️", + "woman-playing-handball::skin-tone-5": "🤾🏾‍♀️", + "woman-playing-handball::skin-tone-6": "🤾🏿‍♀️", + "juggling": "🤹", + "juggling::skin-tone-2": "🤹🏻", + "juggling::skin-tone-3": "🤹🏼", + "juggling::skin-tone-4": "🤹🏽", + "juggling::skin-tone-5": "🤹🏾", + "juggling::skin-tone-6": "🤹🏿", + "man-juggling": "🤹‍♂️", + "man-juggling::skin-tone-2": "🤹🏻‍♂️", + "man-juggling::skin-tone-3": "🤹🏼‍♂️", + "man-juggling::skin-tone-4": "🤹🏽‍♂️", + "man-juggling::skin-tone-5": "🤹🏾‍♂️", + "man-juggling::skin-tone-6": "🤹🏿‍♂️", + "woman-juggling": "🤹‍♀️", + "woman-juggling::skin-tone-2": "🤹🏻‍♀️", + "woman-juggling::skin-tone-3": "🤹🏼‍♀️", + "woman-juggling::skin-tone-4": "🤹🏽‍♀️", + "woman-juggling::skin-tone-5": "🤹🏾‍♀️", + "woman-juggling::skin-tone-6": "🤹🏿‍♀️", + "person_in_lotus_position": "🧘", + "person_in_lotus_position::skin-tone-2": "🧘🏻", + "person_in_lotus_position::skin-tone-3": "🧘🏼", + "person_in_lotus_position::skin-tone-4": "🧘🏽", + "person_in_lotus_position::skin-tone-5": "🧘🏾", + "person_in_lotus_position::skin-tone-6": "🧘🏿", + "man_in_lotus_position": "🧘‍♂️", + "man_in_lotus_position::skin-tone-2": "🧘🏻‍♂️", + "man_in_lotus_position::skin-tone-3": "🧘🏼‍♂️", + "man_in_lotus_position::skin-tone-4": "🧘🏽‍♂️", + "man_in_lotus_position::skin-tone-5": "🧘🏾‍♂️", + "man_in_lotus_position::skin-tone-6": "🧘🏿‍♂️", + "woman_in_lotus_position": "🧘‍♀️", + "woman_in_lotus_position::skin-tone-2": "🧘🏻‍♀️", + "woman_in_lotus_position::skin-tone-3": "🧘🏼‍♀️", + "woman_in_lotus_position::skin-tone-4": "🧘🏽‍♀️", + "woman_in_lotus_position::skin-tone-5": "🧘🏾‍♀️", + "woman_in_lotus_position::skin-tone-6": "🧘🏿‍♀️", + "bath": "🛀", + "bath::skin-tone-2": "🛀🏻", + "bath::skin-tone-3": "🛀🏼", + "bath::skin-tone-4": "🛀🏽", + "bath::skin-tone-5": "🛀🏾", + "bath::skin-tone-6": "🛀🏿", + "sleeping_accommodation": "🛌", + "sleeping_accommodation::skin-tone-2": "🛌🏻", + "sleeping_accommodation::skin-tone-3": "🛌🏼", + "sleeping_accommodation::skin-tone-4": "🛌🏽", + "sleeping_accommodation::skin-tone-5": "🛌🏾", + "sleeping_accommodation::skin-tone-6": "🛌🏿", + "people_holding_hands": "🧑‍🤝‍🧑", + "people_holding_hands::skin-tone-2": "🧑🏻‍🤝‍🧑🏻", + "people_holding_hands::skin-tone-3": "🧑🏼‍🤝‍🧑🏼", + "people_holding_hands::skin-tone-4": "🧑🏽‍🤝‍🧑🏽", + "people_holding_hands::skin-tone-5": "🧑🏾‍🤝‍🧑🏾", + "people_holding_hands::skin-tone-6": "🧑🏿‍🤝‍🧑🏿", + "two_women_holding_hands": "👭", + "women_holding_hands": "👭", + "two_women_holding_hands::skin-tone-2": "👭🏻", + "women_holding_hands::skin-tone-2": "👭🏻", + "two_women_holding_hands::skin-tone-3": "👭🏼", + "women_holding_hands::skin-tone-3": "👭🏼", + "two_women_holding_hands::skin-tone-4": "👭🏽", + "women_holding_hands::skin-tone-4": "👭🏽", + "two_women_holding_hands::skin-tone-5": "👭🏾", + "women_holding_hands::skin-tone-5": "👭🏾", + "two_women_holding_hands::skin-tone-6": "👭🏿", + "women_holding_hands::skin-tone-6": "👭🏿", + "man_and_woman_holding_hands": "👫", + "couple": "👫", + "man_and_woman_holding_hands::skin-tone-2": "👫🏻", + "couple::skin-tone-2": "👫🏻", + "man_and_woman_holding_hands::skin-tone-3": "👫🏼", + "couple::skin-tone-3": "👫🏼", + "man_and_woman_holding_hands::skin-tone-4": "👫🏽", + "couple::skin-tone-4": "👫🏽", + "man_and_woman_holding_hands::skin-tone-5": "👫🏾", + "couple::skin-tone-5": "👫🏾", + "man_and_woman_holding_hands::skin-tone-6": "👫🏿", + "couple::skin-tone-6": "👫🏿", + "two_men_holding_hands": "👬", + "men_holding_hands": "👬", + "two_men_holding_hands::skin-tone-2": "👬🏻", + "men_holding_hands::skin-tone-2": "👬🏻", + "two_men_holding_hands::skin-tone-3": "👬🏼", + "men_holding_hands::skin-tone-3": "👬🏼", + "two_men_holding_hands::skin-tone-4": "👬🏽", + "men_holding_hands::skin-tone-4": "👬🏽", + "two_men_holding_hands::skin-tone-5": "👬🏾", + "men_holding_hands::skin-tone-5": "👬🏾", + "two_men_holding_hands::skin-tone-6": "👬🏿", + "men_holding_hands::skin-tone-6": "👬🏿", + "couplekiss": "💏", + "couplekiss::skin-tone-2": "💏🏻", + "couplekiss::skin-tone-3": "💏🏼", + "couplekiss::skin-tone-4": "💏🏽", + "couplekiss::skin-tone-5": "💏🏾", + "couplekiss::skin-tone-6": "💏🏿", + "woman-kiss-man": "👩‍❤️‍💋‍👨", + "woman-kiss-man::skin-tone-2": "👩🏻‍❤️‍💋‍👨🏻", + "woman-kiss-man::skin-tone-3": "👩🏼‍❤️‍💋‍👨🏼", + "woman-kiss-man::skin-tone-4": "👩🏽‍❤️‍💋‍👨🏽", + "woman-kiss-man::skin-tone-5": "👩🏾‍❤️‍💋‍👨🏾", + "woman-kiss-man::skin-tone-6": "👩🏿‍❤️‍💋‍👨🏿", + "man-kiss-man": "👨‍❤️‍💋‍👨", + "man-kiss-man::skin-tone-2": "👨🏻‍❤️‍💋‍👨🏻", + "man-kiss-man::skin-tone-3": "👨🏼‍❤️‍💋‍👨🏼", + "man-kiss-man::skin-tone-4": "👨🏽‍❤️‍💋‍👨🏽", + "man-kiss-man::skin-tone-5": "👨🏾‍❤️‍💋‍👨🏾", + "man-kiss-man::skin-tone-6": "👨🏿‍❤️‍💋‍👨🏿", + "woman-kiss-woman": "👩‍❤️‍💋‍👩", + "woman-kiss-woman::skin-tone-2": "👩🏻‍❤️‍💋‍👩🏻", + "woman-kiss-woman::skin-tone-3": "👩🏼‍❤️‍💋‍👩🏼", + "woman-kiss-woman::skin-tone-4": "👩🏽‍❤️‍💋‍👩🏽", + "woman-kiss-woman::skin-tone-5": "👩🏾‍❤️‍💋‍👩🏾", + "woman-kiss-woman::skin-tone-6": "👩🏿‍❤️‍💋‍👩🏿", + "couple_with_heart": "💑", + "couple_with_heart::skin-tone-2": "💑🏻", + "couple_with_heart::skin-tone-3": "💑🏼", + "couple_with_heart::skin-tone-4": "💑🏽", + "couple_with_heart::skin-tone-5": "💑🏾", + "couple_with_heart::skin-tone-6": "💑🏿", + "woman-heart-man": "👩‍❤️‍👨", + "woman-heart-man::skin-tone-2": "👩🏻‍❤️‍👨🏻", + "woman-heart-man::skin-tone-3": "👩🏼‍❤️‍👨🏼", + "woman-heart-man::skin-tone-4": "👩🏽‍❤️‍👨🏽", + "woman-heart-man::skin-tone-5": "👩🏾‍❤️‍👨🏾", + "woman-heart-man::skin-tone-6": "👩🏿‍❤️‍👨🏿", + "man-heart-man": "👨‍❤️‍👨", + "man-heart-man::skin-tone-2": "👨🏻‍❤️‍👨🏻", + "man-heart-man::skin-tone-3": "👨🏼‍❤️‍👨🏼", + "man-heart-man::skin-tone-4": "👨🏽‍❤️‍👨🏽", + "man-heart-man::skin-tone-5": "👨🏾‍❤️‍👨🏾", + "man-heart-man::skin-tone-6": "👨🏿‍❤️‍👨🏿", + "woman-heart-woman": "👩‍❤️‍👩", + "woman-heart-woman::skin-tone-2": "👩🏻‍❤️‍👩🏻", + "woman-heart-woman::skin-tone-3": "👩🏼‍❤️‍👩🏼", + "woman-heart-woman::skin-tone-4": "👩🏽‍❤️‍👩🏽", + "woman-heart-woman::skin-tone-5": "👩🏾‍❤️‍👩🏾", + "woman-heart-woman::skin-tone-6": "👩🏿‍❤️‍👩🏿", + "family": "👪", + "man-woman-boy": "👨‍👩‍👦", + "man-woman-girl": "👨‍👩‍👧", + "man-woman-girl-boy": "👨‍👩‍👧‍👦", + "man-woman-boy-boy": "👨‍👩‍👦‍👦", + "man-woman-girl-girl": "👨‍👩‍👧‍👧", + "man-man-boy": "👨‍👨‍👦", + "man-man-girl": "👨‍👨‍👧", + "man-man-girl-boy": "👨‍👨‍👧‍👦", + "man-man-boy-boy": "👨‍👨‍👦‍👦", + "man-man-girl-girl": "👨‍👨‍👧‍👧", + "woman-woman-boy": "👩‍👩‍👦", + "woman-woman-girl": "👩‍👩‍👧", + "woman-woman-girl-boy": "👩‍👩‍👧‍👦", + "woman-woman-boy-boy": "👩‍👩‍👦‍👦", + "woman-woman-girl-girl": "👩‍👩‍👧‍👧", + "man-boy": "👨‍👦", + "man-boy-boy": "👨‍👦‍👦", + "man-girl": "👨‍👧", + "man-girl-boy": "👨‍👧‍👦", + "man-girl-girl": "👨‍👧‍👧", + "woman-boy": "👩‍👦", + "woman-boy-boy": "👩‍👦‍👦", + "woman-girl": "👩‍👧", + "woman-girl-boy": "👩‍👧‍👦", + "woman-girl-girl": "👩‍👧‍👧", + "speaking_head_in_silhouette": "🗣️", + "bust_in_silhouette": "👤", + "busts_in_silhouette": "👥", + "people_hugging": "🫂", + "footprints": "👣", + "monkey_face": "🐵", + "monkey": "🐒", + "gorilla": "🦍", + "orangutan": "🦧", + "dog": "🐶", + "dog2": "🐕", + "guide_dog": "🦮", + "service_dog": "🐕‍🦺", + "poodle": "🐩", + "wolf": "🐺", + "fox_face": "🦊", + "raccoon": "🦝", + "cat": "🐱", + "cat2": "🐈", + "black_cat": "🐈‍⬛", + "lion_face": "🦁", + "tiger": "🐯", + "tiger2": "🐅", + "leopard": "🐆", + "horse": "🐴", + "racehorse": "🐎", + "unicorn_face": "🦄", + "zebra_face": "🦓", + "deer": "🦌", + "bison": "🦬", + "cow": "🐮", + "ox": "🐂", + "water_buffalo": "🐃", + "cow2": "🐄", + "pig": "🐷", + "pig2": "🐖", + "boar": "🐗", + "pig_nose": "🐽", + "ram": "🐏", + "sheep": "🐑", + "goat": "🐐", + "dromedary_camel": "🐪", + "camel": "🐫", + "llama": "🦙", + "giraffe_face": "🦒", + "elephant": "🐘", + "mammoth": "🦣", + "rhinoceros": "🦏", + "hippopotamus": "🦛", + "mouse": "🐭", + "mouse2": "🐁", + "rat": "🐀", + "hamster": "🐹", + "rabbit": "🐰", + "rabbit2": "🐇", + "chipmunk": "🐿️", + "beaver": "🦫", + "hedgehog": "🦔", + "bat": "🦇", + "bear": "🐻", + "polar_bear": "🐻‍❄️", + "koala": "🐨", + "panda_face": "🐼", + "sloth": "🦥", + "otter": "🦦", + "skunk": "🦨", + "kangaroo": "🦘", + "badger": "🦡", + "feet": "🐾", + "paw_prints": "🐾", + "turkey": "🦃", + "chicken": "🐔", + "rooster": "🐓", + "hatching_chick": "🐣", + "baby_chick": "🐤", + "hatched_chick": "🐥", + "bird": "🐦", + "penguin": "🐧", + "dove_of_peace": "🕊️", + "eagle": "🦅", + "duck": "🦆", + "swan": "🦢", + "owl": "🦉", + "dodo": "🦤", + "feather": "🪶", + "flamingo": "🦩", + "peacock": "🦚", + "parrot": "🦜", + "frog": "🐸", + "crocodile": "🐊", + "turtle": "🐢", + "lizard": "🦎", + "snake": "🐍", + "dragon_face": "🐲", + "dragon": "🐉", + "sauropod": "🦕", + "t-rex": "🦖", + "philosoraptor": "🦖", + "whale": "🐳", + "whale2": "🐋", + "dolphin": "🐬", + "flipper": "🐬", + "seal": "🦭", + "fish": "🐟", + "tropical_fish": "🐠", + "blowfish": "🐡", + "shark": "🦈", + "octopus": "🐙", + "shell": "🐚", + "coral": "🪸", + "snail": "🐌", + "butterfly": "🦋", + "bug": "🐛", + "ant": "🐜", + "bee": "🐝", + "honeybee": "🐝", + "beetle": "🪲", + "ladybug": "🐞", + "lady_beetle": "🐞", + "cricket": "🦗", + "cockroach": "🪳", + "spider": "🕷️", + "spider_web": "🕸️", + "scorpion": "🦂", + "mosquito": "🦟", + "fly": "🪰", + "worm": "🪱", + "microbe": "🦠", + "bouquet": "💐", + "cherry_blossom": "🌸", + "white_flower": "💮", + "lotus": "🪷", + "rosette": "🏵️", + "rose": "🌹", + "wilted_flower": "🥀", + "hibiscus": "🌺", + "sunflower": "🌻", + "blossom": "🌼", + "tulip": "🌷", + "seedling": "🌱", + "potted_plant": "🪴", + "evergreen_tree": "🌲", + "deciduous_tree": "🌳", + "palm_tree": "🌴", + "cactus": "🌵", + "ear_of_rice": "🌾", + "herb": "🌿", + "shamrock": "☘️", + "four_leaf_clover": "🍀", + "maple_leaf": "🍁", + "fallen_leaf": "🍂", + "leaves": "🍃", + "empty_nest": "🪹", + "nest_with_eggs": "🪺", + "grapes": "🍇", + "melon": "🍈", + "watermelon": "🍉", + "tangerine": "🍊", + "lemon": "🍋", + "banana": "🍌", + "pineapple": "🍍", + "mango": "🥭", + "apple": "🍎", + "green_apple": "🍏", + "pear": "🍐", + "peach": "🍑", + "cherries": "🍒", + "strawberry": "🍓", + "blueberries": "🫐", + "kiwifruit": "🥝", + "tomato": "🍅", + "olive": "🫒", + "coconut": "🥥", + "avocado": "🥑", + "eggplant": "🍆", + "potato": "🥔", + "carrot": "🥕", + "corn": "🌽", + "hot_pepper": "🌶️", + "bell_pepper": "🫑", + "cucumber": "🥒", + "leafy_green": "🥬", + "broccoli": "🥦", + "garlic": "🧄", + "onion": "🧅", + "mushroom": "🍄", + "peanuts": "🥜", + "beans": "🫘", + "chestnut": "🌰", + "bread": "🍞", + "croissant": "🥐", + "baguette_bread": "🥖", + "flatbread": "🫓", + "pretzel": "🥨", + "bagel": "🥯", + "pancakes": "🥞", + "waffle": "🧇", + "cheese_wedge": "🧀", + "meat_on_bone": "🍖", + "poultry_leg": "🍗", + "cut_of_meat": "🥩", + "bacon": "🥓", + "hamburger": "🍔", + "fries": "🍟", + "pizza": "🍕", + "hotdog": "🌭", + "sandwich": "🥪", + "taco": "🌮", + "burrito": "🌯", + "tamale": "🫔", + "stuffed_flatbread": "🥙", + "falafel": "🧆", + "egg": "🥚", + "fried_egg": "🍳", + "cooking": "🍳", + "shallow_pan_of_food": "🥘", + "stew": "🍲", + "fondue": "🫕", + "bowl_with_spoon": "🥣", + "green_salad": "🥗", + "popcorn": "🍿", + "butter": "🧈", + "salt": "🧂", + "canned_food": "🥫", + "bento": "🍱", + "rice_cracker": "🍘", + "rice_ball": "🍙", + "rice": "🍚", + "curry": "🍛", + "ramen": "🍜", + "spaghetti": "🍝", + "sweet_potato": "🍠", + "oden": "🍢", + "sushi": "🍣", + "fried_shrimp": "🍤", + "fish_cake": "🍥", + "moon_cake": "🥮", + "dango": "🍡", + "dumpling": "🥟", + "fortune_cookie": "🥠", + "takeout_box": "🥡", + "crab": "🦀", + "lobster": "🦞", + "shrimp": "🦐", + "squid": "🦑", + "oyster": "🦪", + "icecream": "🍦", + "shaved_ice": "🍧", + "ice_cream": "🍨", + "doughnut": "🍩", + "cookie": "🍪", + "birthday": "🎂", + "cake": "🍰", + "cupcake": "🧁", + "pie": "🥧", + "chocolate_bar": "🍫", + "candy": "🍬", + "lollipop": "🍭", + "custard": "🍮", + "honey_pot": "🍯", + "baby_bottle": "🍼", + "glass_of_milk": "🥛", + "coffee": "☕", + "teapot": "🫖", + "tea": "🍵", + "sake": "🍶", + "champagne": "🍾", + "wine_glass": "🍷", + "cocktail": "🍸", + "tropical_drink": "🍹", + "beer": "🍺", + "beers": "🍻", + "clinking_glasses": "🥂", + "tumbler_glass": "🥃", + "pouring_liquid": "🫗", + "cup_with_straw": "🥤", + "bubble_tea": "🧋", + "beverage_box": "🧃", + "mate_drink": "🧉", + "ice_cube": "🧊", + "chopsticks": "🥢", + "knife_fork_plate": "🍽️", + "fork_and_knife": "🍴", + "spoon": "🥄", + "hocho": "🔪", + "knife": "🔪", + "jar": "🫙", + "amphora": "🏺", + "earth_africa": "🌍", + "earth_americas": "🌎", + "earth_asia": "🌏", + "globe_with_meridians": "🌐", + "world_map": "🗺️", + "japan": "🗾", + "compass": "🧭", + "snow_capped_mountain": "🏔️", + "mountain": "⛰️", + "volcano": "🌋", + "mount_fuji": "🗻", + "camping": "🏕️", + "beach_with_umbrella": "🏖️", + "desert": "🏜️", + "desert_island": "🏝️", + "national_park": "🏞️", + "stadium": "🏟️", + "classical_building": "🏛️", + "building_construction": "🏗️", + "bricks": "🧱", + "databricks": "🧱", + "pydata": "🪨", + "rock": "🪨", + "wood": "🪵", + "hut": "🛖", + "house_buildings": "🏘️", + "derelict_house_building": "🏚️", + "house": "🏠", + "house_with_garden": "🏡", + "office": "🏢", + "post_office": "🏣", + "european_post_office": "🏤", + "hospital": "🏥", + "bank": "🏦", + "hotel": "🏨", + "love_hotel": "🏩", + "convenience_store": "🏪", + "school": "🏫", + "department_store": "🏬", + "factory": "🏭", + "japanese_castle": "🏯", + "european_castle": "🏰", + "wedding": "💒", + "tokyo_tower": "🗼", + "statue_of_liberty": "🗽", + "church": "⛪", + "mosque": "🕌", + "hindu_temple": "🛕", + "synagogue": "🕍", + "shinto_shrine": "⛩️", + "kaaba": "🕋", + "fountain": "⛲", + "tent": "⛺", + "foggy": "🌁", + "night_with_stars": "🌃", + "cityscape": "🏙️", + "sunrise_over_mountains": "🌄", + "sunrise": "🌅", + "city_sunset": "🌆", + "city_sunrise": "🌇", + "bridge_at_night": "🌉", + "hotsprings": "♨️", + "carousel_horse": "🎠", + "playground_slide": "🛝", + "ferris_wheel": "🎡", + "roller_coaster": "🎢", + "barber": "💈", + "circus_tent": "🎪", + "steam_locomotive": "🚂", + "railway_car": "🚃", + "bullettrain_side": "🚄", + "bullettrain_front": "🚅", + "train2": "🚆", + "metro": "🚇", + "light_rail": "🚈", + "station": "🚉", + "tram": "🚊", + "monorail": "🚝", + "mountain_railway": "🚞", + "train": "🚋", + "bus": "🚌", + "oncoming_bus": "🚍", + "trolleybus": "🚎", + "minibus": "🚐", + "ambulance": "🚑", + "fire_engine": "🚒", + "police_car": "🚓", + "oncoming_police_car": "🚔", + "taxi": "🚕", + "oncoming_taxi": "🚖", + "car": "🚗", + "red_car": "🚗", + "oncoming_automobile": "🚘", + "blue_car": "🚙", + "pickup_truck": "🛻", + "truck": "🚚", + "articulated_lorry": "🚛", + "tractor": "🚜", + "racing_car": "🏎️", + "racing_motorcycle": "🏍️", + "motor_scooter": "🛵", + "manual_wheelchair": "🦽", + "motorized_wheelchair": "🦼", + "auto_rickshaw": "🛺", + "bike": "🚲", + "scooter": "🛴", + "skateboard": "🛹", + "roller_skate": "🛼", + "busstop": "🚏", + "motorway": "🛣️", + "railway_track": "🛤️", + "oil_drum": "🛢️", + "fuelpump": "⛽", + "wheel": "🛞", + "rotating_light": "🚨", + "traffic_light": "🚥", + "vertical_traffic_light": "🚦", + "octagonal_sign": "🛑", + "construction": "🚧", + "anchor": "⚓", + "ring_buoy": "🛟", + "boat": "⛵", + "sailboat": "⛵", + "canoe": "🛶", + "speedboat": "🚤", + "passenger_ship": "🛳️", + "ferry": "⛴️", + "motor_boat": "🛥️", + "ship": "🚢", + "airplane": "✈️", + "small_airplane": "🛩️", + "airplane_departure": "🛫", + "airplane_arriving": "🛬", + "parachute": "🪂", + "seat": "💺", + "helicopter": "🚁", + "suspension_railway": "🚟", + "mountain_cableway": "🚠", + "aerial_tramway": "🚡", + "satellite": "🛰️", + "rocket": "🚀", + "rocketing": "🚀", + "rocking": "🚀", + "ahhhhhhhhh": "🚀", + "flying_saucer": "🛸", + "bellhop_bell": "🛎️", + "luggage": "🧳", + "hourglass": "⌛", + "hourglass_flowing_sand": "⏳", + "watch": "⌚", + "alarm_clock": "⏰", + "stopwatch": "⏱️", + "timer_clock": "⏲️", + "mantelpiece_clock": "🕰️", + "clock12": "🕛", + "clock1230": "🕧", + "clock1": "🕐", + "clock130": "🕜", + "clock2": "🕑", + "clock230": "🕝", + "clock3": "🕒", + "clock330": "🕞", + "clock4": "🕓", + "clock430": "🕟", + "clock5": "🕔", + "clock530": "🕠", + "clock6": "🕕", + "clock630": "🕡", + "clock7": "🕖", + "clock730": "🕢", + "clock8": "🕗", + "clock830": "🕣", + "clock9": "🕘", + "clock930": "🕤", + "clock10": "🕙", + "clock1030": "🕥", + "clock11": "🕚", + "clock1130": "🕦", + "new_moon": "🌑", + "waxing_crescent_moon": "🌒", + "first_quarter_moon": "🌓", + "moon": "🌔", + "waxing_gibbous_moon": "🌔", + "full_moon": "🌕", + "waning_gibbous_moon": "🌖", + "last_quarter_moon": "🌗", + "waning_crescent_moon": "🌘", + "crescent_moon": "🌙", + "new_moon_with_face": "🌚", + "first_quarter_moon_with_face": "🌛", + "last_quarter_moon_with_face": "🌜", + "thermometer": "🌡️", + "sunny": "☀️", + "full_moon_with_face": "🌝", + "sun_with_face": "🌞", + "ringed_planet": "🪐", + "star": "⭐", + "star2": "🌟", + "stars": "🌠", + "milky_way": "🌌", + "cloud": "☁️", + "partly_sunny": "⛅", + "thunder_cloud_and_rain": "⛈️", + "mostly_sunny": "🌤️", + "sun_small_cloud": "🌤️", + "barely_sunny": "🌥️", + "sun_behind_cloud": "🌥️", + "partly_sunny_rain": "🌦️", + "sun_behind_rain_cloud": "🌦️", + "rain_cloud": "🌧️", + "snow_cloud": "🌨️", + "lightning": "🌩️", + "lightning_cloud": "🌩️", + "tornado": "🌪️", + "tornado_cloud": "🌪️", + "fog": "🌫️", + "wind_blowing_face": "🌬️", + "cyclone": "🌀", + "rainbow": "🌈", + "rainbow-daggy": "🌈", + "closed_umbrella": "🌂", + "umbrella": "☂️", + "umbrella_with_rain_drops": "☔", + "umbrella_on_ground": "⛱️", + "zap": "⚡", + "snowflake": "❄️", + "snowman": "☃️", + "snowman_without_snow": "⛄", + "comet": "☄️", + "fire": "🔥", + "tuzki_onfire": "🔥", + "tuzki-onfire": "🔥", + "droplet": "💧", + "ocean": "🌊", + "jack_o_lantern": "🎃", + "christmas_tree": "🎄", + "fireworks": "🎆", + "sparkler": "🎇", + "firecracker": "🧨", + "sparkles": "✨", + "balloon": "🎈", + "tada": "🎉", + "confetti_ball": "🎊", + "tanabata_tree": "🎋", + "bamboo": "🎍", + "dolls": "🎎", + "flags": "🎏", + "wind_chime": "🎐", + "rice_scene": "🎑", + "red_envelope": "🧧", + "ribbon": "🎀", + "gift": "🎁", + "reminder_ribbon": "🎗️", + "admission_tickets": "🎟️", + "ticket": "🎫", + "medal": "🎖️", + "trophy": "🏆", + "sports_medal": "🏅", + "first_place_medal": "🥇", + "second_place_medal": "🥈", + "third_place_medal": "🥉", + "soccer": "⚽", + "baseball": "⚾", + "softball": "🥎", + "basketball": "🏀", + "volleyball": "🏐", + "football": "🏈", + "rugby_football": "🏉", + "tennis": "🎾", + "flying_disc": "🥏", + "bowling": "🎳", + "cricket_bat_and_ball": "🏏", + "field_hockey_stick_and_ball": "🏑", + "ice_hockey_stick_and_puck": "🏒", + "lacrosse": "🥍", + "table_tennis_paddle_and_ball": "🏓", + "badminton_racquet_and_shuttlecock": "🏸", + "boxing_glove": "🥊", + "martial_arts_uniform": "🥋", + "goal_net": "🥅", + "golf": "⛳", + "ice_skate": "⛸️", + "fishing_pole_and_fish": "🎣", + "diving_mask": "🤿", + "running_shirt_with_sash": "🎽", + "ski": "🎿", + "sled": "🛷", + "curling_stone": "🥌", + "dart": "🎯", + "yo-yo": "🪀", + "kite": "🪁", + "8ball": "🎱", + "crystal_ball": "🔮", + "magic_wand": "🪄", + "nazar_amulet": "🧿", + "hamsa": "🪬", + "video_game": "🎮", + "joystick": "🕹️", + "slot_machine": "🎰", + "game_die": "🎲", + "jigsaw": "🧩", + "teddy_bear": "🧸", + "pinata": "🪅", + "mirror_ball": "🪩", + "nesting_dolls": "🪆", + "spades": "♠️", + "hearts": "♥️", + "diamonds": "♦️", + "clubs": "♣️", + "chess_pawn": "♟️", + "black_joker": "🃏", + "mahjong": "🀄", + "flower_playing_cards": "🎴", + "performing_arts": "🎭", + "frame_with_picture": "🖼️", + "art": "🎨", + "thread": "🧵", + "sewing_needle": "🪡", + "yarn": "🧶", + "knot": "🪢", + "eyeglasses": "👓", + "dark_sunglasses": "🕶️", + "goggles": "🥽", + "lab_coat": "🥼", + "safety_vest": "🦺", + "necktie": "👔", + "shirt": "👕", + "tshirt": "👕", + "jeans": "👖", + "scarf": "🧣", + "gloves": "🧤", + "coat": "🧥", + "socks": "🧦", + "dress": "👗", + "kimono": "👘", + "sari": "🥻", + "one-piece_swimsuit": "🩱", + "briefs": "🩲", + "shorts": "🩳", + "bikini": "👙", + "womans_clothes": "👚", + "purse": "👛", + "handbag": "👜", + "pouch": "👝", + "shopping_bags": "🛍️", + "school_satchel": "🎒", + "thong_sandal": "🩴", + "mans_shoe": "👞", + "shoe": "👞", + "athletic_shoe": "👟", + "hiking_boot": "🥾", + "womans_flat_shoe": "🥿", + "high_heel": "👠", + "sandal": "👡", + "ballet_shoes": "🩰", + "boot": "👢", + "crown": "👑", + "womans_hat": "👒", + "tophat": "🎩", + "mortar_board": "🎓", + "billed_cap": "🧢", + "military_helmet": "🪖", + "helmet_with_white_cross": "⛑️", + "prayer_beads": "📿", + "lipstick": "💄", + "ring": "💍", + "gem": "💎", + "mute": "🔇", + "speaker": "🔈", + "sound": "🔉", + "loud_sound": "🔊", + "loudspeaker": "📢", + "mega": "📣", + "postal_horn": "📯", + "bell": "🔔", + "no_bell": "🔕", + "musical_score": "🎼", + "musical_note": "🎵", + "notes": "🎶", + "studio_microphone": "🎙️", + "level_slider": "🎚️", + "control_knobs": "🎛️", + "microphone": "🎤", + "headphones": "🎧", + "radio": "📻", + "saxophone": "🎷", + "accordion": "🪗", + "guitar": "🎸", + "musical_keyboard": "🎹", + "trumpet": "🎺", + "violin": "🎻", + "banjo": "🪕", + "drum_with_drumsticks": "🥁", + "long_drum": "🪘", + "iphone": "📱", + "calling": "📲", + "phone": "☎️", + "telephone": "☎️", + "telephone_receiver": "📞", + "pager": "📟", + "fax": "📠", + "battery": "🔋", + "low_battery": "🪫", + "electric_plug": "🔌", + "computer": "💻", + "desktop_computer": "🖥️", + "printer": "🖨️", + "keyboard": "⌨️", + "three_button_mouse": "🖱️", + "trackball": "🖲️", + "minidisc": "💽", + "floppy_disk": "💾", + "cd": "💿", + "dvd": "📀", + "abacus": "🧮", + "movie_camera": "🎥", + "film_frames": "🎞️", + "film_projector": "📽️", + "webassembly": ".wasm", + "clapper": "🎬", + "tv": "📺", + "camera": "📷", + "camera_with_flash": "📸", + "video_camera": "📹", + "vhs": "📼", + "mag": "🔍", + "mag_right": "🔎", + "candle": "🕯️", + "bulb": "💡", + "flashlight": "🔦", + "izakaya_lantern": "🏮", + "lantern": "🏮", + "diya_lamp": "🪔", + "notebook_with_decorative_cover": "📔", + "closed_book": "📕", + "book": "📖", + "open_book": "📖", + "green_book": "📗", + "blue_book": "📘", + "orange_book": "📙", + "books": "📚", + "notebook": "📓", + "ledger": "📒", + "page_with_curl": "📃", + "scroll": "📜", + "page_facing_up": "📄", + "newspaper": "📰", + "rolled_up_newspaper": "🗞️", + "bookmark_tabs": "📑", + "bookmark": "🔖", + "label": "🏷️", + "moneybag": "💰", + "coin": "🪙", + "yen": "💴", + "dollar": "💵", + "euro": "💶", + "pound": "💷", + "money_with_wings": "💸", + "money_flying": "💸", + "credit_card": "💳", + "receipt": "🧾", + "chart": "💹", + "email": "✉️", + "envelope": "✉️", + "e-mail": "📧", + "incoming_envelope": "📨", + "envelope_with_arrow": "📩", + "outbox_tray": "📤", + "inbox_tray": "📥", + "package": "📦", + "mailbox": "📫", + "mailbox_closed": "📪", + "mailbox_with_mail": "📬", + "mailbox_with_no_mail": "📭", + "postbox": "📮", + "ballot_box_with_ballot": "🗳️", + "pencil2": "✏️", + "black_nib": "✒️", + "lower_left_fountain_pen": "🖋️", + "lower_left_ballpoint_pen": "🖊️", + "lower_left_paintbrush": "🖌️", + "lower_left_crayon": "🖍️", + "memo": "📝", + "pencil": "📝", + "briefcase": "💼", + "file_folder": "📁", + "open_file_folder": "📂", + "card_index_dividers": "🗂️", + "date": "📅", + "calendar": "📆", + "spiral_note_pad": "🗒️", + "spiral_calendar_pad": "🗓️", + "card_index": "📇", + "chart_with_upwards_trend": "📈", + "chart_with_downwards_trend": "📉", + "bar_chart": "📊", + "clipboard": "📋", + "pushpin": "📌", + "kodee_pin": "📌", + "kodee-pin": "📌", + "round_pushpin": "📍", + "paperclip": "📎", + "linked_paperclips": "🖇️", + "straight_ruler": "📏", + "triangular_ruler": "📐", + "scissors": "✂️", + "card_file_box": "🗃️", + "file_cabinet": "🗄️", + "wastebasket": "🗑️", + "lock": "🔒", + "unlock": "🔓", + "lock_with_ink_pen": "🔏", + "closed_lock_with_key": "🔐", + "key": "🔑", + "old_key": "🗝️", + "hammer": "🔨", + "axe": "🪓", + "pick": "⛏️", + "hammer_and_pick": "⚒️", + "hammer_and_wrench": "🛠️", + "dagger_knife": "🗡️", + "crossed_swords": "⚔️", + "gun": "🔫", + "boomerang": "🪃", + "bow_and_arrow": "🏹", + "shield": "🛡️", + "carpentry_saw": "🪚", + "wrench": "🔧", + "screwdriver": "🪛", + "nut_and_bolt": "🔩", + "gear": "⚙️", + "compression": "🗜️", + "scales": "⚖️", + "probing_cane": "🦯", + "link": "🔗", + "chains": "⛓️", + "hook": "🪝", + "toolbox": "🧰", + "magnet": "🧲", + "ladder": "🪜", + "alembic": "⚗️", + "test_tube": "🧪", + "petri_dish": "🧫", + "dna": "🧬", + "microscope": "🔬", + "telescope": "🔭", + "satellite_antenna": "📡", + "syringe": "💉", + "drop_of_blood": "🩸", + "pill": "💊", + "adhesive_bandage": "🩹", + "crutch": "🩼", + "stethoscope": "🩺", + "x-ray": "🩻", + "door": "🚪", + "elevator": "🛗", + "mirror": "🪞", + "window": "🪟", + "bed": "🛏️", + "couch_and_lamp": "🛋️", + "chair": "🪑", + "toilet": "🚽", + "plunger": "🪠", + "shower": "🚿", + "bathtub": "🛁", + "mouse_trap": "🪤", + "razor": "🪒", + "lotion_bottle": "🧴", + "safety_pin": "🧷", + "broom": "🧹", + "basket": "🧺", + "roll_of_paper": "🧻", + "bucket": "🪣", + "soap": "🧼", + "bubbles": "🫧", + "toothbrush": "🪥", + "sponge": "🧽", + "fire_extinguisher": "🧯", + "shopping_trolley": "🛒", + "smoking": "🚬", + "coffin": "⚰️", + "headstone": "🪦", + "funeral_urn": "⚱️", + "moyai": "🗿", + "placard": "🪧", + "identification_card": "🪪", + "atm": "🏧", + "put_litter_in_its_place": "🚮", + "potable_water": "🚰", + "wheelchair": "♿", + "mens": "🚹", + "womens": "🚺", + "restroom": "🚻", + "baby_symbol": "🚼", + "wc": "🚾", + "passport_control": "🛂", + "customs": "🛃", + "baggage_claim": "🛄", + "left_luggage": "🛅", + "warning": "⚠️", + "children_crossing": "🚸", + "no_entry": "⛔", + "no_entry_sign": "🚫", + "no_bicycles": "🚳", + "no_smoking": "🚭", + "do_not_litter": "🚯", + "non-potable_water": "🚱", + "no_pedestrians": "🚷", + "no_mobile_phones": "📵", + "underage": "🔞", + "radioactive_sign": "☢️", + "biohazard_sign": "☣️", + "arrow_up": "⬆️", + "arrow_upper_right": "↗️", + "arrow_right": "➡️", + "arrow_lower_right": "↘️", + "arrow_down": "⬇️", + "arrow_lower_left": "↙️", + "arrow_left": "⬅️", + "arrow_upper_left": "↖️", + "arrow_up_down": "↕️", + "left_right_arrow": "↔️", + "leftwards_arrow_with_hook": "↩️", + "arrow_right_hook": "↪️", + "arrow_heading_up": "⤴️", + "arrow_heading_down": "⤵️", + "arrows_clockwise": "🔃", + "arrows_counterclockwise": "🔄", + "back": "🔙", + "end": "🔚", + "on": "🔛", + "soon": "🔜", + "top": "🔝", + "place_of_worship": "🛐", + "atom_symbol": "⚛️", + "om_symbol": "🕉️", + "star_of_david": "✡️", + "wheel_of_dharma": "☸️", + "yin_yang": "☯️", + "latin_cross": "✝️", + "orthodox_cross": "☦️", + "star_and_crescent": "☪️", + "peace_symbol": "☮️", + "menorah_with_nine_branches": "🕎", + "six_pointed_star": "🔯", + "aries": "♈", + "taurus": "♉", + "gemini": "♊", + "cancer": "♋", + "leo": "♌", + "virgo": "♍", + "libra": "♎", + "scorpius": "♏", + "sagittarius": "♐", + "capricorn": "♑", + "aquarius": "♒", + "pisces": "♓", + "ophiuchus": "⛎", + "twisted_rightwards_arrows": "🔀", + "repeat": "🔁", + "repeat_one": "🔂", + "arrow_forward": "▶️", + "fast_forward": "⏩", + "black_right_pointing_double_triangle_with_vertical_bar": "⏭️", + "black_right_pointing_triangle_with_double_vertical_bar": "⏯️", + "arrow_backward": "◀️", + "rewind": "⏪", + "black_left_pointing_double_triangle_with_vertical_bar": "⏮️", + "arrow_up_small": "🔼", + "arrow_double_up": "⏫", + "arrow_down_small": "🔽", + "arrow_double_down": "⏬", + "double_vertical_bar": "⏸️", + "black_square_for_stop": "⏹️", + "black_circle_for_record": "⏺️", + "eject": "⏏️", + "cinema": "🎦", + "low_brightness": "🔅", + "high_brightness": "🔆", + "signal_strength": "📶", + "vibration_mode": "📳", + "mobile_phone_off": "📴", + "female_sign": "♀️", + "male_sign": "♂️", + "transgender_symbol": "⚧️", + "heavy_multiplication_x": "✖️", + "heavy_plus_sign": "➕", + "heavy_minus_sign": "➖", + "heavy_division_sign": "➗", + "heavy_equals_sign": "🟰", + "infinity": "♾️", + "bangbang": "‼️", + "interrobang": "⁉️", + "question": "❓", + "grey_question": "❔", + "grey_exclamation": "❕", + "exclamation": "❗", + "heavy_exclamation_mark": "❗", + "wavy_dash": "〰️", + "currency_exchange": "💱", + "heavy_dollar_sign": "💲", + "medical_symbol": "⚕️", + "staff_of_aesculapius": "⚕️", + "recycle": "♻️", + "fleur_de_lis": "⚜️", + "trident": "🔱", + "name_badge": "📛", + "beginner": "🔰", + "o": "⭕", + "white_check_mark": "✅", + "ballot_box_with_check": "☑️", + "heavy_check_mark": "✔️", + "x": "❌", + "negative_squared_cross_mark": "❎", + "curly_loop": "➰", + "loop": "➿", + "part_alternation_mark": "〽️", + "eight_spoked_asterisk": "✳️", + "eight_pointed_black_star": "✴️", + "sparkle": "❇️", + "copyright": "©️", + "registered": "®️", + "tm": "™️", + "hash": "#️⃣", + "keycap_star": "*️⃣", + "zero": "0️⃣", + "one": "1️⃣", + "two": "2️⃣", + "three": "3️⃣", + "four": "4️⃣", + "five": "5️⃣", + "six": "6️⃣", + "seven": "7️⃣", + "eight": "8️⃣", + "nine": "9️⃣", + "keycap_ten": "🔟", + "capital_abcd": "🔠", + "fivetran": "5️⃣", + "abcd": "🔡", + "symbols": "🔣", + "abc": "🔤", + "a": "🅰️", + "ab": "🆎", + "b": "🅱️", + "cl": "🆑", + "cool": "🆒", + "free": "🆓", + "information_source": "ℹ️", + "id": "🆔", + "m": "Ⓜ️", + "new": "🆕", + "ng": "🆖", + "o2": "🅾️", + "ok": "🆗", + "parking": "🅿️", + "sos": "🆘", + "up": "🆙", + "vs": "🆚", + "koko": "🈁", + "sa": "🈂️", + "u6708": "🈷️", + "u6709": "🈶", + "u6307": "🈯", + "ideograph_advantage": "🉐", + "u5272": "🈹", + "u7121": "🈚", + "u7981": "🈲", + "accept": "🉑", + "u7533": "🈸", + "u5408": "🈴", + "u7a7a": "🈳", + "congratulations": "㊗️", + "secret": "㊙️", + "u55b6": "🈺", + "u6e80": "🈵", + "red_circle": "🔴", + "large_orange_circle": "🟠", + "large_yellow_circle": "🟡", + "large_green_circle": "🟢", + "large_blue_circle": "🔵", + "large_purple_circle": "🟣", + "large_brown_circle": "🟤", + "black_circle": "⚫", + "white_circle": "⚪", + "large_red_square": "🟥", + "large_orange_square": "🟧", + "large_yellow_square": "🟨", + "large_green_square": "🟩", + "large_blue_square": "🟦", + "large_purple_square": "🟪", + "large_brown_square": "🟫", + "black_large_square": "⬛", + "white_large_square": "⬜", + "black_medium_square": "◼️", + "white_medium_square": "◻️", + "black_medium_small_square": "◾", + "white_medium_small_square": "◽", + "black_small_square": "▪️", + "white_small_square": "▫️", + "large_orange_diamond": "🔶", + "large_blue_diamond": "🔷", + "small_orange_diamond": "🔸", + "small_blue_diamond": "🔹", + "small_red_triangle": "🔺", + "small_red_triangle_down": "🔻", + "diamond_shape_with_a_dot_inside": "💠", + "radio_button": "🔘", + "white_square_button": "🔳", + "black_square_button": "🔲", + "checkered_flag": "🏁", + "triangular_flag_on_post": "🚩", + "crossed_flags": "🎌", + "waving_black_flag": "🏴", + "waving_white_flag": "🏳️", + "rainbow-flag": "🏳️‍🌈", + "transgender_flag": "🏳️‍⚧️", + "pirate_flag": "🏴‍☠️", + "flag-ac": "🇦🇨", + "flag-ad": "🇦🇩", + "flag-ae": "🇦🇪", + "flag-af": "🇦🇫", + "flag-ag": "🇦🇬", + "flag-ai": "🇦🇮", + "flag-al": "🇦🇱", + "flag-am": "🇦🇲", + "flag-ao": "🇦🇴", + "flag-aq": "🇦🇶", + "flag-ar": "🇦🇷", + "flag-as": "🇦🇸", + "flag-at": "🇦🇹", + "flag-au": "🇦🇺", + "flag-aw": "🇦🇼", + "flag-ax": "🇦🇽", + "flag-az": "🇦🇿", + "flag-ba": "🇧🇦", + "flag-bb": "🇧🇧", + "flag-bd": "🇧🇩", + "flag-be": "🇧🇪", + "flag-bf": "🇧🇫", + "flag-bg": "🇧🇬", + "flag-bh": "🇧🇭", + "flag-bi": "🇧🇮", + "flag-bj": "🇧🇯", + "flag-bl": "🇧🇱", + "flag-bm": "🇧🇲", + "flag-bn": "🇧🇳", + "flag-bo": "🇧🇴", + "flag-bq": "🇧🇶", + "flag-br": "🇧🇷", + "flag-bs": "🇧🇸", + "flag-bt": "🇧🇹", + "flag-bv": "🇧🇻", + "flag-bw": "🇧🇼", + "flag-by": "🇧🇾", + "flag-bz": "🇧🇿", + "flag-ca": "🇨🇦", + "flag-cc": "🇨🇨", + "flag-cd": "🇨🇩", + "flag-cf": "🇨🇫", + "flag-cg": "🇨🇬", + "flag-ch": "🇨🇭", + "flag-ci": "🇨🇮", + "flag-ck": "🇨🇰", + "flag-cl": "🇨🇱", + "flag-cm": "🇨🇲", + "cn": "🇨🇳", + "flag-cn": "🇨🇳", + "flag-co": "🇨🇴", + "flag-cp": "🇨🇵", + "flag-cr": "🇨🇷", + "flag-cu": "🇨🇺", + "flag-cv": "🇨🇻", + "flag-cw": "🇨🇼", + "flag-cx": "🇨🇽", + "flag-cy": "🇨🇾", + "flag-cz": "🇨🇿", + "de": "🇩🇪", + "flag-de": "🇩🇪", + "flag-dg": "🇩🇬", + "flag-dj": "🇩🇯", + "flag-dk": "🇩🇰", + "flag-dm": "🇩🇲", + "flag-do": "🇩🇴", + "flag-dz": "🇩🇿", + "flag-ea": "🇪🇦", + "flag-ec": "🇪🇨", + "flag-ee": "🇪🇪", + "flag-eg": "🇪🇬", + "flag-eh": "🇪🇭", + "flag-er": "🇪🇷", + "es": "🇪🇸", + "flag-es": "🇪🇸", + "flag-et": "🇪🇹", + "flag-eu": "🇪🇺", + "flag-fi": "🇫🇮", + "flag-fj": "🇫🇯", + "flag-fk": "🇫🇰", + "flag-fm": "🇫🇲", + "flag-fo": "🇫🇴", + "fr": "🇫🇷", + "flag-fr": "🇫🇷", + "flag-ga": "🇬🇦", + "gb": "🇬🇧", + "flag-gb": "🇬🇧", + "flag-gd": "🇬🇩", + "flag-ge": "🇬🇪", + "flag-gf": "🇬🇫", + "flag-gg": "🇬🇬", + "flag-gh": "🇬🇭", + "flag-gi": "🇬🇮", + "flag-gl": "🇬🇱", + "flag-gm": "🇬🇲", + "flag-gn": "🇬🇳", + "flag-gp": "🇬🇵", + "flag-gq": "🇬🇶", + "flag-gr": "🇬🇷", + "flag-gs": "🇬🇸", + "flag-gt": "🇬🇹", + "flag-gu": "🇬🇺", + "flag-gw": "🇬🇼", + "flag-gy": "🇬🇾", + "flag-hk": "🇭🇰", + "flag-hm": "🇭🇲", + "flag-hn": "🇭🇳", + "flag-hr": "🇭🇷", + "flag-ht": "🇭🇹", + "flag-hu": "🇭🇺", + "flag-ic": "🇮🇨", + "flag-id": "🇮🇩", + "flag-ie": "🇮🇪", + "flag-il": "🇮🇱", + "flag-im": "🇮🇲", + "flag-in": "🇮🇳", + "flag-io": "🇮🇴", + "flag-iq": "🇮🇶", + "flag-ir": "🇮🇷", + "flag-is": "🇮🇸", + "it": "🇮🇹", + "flag-it": "🇮🇹", + "flag-je": "🇯🇪", + "flag-jm": "🇯🇲", + "flag-jo": "🇯🇴", + "jp": "🇯🇵", + "flag-jp": "🇯🇵", + "flag-ke": "🇰🇪", + "flag-kg": "🇰🇬", + "flag-kh": "🇰🇭", + "flag-ki": "🇰🇮", + "flag-km": "🇰🇲", + "flag-kn": "🇰🇳", + "flag-kp": "🇰🇵", + "kr": "🇰🇷", + "flag-kr": "🇰🇷", + "flag-kw": "🇰🇼", + "flag-ky": "🇰🇾", + "flag-kz": "🇰🇿", + "flag-la": "🇱🇦", + "flag-lb": "🇱🇧", + "flag-lc": "🇱🇨", + "flag-li": "🇱🇮", + "flag-lk": "🇱🇰", + "flag-lr": "🇱🇷", + "flag-ls": "🇱🇸", + "flag-lt": "🇱🇹", + "flag-lu": "🇱🇺", + "flag-lv": "🇱🇻", + "flag-ly": "🇱🇾", + "flag-ma": "🇲🇦", + "flag-mc": "🇲🇨", + "flag-md": "🇲🇩", + "flag-me": "🇲🇪", + "flag-mf": "🇲🇫", + "flag-mg": "🇲🇬", + "flag-mh": "🇲🇭", + "flag-mk": "🇲🇰", + "flag-ml": "🇲🇱", + "flag-mm": "🇲🇲", + "flag-mn": "🇲🇳", + "flag-mo": "🇲🇴", + "flag-mp": "🇲🇵", + "flag-mq": "🇲🇶", + "flag-mr": "🇲🇷", + "flag-ms": "🇲🇸", + "flag-mt": "🇲🇹", + "flag-mu": "🇲🇺", + "flag-mv": "🇲🇻", + "flag-mw": "🇲🇼", + "flag-mx": "🇲🇽", + "flag-my": "🇲🇾", + "flag-mz": "🇲🇿", + "flag-na": "🇳🇦", + "flag-nc": "🇳🇨", + "flag-ne": "🇳🇪", + "flag-nf": "🇳🇫", + "flag-ng": "🇳🇬", + "flag-ni": "🇳🇮", + "flag-nl": "🇳🇱", + "flag-no": "🇳🇴", + "flag-np": "🇳🇵", + "flag-nr": "🇳🇷", + "flag-nu": "🇳🇺", + "flag-nz": "🇳🇿", + "flag-om": "🇴🇲", + "flag-pa": "🇵🇦", + "flag-pe": "🇵🇪", + "flag-pf": "🇵🇫", + "flag-pg": "🇵🇬", + "flag-ph": "🇵🇭", + "flag-pk": "🇵🇰", + "flag-pl": "🇵🇱", + "flag-pm": "🇵🇲", + "flag-pn": "🇵🇳", + "flag-pr": "🇵🇷", + "flag-ps": "🇵🇸", + "flag-pt": "🇵🇹", + "flag-pw": "🇵🇼", + "flag-py": "🇵🇾", + "flag-qa": "🇶🇦", + "flag-re": "🇷🇪", + "flag-ro": "🇷🇴", + "flag-rs": "🇷🇸", + "ru": "🇷🇺", + "flag-ru": "🇷🇺", + "flag-rw": "🇷🇼", + "flag-sa": "🇸🇦", + "flag-sb": "🇸🇧", + "flag-sc": "🇸🇨", + "flag-sd": "🇸🇩", + "flag-se": "🇸🇪", + "flag-sg": "🇸🇬", + "flag-sh": "🇸🇭", + "flag-si": "🇸🇮", + "flag-sj": "🇸🇯", + "flag-sk": "🇸🇰", + "flag-sl": "🇸🇱", + "flag-sm": "🇸🇲", + "flag-sn": "🇸🇳", + "flag-so": "🇸🇴", + "flag-sr": "🇸🇷", + "flag-ss": "🇸🇸", + "flag-st": "🇸🇹", + "flag-sv": "🇸🇻", + "flag-sx": "🇸🇽", + "flag-sy": "🇸🇾", + "flag-sz": "🇸🇿", + "flag-ta": "🇹🇦", + "flag-tc": "🇹🇨", + "flag-td": "🇹🇩", + "flag-tf": "🇹🇫", + "flag-tg": "🇹🇬", + "flag-th": "🇹🇭", + "flag-tj": "🇹🇯", + "flag-tk": "🇹🇰", + "flag-tl": "🇹🇱", + "flag-tm": "🇹🇲", + "flag-tn": "🇹🇳", + "flag-to": "🇹🇴", + "flag-tr": "🇹🇷", + "flag-tt": "🇹🇹", + "flag-tv": "🇹🇻", + "flag-tw": "🇹🇼", + "flag-tz": "🇹🇿", + "flag-ua": "🇺🇦", + "flag-ug": "🇺🇬", + "flag-um": "🇺🇲", + "flag-un": "🇺🇳", + "us": "🇺🇸", + "flag-us": "🇺🇸", + "flag-uy": "🇺🇾", + "flag-uz": "🇺🇿", + "flag-va": "🇻🇦", + "flag-vc": "🇻🇨", + "flag-ve": "🇻🇪", + "flag-vg": "🇻🇬", + "flag-vi": "🇻🇮", + "flag-vn": "🇻🇳", + "flag-vu": "🇻🇺", + "flag-wf": "🇼🇫", + "flag-ws": "🇼🇸", + "flag-xk": "🇽🇰", + "flag-ye": "🇾🇪", + "flag-yt": "🇾🇹", + "flag-za": "🇿🇦", + "flag-zm": "🇿🇲", + "flag-zw": "🇿🇼", + "flag-england": "🏴󠁧󠁢󠁥󠁮󠁧󠁿", + "flag-scotland": "🏴󠁧󠁢󠁳󠁣󠁴󠁿", + "flag-wales": "🏴󠁧󠁢󠁷󠁬󠁳󠁿", + "kotlinconf23": "K", + "kotlinconf-2023": "K", + "kotlin-intensifies-purple": "K", + "kotlin-intensifies": "K", + "compose-multiplatform": "K", + "kotlinnew": "K", + "kotlin": "K", + "kotlin_emoji": "K", + "kotlin-emoji": "K", + "kotlin-gradient": "K", + "kotlin_gradient": "K", + "kotlin-golf": "🏌️", + "mascot": "🧸", + "mascot-wink": "🧸", + "compose": "✏️", + "yes": "👌", + "ohyes": "👌", + "thank-you": "🙏", + "tnx": "🙏", + "cool-doge": "🐕", + "blob-hype": "🦠", + "suspend": "⏸️", + "parrot-upside-down": "🦜", + "twitter": "T", + "javascript": "JS", + "this-is-fine": "🙂", + "marrrcin": "🙂", + "partydagster": "🎉", + "kedroid-party": "🎉", + "planet-daggy": "🪐", + "flames-daggy": "🔥", + "next-level-daggy": "👌", + "awesome": "👌", + "dagster": "D", + "super": "👌", + "blob_ok_hand": "👌", + "blob-okay-hand": "👌", + "gradle": "G", + "maskot-wink": "🧸", + "maskot": "🧸", + "kotlin-flag": "K", + "oh-yeah": "🎉", + "thumbsup_all": "👍", + "not-kotlin": "😶", + "trollface": "🧌", + "no": "🚫", + "ohno": "🚫", + "nospam": "🚫", + "blob_shrug": "🤷", + "thread-please": "🧵", + "dagster-bot-resolve": "🤖", + "bananadance": "🍌", + "bananas": "🍌", + "laptop_parrot": "🦜", + "laptop-parrot": "🦜", + "dancing_parrot": "🦜", + "dancing-parrot": "🦜", + "bongo_blob": "🦠", + "blob": "🦠", + "blob_wave": "👋", + "pikachu_wave": "👋", + "android_wave": "👋", + "android-wave": "👋", + "dagster-bot-surfaced-to-issue": "🤖", + "tsow-slack-icon": "S", + "kedro": "K", + "pipe": "|", + "prefect": "P", + "snowflake-inc": "❄️", + "kestra": "K" +} \ No newline at end of file diff --git a/front/src/logic/nostril.ts b/front/src/logic/nostril.ts new file mode 100644 index 0000000..4e5549d --- /dev/null +++ b/front/src/logic/nostril.ts @@ -0,0 +1,36 @@ +import type { Event } from "@/types/nostr"; +import type { FC, FlatFeed, Poast } from "@/types/trill"; +import { engagementBunt, openLock } from "./bunts"; +export function eventsToFc(relayData: Record): FC { + const start = null; + const end = null; + const feed = Object.values(relayData).reduce((acc: FlatFeed, events) => { + const poasts = events.map(eventToPoast); + for (const p of poasts) { + if (p) acc[p.id] = p; + } + return acc; + }, {}); + return { feed, start, end }; +} +export function eventToPoast(event: Event): Poast | null { + if (event.kind !== 1) return null; + const contents = [{ paragraph: [{ text: event.content }] }]; + const ts = event.created_at * 1000; + const id = `${ts}`; + const poast: Poast = { + id, + host: event.pubkey, + author: event.pubkey, + contents, + thread: id, + parent: null, + read: openLock, + write: openLock, + tags: [], + time: ts, + engagement: engagementBunt, + children: [], + }; + return poast; +} diff --git a/front/src/logic/requests/nostril.ts b/front/src/logic/requests/nostril.ts new file mode 100644 index 0000000..6f0edcf --- /dev/null +++ b/front/src/logic/requests/nostril.ts @@ -0,0 +1,139 @@ +import type Urbit from "urbit-api"; +import type { Cursor, PostID, SentPoast } from "@/types/trill"; +import type { Ship } from "@/types/urbit"; +import { FeedPostCount } from "../constants"; +import type { UserProfile } from "@/types/nostril"; + +// Subscribe +type Handler = (date: any) => void; +export default class IO { + airlock; + constructor(airlock: Urbit) { + this.airlock = airlock; + } + private async poke(json: any) { + return this.airlock.poke({ app: "nostril", mark: "json", json }); + } + private async scry(path: string) { + return this.airlock.scry({ app: "nostril", path }); + } + private async sub(path: string, handler: Handler) { + const err = (err: any, _id: string) => + console.log(err, "error on nostril subscription"); + const quit = (data: any) => + console.log(data, "nostril subscription kicked"); + const res = await this.airlock.subscribe({ + app: "nostril", + path, + event: handler, + err, + quit, + }); + console.log(res, "subscribed to nostril agent"); + } + async unsub(sub: number) { + return await this.airlock.unsubscribe(sub); + } + // subs + async subscribeStore(handler: Handler) { + const res = await this.sub("/ui", handler); + return res; + } + // scries + + async scryFeed(start: Cursor, end: Cursor, desc = true) { + const order = desc ? 1 : 0; + const term = "feed"; + + const path = `/j/feed/${term}/${start}/${end}/${FeedPostCount}/${order}`; + return await this.scry(path); + } + async scryPost( + host: Ship, + id: PostID, + start: Cursor, + end: Cursor, + desc = true, + ) { + const order = desc ? 1 : 0; + + const path = `/j/post/${host}/${id}/${start}/${end}/${FeedPostCount}/${order}`; + return await this.scry(path); + } + // pokes + + async pokeAlive() { + return await this.poke({ alive: true }); + } + async addPost(pubkey: string, content: string) { + const json = { add: { pubkey, content } }; + return this.poke({ post: json }); + } + // async addPost(post: SentPoast, gossip: boolean) { + // const json = { + // "new-post": { + // "sent-post": post, + // gossip, + // }, + // }; + // return this.poke(json); + // } + + async deletePost(id: string) { + const host = `~${this.airlock.ship}`; + const json = { + "del-post": { + ship: host, + id: id, + }, + }; + return this.poke(json); + } + + async addReact(ship: Ship, id: PostID, reaction: string) { + const json = { + "new-react": { + react: reaction, + pid: { + id: id, + ship: ship, + }, + }, + }; + + return this.poke(json); + } + + // follows + async follow(ship: Ship) { + const json = { add: ship }; + return this.poke({ fols: json }); + } + + async unfollow(ship: Ship) { + const json = { del: ship }; + return await this.poke({ fols: json }); + } + // profiles + async createProfile(pubkey: string, profile: UserProfile) { + const json = { add: { pubkey, profile } }; + return await this.poke({ prof: json }); + } + async createKey() { + const json = { add: null }; + return await this.poke({ keys: json }); + } + async removeKey(pubkey: string) { + const json = { del: pubkey }; + return await this.poke({ keys: json }); + } + // relaying + async relayPost(host: string, id: string, relays: string[]) { + const json = { send: { host, id, relays } }; + return await this.poke({ rela: json }); + } +} + +// notifications + +// mark as read diff --git a/front/src/logic/utils.ts b/front/src/logic/utils.ts new file mode 100644 index 0000000..dbd246e --- /dev/null +++ b/front/src/logic/utils.ts @@ -0,0 +1,459 @@ +import type { + Content, + Notification, + ID, + ExternalContent, + Poast, + Reference, + Inline, + PID, + SortugRef, +} from "@/types/trill"; +import type { Ship } from "@/types/urbit"; +import anyAscii from "any-ascii"; +import type { ReactGrouping, SPID } from "@/types/ui"; +import { openLock } from "./bunts"; +import { isValidPatp, patp2dec } from "urbit-ob"; +import { REF_REGEX } from "./constants"; + +export function parseSortugLink(link: string): SortugRef { + const s = link.replace("urbit://", "").split("/"); + const [type, ship, ...pat] = s; + const path = `/${pat.join("/")}`; + return { type, ship, path }; +} +export function sortugRefTolink(r: SortugRef): string { + return `urbit://${r.type}/${r.ship}${r.path}`; +} +// TODO + +export function createReference(ship: Ship, id: ID) { + return { + reference: { + feed: { id: id, ship: ship }, + }, + }; +} + +export function addScheme(url: string) { + if (url.includes("localhost")) { + return `http://${url.replace("http://", "")}`; + } else { + return `https://${url.replace("http://", "")}`; + } +} + +export function easyCode(code: string) { + const string = code.replace(/-/g, ""); + const matches = string.match(/.{1,6}/g); + if (matches) return matches.join("-"); +} + +export function tilde(patp: Ship) { + if (patp[0] == "~") { + return patp; + } else { + return "~" + patp; + } +} + +export function color_to_hex(color: string) { + let hex = "#" + color.replace(".", "").replace("0x", "").toUpperCase(); + if (hex == "#0") { + hex = "#000000"; + } + return hex; +} + +export function date_diff(date: number | Date, type: "short" | "long") { + const now = new Date().getTime(); + const diff = now - new Date(date).getTime(); + if (type == "short") { + return to_string(diff / 1000); + } else { + return to_string_long(diff / 1000); + } +} + +function to_string(s: number) { + if (s < 60) { + return "now"; + } else if (s < 3600) { + return `${Math.ceil(s / 60)}m`; + } else if (s < 86400) { + return `${Math.ceil(s / 60 / 60)}h`; + } else if (s < 2678400) { + return `${Math.ceil(s / 60 / 60 / 24)}d`; + } else if (s < 32140800) { + return `${Math.ceil(s / 60 / 60 / 24 / 30)}mo`; + } else { + return `${Math.ceil(s / 60 / 60 / 24 / 30 / 12)}y`; + } +} + +function to_string_long(s: number) { + if (s < 60) { + return "right now"; + } else if (s < 3600) { + return `${Math.ceil(s / 60)} minutes ago`; + } else if (s < 86400) { + return `${Math.ceil(s / 60 / 60)} hours ago`; + } else if (s < 2678400) { + return `${Math.ceil(s / 60 / 60 / 24)} days ago`; + } else if (s < 32140800) { + return `${Math.ceil(s / 60 / 60 / 24 / 30)} months ago`; + } else { + return `${Math.ceil(s / 60 / 60 / 24 / 30 / 12)} years ago`; + } +} + +export function regexes() { + const IMAGE_REGEX = new RegExp(/(jpg|img|png|gif|tiff|jpeg|webp|webm|svg)$/i); + const AUDIO_REGEX = new RegExp(/(mp3|wav|ogg)$/i); + const VIDEO_REGEX = new RegExp(/(mov|mp4|ogv)$/i); + return { img: IMAGE_REGEX, aud: AUDIO_REGEX, vid: VIDEO_REGEX }; +} + +export function stringToSymbol(str: string) { + const ascii = anyAscii(str); + let result = ""; + for (let i = 0; i < ascii.length; i++) { + const n = ascii.charCodeAt(i); + if ((n >= 97 && n <= 122) || (n >= 48 && n <= 57)) { + result += ascii[i]; + } else if (n >= 65 && n <= 90) { + result += String.fromCharCode(n + 32); + } else { + result += "-"; + } + } + result = result.replace(/^[\-\d]+|\-+/g, "-"); + result = result.replace(/^\-+|\-+$/g, ""); + return result; +} +export function buildDM(author: Ship, recipient: Ship, contents: Content[]) { + const node: any = {}; + const point = patp2dec(recipient); + const index = `/${point}/${makeIndex()}`; + node[index] = { + children: null, + post: { + author: author, + contents: contents, + hash: null, + index: index, + signatures: [], + "time-sent": Date.now(), + }, + }; + return { + app: "dm-hook", + mark: "graph-update-3", + json: { + "add-nodes": { + resource: { name: "dm-inbox", ship: author }, + nodes: node, + }, + }, + }; +} + +export function makeIndex(): string { + const DA_UNIX_EPOCH = BigInt("170141184475152167957503069145530368000"); + const DA_SECOND = BigInt("18446744073709551616"); + const timeSinceEpoch = (BigInt(Date.now()) * DA_SECOND) / BigInt(1000); + return (DA_UNIX_EPOCH + timeSinceEpoch).toString(); +} +export function makeDottedIndex() { + const DA_UNIX_EPOCH = BigInt("170141184475152167957503069145530368000"); + const DA_SECOND = BigInt("18446744073709551616"); + const timeSinceEpoch = (BigInt(Date.now()) * DA_SECOND) / BigInt(1000); + const index = (DA_UNIX_EPOCH + timeSinceEpoch).toString(); + return index.replace(/\B(?=(\d{3})+(?!\d))/g, "."); +} + +export function repostData(p: Poast): PID | null { + if ( + p.contents.length === 1 && + "ref" in p.contents[0] && + p.contents[0].ref.type === "trill" + ) + return { + id: p.contents[0].ref.path.slice(1), + ship: p.contents[0].ref.ship, + }; + else return null; +} + +export function getNotificationTime(n: Notification): number { + if ("follow" in n) { + return n.follow.time; + } else if ("unfollow" in n) { + return n.unfollow.time; + } else if ("mention" in n) { + return n.mention.time; + } else if ("react" in n) { + return n.react.time; + } else if ("reply" in n) { + return n.reply.time; + } else if ("quote" in n) { + return n.quote.time; + } else if ("share" in n) { + return n.share.time; + } else { + return Date.now(); + } +} +export function abbreviateChat(s: string): string { + const plist = s.trim().split(" "); + if (isValidPatp(plist[0]) && plist.length > 1) { + return `${plist[0]} & ${plist.length - 1}+`; + } else if (s.length < 25) return s; + else return `${s.substring(0, 25)}...`; +} + +export function timestring(n: number): string { + const nn = new Date(n); + return nn.toTimeString().slice(0, 5); +} +export function wait(ms: number) { + return new Promise((resolve, _reject) => { + setTimeout(resolve, ms); + }); +} + +export function quoteToReference(d: SPID): Reference | ExternalContent { + if (d.service === "twatter") + return { + json: { + origin: "twatter", + content: JSON.stringify(d.post), + }, + }; + else + return { + ref: { + type: "trill", + ship: d.post.host, + path: `/${d.post.id}`, + }, + }; +} + +export function trillPermalink(t: Poast) { + return `urbit://trill/${t.host}/${t.id}`; +} +export function isFeedRef(c: Content): boolean { + return "ref" in c && (c as Reference).ref.type === "trill"; +} + +export function checkTilde(s: string) { + if (s[0] === "~") return s; + else return "~" + s; +} + +export function addDots(s: string, num: number): string { + const reversed = s.split("").reverse().join(""); + const reg = new RegExp(`.{${num}}`, "g"); + const withCommas = reversed.replace(reg, "$&."); + return withCommas.split("").reverse().join("").slice(1); +} +export function addDots5(s: string): string { + const reversed = s.split("").reverse().join(""); + const withCommas = reversed.replace(/.{5}/g, "$&."); + return withCommas.split("").reverse().join(""); +} +// TODO +export function getTrillText(c: Content): string { + if (!c) return ""; + const reducePara = (acc: string, item: Inline) => { + let t = ""; + if ("text" in item) t = item.text + " "; + if ("italic" in item) t = item.italic + " "; + if ("bold" in item) t = item.bold + " "; + if ("strike" in item) t = item.strike + " "; + if ("ship" in item) t = item.ship + " "; + if ("codespan" in item) t = item.codespan + " "; + if ("link" in item) t = item.link.href + " "; + if ("break" in item) t = "\n"; + return acc + t; + }; + return c.reduce((acc, item) => { + if ("paragraph" in item) { + const text = item.paragraph.reduce(reducePara, ""); + return acc + text + "\n"; + } else return acc; + }, ""); +} +export function isTwatterLink(s: string) { + const sp = s + .replace("https://", "") + .split("/") + .filter((s) => s); + return sp.length === 4 && sp[0] === "twitter.com" && sp[2] === "status"; +} +export const isSortugLink = (s: string) => !!s.match(REF_REGEX); +export function parseOutSortugLinks(s: string): [SortugRef[], string] { + const matches = s.match(REF_REGEX); + let refs = []; + let rest = s; + for (let m of matches || []) { + rest = rest.replace(m, ""); + refs.push(parseSortugLink(m)); + } + return [refs, rest]; +} + +export function isTrillLink(s: string): boolean { + if (!isSortugLink(s)) return false; + const r = parseSortugLink(s); + if (r.type !== "trill") return false; + return isValidPatp(r.ship) && !isNaN(Number(r.path.slice(1))); +} + +export function auraToHex(s: string): string { + if (s.startsWith("0x")) { + let numbers = s.replace("0x", "").replace(".", ""); + while (numbers.length < 6) { + numbers = "0" + numbers; + } + return "#" + numbers; + } else if (s.startsWith("#")) return s; + else { + // console.log(s, "weird hex"); + return "black"; + } +} + +export function buildPost( + author: Ship, + id: string, + time: number, + s: string, + content: string, +): Poast { + return { + host: author, + author: author, + thread: null, + parent: null, + contents: [{ paragraph: [{ text: s }] }], + read: openLock, + write: openLock, + tags: [], + id, + time, + children: [], + engagement: { reacts: {}, quoted: [], shared: [] }, + json: { origin: "rumors", content }, + }; +} + +// default cursors +export function makeNewestIndex() { + const DA_UNIX_EPOCH = BigInt("170141184475152167957503069145530368000"); + const DA_SECOND = BigInt("18446744073709551616"); + const timeSinceEpoch = (BigInt(Date.now()) * DA_SECOND) / BigInt(1000); + return (DA_UNIX_EPOCH + timeSinceEpoch).toString(); +} +export const startCursor = makeNewestIndex(); +export const endCursor = "0"; + +export function displayCount(c: number): string { + if (c <= 0) return ""; + if (c < 1_000) return `${c}`; + if (c >= 1_000 && c < 1_000_000) return `${Math.round(c / 1_00) / 10}K`; + if (c >= 1_000_000) return `${Math.round(c / 100_000) / 10}M`; + else return ""; +} +export function isWhiteish(hex: string): boolean { + if (hex.indexOf("#") === 0) hex = hex.slice(1); + const r = parseInt(hex.slice(0, 2), 16); + const g = parseInt(hex.slice(2, 4), 16); + const b = parseInt(hex.slice(4, 6), 16); + return r > 200 && g > 200 && b > 200; +} + +export function localISOString(date: Date) { + const offset = new Date().getTimezoneOffset(); + const localts = date.getTime() - offset * 60_000; + return new Date(localts).toISOString().slice(0, 16); +} + +export function goback() { + window.history.back(); +} + +export function groupReacts(reacts: Record): ReactGrouping { + const byReact = Object.entries(reacts).reduce( + (acc: Record, item) => { + const shipList = acc[item[1]]; + if (!shipList) acc[item[1]] = [item[0]]; + else acc[item[1]] = [...shipList, item[0]]; + return acc; + }, + {}, + ); + return Object.entries(byReact) + .reduce((acc: ReactGrouping, item) => { + const pair = { react: item[0], ships: item[1] }; + return [...acc, pair]; + }, []) + .sort((a, b) => b.ships.length - a.ships.length); +} + +export function reverseRecord( + a: Record, +): Record { + return Object.entries(a).reduce((acc: Record, [k, v]) => { + acc[v] = k; + return acc; + }, {}); +} + +export function getColorHex(color: string): string { + if (color.startsWith("0x")) + return `#${padString(stripFuckingDots(color), 6)}`; + else if (color.startsWith("#") && color.length === 7) return color; + else if (color.length === 6) return `#${color}`; + else { + console.log(color, "something weird with this color"); + return "#FFFFFF"; + } +} + +export function stripFuckingDots(hex: string) { + return hex.replace("0x", "").replaceAll(".", ""); +} +export function padString(s: string, size: number) { + if (s.length >= size) return s; + else return padString(`0${s}`, size); +} +export function isDark(hexColor: string): boolean { + const r = parseInt(hexColor.substring(1, 2), 16); + const g = parseInt(hexColor.substring(3, 5), 16); + const b = parseInt(hexColor.substring(5, 7), 16); + + const sr = r / 255; + const sg = g / 255; + const sb = b / 255; + const rSrgb = + sr <= 0.03928 ? sr / 12.92 : Math.pow((sr + 0.055) / 1.055, 2.4); + const gSrgb = + sg <= 0.03928 ? sg / 12.92 : Math.pow((sg + 0.055) / 1.055, 2.4); + const bSrgb = + sb <= 0.03928 ? sb / 12.92 : Math.pow((sb + 0.055) / 1.055, 2.4); + + // Calculate luminance + const luminance = 0.2126 * rSrgb + 0.7152 * gSrgb + 0.0722 * bSrgb; + return luminance < 0.12; +} + +export function checkIfClickedOutside( + e: React.MouseEvent, + el: HTMLElement, + close: any, +) { + e.stopPropagation(); + if (el.contains(e.currentTarget)) close(); +} diff --git a/front/src/main.tsx b/front/src/main.tsx new file mode 100644 index 0000000..5d4a2be --- /dev/null +++ b/front/src/main.tsx @@ -0,0 +1,9 @@ +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import App from "./App.tsx"; + +createRoot(document.getElementById("root")!).render( + + + , +); diff --git a/front/src/pages/Feed.tsx b/front/src/pages/Feed.tsx new file mode 100644 index 0000000..e29033e --- /dev/null +++ b/front/src/pages/Feed.tsx @@ -0,0 +1,104 @@ +// import spinner from "@/assets/icons/spinner.svg"; +import "@/styles/trill.css"; +import UserFeed from "./User"; +import PostList from "@/components/feed/PostList"; +import useLocalState from "@/state/state"; +import { useParams, useLocation } from "wouter"; +import spinner from "@/assets/triangles.svg"; +import { useState } from "react"; +import Composer from "@/components/feed/Composer"; +// import UserFeed from "./User"; +import { P404 } from "@/Router"; +import { useQuery } from "@tanstack/react-query"; +import { isValidPatp } from "urbit-ob"; +import { eventsToFc } from "@/logic/nostril"; + +type FeedType = "global" | "following" | "nostr"; +function Loader() { + // const { api } = useLocalState(); + const params = useParams(); + console.log({ params }); + // const [loc, navigate] = useLocation(); + // console.log({ loc }); + // const our = api!.airlock.ship; + if (params.taip === "global") return ; + if (params.taip === "nostr") return ; + // else if (param === FeedType.Rumors) return ; + // else if (param === FeedType.Home) return ; + else if (isValidPatp(params.taip!)) return ; + else return ; +} +function FeedPage({ t }: { t: FeedType }) { + const [active, setActive] = useState(t); + return ( +
+
+
setActive("global")} + > + Global +
+
setActive("following")} + > + Following +
+
setActive("nostr")} + > + Nostr +
+
+
+ + {active === "global" ? ( + + ) : active === "following" ? ( + + ) : active === "nostr" ? ( + + ) : null} +
+
+ ); +} +// {active === "global" ? ( +// +// ) : active === "following" ? ( +// +// ) : ( +// +// )} + +function Global() { + // const { api } = useLocalState(); + // const { isPending, data, refetch } = useQuery({ + // queryKey: ["globalFeed"], + // queryFn: () => { + // return api!.scryFeed(null, null); + // }, + // }); + // console.log(data, "scry feed data"); + // if (isPending) return ; + // else if ("bucun" in data) return

Error

; + // else return ; + return

Error

; +} +function Nostr() { + const { relays } = useLocalState(); + const feed = eventsToFc(relays); + console.log({ feed }); + const refetch = () => feed; + return ; +} + +export default Loader; +// TODO +type MixFeed = any; + +function Inner({ data, refetch }: { data: MixFeed; refetch: Function }) { + return ; +} diff --git a/front/src/pages/Settings.tsx b/front/src/pages/Settings.tsx new file mode 100644 index 0000000..e0f1da9 --- /dev/null +++ b/front/src/pages/Settings.tsx @@ -0,0 +1,92 @@ +import useLocalState from "@/state/state"; +import type { UserProfile } from "@/types/nostril"; +import { useState } from "react"; + +function Settings() { + const { UISettings, keys, profiles, relays, api } = useLocalState(); + const [newRelay, setNewRelay] = useState(""); + async function saveSetting( + bucket: string, + key: string, + value: string | boolean | number | string[], + ) { + const json = { + "put-entry": { + desk: "trill", + "bucket-key": bucket, + "entry-key": key, + value, + }, + }; + // const res = await poke("settings", "settings-event", json); + // if (res) refetchSettings(); + } + async function removeRelay(url: string) { + console.log({ url }); + } + async function addNewRelay() { + // + // await addnr(newRelay); + } + async function removeProfile(pubkey: string) { + api!.removeKey(pubkey); + } + async function createProfile() { + // + api!.createKey(); + } + + return ( +
+

Settings

+
+ + {keys.map((k) => { + const profile = profiles.get(k); + const profileDiv = !profile ? ( +
+
Pubkey: {k}
+

No profile set

) +
+ ) : ( +
+ {profile.picture && } +
Name: {profile.name}
+
Pubkey: {k}
+
About: {profile.about}
+ +
+ ); + return ( +
+ {profileDiv} +
+ ); + })} +
+ +
+
+
+ + {Object.keys(relays).map((r) => ( + // TODO: add connect button to connect and disc to relay one by one +
+
{r}
+ +
+ ))} +
+ + setNewRelay(e.target.value)} + /> + +
+
+
+ ); +} +export default Settings; diff --git a/front/src/pages/User.tsx b/front/src/pages/User.tsx new file mode 100644 index 0000000..fc727e4 --- /dev/null +++ b/front/src/pages/User.tsx @@ -0,0 +1,18 @@ +// import spinner from "@/assets/icons/spinner.svg"; +import PostList from "@/components/feed/PostList"; +import useLocalState from "@/state/state"; +import type { Ship } from "@/types/urbit"; + +function UserFeed({ p }: { p: Ship }) { + const { api, following } = useLocalState(); + const feed = following.get(api!.airlock.our!); + const refetch = () => feed; + if (p === api!.airlock.our) + return ( +
+ +
+ ); +} + +export default UserFeed; diff --git a/front/src/state/state.ts b/front/src/state/state.ts new file mode 100644 index 0000000..28f3fb2 --- /dev/null +++ b/front/src/state/state.ts @@ -0,0 +1,64 @@ +import type { JSX } from "react"; +import { start } from "@/logic/api"; +import IO from "@/logic/requests/nostril"; +import type { ComposerData } from "@/types/ui"; +import { create } from "zustand"; +import type { UserProfile } from "@/types/nostril"; +import type { Event } from "@/types/nostr"; +import type { FC } from "@/types/trill"; +// TODO handle airlock connection issues +// the SSE pipeline has a "status-update" event FWIW +// type AirlockState = "connecting" | "connected" | "failed"; +export type LocalState = { + isNew: boolean; + api: IO | null; + init: () => Promise; + UISettings: Record; + modal: JSX.Element | null; + setModal: (modal: JSX.Element | null) => void; + composerData: ComposerData | null; + setComposerData: (c: ComposerData | null) => void; + keys: string[]; + relays: Record; + profiles: Map; // pubkey key + following: Map; + followers: string[]; +}; + +const creator = create(); +const useLocalState = creator((set, _get) => ({ + isNew: false, + api: null, + init: async () => { + const airlock = await start(); + const api = new IO(airlock); + console.log({ api }); + await api.subscribeStore((data) => { + console.log("store sub", data); + const { feed, following, relays, profiles, keys } = data; + + const flwing = new Map(Object.entries(following as Record)); + flwing.set(api!.airlock.our!, feed); + set({ + relays, + profiles: new Map(Object.entries(profiles)), + following: flwing, + keys, + }); + }); + set({ api }); + }, + keys: [], + profiles: new Map(), + relays: {}, + following: new Map(), + followers: [], + UISettings: {}, + modal: null, + setModal: (modal) => set({ modal }), + // composer data + composerData: null, + setComposerData: (composerData) => set({ composerData }), +})); + +export default useLocalState; diff --git a/front/src/styles/ThemeProvider.tsx b/front/src/styles/ThemeProvider.tsx new file mode 100644 index 0000000..2cc0ca6 --- /dev/null +++ b/front/src/styles/ThemeProvider.tsx @@ -0,0 +1,302 @@ +import React, { + createContext, + useContext, + useEffect, + useState, + type ReactNode, +} from "react"; + +export type ThemeName = + | "light" + | "dark" + | "sepia" + | "noir" + | "ocean" + | "forest" + | "gruvbox"; + +export interface ThemeColors { + primary: string; + primaryHover: string; + secondary: string; + background: string; + surface: string; + surfaceHover: string; + text: string; + textSecondary: string; + textMuted: string; + border: string; + borderLight: string; + success: string; + warning: string; + error: string; + info: string; + link: string; + linkHover: string; + shadow: string; + overlay: string; +} + +export interface Theme { + name: ThemeName; + colors: ThemeColors; +} + +const themes: Record = { + light: { + name: "light", + colors: { + primary: "#543fd7", + primaryHover: "#4532b8", + secondary: "#f39c12", + background: "#ffffff", + surface: "#f8f9fa", + surfaceHover: "#e9ecef", + text: "#212529", + textSecondary: "#495057", + textMuted: "#6c757d", + border: "#dee2e6", + borderLight: "#e9ecef", + success: "#28a745", + warning: "#ffc107", + error: "#dc3545", + info: "#17a2b8", + link: "#543fd7", + linkHover: "#4532b8", + shadow: "rgba(0, 0, 0, 0.1)", + overlay: "rgba(0, 0, 0, 0.5)", + }, + }, + dark: { + name: "dark", + colors: { + primary: "#7c6ef7", + primaryHover: "#9085f9", + secondary: "#f39c12", + background: "#0d1117", + surface: "#161b22", + surfaceHover: "#21262d", + text: "#c9d1d9", + textSecondary: "#8b949e", + textMuted: "#6e7681", + border: "#30363d", + borderLight: "#21262d", + success: "#3fb950", + warning: "#d29922", + error: "#f85149", + info: "#58a6ff", + link: "#58a6ff", + linkHover: "#79b8ff", + shadow: "rgba(0, 0, 0, 0.3)", + overlay: "rgba(0, 0, 0, 0.7)", + }, + }, + sepia: { + name: "sepia", + colors: { + primary: "#8b4513", + primaryHover: "#6b3410", + secondary: "#d2691e", + background: "#f4e8d0", + surface: "#ede0c8", + surfaceHover: "#e6d9c0", + text: "#3e2723", + textSecondary: "#5d4037", + textMuted: "#6d4c41", + border: "#d7ccc8", + borderLight: "#e0d5d0", + success: "#689f38", + warning: "#ff9800", + error: "#d32f2f", + info: "#0288d1", + link: "#8b4513", + linkHover: "#6b3410", + shadow: "rgba(62, 39, 35, 0.1)", + overlay: "rgba(62, 39, 35, 0.5)", + }, + }, + noir: { + name: "noir", + colors: { + primary: "#ffffff", + primaryHover: "#e0e0e0", + secondary: "#808080", + background: "#000000", + surface: "#0a0a0a", + surfaceHover: "#1a1a1a", + text: "#ffffff", + textSecondary: "#b0b0b0", + textMuted: "#808080", + border: "#333333", + borderLight: "#1a1a1a", + success: "#4caf50", + warning: "#ff9800", + error: "#f44336", + info: "#2196f3", + link: "#b0b0b0", + linkHover: "#ffffff", + shadow: "rgba(255, 255, 255, 0.1)", + overlay: "rgba(0, 0, 0, 0.9)", + }, + }, + ocean: { + name: "ocean", + colors: { + primary: "#006994", + primaryHover: "#005577", + secondary: "#00acc1", + background: "#e1f5fe", + surface: "#b3e5fc", + surfaceHover: "#81d4fa", + text: "#01579b", + textSecondary: "#0277bd", + textMuted: "#4fc3f7", + border: "#81d4fa", + borderLight: "#b3e5fc", + success: "#00c853", + warning: "#ffab00", + error: "#d50000", + info: "#00b0ff", + link: "#0277bd", + linkHover: "#01579b", + shadow: "rgba(1, 87, 155, 0.1)", + overlay: "rgba(1, 87, 155, 0.5)", + }, + }, + forest: { + name: "forest", + colors: { + primary: "#2e7d32", + primaryHover: "#1b5e20", + secondary: "#689f38", + background: "#f1f8e9", + surface: "#dcedc8", + surfaceHover: "#c5e1a5", + text: "#1b5e20", + textSecondary: "#33691e", + textMuted: "#558b2f", + border: "#aed581", + borderLight: "#c5e1a5", + success: "#4caf50", + warning: "#ff9800", + error: "#f44336", + info: "#03a9f4", + link: "#388e3c", + linkHover: "#2e7d32", + shadow: "rgba(27, 94, 32, 0.1)", + overlay: "rgba(27, 94, 32, 0.5)", + }, + }, + gruvbox: { + name: "gruvbox", + colors: { + primary: "#fe8019", + primaryHover: "#d65d0e", + secondary: "#fabd2f", + background: "#282828", + surface: "#3c3836", + surfaceHover: "#504945", + text: "#ebdbb2", + textSecondary: "#d5c4a1", + textMuted: "#bdae93", + border: "#665c54", + borderLight: "#504945", + success: "#b8bb26", + warning: "#fabd2f", + error: "#fb4934", + info: "#83a598", + link: "#8ec07c", + linkHover: "#b8bb26", + shadow: "rgba(0, 0, 0, 0.3)", + overlay: "rgba(40, 40, 40, 0.8)", + }, + }, +}; + +interface ThemeContextType { + theme: Theme; + themeName: ThemeName; + setTheme: (name: ThemeName) => void; + availableThemes: ThemeName[]; +} + +const ThemeContext = createContext(undefined); + +interface ThemeProviderProps { + children: ReactNode; + defaultTheme?: ThemeName; +} + +export const ThemeProvider: React.FC = ({ + children, + defaultTheme = "light", +}) => { + const [themeName, setThemeName] = useState(() => { + const savedTheme = localStorage.getItem("theme") as ThemeName; + if (savedTheme && themes[savedTheme]) { + return savedTheme; + } + + if ( + window.matchMedia && + window.matchMedia("(prefers-color-scheme: dark)").matches + ) { + return "dark"; + } + + return defaultTheme; + }); + + const theme = themes[themeName]; + + useEffect(() => { + const root = document.documentElement; + + root.setAttribute("data-theme", themeName); + + Object.entries(theme.colors).forEach(([key, value]) => { + const cssVarName = `--color-${key.replace(/([A-Z])/g, "-$1").toLowerCase()}`; + root.style.setProperty(cssVarName, value); + }); + + localStorage.setItem("theme", themeName); + }, [themeName, theme]); + + useEffect(() => { + const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)"); + const handleChange = (e: MediaQueryListEvent) => { + const savedTheme = localStorage.getItem("theme"); + if (!savedTheme) { + setThemeName(e.matches ? "dark" : "light"); + } + }; + + mediaQuery.addEventListener("change", handleChange); + return () => mediaQuery.removeEventListener("change", handleChange); + }, []); + + const setTheme = (name: ThemeName) => { + if (themes[name]) { + setThemeName(name); + } + }; + + const value: ThemeContextType = { + theme, + themeName, + setTheme, + availableThemes: Object.keys(themes) as ThemeName[], + }; + + return ( + {children} + ); +}; + +export const useTheme = (): ThemeContextType => { + const context = useContext(ThemeContext); + if (context === undefined) { + throw new Error("useTheme must be used within a ThemeProvider"); + } + return context; +}; diff --git a/front/src/styles/ThemeSwitcher.css b/front/src/styles/ThemeSwitcher.css new file mode 100644 index 0000000..518a00d --- /dev/null +++ b/front/src/styles/ThemeSwitcher.css @@ -0,0 +1,249 @@ +/* Theme Switcher Styles */ + +/* Compact variant */ +.theme-switcher-compact { + display: inline-flex; + align-items: center; + gap: var(--spacing-sm); + padding: var(--spacing-sm) var(--spacing-md); + background-color: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--radius-full); + cursor: pointer; + transition: all var(--transition-fast); + font-size: var(--font-md); + color: var(--color-text); +} + +.theme-switcher-compact:hover { + background-color: var(--color-surface-hover); + border-color: var(--color-primary); + transform: scale(1.05); +} + +.theme-switcher-compact:active { + transform: scale(0.98); +} + +.theme-switcher-compact .theme-icon { + font-size: 1.2em; + display: flex; + align-items: center; +} + +.theme-switcher-compact .theme-label { + font-weight: var(--font-medium); +} + +/* Buttons variant */ +.theme-switcher-buttons { + display: flex; + align-items: center; + gap: var(--spacing-md); +} + +.theme-switcher-buttons .theme-label { + color: var(--color-text-secondary); + font-weight: var(--font-medium); +} + +.theme-buttons-group { + display: flex; + gap: var(--spacing-xs); + background-color: var(--color-surface); + padding: var(--spacing-xs); + border-radius: var(--radius-lg); + border: 1px solid var(--color-border); +} + +.theme-button { + display: flex; + align-items: center; + gap: var(--spacing-xs); + padding: var(--spacing-xs) var(--spacing-sm); + background-color: transparent; + border: 1px solid transparent; + border-radius: var(--radius-md); + cursor: pointer; + transition: all var(--transition-fast); + color: var(--color-text-secondary); + font-size: var(--font-sm); +} + +.theme-button:hover { + background-color: var(--color-surface-hover); + color: var(--color-text); +} + +.theme-button.active { + background-color: var(--color-primary); + color: white; + border-color: var(--color-primary); +} + +.theme-button .theme-icon { + font-size: 1.1em; +} + +.theme-button .theme-name { + display: none; +} + +@media (min-width: 768px) { + .theme-button .theme-name { + display: inline; + } +} + +/* Dropdown variant */ +.theme-switcher-dropdown { + position: relative; + display: inline-block; +} + +.theme-dropdown-toggle { + display: flex; + align-items: center; + gap: var(--spacing-sm); + padding: var(--spacing-sm) var(--spacing-md); + background-color: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + cursor: pointer; + transition: all var(--transition-fast); + color: var(--color-text); + font-size: var(--font-md); +} + +.theme-dropdown-toggle:hover { + background-color: var(--color-surface-hover); + border-color: var(--color-primary); +} + +.theme-dropdown-toggle .theme-icon { + font-size: 1.2em; +} + +.theme-dropdown-toggle .theme-label { + font-weight: var(--font-medium); +} + +.theme-dropdown-toggle .dropdown-arrow { + font-size: 0.7em; + margin-left: var(--spacing-xs); + transition: transform var(--transition-fast); + color: var(--color-text-muted); +} + +.theme-dropdown-toggle[aria-expanded="true"] .dropdown-arrow { + transform: rotate(180deg); +} + +.theme-dropdown-backdrop { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: var(--z-dropdown); + background-color: transparent; +} + +.theme-dropdown-menu { + position: absolute; + top: calc(100% + var(--spacing-xs)); + right: 0; + min-width: 180px; + background-color: var(--color-background); + border: 1px solid var(--color-border); + border-radius: var(--radius-lg); + box-shadow: 0 4px 12px var(--color-shadow); + z-index: calc(var(--z-dropdown) + 1); + padding: var(--spacing-xs); + animation: dropdownSlide 0.2s ease-out; +} + +@keyframes dropdownSlide { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.theme-dropdown-item { + display: flex; + align-items: center; + gap: var(--spacing-sm); + width: 100%; + padding: var(--spacing-sm) var(--spacing-md); + background-color: transparent; + border: none; + border-radius: var(--radius-md); + cursor: pointer; + transition: all var(--transition-fast); + color: var(--color-text); + font-size: var(--font-md); + text-align: left; +} + +.theme-dropdown-item:hover { + background-color: var(--color-surface); +} + +.theme-dropdown-item.active { + background-color: var(--color-surface); + color: var(--color-primary); + font-weight: var(--font-medium); +} + +.theme-dropdown-item .theme-icon { + font-size: 1.2em; + width: 1.5em; + text-align: center; +} + +.theme-dropdown-item .theme-name { + flex: 1; +} + +.theme-dropdown-item .checkmark { + color: var(--color-success); + font-weight: var(--font-bold); +} + +/* Accessibility */ +.theme-switcher-compact:focus, +.theme-button:focus, +.theme-dropdown-toggle:focus, +.theme-dropdown-item:focus { + outline: 2px solid var(--color-primary); + outline-offset: 2px; +} + +/* Dark theme adjustments */ +[data-theme="dark"] .theme-dropdown-menu { + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5); +} + +/* Reduced motion */ +@media (prefers-reduced-motion: reduce) { + .theme-switcher-compact, + .theme-button, + .theme-dropdown-toggle, + .theme-dropdown-item, + .dropdown-arrow { + transition: none; + } + + .theme-dropdown-menu { + animation: none; + } + + .theme-switcher-compact:hover { + transform: none; + } +} \ No newline at end of file diff --git a/front/src/styles/ThemeSwitcher.tsx b/front/src/styles/ThemeSwitcher.tsx new file mode 100644 index 0000000..425bed9 --- /dev/null +++ b/front/src/styles/ThemeSwitcher.tsx @@ -0,0 +1,131 @@ +import React, { useState } from "react"; +import { useTheme, type ThemeName } from "../styles/ThemeProvider"; +import "./ThemeSwitcher.css"; + +interface ThemeSwitcherProps { + variant?: "dropdown" | "buttons" | "compact"; + showLabel?: boolean; +} + +const themeIcons: Record = { + light: "☀️", + dark: "🌙", + sepia: "📜", + noir: "⚫", + ocean: "🌊", + forest: "🌲", + gruvbox: "🍂", +}; + +const themeLabels: Record = { + light: "Light", + dark: "Dark", + sepia: "Sepia", + noir: "Noir", + ocean: "Ocean", + forest: "Forest", + gruvbox: "Gruvbox", +}; + +export const ThemeSwitcher: React.FC = ({ + variant = "dropdown", + showLabel = true, +}) => { + const { themeName, setTheme, availableThemes } = useTheme(); + const [isOpen, setIsOpen] = useState(false); + + const handleThemeChange = (theme: ThemeName) => { + setTheme(theme); + setIsOpen(false); + }; + + const cycleTheme = () => { + const currentIndex = availableThemes.indexOf(themeName); + const nextIndex = (currentIndex + 1) % availableThemes.length; + setTheme(availableThemes[nextIndex]); + }; + + if (variant === "compact") { + return ( + + ); + } + + if (variant === "buttons") { + return ( +
+ {showLabel && Theme:} +
+ {availableThemes.map((theme) => ( + + ))} +
+
+ ); + } + + // Default dropdown variant + return ( +
+ + + {isOpen && ( + <> +
setIsOpen(false)} + aria-hidden="true" + /> +
+ {availableThemes.map((theme) => ( + + ))} +
+ + )} +
+ ); +}; diff --git a/front/src/styles/styles.css b/front/src/styles/styles.css new file mode 100644 index 0000000..c2b05d6 --- /dev/null +++ b/front/src/styles/styles.css @@ -0,0 +1,438 @@ +/* assets */ +/* fonts */ +@font-face { + font-family: "Inter"; + src: url(/fonts/Inter/Inter-VariableFont_opsz,wght.ttf); + font-weight: 100 900; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: "Inter"; + src: url(/fonts/Inter/Inter-Italic-VariableFont_opsz,wght.ttf); + font-weight: 100 900; + font-style: italic; + font-display: swap; +} + +@font-face { + font-family: "Source Code Pro"; + src: url(/fonts/Source_Code_Pro/SourceCodePro-VariableFont_wght.ttf); + font-weight: 100 900; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: "Source Code Pro"; + src: url(/fonts/Source_Code_Pro/SourceCodePro-Italic-VariableFont_wght.ttf); + font-weight: 100 900; + font-style: italic; + font-display: swap; +} + +/* tailwindy */ + +.grow { + flex-grow: 1; +} + +button { + cursor: pointer; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", + monospace; +} + +t .red { + background-color: rgb(200, 0, 0, 0.9); +} + +.tc, +.ct { + text-align: center; +} + +.cb { + margin: auto; +} + +.xc { + position: absolute; + left: 50%; + transform: translateX(-50%); +} + +.hidden { + display: none; +} + +.x-center { + margin: auto; + text-align: center; + display: block; +} + +.flex { + display: flex; +} + +.f1 { + display: flex; + justify-content: space-between; + align-items: center; +} + +.flex-align { + display: flex; + gap: 1rem; + align-items: center; +} + +.noscroll { + overflow: hidden; +} + +.scroll-y { + overflow-y: scroll; +} + +.cp { + cursor: pointer; +} + +.m0 { + margin: 0; +} + +.mb { + margin: 0 0 1rem 0; +} + +.mt { + margin-top: 1rem; +} + +.mr { + margin-right: 0.5rem; +} + +.s-50 { + width: 50px; +} + +.s-100 { + width: 100px; +} + +.border { + border: 1px solid var(--text-color); +} + +/* styles */ + +/* common */ +html { + box-sizing: border-box; + color: var(--text-color); + background-color: var(--background-color); +} + +html, +body, +#root, +#mobile-ui { + height: 100%; + width: 100vw; + overflow: hidden; + /* no scrolling!!!*/ +} + +*, +*:before, +*:after { + box-sizing: inherit; +} + +body { + margin: 0; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + background-color: var(--color-background); + color: var(--color-text); + line-height: 1.6; + transition: background-color var(--transition-normal), color var(--transition-normal); +} + +/* Typography */ +h1, +h2, +h3, +h4, +h5, +h6 { + margin-bottom: var(--spacing-md); + font-weight: var(--font-semibold); + line-height: 1.2; + color: var(--color-text); +} + +#root { + margin: 1rem 2rem; + height: 100%; + overflow-y: auto; + font-family: "Inter"; + + + display: flex; + + & #left-menu { + margin-right: 1rem; + + #logo { + display: flex; + gap: 0.3rem; + + & img { + width: 48px; + height: 48px; + } + } + + & .opt { + cursor: pointer; + display: flex; + gap: 1rem; + margin: 1rem 0; + + & img { + width: 24px; + height: 24px; + } + } + } + + & main { + width: 726px; + margin: auto; + height: 100vh; + + & #top-tabs { + display: flex; + gap: 2rem; + justify-content: center; + + & div { + cursor: pointer; + } + + & .active { + font-weight: 700; + border-bottom: 3px solid var(--color-text); + } + } + + & #feed-proper { + margin-top: 1rem; + border: 1px solid grey; + border-radius: 0.75rem; + + & #composer { + padding: 10px; + display: flex; + gap: 0.5rem; + + & .sigil { + width: 48px; + height: 48px; + + & img { + width: inherit; + } + } + + & input { + background-color: transparent; + color: var(--color-text); + flex-grow: 1; + border: none; + outline: none; + } + } + } + + & .trill-post, + & .twatter-post { + border-top: 1px solid grey; + + & .left { + margin-right: 10px; + width: unset; + + & .sigil { + width: 48px; + height: 48px; + } + } + + & header { + align-items: center; + justify-content: left; + + & .author { + flex: unset; + gap: 0; + + & .name { + display: flex; + align-items: center; + + & .p { + font-family: "Source Code Pro"; + } + } + } + + & .date { + color: grey; + } + + } + + & footer { + justify-content: left; + margin: unset; + + & .icon { + margin: 0; + align-items: center; + gap: 0.2rem; + width: 64px; + + & img { + height: 18px; + } + + & .react-img { + height: 24px; + } + + & .react-icon { + font-size: 20px; + } + + & span { + margin-right: unset; + text-align: left; + font-size: 14px; + line-height: 1rem; + color: grey; + width: unset; + } + } + + & .menu-icon { + margin-left: auto; + } + } + } + + + & .user-contact { + & .contact-cover { + margin-bottom: -40px; + + & img { + width: 100%; + height: 100%; + object-fit: cover; + } + } + + & .contact-name { + display: flex; + align-items: center; + gap: 0.5rem; + } + + & .contact-username { + margin-top: 1rem; + font-family: "Source Code Pro"; + font-weight: 400; + } + + & button { + width: unset; + margin: unset; + height: unset; + } + } + } + + & button { + font-size: 0.9rem; + font-weight: 700; + line-height: 1rem; + border: none; + border-radius: 2rem; + padding: 0.5rem 2rem; + } + + & .sigil, + & .sigil svg { + border-radius: 0.5rem; + } +} + +#big-button { + position: absolute; + right: 2rem; + bottom: 2rem; + font-size: 1.5rem; + font-weight: bold; + cursor: pointer; + text-align: center; + line-height: 3rem; + width: 3rem; + height: 3rem; + border-radius: 50%; + z-index: 100; +} + +/* modal */ +#modal-background { + height: 100vh; + width: 100vw; + background-color: rgb(0, 0, 0, 0.9); + position: fixed; + top: 0; + left: 0; + z-index: 100; +} + +#modal { + position: fixed; + top: 50%; + left: 50%; + width: 80%; + z-index: 101; + transform: translate(-50%, -50%); + background-color: var(--background-color); + padding: 1rem; + max-height: 80%; +} + +.modal-buttons { + display: flex; + justify-content: space-around; +} + +::-webkit-scrollbar { + display: none; +} \ No newline at end of file diff --git a/front/src/styles/trill.css b/front/src/styles/trill.css new file mode 100644 index 0000000..5687c7a --- /dev/null +++ b/front/src/styles/trill.css @@ -0,0 +1,612 @@ +#not-found { + margin: auto; + padding: 2rem; + text-align: center; +} + +#not-found button { + height: 1.5rem; + margin: auto; +} + +#timeline { + overflow-y: auto; + overflow-x: hidden; +} + +.timeline-post { + width: 100%; + border-top: 1px solid var(--text-color); +} + +.trill-post { + display: flex; + padding: 0.5rem; + /* min-height: 150px; */ +} + +.trill-post .author { + flex: 1 0 auto; +} + +.trill-reply-thread { + border-top: 1px solid var(--text-color); +} + +.trill-post:first-child { + border-top: none; +} + +.trill-post:last-child { + border-bottom: 1px solid var(--text-color); +} + +.trill-post .left { + width: 8%; + margin-right: 1rem; +} + +.trill-post .sigil { + height: 42px; + width: 42px; +} + +.trill-post .right { + width: 90%; +} + +.trill-post header { + display: flex; + justify-content: space-between; +} + +.trill-post header p { + margin: 0 0.3rem; +} + +.trill-post .nick { + font-weight: 700; +} + +.trill-post header .p { + font-family: "Courier New", Courier, monospace; + font-weight: 100; + font-size: 1rem; +} + +.trill-post .p-only { + margin: 0.7rem 0.3rem; + font-weight: 700; +} + +.trill-post .p { + /* margin-top: -5px; */ +} + +.trill-post a { + text-decoration: 0; + color: var(--text-color); +} + +.trill-post blockquote { + border-left: 2px solid grey; + margin-left: 0; + padding-left: 0.5rem; + opacity: 70%; +} + +.trill-post .body { + margin: 1rem; + margin-left: 0; +} + +.trill-post-body p span { + /* margin: 0 3px; */ +} + +.trill-post pre { + font-family: "Courier New", Courier, monospace; + background-color: rgb(200, 200, 200, 0.5); + padding: 0.2rem; + max-width: 90%; + border: 1px solid var(--text-color); + overflow: scroll; +} + +.trill-post .quote-in-post .body { + margin: 0; +} + +.quote-in-post svg { + margin-right: 0.5rem; +} + +.trill-post .body-text { + /* font-family: Arial, Helvetica, sans-serif; */ + margin: 0.3rem 0 1rem 0; + word-break: break-word; +} + +.trill-post .trill-post-paragraph { + margin-block-start: 1em; + margin-block-end: 1em; +} + +.trill-post .body-text a { + text-decoration: underline; +} + +.trill-post .token { + margin: 0 0.5rem; +} + +.trill-post .date { + float: right; +} + +.trill-post .nav { + display: flex; +} + +.trill-post .chevron { + width: 1.5rem; + height: 1.5rem; +} + +.body-media { + width: 100%; + max-height: 520px; + text-align: center; + /* images being inline */ +} + +.body-media img { + margin: 1px 3px; +} + +.body-img-1-of-1 { + max-width: 100%; + max-height: inherit; + margin: auto !important; +} + +.body-img-1-of-2 { + max-width: 48.5%; + max-height: inherit; +} + +.body-img-1-of-3 { + max-width: 48.5%; +} + +.body-img-1-of-4 { + max-width: 48.5%; +} + +.body-img-1-of-5 { + max-width: 31%; +} + +.body-img-1-of-6 { + max-width: 31%; +} + +.body-img-1-of-7 { + max-width: 31%; +} + +.body-img-1-of-8 { + max-width: 31%; +} + +.body-img-1-of-9 { + max-width: 31%; +} + +/* quotes */ + +.quote-in-post { + margin-top: 1rem; + padding: 0.5rem; + border: 1px solid grey; + border-radius: 0.5rem; + cursor: pointer; +} + +.quote-in-post header { + display: flex; +} + +.mention { + font-family: "Courier New", Courier, monospace; + font-weight: 700; +} + +.mention:hover { + cursor: pointer; + text-decoration: underline; +} + +.bad-quote { + border: 1px solid var(--text-color); + padding: 7px; + border-radius: 0.5rem; +} + +/* post-cards */ +.trill-post-card { + position: relative; + border-radius: 0.3rem; + /* margin: 1rem 0 0 -8%; */ + margin: 0.5rem 0; +} + +.trill-post-card-logo { + position: absolute; + width: 25px; + height: 25px; + top: -17px; + left: -17px; +} + +#post-menu { + position: absolute; + top: 0; + right: 50px; + z-index: 99; +} + +.deleted-post { + text-align: center; + border: 1px solid var(--text-color); + padding: 0.4rem; +} + +#post-menu p { + background-color: var(--background-color); + margin: 0; + padding: 0.5rem; + cursor: pointer; + border: 1px solid var(--text-color); + height: 40px; +} + +#post-menu p:hover { + /* background-color: var(--highlighted-grey); */ +} + +/* threads */ +.trill-reply-thread { + margin-top: 1rem; +} + +#replies>.trill-post:first-child { + border-top: 1px solid black; +} + +/* footer */ + +.footer-wrapper { + position: relative; + /* transform: rotate(0deg); */ + /* the dummy transform enforces position fixed inheritance */ +} + +.post-footer footer { + display: flex; + margin-left: -20px; + height: 24px; + justify-content: space-between; +} + +footer .icon { + cursor: pointer; + margin: 0 0.2rem; + display: flex; + /* min-width: 64px; */ +} + +footer #menu-icon { + width: 32px !important; + /* margin-left: 20px; */ +} + +.post-footer footer .icon img { + display: block; + width: 24px; + height: 24px; +} + +footer .icon span { + display: block; + width: 30px; + text-align: right; + padding-top: 0.2rem; + margin-right: 0.4rem; +} + +footer .icon span:hover { + text-decoration: underline; +} + +.react-icon { + font-size: 26px; + margin: -10px 0 0 0 !important; + padding: 0; + padding-top: 0 !important; +} + +#react-list { + display: flex; + flex-wrap: wrap; +} + +#react-list img { + margin: 3px; + width: 50px; + height: 50px; + cursor: pointer; + border: 1px solid transparent; +} + +#react-list span { + width: 50px; + height: 50px; + font-size: 38px; + margin: 3px; + cursor: pointer; + border: 1px solid transparent; +} + +#react-list span:hover, +#react-list img:hover { + border: 1px solid var(--text-color); +} + +#menu-background { + position: fixed; + top: 0; + left: 0; + opacity: 0; + height: 100vh; + width: 100vw; +} + +/* contact */ + +.contact-cover { + height: 150px; + max-width: 100vw; + margin-bottom: -50px; +} + +#contact-proper { + padding: 1rem; +} + +#contact-proper .row { + display: flex; +} + +.contact-avatar { + width: 6rem; + height: 6rem; +} + +.contact-name { + margin-top: 1rem; + margin-bottom: 0.5rem; + margin-left: 0.3rem; + font-weight: 700; + font-size: 1.1rem; +} + +.contact-username { + margin-top: -10px; +} + +#contact-proper .buttons { + margin-top: 2rem; + margin-left: auto; +} + +#contact-proper .buttons button { + width: 5rem; + margin-bottom: 5px; + height: 1.5rem; +} + +#contact-proper .p { + font-family: "Courier New", Courier, monospace; +} + +#contact-proper .p-only { + margin-top: 1rem; +} + +.bio-row { + display: flex; + align-items: center; +} + +.stats-row { + display: flex; + justify-content: center; +} + +.stats-icon { + margin: 0 2px; +} + +.stats-row p { + text-align: center; + font-size: 1.3rem; + margin: -5px 0 0 0; +} + +.stats-row img { + width: 32px; +} + +.locked-notice, +.suspended-notice { + text-align: center; +} + +.cover-placeholder { + height: 150px; + background-color: rgb(125, 125, 125, 0.5); +} + +#stats-modal .trill-post { + border-bottom: 1px solid var(--text-color) !important; +} + +#stats-modal { + height: 80vh; +} + +#stats-modal #engagement { + min-height: 40%; + max-height: 40%; + overflow-y: scroll; +} + +#stats-modal .trill-post { + max-height: 50%; + overflow-y: scroll; +} + +.btw { + display: flex; + justify-content: space-between; + align-items: center; +} + +#stats-modal .react-stat img { + width: 32px; + height: 32px; +} + +#stats-modal .react-stat react-icon { + width: 32px; + height: 32px; +} + +#stats-modal #engagement .nickname { + font-size: 1rem; +} + +#stats-modal #engagement .p { + font-size: 0.9rem; +} + +#stats-modal .tab h4 { + font-weight: 100; +} + +#stats-modal .tab.active-tab h4 { + font-weight: 700; +} + +/* .not-found { + border: 1px solid var(--text-color); + border-radius: 1rem; + padding: 0.5rem; +} */ + +/* refs */ +.reference {} + +/* polls */ +.trill-poll { + /* border: 1px solid var(--text-color); */ + border-radius: 1rem; + padding: 0.5rem; + position: relative; + background: linear-gradient(90deg, + rgba(255, 255, 168, 0.4) 0%, + /* Lighter yellow */ + rgba(255, 233, 150, 0.5) 52%, + /* Mid-tone gold */ + rgba(255, 209, 0, 0.4) 100% + /* Deeper gold */ + ); +} + +.trill-poll .poll-option { + height: 2rem; + align-items: center; + text-align: center; + border: 1px solid var(--text-color); + border-radius: 0.7rem; + margin: 1rem; + position: relative; + outline: 3px solid transparent; +} + +.trill-poll .my-vote:hover { + /* cursor:not-allowed */ +} + +.trill-poll .poll-option:hover { + opacity: 50%; + outline-color: var(--text-color); +} + +.trill-poll .poll-option p { + padding: 0 0.5rem; + margin: 0; + line-height: 2rem; +} + +.trill-poll .poll-option-stats { + height: 2rem; + position: relative; +} + +.trill-poll .poll-option-bar { + height: 100%; + position: absolute; + background-color: rgb(100, 100, 100, 0.3); + border-radius: 0.7rem; +} + +.trill-poll .my-vote { + border: 3px solid var(--text-color); + border-right: 4px solid var(--text-color); +} + +.trill-poll .bottom-row { + opacity: 60%; +} + +.youtube-thumbnail { + width: 70%; + margin: 0.7rem auto; +} + +.cursor-button { + width: 100%; + padding: 1rem; + border-top: 1px solid var(--text-color); +} + +.cursor-button button { + display: block; + margin: auto; + padding: 0.5rem; +} + +.rumor-quote img { + width: 50px; + margin-right: 1rem; +} + +#trill-thread { + flex-grow: 1; + height: 100%; + overflow-y: auto; + + +} \ No newline at end of file diff --git a/front/src/types/nostr.ts b/front/src/types/nostr.ts new file mode 100644 index 0000000..0ccfaf3 --- /dev/null +++ b/front/src/types/nostr.ts @@ -0,0 +1,11 @@ +export type Event = { + id: string; // hex, no 0x, 32bytes + pubkey: string; // "" + sig: string; // "", 64 bytes + created_at: number; + kind: number; + tags: Tag[]; + content: string; +}; + +export type Tag = any[]; diff --git a/front/src/types/nostril.ts b/front/src/types/nostril.ts new file mode 100644 index 0000000..65a6194 --- /dev/null +++ b/front/src/types/nostril.ts @@ -0,0 +1,6 @@ +export type UserProfile = { + name: string; + picture: string; // URL + about: string; + other: Record; +}; diff --git a/front/src/types/trill.ts b/front/src/types/trill.ts new file mode 100644 index 0000000..e0936ad --- /dev/null +++ b/front/src/types/trill.ts @@ -0,0 +1,420 @@ +import type { Ship } from "./urbit"; + +export type SortugRef = { + type: string; // could call it app... anyway + ship: Ship; + path: string; // `/${string}` +}; + +export type PostID = string; // +export type ID = string; // +export interface PID { + ship: Ship; + id: ID; +} + +export type TrillNode = Poast | FullNode; +export type FullFeed = Record; +export type FlatFeed = Record; + +export interface Engagement { + reacts: ReactMap; + quoted: Array<{ pid: PID }>; + shared: Array<{ pid: PID }>; +} +export type ReactMap = Record; +export interface SentPoast { + host: Ship; + author: Ship; + thread: ID | null; + parent: ID | null; + contents: string; + read: Lock; + write: Lock; + tags: string[]; +} +export type Poast = { + host: Ship; + author: Ship; + thread: ID | null; + parent: ID | null; + read: Lock; + write: Lock; + tags: string[]; + contents: Content; + id: string; + time: number; // not in the backend + children: ID[]; + engagement: Engagement; + tlonRumor?: boolean; + json?: { origin: ExternalApp; content: string }; // for rumor quoting +}; +export type FullNode = Omit & { + children: FullFeed; + prov?: boolean; +}; +export type Content = Block[]; +export type Block = + | Paragraph + | Blockquote + | Heading + | ListBlock + | Codeblock + | Eval + | Media + | Reference + | ExternalContent; + +export type Paragraph = { paragraph: Inline[] }; +export type Blockquote = { blockquote: Inline[] }; +export type Heading = { heading: { text: string; num: number } }; +export type Codeblock = { codeblock: { code: string; lang: string } }; +export type Eval = { hoon: string }; +export type ListBlock = { list: { ordered: boolean; text: Inline[] } }; +export type Media = { media: PostImages | PostVideo | PostAudio }; +export type PostImages = { images: string[] }; +export type PostVideo = { video: string }; +export type PostAudio = { audio: string }; +export type Reference = { ref: { type: string; ship: Ship; path: string } }; + +export type Inline = + | TextInline + | Italic + | Bold + | Strike + | Underline + | Superscript + | Subscript + | Mention + | Codespan + | LinkInline + | Break; +export type TextInline = { text: string }; +export type Italic = { italic: string }; +export type Bold = { bold: string }; +export type Strike = { strike: string }; +export type Underline = { underline: string }; +export type Superscript = { sup: string }; +export type Subscript = { sub: string }; +export type Mention = { ship: Ship }; +// TODO! export type Da = {date: number} +export type Codespan = { codespan: string }; +export type LinkInline = { link: { href: string; show: string } }; +export type Break = { break: null }; + +export type ExternalContent = { + json: { + origin: ExternalApp; + content: string; + }; +}; +export type ExternalApp = "twatter" | "insta" | "anon" | "rumors"; +export interface TwatterReference { + json: { + origin: "twatter"; + content: string; + }; +} +// interface CodeContent { +// code: { +// expression: string; +// output: string[][]; +// }; +// } +// Notifications +export interface Notifications { + engagement: EngagementNotification[]; + unread: Record; +} +export type Notification = + | EngagementNotification + | FollowNotification + | UnfollowNotification; +export type EngagementNotification = + | ReactNotification + | ReplyNotification + | QuoteNotification + | RepostNotification + | MentionNotification; +export type NotificationData = { ship: Ship; time: number }; +export interface FollowNotification { + follow: NotificationData; +} +export interface UnfollowNotification { + unfollow: NotificationData; +} +export interface ReactNotification { + react: { + pid: PID; + react: string; + } & NotificationData; +} +export interface ReplyNotification { + reply: { + ab: PID; + ad: PID; + } & NotificationData; +} +export interface QuoteNotification { + quote: { + ab: PID; + ad: PID; + } & NotificationData; +} +export interface RepostNotification { + share: { + ab: PID; + ad: PID; + } & NotificationData; +} +export interface MentionNotification { + mention: { + pid: PID; + } & NotificationData; +} +export interface UnreadDisplay { + [s: Ship]: string[]; +} + +// data fetching +export type MixFeedScry = MixFeed | { bucun: string }; + +export type Cursor = string | null; +export type FC = { + feed: FlatFeed; + start: Cursor; + end: Cursor; +}; +export type MixFeed = { + mix: { + name: string; + fc: FC; + }; +}; +export type PoastScry = { post: Poast } | Bucun | NotFollowScry; +export type Bucun = { bucun: PID }; +// TODO bucun no-node come on +export type UserFeedScry = UserScry | NotFollowScry; +export type NotFollowScry = { bugen: Ship }; +export interface UserScry { + feed: { + ship: Ship; + fc: FC; + }; +} + +export type FullNodeScry = + | { fpost: FullNode } + | { "no-node": { ship: Ship; id: ID } }; + +// Facts +export type PostFact = { + post: ThreadFact | GossipFact; +}; +export type ThreadFact = { thread: FullNode }; +export type GossipFact = { gossip: { post: FullNode; feeds: string[] } }; + +export type PullFact = PeekFact | BegFact; +export type PeekFact = { peek: any }; +export type BegFact = { beg: any }; +export type HarkFact = any; +export type ListsFact = any; + +export type TrillProfile = {}; + +export type TrillPostPermisssion = + | "everyone" + | "planets" + | "followers" + | "pals" + | "tag"; + +// Lists + +export type List = { + name: string; + symbol: string; // @tas + public: boolean; + desc: string; + members: ListEntry[]; + icon: string; + cover: string; +}; +export type ListEntry = { + service: "trill" | "twatter" | "twitter"; + username: string; +}; + +export type Lock = { + rank: { caveats: Rank[]; locked: boolean; public: boolean }; + luk: { caveats: Ship[]; locked: boolean; public: boolean }; + ship: { caveats: Ship[]; locked: boolean; public: boolean }; + tags: { caveats: string[]; locked: boolean; public: boolean }; + custom: { fn: null; public: boolean }; +}; +export type Rank = "czar" | "king" | "duke" | "earl" | "pawn"; +// Fetch return types +export type PushState = { + followers: Ship[]; + gate: { + lock: Lock; + begs: Ship[]; + postBegs: PID[]; + mute: Lock; + backlog: number; + }; +}; + +export type PullState = { + begs: Ship[]; + postBegs: PID[]; + following: Ship[]; +}; +export type TrillSearchResponse = { + search: { + query: string; + fc: FC; + }; +}; +export type ListsResponse = { + lists: List[]; +}; +export type MetaPeek = { + posts: number; + inc: Ship[]; + out: Ship[]; + lock: Lock; + ship: Ship; +}; +export type NodePeek = {}; +export type FeedPeek = { + ship: Ship; + feed: FlatFeed; +}; +export interface FollowAttempt { + ship: Ship; + timestamp: number; +} +export interface Key { + ship: Ship; + name: string; +} +// pals stuff +// TODO +// export interface SocialData { +// groups: any | null; +// clubs: any | null; +// lists: List[]; +// pals: Pals | null; +// contacts: Contacts; +// } + +export type Poll = { + host: Ship; + id: string; // atom id + expiry: number; + text: string; + options: string[]; + votes: PollVotes; + // TODO locks +}; +export type PollVotes = HiddenVotes | OpenExcVotes | OpenIncVotes; +export type HiddenVotes = { + type: "hid"; + exc: boolean; + votes: Record; +}; +export type OpenExcVotes = { + type: "exc"; + votes: Record; +}; +export type OpenIncVotes = { + type: "inc"; + votes: Record; +}; +export type VoteComment = { option: number; comment: string }; + +export type PollPoke = + | CreatePoll + | CancelPoll + | ChangeExpiry + | VotePoke + | CancelVote + | PeekPoll; +export type PeekPoll = { peek: PID }; +export type SentPoll = { + text: string; + expiry: number; + options: string[]; + exc: boolean; + hidden: boolean; + private: boolean; + id: string; +}; +export type CreatePoll = { + propose: SentPoll; +}; +export type CancelPoll = { + cancel: ID; +}; +export type ChangeExpiry = { + "change-expiry": { + pid: PID; + expiry: number; + }; +}; +export type VotePoke = { + vote: { + pid: PID; + option: number; + comment: string; + }; +}; +export type CancelVote = { + "cancel-vote": { pid: PID; option: number }; +}; +export type PollScry = OnePoll | DonePolls | CurrentPolls | BadPoll; +export type OnePoll = { poll: Poll }; +export type DonePolls = { done: Poll[] }; +export type CurrentPolls = { cur: Poll[] }; +export type BadPoll = { ng: null }; +export type TombPoll = { tomb: null }; + +export type PollUpdate = NewPollU | DedPollU | OldPollU | PollPeekRes; +export type DedPollU = { "ded-poll": PID }; +export type OldPollU = { pid: PID } & ( + | NewVoteU + | PollExpiryChanged + | VoteCanceled + | PollPeekRes +); +export type PollPeekRes = { + "peek-res": PollPeekOK | PollPeekNG | PollPeekNF; +}; +export type PollPeekOK = { + "peek-ok": Poll; +}; +export type PollPeekNG = { "peek-ng": string }; +export type PollPeekNF = { "no-poll": null }; + +export type NewPollU = { + "new-poll": Poll; +}; +export type NewVoteU = { + type: "new-vote"; + update: { + option: number; + ship: Ship; + comment: string; + }; +}; +export type PollExpiryChanged = { + type: "expiry-changed"; + update: { + expiry: number; + }; +}; +export type VoteCanceled = { + type: "vote-canceled"; + update: { option: number; ship: Ship }; +}; diff --git a/front/src/types/twatter.ts b/front/src/types/twatter.ts new file mode 100644 index 0000000..9814cbf --- /dev/null +++ b/front/src/types/twatter.ts @@ -0,0 +1,336 @@ +import type { Ship } from "./urbit"; +import type { Content as TrillContent } from "@/types/trill"; + +export interface APITweet { + core: APITweetCore; + legacy: APITweetLegacy; + rest_id?: string; // number + __typename?: string; + card?: any; + quoted_status_result?: { result: APIQuoteTweet }; +} +export interface APIQuoteTweet extends APITweet { + quotedRefResult: { result: { rest_id: string; __typename: string } }; +} +export interface APITwitterPoll { + binding_values: any[]; + card_platform: any; + name: string; + url: string; + user_refs_results: any[]; +} +export interface UserEntities { + description: { + urls: any[]; + }; + url: { + urls: URLEntity[]; + }; +} +export interface TweetEntities { + user_mentions: UserMentionEntity[]; + urls: URLEntity[]; + hashtags: HashtagEntity[]; + symbols: any[]; + media?: MediaEntity[]; +} +export interface UserMentionEntity { + id_str: string; // "144930676" + indices: [number, number]; + name: string; // "Naninizhoni" + screen_name: string; // "naninizhoni" +} +export interface HashtagEntity { + indices: [number, number]; + text: string; +} +export interface URLEntity { + url: string; + display_url: string; + expanded_url: string; + indices: [number, number]; +} +export interface MediaEntity { + display_url: string; // "pic.twitter.com/0qkz8kpFPQ" + expanded_url: string; // "https://twitter.com/ThaiNewsReports/status/1476368702924898304/photo/1" + media_url_https: string; // "https://pbs.twimg.com/media/FH0dgqeXEAEHVgI.jpg" + url: string; // "https://t.co/0qkz8kpFPQ" + features: { + large: { faces: any[] }; + medium: { faces: any[] }; + orig: { faces: any[] }; + small: { faces: any[] }; + }; + id_str: string; + indices: [number, number]; + original_info: { + height: number; + width: number; + focus_rects?: { x: number; y: number; w: number; h: number }[]; + }; + sizes: { + large: MediaSize; + medium: MediaSize; + small: MediaSize; + thumb: MediaSize; + }; + type: "photo"; //"photo" | ?? +} +export interface ExtendedEntity { + media: ExtendedMediaEntity[] | VideoEntity[]; +} +export interface ExtendedMediaEntity { + display_url: string; // "pic.twitter.com/0qkz8kpFPQ" + expanded_url: string; // "https://twitter.com/ThaiNewsReports/status/1476368702924898304/photo/1" + media_url_https: string; // "https://pbs.twimg.com/media/FH0dgqeXEAEHVgI.jpg" + url: string; // "https://t.co/0qkz8kpFPQ" + features: { + large: { faces: any[] }; + medium: { faces: any[] }; + orig: { faces: any[] }; + small: { faces: any[] }; + }; + id_str: string; + media_key: string; // "3_1476368699842039809" + indices: [number, number]; + original_info: { + height: number; + width: number; + focus_rects?: { x: number; y: number; w: number; h: number }[]; + }; + sizes: { + large: MediaSize; + medium: MediaSize; + small: MediaSize; + thumb: MediaSize; + }; + type: "photo" | "video"; // ?? + ext_media_availability: { status: string }; // "Available" + ext_media_color: { + palette: { + percentage: number; + rgb: { red: number; blue: number; green: number }; + }[]; + }; +} +export interface VideoEntity extends ExtendedMediaEntity { + original_info: { height: number; width: number }; + additional_media_info: { monetizable: boolean }; + mediaStats: { viewCount: number }; + video_info: { + aspect_ratio: [number, number]; + duration_millis: number; + variants: VideoVariant[]; + }; +} +export interface VideoVariant { + bitrate?: number; + content_type: string; // "video/mp4" "application/x-mpegURL" + url: string; // "https://video.twimg.com/ext_tw_video/1476257027378888711/pu/vid/640x360/KwFE_5vWD7hAVtu4.mp4?tag=12" +} +export interface MediaSize { + h: number; + w: number; + resize: "crop" | "fit"; +} +export interface APITweetLegacy { + conversation_id_str: string; // thread id + created_at: string; // "Wed Dec 15 14:02:32 +0000 2021" + display_text_range: [number, number]; // [0, 96] + entities: TweetEntities; + favorite_count: number; + favorited: boolean; + full_text: string; // + id_str: string; // "1471118482095943680" + is_quote_status: boolean; + lang: string; // "en" + possibly_sensitive: boolean; + possibly_sensitive_editable: boolean; + quote_count: number; + reply_count: number; + retweet_count: number; + retweeted: boolean; + source: string; // "Twitter Web App" + user_id_str: string; // "368897808" + retweeted_status_result?: { result: APITweet }; + quoted_status_id_str?: string; + quoted_status_permalink?: { + display: string; //"twitter.com/lijukic/status…" + expanded: string; //"https://twitter.com/lijukic/status/1476284640826736640" + url: string; //"https://t.co/1yLiM97600" + }; + in_reply_to_screen_name?: string; + in_reply_to_status_id_str?: string; + in_reply_to_user_id_str?: string; + self_thread?: { id_str: string }; + extended_entities?: ExtendedEntity; +} +export interface Tweet { + index: string; // number + parent: string | null; // number + thread: string; // number + time: number; + author: TweetAuthor; + contents: TwatterToken[]; + text: string; + media: TweetMedia[]; + poll: TwatterPoll | null; + rt_by: TweetAuthor | null; + rt_time: number | null; + language: string; + quoting: Tweet | null; + replies: number; + rts: number; + likes: number; + quotes: number; +} +export type TweetMedia = TweetPic | TweetVideo; +export interface TweetPic { + url: string; //url + thumbnail?: string; //url +} +export interface TweetVideo { + url: string; //url + thumbnail: string; //url +} + +export interface TwatterPoll { + card_url: string; + api: string; + last_updated_datetime_utc: Date; + end_datetime_utc: Date; + counts_are_final: boolean; + choice1_label: string; + choice1_count: string; + choice2_label: string; + choice2_count: string; + choice3_label?: string; + choice3_count?: string; + choice4_label?: string; + choice4_count?: string; +} + +export interface TweetAuthor { + suspended?: boolean; + username: string; + name: string; + id: string; // number + created: number; // date + bio: string; + avatar: string; + avatar_big: string; + cover_img: string; + following: number; + followers: number; + location: string; + url: string; + bluecheck: boolean; + locked: boolean; + withheld_in_countries: string[]; + post_count: number; + media_count: number; + listed_count: number; + patp: Ship | null; +} +export type EntityType = + | "user_mentions" + | "hashtags" + | "urls" + | "media" + | "symbol"; +export type tokenizerData = [string, taggedContent[]]; +export type taggedContent = [string, TwatterToken]; +export type TwatterToken = TwatterContent | EmojiContent | HashtagContent; +export type TwatterContent = + | { text: string } + | { mention: string } + | { url: string } + | { hashtag: string }; +export interface EmojiContent { + emoji: string; +} +export interface HashtagContent { + hashtag: string; +} +export interface TwatterThread { + thread: TweetsWithCursor; + replyThreads: TweetsWithCursor[]; + cursor: string; +} +export interface TweetsWithCursor { + tweets: Tweet[]; + cursor: string; + cursorBottom?: string; + type?: string; +} +export interface APITweetCore { + user: APIUserProfile; +} +export interface APIUserProfile { + affiliates_highlighted_label?: any; + id?: string; // base64 + rest_id: string; // number + legacy: { + created_at: string; // "Tue Sep 06 12:23:27 +0000 2011" + default_profile: boolean; + default_profile_image: boolean; + description: string; + entities: UserEntities; + fast_followers_count: number; + favourites_count: number; + followers_count: number; + friends_count: number; + has_custom_timelines: boolean; + is_translator: boolean; + listed_count: number; + location: string; + media_count: number; + name: string; + normal_followers_count: number; + pinned_tweet_ids_str: string[]; // ['1471118482095943680'] + profile_banner_extensions: any; //{mediaColor: {…}} + profile_banner_url: string; // "https://pbs.twimg.com/profile_banners/368897808/1398230281" + profile_image_extensions: any; // {mediaColor: {…}} + profile_image_url_https: string; //"https://pbs.twimg.com/profile_images/1193225494994571264/So4axAeC_normal.jpg" + profile_interstitial_type: string; // "" + protected: boolean; + screen_name: string; + statuses_count: number; + translator_type: string; // "none" + url?: string; // "https://t.co/uaINnItg4d" + verified: boolean; + withheld_in_countries: string[]; + }; +} + +// return types of our Urbit fetcher +export type NoCokiRes = { "no-coki": null }; +export type BadRequestRes = { fail: string }; +export type TwatterSearchRes = TwatterSearchResOK | NoCokiRes | BadRequestRes; +export type TwatterUserRes = TwatterUserResOK | NoCokiRes | BadRequestRes; +export type TwatterThreadRes = TwatterThreadResOK | NoCokiRes | BadRequestRes; +export type TwatterUserResOK = { + user: { + profile: string; + feed: string; + }; +}; +export type TwatterThreadResOK = TwatterLoggedThreadRes | TwatterLurkThreadRes; +export type TwatterLurkThreadRes = { + "thread-lurk": string; +}; +export type TwatterLoggedThreadRes = { + thread: string; +}; +export type TwatterSearchResOK = { + search: { + query: string; + data: string; + } +} +export type TwatterNotification = { + type: string; + user: string; + post?: string; + text: string; +} \ No newline at end of file diff --git a/front/src/types/ui.ts b/front/src/types/ui.ts new file mode 100644 index 0000000..d964d84 --- /dev/null +++ b/front/src/types/ui.ts @@ -0,0 +1,49 @@ +import {Poast } from "./trill"; +import { Tweet } from "./twatter"; +import { Ship } from "./urbit"; + +export type Timestamp = number; +export type UrbitTime = string; + +export interface ComposerData { + type: "quote" | "reply"; + post: SPID; +} +export type SPID = TrillPID | TwatterPID | RumorsPID; + +export interface TrillPID { + service: "trill"; + post: Poast; +} +export interface TwatterPID { + service: "twatter"; + post: Tweet; +} +export interface RumorsPID { + service: "rumors"; + post: Poast +} +export interface Guanxi { + trill: Relationship; + pals: Relationship; +} +export type Relationship = "mutual" | "incoming" | "outgoing" | "none"; + +// should make a sortug type codebase + +export type BucketCreds = { + opts: { + bucket: string; + origin: string; // this is the endpoint + region: string; + }, creds: { + credentials: { + accessKey: string; + secretKey: string; + } + } +} + +export type DateStruct = {year: number, month: number, day: number} +export type ChatQuoteParams = {p: Ship, nest: string, id: string} +export type ReactGrouping = Array<{react: string, ships: Ship[]}> \ No newline at end of file diff --git a/front/src/types/urbit.ts b/front/src/types/urbit.ts new file mode 100644 index 0000000..af9ee06 --- /dev/null +++ b/front/src/types/urbit.ts @@ -0,0 +1,8 @@ +export type Ship = string; +export interface S3Bucket { + accessKeyId: string; + endpoint: string; + secretAccessKey: string; + bucket: string; + region: string; +} \ No newline at end of file diff --git a/front/src/vite-env.d.ts b/front/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/front/src/vite-env.d.ts @@ -0,0 +1 @@ +/// -- cgit v1.2.3