Commit Graph

143 Commits

Author SHA1 Message Date
Sravan 9eb4a99679 mobile: revert chatcol absolute-behind — it broke chat opening (batch127)
The list-column position:absolute change stopped conversations from opening. Revert to the
known-good display:none. Keep the other batch124-125 fixes (content promoted, reconcile,
reopen-at-latest, download interceptor). Slide back to the batch125 behaviour (opens fine,
minor end hitch) while I find a safe way to remove the reflow.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 06:35:45 +05:30
Sravan 82d0154313 mobile: kill the real slide hitch — stop reflowing the list column at slide-end (batch126)
Device log proved the residual 30-78ms end-of-slide hitch is NOT the content-layer demote
(content stays promoted) and NOT the re-render (reconcile, longtasks:[]). It's the chat-list
column flipping display:flex->none at transitionend, which reflows that whole subtree. Keep
the list RENDERED behind the conversation (position:absolute; z-index:0; covered by the opaque
content pane) instead of toggling display. Teardown now only clears transforms = no reflow.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 01:04:40 +05:30
Sravan ea04556596 mobile: definitive slide fix (zero teardown) + real native download + reconcile (batch125)
Slide: the residual ~40ms end-of-slide hitch (down from 128ms in b124) was the layer
DEMOTE + the deferred full renderThread at transitionend. Two fixes:
- Keep .content promoted for the WHOLE time a chat is open (will-change lives on
  body.chat-open, not on the animating class). The open transition now only changes the
  transform VALUE 100%->0; when it ends there is nothing to tear down -> no demote raster.
- Slide-end no longer rebuilds all 40 nodes: reconcileOpen() appends only the 0-2 genuinely
  new tail messages (full renderThread only if the page structure diverged).

Download: routing to Safari failed auth (no login cookie). Real fix: fetch the file in the
WebView (cookie present) and hand the bytes to the OS save/share sheet via Filesystem+Share
(added to mobile deps; ships next TestFlight build). Until then, images fall back to the iOS
long-press "Save to Photos" instead of breaking the app.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 00:48:34 +05:30
Sravan 91b3e5c522 mobile: kill slide end-hitch (compositor teardown), fix reopen-at-latest + native download hang (batch124)
Slide: the 40-128ms hitch was a compositor RE-RASTER at transitionend, not JS
(longtasks:[], renderMs 2-6ms in the device log). Removed contain:layout paint /
isolation:isolate from the .content slide layer and the filter:brightness parallax
on .chatcol (both force a full re-raster on teardown); defer the layer demotion two
frames past transitionend so it lands off the motion path. Also replaced the fragile
length+lastId slide-end skip (it almost never held once a live message grew the cache,
so the full innerHTML rebuild kept running at slide-end) with an exact per-message diff.

Reopen-at-latest: loadOlder wrote the GROWN thread (100+ msgs) into THREAD_CACHE, so
re-opening re-rendered all of it and stranded you mid-history as images shifted the
(pre-load) bottom past the 1200px pin guard. Open now renders the latest PAGE only;
added _forcePinOpen to glue to the newest through late image loads until the user scrolls.

