Open for work
code/ nulled-room /index.html
1247 lines · 37.2 KB · xml Raw
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>NulledRoom</title>
<link href="https://fonts.googleapis.com/css2?family=Share+Tech+Mono&family=VT323&display=swap" rel="stylesheet">
<style>
  :root {
    --green: #00ff41;
    --green-dim: #00aa2a;
    --green-dark: #003a0e;
    --green-glow: rgba(0, 255, 65, 0.15);
    --bg: #0a0a0a;
    --bg2: #0d0d0d;
    --border: #1a3a1a;
    --red: #ff3131;
    --grey: #444;
  }

  * { margin: 0; padding: 0; box-sizing: border-box; -webkit-tap-highlight-color: transparent; }

  html, body {
    background: var(--bg);
    color: var(--green);
    font-family: 'Share Tech Mono', monospace;
    font-size: 14px;
    height: 100%;
    width: 100%;
    overflow: hidden;
    position: fixed;
  }

  body::before {
    content: '';
    position: fixed;
    top: 0; left: 0; right: 0; bottom: 0;
    background: repeating-linear-gradient(
      0deg, transparent, transparent 2px,
      rgba(0,0,0,0.07) 2px, rgba(0,0,0,0.07) 4px
    );
    pointer-events: none;
    z-index: 9999;
  }

  @keyframes flicker {
    0%, 100% { opacity: 1; }
    92% { opacity: 1; } 93% { opacity: 0.97; }
    94% { opacity: 1; } 96% { opacity: 0.95; } 97% { opacity: 1; }
  }
  body { animation: flicker 8s infinite; }

  .screen { display: none; height: 100%; flex-direction: column; }
  .screen.active { display: flex; }

  /* ===== BOOT ===== */
  #screen-boot {
    justify-content: center;
    align-items: center;
    padding: 24px 20px;
    overflow-y: auto;
  }

  .boot-logo {
    font-family: 'VT323', monospace;
    font-size: clamp(42px, 10vw, 96px);
    color: var(--green);
    text-shadow: 0 0 20px var(--green), 0 0 40px var(--green-dim);
    letter-spacing: 6px;
    text-align: center;
    margin-bottom: 6px;
    line-height: 1;
  }

  .boot-sub {
    color: var(--green-dim);
    font-size: clamp(9px, 2vw, 11px);
    letter-spacing: 3px;
    text-align: center;
    margin-bottom: 36px;
  }

  .boot-menu {
    display: flex;
    flex-direction: column;
    gap: 10px;
    width: 100%;
    max-width: 400px;
  }

  .boot-btn {
    background: transparent;
    border: 1px solid var(--green-dim);
    color: var(--green);
    font-family: 'Share Tech Mono', monospace;
    font-size: clamp(13px, 3.5vw, 15px);
    padding: 16px 20px;
    cursor: pointer;
    text-align: left;
    letter-spacing: 2px;
    transition: all 0.1s;
    width: 100%;
  }
  .boot-btn::before { content: '> '; color: var(--green-dim); }
  .boot-btn:hover, .boot-btn:active {
    background: var(--green-glow);
    border-color: var(--green);
    box-shadow: 0 0 12px var(--green-glow);
  }

  .boot-divider { border: none; border-top: 1px solid var(--border); margin: 4px 0; }

  /* Room options shown before create */
  .room-options {
    display: none;
    flex-direction: column;
    gap: 8px;
    margin-top: 4px;
    padding: 12px;
    border: 1px solid var(--border);
    background: rgba(0,255,65,0.03);
  }
  .room-options.visible { display: flex; }

  .option-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
  }

  .option-label {
    font-size: 11px;
    color: var(--green-dim);
    letter-spacing: 1px;
    flex: 1;
  }

  .option-label span {
    display: block;
    font-size: 10px;
    color: var(--grey);
    margin-top: 2px;
  }

  .term-select {
    background: transparent;
    border: 1px solid var(--green-dim);
    color: var(--green);
    font-family: 'Share Tech Mono', monospace;
    font-size: 12px;
    padding: 6px 8px;
    outline: none;
    cursor: pointer;
    min-width: 110px;
  }
  .term-select:focus { border-color: var(--green); }
  .term-select option { background: #0d0d0d; color: var(--green); }

  .toggle-wrap {
    display: flex;
    align-items: center;
    gap: 8px;
  }

  .toggle {
    appearance: none;
    width: 36px;
    height: 20px;
    background: var(--border);
    border: 1px solid var(--green-dim);
    border-radius: 0;
    cursor: pointer;
    position: relative;
    transition: background 0.2s;
    flex-shrink: 0;
  }
  .toggle::after {
    content: '';
    position: absolute;
    top: 2px;
    left: 2px;
    width: 14px;
    height: 14px;
    background: var(--green-dim);
    transition: all 0.2s;
  }
  .toggle:checked {
    background: var(--green-glow);
    border-color: var(--green);
  }
  .toggle:checked::after {
    left: 18px;
    background: var(--green);
  }

  .toggle-label {
    font-size: 11px;
    color: var(--green-dim);
    letter-spacing: 1px;
  }
  input[type=checkbox]:checked + .toggle-label { color: var(--green); }

  .create-confirm-btn {
    background: var(--green-glow);
    border: 1px solid var(--green);
    color: var(--green);
    font-family: 'Share Tech Mono', monospace;
    font-size: 13px;
    padding: 12px;
    cursor: pointer;
    letter-spacing: 2px;
    width: 100%;
    margin-top: 4px;
  }
  .create-confirm-btn:active { background: rgba(0,255,65,0.25); }

  .join-form { display: none; flex-direction: column; gap: 8px; margin-top: 4px; }
  .join-form.visible { display: flex; }

  .term-input {
    background: transparent;
    border: 1px solid var(--green-dim);
    color: var(--green);
    font-family: 'Share Tech Mono', monospace;
    font-size: clamp(15px, 4vw, 17px);
    padding: 14px;
    letter-spacing: 4px;
    outline: none;
    text-transform: uppercase;
    width: 100%;
  }
  .term-input::placeholder { color: var(--green-dark); text-transform: none; letter-spacing: 1px; font-size: 13px; }
  .term-input:focus { border-color: var(--green); box-shadow: 0 0 8px var(--green-glow); }

  .term-btn {
    background: transparent;
    border: 1px solid var(--green);
    color: var(--green);
    font-family: 'Share Tech Mono', monospace;
    font-size: 14px;
    padding: 14px;
    cursor: pointer;
    letter-spacing: 2px;
    width: 100%;
  }
  .term-btn:active { background: var(--green-glow); }

  .boot-status {
    margin-top: 16px;
    font-size: 11px;
    color: var(--green-dim);
    letter-spacing: 1px;
    min-height: 16px;
    text-align: center;
  }
  .boot-status.error { color: var(--red); }

  /* ===== ROOM ID BANNER ===== */
  #room-id-banner {
    display: none;
    margin-top: 20px;
    width: 100%;
    max-width: 400px;
    border: 1px solid var(--green);
    background: var(--green-glow);
    padding: 20px 16px 14px;
    text-align: center;
    box-shadow: 0 0 24px var(--green-glow);
  }
  #room-id-banner.visible { display: block; }

  .banner-label { font-size: 10px; color: var(--green-dim); letter-spacing: 4px; margin-bottom: 10px; }

  .banner-id {
    font-family: 'VT323', monospace;
    font-size: clamp(52px, 14vw, 72px);
    color: var(--green);
    text-shadow: 0 0 24px var(--green), 0 0 48px var(--green-dim);
    letter-spacing: 10px;
    line-height: 1;
  }

  .banner-hint { font-size: 10px; color: var(--green-dim); letter-spacing: 2px; margin-top: 10px; line-height: 1.6; }

  .banner-settings {
    margin-top: 10px;
    font-size: 10px;
    color: var(--grey);
    letter-spacing: 1px;
    border-top: 1px solid var(--border);
    padding-top: 8px;
  }
  .banner-settings span { color: var(--green-dim); }

  .banner-copy-btn {
    margin-top: 12px;
    background: transparent;
    border: 1px solid var(--green-dim);
    color: var(--green-dim);
    font-family: 'Share Tech Mono', monospace;
    font-size: 12px;
    padding: 10px 16px;
    cursor: pointer;
    letter-spacing: 2px;
    width: 100%;
  }
  .banner-copy-btn:active { background: var(--green-glow); color: var(--green); border-color: var(--green); }

  .banner-enter-btn {
    margin-top: 8px;
    background: rgba(0,255,65,0.12);
    border: 1px solid var(--green);
    color: var(--green);
    font-family: 'Share Tech Mono', monospace;
    font-size: 14px;
    padding: 14px 16px;
    cursor: pointer;
    letter-spacing: 2px;
    width: 100%;
  }
  .banner-enter-btn:active { background: rgba(0,255,65,0.25); }

  /* ===== CHAT SCREEN ===== */
  #screen-chat { height: 100%; overflow: hidden; }

  .chat-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 8px 12px;
    border-bottom: 1px solid var(--border);
    background: var(--bg2);
    flex-shrink: 0;
    min-height: 52px;
    gap: 8px;
  }

  .chat-header-left { display: flex; align-items: center; gap: 10px; min-width: 0; flex: 1; }

  .header-logo {
    font-family: 'VT323', monospace;
    font-size: 26px;
    color: var(--green);
    text-shadow: 0 0 10px var(--green);
    letter-spacing: 2px;
    flex-shrink: 0;
  }

  .room-info { display: flex; flex-direction: column; gap: 1px; min-width: 0; }

  .room-id-display {
    font-size: clamp(11px, 3vw, 13px);
    color: var(--green-dim);
    letter-spacing: 1px;
    white-space: nowrap;
  }
  .room-id-display span { color: var(--green); font-size: clamp(13px, 4vw, 16px); letter-spacing: 3px; }

  .status-line { font-size: 10px; color: var(--green-dim); letter-spacing: 1px; }

  .status-dot {
    display: inline-block;
    width: 6px; height: 6px;
    border-radius: 50%;
    background: var(--green);
    box-shadow: 0 0 5px var(--green);
    margin-right: 4px;
    animation: pulse 2s infinite;
  }
  @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } }

  .chat-header-right { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }

  .online-count { font-size: 10px; color: var(--green-dim); border: 1px solid var(--border); padding: 3px 7px; white-space: nowrap; }
  .online-count span { color: var(--green); }

  .my-name-display { font-size: 10px; color: var(--green-dim); display: none; }
  .my-name-display span { color: var(--green); }
  @media (min-width: 480px) { .my-name-display { display: block; } }

  .nuke-btn {
    background: transparent;
    border: 1px solid var(--red);
    color: var(--red);
    font-family: 'Share Tech Mono', monospace;
    font-size: 11px;
    padding: 7px 10px;
    cursor: pointer;
    letter-spacing: 1px;
    display: none;
    white-space: nowrap;
  }
  .nuke-btn:active { background: rgba(255,49,49,0.12); }
  .nuke-btn.visible { display: block; }

  .chat-body { display: flex; flex: 1; overflow: hidden; min-height: 0; }

  .messages {
    flex: 1;
    overflow-y: auto;
    padding: 12px;
    display: flex;
    flex-direction: column;
    gap: 4px;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: thin;
    scrollbar-color: var(--green-dim) transparent;
  }
  .messages::-webkit-scrollbar { width: 3px; }
  .messages::-webkit-scrollbar-thumb { background: var(--green-dim); }

  .msg { display: flex; gap: 8px; line-height: 1.5; word-break: break-word; align-items: baseline; }
  .msg-time { color: var(--grey); font-size: 10px; flex-shrink: 0; }
  .msg-from { color: var(--green-dim); flex-shrink: 0; font-size: 12px; }
  .msg-from.self { color: var(--green); }
  .msg-text { color: #ccc; font-size: 13px; }
  .msg.system .msg-text { color: var(--green-dim); font-style: italic; font-size: 11px; }
  .msg.system .msg-from { display: none; }

  /* Image messages */
  .msg-image-wrap { display: flex; flex-direction: column; gap: 4px; }
  .msg-image {
    max-width: min(280px, 70vw);
    max-height: 200px;
    border: 1px solid var(--border);
    cursor: pointer;
    display: block;
    object-fit: cover;
    transition: border-color 0.1s;
  }
  .msg-image:hover { border-color: var(--green); }
  .msg-image-size { font-size: 10px; color: var(--grey); }

  .users-panel {
    width: 150px;
    border-left: 1px solid var(--border);
    padding: 10px;
    background: var(--bg2);
    overflow-y: auto;
    flex-shrink: 0;
    display: none;
  }
  @media (min-width: 580px) { .users-panel { display: block; } }

  .users-title { font-size: 9px; color: var(--green-dim); letter-spacing: 3px; margin-bottom: 8px; border-bottom: 1px solid var(--border); padding-bottom: 6px; }

  .user-entry { font-size: 11px; color: var(--green-dim); padding: 3px 0; word-break: break-all; }
  .user-entry::before { content: '· '; }
  .user-entry.self { color: var(--green); }
  .user-entry.self::before { content: '» '; }

  /* Input area */
  .chat-input-area {
    border-top: 1px solid var(--border);
    padding: 10px 12px;
    display: flex;
    align-items: center;
    gap: 8px;
    background: var(--bg2);
    flex-shrink: 0;
  }

  .input-prompt { color: var(--green); font-size: 14px; flex-shrink: 0; }

  #msg-input {
    flex: 1;
    background: transparent;
    border: none;
    color: #ddd;
    font-family: 'Share Tech Mono', monospace;
    font-size: 16px;
    outline: none;
    caret-color: var(--green);
    min-width: 0;
  }
  #msg-input::placeholder { color: var(--grey); font-size: 13px; }

  .img-btn {
    background: transparent;
    border: 1px solid var(--border);
    color: var(--grey);
    font-size: 16px;
    padding: 7px 10px;
    cursor: pointer;
    flex-shrink: 0;
    line-height: 1;
    transition: all 0.1s;
  }
  .img-btn:hover, .img-btn:active { border-color: var(--green-dim); color: var(--green-dim); }

  .send-btn {
    background: transparent;
    border: 1px solid var(--green-dim);
    color: var(--green-dim);
    font-family: 'Share Tech Mono', monospace;
    font-size: 12px;
    padding: 9px 14px;
    cursor: pointer;
    letter-spacing: 1px;
    flex-shrink: 0;
    white-space: nowrap;
  }
  .send-btn:active { border-color: var(--green); color: var(--green); background: var(--green-glow); }

  /* Hidden file input */
  #img-file-input { display: none; }

  /* Image lightbox */
  #lightbox {
    display: none;
    position: fixed;
    inset: 0;
    background: rgba(0,0,0,0.95);
    z-index: 500;
    justify-content: center;
    align-items: center;
    padding: 20px;
  }
  #lightbox.visible { display: flex; }
  #lightbox img {
    max-width: 100%;
    max-height: 100%;
    object-fit: contain;
    border: 1px solid var(--border);
  }
  #lightbox-close {
    position: absolute;
    top: 16px; right: 16px;
    background: transparent;
    border: 1px solid var(--grey);
    color: var(--grey);
    font-family: 'Share Tech Mono', monospace;
    font-size: 12px;
    padding: 8px 14px;
    cursor: pointer;
    letter-spacing: 2px;
  }
  #lightbox-close:active { color: var(--green); border-color: var(--green); }

  /* Nuked overlay */
  #nuked-overlay {
    display: none;
    position: fixed;
    inset: 0;
    background: rgba(0,0,0,0.96);
    z-index: 1000;
    justify-content: center;
    align-items: center;
    flex-direction: column;
    gap: 16px;
    padding: 24px;
    text-align: center;
  }
  #nuked-overlay.visible { display: flex; }

  .nuked-title {
    font-family: 'VT323', monospace;
    font-size: clamp(56px, 15vw, 96px);
    color: var(--red);
    text-shadow: 0 0 30px var(--red);
    letter-spacing: 8px;
    animation: glitch 0.3s infinite;
  }
  @keyframes glitch {
    0%, 100% { transform: translate(0); }
    25% { transform: translate(-2px, 1px); }
    75% { transform: translate(2px, -1px); }
  }
  .nuked-sub { color: var(--grey); font-size: 11px; letter-spacing: 3px; }
  .nuked-btn {
    margin-top: 16px;
    background: transparent;
    border: 1px solid var(--grey);
    color: var(--grey);
    font-family: 'Share Tech Mono', monospace;
    font-size: 13px;
    padding: 14px 24px;
    cursor: pointer;
    letter-spacing: 2px;
    width: 100%;
    max-width: 300px;
  }
  .nuked-btn:active { border-color: var(--green); color: var(--green); }

  @keyframes blink { 50% { opacity: 0; } }
  .blink { animation: blink 1s step-end infinite; }
