diff --git a/README.md b/README.md new file mode 100644 index 0000000..57bdc2e --- /dev/null +++ b/README.md @@ -0,0 +1,167 @@ +# Cathode + +A terminal-style web client for [WeeChat](https://weechat.org/), using the +modern **API relay protocol** (WeeChat ≥ 4.0). No build step. No framework. +Drop three files on a web server and go. + +> Inspired by [Glowing Bear](https://github.com/glowing-bear/glowing-bear). + +--- + +## Features + +- Real-time IRC via WeeChat's JSON/WebSocket relay API +- Terminal aesthetic — white on black (default) and black on white, togglable +- Buffer list, nicklist, message history, input with command history (↑/↓) +- ANSI colour rendering, URL linkification +- Warns on browser-blocked ports before you try to connect +- Self-signed cert helper (opens relay URL in a new tab to accept the warning) +- Zero dependencies, zero build step — plain HTML/CSS/ES-module JS + +--- + +## Requirements + +- **WeeChat ≥ 4.0** with the relay plugin enabled and the **api** protocol configured +- A web server to serve the three static files (nginx, Caddy, Apache, or even + `python3 -m http.server`) +- A domain + TLS cert for production use (certbot/Let's Encrypt recommended) + +--- + +## WeeChat relay setup + +``` +# Inside WeeChat: + +# Load the relay plugin if not already loaded +/plugin load relay + +# Set a relay password (required) +/set relay.network.password "your_strong_password_here" + +# Create the API relay listener on port 9000 +# Do NOT use port 6667/6697 — those are blocked by browsers +/relay add api 9000 + +# For TLS (recommended for non-localhost): +/relay add tls.api 9000 + +# Verify it's listening: +/relay listport +``` + +> **Port note:** Ports 6665–6669 and 6697 (the traditional IRC port range) +> are blocked by all major browsers. Use any other port — 9000 is a safe +> default. + +--- + +## Installation + +### 1. Copy the files + +```bash +# Clone or download, then copy to your web root: +cp index.html app.js style.css /var/www/cathode/ + +# Or serve from the repo directly for development: +python3 -m http.server 8080 +``` + +### 2. Set up a reverse proxy (recommended) + +For production you'll want a reverse proxy to handle TLS and serve both the +static files and the WeeChat relay API under the same origin — this avoids +mixed-content issues and makes self-signed certs unnecessary. + +See the `proxy/` directory for ready-to-use configs: + +| Server | File | +|--------|------| +| Caddy | `proxy/Caddyfile` | +| nginx | `proxy/nginx.conf` | +| Apache | `proxy/apache.conf` | + +All three configs follow the same pattern: +- Serve static files at `/` +- Proxy `/api*` to `localhost:9000` (your WeeChat relay port) +- Handle WebSocket upgrade headers + +### 3. Open in browser + +Navigate to your domain. Enter the WeeChat relay host/port and password. +If you're using the reverse proxy setup, the host is your domain and the +port is 443 — the proxy forwards `/api` internally. + +--- + +## Self-signed certificates + +If you can't use Let's Encrypt (no domain, air-gapped, LAN-only), you can +still use TLS with a self-signed cert on the WeeChat relay. The browser will +refuse the WebSocket connection until you accept the cert exception: + +1. Enter your host and port in Cathode's connect screen +2. Click the **⚠ CERT** button — this opens `https://host:port/api/version` + in a new tab +3. Accept the browser's security warning in that tab +4. Return to Cathode and click **CONNECT** + +The exception is remembered by the browser for subsequent sessions. + +--- + +## LAN / local use (no TLS) + +If you're connecting from the same machine or a trusted LAN, you can skip TLS: + +- In WeeChat: `/relay add api 9000` (without `tls.`) +- In Cathode: uncheck **USE TLS** before connecting +- Serve Cathode itself over plain HTTP too (or `file://` directly) + +Note: `ws://` from an `https://` page is blocked by browsers (mixed content). +Either serve Cathode over plain HTTP as well, or use the reverse proxy approach. + +--- + +## Directory structure + +``` +cathode/ +├── index.html — app shell +├── app.js — all client logic (ES module, no build step) +├── style.css — terminal theme, dark + light +├── proxy/ +│ ├── Caddyfile — Caddy reverse proxy config +│ ├── nginx.conf — nginx reverse proxy config +│ └── apache.conf — Apache reverse proxy config +└── README.md +``` + +--- + +## How it works + +Cathode uses WeeChat's **API relay protocol** — a clean JSON-over-WebSocket +protocol introduced in WeeChat 4.0, replacing the old binary `weechat` relay +protocol that Glowing Bear was built on. + +On connect, Cathode: +1. Opens a WebSocket to `wss://host:port/api` with the password encoded in + the `Sec-WebSocket-Protocol` header (the only header the browser WebSocket + API allows you to set) +2. Sends a batched request: fetch all buffers with last 200 lines and nicks, + then subscribe to real-time events +3. Listens for push events (`buffer_line_added`, `buffer_opened`, + `nicklist_nick_added`, etc.) and updates the UI accordingly + +--- + +## License + +GPL-3.0 — same as WeeChat and Glowing Bear. + +--- + +*Cathode — Inspired by [Glowing Bear](https://github.com/glowing-bear/glowing-bear)* diff --git a/apple-touch-icon.png b/apple-touch-icon.png new file mode 100644 index 0000000..b450ae6 Binary files /dev/null and b/apple-touch-icon.png differ diff --git a/config.js b/config.js new file mode 100644 index 0000000..91a23a6 --- /dev/null +++ b/config.js @@ -0,0 +1,37 @@ +/** + * Cathode — operator configuration + * + * This file is loaded before app.js. Set properties on window.CATHODE_CONFIG + * to configure your deployment. All properties are optional — omit anything + * you don't need. + * + * For a self-hosted personal instance, fill this in once and forget it. + * For a shared/public instance, set the upload backend here so users don't + * need to configure it themselves (and can't change it). + */ + +window.CATHODE_CONFIG = { + + // ── Notifications ───────────────────────────────────────────────────────── + // The relay API doesn't expose WeeChat's per-buffer notify setting, so + // Cathode can't read it directly. Use these flags as a workaround. + // + // notifyServerBuffers: false — suppress notifications from IRC server buffers + // (type=server). Set to false if your server buffers are set to + // "notify none" in WeeChat. Default: true (don't suppress). + notifyServerBuffers: false, + + // ── Upload backend ──────────────────────────────────────────────────────── + // 'none' — disable file upload entirely (default) + // 'filehost' — single_php_filehost (https://github.com/Rouji/single_php_filehost) + // 'imgur' — Imgur API (requires a Client ID from https://api.imgur.com/oauth2/addclient) + uploadBackend: 'none', + filehostUrl: '', // e.g. 'https://files.example.com/' (filehost only) + imgurClientId: '', // e.g. 'abc123def456' (imgur only) + + // ── Prefix align max ────────────────────────────────────────────────────── + // Mirrors weechat.look.prefix_align_max — truncates long nicks in the + // message column. Set to match your WeeChat config. Default: 16. + prefixAlignMax: 16, + +}; diff --git a/favicon.ico b/favicon.ico new file mode 100644 index 0000000..8fcd44c Binary files /dev/null and b/favicon.ico differ diff --git a/index.html b/index.html new file mode 100644 index 0000000..87d57e6 --- /dev/null +++ b/index.html @@ -0,0 +1,195 @@ + + + + + + + + + Cathode + + + + + + +
+ + +
+ +
+
+ DISCONNECTED +
+
+ + + +
+ + +
+ + + + +
+
WEECHAT RELAY — API PROTOCOL
+
+ +
+
+ + +
+
+ + +
+
+ +
+ +
+ + +
+ + + +
+ +
+ + +
+ +

+ Using a self-signed cert? Click ⚠ CERT to open the relay URL in a new + tab, accept the browser warning, then return here and connect. +