Download hang: <a download> navigated the whole WKWebView away to the raw file (no back,
app frozen). Native-only capture-phase interceptor opens downloads in the system browser.
Lightbox close/download buttons: solid dark chips so they're visible over bright images.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 00:31:57 +05:30
Sravan 433703cfeb perf(mobile): skip redundant end-of-slide re-render (unchanged content) + load-older before rubber-band
Device log: slide is 60fps EXCEPT one ~60-130ms hitch at the END = the deferred renderThread rebuilding
the (identical) DOM at slide end. Skip it when the network result matches the cache render (signature).
Load-older: fired at scrollTop<120 = during the rubber-band over-scroll past the top (beforeScrollTop
was -289), so the prepend+anchor jerked momentum. Trigger earlier (<700, skip negative scrollTop) so it
loads while still scrolling and never interrupts the bounce.
Build marker -> 2026-07-19-batch123.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 00:10:27 +05:30
Sravan a4600f6b5f fix(mobile): close the debug <script> block (batch122 left it open, breaking the keyboard block)
The perf-diagnostics script tag was never closed, so it merged with the following native/keyboard
<script> and threw 'Unexpected token <', disabling the keyboard/safe-area init. Add the missing </script>.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 00:02:48 +05:30
Sravan d02d8976d3 diag(mobile): instrument slide frame-timing + render cost + load-older scroll to find the real jank
Re-add /api/dbg + a frame-gap/long-task probe over the chat-open slide, the innerHTML render time, and
before/after scroll for load-older — to measure on-device where the jank actually is (main-thread block
vs compositor) instead of guessing. Temporary; removed once fixed.
Build marker -> 2026-07-19-batch122.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 00:00:28 +05:30
Sravan 529bc26cb5 perf(mobile): compositor 'render-then-arm' chat-open slide + robust load-older anchor; remove diagnostics
Slide (workflow-verified): the jump was the main thread building the message DOM + scroll writes DURING
the 300ms animation. Now: render off-screen first, then arm a transform-only slide on a promoted GPU
layer (translate3d/contain:layout paint) with all innerHTML/scrollTop work deferred to transitionend
(afterOpenSlide queue). armOpenSlide runs after the sync render; appendBubble defers live writes mid-slide.
Load-older jump: anchor on the specific oldest-loaded message element (not scrollHeight math, which broke
when prepended images loaded and shifted content) and re-anchor as those images load.
Removed all /api/dbg diagnostics (client reporter + server route + spike/diag probes).
Build marker -> 2026-07-18-batch121.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 23:49:38 +05:30
Sravan f3c3e08be4 perf(mobile): paginate threads to 40 (was 500) — kills the heavy-DOM jank at the source
User's insight: the jank/jumps come from rendering ALL ~500 messages on open. Load only the latest 40
(server + client PAGE), and the existing loadOlder() pages in older history on scroll-up with a scroll
anchor (no jump). Whole-thread SEARCH is a separate endpoint, unaffected. Also #1: keyboard-show only
pins to newest when already near the bottom, so replying to an OLD message no longer yanks to latest.
Build marker -> 2026-07-18-batch120.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 23:31:05 +05:30
Sravan 2bd8b3c8c1 fix(mobile): image pager direction + end-arrows; reset --kb on composer blur
- Pager: force the staged (opposite-edge) frame to PAINT (void offsetWidth) before animating in, so the
  incoming image enters from the correct side (was appearing from the exit side). Hide prev-arrow at the
  first image and next-arrow at the last.
- #1: keyboard closed after reply-cancel but the --kb lift stayed (empty gap). Add keyboardDidHide reset +
  a composer-blur fallback that drops --kb when focus leaves the composer.
Build marker -> 2026-07-18-batch119.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 23:11:05 +05:30
Sravan da3d44cbb7 fix(mobile): clamp reply-quote (balloon), clip swipe-reply overflow, animated image pager
- Reply quote: was showing the FULL quoted body untruncated -> DIV.quote rendered 1571px, ballooning the
  bubble + leaving the chat not-at-bottom. Clamp to 2 lines (-webkit-line-clamp) + collapse newlines.
- Swipe-to-reply: translateX(+72px) on a right-edge bubble pushed past the viewport -> a horizontal scroll
  bar. .convo-msgs overflow-x:clip stops it.