</style>
</head>
<body>

<!-- BOOT SCREEN -->
<div id="screen-boot" class="screen active">
  <div class="boot-logo">NULLEDROOM</div>
  <div class="boot-sub">// FULLY ENCRYPTED · ZERO LOGS · NO MESSAGE HISTORY · NO ACCOUNTS · NOTHING IS STORED //</div>

  <div class="boot-menu">
    <!-- CREATE -->
    <button class="boot-btn" onclick="toggleCreateOptions()">CREATE ROOM</button>
    <div class="room-options" id="room-options">
      <div class="option-row">
        <div class="option-label">
          INACTIVITY TIMEOUT
          <span>kick idle users after</span>
        </div>
        <select class="term-select" id="inactivity-select">
          <option value="5">5 minutes</option>
          <option value="15">15 minutes</option>
          <option value="30">30 minutes</option>
          <option value="60">1 hour</option>
          <option value="180">3 hours</option>
          <option value="360">6 hours</option>
          <option value="720">12 hours</option>
          <option value="1440">24 hours</option>
        </select>
      </div>
      <div class="option-row">
        <div class="option-label">
          NUKE ON HOST LEAVE
          <span>destroy room if you disconnect</span>
        </div>
        <div class="toggle-wrap">
          <input type="checkbox" class="toggle" id="nuke-on-leave-toggle">
          <label class="toggle-label" for="nuke-on-leave-toggle">OFF</label>
        </div>
      </div>
      <button class="create-confirm-btn" onclick="createRoom()">INITIALIZE ROOM →</button>
    </div>

    <hr class="boot-divider">

    <!-- JOIN -->
    <button class="boot-btn" onclick="toggleJoinForm()">JOIN ROOM</button>
    <div class="join-form" id="join-form">
      <input class="term-input" id="join-id-input" type="text" maxlength="10"
        placeholder="enter room ID..." autocomplete="off" spellcheck="false"
        autocorrect="off" autocapitalize="characters">
      <button class="term-btn" onclick="joinRoom()">CONNECT →</button>
    </div>
  </div>

  <!-- Room ID banner -->
  <div id="room-id-banner">
    <div class="banner-label">// YOUR ROOM ID //</div>
    <div class="banner-id" id="banner-id-text">——</div>
    <div class="banner-hint">SHARE THIS ID WITH WHOEVER YOU WANT IN THE ROOM</div>
    <div class="banner-settings" id="banner-settings"></div>
    <button class="banner-copy-btn" onclick="copyRoomId()">[ COPY ID ]</button>
    <button class="banner-enter-btn" onclick="enterRoom()">ENTER ROOM →</button>
  </div>

  <div class="boot-status" id="boot-status"><span class="blink">_</span></div>
