<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Phase 6 — Chat Refresh — Arc Plan</title>
<style>
body { font-family: system-ui, sans-serif; max-width: 760px;
margin: 1.5em auto; padding: 0 1em; line-height: 1.5; color: #1a1a1a; }
h1 { font-size: 1.4em; }
h2 { font-size: 1.1em; margin-top: 1.4em; }
ol { margin: 0.3em 0; } li { margin: 0.25em 0; }
ol.items { list-style: none; padding-left: 0; }
ol.items.numbered { list-style: decimal; padding-left: 2em; }
.check { color: #1a7f37; font-weight: bold; }
.next { color: #b35900; font-weight: bold; }
.review { color: #0969da; font-weight: bold; }
.nope { color: #82071e; font-weight: bold; }
.q { color: #8250df; font-weight: bold; }
code { background: #f2f2f2; padding: 0 0.25em; border-radius: 3px; }
em { color: #555; }
</style>
</head>
<body>
<h1>Phase 6 — Chat Refresh</h1>
<p><strong>Arc Post-Mortem Summary.</strong>
This arc refreshed the one-to-one chat. It moved message delivery onto a
plain-text push channel and preferred WebSockets over server-sent events
(I), and rebuilt the input row into a single rounded pill: a plus button
opens the attachment dialog, the emoji button and an auto-growing textarea
sit in the pill, and the trailing control swaps between a microphone and a
send arrow as text is entered (II). It removed the per-conversation
translation feature and its per-message API rather than reworking them (IV),
which left nothing tertiary in the header, so the planned overflow menu was
dropped (III); the message bubbles were recolored through the Phase 5 custom
properties, the sender's own tinted and the other party's neutral (V).
Alongside the planned work it added smart-HTTP serving so the repository's
wiki pages can be git-cloned, per-domain favicon serving, and it centered the
header's video-call symbol after finding that a <code>setDisplay</code> call
was overriding the button's flex layout with <code>block</code>.</p>
<p><em>Arc started 2026-07-21, after Phase 5. Sections are Roman numerals
and steps within them are lettered; each step's title says what it does,
followed by a sentence on what was proposed and, once worked, a few on what
was done and anything learned. Marker legend:
<span class="next">▶</span> being worked on,
<span class="review">⚑</span> implemented and waiting to be reviewed,
<span class="check">✓</span> done,
<span class="check">✓</span><span class="nope">✗</span>
closed by deciding not to do it, <span class="q">?</span> open decision,
unmarked = planned.</em></p>
<h2>What this arc is for</h2>
<p>Yioop's group messaging works but shows its age. The message that
carries a chat still arrives by polling in the common case: plain text
messaging asks the server for new messages on a timer, and the server's
push channel is only opened while a voice or video call is up. The input
row is a run of separate controls — a paper-clip link, an emoji
button, a single-line text field, and a microphone button sitting beside
each other — rather than the single rounded field a person expects
from a modern chat. The header carries every action inline, the
per-conversation translation control reads as clutter, and the message
bubbles do not yet use the color and spacing groundwork Phase 5 laid down.
This arc brings the chat up to date on both counts: how a message arrives
(push instead of poll, where the server can push), and how the chat looks
and feels to use. It builds on Phase 5's custom properties for the visual
work and on atto's WebSocket support for the delivery work.</p>
<h2>I. Deliver messages by push, not by polling</h2>
<ol class="items">
<li><span class="check">✓</span> A. <strong>Push plain text messages
instead of polling for them.</strong> The wait the server-sent event
stream tells the browser to hold before reconnecting is now a named
constant, <code>USER_MESSAGE_STATUS_POLL_TIME</code>. The full push,
without any reconnect, ships in I.B. To-do item 2. Done.</li>
<li><span class="check">✓</span> B. <strong>Prefer WebSockets over
server-sent events where the server can hold a socket.</strong> Under
the command-line server, a <code>/ws-messages</code> WebSocket route
holds a conversation's socket open and pushes each new message the moment
it lands; browsers on a server that cannot hold a socket keep using the
event stream. This needed one general addition to the atto server: a
WebSocket route now sees its handshake request through the usual request
globals, so it can authenticate the connection. To-do item 1. Done.</li>
</ol>
<h2>II. Rebuild the chat input row</h2>
<ol class="items">
<li><span class="check">✓</span> A. <strong>Make the input a single
rounded pill.</strong> Settled with Chris as
<code>⊕ (🙂 New message 🎤)</code>: a filled
<code>⊕</code> circle sits to the left of the pill and opens the
attachment dialog directly, and the pill (<code>#user-input-container</code>,
a <code>--gray-93</code> surface with a <code>--gray-80</code> border)
holds the emoji button, the growing text field, and the trailing
record/send control. Keeping the emoji and attach as their own one-tap
controls avoids a nested "emoji or attachment" chooser. In
<code>UsermessagesElement</code> the file input and its now-plus attach
left the pill's left edge to become that circle, and the record, audio,
and send controls moved inside the pill; in <code>messages.css</code> the
attach became the round circle, the emoji, mic, and send became
transparent inline icons, and the text field grows to fill
(<code>flex: 1; min-width: 0</code>). Rendered headless at 360px and
800px with no viewport overflow. To-do item 6.</li>
<li><span class="check">✓</span> B. <strong>Grow the field as the
message grows.</strong> The single-line <code><input type="text"></code>
became a <code><textarea></code> whose height is set to its own
<code>scrollHeight</code> on each input by
<code>autoGrowMessageField</code>, so it starts one line tall and grows
with the text up to a <code>6rem</code> cap, after which it scrolls; the
pill grows with it and bottom-aligns its controls. A keydown handler
sends on Enter (through the form's <code>requestSubmit</code>, only when
the field is not empty) and lets Shift+Enter start a new line. Rendered
headless at one, three, and many lines with no viewport overflow and the
height capping as intended. To-do item 6.</li>
<li><span class="check">✓</span> C. <strong>Swap the microphone for a
send button when there is text to send.</strong> Done together with II.A,
since the two share the trailing slot inside the pill. The field's
<code>input</code> event drives <code>updateMessageSendControl</code>,
which shows the microphone (record) while the field is empty and the send
button once anything is typed. So the recorded-audio case also flips to
send and back, <code>initializeAudioRecorder</code> now fires an
<code>input</code> event when it fills the field on stop and when it
clears the field on delete, and the two direct
<code>record_button.style.display</code> writes it used to make, now
redundant, were removed. To-do item 6.</li>
</ol>
<h2>III. Tidy the chat header</h2>
<ol class="items">
<li><span class="check">✓</span><span class="nope">✗</span> A.
<strong>Move the header's lesser actions behind a kebab menu.</strong>
Closed as not needed. With the translation control removed in IV, the
header carries only primary things — the contact identity, the
video-call button, and on a phone the back arrow — so nothing
tertiary is left to move behind an overflow menu. If the header later
gains secondary actions, an overflow menu can be reconsidered then.
To-do item 7.</li>
</ol>
<h2>IV. Rethink per-message translation</h2>
<ol class="items">
<li><span class="check">✓</span> A. <strong>Replace the
per-conversation translation dropdown.</strong> Decided with Chris to
remove the feature entirely rather than build a per-message replacement.
Removed: the translation-language dropdown from the chat header; the
<code>SocialComponent</code> translation methods
(<code>setTranslationLanguage</code>, <code>translateMessageBatch</code>,
<code>getTranslationLocales</code>, <code>translateText</code>) and the
two message-path blocks that translated a loaded batch; the
<code>GroupModel</code> per-contact translation storage
(<code>get</code>/<code>setTranslationLanguage</code>); the
<code>ApiController</code> <code>translate</code> action and its
<code>translateText</code>, with <code>translate</code> dropped from that
controller's activity list; and the translation-language CSS. The general
localization system (<code>tl()</code>, locale files) is untouched. To-do
item 8.</li>
</ol>
<h2>V. Polish the bubbles and spacing</h2>
<ol class="items">
<li><span class="check">✓</span> A. <strong>Color and space the bubbles
with the Phase 5 custom properties.</strong> The bubble background classes
moved from the hardcoded <code>back-light-blue</code>/<code>back-light-gray</code>
utilities to semantic <code>message-mine</code>/<code>message-theirs</code>
classes, set in both the initial render (<code>UsermessagesElement</code>)
and the refresh append (<code>SocialComponent</code>). The sender's own
messages now take a tinted bubble (<code>--color-topbar</code>) and the
other party's a neutral one (<code>--gray-93</code>), each with a
<code>--gray-80</code> border for definition, so both track light and
dark. Time labels between message groups are unchanged. The quoted-reply
visual has no target yet — the chat has no reply/quote feature to
style — so that part waits until one exists. The light-mode tint is
deliberately subtle (the pale custom properties); say if you want more
contrast between the two sides. To-do item 9.</li>
</ol>
<h2>Order and dependencies</h2>
<p>The delivery work (I) and the visual work (II–V) are independent and
can proceed in either order. Within delivery, push plain messaging (I.A)
comes before choosing the channel (I.B), since the channel choice is a
refinement of the push path. Within the input row, the pill (II.A) comes
before the growing field and the mic/send swap (II.B, II.C) sit inside it.
The translation decision (IV) is an open question to settle with Chris
before it is worked, so it is marked with a question rather than planned as
written. The bubble polish (V) depends only on the Phase 5 custom
properties, which are in place.</p>
<h2>Other work accomplished</h2>
<ol class="items numbered">
<li><strong>Smart-HTTP serving for the git repository wiki pages, in pure
PHP.</strong> Yioop serves a group's git repository over the dumb side
of git's HTTP protocol, so a client must download the whole history and
cannot make a shallow (<code>--depth</code>) clone. This adds the smart
side, in pure PHP with no git program involved.
<ol>
<li><span class="check">✓</span> A new pure-logic library class,
<code>GitUploadPack</code>, with unit tests. It frames git's
pkt-lines, advertises the repository's refs with the shallow
capability, reads a client's want-and-depth request, walks history
to the requested depth (reporting the shallow boundary), and writes
a fresh packfile of just those objects, reading objects through the
existing <code>GitRepository</code>. Done.</li>
<li><span class="check">✓</span> Wiring into
<code>GitComponent</code> so a real <code>git clone --depth</code>
against a repository wiki page works: the ref advertisement answers
<code>GET info/refs?service=git-upload-pack</code>, and the packfile
answers <code>POST git-upload-pack</code>. Done.</li>
</ol></li>
<li><span class="check">✓</span> <strong>Per-domain favicon
serving.</strong> When a browser asks a Yioop web server for
<code>/favicon.ico</code>, <code>ResourceController</code> now looks up
the request host's saved appearance and serves that domain's own favicon
from its per-domain resource subfolder, falling back to the global icon
when the domain has none or the file is missing. This matters for pages
that carry no favicon link of their own, such as a served static HTML
file, which would otherwise all show the one shared icon whatever domain
served them. A follow-up guard keeps a found per-domain icon from being
overwritten by the later global-favicon path. Done.</li>
<li><span class="check">✓</span> <strong>Center the video-call symbol
in its button.</strong> Two causes. First, on a narrow or short viewport
the shared icon-button rule shrank every <code>.icon-button-container</code>
to <code>1.2rem</code> wide while its height stayed <code>2rem</code>, so
the call button became a portrait box; the call-control buttons now keep
the square <code>--control-height</code> on every viewport. Second, and the
real cause of the symbol sitting centered on load then jumping to the top a
moment later: <code>setCallState</code> runs on load and calls
<code>setDisplay('video-start', true)</code>, whose default display type is
<code>block</code>, so it replaced the button's <code>inline-flex</code>
with <code>block</code> and the flex centering stopped applying. The four
show-calls for <code>video-start</code> and <code>video-end</code> now pass
<code>'inline-flex'</code> as the display type, so the buttons stay flex and
the glyph stays centered. Measured headless: under <code>block</code> the
glyph rides 5px high, under <code>inline-flex</code> it is centered.</li>
</ol>
</body>
</html>