- Image pager (#8): replace instant src swap with a real slide — current image slides out, next slides in
  from the opposite side; clamp at first/last (no wrap -> no blank slide past the end).
Build marker -> 2026-07-18-batch118.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 22:32:10 +05:30
Sravan 1f5b4bd42c fix(mobile): stop bubble/composer balloon + transition pop (shared root cause, workflow-verified)
Parallel root-cause analysis: during body.chat-opening the pane is momentarily ~0 width; word-break:
break-word / overflow-wrap:anywhere let bubbles collapse to 1-GLYPH min-content, so pre-wrap text wraps
into hundreds of lines (5684px 'balloon'); same inflates the empty/draft textarea (autoGrow->140=4 lines);
and translateX(100%) of a ~0-width pane = no travel = the 'jump'. The tall box hid in anonymous line
boxes (renderMsgBody emits bare text nodes), which is why the probe only saw the 84px timestamp span.
Fixes (all @media max-width:760px / chat-opening scoped, desktop untouched):
 1. .bubble: word-break:normal + overflow-wrap:break-word (min-content = longest word, never collapses)
 2. mobile bubble group: overflow-wrap:anywhere -> break-word
 3. .msg-link: word-break:break-word -> overflow-wrap:break-word (URL-only msgs can't reopen the hole)
 4. body.chat-opening .content: width:100vw -> definite width, no collapse frame, real slide travel
Build marker -> 2026-07-18-batch117.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 18:53:38 +05:30
Sravan 898d389dfe diag(mobile): probe composer measurements + transition animation + tall-bubble content
tallImg was no-img, so the balloon is a TEXT bubble stretching (even in a 17-msg chat). Add: composer/
textarea height+scrollHeight+rows+value+fontSize+lineHeight (why ~4 lines), whether chat-opening class +
.content animationName are applied (why transition jumps), and the tall bubble's text + tallest child.
Build marker -> 2026-07-18-batch116.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 17:59:58 +05:30
Sravan 7f7ff8ba1f fix(mobile): cap in-bubble image height (kills the stretch balloon) + sharper spike diagnostic
Spike log showed bubbles ballooning to 3576-5684px (an image loading at natural ~2778px height before
constraint). att-img is capped 240 but some image path wasn't — added .convo-msgs img/.bubble img
max-height:340px as a blanket cap so no message can stretch the thread. Enhanced spike report to name
the tall bubble's img (natural size, rendered height, computed max-height) to confirm.
Build marker -> 2026-07-18-batch115.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 17:34:31 +05:30
Sravan 676612a12b fix(mobile): disable zoom via viewport meta (not font-size) + reveal msgs during slide
- Revert font-size:16px !important (it made the composer ~4 lines tall). Disable iOS zoom with
  viewport maximum-scale=1,user-scalable=no instead -> composer keeps its size, AND double-tap-to-zoom
  (in-chat search arrows #3) is gone. touch-action:manipulation on controls as belt-and-suspenders.
  Custom lightbox pinch still works (it's transform-based, not browser zoom).
- Transition: show the newest message immediately (was hidden until images loaded -> empty pane slid in
  then popped = the 'jumping' feel). Pinning still prevents dance.
Build marker -> 2026-07-18-batch114.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 17:27:54 +05:30
Sravan 78ea36f7d0 polish(mobile): native-style parallax chat-open transition; diagnose scrollH balloon
- Transition: open now slides the whole .content pane in over the list with a subtle parallax + iOS
  easing (was an instant/panel-only slide). Close/back already parallax.
- scrollH blip: added a spike watcher that reports the tallest element (class + img src tail) when the
  thread's per-message height balloons, so we can see what stretches then settles.
Build marker -> 2026-07-18-batch113.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 17:13:39 +05:30
Sravan 2cadf90bc8 fix(mobile): lift composer by Capacitor Keyboard plugin height (resize:none, vv doesn't reflect kbd)
Device log (post-zoom-fix): keyboard open leaves clientHeight AND visualViewport at 926 -> vv gives 0 ->
composer covered. On resize:none the plugin's keyboardHeight is the only signal; now that zoom is gone
it's clean, so --kb = keyboardHeight lifts the composer flush, and keyboardWillShow at animation start +
the CSS bottom-transition makes it slide up WITH the keyboard (smooth). Config pinned to resize:none.
Build marker -> 2026-07-18-batch112.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 16:46:00 +05:30
Sravan 9f7561bf8b diag+fix(mobile): capture keyboard-open composer position; replay open slide transition
- Debug now reports curKb/compBottom/kbTop/coveredBy on keyboard events so I can see WHY the composer is
  covered (lift coming out 0) from the server log.
- Transition: force the bzConvoIn slide to replay on every chat open (CSS animation only auto-plays on a
  class change, so chat->chat looked instant). Restart via reflow + inline animation.
Build marker -> 2026-07-18-batch111.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 16:23:22 +05:30
Sravan bf8df593a1 fix(mobile): force input font-size 16px with !important (was overridden by .composer-row .92rem)
Device log confirmed zoom persisted (vvW:394,vvTop:74) because .composer-row textarea{font-size:.92rem}
out-specified the generic rule, keeping the composer <16px -> iOS still zoomed on focus. !important
forces 16px on all fields so the zoom never fires. (#7 latest + budge already verified fixed.)
Build marker -> 2026-07-18-batch110.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 15:58:41 +05:30
Sravan 0e38bd9229 fix(mobile): stop iOS input auto-zoom (font-size 16px) = real keyboard fix; robust pin-to-newest
Device log revealed the truth:
- Keyboard: focusing the <16px composer made iOS ZOOM the page (vvW 428->394, vvTop->74), scrambling
  every viewport/keyboard calc. Set all form fields to 16px on mobile -> no zoom -> keyboard math holds.
- Latest messages: on a 500-msg chat atBottom was false because late images grow the thread after the
  initial scroll. Now re-pin on a schedule + on each image load while near the bottom.
- Budge already gone (device reported ovf:false).
Build marker -> 2026-07-18-batch109.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 14:17:20 +05:30
Sravan f9d6472f77 chore(mobile): add /api/dbg diagnostics sink + client reporter (build, overflow, viewport, scroll)
Temporary: the app POSTs its real runtime state to the server log so device-only bugs (budge source,
keyboard viewport numbers, chat-open scroll state) can be diagnosed from docker logs instead of
screenshots. Removed once mobile issues settle.
Build marker -> 2026-07-18-batch108.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 13:18:58 +05:30
Sravan 720b1f0c40 fix(mobile): revert keyboard to live VisualViewport tracking (no timer), undo budge; add unit test
- Keyboard: the 60ms timer in batch106 sampled the viewport MID-animation (clientHeight/visualViewport
  temporarily inconsistent) and over-lifted -> gap. Reverted to reading clientHeight-visualViewport LIVE
  on every vv resize, which is correct and self-adapting (unit test: residual=46 flush on the device).
- Budge: reverted the file <input> back to display:none (the position:fixed;left:50% hack overflowed
  the viewport horizontally).
- Config back to resize:native to match the installed build (works, no rebuild).
- Added test/keyboard-lift.test.js (5/5 pass) verifying the lift math against real device numbers.
Build marker -> 2026-07-18-batch107.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 10:22:45 +05:30
Sravan 6e3cf95deb fix(mobile): mode-adaptive smooth keyboard + force-latest-scroll + bottom-anchored file input
- Keyboard: auto-detect resize mode. resize:none -> lift full keyboard height at willShow so the CSS
  transition slides the composer up WITH the keyboard (smooth, no iOS resize lag). resize:native ->
  residual only. Config set to resize:none for the smooth path (needs build); web stays correct on the
  current resize:native build meanwhile.
- #7: force scroll-to-bottom at 0/120/320/600ms on open so a chat always opens on the newest message.
- File picker: anchor the file <input> to a fixed bottom spot (was display:none -> iOS dropped the menu
  mid-screen); now it comes up as a bottom sheet even as the composer moves.
Build marker -> 2026-07-18-batch106.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 08:32:23 +05:30
Sravan c775ca2740 fix(mobile): lock in device-independent keyboard (VisualViewport residual), remove debug
Debug confirmed resize:native shrinks the WebView by the keyboard height, so the composer only needs the
residual lift = clientHeight - visualViewport.height (46px on the test device — the prediction-bar sliver).
Tracked live via VisualViewport so it follows the keyboard. Zero per-device constants. Removed debug bar;
reverted Keyboard config to resize:native (which the logic relies on).
Build marker -> 2026-07-18-batch105.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 08:11:39 +05:30
Sravan 8867d0abfc fix(mobile): keyboard via VisualViewport (device-independent) + temp debug readout
Replace the hardcoded factor with the real overlap = clientHeight - visualViewport.height (works on any
screen). Adds a temporary on-screen debug line to confirm which signal the device reports, so the final
logic needs no per-device tuning. Debug removed once confirmed.
Build marker -> 2026-07-18-batch104.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 01:25:10 +05:30
Sravan c5489d7ad6 fix(mobile): scale keyboard lift by measured 0.52 (plugin over-reports ~2x)
Measured from device screenshot: composer at ~678pt from bottom vs 351pt keyboard = 2x overshoot.
Scale the reported height by 0.52 so the composer sits flush on the keyboard. Single tunable const.
Build marker -> 2026-07-18-batch103.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 01:19:49 +05:30
Sravan 77a3ad455f fix(mobile): normalize keyboard height device-px -> points (was overshooting ~3x on 3x screens)
Root cause: the Keyboard plugin reports keyboardHeight in device pixels on this build; CSS needs points,
so the raw value overshot by the devicePixelRatio. Divide by DPR when the raw value exceeds the screen's
point height. Web-only.
Build marker -> 2026-07-18-batch102.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 01:13:57 +05:30
Sravan 233db2c140 fix(mobile): keyboard lift via VisualViewport (measured gap, exact) not plugin height
The Keyboard plugin's reported height overshot. With resize:none the WebView stays full-size, so
window.innerHeight - visualViewport.height is the EXACT keyboard overlap -> composer lands on the
keyboard. Web-only, works on the resize:none build already installed.
Build marker -> 2026-07-18-batch101.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 01:05:12 +05:30
Sravan 357f0168ba fix(mobile): keyboard resize:none + web-tunable JS lift (deterministic, no plugin fight)
resize:native gave contradictory results (covered vs header) because the safe-area plugin fights it.
Set resize:none so the native layer does nothing, and lift the composer purely in JS by keyboardHeight
* KB_LIFT. Once resize:none is built, the lift is web-tunable with zero further builds.
Build marker -> 2026-07-18-batch100.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 00:55:17 +05:30
Sravan 9bee2c635c fix(mobile): remove redundant --kb lift; keyboard uses resize:native alone (was double-lifting to header)
Isolated test showed 'too high' = resize:native already lifts the composer, so the JS --kb added a
second lift up to the header. Removed it. resize:native is the single mover now.
Build marker -> 2026-07-18-batch99.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 00:50:18 +05:30
Sravan f55af18e90 fix(mobile): deterministic keyboard lift via --kb (single mover, no rebuild needed)
batch92 proved resize:native is a no-op with the safe-area plugin, and batch97 turned off the
safe-area keyboard offset, so re-adding the JS --kb lift makes it the ONLY thing moving the composer
= it sits exactly on the keyboard. Works on the current installed build (web-only).
Build marker -> 2026-07-18-batch98.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 00:45:06 +05:30
Sravan cc2a76450f fix(mobile): native keyboard (remove JS hack), open at latest + no double-render dance, splash cfg
- #3 keyboard: the overshoot proved resize:native DOES lift the composer; my --kb JS was a second lift.
  Removed the JS hack entirely; keyboard is now purely @capacitor/keyboard resize:native. Config: turn
  OFF safe-area offsetForKeyboardInsetBug so only one mechanism moves the view (needs the build).
- #7: gate the open-time hide/reveal with _convoRevealed so the cache->network double render doesn't
  flash/dance; on mobile open at the LATEST message (skip auto-scroll to first-unread).
- #6: add SplashScreen config (bg #16294F, no spinner) — asset gen already produces the logo.
Build marker -> 2026-07-18-batch97.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 00:17:58 +05:30
Sravan 763b2996d1 fix(mobile): revert app-breaking interactive edge-back (iOS touchcancel left UI stuck)
The finger-follow edge gesture starting at the very left edge collided with iOS's own edge-swipe,
which fires touchcancel instead of touchend, so cleanup never ran and .content stayed transformed +
chat-dragging stuck on top -> bottom nav hidden and all clicks blocked. Back to the safe release-based
swipe (still gets showWelcome's parallax slide). Keyboard --kb inset, #7 no-dance, #8 image swipe kept.
Build marker -> 2026-07-17-batch96.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 23:58:42 +05:30
Sravan 3393e44691 fix(mobile): JS-driven keyboard inset, interactive edge-back, finger-follow image swipe, no-dance
- #3 keyboard (was B: chat didn't move, keyboard covered composer): native resize wasn't moving our
  fixed layout, so drive it from JS — the Keyboard plugin reports its height, we expose --kb and lift the
  composer to sit exactly on the keyboard; keyboard config -> resize:none so native won't double-adjust.
- #4: the left-edge back is now INTERACTIVE — the conversation follows the finger with the list revealed
  underneath (parallax), commit past 1/3 width else spring back (like Teams). Falls back to release-based
  bzcBack for popups/search.
- #8: the image now follows the finger sideways (and down) during the swipe, not just on release.
- #7: hold the thread hidden until its images load (max 700ms) then reveal at the bottom — no dance.
Build marker -> 2026-07-17-batch95.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 23:11:54 +05:30
Sravan f2e2e8b08a fix(mobile): parallax slide-back, swipe images left/right, glue thread while images load
- #4: on back, reveal the chat list UNDERNEATH with a parallax slide-in while the conversation slides
  off to the right (slide the whole .content, list forced visible beneath) — matches the Teams push/pop.
- #8: swipe left/right in the image viewer flips to the prev/next image (not just swipe-down to close).
- #7: re-pin to the bottom as each image finishes loading (when near bottom), so late images don't
  reflow/'dance' the thread.
Build marker -> 2026-07-17-batch94.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 22:07:42 +05:30
Sravan e29810ea2a fix(mobile): un-clip popups, swipe-to-reply, swipe-down-close image, no msg dance, slide-back
Addresses reported iOS observations (web-side, no rebuild):
- #1: the overflow-x:hidden I added to stop sideways-scroll was clipping the profile/bell popup menus;
  now only the chat-list scroller is clipped and long content wraps instead of widening the page.
- #9: swipe a message bubble right to reply (WhatsApp/Teams style).
- #8: swipe an opened image down to dismiss the lightbox.
- #7: hide the thread for one frame on open so messages don't visibly scroll/'dance' into place.
- #4: slide the outgoing conversation off-screen on back (inline transform, reliable across WebKit).
Build marker -> 2026-07-17-batch93.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 21:34:15 +05:30
Sravan 2ce7ec72fb fix(ios): PERMANENT native safe-area + keyboard via Capacitor 7 + safe-area plugin
Device screenshots proved the WebView reports env(safe-area-inset-*)=0, so no web/CSS/JS trick can
move content off the Dynamic Island — it must be fixed natively. Upgrade the Capacitor shell to v7 and
add @capacitor-community/safe-area (populates env() with real per-device insets, edge-to-edge) and
@capacitor/keyboard (resize:native so the composer sits directly on the keyboard, like Teams).

- mobile: Capacitor 6 -> 7 for all plugins; add safe-area + keyboard; SafeArea/Keyboard config.
- codemagic: npm ci -> npm install (dependency set changed); drop stale Cap6 lockfile.
- web: remove the temporary StatusBar/34px hacks (env() now works); keep var(--sat)/var(--sab) padding.
- share/connect: back link now respects env(safe-area-inset-top/left).
- add native-style slide in/out (push/pop) animation when opening/closing a chat on mobile.
Build marker -> 2026-07-17-batch92.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 17:36:14 +05:30
Sravan a609bfd2e0 fix(ios): status-bar inset via StatusBar plugin, hide nav in open chat, bulletproof h-scroll
Root cause from device screenshots: the iOS Capacitor WebView reports env(safe-area-inset-*)=0,
so CSS padding did nothing and content slid under the Dynamic Island (every page: home/share/
connect/login) and the bottom nav was clipped.
- Use the already-installed StatusBar plugin: setOverlaysWebView(false) pushes content below the
  status bar natively (no rebuild). Applied on all entry pages via a small native-chrome snippet.
- Hard-code the bottom home-indicator inset (--sab:34px on html.native-ios) since env() can't give it.
- #2 keyboard: hide the bottom tab bar inside an open conversation so the composer sits directly on
  the keyboard (was sandwiched between composer and keyboard).
- #3 h-scroll: clip every scroll surface + overflow-wrap:anywhere so long content can't widen the page.
Build marker -> 2026-07-17-batch91.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 17:19:37 +05:30
Sravan 95e13a707e fix(ios/mobile): safe-area insets, no keyboard auto-open, no h-scroll, swipe-back, hide desktop-download
Reported on the iOS TestFlight build:
- #1/#5 notch/home-indicator: apply top safe-area inset per surface (chat list column +
  content panels) instead of on the whole shell, so headers sit below the Dynamic Island
  and nothing is cut off top/bottom.
- #2: opening a chat no longer auto-focuses the composer (was popping the phone keyboard
  and shoving the layout up); tapping the box still focuses normally.
- #3: overflow-x:hidden + max-width:100vw guards so pages can't pan left/right.
- #4: left-edge swipe-right now triggers the existing bzcBack() (close popup/search/open
  chat) — a native-feeling back gesture with no native code.
- #6: hide the 'Download app' (Windows) button when running inside the Capacitor app.
Build marker -> 2026-07-17-batch90.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 16:44:12 +05:30
Sravan c8f383cbcb fix(gif): send a GIF as the active reply; show GIF (not URL) in reply bar/quote (batch89)
sendGif ignored replyTarget, so picking a GIF while replying sent a plain message instead of a
reply. It now carries replyTo and clears the reply, like sendMessage. Reply bar + quote show
'GIF' instead of the raw media URL.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 15:54:31 +05:30
Sravan a172214c44 fix(gif): fixed-height grid rows so GIFs stop stacking over each other (batch88)
aspect-ratio:1 on the grid cells collapsed to ~0 height in the emoji-pop flex column, so the
images overflowed and overlapped. Use grid-auto-rows + a fixed cell height instead.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 15:46:05 +05:30
Sravan 02075fbd47 feat(chat): GIF picker via server-proxied GIPHY (#5) (batch87)
- Server: GET /api/gifs proxies GIPHY search/trending. The API key is read from the server
  env only (config.GIPHY_API_KEY, from the gitignored .env) and NEVER reaches the browser;
  the picker is hidden when it isn't configured.
- Emoji picker gains a GIF tab (separated from the emoji categories) with a search box + a 2-col
  grid, "Powered by GIPHY" attribution. Clicking a GIF sends it immediately.
- GIFs are HOTLINKED to GIPHY's CDN (their terms require this — no re-hosting): the message body
  is the GIF url, and a body that is a lone GIF url renders inline as the animated GIF (reusing
  the image/lightbox path). Sidebar previews + notifications show "🎞️ GIF", not the raw url.

Key is NOT in git — set as GIPHY_API_KEY in the server .env.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 15:30:44 +05:30
Sravan c3dc47a94c fix(meetings): Past shows only scheduled + instant meetings; ad-hoc calls hidden w/o recording (batch86)
Revised rule per user: a call is not a meeting. Past meetings now = real SCHEDULED meetings +
INSTANT meetings (Start-a-meeting, logged in call_history as adhoc). Ad-hoc CALLS — 1:1 direct,
group calls, and a 1:1 that a 3rd person joined — are shown ONLY if they produced a
recording/transcript. Dropped the earlier ">2 participants shows it" exception.
Also stop rendering a bare "Host: —" on logged call cards.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 14:58:48 +05:30
Sravan f708cc2f12 fix(meetings): hide empty 1:1 direct calls; date from/to validation + field alignment (batch85)
#3 The 136 "Direct Call" cards: calls.js writes a scheduled_meetings row for EVERY call
   ("Direct Call" / "Group call"), which is separate from the call_history table. Those rows
   came through the scheduled-meetings path and my earlier filter never touched them. Now an
   auto call-history row is shown in Past ONLY if it produced a recording/transcript OR the
   call ever held >2 people (peak from the call log); plain 1:1s with neither are dropped.
   Removed the now-redundant callRows synthesis. Recordings stay attached (rows WITH a
   recording are always kept).
#4 Date range: from ≤ to enforced by disabling out-of-range days in each picker (can't pick a
   from after to, or a to before from). Filter controls share one height/baseline so the
   calendar icon, preset dropdown and date fields align cleanly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 13:19:12 +05:30
Sravan fad8a52da4 fix: last-seen backfill, report-style date filter, hover overflow (batch84)
#1 Last seen still read "Offline": last_seen is a NEW column, so every existing user was NULL
   until they happened to reconnect. Backfill it from the newest message each user sent (best
   evidence we already have), and also stamp it on message send — not just on connect/disconnect
   — so it stays fresh if a socket never closes cleanly.
#3 Past-meetings filter is a report filter now: presets (Today, Yesterday, Last 7/30 days, This
   month, Year to date, All time) plus a Custom range that uses the SAME branded calendar popup
   as the scheduler. The raw <input type=date> looked foreign and behaved differently per browser.
#4 Hover action row overflowed off-panel on SHORT received messages: it's wider than the bubble
   and was anchored to the bubble's right edge, so it ran off the left. Received bubbles now
   anchor it from the left (growing into the empty space); own messages keep the right anchor.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 01:02:59 +05:30
Sravan 6fa261b68f feat(meetings): call log, past pagination + date filter; last-seen exact time (batch83)
#7 The past list was hard-capped at 12 (a .slice(0,12) in the client) and there was NO record
   of how many people were ever in a finished call — so the "only calls with >2 people" rule
   was impossible to apply. Added a call_history table: signaling tracks the HIGH-WATER
   participant count per room and logs the call when the room tears down (scheduled meetings
   are skipped — they already have their own row).
   Past meetings now follow the rules asked for:
     • a plain 1:1 direct call is NOT listed — unless it produced a recording/transcript
       (those already surface as recording entries);
     • a call that ever held MORE than 2 people IS listed (e.g. a 1:1 a third person joined),
       showing its participant count and duration;
     • entries are visible only to people who were actually in the call (or the group).
   Server-side pagination (10/page) + a from/to date filter; nothing is double-listed.

#2 Last seen now shows the exact time/date, WhatsApp-style — "last seen today at 1:36 PM",
   "last seen yesterday at 10:15 AM", "last seen 14/07/2026 at 9:00 AM" — instead of "10
   minutes ago".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 00:42:53 +05:30
Sravan 860a7bd6cf feat(chat): emoji speed, action rework, copy, phone links, image zoom, upload %, profile card, last seen (batch82)
#6 Emoji were slow because Twemoji swapped EVERY emoji for an <img> fetched individually
   from a CDN — opening the picker fired hundreds of image requests. Now uses the OS's own
   colour emoji font: instant, zero network. twemojify() kept as a no-op.
#4 Message hover row reworked: three one-tap reactions (Like/Laugh/Surprised) + the emoji
   picker + (own) Edit. Reply / Forward / Copy / Delete moved behind a ⋮ menu. Added Copy.
   The row now sits FULLY above the bubble (was top:-14px, overlapping the first text line).
#9 Phone numbers linkify to tel: — mobile gets the OS "call this number?" prompt; desktop has
   no dialer so it offers to copy. Regex kept conservative (10–15 digits, needs +/grouping) so
   it won't grab amounts, dates or 6-digit meeting codes.
#3 Image preview zooms: wheel + pinch + double-click + ± buttons, drag to pan, keys (+/-/0),
   cursor-anchored. Arrows hide while zoomed so panning isn't hijacked.
#8 Upload progress: fetch() can't report upload progress at all, so a large file just said
   "uploading…". Switched to XHR (upload.onprogress) → real bar + %, and cancel aborts in flight.
#1 Clicking a sender in a group opens a mini profile card (photo, presence, last seen) with a
   Message button that opens the 1:1 (and a view-photo button).
#2 Last seen: new users.last_seen column, stamped on connect and when the last socket drops;
   carried on the presence broadcast, so an offline contact reads "Last seen 10 minutes ago"
   instead of a bare "Offline".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 23:46:52 +05:30
Sravan 667b4c69d5 perf(update-check): throttle + single-flight the build check (batch81)
The check itself is cheap (one ~35-byte response, served from a variable the server reads
once at startup), but it was hooked to BOTH `focus` and `visibilitychange` with no throttle —
and a single alt-tab back into the app fires both, so every refocus cost two redundant
requests. Now: one check per minute at most, never overlapping itself, and it stops checking
entirely once a new build is known (the retry loop takes over).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 16:14:50 +05:30
Sravan c1f01e796a fix: DP missing in 1:1 for un-messaged contacts; make web updates fully silent (batch80)
DP bug — real cause found (client-side, not the DB):
loadSidebar builds a DM row for every contact you haven't messaged yet, but it copied only
{name, online} from the contact and DROPPED `avatar` (and status/email). So an un-messaged
contact always rendered initials in the 1:1, while the SAME person showed their photo in a
group (which reads /api/groups/members). Carry the whole contact through.
Kept a server-side safety net: avatarsFor() now indexes known photos under person-id, email
AND name, so a duplicate row missing a photo can match on any of them (the previous single
composite key missed twins with different emails).

Web updates are now completely silent: no banner, no toast. A web build is an implementation
detail — surfacing it makes users reason about "web build vs app version", which is exactly
the confusion to avoid. New code simply applies itself as soon as it's safe.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 16:06:28 +05:30
Sravan 2aeeb0a096 fix: auto-apply new web builds; DP missing in DM but shown in group (batch79)
Auto-update (no manual step, no version confusion):
- New code now applies ITSELF. The client polls /api/build and, as soon as it's SAFE,
  silently hard-reloads onto the new build. Safe = not in a call, no live screen session,
  no dialog open, nothing half-typed; if the user is busy we wait and apply the moment
  they're free. A brief "Updated to the latest version" toast confirms it.
- Removed the "web build" row from Settings: users must never have to reason about an app
  version vs a web build. The only version surfaced is the desktop app's (auto-updater).

DP bug: a contact showed their photo in a GROUP but fell back to initials in the 1:1.
Two causes, both handled:
- Duplicate rows for one person (signed in by email once and by mobile another time before
  the bizgaze_user_id merge landed) — only one row carries the DP, and the group happened to
  reference the row WITH the photo. avatarsFor() now keys rows by stable person identity
  (bizgaze person id → email → name) so a photo-less row borrows its twin's photo. Applied to
  contacts, conversations, group members and group info.
- A DM whose counterparty was merged away is now keyed by the SURVIVING account, so the row
  carries that account's name/photo/presence (and split threads collapse into one).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 15:34:35 +05:30