Add image stickers and fix desktop chat composer

This commit is contained in:
Dorian
2026-05-06 20:32:37 +01:00
parent d7976c8d14
commit de63107420
14 changed files with 244 additions and 13 deletions

View File

@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256">
<rect width="256" height="256" fill="#0a0d17"/>
<path d="M46 54h164v128H46z" fill="#151b2b" stroke="#29ffe6" stroke-width="4"/>
<circle cx="99" cy="105" r="26" fill="#e8edff"/>
<circle cx="157" cy="105" r="26" fill="#e8edff"/>
<path d="M77 159c25 20 77 20 102 0" fill="none" stroke="#ff3df0" stroke-width="8"/>
<path d="M65 62l126 126M191 62L65 188" stroke="#1f2a4a" stroke-width="5"/>
<text x="128" y="222" text-anchor="middle" fill="#ffd84a" font-family="monospace" font-size="18" font-weight="700">ANON FOUND HASHES</text>
</svg>

After

Width:  |  Height:  |  Size: 610 B

View File

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256">
<rect width="256" height="256" fill="#07090f"/>
<rect x="54" y="42" width="148" height="148" fill="#131a2e" stroke="#29ffe6" stroke-width="5"/>
<path d="M76 76l104 104M180 76L76 180" stroke="#ff4f78" stroke-width="15"/>
<text x="128" y="118" text-anchor="middle" fill="#ffd84a" font-family="monospace" font-size="18" font-weight="700">BLOCK</text>
<text x="128" y="142" text-anchor="middle" fill="#ffd84a" font-family="monospace" font-size="18" font-weight="700">DENIED</text>
<text x="128" y="224" text-anchor="middle" fill="#b6c3e0" font-family="monospace" font-size="13">TRY ANOTHER 2 BILLION TIMES</text>
</svg>

After

Width:  |  Height:  |  Size: 691 B

View File

@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256">
<rect width="256" height="256" fill="#07090f"/>
<g>
<animateTransform attributeName="transform" type="translate" values="0 0;0 -10;0 0" dur=".8s" repeatCount="indefinite"/>
<rect x="74" y="52" width="108" height="108" fill="#131a2e" stroke="#29ffe6" stroke-width="5"/>
<text x="128" y="113" text-anchor="middle" fill="#ffd84a" font-family="monospace" font-size="24" font-weight="700">?</text>
</g>
<path d="M82 185c24 13 68 13 92 0" fill="none" stroke="#ff3df0" stroke-width="8"/>
<text x="128" y="224" text-anchor="middle" fill="#b6c3e0" font-family="monospace" font-size="14">THIS ONE IS OURS</text>
</svg>

After

Width:  |  Height:  |  Size: 694 B

View File

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256">
<rect width="256" height="256" fill="#0a0d17"/>
<rect x="65" y="48" width="126" height="145" rx="8" fill="#1b1f2b" stroke="#ff4f78" stroke-width="5"/>
<path d="M89 74h78M89 98h78M89 122h78M89 146h78" stroke="#6a7aa0" stroke-width="7"/>
<path d="M83 181h90" stroke="#ffd84a" stroke-width="8"/>
<text x="128" y="222" text-anchor="middle" fill="#ff4f78" font-family="monospace" font-size="18" font-weight="700">0 H/s AND PROUD</text>
</svg>

After

Width:  |  Height:  |  Size: 511 B

View File

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256">
<rect width="256" height="256" fill="#07090f"/>
<path d="M83 72l28-30h50l24 35-12 75-45 36-45-36z" fill="#d9d3c7" stroke="#29ffe6" stroke-width="5"/>
<path d="M86 76h87l-16-30h-45z" fill="#ff3df0"/>
<path d="M93 108h28M143 108h28" stroke="#07090f" stroke-width="7"/>
<path d="M101 145c23 13 49 13 72 0" fill="none" stroke="#07090f" stroke-width="7"/>
<text x="128" y="220" text-anchor="middle" fill="#6cff8c" font-family="monospace" font-size="18" font-weight="700">HASHING. UNBOTHERED.</text>
</svg>

After

Width:  |  Height:  |  Size: 576 B

View File

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256">
<rect width="256" height="256" fill="#07090f"/>
<rect x="34" y="47" width="188" height="139" fill="#101624" stroke="#2c3a66" stroke-width="4"/>
<path d="M52 164h154M52 127h154M52 90h154" stroke="#1f2a4a" stroke-width="3"/>
<path d="M55 156l32-2 22-47 31 42 18-9 17-60 31 3" fill="none" stroke="#29ffe6" stroke-width="7"/>
<circle cx="175" cy="80" r="9" fill="#ff4f78"/>
<text x="128" y="222" text-anchor="middle" fill="#ffd84a" font-family="monospace" font-size="18" font-weight="700">CHART CRIME</text>
</svg>

After

Width:  |  Height:  |  Size: 586 B

View File

@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256">
<rect width="256" height="256" fill="#07090f"/>
<circle cx="128" cy="112" r="68" fill="#101624" stroke="#29ffe6" stroke-width="5"/>
<g transform-origin="128 112">
<animateTransform attributeName="transform" type="rotate" from="0 128 112" to="360 128 112" dur=".45s" repeatCount="indefinite"/>
<path d="M128 112c38-44 75-21 45 11-16 17-38 7-45-11zM128 112c-38 44-75 21-45-11 16-17 38-7 45 11zM128 112c44 38 21 75-11 45-17-16-7-38 11-45zM128 112c-44-38-21-75 11-45 17 16 7 38-11 45z" fill="#ff3df0"/>
</g>
<circle cx="128" cy="112" r="13" fill="#ffd84a"/>
<text x="128" y="221" text-anchor="middle" fill="#ffd84a" font-family="monospace" font-size="18" font-weight="700">FAN PANIC</text>
</svg>