</div>

<!-- CHAT SCREEN -->
<div id="screen-chat" class="screen">
  <div class="chat-header">
    <div class="chat-header-left">
      <div class="header-logo">NR</div>
      <div class="room-info">
        <div class="room-id-display">ROOM: <span id="display-room-id">—</span></div>
        <div class="status-line"><span class="status-dot"></span>E2EE · LIVE</div>
      </div>
    </div>
    <div class="chat-header-right">
      <div class="online-count">ONLINE: <span id="online-count">0</span></div>
      <div class="my-name-display">YOU: <span id="display-my-name">—</span></div>
      <button class="nuke-btn" id="nuke-btn" onclick="confirmNuke()">⚠ NUKE</button>
    </div>
  </div>

  <div class="chat-body">
    <div class="messages" id="messages"></div>
    <div class="users-panel">
      <div class="users-title">ONLINE</div>
      <div id="user-list"></div>
    </div>
  </div>

  <div class="chat-input-area">
    <span class="input-prompt">&gt;&gt;</span>
    <input type="text" id="msg-input" placeholder="type message..."
      autocomplete="off" spellcheck="false" maxlength="2000"
      autocorrect="off" autocapitalize="none">
    <button class="img-btn" onclick="triggerImagePick()" title="Send image">🖼</button>
    <input type="file" id="img-file-input" accept="image/*" onchange="handleImagePicked(event)">
    <button class="send-btn" onclick="sendMessage()">SEND</button>
  </div>
