# Spaceship — Bug Fix Notes

This pass addressed the items from the bug report. Below is what changed, and
the one item that needs a server-side credential rather than a code change.

## Fixed in code

### 1. Emoji & GIF support  (`index.php`, `css/app.css`, `js/app.js`)
- Expanded the emoji picker with many more smileys plus animals, food,
  people/gestures and symbol categories.
- Added a **GIF picker**: a new "GIF" button next to the emoji button opens a
  searchable GIF tray (trending by default, type to search). Selecting a GIF
  sends it into the chat. GIFs are end-to-end encrypted exactly like other
  media (the GIF URL is encrypted), and they animate in the message thread.
- GIF search is powered by GIPHY. It ships with GIPHY's shared public demo key,
  which is rate-limited. For reliable production use, create a free key at
  https://developers.giphy.com and set `GIPHY_API_KEY` near the top of
  `js/app.js`. If the key/network is unavailable, the picker shows a clear
  message instead of failing silently.

### 6. Login flow — Enter / Go now submits  (`js/app.js`)
- Pressing **Enter** (desktop) or **Go** (mobile keyboards) in either Sign-In
  field now signs in, and in either Register field now creates the account.
  Previously only the on-screen buttons worked.

### 7. Account-specific auto-logout  (`js/app.js`)
- Auto-logout (signing out when switching windows / leaving the active page) now
  applies to the **`Joshz` account only**. Every other account stays signed in
  when they switch tabs or background the app.
- The account is configurable: edit `AUTO_LOGOUT_ACCOUNT` near the top of
  `js/app.js` (comparison is case-insensitive).
- Even for that account, opening a photo/camera picker or running an upload does
  **not** trigger logout — only genuinely switching windows or leaving the page
  does (this guard already existed and remains in place).

## Already addressed in this build (verified)

These were already implemented in the code you sent (with comments describing
the exact symptoms) and were verified during this review:

- **3. Android keyboard hides the chatbox** — the layout height is pinned to the
  visual-viewport height (`--app-h`, set from `window.visualViewport`), so the
  composer stays visible above the keyboard.
- **4. Typing / online-offline status** — typing pings re-send every 3s against a
  5s server window so "typing…" doesn't flicker off; presence is refreshed on
  every request and on each online poll, so users no longer appear offline while
  active.
- **5. Photo upload signing the user out on Android** — a file-dialog guard
  suppresses the visibility-based logout while a picker/upload is in progress.

## Needs a deployment credential, not a code change

### 2. Call / video-call stability (no camera/audio, can't see each other)
- The WebRTC signalling (offer/answer/ICE exchange) and local/remote media
  wiring are complete and correct. The remaining cause of "users cannot see each
  other" on phones and many home networks is that the app currently only has
  **STUN** servers. Across mobile/carrier and most consumer NATs, a **TURN relay
  is required** for audio/video to actually flow.
- To fix: add your TURN server(s) to `ICE_SERVERS` near the top of `js/app.js`
  (a commented template is already there), e.g. coturn, Twilio, Cloudflare, or
  Metered. Once a working TURN entry is present, calls relay reliably.
- Note: camera/microphone also require the page to be served over **HTTPS** (or
  localhost). The app already surfaces a clear error if that isn't the case.

---

# Follow-up fixes (interaction / "can't type or unlock") 

Reported: can't type in the chatbox on any device, and on web can't click a
friend's name to start a chat or unlock messages. The GIF tray also showed up
unstyled in the input row. Two root causes were found and fixed:

## A. Stale cached assets  (`index.php`)
The unstyled, always-visible GIF tray in the screenshot is the tell-tale sign
that the browser was serving an OLD cached `app.css`/`app.js` from before the
fixes (the old CSS has no rule to hide/position the new GIF tray). The CSS and
JS includes now carry a version query string (`?v=20260603`), so every device
fetches the updated files instead of a cached copy. Bump that version whenever
you deploy new CSS/JS.

## B. Unlocking a protected chat used `prompt()`  (`index.php`, `css/app.css`, `js/app.js`)
Opening a password-protected chat disables the composer (by design) and the only
way back in was `window.prompt()`. Installed PWAs and several mobile webviews
(iOS especially) **disable `prompt()`/`confirm()`/`alert()`**, so the "Unlock
messages" button did nothing and the composer stayed stuck — which looks exactly
like "can't type" and "can't unlock."

Unlock now uses an in-page password modal (`#unlock-modal`) instead of
`prompt()`: it verifies the account password, shows an inline error on a wrong
password, and on success re-enables the composer. Enter/Go submits it. This
works in normal browsers and installed PWAs alike.

Note: the "Forward message" action still uses `prompt()` for the target
username; it will be inert in installed-PWA mode. It wasn't part of this report,
but it can be moved to the same modal pattern on request.

Verified with headless (jsdom) runs: app loads with no runtime errors, clicking
a conversation invokes `openChat`, Enter submits the auth fields, and the unlock
modal flow opens, rejects a wrong password, and unlocks + re-enables the composer
on the correct one — all without calling `prompt()`.

---

# Follow-up #2 — composer crushed / can't type or click (real root cause)

