Last active
July 10, 2026 14:38
-
-
Save msciotti/3bfe07c9fbb1d99f3476bb7c2a51bdc5 to your computer and use it in GitHub Desktop.
guilds.join consent flow mockup
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!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