</div>

<!-- LIGHTBOX -->
<div id="lightbox" onclick="closeLightbox()">
  <button id="lightbox-close" onclick="closeLightbox()">[ CLOSE ]</button>
  <img id="lightbox-img" src="" alt="">
</div>

<!-- NUKED OVERLAY -->
<div id="nuked-overlay">
  <div class="nuked-title">NUKED</div>
  <div class="nuked-sub">// ROOM DESTROYED · ALL DATA PURGED //</div>
  <button class="nuked-btn" onclick="goHome()">RETURN TO TERMINAL</button>
</div>

<script>
const WS_URL = (() => {
  const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
  return `${proto}//${location.host}/ws`;
})();

const MAX_IMAGE_SIZE = 20 * 1024 * 1024; // 20MB raw limit
const COMPRESSED_TARGET = 1400 * 1024;   // target ~1.4MB after compression

let ws = null;
let myName = null;
let myClientId = null;
let isCreator = false;
let currentRoomId = null;
let cryptoKey = null;
let pingInterval = null;
let pendingRoomId = null;
let pendingSettings = {};

// Toggle nuke-on-leave label
document.getElementById('nuke-on-leave-toggle').addEventListener('change', function() {
  document.querySelector('.toggle-label').textContent = this.checked ? 'ON' : 'OFF';
});