After

Width:  |  Height:  |  Size: 776 B

View File

@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256">
<rect width="256" height="256" fill="#080a10"/>
<circle cx="128" cy="93" r="58" fill="#8ea0c4" stroke="#2c3a66" stroke-width="5"/>
<circle cx="105" cy="86" r="9" fill="#0a0d17"/>
<circle cx="151" cy="86" r="9" fill="#0a0d17"/>
<path d="M103 123h50" stroke="#0a0d17" stroke-width="7"/>
<rect x="45" y="160" width="166" height="42" fill="#131a2e" stroke="#ff4f78" stroke-width="4"/>
<text x="128" y="187" text-anchor="middle" fill="#ff4f78" font-family="monospace" font-size="17" font-weight="700">I TRUST BANKS</text>
<text x="128" y="225" text-anchor="middle" fill="#ffd84a" font-family="monospace" font-size="14">NORMAL HEATER USER</text>
</svg>

After

Width:  |  Height:  |  Size: 726 B

View File

@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256">
<rect width="256" height="256" fill="#080a10"/>
<g>
<animateTransform attributeName="transform" type="scale" values="1;1.06;1" dur=".7s" repeatCount="indefinite" additive="sum"/>
<path d="M128 35c38 43 59 75 59 111 0 37-26 68-59 68s-59-31-59-68c0-36 21-68 59-111z" fill="#ff4f78" stroke="#ffd84a" stroke-width="5"/>
</g>
<path d="M106 144h44M105 166h46" stroke="#0a0d17" stroke-width="7"/>
<text x="128" y="232" text-anchor="middle" fill="#ffd84a" font-family="monospace" font-size="15" font-weight="700">PROOF OF WARMTH</text>
</svg>

After

Width:  |  Height:  |  Size: 616 B

View File

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256">
<rect width="256" height="256" fill="#0c1120"/>
<rect x="45" y="56" width="166" height="82" fill="#151b2b" stroke="#ff4f78" stroke-width="5"/>
<rect x="64" y="118" width="128" height="62" fill="#101624" stroke="#29ffe6" stroke-width="4"/>
<path d="M79 151h98" stroke="#ffd84a" stroke-width="7" stroke-dasharray="10 8"/>
<text x="128" y="103" text-anchor="middle" fill="#ffd84a" font-family="monospace" font-size="26" font-weight="700">BRRR</text>
<text x="128" y="218" text-anchor="middle" fill="#ff3df0" font-family="monospace" font-size="17" font-weight="700">FIAT PATCH NOTES</text>
</svg>

After

Width:  |  Height:  |  Size: 668 B

View File

@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256">
<rect width="256" height="256" fill="#080a10"/>
<path d="M128 31l53 82H75z" fill="#131a2e" stroke="#ff3df0" stroke-width="5"/>
<circle cx="128" cy="134" r="52" fill="#151b2b" stroke="#29ffe6" stroke-width="5"/>
<circle cx="108" cy="126" r="7" fill="#ffd84a"/>
<circle cx="148" cy="126" r="7" fill="#ffd84a"/>
<path d="M102 157h52" stroke="#ff4f78" stroke-width="6"/>
<text x="128" y="220" text-anchor="middle" fill="#ffd84a" font-family="monospace" font-size="16" font-weight="700">THE ODDS SAID LOL</text>
</svg>

After

Width:  |  Height:  |  Size: 591 B

View File

@@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256">
<rect width="256" height="256" fill="#0c1120"/>
<rect x="48" y="54" width="160" height="72" fill="#151b2b" stroke="#ff4f78" stroke-width="5"/>
<g>
<animateTransform attributeName="transform" type="translate" values="0 -22;0 28;0 -22" dur="1s" repeatCount="indefinite"/>
<rect x="76" y="126" width="104" height="42" fill="#6cff8c" stroke="#0a0d17" stroke-width="4"/>
<text x="128" y="153" text-anchor="middle" fill="#0a0d17" font-family="monospace" font-size="19" font-weight="700">FIAT</text>
</g>
<text x="128" y="101" text-anchor="middle" fill="#ffd84a" font-family="monospace" font-size="24" font-weight="700">BRRR</text>
<text x="128" y="222" text-anchor="middle" fill="#ff3df0" font-family="monospace" font-size="15" font-weight="700">INFINITE PATCH</text>
</svg>

After

Width:  |  Height:  |  Size: 856 B

View File

@@ -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;
}

View File

@@ -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<void> {
await nextTick();
if (listEl.value) listEl.value.scrollTop = listEl.value.scrollHeight;
@@ -169,7 +173,10 @@ async function scrollBottom(): Promise<void> {
<span>{{ timeLabel(message.createdAt) }}</span>
</div>
<p v-if="!stickerText(message.content)">{{ message.content }}</p>
<p v-else :class="['sticker', stickerKind(message.content)]">{{ stickerText(message.content) }}</p>
<div v-else :class="['sticker', stickerKind(message.content)]">
<img v-if="stickerImage(message.content)" :src="stickerImage(message.content) || ''" alt="" />
<span>{{ stickerText(message.content) }}</span>
</div>
</div>
</article>
</div>
@@ -191,6 +198,7 @@ async function scrollBottom(): Promise<void> {
:disabled="!canSend"
@click="sendSticker(sticker)"
>
<img v-if="sticker.image" :src="sticker.image" alt="" />
<span>{{ sticker.label }}</span>
<small>{{ sticker.text }}</small>
</button>
@@ -235,10 +243,13 @@ async function scrollBottom(): Promise<void> {
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;
}