The earlier screenshot showed the GIF search box sprawling across the input row
with the message box squeezed to a thin sliver. That happens when the GIF tray
renders **in the normal input row** (when its hide/position CSS isn't applied)
and steals the horizontal space, collapsing the textarea so there is nothing to
tap or type into — worst on narrow portrait phones.

Structural fix (does not depend on the stylesheet being perfectly deployed):
- The GIF picker is no longer a popover inside the input row. It is now a
  self-contained **modal at the top level of the page** (`#gif-modal`), hidden by
  an **inline `display:none`** and shown/hidden by JS. Because it lives outside
  the input row and is hidden inline, it can never occupy the row or crush the
  message box even if `app.css` is stale or only partly deployed.
- The input row now contains only: emoji button, GIF button, attach button, the
  message box, and send. Verified in a headless run.

Asset version bumped to `?v=20260603b` (bump it again on each deploy).

## Please verify the deploy actually updated
The identical screenshots suggest the running site may not be serving the new
files. After uploading, open the running site and check:
1. View source — the CSS/JS links should end with `?v=20260603b`.
2. Hard refresh (Ctrl/Cmd+Shift+R) or, if installed as an app, remove and
   re-add it to the home screen.

## 30-second self-diagnostic (paste into the browser console on the running site)
This reports exactly which element is on top of the message box and a sidebar
contact — i.e. whatever is intercepting taps/clicks, if anything:
    const ta = document.getElementById('msg-input');
    if (ta){ const r = ta.getBoundingClientRect();
      console.log('composer size', Math.round(r.width)+'x'+Math.round(r.height),
        'top element:', document.elementFromPoint(r.left+r.width/2, r.top+r.height/2),
        'disabled:', ta.disabled); }
    const ci = document.querySelector('#chat-list .ci');
    if (ci){ const r = ci.getBoundingClientRect();
      console.log('contact top element:', document.elementFromPoint(r.left+r.width/2, r.top+r.height/2)); }
If "top element" is anything other than the textarea / the contact row itself,
that element is the blocker — send me what it logs and I'll target it directly.

---

# Change — chat password-protection is now Joshz-only

Per request, the password-protect-a-chat feature is restricted to the `Joshz`
account. For every other account it is fully disabled:
- No chat is ever treated as protected (`isProtectedChat` returns false), so the
  composer is never locked and the unlock modal never appears.
- Any locks the server previously stored for those accounts are ignored on load,
  so those conversations open and decrypt normally — this also clears the
  "can't type because the chat is locked" state for non-Joshz users.
- The header lock button (🔒) is hidden for non-Joshz accounts, and the
  lock/unlock toggle is a no-op for them.

Configurable via `PROTECTED_CHAT_ACCOUNT` near the top of `js/app.js`
(case-insensitive). Joshz keeps the full lock / unlock behavior unchanged.

Asset version bumped to `?v=20260603c`.

---

# Round 3 — encryption bar removed, GIFs, and video/audio calls

## 1. Removed the encryption/IV-Cipher bar
The bottom bar that printed "E2EE · IV: … · Cipher: …" has been removed from the
markup, JS, and CSS. (Messages are still end-to-end encrypted; only the technical
readout is gone.)

## 2. GIF search now works (KLIPY / Tenor / GIPHY)
The old picker used GIPHY's public demo key, which now returns nothing because
GIPHY's API became paid. Today there is **no free keyless GIF API** — GIPHY is
paid and Google is retiring Tenor's developer API, so **WhatsApp moved to KLIPY**.

What changed:
- GIF requests now go through a small backend proxy, `api/gif.php`, that keeps the
  API key server-side, avoids browser CORS, and normalizes results to
  `{ preview, full }`. It supports **KLIPY** (default, what WhatsApp uses), **Tenor**,
  and **GIPHY**.
- Set the provider + key once in `api/config.php`:
    define('GIF_PROVIDER', 'klipy');   // or 'tenor' / 'giphy'
    define('GIF_API_KEY',  'YOUR_FREE_KEY');
  Free KLIPY key: https://klipy.com/developers  (lifetime free tier).
- With no key set, the picker shows a clear hint instead of "No GIFs found."
- Selected GIFs are sent as image messages (URL encrypted like other media) and
  animate in the thread.

## 3. Video/voice calls — TURN relay + ICE fix
Two problems were addressed:
- **No relay (root cause of no video/audio across phones/networks).** The app only
  had STUN servers; across mobile/NAT networks a TURN relay is required. There is
  no working free anonymous public TURN any more, so you supply credentials one of
  two ways (top of `js/app.js`):
    * paste static TURN entries into `ICE_SERVERS`, **or**
    * set `METERED_TURN_API_URL` to a Metered credentials URL (free 20 GB/mo at
      https://dashboard.metered.ca/signup) and the app fetches TURN servers at
      call time.
  Cloudflare's free TURN (https://dash.cloudflare.com → Calls → TURN) works too.
- **ICE-candidate ordering bug.** Remote ICE candidates that arrived before the
  SDP offer/answer was applied were being discarded, which could stop media from
  connecting even on the same network. They are now buffered and flushed once the
  remote description is set. (Camera/mic still require the site to be served over
  HTTPS, or localhost.)

Asset version bumped to `?v=20260603d`.

---

## 2026-06-03e — Call overlay enhancements

- Added encrypted in-call chat inside the call/video-call modal.
- Added Speaker button with browser-supported audio-output switching.
- Added Share Screen button using `getDisplayMedia()`.
- Improved WebRTC connection failure messages for TURN/HTTPS issues.
- Incoming calls now continue polling status while ringing, so cancelled calls do not remain stuck on the receiver screen.
- Added `CALL_GIF_SETUP.md` with GIF key and WebRTC/TURN setup notes.