// ===== CRYPTO =====
async function deriveKey(roomId) {
  const enc = new TextEncoder();
  const km = await crypto.subtle.importKey(
    'raw', enc.encode(roomId.toUpperCase()),
    { name: 'PBKDF2' }, false, ['deriveKey']
  );
  return crypto.subtle.deriveKey(
    { name: 'PBKDF2', salt: enc.encode('nulledroom-salt-v1'), iterations: 100000, hash: 'SHA-256' },
    km, { name: 'AES-GCM', length: 256 }, false, ['encrypt', 'decrypt']
  );
}

// Safe base64 — chunked to avoid call stack overflow on large images
function uint8ToBase64(bytes) {
  let binary = '';
  const chunkSize = 8192;
  for (let i = 0; i < bytes.length; i += chunkSize) {
    binary += String.fromCharCode(...bytes.subarray(i, i + chunkSize));
  }
  return btoa(binary);
}

async function encryptData(data) {
  const iv = crypto.getRandomValues(new Uint8Array(12));
  const enc = typeof data === 'string' ? new TextEncoder().encode(data) : data;
  const ct = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, cryptoKey, enc);
  const combined = new Uint8Array(iv.length + ct.byteLength);
  combined.set(iv, 0);
  combined.set(new Uint8Array(ct), iv.length);
  return uint8ToBase64(combined);
}

async function decryptData(b64, asText = true) {
  const combined = Uint8Array.from(atob(b64), c => c.charCodeAt(0));
  const iv = combined.slice(0, 12);
  const ct = combined.slice(12);
  try {
    const pt = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, cryptoKey, ct);
    return asText ? new TextDecoder().decode(pt) : new Uint8Array(pt);
  } catch { return asText ? '[decryption failed]' : null; }
}

function generateRoomId() {
  const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
  let id = '';
  for (let i = 0; i < 6; i++) id += chars[Math.floor(Math.random() * chars.length)];
  return id;
}

