From de63107420b63653c41f957dcb7d8a11c78d4539 Mon Sep 17 00:00:00 2001 From: Dorian Date: Wed, 6 May 2026 20:32:37 +0100 Subject: [PATCH] Add image stickers and fix desktop chat composer --- apps/web/public/stickers/anon-miner.svg | 9 +++ apps/web/public/stickers/block-denied.svg | 8 ++ apps/web/public/stickers/block-hopium.svg | 10 +++ apps/web/public/stickers/boomer-unit.svg | 7 ++ apps/web/public/stickers/chad-hash.svg | 8 ++ apps/web/public/stickers/chart-crime.svg | 8 ++ apps/web/public/stickers/fan-panic.svg | 10 +++ apps/web/public/stickers/fiat-npc.svg | 10 +++ apps/web/public/stickers/heat-death.svg | 9 +++ apps/web/public/stickers/money-printer.svg | 8 ++ apps/web/public/stickers/odds-wizard.svg | 9 +++ apps/web/public/stickers/printer-loop.svg | 11 +++ apps/web/src/chatStickers.ts | 91 ++++++++++++++++++++-- apps/web/src/components/ChatDrawer.vue | 59 ++++++++++++-- 14 files changed, 244 insertions(+), 13 deletions(-) create mode 100644 apps/web/public/stickers/anon-miner.svg create mode 100644 apps/web/public/stickers/block-denied.svg create mode 100644 apps/web/public/stickers/block-hopium.svg create mode 100644 apps/web/public/stickers/boomer-unit.svg create mode 100644 apps/web/public/stickers/chad-hash.svg create mode 100644 apps/web/public/stickers/chart-crime.svg create mode 100644 apps/web/public/stickers/fan-panic.svg create mode 100644 apps/web/public/stickers/fiat-npc.svg create mode 100644 apps/web/public/stickers/heat-death.svg create mode 100644 apps/web/public/stickers/money-printer.svg create mode 100644 apps/web/public/stickers/odds-wizard.svg create mode 100644 apps/web/public/stickers/printer-loop.svg diff --git a/apps/web/public/stickers/anon-miner.svg b/apps/web/public/stickers/anon-miner.svg new file mode 100644 index 0000000..664bf61 --- /dev/null +++ b/apps/web/public/stickers/anon-miner.svg @@ -0,0 +1,9 @@ + + + + + + + + ANON FOUND HASHES + diff --git a/apps/web/public/stickers/block-denied.svg b/apps/web/public/stickers/block-denied.svg new file mode 100644 index 0000000..59313a1 --- /dev/null +++ b/apps/web/public/stickers/block-denied.svg @@ -0,0 +1,8 @@ + + + + + BLOCK + DENIED + TRY ANOTHER 2 BILLION TIMES + diff --git a/apps/web/public/stickers/block-hopium.svg b/apps/web/public/stickers/block-hopium.svg new file mode 100644 index 0000000..8e6f7be --- /dev/null +++ b/apps/web/public/stickers/block-hopium.svg @@ -0,0 +1,10 @@ + + + + + + ? + + + THIS ONE IS OURS + diff --git a/apps/web/public/stickers/boomer-unit.svg b/apps/web/public/stickers/boomer-unit.svg new file mode 100644 index 0000000..42c2e32 --- /dev/null +++ b/apps/web/public/stickers/boomer-unit.svg @@ -0,0 +1,7 @@ + + + + + + 0 H/s AND PROUD + diff --git a/apps/web/public/stickers/chad-hash.svg b/apps/web/public/stickers/chad-hash.svg new file mode 100644 index 0000000..f6f87c8 --- /dev/null +++ b/apps/web/public/stickers/chad-hash.svg @@ -0,0 +1,8 @@ + + + + + + + HASHING. UNBOTHERED. + diff --git a/apps/web/public/stickers/chart-crime.svg b/apps/web/public/stickers/chart-crime.svg new file mode 100644 index 0000000..6710ab0 --- /dev/null +++ b/apps/web/public/stickers/chart-crime.svg @@ -0,0 +1,8 @@ + + + + + + + CHART CRIME + diff --git a/apps/web/public/stickers/fan-panic.svg b/apps/web/public/stickers/fan-panic.svg new file mode 100644 index 0000000..49e59d8 --- /dev/null +++ b/apps/web/public/stickers/fan-panic.svg @@ -0,0 +1,10 @@ + + + + + + + + + FAN PANIC + diff --git a/apps/web/public/stickers/fiat-npc.svg b/apps/web/public/stickers/fiat-npc.svg new file mode 100644 index 0000000..07e0021 --- /dev/null +++ b/apps/web/public/stickers/fiat-npc.svg @@ -0,0 +1,10 @@ + + + + + + + + I TRUST BANKS + NORMAL HEATER USER + diff --git a/apps/web/public/stickers/heat-death.svg b/apps/web/public/stickers/heat-death.svg new file mode 100644 index 0000000..265bf37 --- /dev/null +++ b/apps/web/public/stickers/heat-death.svg @@ -0,0 +1,9 @@ + + + + + + + + PROOF OF WARMTH + diff --git a/apps/web/public/stickers/money-printer.svg b/apps/web/public/stickers/money-printer.svg new file mode 100644 index 0000000..8abeae8 --- /dev/null +++ b/apps/web/public/stickers/money-printer.svg @@ -0,0 +1,8 @@ + + + + + + BRRR + FIAT PATCH NOTES + diff --git a/apps/web/public/stickers/odds-wizard.svg b/apps/web/public/stickers/odds-wizard.svg new file mode 100644 index 0000000..f622058 --- /dev/null +++ b/apps/web/public/stickers/odds-wizard.svg @@ -0,0 +1,9 @@ + + + + + + + + THE ODDS SAID LOL + diff --git a/apps/web/public/stickers/printer-loop.svg b/apps/web/public/stickers/printer-loop.svg new file mode 100644 index 0000000..ee8d477 --- /dev/null +++ b/apps/web/public/stickers/printer-loop.svg @@ -0,0 +1,11 @@ + + + + + + + FIAT + + BRRR + INFINITE PATCH + diff --git a/apps/web/src/chatStickers.ts b/apps/web/src/chatStickers.ts index 911ed4b..3f1299d 100644 --- a/apps/web/src/chatStickers.ts +++ b/apps/web/src/chatStickers.ts @@ -2,10 +2,12 @@ export type ChatSticker = { label: string; text: string; kind?: "sticker" | "gif"; + image?: string; }; export const STICKER_PREFIX = "[gashboard sticker]"; const GIF_MARKER = "[gif]"; +const IMAGE_MARKER = "[img:"; export const CHAT_STICKERS: ChatSticker[] = [ { label: "fiat heater", text: "FIAT HEATER DETECTED: warm room, cold wallet." }, @@ -48,17 +50,94 @@ export const CHAT_STICKERS: ChatSticker[] = [ { label: "main character", text: "MAIN CHARACTER ENERGY, BACKGROUND HASHRATE." }, { label: "block soon", text: "BLOCK SOON. SOURCE: THE SAME PART OF MY BRAIN THAT BUYS TOPS." }, { label: "heater max", text: "HEATER ON MAX. HASHES ON VACATION." }, + { + label: "anon miner", + text: "ANON DISCOVERS SOLO MINING AND IMMEDIATELY BECOMES A PROBLEM.", + image: "/stickers/anon-miner.svg", + }, + { + label: "fiat npc", + text: "I JUST USE A NORMAL HEATER BECAUSE THE BANK SAID IT WAS FINE.", + image: "/stickers/fiat-npc.svg", + }, + { + label: "chad hash", + text: "LOUD, WARM, WRONG, AND STILL MORE SOVEREIGN THAN YOUR SAVINGS ACCOUNT.", + image: "/stickers/chad-hash.svg", + }, + { + label: "money printer", + text: "CENTRAL BANK MONETARY POLICY, ARTIST'S IMPRESSION.", + image: "/stickers/money-printer.svg", + }, + { + label: "block denied", + text: "THE NETWORK REVIEWED YOUR NONCE AND CHOSE VIOLENCE.", + image: "/stickers/block-denied.svg", + }, + { + label: "boomer unit", + text: "LEGACY HEATER ACHIEVES ZERO HASHES PER RETIREMENT.", + image: "/stickers/boomer-unit.svg", + }, + { + label: "chart crime", + text: "TECHNICAL ANALYSIS CONDUCTED BY A FAN WITH A WALLET.", + image: "/stickers/chart-crime.svg", + }, + { + label: "odds wizard", + text: "I HAVE CONSULTED THE ODDS AND THEY SAID LOL.", + image: "/stickers/odds-wizard.svg", + }, + { + label: "gif fan panic", + kind: "gif", + text: "LIVE FAN FOOTAGE AFTER HASHRATE MOVES BY A DECIMAL.", + image: "/stickers/fan-panic.svg", + }, + { + label: "gif printer", + kind: "gif", + text: "FIAT POLICY IN ONE LOOP.", + image: "/stickers/printer-loop.svg", + }, + { + label: "gif block hopium", + kind: "gif", + text: "SOLO MINER WATCHING EVERY NEW BLOCK LIKE IT OWES HIM MONEY.", + image: "/stickers/block-hopium.svg", + }, + { + label: "gif heat death", + kind: "gif", + text: "THE ROOM IS WARMER THAN THE INVESTMENT THESIS.", + image: "/stickers/heat-death.svg", + }, ]; export function stickerContent(sticker: ChatSticker): string { - return `${STICKER_PREFIX} ${sticker.kind === "gif" ? `${GIF_MARKER} ` : ""}${sticker.text}`; + const image = sticker.image ? `${IMAGE_MARKER}${sticker.image}] ` : ""; + return `${STICKER_PREFIX} ${image}${sticker.kind === "gif" ? `${GIF_MARKER} ` : ""}${sticker.text}`; } -export function unsticker(content: string): { text: string; kind: "sticker" | "gif" } | null { +export function unsticker( + content: string, +): { text: string; kind: "sticker" | "gif"; image?: string } | null { if (!content.startsWith(`${STICKER_PREFIX} `)) return null; - const body = content.slice(STICKER_PREFIX.length + 1); - if (body.startsWith(`${GIF_MARKER} `)) { - return { text: body.slice(GIF_MARKER.length + 1), kind: "gif" }; + let body = content.slice(STICKER_PREFIX.length + 1); + let image: string | undefined; + if (body.startsWith(IMAGE_MARKER)) { + const end = body.indexOf("] "); + if (end > IMAGE_MARKER.length) { + image = body.slice(IMAGE_MARKER.length, end); + body = body.slice(end + 2); + } } - return { text: body, kind: "sticker" }; + if (body.startsWith(`${GIF_MARKER} `)) { + const parsed = { text: body.slice(GIF_MARKER.length + 1), kind: "gif" as const }; + return image ? { ...parsed, image } : parsed; + } + const parsed = { text: body, kind: "sticker" as const }; + return image ? { ...parsed, image } : parsed; } diff --git a/apps/web/src/components/ChatDrawer.vue b/apps/web/src/components/ChatDrawer.vue index dbe7cc9..8ebfa2f 100644 --- a/apps/web/src/components/ChatDrawer.vue +++ b/apps/web/src/components/ChatDrawer.vue @@ -126,6 +126,10 @@ function stickerKind(content: string): "sticker" | "gif" | null { return unsticker(content)?.kind ?? null; } +function stickerImage(content: string): string | null { + return unsticker(content)?.image ?? null; +} + async function scrollBottom(): Promise { await nextTick(); if (listEl.value) listEl.value.scrollTop = listEl.value.scrollHeight; @@ -169,7 +173,10 @@ async function scrollBottom(): Promise { {{ timeLabel(message.createdAt) }}

{{ message.content }}

-

{{ stickerText(message.content) }}

+
+ + {{ stickerText(message.content) }} +
@@ -191,6 +198,7 @@ async function scrollBottom(): Promise { :disabled="!canSend" @click="sendSticker(sticker)" > + {{ sticker.label }} {{ sticker.text }} @@ -235,10 +243,13 @@ async function scrollBottom(): Promise { z-index: 20; width: min(430px, 92vw); height: 100dvh; + --chat-pad: 16px; + --composer-bottom: 24px; + --composer-height: 70px; display: grid; - grid-template-rows: auto auto minmax(0, 1fr) auto; + grid-template-rows: auto auto minmax(0, 1fr); gap: 12px; - padding: 16px 16px 28px; + padding: var(--chat-pad); background: rgba(7, 9, 15, 0.97); border-left: 1px solid var(--line-bright); box-shadow: -16px 0 40px rgba(0, 0, 0, 0.45); @@ -289,6 +300,7 @@ h2 { flex-direction: column; gap: 14px; padding-right: 4px; + padding-bottom: calc(var(--composer-height) + var(--composer-bottom) + 18px); } .empty { padding: 24px 0; @@ -355,7 +367,7 @@ p { overflow-wrap: anywhere; font-size: 12px; } -p.sticker { +.sticker { border: 1px solid var(--neon-magenta); color: var(--neon-amber); padding: 10px; @@ -364,19 +376,39 @@ p.sticker { text-transform: uppercase; box-shadow: inset 0 0 18px rgba(255, 61, 240, 0.08); } -p.sticker.gif { +.sticker img { + display: block; + width: min(220px, 100%); + aspect-ratio: 1 / 1; + height: auto; + margin-bottom: 8px; + border: 1px solid var(--line-bright); + object-fit: contain; + background: var(--bg-0); +} +.sticker span { + display: block; + white-space: pre-wrap; + overflow-wrap: anywhere; +} +.sticker.gif { animation: sticker-bounce 1.4s steps(2, end) infinite; background: linear-gradient(90deg, rgba(255, 61, 240, 0.12), rgba(41, 255, 230, 0.08)), rgba(255, 255, 255, 0.025); } .composer { + position: fixed; + right: var(--chat-pad); + bottom: var(--composer-bottom); + z-index: 4; + width: calc(min(430px, 92vw) - (var(--chat-pad) * 2)); + min-height: var(--composer-height); display: grid; grid-template-columns: 78px minmax(0, 1fr) 64px; gap: 8px; align-items: start; padding-top: 10px; - padding-bottom: 2px; border-top: 1px solid var(--line); background: rgba(7, 9, 15, 0.97); } @@ -424,7 +456,7 @@ p.sticker.gif { } .sticker-tray button { min-width: 0; - min-height: 74px; + min-height: 138px; display: flex; flex-direction: column; gap: 5px; @@ -437,6 +469,14 @@ p.sticker.gif { text-align: left; overflow-wrap: anywhere; } +.sticker-tray button img { + width: 100%; + aspect-ratio: 1 / 1; + max-height: 118px; + object-fit: contain; + border: 1px solid var(--line); + background: var(--bg-0); +} .sticker-tray button span { color: var(--neon-cyan); } @@ -488,6 +528,8 @@ textarea { width: 100vw; height: 100svh; max-height: none; + --chat-pad: 14px; + grid-template-rows: auto auto minmax(0, 1fr) auto; padding: 14px 14px calc(72px + env(safe-area-inset-bottom)); transform: translateY(105%); border-left: 0; @@ -498,8 +540,11 @@ textarea { } .messages { gap: 10px; + padding-bottom: 0; } .composer { + position: static; + width: auto; grid-template-columns: 72px minmax(0, 1fr) 58px; gap: 6px; }