+ +
+
+
+ + + + + + + +
+ + + + + + + + + + + + + diff --git a/js/ansi.js b/js/ansi.js new file mode 100644 index 0000000..adbebe0 --- /dev/null +++ b/js/ansi.js @@ -0,0 +1,123 @@ +// ─── ANSI colour palette ────────────────────────────────────────────────────── +export const ANSI16 = [ + '#1a1a1a','#cc3333','#33cc33','#cccc33', + '#3333cc','#cc33cc','#33cccc','#cccccc', + '#555555','#ff5555','#55ff55','#ffff55', + '#5555ff','#ff55ff','#55ffff','#ffffff', +]; + +export function ansi256(n) { + if (n < 16) return ANSI16[n]; + if (n >= 232) { const v = 8 + (n - 232) * 10; return `rgb(${v},${v},${v})`; } + const i = n - 16; + return `rgb(${Math.floor(i/36)*51},${Math.floor((i%36)/6)*51},${(i%6)*51})`; +} + +export function luminance(css) { + let r, g, b; + const m = css.match(/^rgb\((\d+),(\d+),(\d+)\)$/); + if (m) { r = +m[1]; g = +m[2]; b = +m[3]; } + else if (css.startsWith('#')) { + const h = css.slice(1); + if (h.length === 3) { + r = parseInt(h[0]+h[0],16); g = parseInt(h[1]+h[1],16); b = parseInt(h[2]+h[2],16); + } else { + r = parseInt(h.slice(0,2),16); g = parseInt(h.slice(2,4),16); b = parseInt(h.slice(4,6),16); + } + } else return 0.5; + const lin = c => { c /= 255; return c <= 0.04045 ? c/12.92 : Math.pow((c+0.055)/1.055, 2.4); }; + return 0.2126*lin(r) + 0.7152*lin(g) + 0.0722*lin(b); +} + +// In light theme, force near-white foreground colours to black. +export function safeFg(css) { + const theme = document.documentElement.getAttribute('data-theme') || 'dark'; + if (theme === 'light' && luminance(css) > 0.70) return '#111111'; + return css; +} + +// Partial-reset SGR codes — treat like a full reset (close all open spans). +const PARTIAL_RESET = new Set([21,22,23,24,25,27,28,29,39,49,51,52,53,54,55]); + +// Convert ANSI-escaped text to HTML. Linkifies URLs and adds media-toggle buttons. +export function ansiToHtml(text) { + if (!text) return ''; + let s = text.replace(/&/g,'&').replace(//g,'>'); + let out = '', spans = 0; + // NOTE: the regex literal below must contain a literal ESC character (0x1b) + // followed by \[ — do not modify with automated text tools. + const re = /\x1b\[([0-9;]*)m/g; + let last = 0, m; + while ((m = re.exec(s)) !== null) { + out += s.slice(last, m.index); + last = m.index + m[0].length; + const codes = m[1].split(';').map(Number); + let i = 0; + while (i < codes.length) { + if (codes[i] === 0) { + if (spans > 0) { out += ''.repeat(spans); spans = 0; } + i++; continue; + } + if (PARTIAL_RESET.has(codes[i])) { + if (spans > 0) { out += ''.repeat(spans); spans = 0; } + i++; continue; + } + const st = []; + while (i < codes.length && codes[i] !== 0 && !PARTIAL_RESET.has(codes[i])) { + const c = codes[i]; + if (c === 1) { st.push('font-weight:bold'); } + else if (c === 3) { st.push('font-style:italic'); } + else if (c === 4) { st.push('text-decoration:underline'); } + else if (c >= 30 && c <= 37) { st.push(`color:${safeFg(ANSI16[c-30])}`); } + else if (c === 38 && codes[i+1] === 5) { st.push(`color:${safeFg(ansi256(codes[i+2]))}`); i+=2; } + else if (c === 38 && codes[i+1] === 2) { st.push(`color:${safeFg(`rgb(${codes[i+2]},${codes[i+3]},${codes[i+4]})`)}`); i+=4; } + else if (c >= 40 && c <= 47) { st.push(`background:${ANSI16[c-40]}`); } + else if (c === 48 && codes[i+1] === 5) { st.push(`background:${ansi256(codes[i+2])}`); i+=2; } + else if (c === 48 && codes[i+1] === 2) { st.push(`background:rgb(${codes[i+2]},${codes[i+3]},${codes[i+4]})`); i+=4; } + else if (c >= 90 && c <= 97) { st.push(`color:${safeFg(ANSI16[c-90+8])}`); } + else if (c >= 100 && c <= 107) { st.push(`background:${ANSI16[c-100+8]}`); } + i++; + } + if (st.length) { out += ``; spans++; } + } + } + out += s.slice(last); + if (spans > 0) out += ''.repeat(spans); + + // Linkify URLs; match through & so query strings aren't cut short + return out.replace(/https?:\/\/(?:[^\s<>"']|&)+/g, (url) => { + const href = url.replace(/&/g, '&'); + const isImg = /\.(png|jpe?g|gif|webp|svg|bmp)(\?.*)?$/i.test(href); + const isVid = /\.(mp4|webm|ogv|mov)(\?.*)?$/i.test(href); + const btn = (isImg || isVid) + ? ` ` + : ''; + return `${url}${btn}`; + }); +} + +// ─── WeeChat colour name → CSS ──────────────────────────────────────────────── +const WEECHAT_COLOR_NAMES = { + 'default':'inherit','bar_fg':'inherit','black':'#1a1a1a','darkgray':'#555555', + 'red':'#cc3333','lightred':'#ff5555','green':'#33cc33','lightgreen':'#55ff55', + 'brown':'#cccc33','yellow':'#ffff55','blue':'#3333cc','lightblue':'#5555ff', + 'magenta':'#cc33cc','lightmagenta':'#ff55ff','cyan':'#33cccc','lightcyan':'#55ffff', + 'gray':'#cccccc','white':'#ffffff', +}; + +// Convert a nick color value from the relay API to a CSS color string. +// The API sends either an ANSI escape sequence or a WeeChat color name. +export function nickColorToCss(colorVal) { + if (!colorVal) return ''; + if (colorVal.includes('\x1b')) { + // Extract colour from ANSI escape by running it through ansiToHtml + const html = ansiToHtml(colorVal + 'X\x1b[0m'); + const m = html.match(/style="([^"]+)"/); + if (m) { + const cm = m[1].match(/(?:^|;)color:([^;]+)/); + if (cm) return cm[1]; + } + return ''; + } + return WEECHAT_COLOR_NAMES[colorVal.toLowerCase()] || 'inherit'; +} diff --git a/js/buffers.js b/js/buffers.js new file mode 100644 index 0000000..a0d7621 --- /dev/null +++ b/js/buffers.js @@ -0,0 +1,323 @@ +import { state, saveSettings } from './state.js'; +import { parseId } from './connection.js'; +import { ansiToHtml, nickColorToCss, safeFg } from './ansi.js'; +import { renderMessages, renderChatHeader, hideNewMsgBanner, appendLine } from './chat.js'; +import { maybeNotify, updateTitle } from './notifications.js'; + +const el = id => document.getElementById(id); +const esc = s => String(s).replace(/&/g,'&').replace(//g,'>'); + +// ─── Buffer events ──────────────────────────────────────────────────────────── +export function onBufOpened(buf) { + if (!buf) return; + const id = parseId(buf.id); + buf = { ...buf, id }; + state.buffers.set(id, { ...buf, lines: buf.lines||[], nicks:{}, unread:0, highlight:0 }); + if (!state.smartFilter.has(id)) state.smartFilter.set(id, true); + rebuildBufList(); + if (state.activeBufferId == null) activateBuffer(id); +} + +export function onBufUpdated(buf) { + if (!buf) return; + const id = parseId(buf.id); + const b = state.buffers.get(id); + if (!b) return; + Object.assign(b, { ...buf, id }); + paintNode(id); + if (state.activeBufferId === id) renderChatHeader(); +} + +export function onBufCleared(rawId) { + const id = parseId(rawId); + const b = state.buffers.get(id); + if (b) { b.lines = []; if (state.activeBufferId === id) el('messages').innerHTML = ''; } +} + +export function onBufClosed(rawId) { + const id = parseId(rawId); + state.buffers.delete(id); + removeNode(id); + if (state.activeBufferId === id) { + const first = state.buffers.keys().next().value; + if (first != null) activateBuffer(first); + else { state.activeBufferId = null; el('messages').innerHTML = ''; } + } +} + +export function onLineAdded(rawId, line) { + if (!line) return; + const id = parseId(rawId); + const b = state.buffers.get(id); + if (!b) return; + b.lines.push(line); + if (state.activeBufferId === id) { + appendLine(line); + } else { + b.unread++; + if (line.highlight) b.highlight++; + paintNode(id); + } + maybeNotify(b, line, activateBuffer); + updateTitle(); +} + +// ─── Nick events ────────────────────────────────────────────────────────────── +export function collectNicks(group, out) { + if (!group) return; + for (const n of (group.nicks || [])) out[n.id] = n; + for (const g of (group.groups || [])) collectNicks(g, out); +} + +export function onNickAdded(rawId, nick) { + const id = parseId(rawId); + const b = state.buffers.get(id); + if (!b || !nick) return; + b.nicks[nick.id] = nick; + if (state.activeBufferId === id) renderNicklist(b); +} + +export function onNickRemoved(rawId, nick) { + const id = parseId(rawId); + const b = state.buffers.get(id); + if (!b || !nick) return; + delete b.nicks[nick.id]; + if (state.activeBufferId === id) renderNicklist(b); +} + +export function onGroupChanged(rawId) { + const id = parseId(rawId); + const b = state.buffers.get(id); + if (b && state.activeBufferId === id) renderNicklist(b); +} + +// ─── Nicklist ───────────────────────────────────────────────────────────────── +export function renderNicklist(buf) { + const box = el('nicklist'); + box.innerHTML = ''; + const nicks = Object.values(buf.nicks || {}).sort((a, b) => { + const w = p => p==='~'?0 : p==='&'?1 : p==='@'?2 : p==='%'?3 : p==='+'?4 : 5; + const d = w(a.prefix) - w(b.prefix); + return d !== 0 ? d : a.name.localeCompare(b.name, undefined, {sensitivity:'base'}); + }); + for (const nick of nicks) { + const row = document.createElement('div'); + row.className = 'nick-item'; + const pfxChar = (nick.prefix && nick.prefix.trim()) ? esc(nick.prefix) : ' '; + const pfxHtml = nick.prefix_color + ? `${pfxChar}` + : `${pfxChar}`; + const nameHtml = nick.color + ? `${esc(nick.name)}` + : `${esc(nick.name)}`; + row.innerHTML = pfxHtml + nameHtml; + row.addEventListener('click', () => openNickMenu(nick, buf)); + box.appendChild(row); + } +} + +// ─── Nick context menu ──────────────────────────────────────────────────────── +function openNickMenu(nick, buf) { + closeNickMenu(); + const overlay = document.createElement('div'); + overlay.id = 'nick-overlay'; + overlay.className = 'nick-overlay'; + overlay.addEventListener('click', e => { if (e.target === overlay) closeNickMenu(); }); + + const menu = document.createElement('div'); + menu.className = 'nick-menu'; + + const hdr = document.createElement('div'); + hdr.className = 'nick-menu-hdr'; + hdr.textContent = (nick.prefix && nick.prefix.trim() ? nick.prefix : '') + nick.name; + menu.appendChild(hdr); + + const myPfx = ownPrefix(buf); + const isOp = ['@','~','&'].includes(myPfx); + + const actions = [ + { label: '💬 Query', cmd: `/query ${nick.name}` }, + { label: '🔍 Whois', cmd: `/whois ${nick.name}` }, + { label: '🔍 Whois (full)', cmd: `/whois ${nick.name} ${nick.name}` }, + { label: '📌 Ignore', cmd: `/ignore ${nick.name}` }, + { label: '🔇 Kick', cmd: `/kick ${nick.name}`, op: true }, + { label: '🚫 Ban', cmd: `/ban ${nick.name}`, op: true }, + ]; + for (const a of actions) { + if (a.op && !isOp) continue; + const btn = document.createElement('button'); + btn.className = 'nick-menu-btn'; + btn.textContent = a.label; + btn.addEventListener('click', () => { + wsSendRef(a.cmd, buf.name); + closeNickMenu(); + }); + menu.appendChild(btn); + } + + overlay.appendChild(menu); + document.body.appendChild(overlay); + overlay._esc = e => { if (e.key === 'Escape') closeNickMenu(); }; + document.addEventListener('keydown', overlay._esc); +} + +function closeNickMenu() { + const ov = document.getElementById('nick-overlay'); + if (!ov) return; + document.removeEventListener('keydown', ov._esc); + ov.remove(); +} + +function ownPrefix(buf) { + const nick = (buf.local_variables || {}).nick || ''; + const entry = Object.values(buf.nicks || {}).find(n => n.name === nick); + return entry ? (entry.prefix || '') : ''; +} + +// wsSend reference — set by main.js after connection module loads +let wsSendRef = () => {}; +export function setWsSend(fn) { + wsSendRef = (cmd, bufName) => fn({ request: 'POST /api/input', body: { buffer_name: bufName, command: cmd } }); +} + +// ─── Buffer list DOM ────────────────────────────────────────────────────────── +const bufNodes = new Map(); + +const bKey = id => 'b:' + id; +const gKey = key => 'g:' + key; + +function bufMeta(buf) { + const lv = buf.local_variables || {}; + const plugin = lv.plugin || ''; + const server = lv.server || ''; + const type = lv.type || ''; + if (!plugin || plugin === 'core') + return { group:'\x00core', groupLabel:'weechat', isServer:false, indent:false }; + if (plugin === 'irc') { + if (type === 'server' || !server) + return { group: server||buf.name, groupLabel: server||buf.name, isServer:true, indent:false }; + return { group: server, groupLabel: server, isServer:false, indent:true }; + } + const gk = server ? `${plugin}.${server}` : plugin; + return { group:gk, groupLabel: server ? `${plugin}/${server}` : plugin, + isServer:!server, indent:!!server }; +} + +function buildWanted() { + const sorted = [...state.buffers.values()].sort((a,b) => a.number - b.number); + const groups = new Map(); + for (const buf of sorted) { + const m = bufMeta(buf); + if (!groups.has(m.group)) groups.set(m.group, { label:m.groupLabel, srv:null, ch:[] }); + const g = groups.get(m.group); + if (m.isServer) g.srv = buf; else g.ch.push(buf); + } + const items = []; + for (const [gk, g] of groups) { + if (g.srv) items.push({ key:bKey(g.srv.id), type:'server', buf:g.srv }); + else items.push({ key:gKey(gk), type:'header', label:g.label }); + for (const buf of g.ch) + items.push({ key:bKey(buf.id), type:'channel', buf }); + } + return items; +} + +export function rebuildBufList() { + const container = el('buffer-list'); + for (const [,node] of bufNodes) node.remove(); + bufNodes.clear(); + for (const item of buildWanted()) { + const node = makeNode(item); + bufNodes.set(item.key, node); + container.appendChild(node); + } +} + +function paintNode(id) { + const node = bufNodes.get(bKey(id)); + if (!node) return; + const buf = state.buffers.get(id); + if (!buf) return; + const isServer = node.dataset.isServer === '1'; + const indent = node.dataset.indent === '1'; + const classes = ['buffer-item']; + if (isServer) classes.push('buf-server'); + if (indent) classes.push('buf-indented'); + if (String(buf.id) === String(state.activeBufferId)) classes.push('active'); + if (buf.highlight > 0) classes.push('highlight'); + else if (buf.unread > 0) classes.push('unread'); + node.className = classes.join(' '); + const name = buf.short_name || buf.name || '?'; + const badge = buf.highlight > 0 + ? `${buf.highlight}` + : buf.unread > 0 ? `${buf.unread}` : ''; + node.innerHTML = + `${buf.number}` + + `${esc(name)}${badge}` + + ``; +} + +function removeNode(id) { + const node = bufNodes.get(bKey(id)); + if (node) { node.remove(); bufNodes.delete(bKey(id)); } + rebuildBufList(); +} + +function makeNode(item) { + if (item.type === 'header') { + const node = document.createElement('div'); + node.className = 'buf-group-header'; + node.dataset.key = item.key; + node.textContent = item.label; + return node; + } + const isServer = item.type === 'server'; + const indent = item.type === 'channel'; + const node = document.createElement('div'); + node.dataset.key = item.key; + node.dataset.id = String(item.buf.id); + node.dataset.isServer = isServer ? '1' : '0'; + node.dataset.indent = indent ? '1' : '0'; + node.addEventListener('click', () => activateBuffer(node.dataset.id)); + const classes = ['buffer-item']; + if (isServer) classes.push('buf-server'); + if (indent) classes.push('buf-indented'); + node.className = classes.join(' '); + const buf = item.buf; + const name = buf.short_name || buf.name || '?'; + node.innerHTML = + `${buf.number}` + + `${esc(name)}` + + ``; + return node; +} + +// ─── Activate buffer ────────────────────────────────────────────────────────── +export function activateBuffer(id) { + const prev = state.activeBufferId; + const buf = state.buffers.get(id); + if (!buf) return; + state.activeBufferId = id; + state.scroll.pinned = true; + state.scroll.newCount = 0; + buf.unread = 0; + buf.highlight = 0; + if (prev != null && prev !== id) paintNode(prev); + paintNode(id); + renderChatHeader(); + renderMessages(buf); + renderNicklist(buf); + hideNewMsgBanner(); + updateTitle(); + el('chat-input').focus(); + + // Sync read position back to WeeChat by switching to the buffer there too. + // This marks lines as read in WeeChat's state, updating last_read_line_id. + // We send it as a direct API input command rather than going through the input box. + if (state.ws && state.ws.readyState === WebSocket.OPEN) { + state.ws.send(JSON.stringify({ + request: 'POST /api/input', + body: { buffer_name: 'core.weechat', command: `/buffer ${buf.name}` } + })); + } +} diff --git a/js/chat.js b/js/chat.js new file mode 100644 index 0000000..3df1112 --- /dev/null +++ b/js/chat.js @@ -0,0 +1,183 @@ +import { state } from './state.js'; +import { ansiToHtml } from './ansi.js'; + +const el = id => document.getElementById(id); + +// ─── Prefix truncation ──────────────────────────────────────────────────────── +// Mirrors weechat.look.prefix_align_max — truncates long nicks in the prefix +// column. Strips ANSI to measure visible length, re-wraps with original escape. +// +// IMPORTANT: the regex literals below must contain a literal ESC byte (0x1b). +// Do not modify with automated text replacement tools. +export function truncPrefix(raw) { + if (!raw) return raw; + const visible = raw.replace(/\x1b\[[0-9;]*m/g, ''); + const max = state.prefixAlignMax; + if (visible.length <= max) return raw; + // Preserve leading colour escape, truncate visible chars, close with reset + const esc = raw.match(/^(\x1b\[[0-9;]*m)*/); + const lead = esc ? esc[0] : ''; + const plain = visible.slice(0, max - 1) + '…'; + return lead + plain + '\x1b[0m'; +} + +export function applyPrefixWidth() { + const charWidth = 8.2; // IBM Plex Mono px/char at 13px + const px = Math.round(state.prefixAlignMax * charWidth) + 16; + document.documentElement.style.setProperty('--prefix-col-width', px + 'px'); +} + +// ─── Chat header ────────────────────────────────────────────────────────────── +export function renderChatHeader() { + const buf = state.buffers.get(state.activeBufferId); + if (!buf) return; + el('chat-title').textContent = buf.short_name || buf.name || ''; + el('chat-topic').innerHTML = buf.title ? ansiToHtml(buf.title) : ''; + const lv = buf.local_variables || {}; + const isIrc = lv.plugin === 'irc' && lv.type !== 'server'; + const sfBtn = el('smartfilter-btn'); + if (isIrc) { + const on = state.smartFilter.get(buf.id) !== false; + sfBtn.textContent = on ? 'FILTER: ON' : 'FILTER: OFF'; + sfBtn.classList.toggle('sf-off', !on); + sfBtn.style.display = ''; + } else { + sfBtn.style.display = 'none'; + } +} + +export function toggleSmartFilter() { + const id = state.activeBufferId; + if (id == null) return; + const cur = state.smartFilter.get(id) !== false; + state.smartFilter.set(id, !cur); + renderChatHeader(); + const buf = state.buffers.get(id); + if (buf) renderMessages(buf); +} + +// ─── Message rendering ──────────────────────────────────────────────────────── +export function renderMessages(buf) { + const box = el('messages'); + box.innerHTML = ''; + for (const line of buf.lines) appendLine(line, false, buf.lastReadId); + box.scrollTop = box.scrollHeight; + box.onscroll = onMessagesScroll; +} + +export function appendLine(line, scroll = true, lastReadId = null) { + if (!line.displayed) return; + // Smart filter: skip join/part/quit noise tagged by WeeChat + if (line.tags && line.tags.includes('irc_smart_filter')) { + if (state.smartFilter.get(state.activeBufferId) !== false) return; + } + const box = el('messages'); + + // Read marker divider — insert before the first unread line + if (lastReadId !== null && String(line.id) === String(lastReadId)) { + // This is the last-read line; the NEXT line will be unread. + // We track this by inserting the divider after this line is appended. + // We use a sentinel attribute on the box so we know to insert after this row. + box.dataset.insertDividerAfterNext = '1'; + } else if (box.dataset.insertDividerAfterNext === '1') { + delete box.dataset.insertDividerAfterNext; + const divider = document.createElement('div'); + divider.className = 'read-marker'; + divider.textContent = '─── unread ───'; + box.appendChild(divider); + } + + const subLines = (line.message || '').split('\n'); + const time = line.date ? fmtTime(line.date) : ''; + const prefix = line.prefix ? ansiToHtml(truncPrefix(line.prefix)) : ''; + const hlClass = line.highlight ? ' msg-highlight' : ''; + + subLines.forEach((sub, i) => { + const row = document.createElement('div'); + row.className = 'msg-row' + hlClass; + if (i === 0) { + row.innerHTML = + `${time}` + + `${prefix}` + + `` + + `${ansiToHtml(sub)}`; + } else { + row.innerHTML = + `` + + `` + + `` + + `${ansiToHtml(sub)}`; + } + box.appendChild(row); + }); + + if (scroll) { + if (state.scroll.pinned) { + box.scrollTop = box.scrollHeight; + } else { + state.scroll.newCount++; + showNewMsgBanner(state.scroll.newCount); + } + } +} + +export function sysMsg(id, text) { + if (id != null && id !== state.activeBufferId) return; + const box = el('messages'); + const row = document.createElement('div'); + row.className = 'msg-row msg-system'; + row.innerHTML = + `${fmtTime(new Date().toISOString())}` + + `--` + + `` + + `${escHtml(text)}`; + box.appendChild(row); + if (state.scroll.pinned) box.scrollTop = box.scrollHeight; +} + +// ─── Scroll lock ────────────────────────────────────────────────────────────── +export function onMessagesScroll() { + const box = el('messages'); + const atBottom = box.scrollHeight - box.scrollTop - box.clientHeight < 2; + if (atBottom && !state.scroll.pinned) { + state.scroll.pinned = true; + state.scroll.newCount = 0; + hideNewMsgBanner(); + } else if (!atBottom && state.scroll.pinned) { + state.scroll.pinned = false; + } +} + +export function showNewMsgBanner(count) { + let banner = document.getElementById('new-msg-banner'); + if (!banner) { + banner = document.createElement('div'); + banner.id = 'new-msg-banner'; + banner.className = 'new-msg-banner'; + banner.addEventListener('click', () => { + const box = el('messages'); + box.scrollTop = box.scrollHeight; + state.scroll.pinned = true; + state.scroll.newCount = 0; + hideNewMsgBanner(); + }); + el('main').appendChild(banner); + } + banner.textContent = `▼ ${count} new message${count === 1 ? '' : 's'}`; +} + +export function hideNewMsgBanner() { + const b = document.getElementById('new-msg-banner'); + if (b) b.remove(); +} + +// ─── Helpers (local) ───────────────────────────────────────────────────────── +function fmtTime(iso) { + try { return new Date(iso).toLocaleTimeString([], + {hour:'2-digit', minute:'2-digit', second:'2-digit', hour12:false}); } + catch { return ''; } +} + +function escHtml(s) { + return String(s).replace(/&/g,'&').replace(//g,'>'); +} diff --git a/js/connection.js b/js/connection.js new file mode 100644 index 0000000..f75f34b --- /dev/null +++ b/js/connection.js @@ -0,0 +1,259 @@ +import { state, saveSettings, BLOCKED_PORTS } from './state.js'; +import { initNotifications } from './notifications.js'; +import { applyPrefixWidth, sysMsg } from './chat.js'; +import { + onBufOpened, onBufUpdated, onBufCleared, onBufClosed, + onLineAdded, onNickAdded, onNickRemoved, onGroupChanged, + collectNicks, rebuildBufList, activateBuffer, +} from './buffers.js'; + +const el = id => document.getElementById(id); + +// ─── Reconnect state ────────────────────────────────────────────────────────── +const reconnect = { + enabled: false, // set true after first successful connect; false on user disconnect + timer: null, + backoff: 1000, // ms, doubles each attempt up to MAX_BACKOFF +}; +const MAX_BACKOFF = 30_000; +const INITIAL_BACKOFF = 1_000; + +// ─── Float-safe ID parser ───────────────────────────────────────────────────── +// The relay sometimes sends buffer IDs as JSON floats (e.g. 1709932823649184.0). +// Parsing those as Number loses precision — parse as string when safe. +export function parseId(v) { + if (v == null) return null; + if (typeof v === 'string') return v; // already a string key + if (typeof v === 'number') return String(Math.round(v)); // float → integer string + return String(v); +} + +// ─── WebSocket send ─────────────────────────────────────────────────────────── +export function wsSend(obj) { + if (state.ws && state.ws.readyState === WebSocket.OPEN) + state.ws.send(JSON.stringify(obj)); +} + +// ─── Connect ────────────────────────────────────────────────────────────────── +export function connect() { + const host = el('host').value.trim(); + const port = parseInt(el('port').value, 10); + const pass = el('password').value; + const tls = el('tls').checked; + + if (!host || !port) return showConnError('Host and port are required.'); + if (BLOCKED_PORTS.has(port)) return showConnError( + `Port ${port} is blocked by browsers. Use a different port — e.g. 9000.`); + + reconnect.enabled = false; // reset; re-enabled on first successful connect + clearReconnectTimer(); + connectTo(host, port, pass, tls); +} + +function connectTo(host, port, pass, tls) { + const hostFmt = (host.includes(':') && !host.startsWith('[')) ? `[${host}]` : host; + const url = `${tls ? 'wss' : 'ws'}://${hostFmt}:${port}/api`; + + hideConnError(); + setConnecting(true); + + let ws; + try { + ws = new WebSocket(url, [ + 'api.weechat', + `base64url.bearer.authorization.weechat.${buildAuth(pass)}` + ]); + } catch (e) { + setConnecting(false); + showConnError(`Could not open WebSocket: ${e.message}`); + return; + } + + const timer = setTimeout(() => { + ws.close(); + setConnecting(false); + showConnError('Connection timed out. Check host, port, and relay config.'); + }, 8000); + + ws.onopen = () => { + clearTimeout(timer); + state.ws = ws; + state.connected = true; + reconnect.enabled = true; + reconnect.backoff = INITIAL_BACKOFF; + Object.assign(state.settings, { host, port, pass, tls }); + saveSettings(); + onConnected(); + }; + + ws.onmessage = e => { + let data; + try { data = JSON.parse(e.data); } catch { return; } + if (Array.isArray(data)) data.forEach(dispatch); + else dispatch(data); + }; + + ws.onerror = () => { + clearTimeout(timer); + setConnecting(false); + if (location.protocol === 'https:' && !tls) { + showConnError('Secure connection error — cannot connect to an unencrypted relay (ws://) from an HTTPS page.'); + } else if (!reconnect.enabled) { + showConnError('WebSocket error. Check host, port, TLS, and relay config.'); + } + }; + + ws.onclose = () => { + clearTimeout(timer); + if (state.connected) { + onDisconnected(/* userInitiated */ false); + if (reconnect.enabled) scheduleReconnect(host, port, pass, tls); + } + }; +} + +function scheduleReconnect(host, port, pass, tls) { + clearReconnectTimer(); + const delay = reconnect.backoff; + reconnect.backoff = Math.min(reconnect.backoff * 2, MAX_BACKOFF); + setStatus('connecting', `RECONNECTING in ${Math.round(delay/1000)}s…`); + reconnect.timer = setTimeout(() => { + if (!reconnect.enabled) return; + setStatus('connecting', 'RECONNECTING…'); + connectTo(host, port, pass, tls); + }, delay); +} + +function clearReconnectTimer() { + if (reconnect.timer) { clearTimeout(reconnect.timer); reconnect.timer = null; } +} + +export function disconnect() { + reconnect.enabled = false; + clearReconnectTimer(); + wsSend({ request: 'DELETE /api/sync' }); + onDisconnected(/* userInitiated */ true); +} + +// ─── Auth ───────────────────────────────────────────────────────────────────── +function buildAuth(pw) { + return btoa('plain:' + pw).replace(/\+/g,'-').replace(/\//g,'_').replace(/=/g,''); +} + +// ─── Message dispatch ───────────────────────────────────────────────────────── +function dispatch(msg) { + if (!msg) return; + if (msg.code === 0 && msg.event_name) { handleEvent(msg); return; } + if (msg.request_id === 'init' && msg.body_type === 'buffers') { handleInit(msg); return; } +} + +function handleEvent(msg) { + switch (msg.event_name) { + case 'buffer_opened': onBufOpened(msg.body); break; + case 'buffer_closed': onBufClosed(msg.buffer_id); break; + case 'buffer_renamed': + case 'buffer_title_changed': + case 'buffer_localvar_added': + case 'buffer_localvar_changed': + case 'buffer_localvar_removed': + case 'buffer_moved': + case 'buffer_merged': + case 'buffer_unmerged': + case 'buffer_hidden': + case 'buffer_unhidden': onBufUpdated(msg.body); break; + case 'buffer_cleared': onBufCleared(msg.buffer_id); break; + case 'buffer_line_added': onLineAdded(msg.buffer_id, msg.body); break; + case 'nicklist_nick_added': + case 'nicklist_nick_changed': onNickAdded(msg.buffer_id, msg.body); break; + case 'nicklist_nick_removing': onNickRemoved(msg.buffer_id, msg.body); break; + case 'nicklist_group_added': + case 'nicklist_group_changed': onGroupChanged(msg.buffer_id); break; + case 'upgrade': sysMsg(null, '⟳ WeeChat upgrading…'); break; + case 'upgrade_ended': sysMsg(null, '✓ WeeChat upgrade complete.'); break; + case 'quit': onDisconnected(); break; + } +} + +// ─── Lifecycle ──────────────────────────────────────────────────────────────── +function onConnected() { + setStatus('connected', 'CONNECTED'); + initNotifications(); + wsSend([ + { request: 'GET /api/buffers?lines=-200&nicks=true&colors=ansi', request_id: 'init' }, + { request: 'POST /api/sync', body: { nicks: true, colors: 'ansi' } } + ]); +} + +function handleInit(msg) { + state.buffers.clear(); + const cfg = window.CATHODE_CONFIG || {}; + if (cfg.prefixAlignMax) { + state.prefixAlignMax = cfg.prefixAlignMax; + state.settings.prefixAlignMax = cfg.prefixAlignMax; + } else if (state.settings.prefixAlignMax) { + state.prefixAlignMax = state.settings.prefixAlignMax; + } + for (const buf of (msg.body || [])) { + const nicks = {}; + collectNicks(buf.nicklist_root, nicks); + // Use parseId to handle float IDs from the relay + const id = parseId(buf.id) ?? buf.id; + state.buffers.set(id, { ...buf, id, lines: buf.lines||[], nicks, unread:0, highlight:0, + lastReadId: buf.last_read_line_id ? parseId(buf.last_read_line_id) : null }); + if (!state.smartFilter.has(id)) state.smartFilter.set(id, true); + } + setConnecting(false); + el('disconnect-btn').style.display = ''; + applyPrefixWidth(); + showScreen('chat'); + rebuildBufList(); + state.scroll.pinned = true; + state.scroll.newCount = 0; + const first = state.buffers.keys().next().value; + if (first != null) activateBuffer(first); +} + +export function onDisconnected(userInitiated = true) { + if (!state.connected && !state.ws) return; + state.connected = false; + if (state.ws) { try { state.ws.close(); } catch(_){} state.ws = null; } + setStatus('disconnected', 'DISCONNECTED'); + el('disconnect-btn').style.display = 'none'; + // Only return to connect screen on explicit user disconnect or first-time failure + // On auto-reconnect we stay on the chat screen showing the status + if (userInitiated) { + showScreen('connect'); + state.buffers.clear(); + state.activeBufferId = null; + state.scroll.pinned = true; + state.scroll.newCount = 0; + el('buffer-list').innerHTML = ''; + el('messages').innerHTML = ''; + el('nicklist').innerHTML = ''; + const banner = document.getElementById('new-msg-banner'); + if (banner) banner.remove(); + } +} + +// ─── UI helpers (connection-screen) ────────────────────────────────────────── +function showConnError(msg) { + el('conn-error').textContent = msg; + el('conn-error').style.display = 'block'; +} +function hideConnError() { el('conn-error').style.display = 'none'; } + +function setConnecting(on) { + el('connect-btn').disabled = on; + el('connect-btn').textContent = on ? 'CONNECTING…' : 'CONNECT'; + if (on) setStatus('connecting', 'CONNECTING…'); +} + +function setStatus(s, text) { + el('status-dot').className = 'status-dot ' + s; + el('status-text').textContent = text; +} + +function showScreen(name) { + el('connect-screen').style.display = name === 'connect' ? '' : 'none'; + el('chat-screen').style.display = name === 'chat' ? '' : 'none'; +} diff --git a/js/input.js b/js/input.js new file mode 100644 index 0000000..162e2a9 --- /dev/null +++ b/js/input.js @@ -0,0 +1,131 @@ +import { state } from './state.js'; + +const el = id => document.getElementById(id); + +// Emoji tab completion via emoji-mart SearchIndex. +// Lazily initialised on first use so it doesn't block startup. +let emojiSearchReady = false; +async function initEmojiSearch() { + if (emojiSearchReady) return; + if (!window.EmojiMart?.SearchIndex) return; + try { + const res = await fetch('vendor/emoji-data.json'); + const data = await res.json(); + await EmojiMart.init({ data }); + emojiSearchReady = true; + } catch (e) { + console.warn('Emoji search init failed:', e); + } +} + +async function searchEmoji(stem) { + await initEmojiSearch(); + if (!emojiSearchReady) return []; + const results = await EmojiMart.SearchIndex.search(stem); + return (results || []).map(e => e.skins[0].native + e.id); +} + +const hist = { lines: [], pos: -1, draft: '' }; +const tab = { matches: [], pos: -1, stem: '' }; + +export function sendInput(wsSend) { + const buf = state.buffers.get(state.activeBufferId); + const text = el('chat-input').value.trim(); + if (!buf || !text) return; + hist.lines.unshift(text); + hist.pos = -1; + tab.matches = []; + tab.pos = -1; + wsSend({ request: 'POST /api/input', body: { buffer_name: buf.name, command: text } }); + el('chat-input').value = ''; +} + +export function onInputKey(e, wsSend) { + if (e.key === 'Tab') { + e.preventDefault(); + doTabComplete(); + return; + } + if (e.key !== 'Shift') { tab.matches = []; tab.pos = -1; } + + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + sendInput(wsSend); + } else if (e.key === 'ArrowUp') { + e.preventDefault(); + if (hist.pos === -1) hist.draft = el('chat-input').value; + hist.pos = Math.min(hist.pos + 1, hist.lines.length - 1); + if (hist.lines[hist.pos] !== undefined) el('chat-input').value = hist.lines[hist.pos]; + } else if (e.key === 'ArrowDown') { + e.preventDefault(); + hist.pos = Math.max(hist.pos - 1, -1); + el('chat-input').value = hist.pos === -1 ? hist.draft : hist.lines[hist.pos]; + } +} + +function doTabComplete() { + const input = el('chat-input'); + const val = input.value; + const caret = input.selectionStart; + const before = val.slice(0, caret); + const tokenMatch = before.match(/(\S+)$/); + const token = tokenMatch ? tokenMatch[1] : ''; + if (!token) return; + + const lower = token.toLowerCase(); + + if (tab.matches.length === 0 || tab.stem !== lower) { + const buf = state.buffers.get(state.activeBufferId); + if (!buf) return; + + const prevWord = before.slice(0, before.length - token.length).trim().split(/\s+/).pop() || ''; + const wantChannel = token.startsWith('#') || prevWord.toLowerCase() === '/join'; + const wantEmoji = token.startsWith(':') && token.length > 1; + + if (wantEmoji) { + const stem = token.slice(1).replace(/:$/, '').toLowerCase(); + // Async: kick off search and return; next Tab press will use the results + searchEmoji(stem).then(results => { + tab.matches = results; + tab.stem = lower; + tab.pos = -1; + if (results.length > 0) doComplete(input, val, caret, before, token); + }); + return; // first Tab fetches; subsequent Tab presses cycle + } + + let candidates; + if (wantChannel) { + candidates = [...state.buffers.values()] + .filter(b => { + const lv = b.local_variables || {}; + return lv.type === 'channel' || (b.short_name || '').startsWith('#'); + }) + .map(b => b.short_name || b.name); + } else { + candidates = Object.values(buf.nicks || {}).map(n => n.name); + } + + tab.matches = candidates.filter(c => c.toLowerCase().startsWith(lower)); + tab.matches.sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' })); + tab.pos = -1; + tab.stem = lower; + if (tab.matches.length === 0) return; + } + + doComplete(input, val, caret, before, token); +} + +function doComplete(input, val, caret, before, token) { + if (tab.matches.length === 0) return; + tab.pos = (tab.pos + 1) % tab.matches.length; + const match = tab.matches[tab.pos]; + const isEmoji = /^\p{Emoji}/u.test(match); + const insert = isEmoji ? [...match][0] : match; + const atStart = before.trimStart() === token; + const isNick = !insert.startsWith('#') && !isEmoji; + const suffix = (atStart && isNick) ? ': ' : ' '; + const completed = before.slice(0, before.length - token.length) + insert + suffix; + input.value = completed + val.slice(caret); + input.selectionStart = input.selectionEnd = completed.length; +} diff --git a/js/main.js b/js/main.js new file mode 100644 index 0000000..0f63c20 --- /dev/null +++ b/js/main.js @@ -0,0 +1,194 @@ +import { state } from './state.js'; +import { initTheme, toggleTheme, + openSettings, closeSettings, + saveSettingsPanel, updateBackendVis, + checkPort, openCertPage } from './settings.js'; +import { connect, disconnect, wsSend } from './connection.js'; +import { toggleSmartFilter } from './chat.js'; +import { sendInput, onInputKey } from './input.js'; +import { uploadFile, initDragDrop } from './upload.js'; +import { setWsSend } from './buffers.js'; + +const el = id => document.getElementById(id); + +document.addEventListener('DOMContentLoaded', () => { + initTheme(); + + // ── Operator config (config.js → window.CATHODE_CONFIG) ────────────────── + const cfg = window.CATHODE_CONFIG || {}; + if (cfg.uploadBackend) { + state.settings.uploadBackend = cfg.uploadBackend; + state.settings.filehostUrl = cfg.filehostUrl || ''; + state.settings.imgurClientId = cfg.imgurClientId || ''; + const sec = el('settings-upload-section'); + if (sec) sec.style.display = 'none'; + } + if (cfg.prefixAlignMax) { + state.prefixAlignMax = cfg.prefixAlignMax; + state.settings.prefixAlignMax = cfg.prefixAlignMax; + } + + // ── Restore saved connection settings ──────────────────────────────────── + const s = state.settings; + if (s.host) el('host').value = s.host; + if (s.port) el('port').value = s.port; + if (s.tls !== undefined) el('tls').checked = s.tls; + if (s.prefixAlignMax) state.prefixAlignMax = s.prefixAlignMax; + + // ── Initial UI state ────────────────────────────────────────────────────── + el('connect-screen').style.display = ''; + el('chat-screen').style.display = 'none'; + el('status-dot').className = 'status-dot disconnected'; + el('status-text').textContent = 'DISCONNECTED'; + el('disconnect-btn').style.display = 'none'; + + if (location.protocol === 'https:') { + el('http-notice').style.display = ''; + el('tls-locked-note').style.display = ''; + } + + // ── Wire wsSend into buffers (nick menu commands) ───────────────────────── + setWsSend(wsSend); + + // ── Connection ──────────────────────────────────────────────────────────── + el('connect-btn') .addEventListener('click', connect); + el('disconnect-btn').addEventListener('click', disconnect); + ['host','port','password'].forEach(id => + el(id).addEventListener('keydown', e => { if (e.key === 'Enter') connect(); }) + ); + el('port').addEventListener('input', checkPort); + + // ── Chat input ──────────────────────────────────────────────────────────── + el('send-btn') .addEventListener('click', () => sendInput(wsSend)); + el('chat-input').addEventListener('keydown', e => onInputKey(e, wsSend)); + + // ── Smart filter ────────────────────────────────────────────────────────── + el('smartfilter-btn').addEventListener('click', toggleSmartFilter); + + // ── Buffer close buttons (delegated) ───────────────────────────────────── + el('buffer-list').addEventListener('click', e => { + const btn = e.target.closest('.buf-close'); + if (!btn) return; + e.stopPropagation(); + const buf = state.buffers.get(btn.dataset.id); + if (!buf) return; + wsSend({ request: 'POST /api/input', body: { buffer_name: buf.name, command: '/close' } }); + }); + + // ── Sidebar join button ─────────────────────────────────────────────────── + el('sidebar-join-btn').addEventListener('click', () => { + const ch = prompt('Channel to join (e.g. #weechat):'); + if (!ch) return; + const buf = state.buffers.get(state.activeBufferId); + if (!buf) return; + wsSend({ request: 'POST /api/input', body: { buffer_name: buf.name, command: '/join ' + ch } }); + }); + + // ── Media preview (document-level delegation) ───────────────────────────── + document.addEventListener('click', e => { + const btn = e.target.closest('.media-toggle'); + if (!btn) return; + const url = btn.dataset.url.replace(/&/g, '&'); + const type = btn.dataset.type; + const existing = btn.nextElementSibling; + if (existing && existing.classList.contains('media-preview')) { + existing.remove(); + btn.textContent = type === 'img' ? 'Show Image' : 'Show Video'; + return; + } + const wrap = document.createElement('span'); + wrap.className = 'media-preview'; + if (type === 'img') { + const img = document.createElement('img'); + img.src = url; + img.className = 'preview-img'; + img.alt = 'image'; + img.title = 'Click to open full size'; + img.addEventListener('click', () => window.open(url, '_blank')); + wrap.appendChild(img); + } else { + const vid = document.createElement('video'); + vid.src = url; + vid.controls = true; + vid.className = 'preview-vid'; + wrap.appendChild(vid); + } + btn.after(wrap); + btn.textContent = type === 'img' ? 'Hide Image' : 'Hide Video'; + }); + + // ── Theme ───────────────────────────────────────────────────────────────── + el('theme-toggle').addEventListener('click', toggleTheme); + window.addEventListener('cathode:themechange', () => { + const buf = state.buffers.get(state.activeBufferId); + if (buf) import('./chat.js').then(m => m.renderMessages(buf)); + if (_emojiPicker) { _emojiPicker.remove(); _emojiPicker = null; } + }); + + // ── Emoji picker ────────────────────────────────────────────────────────── + let _emojiPicker = null; + let _emojiDataCache = null; + + async function getEmojiData() { + if (_emojiDataCache) return _emojiDataCache; + const res = await fetch('vendor/emoji-data.json'); + _emojiDataCache = await res.json(); + return _emojiDataCache; + } + + el('emoji-btn').addEventListener('click', async e => { + e.stopPropagation(); + if (_emojiPicker) { + _emojiPicker.remove(); _emojiPicker = null; return; + } + if (!window.EmojiMart?.Picker) return; + const data = await getEmojiData(); + const theme = document.documentElement.getAttribute('data-theme') === 'dark' ? 'dark' : 'light'; + _emojiPicker = new EmojiMart.Picker({ + data, + theme, + onEmojiSelect: emoji => { + const input = el('chat-input'); + const pos = input.selectionStart ?? input.value.length; + input.value = + input.value.slice(0, pos) + emoji.native + input.value.slice(pos); + input.selectionStart = input.selectionEnd = pos + [...emoji.native].length; + _emojiPicker.remove(); _emojiPicker = null; + input.focus(); + }, + onClickOutside: () => { + if (_emojiPicker) { _emojiPicker.remove(); _emojiPicker = null; } + }, + }); + _emojiPicker.style.cssText = 'position:absolute;bottom:48px;right:8px;z-index:200;'; + el('main').appendChild(_emojiPicker); + }); + + // ── Cert helper ─────────────────────────────────────────────────────────── + el('cert-btn').addEventListener('click', openCertPage); + + // ── Upload ──────────────────────────────────────────────────────────────── + el('upload-btn') .addEventListener('click', () => el('upload-file').click()); + el('upload-file').addEventListener('change', e => { + const file = e.target.files[0]; + if (file) { uploadFile(file); e.target.value = ''; } + }); + initDragDrop(); + + // ── Settings panel ──────────────────────────────────────────────────────── + el('settings-btn') .addEventListener('click', openSettings); + el('settings-close') .addEventListener('click', closeSettings); + el('settings-save') .addEventListener('click', saveSettingsPanel); + el('s-upload-backend').addEventListener('change', updateBackendVis); + el('settings-overlay').addEventListener('click', e => { + if (e.target === el('settings-overlay')) closeSettings(); + }); + + // ── Global keyboard shortcuts ───────────────────────────────────────────── + document.addEventListener('keydown', e => { + if (e.key === 'Escape') { + closeSettings(); + if (_emojiPicker) { _emojiPicker.remove(); _emojiPicker = null; } + } + }); +}); diff --git a/js/notifications.js b/js/notifications.js new file mode 100644 index 0000000..13e8420 --- /dev/null +++ b/js/notifications.js @@ -0,0 +1,60 @@ +import { state } from './state.js'; + +export async function initNotifications() { + if (!('Notification' in window)) return; + if (Notification.permission === 'default') { + await Notification.requestPermission(); + } +} + +export function maybeNotify(buf, line, activateBufferFn) { + if (!line) return; + if (!('Notification' in window) || Notification.permission !== 'granted') return; + + // Buffer-level notify gate. + // WeeChat's buffer.notify setting only gates the hotlist — it does NOT affect + // line.highlight or line.notify_level, so we implement our own gate here. + // The relay API doesn't expose buffer.notify, so we use config flags instead. + const lv = buf.local_variables || {}; + const type = lv.type || ''; + const cfg = window.CATHODE_CONFIG || {}; + if (type === 'server' && cfg.notifyServerBuffers === false) return; + + // Detect highlight — check all three sources WeeChat may use + const tags = Array.isArray(line.tags) ? line.tags : []; + const isHL = line.highlight + || (line.notify_level >= 3) + || tags.includes('notify_highlight'); + const isPrivate = (line.notify_level === 2) + || tags.includes('notify_private'); + + if (!isHL && !isPrivate) return; + + // Suppress only when tab is visible AND the user is on this exact buffer + const focused = document.visibilityState === 'visible' + && state.activeBufferId === buf.id; + if (focused) return; + + const bufName = buf.short_name || buf.name || '?'; + const stripAnsi = s => (s || '').replace(/\x1b\[[0-9;]*m/g, '').trim(); + const prefix = stripAnsi(line.prefix); + const body = stripAnsi(line.message); + const title = isPrivate ? `PM from ${prefix}` : `${prefix} in ${bufName}`; + + const n = new Notification(title, { + body, + icon: 'apple-touch-icon.png', + tag: `cathode-${buf.id}`, // collapses repeated pings from same buffer + }); + n.onclick = () => { + window.focus(); + activateBufferFn(buf.id); + n.close(); + }; +} + +export function updateTitle() { + let total = 0; + for (const buf of state.buffers.values()) total += buf.highlight; + document.title = total > 0 ? `(${total}) Cathode` : 'Cathode'; +} diff --git a/js/settings.js b/js/settings.js new file mode 100644 index 0000000..422d0bb --- /dev/null +++ b/js/settings.js @@ -0,0 +1,74 @@ +import { state, saveSettings, BLOCKED_PORTS } from './state.js'; +import { applyPrefixWidth } from './chat.js'; + +const el = id => document.getElementById(id); + +// ─── Theme ──────────────────────────────────────────────────────────────────── +export function initTheme() { + setTheme(localStorage.getItem('cathode_theme') || 'dark'); +} + +export function setTheme(t) { + document.documentElement.setAttribute('data-theme', t); + localStorage.setItem('cathode_theme', t); + el('theme-toggle').textContent = t === 'dark' ? '◐ LIGHT' : '◑ DARK'; +} + +export function toggleTheme() { + const cur = document.documentElement.getAttribute('data-theme') || 'dark'; + setTheme(cur === 'dark' ? 'light' : 'dark'); + // Re-render dispatched via custom event so chat.js can respond without circular import + window.dispatchEvent(new CustomEvent('cathode:themechange')); +} + +// ─── Settings panel ─────────────────────────────────────────────────────────── +export function openSettings() { + const s = state.settings; + el('s-upload-backend').value = s.uploadBackend || 'none'; + el('s-filehost-url').value = s.filehostUrl || ''; + el('s-imgur-key').value = s.imgurClientId || ''; + el('s-prefix-max').value = s.prefixAlignMax || state.prefixAlignMax; + updateBackendVis(); + el('settings-overlay').style.display = ''; +} + +export function closeSettings() { + el('settings-overlay').style.display = 'none'; +} + +export function saveSettingsPanel() { + state.settings.uploadBackend = el('s-upload-backend').value; + state.settings.filehostUrl = el('s-filehost-url').value.trim(); + state.settings.imgurClientId = el('s-imgur-key').value.trim(); + const pm = parseInt(el('s-prefix-max').value, 10); + if (pm >= 4 && pm <= 64) { + state.settings.prefixAlignMax = pm; + state.prefixAlignMax = pm; + applyPrefixWidth(); + } + saveSettings(); + closeSettings(); +} + +export function updateBackendVis() { + const v = el('s-upload-backend').value; + el('s-filehost-opts').style.display = v === 'filehost' ? '' : 'none'; + el('s-imgur-opts').style.display = v === 'imgur' ? '' : 'none'; +} + +// ─── Port warning ───────────────────────────────────────────────────────────── +export function checkPort() { + const port = parseInt(el('port').value, 10); + const show = BLOCKED_PORTS.has(port); + el('port-warning').textContent = show + ? `⚠ Port ${port} is blocked by browsers. Use a different port (e.g. 9000).` : ''; + el('port-warning').style.display = show ? 'block' : 'none'; +} + +// ─── Cert helper ────────────────────────────────────────────────────────────── +export function openCertPage() { + const host = el('host').value.trim(); + const port = parseInt(el('port').value, 10); + if (!host || !port) return alert('Enter host and port first.'); + window.open(`https://${host}:${port}/api/version`, '_blank'); +} diff --git a/js/state.js b/js/state.js new file mode 100644 index 0000000..fddec1c --- /dev/null +++ b/js/state.js @@ -0,0 +1,30 @@ +// ─── Blocked ports (browser hard-block list) ───────────────────────────────── +export const BLOCKED_PORTS = new Set([ + 1,7,9,11,13,15,17,19,20,21,22,23,25,37,42,43,53,69,77,79,87,95, + 101,102,103,104,107,109,110,111,113,115,117,119,123,135,137,139, + 143,161,179,389,427,465,512,513,514,515,526,530,531,532,540,548, + 554,556,563,587,601,636,989,990,993,995,1719,1720,1723,2049,3659, + 4045,5060,5061,6000,6566,6665,6666,6667,6668,6669,6697,10080 +]); + +// ─── Settings persistence ───────────────────────────────────────────────────── +export function loadSettings() { + try { return JSON.parse(localStorage.getItem('cathode_settings') || '{}'); } + catch { return {}; } +} + +export function saveSettings() { + localStorage.setItem('cathode_settings', JSON.stringify(state.settings)); +} + +// ─── Shared application state ───────────────────────────────────────────────── +export const state = { + ws: null, + connected: false, + buffers: new Map(), // id (number) → buffer object + activeBufferId: null, + settings: loadSettings(), + prefixAlignMax: 16, // mirrors weechat.look.prefix_align_max + scroll: { pinned: true, newCount: 0 }, + smartFilter: new Map(), // bufferId → boolean +}; diff --git a/js/upload.js b/js/upload.js new file mode 100644 index 0000000..d04118a --- /dev/null +++ b/js/upload.js @@ -0,0 +1,126 @@ +import { state } from './state.js'; + +const el = id => document.getElementById(id); + +// ─── Upload dispatch ────────────────────────────────────────────────────────── +export async function uploadFile(file) { + const s = state.settings; + const backend = s.uploadBackend || 'none'; + if (backend === 'none') { + showUploadError('No upload backend configured. Open ⚙ Settings to set one up.'); + return; + } + setUploadState('uploading'); + try { + let url; + if (backend === 'filehost') url = await uploadToFilehost(file, s.filehostUrl); + else if (backend === 'imgur') url = await uploadToImgur(file, s.imgurClientId); + + setUploadState('ok'); + setTimeout(() => setUploadState('idle'), 2000); + + const input = el('chat-input'); + const pos = input.selectionStart || input.value.length; + const sep = input.value.length > 0 && !input.value.endsWith(' ') ? ' ' : ''; + input.value = input.value.slice(0, pos) + sep + url + input.value.slice(pos); + input.focus(); + input.selectionStart = input.selectionEnd = pos + sep.length + url.length; + } catch (err) { + setUploadState('err'); + setTimeout(() => setUploadState('idle'), 3000); + showUploadError(`Upload failed: ${err.message}`); + } +} + +async function uploadToFilehost(file, baseUrl) { + if (!baseUrl) throw new Error('Filehost URL not configured in Settings.'); + const url = baseUrl.endsWith('/') ? baseUrl : baseUrl + '/'; + const form = new FormData(); + form.append('file', file, file.name); + const res = await fetch(url, { method: 'POST', body: form }); + if (!res.ok) throw new Error(`Server returned ${res.status}`); + const text = (await res.text()).trim(); + if (!text.startsWith('http')) throw new Error(`Unexpected response: ${text}`); + return text; +} + +async function uploadToImgur(file, clientId) { + if (!clientId) throw new Error('Imgur Client ID not configured in Settings.'); + const form = new FormData(); + form.append('image', file); + const res = await fetch('https://api.imgur.com/3/image', { + method: 'POST', + headers: { Authorization: `Client-ID ${clientId}` }, + body: form, + }); + const json = await res.json(); + if (!json.success) throw new Error(json.data?.error || 'Imgur upload failed'); + return json.data.link; +} + +function setUploadState(s) { + const btn = el('upload-btn'); + btn.classList.remove('uploading', 'upload-ok', 'upload-err'); + if (s === 'uploading') { btn.classList.add('uploading'); btn.textContent = '⏳'; } + else if (s === 'ok') { btn.classList.add('upload-ok'); btn.textContent = '✓'; } + else if (s === 'err') { btn.classList.add('upload-err'); btn.textContent = '✗'; } + else { btn.textContent = '📎'; } +} + +function showUploadError(msg) { + const box = el('messages'); + if (!box) return; + const row = document.createElement('div'); + row.className = 'msg-row msg-system'; + row.style.color = 'var(--status-disc)'; + row.innerHTML = + `` + + `upload` + + `` + + `${msg}`; + box.appendChild(row); + if (state.scroll.pinned) box.scrollTop = box.scrollHeight; +} + +// ─── Drag & drop and clipboard paste ───────────────────────────────────────── +export function initDragDrop() { + let dragCounter = 0; + + window.addEventListener('dragenter', e => { + if (!e.dataTransfer.types.includes('Files')) return; + dragCounter++; + el('drag-overlay').style.display = 'flex'; + }); + + window.addEventListener('dragleave', () => { + dragCounter--; + if (dragCounter <= 0) { + dragCounter = 0; + el('drag-overlay').style.display = 'none'; + } + }); + + window.addEventListener('dragover', e => { + if (!e.dataTransfer.types.includes('Files')) return; + e.preventDefault(); + }); + + window.addEventListener('drop', e => { + e.preventDefault(); + dragCounter = 0; + el('drag-overlay').style.display = 'none'; + if (!state.connected) return; + const files = [...e.dataTransfer.files]; + if (files.length) uploadFile(files[0]); + }); + + // Paste image from clipboard + el('chat-input').addEventListener('paste', e => { + const items = [...(e.clipboardData?.items || [])]; + const imageItem = items.find(i => i.kind === 'file' && i.type.startsWith('image/')); + if (!imageItem) return; + e.preventDefault(); + const file = imageItem.getAsFile(); + if (file) uploadFile(file); + }); +} diff --git a/proxy/Caddyfile b/proxy/Caddyfile new file mode 100644 index 0000000..116d382 --- /dev/null +++ b/proxy/Caddyfile @@ -0,0 +1,56 @@ +# Caddyfile — Cathode + WeeChat relay reverse proxy +# +# This config does two things: +# 1. Serves the Cathode static files at the root +# 2. Proxies /api/* to WeeChat's relay (API protocol) with WebSocket support +# +# Caddy handles TLS automatically (Let's Encrypt) when you use a real domain. +# Replace cathode.example.com with your actual domain. + +cathode.example.com { + + # Serve Cathode static files + root * /var/www/cathode + file_server + + # Proxy WeeChat relay API (REST + WebSocket) + # WeeChat listens on localhost:9000 — adjust if needed + handle /api* { + reverse_proxy localhost:9000 { + # Pass the real client IP to WeeChat + header_up X-Real-IP {remote_host} + + # Required for WebSocket upgrade + transport http { + # If WeeChat relay uses a self-signed cert on localhost, + # disable verification for the backend connection + # tls_insecure_skip_verify + } + } + } + + # Security headers + header { + X-Content-Type-Options nosniff + X-Frame-Options DENY + Referrer-Policy strict-origin-when-cross-origin + } + + # Optional: enable compression for static assets + encode gzip +} + + +# ── Local / LAN setup (no domain, plain HTTP) ──────────────────────────────── +# If you're running on a LAN and don't have a domain, use this instead. +# Note: browsers will require ws:// (not wss://) and you must uncheck TLS +# in Cathode's connect screen. +# +# :8080 { +# root * /var/www/cathode +# file_server +# +# handle /api* { +# reverse_proxy localhost:9000 +# } +# } diff --git a/proxy/apache.conf b/proxy/apache.conf new file mode 100644 index 0000000..810c99f --- /dev/null +++ b/proxy/apache.conf @@ -0,0 +1,89 @@ +# Apache VirtualHost config for Cathode +# Drop in /etc/apache2/sites-available/cathode.conf +# Enable with: sudo a2ensite cathode +# +# Required modules: +# sudo a2enmod ssl proxy proxy_http proxy_wstunnel rewrite headers +# +# For TLS certs use certbot: +# sudo certbot --apache -d cathode.example.com + +# HTTP → HTTPS redirect + + ServerName cathode.example.com + Redirect permanent / https://cathode.example.com/ + + + + ServerName cathode.example.com + + # TLS (certbot will fill these in, or provide your own) + SSLEngine on + SSLCertificateFile /etc/letsencrypt/live/cathode.example.com/fullchain.pem + SSLCertificateKeyFile /etc/letsencrypt/live/cathode.example.com/privkey.pem + + # Modern TLS + SSLProtocol -all +TLSv1.2 +TLSv1.3 + SSLHonorCipherOrder off + + # Serve Cathode static files + DocumentRoot /var/www/cathode + + Options -Indexes +FollowSymLinks + AllowOverride None + Require all granted + + + # Proxy WeeChat relay API — REST and WebSocket + # WeeChat listens on localhost:9000 — adjust if needed + + # Enable proxy for this vhost + ProxyRequests off + + # WebSocket proxy: must come before the plain HTTP proxy rule + # Apache uses mod_proxy_wstunnel for WebSocket upgrades + RewriteEngine on + RewriteCond %{HTTP:Upgrade} =websocket [NC] + RewriteRule ^/api(.*) ws://localhost:9000/api$1 [P,L] + + # Plain HTTP proxy for REST requests to /api + ProxyPass /api http://localhost:9000/api + ProxyPassReverse /api http://localhost:9000/api + + # Pass real client IP + ProxyPreserveHost on + RequestHeader set X-Real-IP "%{REMOTE_ADDR}s" + + # Timeouts for long-lived WebSocket connections + ProxyTimeout 3600 + + # Security headers + Header always set X-Content-Type-Options "nosniff" + Header always set X-Frame-Options "DENY" + Header always set Referrer-Policy "strict-origin-when-cross-origin" + + # Logging + ErrorLog ${APACHE_LOG_DIR}/cathode_error.log + CustomLog ${APACHE_LOG_DIR}/cathode_access.log combined + + + +# ── Local / LAN setup (no domain, plain HTTP) ──────────────────────────────── +# If you're on a LAN without a domain, use this simpler block. +# Cathode connect screen: uncheck TLS, use ws:// (port 8080 here). +# +# +# DocumentRoot /var/www/cathode +# +# Options -Indexes +# Require all granted +# +# +# ProxyRequests off +# RewriteEngine on +# RewriteCond %{HTTP:Upgrade} =websocket [NC] +# RewriteRule ^/api(.*) ws://localhost:9000/api$1 [P,L] +# ProxyPass /api http://localhost:9000/api +# ProxyPassReverse /api http://localhost:9000/api +# ProxyTimeout 3600 +# diff --git a/proxy/nginx.conf b/proxy/nginx.conf new file mode 100644 index 0000000..1483b0a --- /dev/null +++ b/proxy/nginx.conf @@ -0,0 +1,100 @@ +# nginx.conf (or drop in /etc/nginx/sites-available/cathode) +# +# This config: +# 1. Serves the Cathode static files at the root +# 2. Proxies /api/* to WeeChat's relay with WebSocket support +# 3. Handles TLS termination (you must provide your own certs, or use +# certbot: sudo certbot --nginx -d cathode.example.com) +# +# Replace cathode.example.com and cert paths as needed. + +server { + listen 80; + server_name cathode.example.com; + + # Redirect all HTTP → HTTPS + return 301 https://$host$request_uri; +} + +server { + listen 443 ssl http2; + server_name cathode.example.com; + + # TLS certificates (use certbot or provide your own) + ssl_certificate /etc/letsencrypt/live/cathode.example.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/cathode.example.com/privkey.pem; + + # Modern TLS settings + ssl_protocols TLSv1.2 TLSv1.3; + ssl_prefer_server_ciphers off; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 1d; + + # Serve Cathode static files + root /var/www/cathode; + index index.html; + + location / { + try_files $uri $uri/ =404; + } + + # Proxy WeeChat relay API — REST and WebSocket + # WeeChat listens on localhost:9000 — adjust if needed + location /api { + + proxy_pass http://localhost:9000; + + # Required for WebSocket upgrade + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + + # Standard proxy headers + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + # WebSocket connections can be long-lived — raise the timeouts + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + proxy_connect_timeout 10s; + + # Disable buffering for real-time streaming + proxy_buffering off; + } + + # Security headers + add_header X-Content-Type-Options nosniff always; + add_header X-Frame-Options DENY always; + add_header Referrer-Policy strict-origin-when-cross-origin always; + + # Compression + gzip on; + gzip_types text/css application/javascript text/plain; + gzip_min_length 1024; +} + + +# ── Local / LAN setup (no domain, plain HTTP) ──────────────────────────────── +# If you're on a LAN without a domain, use this simpler block. +# Cathode connect screen: uncheck TLS, use ws:// (port 8080 here). +# +# server { +# listen 8080; +# +# root /var/www/cathode; +# index index.html; +# +# location / { +# try_files $uri $uri/ =404; +# } +# +# location /api { +# proxy_pass http://localhost:9000; +# proxy_http_version 1.1; +# proxy_set_header Upgrade $http_upgrade; +# proxy_set_header Connection "upgrade"; +# proxy_read_timeout 3600s; +# proxy_buffering off; +# } +# } diff --git a/style.css b/style.css new file mode 100644 index 0000000..015dd16 --- /dev/null +++ b/style.css @@ -0,0 +1,1074 @@ +/* ─── Cathode — terminal IRC client ─────────────────────────────────────── */ + +@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:ital,wght@0,400;0,600;1,400&display=swap'); + +/* ─── Theme variables ─────────────────────────────────────────────────────── */ +:root, +[data-theme="dark"] { + --bg: #0a0a0a; + --bg-panel: #111111; + --bg-input: #0f0f0f; + --bg-hover: #1a1a1a; + --bg-active: #1f1f1f; + --fg: #e0e0e0; + --fg-dim: #888888; + --fg-dimmer: #3a3a3a; + --accent: #e0e0e0; + --accent-hl: #ffffff; + --border: #2a2a2a; + --border-l: #333333; + --unread: #888888; + --highlight: #ffee00; + --hl-bg: #1a1800; + --cursor: #e0e0e0; + --scrollbar: #2a2a2a; + --status-conn: #55cc55; + --status-disc: #cc5555; + --link: #88bbff; + --scanline-opacity: 0.015; + --prefix-col-width: 148px; /* updated by applyPrefixWidth() */ +} + +[data-theme="light"] { + --bg: #f5f5f5; + --bg-panel: #ebebeb; + --bg-input: #ffffff; + --bg-hover: #e0e0e0; + --bg-active: #d8d8d8; + --fg: #111111; + --fg-dim: #666666; + --fg-dimmer: #bbbbbb; + --accent-hl: #000000; + --border: #d0d0d0; + --border-l: #c0c0c0; + --unread: #555555; + --highlight: #996600; + --hl-bg: #fff8e0; + --cursor: #111111; + --scrollbar: #cccccc; + --status-conn: #228822; + --status-disc: #882222; + --link: #0055cc; + --scanline-opacity: 0; +} + +/* ─── Reset & base ────────────────────────────────────────────────────────── */ +*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } + +html, body { + height: 100%; + overflow: hidden; + font-family: 'IBM Plex Mono', 'Courier New', monospace; + font-size: 13px; + line-height: 1.5; + background: var(--bg); + color: var(--fg); + caret-color: var(--cursor); +} + +/* CRT scanline overlay */ +body::after { + content: ''; + position: fixed; + inset: 0; + pointer-events: none; + z-index: 9999; + background: repeating-linear-gradient( + 0deg, + transparent, + transparent 2px, + rgba(0,0,0,var(--scanline-opacity)) 2px, + rgba(0,0,0,var(--scanline-opacity)) 4px + ); +} + +a { color: var(--link); text-decoration: none; } +a:hover { text-decoration: underline; } + +::-webkit-scrollbar { width: 6px; height: 6px; } +::-webkit-scrollbar-track { background: transparent; } +::-webkit-scrollbar-thumb { background: var(--scrollbar); border-radius: 0; } + +/* ─── Layout shell ────────────────────────────────────────────────────────── */ +#app { + display: flex; + flex-direction: column; + height: 100vh; + width: 100vw; +} + +/* ─── Topbar ──────────────────────────────────────────────────────────────── */ +#topbar { + display: flex; + align-items: center; + gap: 12px; + padding: 0 16px; + height: 36px; + border-bottom: 1px solid var(--border); + background: var(--bg-panel); + flex-shrink: 0; + user-select: none; +} + +#topbar .logo { + font-weight: 600; + font-size: 14px; + letter-spacing: 0.15em; + color: var(--accent-hl); +} + +#topbar .logo span { + color: var(--fg-dim); + font-weight: 400; +} + +.status-pill { + display: flex; + align-items: center; + gap: 6px; + margin-left: 4px; +} + +.status-dot { + width: 7px; + height: 7px; + border-radius: 50%; + flex-shrink: 0; + transition: background 0.3s; +} +.status-dot.connected { background: var(--status-conn); box-shadow: 0 0 6px var(--status-conn); } +.status-dot.disconnected { background: var(--status-disc); } +.status-dot.connecting { background: var(--fg-dim); animation: blink 0.8s step-end infinite; } + +@keyframes blink { 50% { opacity: 0; } } + +#status-text { + font-size: 10px; + letter-spacing: 0.12em; + color: var(--fg-dim); +} + +.topbar-spacer { flex: 1; } + +.topbar-btn { + background: none; + border: 1px solid var(--border); + color: var(--fg-dim); + font-family: inherit; + font-size: 10px; + letter-spacing: 0.1em; + padding: 3px 9px; + cursor: pointer; + transition: border-color 0.15s, color 0.15s, background 0.15s; +} +.topbar-btn:hover { + border-color: var(--border-l); + color: var(--fg); +} + +/* Theme toggle — amber in dark, indigo in light */ +#theme-toggle { + color: #b8860b; + border-color: #3a2e00; +} +[data-theme="light"] #theme-toggle { + color: #4455bb; + border-color: #9aabdd; +} +#theme-toggle:hover { + color: #e0a800; + border-color: #6a5200; + background: #1a1000; +} +[data-theme="light"] #theme-toggle:hover { + color: #2233aa; + border-color: #6677cc; + background: #eef0ff; +} + +/* Quit button — red */ +#disconnect-btn { + color: #aa3333; + border-color: #3a1010; +} +[data-theme="light"] #disconnect-btn { + color: #991111; + border-color: #ddaaaa; +} +#disconnect-btn:hover { + color: #ff5555; + border-color: #aa3333; + background: #1a0000; +} +[data-theme="light"] #disconnect-btn:hover { + color: #cc1111; + border-color: #cc4444; + background: #fff0f0; +} + +/* ─── Connect screen ─────────────────────────────────────────────────────── */ +#connect-screen { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 16px; + background: var(--bg); +} + +/* HTTPS/ws warning banner — shown above the connect box */ +#http-notice { + width: 420px; + padding: 10px 14px; + border: 1px solid #553300; + border-left: 3px solid #cc6600; + background: #1a0d00; + font-size: 11px; + line-height: 1.7; + color: #cc8833; +} +[data-theme="light"] #http-notice { + background: #fff5e6; + border-color: #e09040; + border-left-color: #cc6600; + color: #7a4400; +} +#http-notice strong { color: inherit; font-weight: 600; } +#http-notice code { font-family: inherit; opacity: 0.85; } +#http-notice a { color: inherit; text-decoration: underline; } +#http-notice a:hover { opacity: 0.8; } + +#tls-locked-note { + color: #cc6600; + font-size: 10px; + letter-spacing: 0.05em; +} +[data-theme="light"] #tls-locked-note { color: #994400; } + +.connect-box { + width: 420px; + border: 1px solid var(--border-l); + background: var(--bg-panel); + padding: 0; +} + +.connect-box-header { + padding: 12px 20px; + border-bottom: 1px solid var(--border); + font-size: 11px; + letter-spacing: 0.2em; + color: var(--fg); +} + +.connect-box-body { + padding: 24px 20px; + display: flex; + flex-direction: column; + gap: 14px; +} + +.field { + display: flex; + flex-direction: column; + gap: 5px; +} + +.field label { + font-size: 10px; + letter-spacing: 0.18em; + color: var(--fg); +} + +.field input[type="text"], +.field input[type="number"], +.field input[type="password"] { + background: var(--bg-input); + border: 1px solid var(--border); + color: var(--fg); + font-family: inherit; + font-size: 13px; + padding: 7px 10px; + outline: none; + transition: border-color 0.15s; + width: 100%; +} +.field input:focus { + border-color: var(--border-l); +} + +.field-row { + display: flex; + gap: 10px; +} +.field-row .field { flex: 1; } +.field-row .field.short { flex: 0 0 100px; } + +.checkbox-row { + display: flex; + align-items: center; + gap: 8px; + font-size: 11px; + color: var(--fg); + cursor: pointer; + user-select: none; +} +.checkbox-row input { cursor: pointer; accent-color: var(--fg); } + +#port-warning { + display: none; + font-size: 11px; + color: #cc8800; + padding: 6px 8px; + border: 1px solid #553300; + background: #1a0f00; +} +[data-theme="light"] #port-warning { + background: #fff8e0; + border-color: #cc8800; +} + +#conn-error { + display: none; + font-size: 11px; + color: #ff6666; + padding: 6px 8px; + border: 1px solid #440000; + background: #110000; +} +[data-theme="light"] #conn-error { + background: #fff0f0; + border-color: #cc4444; + color: #881111; +} + +.connect-actions { + display: flex; + gap: 10px; + padding-top: 4px; +} + +.btn-primary { + flex: 1; + background: var(--fg); + color: var(--bg); + border: none; + font-family: inherit; + font-size: 11px; + font-weight: 600; + letter-spacing: 0.18em; + padding: 9px 16px; + cursor: pointer; + transition: opacity 0.15s; +} +.btn-primary:hover:not(:disabled) { opacity: 0.85; } +.btn-primary:disabled { opacity: 0.4; cursor: not-allowed; } + +.btn-secondary { + background: none; + border: 1px solid var(--border-l); + color: var(--fg-dim); + font-family: inherit; + font-size: 10px; + letter-spacing: 0.12em; + padding: 9px 12px; + cursor: pointer; + white-space: nowrap; + transition: border-color 0.15s, color 0.15s; +} +.btn-secondary:hover { border-color: var(--fg-dim); color: var(--fg); } + +/* Self-signed cert hint — amber callout box */ +.cert-hint { + font-size: 11px; + color: #c8920a; + line-height: 1.6; + padding: 8px 12px; + border: 1px solid #6a4a00; + background: #1a1000; + border-left: 3px solid #c8920a; +} +[data-theme="light"] .cert-hint { + color: #7a5200; + border-color: #e0b040; + border-left-color: #c8920a; + background: #fffae8; +} + +/* ─── Chat screen ────────────────────────────────────────────────────────── */ +#chat-screen { + flex: 1; + display: flex; + overflow: hidden; +} + +/* ─── Buffer sidebar ─────────────────────────────────────────────────────── */ +#sidebar { + width: 180px; + flex-shrink: 0; + display: flex; + flex-direction: column; + border-right: 1px solid var(--border); + background: var(--bg-panel); + overflow: hidden; +} + +#sidebar-header { + padding: 8px 12px; + font-size: 10px; + letter-spacing: 0.18em; + color: var(--fg-dim); + border-bottom: 1px solid var(--border); + flex-shrink: 0; +} + +#buffer-list { + flex: 1; + overflow-y: auto; + padding: 4px 0; +} + +/* Close (×) button on buffer items — hidden until hover */ +.buf-close { + background: none; + border: none; + color: var(--fg-dimmer); + font-size: 13px; + line-height: 1; + padding: 0 2px; + cursor: pointer; + flex-shrink: 0; + opacity: 0; + transition: opacity 0.1s, color 0.1s; + margin-left: auto; +} +.buffer-item:hover .buf-close { opacity: 1; } +.buf-close:hover { color: var(--status-disc); } + +/* Sidebar footer — join button */ +#sidebar-footer { + flex-shrink: 0; + border-top: 1px solid var(--border); + padding: 6px 8px; +} +.sidebar-footer-btn { + width: 100%; + background: none; + border: 1px solid var(--border); + color: var(--fg-dim); + font-family: inherit; + font-size: 10px; + letter-spacing: 0.12em; + padding: 5px 0; + cursor: pointer; + transition: border-color 0.15s, color 0.15s, background 0.15s; +} +.sidebar-footer-btn:hover { + border-color: var(--border-l); + color: var(--fg); + background: var(--bg-hover); +} + +.buffer-item { + display: flex; + align-items: center; + gap: 6px; + padding: 4px 12px; + cursor: pointer; + font-size: 12px; + color: var(--fg-dim); + transition: background 0.1s, color 0.1s; + min-height: 24px; +} +.buffer-item:hover { background: var(--bg-hover); color: var(--fg); } +.buffer-item.active { background: var(--bg-active); color: var(--fg); } +.buffer-item.unread { color: var(--unread); } +.buffer-item.highlight { color: var(--highlight); } + +/* Group header (synthetic, no buffer) */ +.buf-group-header { + padding: 8px 12px 3px; + font-size: 10px; + letter-spacing: 0.15em; + color: var(--fg-dimmer); + text-transform: uppercase; + user-select: none; + border-top: 1px solid var(--border); + margin-top: 2px; +} +.buf-group-header:first-child { + border-top: none; + margin-top: 0; +} + +/* Server buffer row (bold, acts as section header) */ +.buffer-item.buf-server { + font-size: 11px; + letter-spacing: 0.06em; + color: var(--fg); + padding-top: 6px; + padding-bottom: 6px; + border-top: 1px solid var(--border); + margin-top: 2px; +} +.buffer-item.buf-server:first-child { + border-top: none; + margin-top: 0; +} +.buffer-item.buf-server .buf-name { + font-weight: 600; +} + +/* Channel/private rows indented under server */ +.buffer-item.buf-indented { + padding-left: 20px; +} +.buffer-item.buf-indented .buf-num { + width: 14px; +} + +.buf-num { + font-size: 10px; + color: var(--fg-dimmer); + width: 16px; + flex-shrink: 0; + text-align: right; +} +.buf-name { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.badge { + font-size: 9px; + padding: 1px 5px; + background: var(--fg-dimmer); + color: var(--bg); + flex-shrink: 0; +} +.hl-badge { + background: var(--highlight); + color: #000; +} + +/* ─── Main chat area ─────────────────────────────────────────────────────── */ +#main { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; + min-width: 0; + position: relative; /* anchor for .new-msg-banner */ +} + +/* ─── Chat header ────────────────────────────────────────────────────────── */ +#chat-header { + padding: 6px 14px; + border-bottom: 1px solid var(--border); + background: var(--bg-panel); + flex-shrink: 0; + display: flex; + align-items: baseline; + gap: 12px; + min-height: 34px; +} + +#chat-title { + font-weight: 600; + font-size: 13px; + color: var(--accent-hl); + flex-shrink: 0; +} + +#chat-topic { + font-size: 11px; + color: var(--fg-dim); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + flex: 1; +} + +/* Buttons sitting in the chat header */ +.header-btn { + background: none; + border: 1px solid var(--border); + color: var(--fg-dim); + font-family: inherit; + font-size: 10px; + letter-spacing: 0.1em; + padding: 2px 8px; + cursor: pointer; + flex-shrink: 0; + margin-left: auto; + transition: border-color 0.15s, color 0.15s; +} +.header-btn:hover { border-color: var(--border-l); color: var(--fg); } +.header-btn.sf-off { color: #cc8800; border-color: #553300; } +[data-theme="light"] .header-btn.sf-off { color: #885500; border-color: #cc8800; } + +/* ─── Messages ───────────────────────────────────────────────────────────── */ +#messages { + flex: 1; + overflow-y: auto; + padding: 6px 0; + background: var(--bg); +} + +.msg-row { + display: flex; + align-items: stretch; /* stretch so msg-sep fills full row height */ + gap: 0; + padding: 1px 14px; + font-size: 13px; + line-height: 1.55; + transition: background 0.1s; +} +.msg-row:hover { background: var(--bg-hover); } +.msg-row.msg-highlight { background: var(--hl-bg); } +.msg-row.msg-system { color: var(--fg-dim); font-style: italic; } + +/* ─── Unread divider ─────────────────────────────────────────────────────── */ +.read-marker { + display: flex; + align-items: center; + gap: 8px; + padding: 4px 14px; + font-size: 10px; + letter-spacing: 0.15em; + color: #cc8800; + user-select: none; + pointer-events: none; +} +[data-theme="light"] .read-marker { color: #885500; } +.read-marker::before, +.read-marker::after { + content: ''; + flex: 1; + height: 1px; + background: currentColor; + opacity: 0.4; +} + +.msg-time { + color: var(--fg-dimmer); + font-size: 11px; + flex-shrink: 0; + width: 60px; + padding-right: 6px; + padding-top: 2px; /* realign text to top within stretched row */ + user-select: none; +} + +.msg-prefix { + flex-shrink: 0; + width: var(--prefix-col-width); + text-align: right; + padding-right: 8px; + padding-top: 2px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + /* colour and content come from WeeChat ANSI — no override here */ +} + +.msg-text { + flex: 1; + word-break: break-word; + overflow-wrap: anywhere; + color: var(--fg); + min-width: 0; + padding-top: 2px; + padding-bottom: 1px; +} + +/* Border-based separator — stretches naturally with the row height */ +.msg-sep { + flex-shrink: 0; + width: 0; + border-left: 1px solid var(--fg-dimmer); + margin-right: 8px; + user-select: none; +} + +/* ─── Media preview ──────────────────────────────────────────────────────── */ +.media-toggle { + background: none; + border: none; + color: var(--link); + font-family: inherit; + font-size: 11px; + padding: 0 4px; + cursor: pointer; + vertical-align: middle; + text-decoration: underline; + transition: opacity 0.1s; +} +.media-toggle:hover { opacity: 0.75; } + +.media-preview { + display: block; + margin: 4px 0 2px; +} +.preview-img { + max-width: min(480px, 100%); + max-height: 300px; + display: block; + border: 1px solid var(--border); + cursor: pointer; +} +.preview-img:hover { border-color: var(--border-l); } +.preview-vid { + max-width: min(480px, 100%); + display: block; + border: 1px solid var(--border); +} +.new-msg-banner { + position: absolute; + bottom: 46px; /* above inputbar */ + left: 50%; + transform: translateX(-50%); + background: var(--fg); + color: var(--bg); + font-family: inherit; + font-size: 11px; + font-weight: 600; + letter-spacing: 0.12em; + padding: 5px 16px; + cursor: pointer; + white-space: nowrap; + z-index: 100; + opacity: 0.92; + transition: opacity 0.15s; +} +.new-msg-banner:hover { opacity: 1; } + +/* ─── Nicklist panel — always visible ────────────────────────────────────── */ +#nicklist-panel { + width: 150px; + flex-shrink: 0; + display: flex; + flex-direction: column; + border-left: 1px solid var(--border); + background: var(--bg-panel); + overflow: hidden; +} + +#nicklist-header { + padding: 8px 12px; + font-size: 10px; + letter-spacing: 0.18em; + color: var(--fg-dim); + border-bottom: 1px solid var(--border); + flex-shrink: 0; + user-select: none; +} + +#nicklist { + flex: 1; + overflow-y: auto; + padding: 4px 0; +} + +.nick-item { + display: flex; + align-items: center; + padding: 2px 10px; + font-size: 12px; + color: var(--fg-dim); + gap: 3px; + cursor: pointer; + transition: background 0.1s, color 0.1s; +} +.nick-item:hover { + background: var(--bg-hover); + color: var(--fg); +} +.nick-pfx { + color: var(--fg-dimmer); + width: 10px; + flex-shrink: 0; +} +.nick-name { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +/* ─── Nick context menu ──────────────────────────────────────────────────── */ +.nick-overlay { + position: fixed; + inset: 0; + z-index: 1000; + display: flex; + align-items: center; + justify-content: center; + background: rgba(0,0,0,0.5); +} +[data-theme="light"] .nick-overlay { background: rgba(0,0,0,0.2); } + +.nick-menu { + background: var(--bg-panel); + border: 1px solid var(--border-l); + min-width: 210px; + display: flex; + flex-direction: column; +} + +.nick-menu-hdr { + padding: 10px 16px; + font-size: 12px; + font-weight: 600; + letter-spacing: 0.08em; + color: var(--accent-hl); + border-bottom: 1px solid var(--border); + user-select: none; +} + +.nick-menu-btn { + background: none; + border: none; + border-bottom: 1px solid var(--border); + color: var(--fg-dim); + font-family: inherit; + font-size: 12px; + padding: 9px 16px; + text-align: left; + cursor: pointer; + transition: background 0.1s, color 0.1s; +} +.nick-menu-btn:last-child { border-bottom: none; } +.nick-menu-btn:hover { background: var(--bg-hover); color: var(--fg); } + +/* ─── Input bar ──────────────────────────────────────────────────────────── */ +#inputbar { + display: flex; + align-items: center; + border-top: 1px solid var(--border); + background: var(--bg-panel); + flex-shrink: 0; + height: 38px; +} + +/* ─── Emoji picker popup ─────────────────────────────────────────────────── */ +#emoji-popup { + position: absolute; + bottom: 42px; /* sits just above the inputbar */ + right: 80px; + z-index: 200; + filter: drop-shadow(0 4px 16px rgba(0,0,0,0.5)); +} + +/* Wire Cathode's theme variables into emoji-picker-element's CSS custom props */ +emoji-picker { + --background: var(--bg-panel); + --border-color: var(--border); + --border-radius: 0; + --button-active-background: var(--bg-active); + --button-hover-background: var(--bg-hover); + --category-emoji-padding: .25rem; + --emoji-padding: .25rem; + --emoji-size: 1.4rem; + --indicator-color: var(--fg); + --input-border-color: var(--border); + --input-border-radius: 0; + --input-font-color: var(--fg); + --input-placeholder-color: var(--fg-dim); + --outline-color: var(--border-l); + --search-background: var(--bg-input); + --text-color: var(--fg); + --category-font-color: var(--fg-dim); + font-family: 'IBM Plex Mono', monospace; + width: 320px; + height: 360px; +} + +#input-prompt { + padding: 0 10px; + color: var(--fg-dimmer); + font-size: 14px; + user-select: none; + flex-shrink: 0; +} + +#chat-input { + flex: 1; + background: transparent; + border: none; + color: var(--fg); + font-family: inherit; + font-size: 13px; + outline: none; + padding: 0; + height: 100%; +} +#chat-input::placeholder { color: var(--fg-dimmer); } + +#send-btn { + background: none; + border: none; + border-left: 1px solid var(--border); + color: var(--fg-dim); + font-family: inherit; + font-size: 10px; + letter-spacing: 0.12em; + padding: 0 14px; + height: 100%; + cursor: pointer; + flex-shrink: 0; + transition: color 0.15s, background 0.15s; +} +#send-btn:hover { color: var(--fg); background: var(--bg-hover); } + +/* Paperclip upload button in inputbar */ +.inputbar-btn { + background: none; + border: none; + border-left: 1px solid var(--border); + color: var(--fg-dim); + font-size: 14px; + padding: 0 10px; + height: 100%; + cursor: pointer; + flex-shrink: 0; + transition: color 0.15s, background 0.15s; +} +.inputbar-btn:hover { color: var(--fg); background: var(--bg-hover); } +.inputbar-btn.uploading { color: #cc8800; animation: blink 0.6s step-end infinite; } +.inputbar-btn.upload-ok { color: var(--status-conn); } +.inputbar-btn.upload-err { color: var(--status-disc); } + +/* ─── Drag overlay ───────────────────────────────────────────────────────── */ +#drag-overlay { + position: fixed; + inset: 0; + z-index: 500; + background: rgba(0,0,0,0.75); + display: flex; + align-items: center; + justify-content: center; + pointer-events: none; +} +[data-theme="light"] #drag-overlay { background: rgba(255,255,255,0.75); } + +.drag-overlay-inner { + border: 2px dashed var(--fg-dim); + padding: 40px 60px; + font-size: 16px; + letter-spacing: 0.3em; + color: var(--fg); +} + +/* Messages area drag-over state */ +#messages.drag-over { outline: 2px dashed var(--fg-dim); outline-offset: -4px; } + +/* ─── Settings panel ─────────────────────────────────────────────────────── */ +#settings-overlay { + position: fixed; + inset: 0; + z-index: 900; + background: rgba(0,0,0,0.5); + display: flex; + align-items: center; + justify-content: center; +} +[data-theme="light"] #settings-overlay { background: rgba(0,0,0,0.2); } + +#settings-panel { + width: 380px; + background: var(--bg-panel); + border: 1px solid var(--border-l); + display: flex; + flex-direction: column; + max-height: 80vh; +} + +.settings-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px 16px; + border-bottom: 1px solid var(--border); + font-size: 11px; + letter-spacing: 0.2em; + color: var(--fg); + flex-shrink: 0; +} + +#settings-close { + background: none; + border: none; + color: var(--fg-dim); + font-family: inherit; + font-size: 12px; + cursor: pointer; + padding: 0 2px; +} +#settings-close:hover { color: var(--fg); } + +.settings-body { + padding: 20px 16px; + display: flex; + flex-direction: column; + gap: 12px; + overflow-y: auto; +} + +.settings-section { + font-size: 10px; + letter-spacing: 0.18em; + color: var(--fg-dimmer); + border-bottom: 1px solid var(--border); + padding-bottom: 4px; + margin-top: 4px; +} + +.settings-body .field label { + font-size: 10px; + letter-spacing: 0.12em; + color: var(--fg-dim); +} + +.settings-body select { + background: var(--bg-input); + border: 1px solid var(--border); + color: var(--fg); + font-family: inherit; + font-size: 13px; + padding: 7px 10px; + outline: none; + width: 100%; + cursor: pointer; +} +.settings-body select:focus { border-color: var(--border-l); } + +.settings-actions { + padding-top: 8px; +} + +/* ─── Footer ─────────────────────────────────────────────────────────────── */ +#footer { + padding: 3px 14px; + font-size: 10px; + color: var(--fg-dimmer); + border-top: 1px solid var(--border); + background: var(--bg-panel); + flex-shrink: 0; + letter-spacing: 0.05em; +} +#footer a { color: var(--fg-dimmer); } +#footer a:hover { color: var(--fg-dim); } + +/* ─── Responsive ─────────────────────────────────────────────────────────── */ +@media (max-width: 700px) { + #sidebar { width: 140px; } + #nicklist-panel { display: none; } + .msg-prefix { width: 90px; } + .msg-time { width: 56px; font-size: 10px; } +} + +@media (max-width: 480px) { + #sidebar { width: 110px; } + .msg-prefix { width: 70px; } +} diff --git a/vendor/emoji-data.json b/vendor/emoji-data.json new file mode 100644 index 0000000..2074096 --- /dev/null +++ b/vendor/emoji-data.json @@ -0,0 +1 @@ +{"categories":[{"id":"people","emojis":["grinning","smiley","smile","grin","laughing","sweat_smile","joy","slightly_smiling_face","upside_down_face","wink","blush","innocent","heart_eyes","kissing_heart","kissing","relaxed","kissing_closed_eyes","kissing_smiling_eyes","yum","stuck_out_tongue","stuck_out_tongue_winking_eye","stuck_out_tongue_closed_eyes","money_mouth_face","hugging_face","thinking_face","zipper_mouth_face","neutral_face","expressionless","no_mouth","smirk","unamused","face_with_rolling_eyes","grimacing","relieved","pensive","sleepy","sleeping","mask","face_with_thermometer","face_with_head_bandage","dizzy_face","sunglasses","nerd_face","confused","worried","slightly_frowning_face","white_frowning_face","open_mouth","hushed","astonished","flushed","frowning","anguished","fearful","cold_sweat","disappointed_relieved","cry","sob","scream","confounded","persevere","disappointed","sweat","weary","tired_face","triumph","rage","angry","smiling_imp","imp","skull","skull_and_crossbones","hankey","japanese_ogre","japanese_goblin","ghost","alien","space_invader","robot_face","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","love_letter","cupid","gift_heart","sparkling_heart","heartpulse","heartbeat","revolving_hearts","two_hearts","heart_decoration","heavy_heart_exclamation_mark_ornament","broken_heart","heart","yellow_heart","green_heart","blue_heart","purple_heart","kiss","100","anger","boom","dizzy","sweat_drops","dash","wave","raised_hand_with_fingers_splayed","hand","spock-hand","ok_hand","v","the_horns","point_left","point_right","point_up_2","middle_finger","point_down","point_up","+1","-1","fist","facepunch","clap","raised_hands","open_hands","pray","writing_hand","nail_care","muscle","ear","nose","eyes","eye","tongue","lips","baby","boy","girl","person_with_blond_hair","man","woman","older_man","older_woman","person_frowning","person_with_pouting_face","no_good","ok_woman","information_desk_person","raising_hand","bow","cop","sleuth_or_spy","guardsman","construction_worker","princess","man_with_turban","man_with_gua_pi_mao","bride_with_veil","angel","santa","massage","haircut","walking","runner","dancer","man_in_business_suit_levitating","dancers","horse_racing","skier","snowboarder","golfer","surfer","rowboat","swimmer","person_with_ball","weight_lifter","bicyclist","mountain_bicyclist","bath","sleeping_accommodation","two_women_holding_hands","man_and_woman_holding_hands","two_men_holding_hands","couplekiss","woman-kiss-man","man-kiss-man","woman-kiss-woman","couple_with_heart","woman-heart-man","man-heart-man","woman-heart-woman","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","speaking_head_in_silhouette","bust_in_silhouette","busts_in_silhouette","footprints","hole","speech_balloon","eye-in-speech-bubble","left_speech_bubble","right_anger_bubble","thought_balloon","zzz"]},{"id":"nature","emojis":["monkey_face","monkey","dog","dog2","poodle","wolf","cat","cat2","lion_face","tiger","tiger2","leopard","horse","racehorse","unicorn_face","cow","ox","water_buffalo","cow2","pig","pig2","boar","pig_nose","ram","sheep","goat","dromedary_camel","camel","elephant","mouse","mouse2","rat","hamster","rabbit","rabbit2","chipmunk","bear","koala","panda_face","feet","turkey","chicken","rooster","hatching_chick","baby_chick","hatched_chick","bird","penguin","dove_of_peace","frog","crocodile","turtle","snake","dragon_face","dragon","whale","whale2","dolphin","fish","tropical_fish","blowfish","octopus","shell","snail","bug","ant","bee","ladybug","spider","spider_web","scorpion","bouquet","cherry_blossom","white_flower","rosette","rose","hibiscus","sunflower","blossom","tulip","seedling","evergreen_tree","deciduous_tree","palm_tree","cactus","ear_of_rice","herb","shamrock","four_leaf_clover","maple_leaf","fallen_leaf","leaves","mushroom"]},{"id":"foods","emojis":["grapes","melon","watermelon","tangerine","lemon","banana","pineapple","apple","green_apple","pear","peach","cherries","strawberry","tomato","eggplant","corn","hot_pepper","chestnut","bread","cheese_wedge","meat_on_bone","poultry_leg","hamburger","fries","pizza","hotdog","taco","burrito","fried_egg","stew","popcorn","bento","rice_cracker","rice_ball","rice","curry","ramen","spaghetti","sweet_potato","oden","sushi","fried_shrimp","fish_cake","dango","crab","icecream","shaved_ice","ice_cream","doughnut","cookie","birthday","cake","chocolate_bar","candy","lollipop","custard","honey_pot","baby_bottle","coffee","tea","sake","champagne","wine_glass","cocktail","tropical_drink","beer","beers","knife_fork_plate","fork_and_knife","hocho","amphora"]},{"id":"activity","emojis":["jack_o_lantern","christmas_tree","fireworks","sparkler","sparkles","balloon","tada","confetti_ball","tanabata_tree","bamboo","dolls","flags","wind_chime","rice_scene","ribbon","gift","reminder_ribbon","admission_tickets","ticket","medal","trophy","sports_medal","soccer","baseball","basketball","volleyball","football","rugby_football","tennis","bowling","cricket_bat_and_ball","field_hockey_stick_and_ball","ice_hockey_stick_and_puck","table_tennis_paddle_and_ball","badminton_racquet_and_shuttlecock","golf","ice_skate","fishing_pole_and_fish","running_shirt_with_sash","ski","dart","gun","8ball","crystal_ball","video_game","joystick","slot_machine","game_die","spades","hearts","diamonds","clubs","black_joker","mahjong","flower_playing_cards","performing_arts","frame_with_picture","art"]},{"id":"places","emojis":["earth_africa","earth_americas","earth_asia","globe_with_meridians","world_map","japan","snow_capped_mountain","mountain","volcano","mount_fuji","camping","beach_with_umbrella","desert","desert_island","national_park","stadium","classical_building","building_construction","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","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","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","oncoming_automobile","blue_car","truck","articulated_lorry","tractor","racing_car","racing_motorcycle","bike","busstop","motorway","railway_track","oil_drum","fuelpump","rotating_light","traffic_light","vertical_traffic_light","construction","anchor","boat","speedboat","passenger_ship","ferry","motor_boat","ship","airplane","small_airplane","airplane_departure","airplane_arriving","seat","helicopter","suspension_railway","mountain_cableway","aerial_tramway","satellite","rocket","bellhop_bell","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","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","star","star2","stars","milky_way","cloud","partly_sunny","thunder_cloud_and_rain","mostly_sunny","barely_sunny","partly_sunny_rain","rain_cloud","snow_cloud","lightning","tornado","fog","wind_blowing_face","cyclone","rainbow","closed_umbrella","umbrella","umbrella_with_rain_drops","umbrella_on_ground","zap","snowflake","snowman","snowman_without_snow","comet","fire","droplet","ocean"]},{"id":"objects","emojis":["eyeglasses","dark_sunglasses","necktie","shirt","jeans","dress","kimono","bikini","womans_clothes","purse","handbag","pouch","shopping_bags","school_satchel","mans_shoe","athletic_shoe","high_heel","sandal","boot","crown","womans_hat","tophat","mortar_board","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","guitar","musical_keyboard","trumpet","violin","iphone","calling","phone","telephone_receiver","pager","fax","battery","electric_plug","computer","desktop_computer","printer","keyboard","three_button_mouse","trackball","minidisc","floppy_disk","cd","dvd","movie_camera","film_frames","film_projector","clapper","tv","camera","camera_with_flash","video_camera","vhs","mag","mag_right","candle","bulb","flashlight","izakaya_lantern","notebook_with_decorative_cover","closed_book","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","yen","dollar","euro","pound","money_with_wings","credit_card","chart","email","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","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","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","pick","hammer_and_pick","hammer_and_wrench","dagger_knife","crossed_swords","bomb","bow_and_arrow","shield","wrench","nut_and_bolt","gear","compression","scales","link","chains","alembic","microscope","telescope","satellite_antenna","syringe","pill","door","bed","couch_and_lamp","toilet","shower","bathtub","smoking","coffin","funeral_urn","moyai"]},{"id":"symbols","emojis":["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","heavy_multiplication_x","heavy_plus_sign","heavy_minus_sign","heavy_division_sign","bangbang","interrobang","question","grey_question","grey_exclamation","exclamation","wavy_dash","currency_exchange","heavy_dollar_sign","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","one","two","three","four","five","six","seven","eight","nine","keycap_ten","capital_abcd","abcd","1234","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_blue_circle","black_circle","white_circle","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"]},{"id":"flags","emojis":["checkered_flag","cn","crossed_flags","de","es","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","flag-co","flag-cp","flag-cr","flag-cu","flag-cv","flag-cw","flag-cx","flag-cy","flag-cz","flag-dg","flag-dj","flag-dk","flag-dm","flag-do","flag-dz","flag-ea","flag-ec","flag-ee","flag-eg","flag-eh","flag-er","flag-et","flag-eu","flag-fi","flag-fj","flag-fk","flag-fm","flag-fo","flag-ga","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","flag-je","flag-jm","flag-jo","flag-ke","flag-kg","flag-kh","flag-ki","flag-km","flag-kn","flag-kp","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","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-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","fr","gb","it","jp","kr","ru","triangular_flag_on_post","us","waving_black_flag","waving_white_flag"]}],"emojis":{"100":{"id":"100","name":"Hundred Points","keywords":["100","score","perfect","numbers","century","exam","quiz","test","pass"],"skins":[{"unified":"1f4af","native":"💯"}],"version":1},"1234":{"id":"1234","name":"Input Numbers","keywords":["1234","blue","square","1","2","3","4"],"skins":[{"unified":"1f522","native":"🔢"}],"version":1},"grinning":{"id":"grinning","name":"Grinning Face","emoticons":[":D"],"keywords":["smile","happy","joy",":D","grin"],"skins":[{"unified":"1f600","native":"😀"}],"version":1},"smiley":{"id":"smiley","name":"Grinning Face with Big Eyes","emoticons":[":)","=)","=-)"],"keywords":["smiley","happy","joy","haha",":D",":)","smile","funny"],"skins":[{"unified":"1f603","native":"😃"}],"version":1},"smile":{"id":"smile","name":"Grinning Face with Smiling Eyes","emoticons":[":)","C:","c:",":D",":-D"],"keywords":["smile","happy","joy","funny","haha","laugh","like",":D",":)"],"skins":[{"unified":"1f604","native":"😄"}],"version":1},"grin":{"id":"grin","name":"Beaming Face with Smiling Eyes","keywords":["grin","happy","smile","joy","kawaii"],"skins":[{"unified":"1f601","native":"😁"}],"version":1},"laughing":{"id":"laughing","name":"Grinning Squinting Face","emoticons":[":>",":->"],"keywords":["laughing","satisfied","happy","joy","lol","haha","glad","XD","laugh"],"skins":[{"unified":"1f606","native":"😆"}],"version":1},"sweat_smile":{"id":"sweat_smile","name":"Grinning Face with Sweat","keywords":["smile","hot","happy","laugh","relief"],"skins":[{"unified":"1f605","native":"😅"}],"version":1},"joy":{"id":"joy","name":"Face with Tears of Joy","keywords":["cry","weep","happy","happytears","haha"],"skins":[{"unified":"1f602","native":"😂"}],"version":1},"slightly_smiling_face":{"id":"slightly_smiling_face","name":"Slightly Smiling Face","emoticons":[":)","(:",":-)"],"keywords":["smile"],"skins":[{"unified":"1f642","native":"🙂"}],"version":1},"upside_down_face":{"id":"upside_down_face","name":"Upside-Down Face","keywords":["upside","down","flipped","silly","smile"],"skins":[{"unified":"1f643","native":"🙃"}],"version":1},"wink":{"id":"wink","name":"Winking Face","emoticons":[";)",";-)"],"keywords":["wink","happy","mischievous","secret",";)","smile","eye"],"skins":[{"unified":"1f609","native":"😉"}],"version":1},"blush":{"id":"blush","name":"Smiling Face with Smiling Eyes","emoticons":[":)"],"keywords":["blush","smile","happy","flushed","crush","embarrassed","shy","joy"],"skins":[{"unified":"1f60a","native":"😊"}],"version":1},"innocent":{"id":"innocent","name":"Smiling Face with Halo","keywords":["innocent","angel","heaven"],"skins":[{"unified":"1f607","native":"😇"}],"version":1},"heart_eyes":{"id":"heart_eyes","name":"Smiling Face with Heart-Eyes","keywords":["heart","eyes","love","like","affection","valentines","infatuation","crush"],"skins":[{"unified":"1f60d","native":"😍"}],"version":1},"kissing_heart":{"id":"kissing_heart","name":"Face Blowing a Kiss","emoticons":[":*",":-*"],"keywords":["kissing","heart","love","like","affection","valentines","infatuation"],"skins":[{"unified":"1f618","native":"😘"}],"version":1},"kissing":{"id":"kissing","name":"Kissing Face","keywords":["love","like","3","valentines","infatuation","kiss"],"skins":[{"unified":"1f617","native":"😗"}],"version":1},"relaxed":{"id":"relaxed","name":"Smiling Face","keywords":["relaxed","blush","massage","happiness"],"skins":[{"unified":"263a-fe0f","native":"☺️"}],"version":1},"kissing_closed_eyes":{"id":"kissing_closed_eyes","name":"Kissing Face with Closed Eyes","keywords":["love","like","affection","valentines","infatuation","kiss"],"skins":[{"unified":"1f61a","native":"😚"}],"version":1},"kissing_smiling_eyes":{"id":"kissing_smiling_eyes","name":"Kissing Face with Smiling Eyes","keywords":["affection","valentines","infatuation","kiss"],"skins":[{"unified":"1f619","native":"😙"}],"version":1},"yum":{"id":"yum","name":"Face Savoring Food","keywords":["yum","happy","joy","tongue","smile","silly","yummy","nom","delicious","savouring"],"skins":[{"unified":"1f60b","native":"😋"}],"version":1},"stuck_out_tongue":{"id":"stuck_out_tongue","name":"Face with Tongue","emoticons":[":p",":-p",":P",":-P",":b",":-b"],"keywords":["stuck","out","prank","childish","playful","mischievous","smile"],"skins":[{"unified":"1f61b","native":"😛"}],"version":1},"stuck_out_tongue_winking_eye":{"id":"stuck_out_tongue_winking_eye","name":"Winking Face with Tongue","emoticons":[";p",";-p",";b",";-b",";P",";-P"],"keywords":["stuck","out","eye","prank","childish","playful","mischievous","smile","wink"],"skins":[{"unified":"1f61c","native":"😜"}],"version":1},"stuck_out_tongue_closed_eyes":{"id":"stuck_out_tongue_closed_eyes","name":"Squinting Face with Tongue","keywords":["stuck","out","closed","eyes","prank","playful","mischievous","smile"],"skins":[{"unified":"1f61d","native":"😝"}],"version":1},"money_mouth_face":{"id":"money_mouth_face","name":"Money-Mouth Face","keywords":["money","mouth","rich","dollar"],"skins":[{"unified":"1f911","native":"🤑"}],"version":1},"hugging_face":{"id":"hugging_face","name":"Hugging Face","keywords":["smile","hug"],"skins":[{"unified":"1f917","native":"🤗"}],"version":1},"thinking_face":{"id":"thinking_face","name":"Thinking Face","keywords":["hmmm","think","consider"],"skins":[{"unified":"1f914","native":"🤔"}],"version":1},"zipper_mouth_face":{"id":"zipper_mouth_face","name":"Zipper-Mouth Face","keywords":["zipper","mouth","sealed","secret"],"skins":[{"unified":"1f910","native":"🤐"}],"version":1},"neutral_face":{"id":"neutral_face","name":"Neutral Face","emoticons":[":|",":-|"],"keywords":["indifference","meh",":",""],"skins":[{"unified":"1f610","native":"😐"}],"version":1},"expressionless":{"id":"expressionless","name":"Expressionless Face","emoticons":["-_-"],"keywords":["indifferent","-","","meh","deadpan"],"skins":[{"unified":"1f611","native":"😑"}],"version":1},"no_mouth":{"id":"no_mouth","name":"Face Without Mouth","keywords":["no","hellokitty"],"skins":[{"unified":"1f636","native":"😶"}],"version":1},"smirk":{"id":"smirk","name":"Smirking Face","keywords":["smirk","smile","mean","prank","smug","sarcasm"],"skins":[{"unified":"1f60f","native":"😏"}],"version":1},"unamused":{"id":"unamused","name":"Unamused Face","emoticons":[":("],"keywords":["indifference","bored","straight","serious","sarcasm","unimpressed","skeptical","dubious","side","eye"],"skins":[{"unified":"1f612","native":"😒"}],"version":1},"face_with_rolling_eyes":{"id":"face_with_rolling_eyes","name":"Face with Rolling Eyes","keywords":["eyeroll","frustrated"],"skins":[{"unified":"1f644","native":"🙄"}],"version":1},"grimacing":{"id":"grimacing","name":"Grimacing Face","keywords":["grimace","teeth"],"skins":[{"unified":"1f62c","native":"😬"}],"version":1},"relieved":{"id":"relieved","name":"Relieved Face","keywords":["relaxed","phew","massage","happiness"],"skins":[{"unified":"1f60c","native":"😌"}],"version":1},"pensive":{"id":"pensive","name":"Pensive Face","keywords":["sad","depressed","upset"],"skins":[{"unified":"1f614","native":"😔"}],"version":1},"sleepy":{"id":"sleepy","name":"Sleepy Face","keywords":["tired","rest","nap"],"skins":[{"unified":"1f62a","native":"😪"}],"version":1},"sleeping":{"id":"sleeping","name":"Sleeping Face","keywords":["tired","sleepy","night","zzz"],"skins":[{"unified":"1f634","native":"😴"}],"version":1},"mask":{"id":"mask","name":"Face with Medical Mask","keywords":["sick","ill","disease","covid"],"skins":[{"unified":"1f637","native":"😷"}],"version":1},"face_with_thermometer":{"id":"face_with_thermometer","name":"Face with Thermometer","keywords":["sick","temperature","cold","fever","covid"],"skins":[{"unified":"1f912","native":"🤒"}],"version":1},"face_with_head_bandage":{"id":"face_with_head_bandage","name":"Face with Head-Bandage","keywords":["head","bandage","injured","clumsy","hurt"],"skins":[{"unified":"1f915","native":"🤕"}],"version":1},"dizzy_face":{"id":"dizzy_face","name":"Dizzy Face","keywords":["spent","unconscious","xox"],"skins":[{"unified":"1f635","native":"😵"}],"version":1},"sunglasses":{"id":"sunglasses","name":"Smiling Face with Sunglasses","emoticons":["8)"],"keywords":["cool","smile","summer","beach","sunglass"],"skins":[{"unified":"1f60e","native":"😎"}],"version":1},"nerd_face":{"id":"nerd_face","name":"Nerd Face","keywords":["nerdy","geek","dork"],"skins":[{"unified":"1f913","native":"🤓"}],"version":1},"confused":{"id":"confused","name":"Confused Face","emoticons":[":\\",":-\\",":/",":-/"],"keywords":["indifference","huh","weird","hmmm",":/"],"skins":[{"unified":"1f615","native":"😕"}],"version":1},"worried":{"id":"worried","name":"Worried Face","keywords":["concern","nervous",":("],"skins":[{"unified":"1f61f","native":"😟"}],"version":1},"slightly_frowning_face":{"id":"slightly_frowning_face","name":"Slightly Frowning Face","keywords":["disappointed","sad","upset"],"skins":[{"unified":"1f641","native":"🙁"}],"version":1},"white_frowning_face":{"id":"white_frowning_face","name":"Frowning Face","keywords":["white","sad","upset","frown"],"skins":[{"unified":"2639-fe0f","native":"☹️"}],"version":1},"open_mouth":{"id":"open_mouth","name":"Face with Open Mouth","emoticons":[":o",":-o",":O",":-O"],"keywords":["surprise","impressed","wow","whoa",":O"],"skins":[{"unified":"1f62e","native":"😮"}],"version":1},"hushed":{"id":"hushed","name":"Hushed Face","keywords":["woo","shh"],"skins":[{"unified":"1f62f","native":"😯"}],"version":1},"astonished":{"id":"astonished","name":"Astonished Face","keywords":["xox","surprised","poisoned"],"skins":[{"unified":"1f632","native":"😲"}],"version":1},"flushed":{"id":"flushed","name":"Flushed Face","keywords":["blush","shy","flattered"],"skins":[{"unified":"1f633","native":"😳"}],"version":1},"frowning":{"id":"frowning","name":"Frowning Face with Open Mouth","keywords":["aw","what"],"skins":[{"unified":"1f626","native":"😦"}],"version":1},"anguished":{"id":"anguished","name":"Anguished Face","emoticons":["D:"],"keywords":["stunned","nervous"],"skins":[{"unified":"1f627","native":"😧"}],"version":1},"fearful":{"id":"fearful","name":"Fearful Face","keywords":["scared","terrified","nervous"],"skins":[{"unified":"1f628","native":"😨"}],"version":1},"cold_sweat":{"id":"cold_sweat","name":"Anxious Face with Sweat","keywords":["cold","nervous"],"skins":[{"unified":"1f630","native":"😰"}],"version":1},"disappointed_relieved":{"id":"disappointed_relieved","name":"Sad but Relieved Face","keywords":["disappointed","phew","sweat","nervous"],"skins":[{"unified":"1f625","native":"😥"}],"version":1},"cry":{"id":"cry","name":"Crying Face","emoticons":[":'("],"keywords":["cry","tears","sad","depressed","upset",":'("],"skins":[{"unified":"1f622","native":"😢"}],"version":1},"sob":{"id":"sob","name":"Loudly Crying Face","emoticons":[":'("],"keywords":["sob","cry","tears","sad","upset","depressed"],"skins":[{"unified":"1f62d","native":"😭"}],"version":1},"scream":{"id":"scream","name":"Face Screaming in Fear","keywords":["scream","munch","scared","omg"],"skins":[{"unified":"1f631","native":"😱"}],"version":1},"confounded":{"id":"confounded","name":"Confounded Face","keywords":["confused","sick","unwell","oops",":S"],"skins":[{"unified":"1f616","native":"😖"}],"version":1},"persevere":{"id":"persevere","name":"Persevering Face","keywords":["persevere","sick","no","upset","oops"],"skins":[{"unified":"1f623","native":"😣"}],"version":1},"disappointed":{"id":"disappointed","name":"Disappointed Face","emoticons":["):",":(",":-("],"keywords":["sad","upset","depressed",":("],"skins":[{"unified":"1f61e","native":"😞"}],"version":1},"sweat":{"id":"sweat","name":"Face with Cold Sweat","keywords":["downcast","hot","sad","tired","exercise"],"skins":[{"unified":"1f613","native":"😓"}],"version":1},"weary":{"id":"weary","name":"Weary Face","keywords":["tired","sleepy","sad","frustrated","upset"],"skins":[{"unified":"1f629","native":"😩"}],"version":1},"tired_face":{"id":"tired_face","name":"Tired Face","keywords":["sick","whine","upset","frustrated"],"skins":[{"unified":"1f62b","native":"😫"}],"version":1},"triumph":{"id":"triumph","name":"Face with Look of Triumph","keywords":["steam","from","nose","gas","phew","proud","pride"],"skins":[{"unified":"1f624","native":"😤"}],"version":1},"rage":{"id":"rage","name":"Pouting Face","keywords":["rage","angry","mad","hate","despise"],"skins":[{"unified":"1f621","native":"😡"}],"version":1},"angry":{"id":"angry","name":"Angry Face","emoticons":[">:(",">:-("],"keywords":["mad","annoyed","frustrated"],"skins":[{"unified":"1f620","native":"😠"}],"version":1},"smiling_imp":{"id":"smiling_imp","name":"Smiling Face with Horns","keywords":["imp","devil"],"skins":[{"unified":"1f608","native":"😈"}],"version":1},"imp":{"id":"imp","name":"Imp","keywords":["angry","face","with","horns","devil"],"skins":[{"unified":"1f47f","native":"👿"}],"version":1},"skull":{"id":"skull","name":"Skull","keywords":["dead","skeleton","creepy","death"],"skins":[{"unified":"1f480","native":"💀"}],"version":1},"skull_and_crossbones":{"id":"skull_and_crossbones","name":"Skull and Crossbones","keywords":["poison","danger","deadly","scary","death","pirate","evil"],"skins":[{"unified":"2620-fe0f","native":"☠️"}],"version":1},"hankey":{"id":"hankey","name":"Pile of Poo","keywords":["hankey","poop","shit","shitface","fail","turd"],"skins":[{"unified":"1f4a9","native":"💩"}],"version":1},"japanese_ogre":{"id":"japanese_ogre","name":"Ogre","keywords":["japanese","monster","red","mask","halloween","scary","creepy","devil","demon"],"skins":[{"unified":"1f479","native":"👹"}],"version":1},"japanese_goblin":{"id":"japanese_goblin","name":"Goblin","keywords":["japanese","red","evil","mask","monster","scary","creepy"],"skins":[{"unified":"1f47a","native":"👺"}],"version":1},"ghost":{"id":"ghost","name":"Ghost","keywords":["halloween","spooky","scary"],"skins":[{"unified":"1f47b","native":"👻"}],"version":1},"alien":{"id":"alien","name":"Alien","keywords":["UFO","paul","weird","outer","space"],"skins":[{"unified":"1f47d","native":"👽"}],"version":1},"space_invader":{"id":"space_invader","name":"Alien Monster","keywords":["space","invader","game","arcade","play"],"skins":[{"unified":"1f47e","native":"👾"}],"version":1},"robot_face":{"id":"robot_face","name":"Robot","keywords":["face","computer","machine","bot"],"skins":[{"unified":"1f916","native":"🤖"}],"version":1},"smiley_cat":{"id":"smiley_cat","name":"Grinning Cat","keywords":["smiley","animal","cats","happy","smile"],"skins":[{"unified":"1f63a","native":"😺"}],"version":1},"smile_cat":{"id":"smile_cat","name":"Grinning Cat with Smiling Eyes","keywords":["smile","animal","cats"],"skins":[{"unified":"1f638","native":"😸"}],"version":1},"joy_cat":{"id":"joy_cat","name":"Cat with Tears of Joy","keywords":["animal","cats","haha","happy"],"skins":[{"unified":"1f639","native":"😹"}],"version":1},"heart_eyes_cat":{"id":"heart_eyes_cat","name":"Smiling Cat with Heart-Eyes","keywords":["heart","eyes","animal","love","like","affection","cats","valentines"],"skins":[{"unified":"1f63b","native":"😻"}],"version":1},"smirk_cat":{"id":"smirk_cat","name":"Cat with Wry Smile","keywords":["smirk","animal","cats"],"skins":[{"unified":"1f63c","native":"😼"}],"version":1},"kissing_cat":{"id":"kissing_cat","name":"Kissing Cat","keywords":["animal","cats","kiss"],"skins":[{"unified":"1f63d","native":"😽"}],"version":1},"scream_cat":{"id":"scream_cat","name":"Weary Cat","keywords":["scream","animal","cats","munch","scared"],"skins":[{"unified":"1f640","native":"🙀"}],"version":1},"crying_cat_face":{"id":"crying_cat_face","name":"Crying Cat","keywords":["face","animal","tears","weep","sad","cats","upset","cry"],"skins":[{"unified":"1f63f","native":"😿"}],"version":1},"pouting_cat":{"id":"pouting_cat","name":"Pouting Cat","keywords":["animal","cats"],"skins":[{"unified":"1f63e","native":"😾"}],"version":1},"see_no_evil":{"id":"see_no_evil","name":"See-No-Evil Monkey","keywords":["see","no","evil","animal","nature","haha"],"skins":[{"unified":"1f648","native":"🙈"}],"version":1},"hear_no_evil":{"id":"hear_no_evil","name":"Hear-No-Evil Monkey","keywords":["hear","no","evil","animal","nature"],"skins":[{"unified":"1f649","native":"🙉"}],"version":1},"speak_no_evil":{"id":"speak_no_evil","name":"Speak-No-Evil Monkey","keywords":["speak","no","evil","animal","nature","omg"],"skins":[{"unified":"1f64a","native":"🙊"}],"version":1},"love_letter":{"id":"love_letter","name":"Love Letter","keywords":["email","like","affection","envelope","valentines"],"skins":[{"unified":"1f48c","native":"💌"}],"version":1},"cupid":{"id":"cupid","name":"Heart with Arrow","keywords":["cupid","love","like","affection","valentines"],"skins":[{"unified":"1f498","native":"💘"}],"version":1},"gift_heart":{"id":"gift_heart","name":"Heart with Ribbon","keywords":["gift","love","valentines"],"skins":[{"unified":"1f49d","native":"💝"}],"version":1},"sparkling_heart":{"id":"sparkling_heart","name":"Sparkling Heart","keywords":["love","like","affection","valentines"],"skins":[{"unified":"1f496","native":"💖"}],"version":1},"heartpulse":{"id":"heartpulse","name":"Growing Heart","keywords":["heartpulse","like","love","affection","valentines","pink"],"skins":[{"unified":"1f497","native":"💗"}],"version":1},"heartbeat":{"id":"heartbeat","name":"Beating Heart","keywords":["heartbeat","love","like","affection","valentines","pink"],"skins":[{"unified":"1f493","native":"💓"}],"version":1},"revolving_hearts":{"id":"revolving_hearts","name":"Revolving Hearts","keywords":["love","like","affection","valentines"],"skins":[{"unified":"1f49e","native":"💞"}],"version":1},"two_hearts":{"id":"two_hearts","name":"Two Hearts","keywords":["love","like","affection","valentines","heart"],"skins":[{"unified":"1f495","native":"💕"}],"version":1},"heart_decoration":{"id":"heart_decoration","name":"Heart Decoration","keywords":["purple","square","love","like"],"skins":[{"unified":"1f49f","native":"💟"}],"version":1},"heavy_heart_exclamation_mark_ornament":{"id":"heavy_heart_exclamation_mark_ornament","name":"Heart Exclamation","keywords":["heavy","mark","ornament","decoration","love"],"skins":[{"unified":"2763-fe0f","native":"❣️"}],"version":1},"broken_heart":{"id":"broken_heart","name":"Broken Heart","emoticons":["e.length)&&(t=e.length);for(var r=0,n=new Array(t);r=0;--i){var a=this.tryEntries[i],s=a.completion;if("root"===a.tryLoc)return o("end");if(a.tryLoc<=this.prev){var c=n.call(a,"catchLoc"),l=n.call(a,"finallyLoc");if(c&&l){if(this.prev=0;--r){var o=this.tryEntries[r];if(o.tryLoc<=this.prev&&n.call(o,"finallyLoc")&&this.prev=0;--t){var r=this.tryEntries[t];if(r.finallyLoc===e)return this.complete(r.completion,r.afterLoc),L(r),v}},catch:function(e){for(var t=this.tryEntries.length-1;t>=0;--t){var r=this.tryEntries[t];if(r.tryLoc===e){var n=r.completion;if("throw"===n.type){var o=n.arg;L(r)}return o}}throw new Error("illegal catch attempt")},delegateYield:function(e,r,n){return this.delegate={iterator:z(e),resultName:r,nextLoc:n},"next"===this.method&&(this.arg=t),v}},e}(z);try{regeneratorRuntime=E}catch(e){"object"==typeof globalThis?globalThis.regeneratorRuntime=E:Function("r","regeneratorRuntime = r")(E)}var P,R,O,B,A,T,H={},I=[],D=/acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i;function F(e,t){for(var r in t)e[r]=t[r];return e}function V(e){var t=e.parentNode;t&&t.removeChild(e)}function U(e,t,r){var n,o,i,a={};for(i in t)"key"==i?n=t[i]:"ref"==i?o=t[i]:a[i]=t[i];if(arguments.length>2&&(a.children=arguments.length>3?P.call(arguments,2):r),"function"==typeof e&&null!=e.defaultProps)for(i in e.defaultProps)void 0===a[i]&&(a[i]=e.defaultProps[i]);return N(e,a,n,o,null)}function N(e,t,r,n,o){var i={type:e,props:t,key:r,ref:n,__k:null,__:null,__b:0,__e:null,__d:void 0,__c:null,__h:null,constructor:void 0,__v:null==o?++O:o};return null==o&&null!=R.vnode&&R.vnode(i),i}function q(e){return e.children}function W(e,t){this.props=e,this.context=t}function G(e,t){if(null==t)return e.__?G(e.__,e.__.__k.indexOf(e)+1):null;for(var r;t0?N(p.type,p.props,p.key,null,p.__v):p)){if(p.__=r,p.__b=r.__b+1,null===(h=m[u])||h&&p.key==h.key&&p.type===h.type)m[u]=void 0;else for(d=0;d<_;d++){if((h=m[d])&&p.key==h.key&&p.type===h.type){m[d]=void 0;break}h=null}oe(e,p,h=h||H,o,i,a,s,c,l),f=p.__e,(d=p.ref)&&h.ref!=d&&(g||(g=[]),h.ref&&g.push(h.ref,null,p),g.push(d,p.__c||f,p)),null!=f?(null==v&&(v=f),"function"==typeof p.type&&p.__k===h.__k?p.__d=c=J(p,c,e):c=Q(e,p,h,m,f,c),"function"==typeof r.type&&(r.__d=c)):c&&h.__e==c&&c.parentNode!=e&&(c=G(h))}for(r.__e=v,u=_;u--;)null!=m[u]&&("function"==typeof r.type&&null!=m[u].__e&&m[u].__e==r.__d&&(r.__d=G(n,u+1)),se(m[u],m[u]));if(g)for(u=0;u=o)return!1;var a=t+i/4%t,s=Math.floor(i/4/t),c=e.getImageData(a,s,1,1).data;return n[i]===c[0]&&n[i+2]===c[2]&&!(e.measureText(r).width>=t)}}(),me={latestVersion:function(){var e=!0,t=!1,r=void 0;try{for(var n,o=fe[Symbol.iterator]();!(e=(n=o.next()).done);e=!0){var i=n.value,a=i.v;if(ve(i.emoji))return a}}catch(e){t=!0,r=e}finally{try{e||null==o.return||o.return()}finally{if(t)throw r}}},noCountryFlags:function(){return!ve("🇨🇦")}},_e=["+1","grinning","kissing_heart","heart_eyes","laughing","stuck_out_tongue_winking_eye","sweat_smile","joy","scream","disappointed","unamused","weary","sob","sunglasses","heart"],be=null;var ye,ke={add:function(e){be||(be=he.get("frequently")||{});var t=e.id||e;t&&(be[t]||(be[t]=0),be[t]+=1,he.set("last",t),he.set("frequently",be))},get:function(e){var t=e.maxFrequentRows,r=e.perLine;if(!t)return[];be||(be=he.get("frequently"));var n=[];if(!be){for(var o in be={},_e.slice(0,r)){var i=_e[o];be[i]=r-o,n.push(i)}return n}var a=t*r,s=he.get("last");for(var c in be)n.push(c);if(n.sort((function(e,t){var r=be[t],n=be[e];return r==n?e.localeCompare(t):r-n})),n.length>a){var l=n.slice(a);n=n.slice(0,a);var u=!0,d=!1,h=void 0;try{for(var p,f=l[Symbol.iterator]();!(u=(p=f.next()).done);u=!0){var v=p.value;v!=s&&delete be[v]}}catch(e){d=!0,h=e}finally{try{u||null==f.return||f.return()}finally{if(d)throw h}}s&&-1==n.indexOf(s)&&(delete be[n[n.length-1]],n.splice(-1,1,s)),he.set("frequently",be)}return n},DEFAULTS:_e};ye=JSON.parse('{"search":"Search","search_no_results_1":"Oh no!","search_no_results_2":"That emoji couldn’t be found","pick":"Pick an emoji…","add_custom":"Add custom emoji","categories":{"activity":"Activity","custom":"Custom","flags":"Flags","foods":"Food & Drink","frequent":"Frequently used","nature":"Animals & Nature","objects":"Objects","people":"Smileys & People","places":"Travel & Places","search":"Search Results","symbols":"Symbols"},"skins":{"1":"Default","2":"Light","3":"Medium-Light","4":"Medium","5":"Medium-Dark","6":"Dark","choose":"Choose default skin tone"}}');var we={autoFocus:{value:!1},dynamicWidth:{value:!1},emojiButtonColors:{value:null},emojiButtonRadius:{value:"100%"},emojiButtonSize:{value:36},emojiSize:{value:24},emojiVersion:{value:15,choices:[1,2,3,4,5,11,12,12.1,13,13.1,14,15]},exceptEmojis:{value:[]},icons:{value:"auto",choices:["auto","outline","solid"]},locale:{value:"en",choices:["en","ar","be","cs","de","es","fa","fi","fr","hi","it","ja","ko","nl","pl","pt","ru","sa","tr","uk","vi","zh"]},maxFrequentRows:{value:4},navPosition:{value:"top",choices:["top","bottom","none"]},noCountryFlags:{value:!1},noResultsEmoji:{value:null},perLine:{value:9},previewEmoji:{value:null},previewPosition:{value:"bottom",choices:["top","bottom","none"]},searchPosition:{value:"sticky",choices:["sticky","static","none"]},set:{value:"native",choices:["native","apple","facebook","google","twitter"]},skin:{value:1,choices:[1,2,3,4,5,6]},skinTonePosition:{value:"preview",choices:["preview","search","none"]},theme:{value:"auto",choices:["auto","light","dark"]},categories:null,categoryIcons:null,custom:null,data:null,i18n:null,getImageURL:null,getSpritesheetURL:null,onAddCustomEmoji:null,onClickOutside:null,onEmojiSelect:null,stickySearch:{deprecated:!0,value:!0}},xe=null,Ce=null,Se={};function je(e){return Le.apply(this,arguments)}function Le(){return(Le=i(t(z).mark((function e(r){var n,o;return t(z).wrap((function(e){for(;;)switch(e.prev=e.next){case 0:if(!Se[r]){e.next=2;break}return e.abrupt("return",Se[r]);case 2:return e.next=4,fetch(r);case 4:return n=e.sent,e.next=7,n.json();case 7:return o=e.sent,Se[r]=o,e.abrupt("return",o);case 10:case"end":return e.stop()}}),e)})))).apply(this,arguments)}var Me=null,ze=null,Ee=!1;function Pe(e){var t=(arguments.length>1&&void 0!==arguments[1]?arguments[1]:{}).caller;return Me||(Me=new Promise((function(e){ze=e}))),e?Re(e):t&&!Ee&&console.warn("`".concat(t,"` requires data to be initialized first. Promise will be pending until `init` is called.")),Me}function Re(e){return Oe.apply(this,arguments)}function Oe(){return(Oe=i(t(z).mark((function e(r){var n,o,i,a,s,c,l,u,d,h,p,f,v,g,m,_,y,k,w,x,C,S,j,L,M,E,P,R,O,B,A,T,H,I,D,F,V,U,N,q,W,G,K;return t(z).wrap((function(e){for(;;)switch(e.prev=e.next){case 0:if(Ee=!0,n=r.emojiVersion,o=r.set,i=r.locale,n||(n=we.emojiVersion.value),o||(o=we.set.value),i||(i=we.locale.value),Ce){e.next=36;break}if("function"!=typeof r.data){e.next=12;break}return e.next=9,r.data();case 9:e.t1=e.sent,e.next=13;break;case 12:e.t1=r.data;case 13:if(e.t0=e.t1,e.t0){e.next=18;break}return e.next=17,je("https://cdn.jsdelivr.net/npm/@emoji-mart/data@latest/sets/".concat(n,"/").concat(o,".json"));case 17:e.t0=e.sent;case 18:(Ce=e.t0).emoticons={},Ce.natives={},Ce.categories.unshift({id:"frequent",emojis:[]}),e.t2=regeneratorRuntime.keys(Ce.aliases);case 23:if((e.t3=e.t2()).done){e.next=33;break}if(a=e.t3.value,s=Ce.aliases[a],c=Ce.emojis[s]){e.next=29;break}return e.abrupt("continue",23);case 29:c.aliases||(c.aliases=[]),c.aliases.push(a),e.next=23;break;case 33:Ce.originalCategories=Ce.categories,e.next=37;break;case 36:Ce.categories=Ce.categories.filter((function(e){return!!!e.name}));case 37:if("function"!=typeof r.i18n){e.next=43;break}return e.next=40,r.i18n();case 40:e.t5=e.sent,e.next=44;break;case 43:e.t5=r.i18n;case 44:if(e.t4=e.t5,e.t4){e.next=54;break}if("en"!=i){e.next=50;break}e.t6=t(ye),e.next=53;break;case 50:return e.next=52,je("https://cdn.jsdelivr.net/npm/@emoji-mart/data@latest/i18n/".concat(i,".json"));case 52:e.t6=e.sent;case 53:e.t4=e.t6;case 54:if(xe=e.t4,!r.custom){e.next=87;break}e.t7=regeneratorRuntime.keys(r.custom);case 57:if((e.t8=e.t7()).done){e.next=87;break}if(l=e.t8.value,l=parseInt(l),u=r.custom[l],d=r.custom[l-1],u.emojis&&u.emojis.length){e.next=64;break}return e.abrupt("continue",57);case 64:for(u.id||(u.id="custom_".concat(l+1)),u.name||(u.name=xe.categories.custom),d&&!u.icon&&(u.target=d.target||d),Ce.categories.push(u),h=!0,p=!1,f=void 0,e.prev=69,v=u.emojis[Symbol.iterator]();!(h=(g=v.next()).done);h=!0)m=g.value,Ce.emojis[m.id]=m;e.next=77;break;case 73:e.prev=73,e.t9=e.catch(69),p=!0,f=e.t9;case 77:e.prev=77,e.prev=78,h||null==v.return||v.return();case 80:if(e.prev=80,!p){e.next=83;break}throw f;case 83:return e.finish(80);case 84:return e.finish(77);case 85:e.next=57;break;case 87:r.categories&&(Ce.categories=Ce.originalCategories.filter((function(e){return-1!=r.categories.indexOf(e.id)})).sort((function(e,t){return r.categories.indexOf(e.id)-r.categories.indexOf(t.id)}))),_=null,y=null,"native"==o&&(_=me.latestVersion(),y=r.noCountryFlags||me.noCountryFlags()),k=Ce.categories.length,w=!1;case 93:if(!k--){e.next=179;break}if("frequent"==(x=Ce.categories[k]).id&&(C=r.maxFrequentRows,S=r.perLine,C=C>=0?C:we.maxFrequentRows.value,S||(S=we.perLine.value),x.emojis=ke.get({maxFrequentRows:C,perLine:S})),x.emojis&&x.emojis.length){e.next=99;break}return Ce.categories.splice(k,1),e.abrupt("continue",93);case 99:(j=r.categoryIcons)&&(L=j[x.id])&&!x.icon&&(x.icon=L),M=x.emojis.length;case 102:if(!M--){e.next=177;break}if(E=x.emojis[M],P=E.id?E:Ce.emojis[E],R=function(){x.emojis.splice(M,1)},!(!P||r.exceptEmojis&&r.exceptEmojis.includes(P.id))){e.next=109;break}return R(),e.abrupt("continue",102);case 109:if(!(_&&P.version>_)){e.next=112;break}return R(),e.abrupt("continue",102);case 112:if(!y||"flags"!=x.id){e.next=116;break}if(De.includes(P.id)){e.next=116;break}return R(),e.abrupt("continue",102);case 116:if(P.search){e.next=175;break}if(w=!0,P.search=","+[[P.id,!1],[P.name,!0],[P.keywords,!1],[P.emoticons,!1]].map((function(e){var t=b(e,2),r=t[0],n=t[1];if(r)return(Array.isArray(r)?r:[r]).map((function(e){return(n?e.split(/[-|_|\s]+/):[e]).map((function(e){return e.toLowerCase()}))})).flat()})).flat().filter((function(e){return e&&e.trim()})).join(","),O=!0,B=!1,A=void 0,!P.emoticons){e.next=145;break}e.prev=121,T=P.emoticons[Symbol.iterator]();case 123:if(O=(H=T.next()).done){e.next=131;break}if(I=H.value,!Ce.emoticons[I]){e.next=127;break}return e.abrupt("continue",128);case 127:Ce.emoticons[I]=P.id;case 128:O=!0,e.next=123;break;case 131:e.next=137;break;case 133:e.prev=133,e.t10=e.catch(121),B=!0,A=e.t10;case 137:e.prev=137,e.prev=138,O||null==T.return||T.return();case 140:if(e.prev=140,!B){e.next=143;break}throw A;case 143:return e.finish(140);case 144:return e.finish(137);case 145:D=0,F=!0,V=!1,U=void 0,e.prev=147,N=P.skins[Symbol.iterator]();case 149:if(F=(q=N.next()).done){e.next=161;break}if(W=q.value){e.next=153;break}return e.abrupt("continue",158);case 153:D++,(G=W.native)&&(Ce.natives[G]=P.id,P.search+=",".concat(G)),K=1==D?"":":skin-tone-".concat(D,":"),W.shortcodes=":".concat(P.id,":").concat(K);case 158:F=!0,e.next=149;break;case 161:e.next=167;break;case 163:e.prev=163,e.t11=e.catch(147),V=!0,U=e.t11;case 167:e.prev=167,e.prev=168,F||null==N.return||N.return();case 170:if(e.prev=170,!V){e.next=173;break}throw U;case 173:return e.finish(170);case 174:return e.finish(167);case 175:e.next=102;break;case 177:e.next=93;break;case 179:w&&Ie.reset(),ze();case 181:case"end":return e.stop()}}),e,null,[[69,73,77,85],[78,,80,84],[121,133,137,145],[138,,140,144],[147,163,167,175],[168,,170,174]])})))).apply(this,arguments)}function Be(e,t,r){e||(e={});var n={};for(var o in t)n[o]=Ae(o,e,t,r);return n}function Ae(e,t,r,n){var o=r[e],i=n&&n.getAttribute(e)||(null!=t[e]&&null!=t[e]?t[e]:null);return o?(null!=i&&o.value&&j(o.value)!=(void 0===i?"undefined":j(i))&&(i="boolean"==typeof o.value?"false"!=i:o.value.constructor(i)),o.transform&&i&&(i=o.transform(i)),(null==i||o.choices&&-1==o.choices.indexOf(i))&&(i=o.value),i):i}var Te=null;function He(){return He=i(t(z).mark((function e(r){var n,o,i,a,s,c,l,u,d,h,p,f,v,g,m,_,b,y,k,w,x=arguments;return t(z).wrap((function(e){for(;;)switch(e.prev=e.next){case 0:if(n=x.length>1&&void 0!==x[1]?x[1]:{},o=n.maxResults,i=n.caller,r&&r.trim().length){e.next=3;break}return e.abrupt("return",null);case 3:return o||(o=90),e.next=6,Pe(null,{caller:i||"SearchIndex.search"});case 6:if((a=r.toLowerCase().replace(/(\w)-/,"$1 ").split(/[\s|,]+/).filter((function(e,t,r){return e.trim()&&r.indexOf(e)==t}))).length){e.next=9;break}return e.abrupt("return");case 9:s=Te||(Te=Object.values(Ce.emojis)),u=!0,d=!1,h=void 0,e.prev=12,p=a[Symbol.iterator]();case 14:if(u=(f=p.next()).done){e.next=54;break}if(v=f.value,s.length){e.next=18;break}return e.abrupt("break",54);case 18:c=[],l={},g=!0,m=!1,_=void 0,e.prev=21,b=s[Symbol.iterator]();case 23:if(g=(y=b.next()).done){e.next=36;break}if((k=y.value).search){e.next=27;break}return e.abrupt("continue",33);case 27:if(-1!=(w=k.search.indexOf(",".concat(v)))){e.next=30;break}return e.abrupt("continue",33);case 30:c.push(k),l[k.id]||(l[k.id]=0),l[k.id]+=k.id==v?0:w+1;case 33:g=!0,e.next=23;break;case 36:e.next=42;break;case 38:e.prev=38,e.t0=e.catch(21),m=!0,_=e.t0;case 42:e.prev=42,e.prev=43,g||null==b.return||b.return();case 45:if(e.prev=45,!m){e.next=48;break}throw _;case 48:return e.finish(45);case 49:return e.finish(42);case 50:s=c;case 51:u=!0,e.next=14;break;case 54:e.next=60;break;case 56:e.prev=56,e.t1=e.catch(12),d=!0,h=e.t1;case 60:e.prev=60,e.prev=61,u||null==p.return||p.return();case 63:if(e.prev=63,!d){e.next=66;break}throw h;case 66:return e.finish(63);case 67:return e.finish(60);case 68:if(!(c.length<2)){e.next=70;break}return e.abrupt("return",c);case 70:return c.sort((function(e,t){var r=l[e.id],n=l[t.id];return r==n?e.id.localeCompare(t.id):r-n})),c.length>o&&(c=c.slice(0,o)),e.abrupt("return",c);case 73:case"end":return e.stop()}}),e,null,[[12,56,60,68],[21,38,42,50],[43,,45,49],[61,,63,67]])}))),He.apply(this,arguments)}var Ie={search:function(e){return He.apply(this,arguments)},get:function(e){return e.id?e:Ce.emojis[e]||Ce.emojis[Ce.aliases[e]]||Ce.emojis[Ce.natives[e]]},reset:function(){Te=null},SHORTCODES_REGEX:/^(?:\:([^\:]+)\:)(?:\:skin-tone-(\d)\:)?$/},De=["checkered_flag","crossed_flags","pirate_flag","rainbow-flag","transgender_flag","triangular_flag_on_post","waving_black_flag","waving_white_flag"];function Fe(){return Ve.apply(this,arguments)}function Ve(){return Ve=i(t(z).mark((function e(){var r,n=arguments;return t(z).wrap((function(e){for(;;)switch(e.prev=e.next){case 0:r=n.length>0&&void 0!==n[0]?n[0]:1,e.t0=regeneratorRuntime.keys(w(Array(r).keys()));case 2:if((e.t1=e.t0()).done){e.next=8;break}return e.t1.value,e.next=6,new Promise(requestAnimationFrame);case 6:e.next=2;break;case 8:case"end":return e.stop()}}),e)}))),Ve.apply(this,arguments)}function Ue(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},r=t.skinIndex,n=void 0===r?0:r,o=e.skins[n]||(n=0,e.skins[n]),i={id:e.id,name:e.name,native:o.native,unified:o.unified,keywords:e.keywords,shortcodes:o.shortcodes||e.shortcodes};return e.skins.length>1&&(i.skin=n+1),o.src&&(i.src=o.src),e.aliases&&e.aliases.length&&(i.aliases=e.aliases),e.emoticons&&e.emoticons.length&&(i.emoticons=e.emoticons),i}function Ne(e){return qe.apply(this,arguments)}function qe(){return(qe=i(t(z).mark((function e(r){var n,o,i,a,s,c,l,u;return t(z).wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.next=2,Ie.search(r,{maxResults:1,caller:"getEmojiDataFromNative"});case 2:if((n=e.sent)&&n.length){e.next=5;break}return e.abrupt("return",null);case 5:o=n[0],i=0,a=!0,s=!1,c=void 0,e.prev=8,l=o.skins[Symbol.iterator]();case 10:if(a=(u=l.next()).done){e.next=18;break}if(u.value.native!=r){e.next=14;break}return e.abrupt("break",18);case 14:i++;case 15:a=!0,e.next=10;break;case 18:e.next=24;break;case 20:e.prev=20,e.t0=e.catch(8),s=!0,c=e.t0;case 24:e.prev=24,e.prev=25,a||null==l.return||l.return();case 27:if(e.prev=27,!s){e.next=30;break}throw c;case 30:return e.finish(27);case 31:return e.finish(24);case 32:return e.abrupt("return",Ue(o,{skinIndex:i}));case 33:case"end":return e.stop()}}),e,null,[[8,20,24,32],[25,,27,31]])})))).apply(this,arguments)}var We={categories:{activity:{outline:de("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:de("path",{d:"M12 0C5.373 0 0 5.372 0 12c0 6.627 5.373 12 12 12 6.628 0 12-5.373 12-12 0-6.628-5.372-12-12-12m9.949 11H17.05c.224-2.527 1.232-4.773 1.968-6.113A9.966 9.966 0 0 1 21.949 11M13 11V2.051a9.945 9.945 0 0 1 4.432 1.564c-.858 1.491-2.156 4.22-2.392 7.385H13zm-2 0H8.961c-.238-3.165-1.536-5.894-2.393-7.385A9.95 9.95 0 0 1 11 2.051V11zm0 2v8.949a9.937 9.937 0 0 1-4.432-1.564c.857-1.492 2.155-4.221 2.393-7.385H11zm4.04 0c.236 3.164 1.534 5.893 2.392 7.385A9.92 9.92 0 0 1 13 21.949V13h2.04zM4.982 4.887C5.718 6.227 6.726 8.473 6.951 11h-4.9a9.977 9.977 0 0 1 2.931-6.113M2.051 13h4.9c-.226 2.527-1.233 4.771-1.969 6.113A9.972 9.972 0 0 1 2.051 13m16.967 6.113c-.735-1.342-1.744-3.586-1.968-6.113h4.899a9.961 9.961 0 0 1-2.931 6.113"})}),solid:de("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 512 512",children:de("path",{d:"M16.17 337.5c0 44.98 7.565 83.54 13.98 107.9C35.22 464.3 50.46 496 174.9 496c9.566 0 19.59-.4707 29.84-1.271L17.33 307.3C16.53 317.6 16.17 327.7 16.17 337.5zM495.8 174.5c0-44.98-7.565-83.53-13.98-107.9c-4.688-17.54-18.34-31.23-36.04-35.95C435.5 27.91 392.9 16 337 16c-9.564 0-19.59 .4707-29.84 1.271l187.5 187.5C495.5 194.4 495.8 184.3 495.8 174.5zM26.77 248.8l236.3 236.3c142-36.1 203.9-150.4 222.2-221.1L248.9 26.87C106.9 62.96 45.07 177.2 26.77 248.8zM256 335.1c0 9.141-7.474 16-16 16c-4.094 0-8.188-1.564-11.31-4.689L164.7 283.3C161.6 280.2 160 276.1 160 271.1c0-8.529 6.865-16 16-16c4.095 0 8.189 1.562 11.31 4.688l64.01 64C254.4 327.8 256 331.9 256 335.1zM304 287.1c0 9.141-7.474 16-16 16c-4.094 0-8.188-1.564-11.31-4.689L212.7 235.3C209.6 232.2 208 228.1 208 223.1c0-9.141 7.473-16 16-16c4.094 0 8.188 1.562 11.31 4.688l64.01 64.01C302.5 279.8 304 283.9 304 287.1zM256 175.1c0-9.141 7.473-16 16-16c4.094 0 8.188 1.562 11.31 4.688l64.01 64.01c3.125 3.125 4.688 7.219 4.688 11.31c0 9.133-7.468 16-16 16c-4.094 0-8.189-1.562-11.31-4.688l-64.01-64.01C257.6 184.2 256 180.1 256 175.1z"})})},custom:de("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 448 512",children:de("path",{d:"M417.1 368c-5.937 10.27-16.69 16-27.75 16c-5.422 0-10.92-1.375-15.97-4.281L256 311.4V448c0 17.67-14.33 32-31.1 32S192 465.7 192 448V311.4l-118.3 68.29C68.67 382.6 63.17 384 57.75 384c-11.06 0-21.81-5.734-27.75-16c-8.828-15.31-3.594-34.88 11.72-43.72L159.1 256L41.72 187.7C26.41 178.9 21.17 159.3 29.1 144C36.63 132.5 49.26 126.7 61.65 128.2C65.78 128.7 69.88 130.1 73.72 132.3L192 200.6V64c0-17.67 14.33-32 32-32S256 46.33 256 64v136.6l118.3-68.29c3.838-2.213 7.939-3.539 12.07-4.051C398.7 126.7 411.4 132.5 417.1 144c8.828 15.31 3.594 34.88-11.72 43.72L288 256l118.3 68.28C421.6 333.1 426.8 352.7 417.1 368z"})}),flags:{outline:de("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:de("path",{d:"M0 0l6.084 24H8L1.916 0zM21 5h-4l-1-4H4l3 12h3l1 4h13L21 5zM6.563 3h7.875l2 8H8.563l-2-8zm8.832 10l-2.856 1.904L12.063 13h3.332zM19 13l-1.5-6h1.938l2 8H16l3-2z"})}),solid:de("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 512 512",children:de("path",{d:"M64 496C64 504.8 56.75 512 48 512h-32C7.25 512 0 504.8 0 496V32c0-17.75 14.25-32 32-32s32 14.25 32 32V496zM476.3 0c-6.365 0-13.01 1.35-19.34 4.233c-45.69 20.86-79.56 27.94-107.8 27.94c-59.96 0-94.81-31.86-163.9-31.87C160.9 .3055 131.6 4.867 96 15.75v350.5c32-9.984 59.87-14.1 84.85-14.1c73.63 0 124.9 31.78 198.6 31.78c31.91 0 68.02-5.971 111.1-23.09C504.1 355.9 512 344.4 512 332.1V30.73C512 11.1 495.3 0 476.3 0z"})})},foods:{outline:de("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:de("path",{d:"M17 4.978c-1.838 0-2.876.396-3.68.934.513-1.172 1.768-2.934 4.68-2.934a1 1 0 0 0 0-2c-2.921 0-4.629 1.365-5.547 2.512-.064.078-.119.162-.18.244C11.73 1.838 10.798.023 9.207.023 8.579.022 7.85.306 7 .978 5.027 2.54 5.329 3.902 6.492 4.999 3.609 5.222 0 7.352 0 12.969c0 4.582 4.961 11.009 9 11.009 1.975 0 2.371-.486 3-1 .629.514 1.025 1 3 1 4.039 0 9-6.418 9-11 0-5.953-4.055-8-7-8M8.242 2.546c.641-.508.943-.523.965-.523.426.169.975 1.405 1.357 3.055-1.527-.629-2.741-1.352-2.98-1.846.059-.112.241-.356.658-.686M15 21.978c-1.08 0-1.21-.109-1.559-.402l-.176-.146c-.367-.302-.816-.452-1.266-.452s-.898.15-1.266.452l-.176.146c-.347.292-.477.402-1.557.402-2.813 0-7-5.389-7-9.009 0-5.823 4.488-5.991 5-5.991 1.939 0 2.484.471 3.387 1.251l.323.276a1.995 1.995 0 0 0 2.58 0l.323-.276c.902-.78 1.447-1.251 3.387-1.251.512 0 5 .168 5 6 0 3.617-4.187 9-7 9"})}),solid:de("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 512 512",children:de("path",{d:"M481.9 270.1C490.9 279.1 496 291.3 496 304C496 316.7 490.9 328.9 481.9 337.9C472.9 346.9 460.7 352 448 352H64C51.27 352 39.06 346.9 30.06 337.9C21.06 328.9 16 316.7 16 304C16 291.3 21.06 279.1 30.06 270.1C39.06 261.1 51.27 256 64 256H448C460.7 256 472.9 261.1 481.9 270.1zM475.3 388.7C478.3 391.7 480 395.8 480 400V416C480 432.1 473.3 449.3 461.3 461.3C449.3 473.3 432.1 480 416 480H96C79.03 480 62.75 473.3 50.75 461.3C38.74 449.3 32 432.1 32 416V400C32 395.8 33.69 391.7 36.69 388.7C39.69 385.7 43.76 384 48 384H464C468.2 384 472.3 385.7 475.3 388.7zM50.39 220.8C45.93 218.6 42.03 215.5 38.97 211.6C35.91 207.7 33.79 203.2 32.75 198.4C31.71 193.5 31.8 188.5 32.99 183.7C54.98 97.02 146.5 32 256 32C365.5 32 457 97.02 479 183.7C480.2 188.5 480.3 193.5 479.2 198.4C478.2 203.2 476.1 207.7 473 211.6C469.1 215.5 466.1 218.6 461.6 220.8C457.2 222.9 452.3 224 447.3 224H64.67C59.73 224 54.84 222.9 50.39 220.8zM372.7 116.7C369.7 119.7 368 123.8 368 128C368 131.2 368.9 134.3 370.7 136.9C372.5 139.5 374.1 141.6 377.9 142.8C380.8 143.1 384 144.3 387.1 143.7C390.2 143.1 393.1 141.6 395.3 139.3C397.6 137.1 399.1 134.2 399.7 131.1C400.3 128 399.1 124.8 398.8 121.9C397.6 118.1 395.5 116.5 392.9 114.7C390.3 112.9 387.2 111.1 384 111.1C379.8 111.1 375.7 113.7 372.7 116.7V116.7zM244.7 84.69C241.7 87.69 240 91.76 240 96C240 99.16 240.9 102.3 242.7 104.9C244.5 107.5 246.1 109.6 249.9 110.8C252.8 111.1 256 112.3 259.1 111.7C262.2 111.1 265.1 109.6 267.3 107.3C269.6 105.1 271.1 102.2 271.7 99.12C272.3 96.02 271.1 92.8 270.8 89.88C269.6 86.95 267.5 84.45 264.9 82.7C262.3 80.94 259.2 79.1 256 79.1C251.8 79.1 247.7 81.69 244.7 84.69V84.69zM116.7 116.7C113.7 119.7 112 123.8 112 128C112 131.2 112.9 134.3 114.7 136.9C116.5 139.5 118.1 141.6 121.9 142.8C124.8 143.1 128 144.3 131.1 143.7C134.2 143.1 137.1 141.6 139.3 139.3C141.6 137.1 143.1 134.2 143.7 131.1C144.3 128 143.1 124.8 142.8 121.9C141.6 118.1 139.5 116.5 136.9 114.7C134.3 112.9 131.2 111.1 128 111.1C123.8 111.1 119.7 113.7 116.7 116.7L116.7 116.7z"})})},frequent:{outline:de("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:[de("path",{d:"M13 4h-2l-.001 7H9v2h2v2h2v-2h4v-2h-4z"}),de("path",{d:"M12 0C5.373 0 0 5.373 0 12s5.373 12 12 12 12-5.373 12-12S18.627 0 12 0m0 22C6.486 22 2 17.514 2 12S6.486 2 12 2s10 4.486 10 10-4.486 10-10 10"})]}),solid:de("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 512 512",children:de("path",{d:"M256 512C114.6 512 0 397.4 0 256C0 114.6 114.6 0 256 0C397.4 0 512 114.6 512 256C512 397.4 397.4 512 256 512zM232 256C232 264 236 271.5 242.7 275.1L338.7 339.1C349.7 347.3 364.6 344.3 371.1 333.3C379.3 322.3 376.3 307.4 365.3 300L280 243.2V120C280 106.7 269.3 96 255.1 96C242.7 96 231.1 106.7 231.1 120L232 256z"})})},nature:{outline:de("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:[de("path",{d:"M15.5 8a1.5 1.5 0 1 0 .001 3.001A1.5 1.5 0 0 0 15.5 8M8.5 8a1.5 1.5 0 1 0 .001 3.001A1.5 1.5 0 0 0 8.5 8"}),de("path",{d:"M18.933 0h-.027c-.97 0-2.138.787-3.018 1.497-1.274-.374-2.612-.51-3.887-.51-1.285 0-2.616.133-3.874.517C7.245.79 6.069 0 5.093 0h-.027C3.352 0 .07 2.67.002 7.026c-.039 2.479.276 4.238 1.04 5.013.254.258.882.677 1.295.882.191 3.177.922 5.238 2.536 6.38.897.637 2.187.949 3.2 1.102C8.04 20.6 8 20.795 8 21c0 1.773 2.35 3 4 3 1.648 0 4-1.227 4-3 0-.201-.038-.393-.072-.586 2.573-.385 5.435-1.877 5.925-7.587.396-.22.887-.568 1.104-.788.763-.774 1.079-2.534 1.04-5.013C23.929 2.67 20.646 0 18.933 0M3.223 9.135c-.237.281-.837 1.155-.884 1.238-.15-.41-.368-1.349-.337-3.291.051-3.281 2.478-4.972 3.091-5.031.256.015.731.27 1.265.646-1.11 1.171-2.275 2.915-2.352 5.125-.133.546-.398.858-.783 1.313M12 22c-.901 0-1.954-.693-2-1 0-.654.475-1.236 1-1.602V20a1 1 0 1 0 2 0v-.602c.524.365 1 .947 1 1.602-.046.307-1.099 1-2 1m3-3.48v.02a4.752 4.752 0 0 0-1.262-1.02c1.092-.516 2.239-1.334 2.239-2.217 0-1.842-1.781-2.195-3.977-2.195-2.196 0-3.978.354-3.978 2.195 0 .883 1.148 1.701 2.238 2.217A4.8 4.8 0 0 0 9 18.539v-.025c-1-.076-2.182-.281-2.973-.842-1.301-.92-1.838-3.045-1.853-6.478l.023-.041c.496-.826 1.49-1.45 1.804-3.102 0-2.047 1.357-3.631 2.362-4.522C9.37 3.178 10.555 3 11.948 3c1.447 0 2.685.192 3.733.57 1 .9 2.316 2.465 2.316 4.48.313 1.651 1.307 2.275 1.803 3.102.035.058.068.117.102.178-.059 5.967-1.949 7.01-4.902 7.19m6.628-8.202c-.037-.065-.074-.13-.113-.195a7.587 7.587 0 0 0-.739-.987c-.385-.455-.648-.768-.782-1.313-.076-2.209-1.241-3.954-2.353-5.124.531-.376 1.004-.63 1.261-.647.636.071 3.044 1.764 3.096 5.031.027 1.81-.347 3.218-.37 3.235"})]}),solid:de("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 576 512",children:de("path",{d:"M332.7 19.85C334.6 8.395 344.5 0 356.1 0C363.6 0 370.6 3.52 375.1 9.502L392 32H444.1C456.8 32 469.1 37.06 478.1 46.06L496 64H552C565.3 64 576 74.75 576 88V112C576 156.2 540.2 192 496 192H426.7L421.6 222.5L309.6 158.5L332.7 19.85zM448 64C439.2 64 432 71.16 432 80C432 88.84 439.2 96 448 96C456.8 96 464 88.84 464 80C464 71.16 456.8 64 448 64zM416 256.1V480C416 497.7 401.7 512 384 512H352C334.3 512 320 497.7 320 480V364.8C295.1 377.1 268.8 384 240 384C211.2 384 184 377.1 160 364.8V480C160 497.7 145.7 512 128 512H96C78.33 512 64 497.7 64 480V249.8C35.23 238.9 12.64 214.5 4.836 183.3L.9558 167.8C-3.331 150.6 7.094 133.2 24.24 128.1C41.38 124.7 58.76 135.1 63.05 152.2L66.93 167.8C70.49 182 83.29 191.1 97.97 191.1H303.8L416 256.1z"})})},objects:{outline:de("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:[de("path",{d:"M12 0a9 9 0 0 0-5 16.482V21s2.035 3 5 3 5-3 5-3v-4.518A9 9 0 0 0 12 0zm0 2c3.86 0 7 3.141 7 7s-3.14 7-7 7-7-3.141-7-7 3.14-7 7-7zM9 17.477c.94.332 1.946.523 3 .523s2.06-.19 3-.523v.834c-.91.436-1.925.689-3 .689a6.924 6.924 0 0 1-3-.69v-.833zm.236 3.07A8.854 8.854 0 0 0 12 21c.965 0 1.888-.167 2.758-.451C14.155 21.173 13.153 22 12 22c-1.102 0-2.117-.789-2.764-1.453z"}),de("path",{d:"M14.745 12.449h-.004c-.852-.024-1.188-.858-1.577-1.824-.421-1.061-.703-1.561-1.182-1.566h-.009c-.481 0-.783.497-1.235 1.537-.436.982-.801 1.811-1.636 1.791l-.276-.043c-.565-.171-.853-.691-1.284-1.794-.125-.313-.202-.632-.27-.913-.051-.213-.127-.53-.195-.634C7.067 9.004 7.039 9 6.99 9A1 1 0 0 1 7 7h.01c1.662.017 2.015 1.373 2.198 2.134.486-.981 1.304-2.058 2.797-2.075 1.531.018 2.28 1.153 2.731 2.141l.002-.008C14.944 8.424 15.327 7 16.979 7h.032A1 1 0 1 1 17 9h-.011c-.149.076-.256.474-.319.709a6.484 6.484 0 0 1-.311.951c-.429.973-.79 1.789-1.614 1.789"})]}),solid:de("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 384 512",children:de("path",{d:"M112.1 454.3c0 6.297 1.816 12.44 5.284 17.69l17.14 25.69c5.25 7.875 17.17 14.28 26.64 14.28h61.67c9.438 0 21.36-6.401 26.61-14.28l17.08-25.68c2.938-4.438 5.348-12.37 5.348-17.7L272 415.1h-160L112.1 454.3zM191.4 .0132C89.44 .3257 16 82.97 16 175.1c0 44.38 16.44 84.84 43.56 115.8c16.53 18.84 42.34 58.23 52.22 91.45c.0313 .25 .0938 .5166 .125 .7823h160.2c.0313-.2656 .0938-.5166 .125-.7823c9.875-33.22 35.69-72.61 52.22-91.45C351.6 260.8 368 220.4 368 175.1C368 78.61 288.9-.2837 191.4 .0132zM192 96.01c-44.13 0-80 35.89-80 79.1C112 184.8 104.8 192 96 192S80 184.8 80 176c0-61.76 50.25-111.1 112-111.1c8.844 0 16 7.159 16 16S200.8 96.01 192 96.01z"})})},people:{outline:de("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:[de("path",{d:"M12 0C5.373 0 0 5.373 0 12s5.373 12 12 12 12-5.373 12-12S18.627 0 12 0m0 22C6.486 22 2 17.514 2 12S6.486 2 12 2s10 4.486 10 10-4.486 10-10 10"}),de("path",{d:"M8 7a2 2 0 1 0-.001 3.999A2 2 0 0 0 8 7M16 7a2 2 0 1 0-.001 3.999A2 2 0 0 0 16 7M15.232 15c-.693 1.195-1.87 2-3.349 2-1.477 0-2.655-.805-3.347-2H15m3-2H6a6 6 0 1 0 12 0"})]}),solid:de("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 512 512",children:de("path",{d:"M0 256C0 114.6 114.6 0 256 0C397.4 0 512 114.6 512 256C512 397.4 397.4 512 256 512C114.6 512 0 397.4 0 256zM256 432C332.1 432 396.2 382 415.2 314.1C419.1 300.4 407.8 288 393.6 288H118.4C104.2 288 92.92 300.4 96.76 314.1C115.8 382 179.9 432 256 432V432zM176.4 160C158.7 160 144.4 174.3 144.4 192C144.4 209.7 158.7 224 176.4 224C194 224 208.4 209.7 208.4 192C208.4 174.3 194 160 176.4 160zM336.4 224C354 224 368.4 209.7 368.4 192C368.4 174.3 354 160 336.4 160C318.7 160 304.4 174.3 304.4 192C304.4 209.7 318.7 224 336.4 224z"})})},places:{outline:de("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:[de("path",{d:"M6.5 12C5.122 12 4 13.121 4 14.5S5.122 17 6.5 17 9 15.879 9 14.5 7.878 12 6.5 12m0 3c-.275 0-.5-.225-.5-.5s.225-.5.5-.5.5.225.5.5-.225.5-.5.5M17.5 12c-1.378 0-2.5 1.121-2.5 2.5s1.122 2.5 2.5 2.5 2.5-1.121 2.5-2.5-1.122-2.5-2.5-2.5m0 3c-.275 0-.5-.225-.5-.5s.225-.5.5-.5.5.225.5.5-.225.5-.5.5"}),de("path",{d:"M22.482 9.494l-1.039-.346L21.4 9h.6c.552 0 1-.439 1-.992 0-.006-.003-.008-.003-.008H23c0-1-.889-2-1.984-2h-.642l-.731-1.717C19.262 3.012 18.091 2 16.764 2H7.236C5.909 2 4.738 3.012 4.357 4.283L3.626 6h-.642C1.889 6 1 7 1 8h.003S1 8.002 1 8.008C1 8.561 1.448 9 2 9h.6l-.043.148-1.039.346a2.001 2.001 0 0 0-1.359 2.097l.751 7.508a1 1 0 0 0 .994.901H3v1c0 1.103.896 2 2 2h2c1.104 0 2-.897 2-2v-1h6v1c0 1.103.896 2 2 2h2c1.104 0 2-.897 2-2v-1h1.096a.999.999 0 0 0 .994-.901l.751-7.508a2.001 2.001 0 0 0-1.359-2.097M6.273 4.857C6.402 4.43 6.788 4 7.236 4h9.527c.448 0 .834.43.963.857L19.313 9H4.688l1.585-4.143zM7 21H5v-1h2v1zm12 0h-2v-1h2v1zm2.189-3H2.811l-.662-6.607L3 11h18l.852.393L21.189 18z"})]}),solid:de("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 512 512",children:de("path",{d:"M39.61 196.8L74.8 96.29C88.27 57.78 124.6 32 165.4 32H346.6C387.4 32 423.7 57.78 437.2 96.29L472.4 196.8C495.6 206.4 512 229.3 512 256V448C512 465.7 497.7 480 480 480H448C430.3 480 416 465.7 416 448V400H96V448C96 465.7 81.67 480 64 480H32C14.33 480 0 465.7 0 448V256C0 229.3 16.36 206.4 39.61 196.8V196.8zM109.1 192H402.9L376.8 117.4C372.3 104.6 360.2 96 346.6 96H165.4C151.8 96 139.7 104.6 135.2 117.4L109.1 192zM96 256C78.33 256 64 270.3 64 288C64 305.7 78.33 320 96 320C113.7 320 128 305.7 128 288C128 270.3 113.7 256 96 256zM416 320C433.7 320 448 305.7 448 288C448 270.3 433.7 256 416 256C398.3 256 384 270.3 384 288C384 305.7 398.3 320 416 320z"})})},symbols:{outline:de("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:de("path",{d:"M0 0h11v2H0zM4 11h3V6h4V4H0v2h4zM15.5 17c1.381 0 2.5-1.116 2.5-2.493s-1.119-2.493-2.5-2.493S13 13.13 13 14.507 14.119 17 15.5 17m0-2.986c.276 0 .5.222.5.493 0 .272-.224.493-.5.493s-.5-.221-.5-.493.224-.493.5-.493M21.5 19.014c-1.381 0-2.5 1.116-2.5 2.493S20.119 24 21.5 24s2.5-1.116 2.5-2.493-1.119-2.493-2.5-2.493m0 2.986a.497.497 0 0 1-.5-.493c0-.271.224-.493.5-.493s.5.222.5.493a.497.497 0 0 1-.5.493M22 13l-9 9 1.513 1.5 8.99-9.009zM17 11c2.209 0 4-1.119 4-2.5V2s.985-.161 1.498.949C23.01 4.055 23 6 23 6s1-1.119 1-3.135C24-.02 21 0 21 0h-2v6.347A5.853 5.853 0 0 0 17 6c-2.209 0-4 1.119-4 2.5s1.791 2.5 4 2.5M10.297 20.482l-1.475-1.585a47.54 47.54 0 0 1-1.442 1.129c-.307-.288-.989-1.016-2.045-2.183.902-.836 1.479-1.466 1.729-1.892s.376-.871.376-1.336c0-.592-.273-1.178-.818-1.759-.546-.581-1.329-.871-2.349-.871-1.008 0-1.79.293-2.344.879-.556.587-.832 1.181-.832 1.784 0 .813.419 1.748 1.256 2.805-.847.614-1.444 1.208-1.794 1.784a3.465 3.465 0 0 0-.523 1.833c0 .857.308 1.56.924 2.107.616.549 1.423.823 2.42.823 1.173 0 2.444-.379 3.813-1.137L8.235 24h2.819l-2.09-2.383 1.333-1.135zm-6.736-6.389a1.02 1.02 0 0 1 .73-.286c.31 0 .559.085.747.254a.849.849 0 0 1 .283.659c0 .518-.419 1.112-1.257 1.784-.536-.651-.805-1.231-.805-1.742a.901.901 0 0 1 .302-.669M3.74 22c-.427 0-.778-.116-1.057-.349-.279-.232-.418-.487-.418-.766 0-.594.509-1.288 1.527-2.083.968 1.134 1.717 1.946 2.248 2.438-.921.507-1.686.76-2.3.76"})}),solid:de("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 512 512",children:de("path",{d:"M500.3 7.251C507.7 13.33 512 22.41 512 31.1V175.1C512 202.5 483.3 223.1 447.1 223.1C412.7 223.1 383.1 202.5 383.1 175.1C383.1 149.5 412.7 127.1 447.1 127.1V71.03L351.1 90.23V207.1C351.1 234.5 323.3 255.1 287.1 255.1C252.7 255.1 223.1 234.5 223.1 207.1C223.1 181.5 252.7 159.1 287.1 159.1V63.1C287.1 48.74 298.8 35.61 313.7 32.62L473.7 .6198C483.1-1.261 492.9 1.173 500.3 7.251H500.3zM74.66 303.1L86.5 286.2C92.43 277.3 102.4 271.1 113.1 271.1H174.9C185.6 271.1 195.6 277.3 201.5 286.2L213.3 303.1H239.1C266.5 303.1 287.1 325.5 287.1 351.1V463.1C287.1 490.5 266.5 511.1 239.1 511.1H47.1C21.49 511.1-.0019 490.5-.0019 463.1V351.1C-.0019 325.5 21.49 303.1 47.1 303.1H74.66zM143.1 359.1C117.5 359.1 95.1 381.5 95.1 407.1C95.1 434.5 117.5 455.1 143.1 455.1C170.5 455.1 191.1 434.5 191.1 407.1C191.1 381.5 170.5 359.1 143.1 359.1zM440.3 367.1H496C502.7 367.1 508.6 372.1 510.1 378.4C513.3 384.6 511.6 391.7 506.5 396L378.5 508C372.9 512.1 364.6 513.3 358.6 508.9C352.6 504.6 350.3 496.6 353.3 489.7L391.7 399.1H336C329.3 399.1 323.4 395.9 321 389.6C318.7 383.4 320.4 376.3 325.5 371.1L453.5 259.1C459.1 255 467.4 254.7 473.4 259.1C479.4 263.4 481.6 271.4 478.7 278.3L440.3 367.1zM116.7 219.1L19.85 119.2C-8.112 90.26-6.614 42.31 24.85 15.34C51.82-8.137 93.26-3.642 118.2 21.83L128.2 32.32L137.7 21.83C162.7-3.642 203.6-8.137 231.6 15.34C262.6 42.31 264.1 90.26 236.1 119.2L139.7 219.1C133.2 225.6 122.7 225.6 116.7 219.1H116.7z"})})}},search:{loupe:de("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 20 20",children:de("path",{d:"M12.9 14.32a8 8 0 1 1 1.41-1.41l5.35 5.33-1.42 1.42-5.33-5.34zM8 14A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"})}),delete:de("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 20 20",children:de("path",{d:"M10 8.586L2.929 1.515 1.515 2.929 8.586 10l-7.071 7.071 1.414 1.414L10 11.414l7.071 7.071 1.414-1.414L11.414 10l7.071-7.071-1.414-1.414L10 8.586z"})})}};function Ge(e){var t=e.id,r=e.skin,n=e.emoji;if(e.shortcodes){var o=e.shortcodes.match(Ie.SHORTCODES_REGEX);o&&(t=o[1],o[2]&&(r=o[2]))}if(n||(n=Ie.get(t||e.native)),!n)return e.fallback;var i=n.skins[r-1]||n.skins[0],a=i.src||("native"==e.set||e.spritesheet?void 0:"function"==typeof e.getImageURL?e.getImageURL(e.set,i.unified):"https://cdn.jsdelivr.net/npm/emoji-datasource-".concat(e.set,"@15.0.1/img/").concat(e.set,"/64/").concat(i.unified,".png")),s="function"==typeof e.getSpritesheetURL?e.getSpritesheetURL(e.set):"https://cdn.jsdelivr.net/npm/emoji-datasource-".concat(e.set,"@15.0.1/img/").concat(e.set,"/sheets-256/64.png");return de("span",{class:"emoji-mart-emoji","data-emoji-set":e.set,children:a?de("img",{style:{maxWidth:e.size||"1em",maxHeight:e.size||"1em",display:"inline-block"},alt:i.native||i.shortcodes,src:a}):"native"==e.set?de("span",{style:{fontSize:e.size,fontFamily:'"EmojiMart", "Segoe UI Emoji", "Segoe UI Symbol", "Segoe UI", "Apple Color Emoji", "Twemoji Mozilla", "Noto Color Emoji", "Android Emoji"'},children:i.native}):de("span",{style:{display:"block",width:e.size,height:e.size,backgroundImage:"url(".concat(s,")"),backgroundSize:"".concat(100*Ce.sheet.cols,"% ").concat(100*Ce.sheet.rows,"%"),backgroundPosition:"".concat(100/(Ce.sheet.cols-1)*i.x,"% ").concat(100/(Ce.sheet.rows-1)*i.y,"%")}})})}function Ke(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{return Date.prototype.toString.call(Reflect.construct(Date,[],(function(){}))),!0}catch(e){return!1}}function $e(e,t,r){return($e=Ke()?Reflect.construct:function(e,t,r){var n=[null];n.push.apply(n,t);var o=new(Function.bind.apply(e,n));return r&&d(o,r.prototype),o}).apply(null,arguments)}function Xe(e,t,r){return $e.apply(null,arguments)}function Ye(e){var t="function"==typeof Map?new Map:void 0;return Ye=function(e){if(null===e||(r=e,-1===Function.toString.call(r).indexOf("[native code]")))return e;var r;if("function"!=typeof e)throw new TypeError("Super expression must either be null or a function");if(void 0!==t){if(t.has(e))return t.get(e);t.set(e,n)}function n(){return Xe(e,arguments,S(this).constructor)}return n.prototype=Object.create(e.prototype,{constructor:{value:n,enumerable:!1,writable:!0,configurable:!0}}),d(n,e)},Ye(e)}function Je(e){return Ye(e)}var Ze=function(e){"use strict";h(r,e);var t=M(r);function r(){var e,o=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};if(a(this,r),(e=t.call(this)).props=o,o.parent||o.ref){var i=null,s=o.parent||(i=o.ref&&o.ref.current);i&&(i.innerHTML=""),s&&s.appendChild(n(e))}return e}return c(r,[{key:"update",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};for(var t in e)this.attributeChangedCallback(t,null,e[t])}},{key:"attributeChangedCallback",value:function(e,t,r){if(this.component){var n=Ae(e,l({},e,r),this.constructor.Props,this);this.component.componentWillReceiveProps?this.component.componentWillReceiveProps(l({},e,n)):(this.component.props[e]=n,this.component.forceUpdate())}}},{key:"disconnectedCallback",value:function(){this.disconnected=!0,this.component&&this.component.unregister&&this.component.unregister()}}],[{key:"observedAttributes",get:function(){return Object.keys(this.Props)}}]),r}("undefined"!=typeof window&&window.HTMLElement?window.HTMLElement:Object),Qe=function(e){"use strict";h(r,e);var t=M(r);function r(e){var n,o=(arguments.length>1&&void 0!==arguments[1]?arguments[1]:{}).styles;return a(this,r),(n=t.call(this,e)).setShadow(),n.injectStyles(o),n}return c(r,[{key:"setShadow",value:function(){this.attachShadow({mode:"open"})}},{key:"injectStyles",value:function(e){if(e){var t=document.createElement("style");t.textContent=e,this.shadowRoot.insertBefore(t,this.shadowRoot.firstChild)}}}]),r}(Je(Ze)),et={fallback:"",id:"",native:"",shortcodes:"",size:{value:"",transform:function(e){return/\D/.test(e)?e:"".concat(e,"px")}},set:we.set,skin:we.skin},tt=function(e){"use strict";h(n,e);var r=M(n);function n(e){return a(this,n),r.call(this,e)}return c(n,[{key:"connectedCallback",value:function(){var e=this;return i(t(z).mark((function r(){var n;return t(z).wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return(n=Be(e.props,et,e)).element=e,n.ref=function(t){e.component=t},t.next=5,Pe();case 5:if(!e.disconnected){t.next=7;break}return t.abrupt("return");case 7:le(de(Ge,p({},n)),e);case 8:case"end":return t.stop()}}),r)})))()}}]),n}(Je(Ze));l(tt,"Props",et),"undefined"==typeof customElements||customElements.get("em-emoji")||customElements.define("em-emoji",tt);var rt,nt,ot=[],it=R.__b,at=R.__r,st=R.diffed,ct=R.__c,lt=R.unmount;function ut(){var e;for(ot.sort((function(e,t){return e.__v.__b-t.__v.__b}));e=ot.pop();)if(e.__P)try{e.__H.__h.forEach(ht),e.__H.__h.forEach(pt),e.__H.__h=[]}catch(t){e.__H.__h=[],R.__e(t,e.__v)}}R.__b=function(e){rt=null,it&&it(e)},R.__r=function(e){at&&at(e),0;var t=(rt=e.__c).__H;t&&(t.__h.forEach(ht),t.__h.forEach(pt),t.__h=[])},R.diffed=function(e){st&&st(e);var t=e.__c;t&&t.__H&&t.__H.__h.length&&(1!==ot.push(t)&&nt===R.requestAnimationFrame||((nt=R.requestAnimationFrame)||function(e){var t,r=function(){clearTimeout(n),dt&&cancelAnimationFrame(t),setTimeout(e)},n=setTimeout(r,100);dt&&(t=requestAnimationFrame(r))})(ut)),rt=null},R.__c=function(e,t){t.some((function(e){try{e.__h.forEach(ht),e.__h=e.__h.filter((function(e){return!e.__||pt(e)}))}catch(r){t.some((function(e){e.__h&&(e.__h=[])})),t=[],R.__e(r,e.__v)}})),ct&&ct(e,t)},R.unmount=function(e){lt&<(e);var t,r=e.__c;r&&r.__H&&(r.__H.__.forEach((function(e){try{ht(e)}catch(e){t=e}})),t&&R.__e(t,r.__v))};var dt="function"==typeof requestAnimationFrame;function ht(e){var t=rt,r=e.__c;"function"==typeof r&&(e.__c=void 0,r()),rt=t}function pt(e){var t=rt;e.__c=e.__(),rt=t}function ft(e,t){for(var r in t)e[r]=t[r];return e}function vt(e,t){for(var r in e)if("__source"!==r&&!(r in t))return!0;for(var n in t)if("__source"!==n&&e[n]!==t[n])return!0;return!1}function gt(e){this.props=e}(gt.prototype=new W).isPureReactComponent=!0,gt.prototype.shouldComponentUpdate=function(e,t){return vt(this.props,e)||vt(this.state,t)};var mt=R.__b;R.__b=function(e){e.type&&e.type.__f&&e.ref&&(e.props.ref=e.ref,e.ref=null),mt&&mt(e)};"undefined"!=typeof Symbol&&Symbol.for&&Symbol.for("react.forward_ref");var _t=R.__e;R.__e=function(e,t,r){if(e.then)for(var n,o=t;o=o.__;)if((n=o.__c)&&n.__c)return null==t.__e&&(t.__e=r.__e,t.__k=r.__k),n.__c(e,t);_t(e,t,r)};var bt=R.unmount;function yt(){this.__u=0,this.t=null,this.__b=null}function kt(e){var t=e.__.__c;return t&&t.__e&&t.__e(e)}function wt(){this.u=null,this.o=null}R.unmount=function(e){var t=e.__c;t&&t.__R&&t.__R(),t&&!0===e.__h&&(e.type=null),bt&&bt(e)},(yt.prototype=new W).__c=function(e,t){var r=t.__c,n=this;null==n.t&&(n.t=[]),n.t.push(r);var o=kt(n.__v),i=!1,a=function(){i||(i=!0,r.__R=null,o?o(s):s())};r.__R=a;var s=function(){if(!--n.__u){if(n.state.__e){var e=n.state.__e;n.__v.__k[0]=function e(t,r,n){return t&&(t.__v=null,t.__k=t.__k&&t.__k.map((function(t){return e(t,r,n)})),t.__c&&t.__c.__P===r&&(t.__e&&n.insertBefore(t.__e,t.__d),t.__c.__e=!0,t.__c.__P=n)),t}(e,e.__c.__P,e.__c.__O)}var t;for(n.setState({__e:n.__b=null});t=n.t.pop();)t.forceUpdate()}},c=!0===t.__h;n.__u++||c||n.setState({__e:n.__b=n.__v.__k[0]}),e.then(a,a)},yt.prototype.componentWillUnmount=function(){this.t=[]},yt.prototype.render=function(e,t){if(this.__b){if(this.__v.__k){var r=document.createElement("div"),n=this.__v.__k[0].__c;this.__v.__k[0]=function e(t,r,n){return t&&(t.__c&&t.__c.__H&&(t.__c.__H.__.forEach((function(e){"function"==typeof e.__c&&e.__c()})),t.__c.__H=null),null!=(t=ft({},t)).__c&&(t.__c.__P===n&&(t.__c.__P=r),t.__c=null),t.__k=t.__k&&t.__k.map((function(t){return e(t,r,n)}))),t}(this.__b,r,n.__O=n.__P)}this.__b=null}var o=t.__e&&U(q,null,e.fallback);return o&&(o.__h=null),[U(q,null,t.__e?null:e.children),o]};var xt=function(e,t,r){if(++r[1]===r[0]&&e.o.delete(t),e.props.revealOrder&&("t"!==e.props.revealOrder[0]||!e.o.size))for(r=e.u;r;){for(;r.length>3;)r.pop()();if(r[1]0&&void 0!==arguments[0]?arguments[0]:this.props;return{skin:he.get("skin")||e.skin,theme:this.initTheme(e.theme)}}},{key:"componentWillMount",value:function(){this.dir=xe.rtl?"rtl":"ltr",this.refs={menu:{current:null},navigation:{current:null},scroll:{current:null},search:{current:null},searchInput:{current:null},skinToneButton:{current:null},skinToneRadio:{current:null}},this.initGrid(),0==this.props.stickySearch&&"sticky"==this.props.searchPosition&&(console.warn("[EmojiMart] Deprecation warning: `stickySearch` has been renamed `searchPosition`."),this.props.searchPosition="static")}},{key:"componentDidMount",value:function(){if(this.register(),this.shadowRoot=this.base.parentNode,this.props.autoFocus){var e=this.refs.searchInput;e.current&&e.current.focus()}}},{key:"componentWillReceiveProps",value:function(e){var t=this;for(var r in this.nextState||(this.nextState={}),e)this.nextState[r]=e[r];clearTimeout(this.nextStateTimer),this.nextStateTimer=setTimeout((function(){var e=!1;for(var r in t.nextState)t.props[r]=t.nextState[r],"custom"!==r&&"categories"!==r||(e=!0);delete t.nextState;var n=t.getInitialState();if(e)return t.reset(n);t.setState(n)}))}},{key:"componentWillUnmount",value:function(){this.unregister()}},{key:"reset",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},r=this;return i(t(z).mark((function n(){return t(z).wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return t.next=2,Pe(r.props);case 2:r.initGrid(),r.unobserve(),r.setState(e,(function(){r.observeCategories(),r.observeRows()}));case 5:case"end":return t.stop()}}),n)})))()}},{key:"register",value:function(){document.addEventListener("click",this.handleClickOutside),this.observe()}},{key:"unregister",value:function(){var e;document.removeEventListener("click",this.handleClickOutside),null===(e=this.darkMedia)||void 0===e||e.removeEventListener("change",this.darkMediaCallback),this.unobserve()}},{key:"observe",value:function(){this.observeCategories(),this.observeRows()}},{key:"unobserve",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=e.except,r=void 0===t?[]:t;Array.isArray(r)||(r=[r]);var n=!0,o=!1,i=void 0;try{for(var a,s=this.observers[Symbol.iterator]();!(n=(a=s.next()).done);n=!0){var c=a.value;r.includes(c)||c.disconnect()}}catch(e){o=!0,i=e}finally{try{n||null==s.return||s.return()}finally{if(o)throw i}}this.observers=[].concat(r)}},{key:"initGrid",value:function(){var e=this,t=Ce.categories;this.refs.categories=new Map;var r=Ce.categories.map((function(e){return e.id})).join(",");this.navKey&&this.navKey!=r&&this.refs.scroll.current&&(this.refs.scroll.current.scrollTop=0),this.navKey=r,this.grid=[],this.grid.setsize=0;var n=function(t,r){var n=[];n.__categoryId=r.id,n.__index=t.length,e.grid.push(n);var o=e.grid.length-1,i=o%Ht?{}:{current:null};return i.index=o,i.posinset=e.grid.setsize+1,t.push(i),n},o=!0,i=!1,a=void 0;try{for(var s,c=t[Symbol.iterator]();!(o=(s=c.next()).done);o=!0){var l=s.value,u=[],d=n(u,l),h=!0,p=!1,f=void 0;try{for(var v,g=l.emojis[Symbol.iterator]();!(h=(v=g.next()).done);h=!0){var m=v.value;d.length==this.getPerLine()&&(d=n(u,l)),this.grid.setsize+=1,d.push(m)}}catch(e){p=!0,f=e}finally{try{h||null==g.return||g.return()}finally{if(p)throw f}}this.refs.categories.set(l.id,{root:{current:null},rows:u})}}catch(e){i=!0,a=e}finally{try{o||null==c.return||c.return()}finally{if(i)throw a}}}},{key:"initTheme",value:function(e){if("auto"!=e)return e;if(!this.darkMedia){if(this.darkMedia=matchMedia("(prefers-color-scheme: dark)"),this.darkMedia.media.match(/^not/))return"light";this.darkMedia.addEventListener("change",this.darkMediaCallback)}return this.darkMedia.matches?"dark":"light"}},{key:"initDynamicPerLine",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:this.props,t=this;if(e.dynamicWidth){var r=e.element,n=e.emojiButtonSize,o=function(){var e=r.getBoundingClientRect().width;return Math.floor(e/n)},i=new ResizeObserver((function(){var e=t;t.unobserve({except:i}),t.setState({perLine:o()},(function(){var t=e;e.initGrid(),e.forceUpdate((function(){t.observeCategories(),t.observeRows()}))}))}));return i.observe(r),this.observers.push(i),o()}}},{key:"getPerLine",value:function(){return this.state.perLine||this.props.perLine}},{key:"getEmojiByPos",value:function(e){var t=b(e,2),r=t[0],n=t[1],o=this.state.searchResults||this.grid,i=o[r]&&o[r][n];if(i)return Ie.get(i)}},{key:"observeCategories",value:function(){var e=this.refs.navigation.current;if(e){var t=new Map,r={root:this.refs.scroll.current,threshold:[0,1]},n=new IntersectionObserver((function(r){var n=!0,o=!1,i=void 0;try{for(var a,s=r[Symbol.iterator]();!(n=(a=s.next()).done);n=!0){var c=a.value,l=c.target.dataset.id;t.set(l,c.intersectionRatio)}}catch(e){o=!0,i=e}finally{try{n||null==s.return||s.return()}finally{if(o)throw i}}var u,d=w(t),h=!0,p=!1,f=void 0;try{for(var v,g=d[Symbol.iterator]();!(h=(v=g.next()).done);h=!0){var m=b(v.value,2),_=m[0];if(m[1]){(u=_)!=e.state.categoryId&&e.setState({categoryId:u});break}}}catch(e){p=!0,f=e}finally{try{h||null==g.return||g.return()}finally{if(p)throw f}}}),r),o=!0,i=!1,a=void 0;try{for(var s,c=this.refs.categories.values()[Symbol.iterator]();!(o=(s=c.next()).done);o=!0){var l=s.value.root;n.observe(l.current)}}catch(e){i=!0,a=e}finally{try{o||null==c.return||c.return()}finally{if(i)throw a}}this.observers.push(n)}}},{key:"observeRows",value:function(){var e=this,t=p({},this.state.visibleRows),r=new IntersectionObserver((function(r){var n=!0,o=!1,i=void 0;try{for(var a,s=r[Symbol.iterator]();!(n=(a=s.next()).done);n=!0){var c=a.value,l=parseInt(c.target.dataset.index);c.isIntersecting?t[l]=!0:delete t[l]}}catch(e){o=!0,i=e}finally{try{n||null==s.return||s.return()}finally{if(o)throw i}}e.setState({visibleRows:t})}),{root:this.refs.scroll.current,rootMargin:"".concat(this.props.emojiButtonSize*(Ht+5),"px 0px ").concat(this.props.emojiButtonSize*Ht,"px")}),n=!0,o=!1,i=void 0;try{for(var a,s=this.refs.categories.values()[Symbol.iterator]();!(n=(a=s.next()).done);n=!0){var c=a.value.rows,l=!0,u=!1,d=void 0;try{for(var h,f=c[Symbol.iterator]();!(l=(h=f.next()).done);l=!0){var v=h.value;v.current&&r.observe(v.current)}}catch(e){u=!0,d=e}finally{try{l||null==f.return||f.return()}finally{if(u)throw d}}}}catch(e){o=!0,i=e}finally{try{n||null==s.return||s.return()}finally{if(o)throw i}}this.observers.push(r)}},{key:"preventDefault",value:function(e){e.preventDefault()}},{key:"unfocusSearch",value:function(){var e=this.refs.searchInput.current;e&&e.blur()}},{key:"navigate",value:function(e){var t=e.e,r=e.input,n=e.left,o=e.right,i=e.up,a=e.down,s=this,c=this.state.searchResults||this.grid;if(c.length){var l=b(this.state.pos,2),u=l[0],d=l[1],h=function(){if(0==u&&0==d&&!t.repeat&&(n||i))return null;if(-1==u)return t.repeat||!o&&!a||r.selectionStart!=r.value.length?null:[0,0];if(n||o){var e=c[u],s=n?-1:1;if(!e[d+=s]){if(!(e=c[u+=s]))return u=n?0:c.length-1,d=n?0:c[u].length-1,[u,d];d=n?e.length-1:0}return[u,d]}if(i||a){var l=c[u+=i?-1:1];return l?(l[d]||(d=l.length-1),[u,d]):(u=i?0:c.length-1,d=i?0:c[u].length-1,[u,d])}}();h?(t.preventDefault(),this.setState({pos:h,keyboard:!0},(function(){s.scrollTo({row:h[0]})}))):this.state.pos[0]>-1&&this.setState({pos:[-1,-1]})}}},{key:"scrollTo",value:function(e){var t=e.categoryId,r=e.row,n=this.state.searchResults||this.grid;if(n.length){var o=this.refs.scroll.current,i=o.getBoundingClientRect(),a=0;if(r>=0&&(t=n[r].__categoryId),t)a=(this.refs[t]||this.refs.categories.get(t).root).current.getBoundingClientRect().top-(i.top-o.scrollTop)+1;if(r>=0)if(r){var s=a+n[r].__index*this.props.emojiButtonSize,c=s+this.props.emojiButtonSize+.88*this.props.emojiButtonSize;if(so.scrollTop+i.height))return;a=c-i.height}}else a=0;this.ignoreMouse(),o.scrollTop=a}}},{key:"ignoreMouse",value:function(){var e=this;this.mouseIsIgnored=!0,clearTimeout(this.ignoreMouseTimer),this.ignoreMouseTimer=setTimeout((function(){delete e.mouseIsIgnored}),100)}},{key:"handleEmojiOver",value:function(e){this.mouseIsIgnored||this.state.showSkins||this.setState({pos:e||[-1,-1],keyboard:!1})}},{key:"handleEmojiClick",value:function(e){var t=e.e,r=e.emoji,n=e.pos;if(this.props.onEmojiSelect&&(!r&&n&&(r=this.getEmojiByPos(n)),r)){var o=Ue(r,{skinIndex:this.state.skin-1});this.props.maxFrequentRows&&ke.add(o,this.props),this.props.onEmojiSelect(o,t)}}},{key:"closeSkins",value:function(){this.state.showSkins&&(this.setState({showSkins:null,tempSkin:null}),this.base.removeEventListener("click",this.handleBaseClick),this.base.removeEventListener("keydown",this.handleBaseKeydown))}},{key:"handleSkinMouseOver",value:function(e){this.setState({tempSkin:e})}},{key:"handleSkinClick",value:function(e){this.ignoreMouse(),this.closeSkins(),this.setState({skin:e,tempSkin:null}),he.set("skin",e)}},{key:"renderNav",value:function(){return de(At,{ref:this.refs.navigation,icons:this.props.icons,theme:this.state.theme,dir:this.dir,unfocused:!!this.state.searchResults,position:this.props.navPosition,onClick:this.handleCategoryClick},this.navKey)}},{key:"renderPreview",value:function(){var e=this.getEmojiByPos(this.state.pos),t=this.state.searchResults&&!this.state.searchResults.length;return de("div",{id:"preview",class:"flex flex-middle",dir:this.dir,"data-position":this.props.previewPosition,children:[de("div",{class:"flex flex-middle flex-grow",children:[de("div",{class:"flex flex-auto flex-middle flex-center",style:{height:this.props.emojiButtonSize,fontSize:this.props.emojiButtonSize},children:de(Ge,{emoji:e,id:t?this.props.noResultsEmoji||"cry":this.props.previewEmoji||("top"==this.props.previewPosition?"point_down":"point_up"),set:this.props.set,size:this.props.emojiButtonSize,skin:this.state.tempSkin||this.state.skin,spritesheet:!0,getSpritesheetURL:this.props.getSpritesheetURL})}),de("div",{class:"margin-".concat(this.dir[0]),children:de("div",e||t?{class:"padding-".concat(this.dir[2]," align-").concat(this.dir[0]),children:[de("div",{class:"preview-title ellipsis",children:e?e.name:xe.search_no_results_1}),de("div",{class:"preview-subtitle ellipsis color-c",children:e?e.skins[0].shortcodes:xe.search_no_results_2})]}:{class:"preview-placeholder color-c",children:xe.pick})})]}),!e&&"preview"==this.props.skinTonePosition&&this.renderSkinToneButton()]})}},{key:"renderEmojiButton",value:function(e,t){var r,n,o=t.pos,i=t.posinset,a=t.grid,s=this,c=this.props.emojiButtonSize,l=this.state.tempSkin||this.state.skin,u=(e.skins[l-1]||e.skins[0]).native,d=(r=this.state.pos,n=o,Array.isArray(r)&&Array.isArray(n)&&r.length===n.length&&r.every((function(e,t){return e==n[t]}))),h=o.concat(e.id).join("");return de(Tt,{selected:d,skin:l,size:c,children:de("button",{"aria-label":u,"aria-selected":d||void 0,"aria-posinset":i,"aria-setsize":a.setsize,"data-keyboard":this.state.keyboard,title:"none"==this.props.previewPosition?e.name:void 0,type:"button",class:"flex flex-center flex-middle",tabindex:"-1",onClick:function(t){return s.handleEmojiClick({e:t,emoji:e})},onMouseEnter:function(){return s.handleEmojiOver(o)},onMouseLeave:function(){return s.handleEmojiOver()},style:{width:this.props.emojiButtonSize,height:this.props.emojiButtonSize,fontSize:this.props.emojiSize,lineHeight:0},children:[de("div",{"aria-hidden":"true",class:"background",style:{borderRadius:this.props.emojiButtonRadius,backgroundColor:this.props.emojiButtonColors?this.props.emojiButtonColors[(i-1)%this.props.emojiButtonColors.length]:void 0}}),de(Ge,{emoji:e,set:this.props.set,size:this.props.emojiSize,skin:l,spritesheet:!0,getSpritesheetURL:this.props.getSpritesheetURL})]})},h)}},{key:"renderSearch",value:function(){var e="none"==this.props.previewPosition||"search"==this.props.skinTonePosition;return de("div",{children:[de("div",{class:"spacer"}),de("div",{class:"flex flex-middle",children:[de("div",{class:"search relative flex-grow",children:[de("input",{type:"search",ref:this.refs.searchInput,placeholder:xe.search,onClick:this.handleSearchClick,onInput:this.handleSearchInput,onKeyDown:this.handleSearchKeyDown,autoComplete:"off"}),de("span",{class:"icon loupe flex",children:We.search.loupe}),this.state.searchResults&&de("button",{title:"Clear","aria-label":"Clear",type:"button",class:"icon delete flex",onClick:this.clearSearch,onMouseDown:this.preventDefault,children:We.search.delete})]}),e&&this.renderSkinToneButton()]})]})}},{key:"renderSearchResults",value:function(){var e=this,t=this.state.searchResults;return t?de("div",{class:"category",ref:this.refs.search,children:[de("div",{class:"sticky padding-small align-".concat(this.dir[0]),children:xe.categories.search}),de("div",{children:t.length?t.map((function(r,n){var o=e;return de("div",{class:"flex",children:r.map((function(e,r){return o.renderEmojiButton(e,{pos:[n,r],posinset:n*o.props.perLine+r+1,grid:t})}))})})):de("div",{class:"padding-small align-".concat(this.dir[0]),children:this.props.onAddCustomEmoji&&de("a",{onClick:this.props.onAddCustomEmoji,children:xe.add_custom})})})]}):null}},{key:"renderCategories",value:function(){var e=this,t=Ce.categories,r=!!this.state.searchResults,n=this.getPerLine();return de("div",{style:{visibility:r?"hidden":void 0,display:r?"none":void 0,height:"100%"},children:t.map((function(t){var r=e,o=e.refs.categories.get(t.id),i=o.root,a=o.rows;return de("div",{"data-id":t.target?t.target.id:t.id,class:"category",ref:i,children:[de("div",{class:"sticky padding-small align-".concat(e.dir[0]),children:t.name||xe.categories[t.id]}),de("div",{class:"relative",style:{height:a.length*e.props.emojiButtonSize},children:a.map((function(e,o){var i,a=r,s=e.index-e.index%Ht,c=r.state.visibleRows[s],l="current"in e?e:void 0;if(!c&&!l)return null;var u=o*n,d=u+n,h=t.emojis.slice(u,d);return h.length*{position:relative}.category button .background{opacity:0;background-color:var(--em-color-border);transition:opacity var(--duration-fast)var(--easing)var(--duration-instant);position:absolute;top:0;bottom:0;left:0;right:0}.category button:hover .background{transition-duration:var(--duration-instant);transition-delay:0s}.category button[aria-selected] .background{opacity:1}.category button[data-keyboard] .background{transition:none}.row{width:100%;position:absolute;top:0;left:0}.skin-tone-button{border:1px solid transparent;border-radius:100%}.skin-tone-button:hover{border-color:var(--em-color-border)}.skin-tone-button:active .skin-tone{transform:scale(.85)!important}.skin-tone-button .skin-tone{transition:transform var(--duration)var(--easing)}.skin-tone-button[aria-selected]{background-color:var(--em-color-border);border-top-color:rgba(0,0,0,.05);border-bottom-color:transparent;border-left-width:0;border-right-width:0}.skin-tone-button[aria-selected] .skin-tone{transform:scale(.9)}.menu{z-index:2;white-space:nowrap;border:1px solid var(--em-color-border);background-color:rgba(var(--em-rgb-background),.9);-webkit-backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);transition-property:opacity,transform;transition-duration:var(--duration);transition-timing-function:var(--easing);border-radius:10px;padding:4px;position:absolute;box-shadow:1px 1px 5px rgba(0,0,0,.05)}.menu.hidden{opacity:0}.menu[data-position=bottom]{transform-origin:100% 100%}.menu[data-position=bottom].hidden{transform:scale(.9)rotate(-3deg)translateY(5%)}.menu[data-position=top]{transform-origin:100% 0}.menu[data-position=top].hidden{transform:scale(.9)rotate(3deg)translateY(-5%)}.menu input[type=radio]{clip:rect(0 0 0 0);width:1px;height:1px;border:0;margin:0;padding:0;position:absolute;overflow:hidden}.menu input[type=radio]:checked+.option{box-shadow:0 0 0 2px rgb(var(--em-rgb-accent))}.option{width:100%;border-radius:6px;padding:4px 6px}.option:hover{color:#fff;background-color:rgb(var(--em-rgb-accent))}.skin-tone{width:16px;height:16px;border-radius:100%;display:inline-block;position:relative;overflow:hidden}.skin-tone:after{content:"";mix-blend-mode:overlay;background:linear-gradient(rgba(255,255,255,.2),transparent);border:1px solid rgba(0,0,0,.8);border-radius:100%;position:absolute;top:0;bottom:0;left:0;right:0;box-shadow:inset 0 -2px 3px #000,inset 0 1px 2px #fff}.skin-tone-1{background-color:#ffc93a}.skin-tone-2{background-color:#ffdab7}.skin-tone-3{background-color:#e7b98f}.skin-tone-4{background-color:#c88c61}.skin-tone-5{background-color:#a46134}.skin-tone-6{background-color:#5d4437}[data-index]{justify-content:space-between}[data-emoji-set=twitter] .skin-tone:after{box-shadow:none;border-color:rgba(0,0,0,.5)}[data-emoji-set=twitter] .skin-tone-1{background-color:#fade72}[data-emoji-set=twitter] .skin-tone-2{background-color:#f3dfd0}[data-emoji-set=twitter] .skin-tone-3{background-color:#eed3a8}[data-emoji-set=twitter] .skin-tone-4{background-color:#cfad8d}[data-emoji-set=twitter] .skin-tone-5{background-color:#a8805d}[data-emoji-set=twitter] .skin-tone-6{background-color:#765542}[data-emoji-set=google] .skin-tone:after{box-shadow:inset 0 0 2px 2px rgba(0,0,0,.4)}[data-emoji-set=google] .skin-tone-1{background-color:#f5c748}[data-emoji-set=google] .skin-tone-2{background-color:#f1d5aa}[data-emoji-set=google] .skin-tone-3{background-color:#d4b48d}[data-emoji-set=google] .skin-tone-4{background-color:#aa876b}[data-emoji-set=google] .skin-tone-5{background-color:#916544}[data-emoji-set=google] .skin-tone-6{background-color:#61493f}[data-emoji-set=facebook] .skin-tone:after{border-color:rgba(0,0,0,.4);box-shadow:inset 0 -2px 3px #000,inset 0 1px 4px #fff}[data-emoji-set=facebook] .skin-tone-1{background-color:#f5c748}[data-emoji-set=facebook] .skin-tone-2{background-color:#f1d5aa}[data-emoji-set=facebook] .skin-tone-3{background-color:#d4b48d}[data-emoji-set=facebook] .skin-tone-4{background-color:#aa876b}[data-emoji-set=facebook] .skin-tone-5{background-color:#916544}[data-emoji-set=facebook] .skin-tone-6{background-color:#61493f}',window.EmojiMart=r}(); +//# sourceMappingURL=browser.js.map