// ===== IMAGE COMPRESSION =====
function compressImage(file) {
  return new Promise((resolve, reject) => {
    if (file.size > MAX_IMAGE_SIZE) {
      reject(new Error(`Image too large (max 20MB)`));
      return;
    }
    const reader = new FileReader();
    reader.onload = (e) => {
      const img = new Image();
      img.onload = () => {
        const canvas = document.createElement('canvas');
        let { width, height } = img;

        // Scale down if needed
        const maxDim = 1920;
        if (width > maxDim || height > maxDim) {
          const ratio = Math.min(maxDim / width, maxDim / height);
          width = Math.round(width * ratio);
          height = Math.round(height * ratio);
        }

        canvas.width = width;
        canvas.height = height;
        const ctx = canvas.getContext('2d');
        ctx.drawImage(img, 0, 0, width, height);

        // Try quality levels until under target size
        let quality = 0.85;
        const tryCompress = () => {
          canvas.toBlob((blob) => {
            if (blob.size <= COMPRESSED_TARGET || quality <= 0.3) {
              const reader2 = new FileReader();
              reader2.onload = (e2) => resolve({
                dataUrl: e2.target.result,
                size: blob.size,
                width, height
              });
              reader2.readAsDataURL(blob);
            } else {
              quality -= 0.1;
              tryCompress();
            }
          }, 'image/jpeg', quality);
        };
        tryCompress();
      };
      img.onerror = () => reject(new Error('Failed to load image'));
      img.src = e.target.result;
    };
    reader.onerror = () => reject(new Error('Failed to read file'));
    reader.readAsDataURL(file);
  });
}

// ===== UI =====
function setStatus(text, isError = false) {
  const el = document.getElementById('boot-status');
  el.textContent = text;
  el.className = 'boot-status' + (isError ? ' error' : '');
}

function showScreen(id) {
  document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
  document.getElementById(id).classList.add('active');
}

function addMessage(type, from, text, isSelf = false) {
  const messages = document.getElementById('messages');
  const div = document.createElement('div');
  div.className = 'msg' + (type === 'system' ? ' system' : '');
  const now = new Date();
  const time = now.getHours().toString().padStart(2,'0') + ':' + now.getMinutes().toString().padStart(2,'0');
  div.innerHTML = `
    <span class="msg-time">${time}</span>
    <span class="msg-from ${isSelf ? 'self' : ''}">${from ? from + ':' : ''}</span>
    <span class="msg-text">${escapeHtml(text)}</span>
  `;
  messages.appendChild(div);
  messages.scrollTop = messages.scrollHeight;
}

function addImageMessage(from, dataUrl, isSelf = false) {
  const messages = document.getElementById('messages');
  const div = document.createElement('div');
  div.className = 'msg';
  const now = new Date();
  const time = now.getHours().toString().padStart(2,'0') + ':' + now.getMinutes().toString().padStart(2,'0');

  const img = document.createElement('img');
  img.className = 'msg-image';
  img.src = dataUrl;
  img.onclick = () => openLightbox(dataUrl);

  const wrap = document.createElement('div');
  wrap.className = 'msg-image-wrap';
  wrap.appendChild(img);

  div.innerHTML = `<span class="msg-time">${time}</span><span class="msg-from ${isSelf ? 'self' : ''}">${escapeHtml(from)}:</span>`;
  div.appendChild(wrap);
  messages.appendChild(div);
  messages.scrollTop = messages.scrollHeight;
}

function openLightbox(src) {
  document.getElementById('lightbox-img').src = src;
  document.getElementById('lightbox').classList.add('visible');
}

function closeLightbox() {
  document.getElementById('lightbox').classList.remove('visible');
  document.getElementById('lightbox-img').src = '';
}

function escapeHtml(t) {
  return t.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}

function updateUserList(users) {
  const ul = document.getElementById('user-list');
  ul.innerHTML = '';
  users.forEach(name => {
    const div = document.createElement('div');
    div.className = 'user-entry' + (name === myName ? ' self' : '');
    div.textContent = name;
    ul.appendChild(div);
  });
  document.getElementById('online-count').textContent = users.length;
}

function formatBytes(bytes) {
  if (bytes < 1024) return bytes + 'B';
  return (bytes / 1024).toFixed(0) + 'KB';
}

// ===== WS =====
const RECONNECT_MAX = 8;        // max attempts
const RECONNECT_DELAY = 2000;   // ms between attempts
let reconnectAttempts = 0;
let reconnectTimer = null;
let intentionalDisconnect = false; // set true when user explicitly leaves

function connectWS(onOpen) {
  ws = new WebSocket(WS_URL);
  ws.onopen = () => {
    onOpen();
    reconnectAttempts = 0; // reset on successful connect
    pingInterval = setInterval(() => {
      if (ws && ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ type: 'ping' }));
    }, 30000);
  };

  ws.onmessage = async (event) => {
    let msg;
    try { msg = JSON.parse(event.data); } catch { return; }

    switch (msg.type) {
      case 'joined':
        myName = msg.name; myClientId = msg.clientId;
        isCreator = msg.isCreator; currentRoomId = msg.roomId;
        document.getElementById('display-room-id').textContent = msg.roomId;
        document.getElementById('display-my-name').textContent = msg.name;
        if (isCreator) document.getElementById('nuke-btn').classList.add('visible');
        showScreen('screen-chat');
        addMessage('system', null, `// connected · room ${msg.roomId} · e2ee active · you are ${msg.name}`);
        if (isCreator) addMessage('system', null, `// share ID to invite: ${msg.roomId}`);
        document.getElementById('msg-input').focus();
        break;

      case 'rejoined':
        // Silent reconnect — just update state, no message shown to self
        myName = msg.name; myClientId = msg.clientId;
        break;

      case 'message':
        if (msg.isImage) {
          const dataUrl = await decryptData(msg.payload, true);
          if (dataUrl) addImageMessage(msg.from, dataUrl, msg.from === myName);
        } else {
          const text = await decryptData(msg.payload, true);
          addMessage('chat', msg.from, text, msg.from === myName);
        }
        break;

      case 'system':
        addMessage('system', null, msg.text);
        break;

      case 'userlist':
        updateUserList(msg.users);
        break;

      case 'nuked':
        showNuked();
        break;

      case 'kicked':
        addMessage('system', null, '// disconnected: inactivity');
        intentionalDisconnect = true;
        setTimeout(goHome, 2000);
        break;

      case 'error':
        setStatus('ERROR: ' + msg.text, true);
        break;
    }
  };

  ws.onclose = () => {
    clearInterval(pingInterval);
    // Guests auto-reconnect. Hosts don't — their disconnect is intentional/meaningful.
    if (!intentionalDisconnect && currentRoomId && !isCreator) {
      scheduleReconnect();
    } else if (currentRoomId && isCreator) {
      addMessage('system', null, '// connection lost');
    }
  };
  ws.onerror = () => {
    // onerror always fires before onclose, just let onclose handle it
  };
}

function scheduleReconnect() {
  if (reconnectAttempts >= RECONNECT_MAX) {
    addMessage('system', null, '// reconnect failed · connection lost');
    return;
  }
  reconnectAttempts++;
  reconnectTimer = setTimeout(() => {
    if (!currentRoomId || intentionalDisconnect) return;
    tryReconnect();
  }, RECONNECT_DELAY);
}

function tryReconnect() {
  if (!currentRoomId || !cryptoKey) return;
  const roomToRejoin = currentRoomId;
  const nameToRejoin = myName;

  ws = new WebSocket(WS_URL);
  ws.onopen = () => {
    reconnectAttempts = 0;
    // Silently rejoin with same name
    ws.send(JSON.stringify({ type: 'rejoin', roomId: roomToRejoin, name: nameToRejoin }));
    pingInterval = setInterval(() => {
      if (ws && ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ type: 'ping' }));
    }, 30000);
  };

  ws.onmessage = async (event) => {
    let msg;
    try { msg = JSON.parse(event.data); } catch { return; }

    switch (msg.type) {
      case 'rejoined':
        myName = msg.name; myClientId = msg.clientId;
        // no message shown, reconnect was silent
        break;

      case 'message':
        if (msg.isImage) {
          const dataUrl = await decryptData(msg.payload, true);
          if (dataUrl) addImageMessage(msg.from, dataUrl, msg.from === myName);
        } else {
          const text = await decryptData(msg.payload, true);
          addMessage('chat', msg.from, text, msg.from === myName);
        }
        break;

      case 'system':
        addMessage('system', null, msg.text);
        break;

      case 'userlist':
        updateUserList(msg.users);
        break;

      case 'nuked':
        showNuked();
        break;

      case 'kicked':
        addMessage('system', null, '// disconnected: inactivity');
        intentionalDisconnect = true;
        setTimeout(goHome, 2000);
        break;

      case 'error':
        // Rejoin failed (room gone, etc) — give up silently
        addMessage('system', null, '// could not reconnect · room may be gone');
        intentionalDisconnect = true;
        break;
    }
  };

  ws.onclose = () => {
    clearInterval(pingInterval);
    if (!intentionalDisconnect && currentRoomId && !isCreator) {
      scheduleReconnect();
    }
  };
  ws.onerror = () => {};
}

// ===== ACTIONS =====
function toggleCreateOptions() {
  const opts = document.getElementById('room-options');
  opts.classList.toggle('visible');
  document.getElementById('join-form').classList.remove('visible');
  document.getElementById('room-id-banner').classList.remove('visible');
}

function toggleJoinForm() {
  const form = document.getElementById('join-form');
  form.classList.toggle('visible');
  document.getElementById('room-options').classList.remove('visible');
  document.getElementById('room-id-banner').classList.remove('visible');
  if (form.classList.contains('visible')) document.getElementById('join-id-input').focus();
}

async function createRoom() {
  const roomId = generateRoomId();
  const inactivityMinutes = parseInt(document.getElementById('inactivity-select').value);
  const nukeOnLeave = document.getElementById('nuke-on-leave-toggle').checked;

  setStatus('GENERATING KEY...');
  cryptoKey = await deriveKey(roomId);
  pendingRoomId = roomId;
  pendingSettings = { inactivityMinutes, nukeOnLeave };

  // Show banner
  document.getElementById('banner-id-text').textContent = roomId;

  // Show settings summary
  const settingsEl = document.getElementById('banner-settings');
  const timeLabel = document.getElementById('inactivity-select').options[document.getElementById('inactivity-select').selectedIndex].text;
  settingsEl.innerHTML = `TIMEOUT: <span>${timeLabel}</span> &nbsp;·&nbsp; NUKE ON LEAVE: <span>${nukeOnLeave ? 'YES' : 'NO'}</span>`;

  document.getElementById('room-id-banner').classList.add('visible');
  document.getElementById('room-options').classList.remove('visible');
  setStatus('');
}

function copyRoomId() {
  if (!pendingRoomId) return;
  navigator.clipboard.writeText(pendingRoomId).then(() => {
    const btn = document.querySelector('.banner-copy-btn');
    btn.textContent = '[ COPIED! ]';
    setTimeout(() => btn.textContent = '[ COPY ID ]', 2000);
  }).catch(() => {
    const el = document.getElementById('banner-id-text');
    const range = document.createRange();
    range.selectNode(el);
    window.getSelection().removeAllRanges();
    window.getSelection().addRange(range);
  });
}

function enterRoom() {
  if (!pendingRoomId || !cryptoKey) return;
  setStatus('CONNECTING...');
  connectWS(() => {
    ws.send(JSON.stringify({
      type: 'create',
      roomId: pendingRoomId,
      inactivityMinutes: pendingSettings.inactivityMinutes,
      nukeOnLeave: pendingSettings.nukeOnLeave
    }));
  });
}

async function joinRoom() {
  const input = document.getElementById('join-id-input');
  const roomId = input.value.trim().toUpperCase();
  if (!roomId || roomId.length < 4) { setStatus('INVALID ROOM ID', true); return; }
  setStatus('DERIVING KEY...');
  cryptoKey = await deriveKey(roomId);
  setStatus('CONNECTING...');
  connectWS(() => ws.send(JSON.stringify({ type: 'join', roomId })));
}

async function sendMessage() {
  const input = document.getElementById('msg-input');
  const text = input.value.trim();
  if (!text || !ws || ws.readyState !== WebSocket.OPEN) return;
  input.value = '';
  const payload = await encryptData(text);
  ws.send(JSON.stringify({ type: 'message', payload, isImage: false }));
  addMessage('chat', myName, text, true);
}

function triggerImagePick() {
  document.getElementById('img-file-input').click();
}

async function handleImagePicked(event) {
  const file = event.target.files[0];
  event.target.value = ''; // reset so same file can be picked again
  if (!file || !ws || ws.readyState !== WebSocket.OPEN) return;

  addMessage('system', null, `// compressing image...`);

  try {
    const { dataUrl, size, width, height } = await compressImage(file);
    addMessage('system', null, `// sending image (${width}x${height} · ${formatBytes(size)})`);

    const payload = await encryptData(dataUrl);
    ws.send(JSON.stringify({ type: 'message', payload, isImage: true }));

    // Show own image locally
    addImageMessage(myName, dataUrl, true);
  } catch (err) {
    addMessage('system', null, `// image error: ${err.message}`);
  }
}

function confirmNuke() {
  if (confirm('NUKE this room? All users will be disconnected.')) {
    ws.send(JSON.stringify({ type: 'nuke' }));
  }
}

function showNuked() {
  clearInterval(pingInterval);
  currentRoomId = null;
  document.getElementById('nuked-overlay').classList.add('visible');
}

function goHome() {
  intentionalDisconnect = true;
  clearTimeout(reconnectTimer);
  document.getElementById('nuked-overlay').classList.remove('visible');
  closeLightbox();
  if (ws) { ws.close(); ws = null; }
  clearInterval(pingInterval);
  myName = myClientId = currentRoomId = cryptoKey = pendingRoomId = null;
  isCreator = false;
  intentionalDisconnect = false;
  reconnectAttempts = 0;
  pendingSettings = {};
  document.getElementById('messages').innerHTML = '';
  document.getElementById('user-list').innerHTML = '';
  document.getElementById('join-id-input').value = '';
  document.getElementById('join-form').classList.remove('visible');
  document.getElementById('room-options').classList.remove('visible');
  document.getElementById('room-id-banner').classList.remove('visible');
  document.getElementById('nuke-btn').classList.remove('visible');
  document.getElementById('online-count').textContent = '0';
  document.querySelector('.toggle-label').textContent = 'OFF';
  document.getElementById('nuke-on-leave-toggle').checked = false;
  setStatus('');
  showScreen('screen-boot');
}

document.getElementById('msg-input').addEventListener('keydown', e => {
  if (e.key === 'Enter') sendMessage();
});
document.getElementById('join-id-input').addEventListener('keydown', e => {
  if (e.key === 'Enter') joinRoom();
});

document.addEventListener('touchmove', e => {
  if (!e.target.closest('.messages') && !e.target.closest('#screen-boot')) {
    e.preventDefault();
  }
}, { passive: false });
</script>
</body>
</html>