Skip to content

Instantly share code, notes, and snippets.

@msciotti
Last active July 10, 2026 14:38
Show Gist options
  • Select an option

  • Save msciotti/3bfe07c9fbb1d99f3476bb7c2a51bdc5 to your computer and use it in GitHub Desktop.

Select an option

Save msciotti/3bfe07c9fbb1d99f3476bb7c2a51bdc5 to your computer and use it in GitHub Desktop.
guilds.join consent flow mockup
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>👮 guilds.join Consent Flow — Mockup</title>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
:root {
--bg-primary: #313338;
--bg-secondary: #2b2d31;
--bg-tertiary: #1e1f22;
--bg-hover: rgba(70,73,80,0.48);
--bg-selected: rgba(78,80,88,0.32);
--text-normal: #dbdee1;
--text-muted: #949ba4;
--text-link: #00a8fc;
--brand: #5865f2;
--green: #23a55a;
--green-hover: #1a9249;
--red: #f23f43;
--header: #f2f3f5;
--interactive: #b5bac1;
--divider: rgba(255,255,255,0.06);
--code-bg: rgba(255,255,255,0.07);
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: "gg sans", "Noto Sans", system-ui, -apple-system, sans-serif;
background: #111214;
color: var(--text-normal);
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
padding: 24px 16px 40px;
font-size: 16px;
}
/* ── Toggle ── */
.toggle-wrap {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
margin-bottom: 20px;
width: 100%;
}
.toggle-label {
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--text-muted);
}
.toggle-row {
display: flex;
background: var(--bg-tertiary);
border-radius: 8px;
padding: 4px;
gap: 4px;
border: 1px solid rgba(255,255,255,0.06);
}
.vtbtn {
padding: 8px 28px;
border-radius: 6px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
border: none;
background: transparent;
color: var(--text-muted);
transition: all 0.12s;
font-family: inherit;
}
.vtbtn.active { background: var(--brand); color: white; }
/* ── Meta strip ── */
.meta-strip {
width: 100%;
max-width: 680px;
background: rgba(88,101,242,0.08);
border: 1px solid rgba(88,101,242,0.18);
border-radius: 8px;
padding: 10px 16px;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
margin-bottom: 16px;
font-size: 13px;
}
.pb-pill {
background: rgba(88,101,242,0.2);
border: 1px solid rgba(88,101,242,0.4);
color: #8b9cf4;
border-radius: 20px;
padding: 2px 10px;
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
white-space: nowrap;
}
.meta-text { color: var(--text-normal); font-weight: 500; }
.meta-sub { color: var(--text-muted); }
.meta-right { margin-left: auto; color: var(--text-muted); font-size: 12px; white-space: nowrap; }
/* ── Window chrome ── */
.window {
width: 100%;
max-width: 680px;
background: var(--bg-primary);
border-radius: 10px;
overflow: hidden;
border: 1px solid rgba(255,255,255,0.06);
box-shadow: 0 8px 32px rgba(0,0,0,0.5);
}
.titlebar {
background: var(--bg-tertiary);
height: 36px;
display: flex;
align-items: center;
padding: 0 14px;
gap: 8px;
border-bottom: 1px solid rgba(0,0,0,0.3);
}
.tb-dot { width: 12px; height: 12px; border-radius: 50%; }
/* ── App layout ── */
.app { display: flex; height: 520px; }
/* Server rail */
.server-rail {
width: 60px;
background: var(--bg-tertiary);
display: flex; flex-direction: column; align-items: center;
padding: 10px 0; gap: 6px; flex-shrink: 0;
}
.s-icon {
width: 40px; height: 40px; border-radius: 50%;
display: flex; align-items: center; justify-content: center;
font-size: 16px; font-weight: 700; color: white;
cursor: pointer; transition: border-radius 0.15s; position: relative;
}
.s-icon:hover { border-radius: 30%; }
.s-icon.active { border-radius: 30%; }
.s-icon.active::before {
content: ''; position: absolute; left: -10px; top: 50%;
transform: translateY(-50%);
width: 4px; height: 28px;
background: white; border-radius: 0 3px 3px 0;
}
.s-divider { width: 28px; height: 2px; background: rgba(255,255,255,0.08); border-radius: 1px; }
/* DM sidebar */
.dm-sidebar {
width: 200px; background: var(--bg-secondary);
display: flex; flex-direction: column; flex-shrink: 0;
border-right: 1px solid rgba(0,0,0,0.2);
}
.dm-search {
margin: 8px; background: var(--bg-tertiary);
border-radius: 4px; padding: 5px 10px;
font-size: 13px; color: var(--text-muted);
}
.dm-list { padding: 6px; display: flex; flex-direction: column; gap: 1px; flex: 1; overflow: hidden; }
.dm-section { font-size: 11px; font-weight: 700; text-transform: uppercase; color: var(--text-muted); padding: 6px 6px 3px; letter-spacing: 0.06em; }
.dm-item {
display: flex; align-items: center; gap: 10px;
padding: 5px 6px; border-radius: 4px;
cursor: pointer; color: var(--interactive); font-size: 13px;
}
.dm-item:hover { background: var(--bg-hover); color: var(--text-normal); }
.dm-item.active { background: var(--bg-selected); color: var(--header); }
.dm-av {
width: 28px; height: 28px; border-radius: 50%; flex-shrink: 0;
display: flex; align-items: center; justify-content: center;
font-size: 13px; font-weight: 700; color: white;
}
.dm-info { flex: 1; overflow: hidden; }
.dm-name { font-size: 13px; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.dm-preview { font-size: 11px; color: var(--text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.badge {
background: var(--red); color: white;
border-radius: 10px; font-size: 10px; font-weight: 700;
padding: 1px 5px; min-width: 16px; text-align: center; flex-shrink: 0;
}
.user-bar {
background: var(--bg-tertiary); padding: 7px 8px;
display: flex; align-items: center; gap: 7px;
}
.user-av {
width: 28px; height: 28px; border-radius: 50%;
background: var(--brand); display: flex; align-items: center; justify-content: center;
font-size: 13px; font-weight: 700; color: white; flex-shrink: 0;
}
.user-name { font-size: 12px; font-weight: 600; color: var(--header); }
.user-status { font-size: 11px; color: var(--text-muted); }
.ub-icons { margin-left: auto; display: flex; gap: 1px; }
.ub-btn {
width: 24px; height: 24px; border-radius: 4px;
display: flex; align-items: center; justify-content: center;
font-size: 14px; color: var(--interactive); cursor: pointer;
}
/* Main */
.main { flex: 1; display: flex; flex-direction: column; overflow: hidden; min-width: 0; }
.chat-header {
height: 44px; padding: 0 14px;
display: flex; align-items: center; gap: 8px;
border-bottom: 1px solid var(--divider); flex-shrink: 0;
}
.chat-header-name { font-size: 15px; font-weight: 600; color: var(--header); }
.chat-header-sub { font-size: 12px; color: var(--text-muted); }
/* View panels */
.view-panel { display: none; flex-direction: column; flex: 1; overflow: hidden; }
.view-panel.visible { display: flex; }
.messages { flex: 1; overflow-y: auto; padding: 12px 0 6px; }
/* Annotation callout */
.annotation {
background: rgba(88,101,242,0.07);
border-left: 3px solid var(--brand);
margin: 0 14px 14px;
padding: 9px 12px;
border-radius: 0 4px 4px 0;
font-size: 12px; color: var(--text-muted); line-height: 1.5;
}
.annotation strong { color: var(--text-normal); }
code {
background: var(--code-bg); padding: 1px 4px; border-radius: 3px;
font-size: 11px; font-family: "Consolas", monospace;
}
.time-div {
display: flex; align-items: center; gap: 8px;
padding: 6px 14px; margin: 2px 0;
}
.time-div::before, .time-div::after { content: ''; flex: 1; height: 1px; background: var(--divider); }
.time-div span { font-size: 11px; color: var(--text-muted); font-weight: 600; white-space: nowrap; }
/* Message row */
.msg { display: flex; gap: 12px; padding: 2px 14px; margin-top: 14px; }
.msg-av {
width: 40px; height: 40px; border-radius: 50%; flex-shrink: 0;
display: flex; align-items: center; justify-content: center;
font-size: 22px; margin-top: 1px;
}
.msg-av.discord-official {
background: var(--brand);
}
.msg-av.app-bot {
background: #23a55a;
}
.msg-body { flex: 1; min-width: 0; }
.msg-head { display: flex; align-items: center; gap: 6px; margin-bottom: 4px; flex-wrap: wrap; }
.msg-name { font-size: 15px; font-weight: 600; }
/* Official badge — matches real Discord "✓ OFFICIAL" */
.official-badge {
display: inline-flex; align-items: center; gap: 3px;
background: var(--brand); color: white;
border-radius: 3px; padding: 1px 5px;
font-size: 10px; font-weight: 700;
text-transform: uppercase; letter-spacing: 0.04em;
}
/* App bot badge */
.bot-badge {
background: var(--brand); color: white;
border-radius: 3px; padding: 1px 5px;
font-size: 10px; font-weight: 700;
text-transform: uppercase; letter-spacing: 0.04em;
}
.msg-ts { font-size: 12px; color: var(--text-muted); }
.msg-text { font-size: 14px; line-height: 1.55; color: var(--text-normal); }
.msg-text p + p { margin-top: 8px; }
.msg-text a { color: var(--text-link); cursor: pointer; text-decoration: none; }
.msg-text a:hover { text-decoration: underline; }
/* "This chat is reserved" system banner */
.reserved-banner {
margin: 12px 14px 0 66px; /* aligned with message text, not avatar */
background: var(--bg-secondary);
border: 1px solid rgba(255,255,255,0.06);
border-radius: 6px;
padding: 10px 14px;
display: flex; align-items: center; gap: 12px;
}
.rb-icon {
width: 36px; height: 36px; border-radius: 50%;
background: var(--bg-tertiary);
display: flex; align-items: center; justify-content: center;
font-size: 18px; flex-shrink: 0;
border: 2px solid rgba(255,255,255,0.06);
}
.rb-text { flex: 1; }
.rb-title { font-size: 13px; font-weight: 600; color: var(--header); margin-bottom: 2px; }
.rb-sub { font-size: 12px; color: var(--text-muted); }
.rb-btn {
background: var(--bg-tertiary); border: 1px solid rgba(255,255,255,0.1);
color: var(--interactive); border-radius: 3px;
padding: 6px 14px; font-size: 13px; font-weight: 500;
cursor: pointer; white-space: nowrap; font-family: inherit;
flex-shrink: 0;
}
.rb-btn:hover { background: var(--bg-hover); color: var(--text-normal); }
/* Invite card */
.invite-card {
background: var(--bg-tertiary);
border-radius: 8px; padding: 14px;
margin-top: 8px; max-width: 380px;
border: 1px solid rgba(255,255,255,0.06);
}
.ic-header {
font-size: 10px; font-weight: 700; text-transform: uppercase;
color: var(--text-muted); letter-spacing: 0.05em; margin-bottom: 10px;
}
.ic-server { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; }
.ic-icon {
width: 44px; height: 44px; border-radius: 14px;
display: flex; align-items: center; justify-content: center;
font-size: 20px; flex-shrink: 0;
}
.ic-name { font-size: 15px; font-weight: 700; color: var(--header); margin-bottom: 3px; }
.ic-meta { font-size: 12px; color: var(--text-muted); display: flex; align-items: center; gap: 10px; }
.dot { display: inline-block; width: 7px; height: 7px; border-radius: 50%; margin-right: 3px; }
.dot.online { background: #23a55a; }
.dot.offline { background: #6d6f78; }
.join-btn {
width: 100%; padding: 9px; border-radius: 3px;
background: var(--green); color: white; border: none;
font-size: 14px; font-weight: 600; cursor: pointer;
transition: background 0.1s; font-family: inherit;
}
.join-btn:hover { background: var(--green-hover); }
.join-btn:active { transform: translateY(1px); }
.chat-input {
margin: 0 14px 14px;
background: rgba(255,255,255,0.06);
border-radius: 8px; padding: 9px 14px;
font-size: 14px; color: var(--text-muted); flex-shrink: 0;
}
::-webkit-scrollbar { width: 4px; }
::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 2px; }
@media (max-width: 520px) {
.dm-sidebar { display: none; }
.reserved-banner { margin-left: 14px; }
}
</style>
</head>
<body>
<!-- Toggle -->
<div class="toggle-wrap">
<div class="toggle-label">View</div>
<div class="toggle-row">
<button class="vtbtn active" onclick="switchView('before', this)">⚡ Current behavior</button>
<button class="vtbtn" onclick="switchView('after', this)">✅ Proposed flow</button>
</div>
</div>
<!-- Meta strip -->
<div class="meta-strip">
<span class="pb-pill">Exploring</span>
<span class="meta-text">guilds.join consent flow</span>
<span class="meta-sub">·</span>
<span class="meta-sub">Silent auto-join → explicit invite from app</span>
<span class="meta-right">Apps Platform · UDP</span>
</div>
<!-- Window -->
<div class="window">
<div class="titlebar">
<div class="tb-dot" style="background:#ff5f57"></div>
<div class="tb-dot" style="background:#febc2e"></div>
<div class="tb-dot" style="background:#28c840"></div>
</div>
<div class="app">
<!-- Server rail -->
<nav class="server-rail">
<div class="s-icon active" style="background:var(--bg-secondary);font-size:18px;">💬</div>
<div class="s-divider"></div>
<div class="s-icon" style="background:#23a55a;font-size:11px;">DK</div>
<div class="s-icon" style="background:#e67e22;font-size:11px;">PT</div>
<div class="s-icon" style="background:#5865f2;font-size:11px;">GV</div>
<div class="s-icon" style="background:rgba(255,255,255,0.06);color:#23a55a;font-size:20px;border:2px dashed rgba(255,255,255,0.12);">+</div>
</nav>
<!-- DM sidebar — changes sender between views -->
<aside class="dm-sidebar">
<div class="dm-search">Find or start a conversation</div>
<div class="dm-list">
<div class="dm-section">Direct Messages</div>
<!-- CURRENT: active = Discord -->
<div class="dm-item active" id="sidebar-before">
<div class="dm-av" style="background:#5865f2;font-size:18px;">🤖</div>
<div class="dm-info">
<div class="dm-name">Discord</div>
<div class="dm-preview">An application, DraftKings…</div>
</div>
<div class="badge">1</div>
</div>
<!-- PROPOSED: active = DraftKings app -->
<div class="dm-item" id="sidebar-after">
<div class="dm-av" style="background:#23a55a;font-size:11px;">DK</div>
<div class="dm-info">
<div class="dm-name">DraftKings</div>
<div class="dm-preview">Join DraftKings Community…</div>
</div>
<div class="badge">1</div>
</div>
<div class="dm-item">
<div class="dm-av" style="background:#e67e22;font-size:11px;">PT</div>
<div class="dm-info">
<div class="dm-name">Patreon</div>
<div class="dm-preview">Thanks for subscribing…</div>
</div>
</div>
<div class="dm-item">
<div class="dm-av" style="background:#444;font-size:13px;">👤</div>
<div class="dm-info">
<div class="dm-name">mason</div>
<div class="dm-preview">sounds good!</div>
</div>
</div>
</div>
<div class="user-bar">
<div class="user-av">M</div>
<div>
<div class="user-name">mason</div>
<div class="user-status">● Online</div>
</div>
<div class="ub-icons">
<div class="ub-btn">🎙</div>
<div class="ub-btn">⚙️</div>
</div>
</div>
</aside>
<!-- Main chat -->
<main class="main">
<!-- CURRENT — DM is from Discord -->
<div class="view-panel visible" id="view-before">
<div class="chat-header">
<div style="width:24px;height:24px;border-radius:50%;background:var(--brand);display:flex;align-items:center;justify-content:center;font-size:14px;">🤖</div>
<div class="chat-header-name">Discord</div>
<div class="chat-header-sub">official system messages</div>
</div>
<div class="messages">
<div class="annotation">
<strong>Current:</strong> App calls <code>guilds.join</code> → user is <strong>silently added immediately</strong>. Discord sends an after-the-fact notification from its own official account. No consent, no action required from the user.
</div>
<div class="time-div"><span>06/18/2024</span></div>
<div class="msg">
<div class="msg-av discord-official">🤖</div>
<div class="msg-body">
<div class="msg-head">
<span class="msg-name" style="color:#fff;">Discord</span>
<span class="official-badge">✓ Official</span>
<span class="msg-ts">06/18/2024 3:38 PM</span>
</div>
<div class="msg-text">
<p>Hello,</p>
<p>An application, <strong>DraftKings</strong>, has added you to the server <strong>DraftKings Community</strong>.</p>
<p>If you don't recognize this application or wish to stop it from adding you to servers, you can revoke its authorization <a>here</a>.</p>
</div>
</div>
</div>
<!-- Reserved chat banner -->
<div class="reserved-banner">
<div class="rb-icon">🔒</div>
<div class="rb-text">
<div class="rb-title">This chat is reserved for official Discord notifications.</div>
<div class="rb-sub">Discord will never ask you for your password or account token.</div>
</div>
<button class="rb-btn">Learn More</button>
</div>
</div>
<div class="chat-input">Message @Discord</div>
</div>
<!-- PROPOSED — DM is from the app (DraftKings) -->
<div class="view-panel" id="view-after">
<div class="chat-header">
<div style="width:24px;height:24px;border-radius:50%;background:#23a55a;display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:700;color:white;">DK</div>
<div class="chat-header-name">DraftKings</div>
<div class="chat-header-sub">app · bot</div>
</div>
<div class="messages">
<div class="annotation">
<strong>Proposed:</strong> App calls <code>guilds.join</code> → Discord sends a DM <strong>from the app's bot account</strong> with a server invite. User must click <strong>Join Server</strong> to be added. Ignoring = never joined. DM copy is fixed boilerplate — the app cannot customize it.
</div>
<div class="time-div"><span>Today</span></div>
<div class="msg">
<div class="msg-av app-bot" style="font-size:12px;font-weight:700;">DK</div>
<div class="msg-body">
<div class="msg-head">
<span class="msg-name" style="color:#fff;">DraftKings</span>
<span class="bot-badge">App</span>
<span class="msg-ts">Today at 10:23 AM</span>
</div>
<div class="msg-text">
<p><strong>DraftKings</strong> wants to add you to one of their Discord servers. Use the invite below to join, or ignore this message to decline.</p>
</div>
<div class="invite-card">
<div class="ic-header">You've been invited to join a server</div>
<div class="ic-server">
<div class="ic-icon" style="background:linear-gradient(135deg,#1a6e3c,#23a55a);">🏈</div>
<div>
<div class="ic-name">DraftKings Community</div>
<div class="ic-meta">
<span><span class="dot online"></span>1,247 Online</span>
<span><span class="dot offline"></span>45,823 Members</span>
</div>
</div>
</div>
<button class="join-btn" onclick="this.textContent='✓ Joined'; this.style.background='#1a9249'; this.disabled=true;">Join Server</button>
</div>
</div>
</div>
</div>
<div class="chat-input">Message @DraftKings</div>
</div>
</main>
</div>
</div>
<script>
function switchView(view, btn) {
document.querySelectorAll('.vtbtn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
document.querySelectorAll('.view-panel').forEach(p => p.classList.remove('visible'));
document.getElementById('view-' + view).classList.add('visible');
// Swap active DM in sidebar
document.getElementById('sidebar-before').classList.toggle('active', view === 'before');
document.getElementById('sidebar-after').classList.toggle('active', view === 'after');
}
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment