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>
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>
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>
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>
- 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>
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>
#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>
#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>
#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>
#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>
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>
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>
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>
Root cause of "the fix works on mobile but not on desktop/web": nothing was wrong with
the code — the desktop app now CLOSES TO TRAY, so it can run for weeks on the page it
loaded on day one and never re-fetch after a deploy. There was also no way to force it
off a stale page (no menu bar → no reload accelerator).
- Server: GET /api/build returns home.html's __BUILD marker.
- Client: polls it (boot, on focus/visibility, every 10 min); when the server's build
differs from the running one, shows a branded "A new version is available — Refresh"
banner. Settings gains an always-available "Refresh app" with the current build shown.
- hardReloadApp(): in the browser it unregisters service workers + clears CacheStorage
then reloads cache-busted; in the shell it calls the native hard reload.
- Desktop: hard-reload IPC (clears the session HTTP cache + reloadIgnoringCache), wired to
Ctrl+R (reload), Ctrl+Shift+R / F5 (hard reload), and a "Refresh app (get latest)" tray
item. Previously there was literally no way to clear the cache from the app.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
#6 Pull-to-refresh genuinely broken: the indicator lived INSIDE the scroller (so it
scrolled away unseen) and only became visible after ~96px while the trigger fired at
70 — and preventDefault was deferred until the pull passed 10px, by which point
iOS/Chrome had already claimed the gesture. Indicator now mounts on the list's
non-scrolling parent with the branded loader, the gesture is claimed immediately, and
the trigger is reachable. Works on Android + iOS.
#4 ⋮ More now TOGGLES (it used to remove then instantly rebuild, so it never closed).
#3 "Ringing…" is centred over the tile with the avatar/name dimmed behind it.
#2 Meeting bar fits the screen on mobile: one row of controls sized to the viewport,
room code on its own line, safe-area padding.
#5 Search box in the in-call Add-people tab.
#1 Speaker control: setSinkId (the only web API for audio output) is NOT implemented in
Android Chrome or iOS Safari — the OS owns the route there, and iOS forces loudspeaker
whenever a mic track is live. Rather than ship a button that silently does nothing, it
now only renders where output switching actually works. Real speaker/earpiece/Bluetooth
switching on phones needs the native (Capacitor) audio plugin.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Regressions I introduced, now fixed:
#8d Mic/Camera OFF went DIM — my new `.meet-ic.off` rule overrode the original RED.
Scoped the lite style to the speaker button only; mic/cam OFF are red again.
#7 Guest "left the meeting" was unstyled (dark text on blue, no card): the card CSS was
scoped to .guest-prejoin only. Shared with .guest-left. Rejoin no longer dead-ends —
if the room is gone we say "This meeting has ended" and hide the button.
Reported:
#8d Speaker button no longer shows on desktop (devices already live under the mic ▾).
#8c/#8e Mobile speaker button now CYCLES Speaker → Earpiece → Bluetooth (icon follows the
route); dropped the redundant "Audio devices" entry from the ⋮ menu.
#8a iOS gap under the bottom rail: the page rubber-band-bounced, exposing background beneath
the fixed bar. overscroll-behavior:none + fixed body pins it.
#8b Pull-to-refresh now fires on iOS too (overscroll-behavior:contain on the lists so Safari's
rubber band stops swallowing the gesture; scrollTop>2 tolerance for momentum).
#5 A meeting link no longer opens a whole new window: same-origin urls navigate the main
window in the shell, and a link clicked in chat joins the meeting IN PLACE.
New observations:
1. The viewer's mic now starts MUTED on a screen session (and can actually be un/muted).
2. iOS lightbox close/download buttons moved below the Dynamic Island (safe-area insets).
3. Meeting chat: recipient picker moved to the BOTTOM next to the input; a private message
auto-targets your reply back to that person; private vs everyone are visibly different
(brand amber vs blue/neutral) and the compose area tints in private mode.
4. Meeting bar + screen-session bars are DRAGGABLE (position remembered) so they stop
covering the shared screen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
#4 Participants list only ever showed "you": in SFU mode meetMakePeer drew a tile but
NEVER added to meetPeers, so the roster (and the meeting-chat "To" list) was empty.
#5 Same person as several tiles: each tab/reload/reconnect gets a new peerId, leaving
ghost peers. One uid = one tile now (dropDupPeers keeps the newest).
#1 Remote control needed a login: /share opened top-level (the no-login "Share my
screen") has the preload bridge RIGHT THERE, but we only postMessage'd to a parent
frame that doesn't exist — so control never armed. Use the native bridge directly.
#2 Sharer's session bar moved to the shared Lucide icon set (was old inline SVGs).
#3/#6 Device menu: collapse Windows' "Default -"/"Communications -" triplicates into one
clean list (single "System default" + each real device once), and actually re-apply the
saved mic/speaker to the room on connect so a picked headset/Bluetooth is used.
#7 Guest "left the meeting" screen rebranded to match the new pre-join.
#9 Tapping a member in group info opens the private conversation with them.
#8a iOS bottom rail: border-box + height incl. the home-indicator inset, px env() fallbacks
(unitless 0 breaks calc() in Safari), promoted layer so it stops drifting.
#8c/#8e Mobile meeting bar: Mic / Camera / Speaker / End only; screen, record, transcript,
chat, participants move behind a ⋮ More menu. No mic device dropdown on mobile.
#8d Speaker button reflects the real route: speaker-on / speaker-off (dimmed) / bluetooth
when a headset is in use; re-detected on devicechange.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1. Guest pre-join redesigned: brand backdrop, live camera preview, mic/cam toggles
applied on entry, initials avatar, name required, "host may admit you" note.
2. Post-admit bug: the guest stayed on "Waiting for the host…" forever — the lobby
screen had replaced the call UI and meeting-joined never re-rendered it. Now it
rebuilds the call on admission (_inLobby).
3. Audio devices: dropped the standalone headphones button. The MIC now has a ▾ caret
opening one Teams-style menu with Speaker + Microphone sections (radio-selected);
speaker uses setSinkId/LiveKit switchActiveDevice, mic switches the live input.
Mobile gets a speakerphone toggle that prefers a connected BT/headset when off.
4. Remote-control keyboard:
- Injector now maps the PHYSICAL key (KeyboardEvent.code) instead of the character,
so Shift+1 types "!" etc. Character mapping was why typing "performed differently".
- Keys reach the sharer ONLY while control is ENGAGED (window focused AND you clicked
their screen). Minimised/unfocused/chat typing stays local. Esc or clicking away
releases; modifiers are released on disengage so nothing sticks.
- Explicit control icons: viewer gets a Control ON/OFF button (green when engaged) +
an on-screen hint; the SHARER gets a control icon beside mic/chat to allow/stop
access at a glance, synced with the consent dialog and banner.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds an ephemeral chat inside meetings (relayed over the meeting signaling socket,
not persisted). A chat button in the meeting bar (with an unread badge) opens a
brand-styled panel: pick "Everyone" or a specific participant (private), send with
Enter/Send. Direct messages are marked private on both sides. Works for guests too.
Chat state resets when you leave the call.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
General #1/#2 (closed-app notifications): the desktop app now CLOSES TO TRAY instead
of quitting, keeping its chat WebSocket alive so calls/messages still notify. Tray
icon + menu (Open / Quit), single-instance lock, first-close hint.
Guest #4 (lobby/admit): meetings can require the host to admit guests joining by link.
Setting on the schedule form ("Guests must be admitted by the host", default on) +
ad-hoc default. Guests wait on a "waiting to be let in" screen; the host gets an
Admit/Deny prompt; auto-cleanup on leave. Logged-in members always join directly.
Guest #5 (speaker): headphones/speaker output picker in the meeting (setSinkId),
remembered and applied to every tile.
Guest #3 (link expiry): guest link/token dies ~2h after a scheduled meeting's end
(HTTP 410) with a clear message; live-room links expire when the room empties.
RC #4 (mobile): touch→mouse mapping so a phone/tablet viewer can control (tap=click,
drag=move). Uses the same letterbox-correct coordinate mapping.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Remote control (connect.html viewer):
- Coordinate offset fixed: map clicks to the actual video CONTENT rect (object-fit
letterbox-aware), not the element rect — cursor now lands where you click.
- Keyboard: capture at document level while a session is live (the <video> lost
focus on bar clicks → keys did nothing). Skips the chat input.
- Control bar moved to bottom-right with tiny modern icons; video fills the viewport.
- Smoother cursor (mousemove ~60/s).
Guest meetings:
- Guest mic inaudible + guest invisible in the participant list + outsider screen
share not showing (#1/#8/#11): root cause was the SFU media→tile map keyed on
identity==uid, but guests had a random LiveKit identity and a null signaling uid.
Guests now carry ONE stable id across signaling (meeting-join guestId) and the
LiveKit token identity, so their media attaches and they appear to everyone.
- Guests can't add participants (#9) and don't see the transcript button (#12).
- Search box in the schedule participant list (#10).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fixes the core "viewer can't control the sharer even on desktop" gap. Three root
causes addressed:
- share.html DISCARDED every input-channel message (onmessage=()=>{}). It now
parses the viewer's mouse/keyboard events and forwards them to the desktop shell.
- The main desktop app had NO OS injector (it lived only in the separate agent).
Ported the nut-js injector (agent/input/inject.js) into desktop/input, wired an
inject IPC + injectInput bridge, HARD-gated behind a consent flag (rcArmed).
- /share runs in an iframe (no direct bridge access) → it postMessages input to
the top frame (home.html), which relays to the native bridge.
Consent + safety: the sharer sees an Allow/Deny prompt the first time the agent
interacts; while active a persistent "your screen is being controlled — Stop"
banner; instant revoke; auto-release on session end/teardown. Browser sharers stay
view-only (no OS injection possible). nut-js is an optionalDependency (N-API, ABI-
stable across Electron) — degrades to no-op if the native module is unavailable.
Windows-first; maps to the primary display.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
#4: scheduled meetings can now invite people who aren't on Connect.
- SMTP config (config.js, env-gated: SMTP_HOST/PORT/USER/PASS/FROM/SECURE,
PUBLIC_BASE_URL) + a small nodemailer wrapper (mailer.js) with a branded
meeting-invite template carrying the guest join link. No-op until SMTP is set.
- /api/meetings/schedule + /update accept participantEmails; external emails are
persisted (scheduled_meetings.guest_emails migration) and emailed the guest
link (plus any invited Connect users with an email on file). Fire-and-forget —
a mail outage never fails scheduling.
- Meetings list DTO returns `link`; schedule form gains an "Invite by email"
chip input; each scheduled-meeting card gets a "Copy link" action.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Server: POST /api/meetings/guest-token issues an unauthenticated LiveKit token
for a room that is currently live or a valid scheduled meeting (throwaway guest
identity). Mesh signaling already tolerates anonymous peers.
- Client: opening /home?meet=CODE while signed out runs a lightweight GUEST mode —
name prompt, then straight into the meeting with the full meeting client and the
chat/sidebar chrome hidden. Signed-in users who open the link auto-join.
- sfuConnect uses the guest token for guests. Guests get a friendly "left the
meeting / rejoin" screen (no chat to fall back to).
- Meeting "Add people" panel gains a "Copy invite link" (copyMeetingLink) that
copies the guest link — foundation for the emailed invites in #4.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Contact search no longer lists BizGaze directory people who aren't on Connect as
addable rows (they can't be messaged). It shows only people actually on Connect;
if a search matches nobody on Connect it shows either "Not on Connect" (they exist
on BizGaze but haven't signed in) or "Contact not found".
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1. Group message toast had no icon: the generated group icon is a data: URL, but
the 0.1.11 handler only used ALREADY-cached icons and skipped it. data: URLs
are synchronous — use them immediately, so every group toast shows an icon.
2. Spell suggestions on a LEFT click: clicking a red-squiggled word in the message
box now pops the native suggestions menu (renderer asks the shell to synthesize
a right-click at that point → real dictionary suggestions). No right-click.
3. Scroll felt "stuck" because an incoming message yanked you to the bottom even
when you'd scrolled up to read history (worse now that bg messages arrive live).
appendBubble now keeps your position unless you were at the bottom / it's your
own message, and surfaces the "jump to latest" control instead.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1. Scroll felt "stuck": the seen-by card had TWO nested scroll containers
(.modal overflow-y:auto + inner .seen-scroll) fighting each other — card is
now overflow:hidden so only the inner list scrolls. Global scrollbar widened
6→11px, dropped the inset border, added a min thumb length so it's easy to
grab and drags smoothly everywhere.
2. Group message toast now carries a generated group icon (initials on a brand
disc) when the group has no photo — was iconless.
3. Seen / Not-seen split by a visual divider in the message-info popup.
4. Auto-correct: curated common-typo dictionary applied as you type (on a word
boundary) — corrections happen automatically, no right-click. Web/desktop/mobile.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1. Message-info popup: ONE scroll region for the whole list with sticky
Seen / Not-seen headers (was a separate scrollbar per section), realigned.
2. Group message notifications now title = GROUP name, body = "Sender: text".
3. Unread taskbar badge: solid red rounded-square (Teams-style) + white outline
instead of the gradient disc.
4. Pasting Excel/Sheets cells no longer uploads a screenshot — when the clipboard
carries real text, the text is pasted; image-only clipboards still upload.
5. Spell check in the message box (red squiggles) with a right-click menu of
corrections + add-to-dictionary; spellcheck enabled on the textarea.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1. Notifications/messages lagged when the desktop window was backgrounded —
Electron throttles hidden renderers by default, stalling the chat WS's
onmessage + timers. Set backgroundThrottling:false → real-time in the tray.
(Transport is WebSocket; no Firebase needed.)
2. Joining a call via the header Join button left the Join/Decline invite popup
on screen — enterMeeting now dismisses the invite (and stops the ring).
3. Seen-by is now "Message info": real contact DPs, a "Not seen yet" list for
groups, and dark lettering (white-on-pastel initials were invisible).
4. App-icon unread badge: red→pink gradient pill with a white ring + shadow,
drawn at 2x, instead of a flat red disc.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Root cause of the persistent "notification opens a dead page / no Join" bug:
openFromNotif gated the in-place open on `window.ME`, but ME is declared with
`let` — which never creates a global property — so window.ME was ALWAYS
undefined and every click hit the full-page-reload fallback. Use bare `ME`.
Also:
- Notifications fire instantly: never block on the DP download. Use the photo
only if already cached; warm the cache in the background + pre-warm all
contact DPs on chat load (precache-avatars IPC). Removes the ~2.5s lag.
- Wire call Join/Decline handlers BEFORE firing the OS notification so Join is
live the instant the invite popup appears (was dead until the toast settled).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Custom branded screen/window picker (picker.html) — useSystemPicker was
silently no-op on Win11 and auto-shared the primary display with no choice.
- Use checkForUpdates (not ...AndNotify): drops electron-updater's own native
"Update ready" toast that duplicated our in-app banner (two restart prompts).
- Call notification DP: wait up to 2.5s for the caller photo on persistent
(call) toasts instead of 600ms so the DP actually shows.
- Re-surface Join/Decline invite when opening a chat with an active incoming
call, so a call-notification click always lands on a joinable call.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- #2: 'Seen by' opens an on-screen popup listing readers (was a flash toast).
- #5: the DM contact-info view now shows the DP; clicking it previews full-size.
- Desktop notifications: cache DPs per-sender (fast AND with photo after the first);
removed timeoutType:'never' which added an unwanted 'Close' button.
- Update flow: dropped the unbranded native restart dialog — the branded web banner
handles Restart. Banner text clearer ('Downloading update…'), plus an update
indicator that cascades profile 'i' badge → Settings → version line, with the
Settings button becoming 'Restart now' when the update is downloaded. desktop 0.1.7.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- #1: URLs in chat are now clickable links (open externally on desktop).
- #2: group 'Seen by X, Y +N more' now shows under the LATEST message to EVERYONE
(server computes reads for all messages; click shows the names).
- #5: tapping the conversation-header avatar previews the DP/group photo full-size.
- #4: in-call invite lists only people you've messaged, excludes those already in the
call/invited, and the Invite button is sticky.
- #3 (0.1.6 shell): auto-updater emits checking/available/downloading/ready/error to
the web UI, which shows a progress banner (+ Restart button) so updates aren't silent.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Desktop notifications were slow (~15s), vanished in ~1s, and clicks did nothing:
* don't block the toast on the avatar download (race a 600ms cap) → shows instantly
* keep a strong reference to each Notification (Electron GC'd them → premature close
+ dead click)
* call invites use timeoutType:'never' + a 45s window so they stay until clicked/ended;
web marks call notifications persistent.
- #9: enable the OS screen/window PICKER (useSystemPicker) so users choose what to share
(a single window avoids the whole-screen mirror); falls back to primary display.
- desktop 0.1.5 -> 0.1.6.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- #9 REAL cause: the tile was forced to avatar whenever the peer's CAMERA was off
(addTile + meeting-peer-state), so a screen-share only appeared if the camera was
also on. Now camera-off only shows the avatar when the peer is NOT sharing a screen.
- #9: screen-share button is highlighted (blue) while active, like mic/cam/record.
- Unanswered call text is viewer-relative: the caller sees 'Call not answered', the
callee sees 'Missed call'.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- REGRESSION (More#1): removed overflow-anchor:none (it stopped the view from staying
at the bottom as images settle) + renderThread re-asserts scroll-to-bottom after late
content, unless a 'New messages' divider is shown → chats open at the latest message.
- #9: dynacast off — a screen-share layer was being paused unless a camera track was
also flowing (blank / only-with-camera / slow). Now every published track flows.
- More#7/#9: 1:1 calls track 'answered'; unanswered calls auto-end after ~40s (caller
no longer stuck ringing) and post 'Missed call' instead of a duration; answered calls
show duration from the answer time. meeting-ended reason 'unanswered' → 'No answer'.
- More#6: selected chat now has a thin yellow (brand) boundary.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- #9: sharing your whole screen no longer creates a 'hall of mirrors' — your own
screen is NOT shown back to you (you see your camera/avatar + a 'You're sharing'
badge); other participants still see your screen. Your own tile is never the local
stage.
- #1: opening a DM now syncs the sidebar tick to the thread's true delivered/read
state, so it can't show a single tick while the conversation shows double.
- #3: nativePlatform() now also detects older desktop builds that expose
bizConnectNative but not __NATIVE__. Those were wrongly subscribing to Web Push, so
the service worker's openWindow RELOADED the page on notification click (losing the
header DP). Desktop now uses only the native toast → opens in-place.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- #7: outgoing 1:1 call shows the person you're calling (DP + name + 'Ringing…')
instead of only your own tile; removed when they answer or the call ends.
- #8: declineDmCall now ignores the decline if that user has ALREADY accepted on
another device (they're in the room) — declining the ringing invite on a 2nd
device no longer tears down the active call.
- #9: sfuAttach retries (bounded) when the uid→peerId map lags the LiveKit track,
so a shared screen isn't silently dropped due to a race.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- #4: forward modal buttons now branded (bzc-* un-scoped from .bz-confirm); target
rows get real checkboxes + border highlight; selected MESSAGES highlight in green
with a filled tick (distinct from blue bubbles), checkbox on every message.
- #5: forwarded messages show an italic 'Forwarded from <origin>' label; new fwd_from
column preserves the true origin across re-forwards.
- #6: image-preview arrows are smaller (32px) chevron icons, clear of the image
(image max 82vw); icons.js?v=5.
- #10: image preview now opens ABOVE the media modal (z 9900) and Esc/close returns
to the media view instead of closing everything.
- #11: Media/Links/Docs active tab underline is brand-blue, not green.
- #2: chat-list + conversation scrollbars 6px, stepper arrows hidden; pagination adds
a cooldown + overflow-anchor:none so scrolling up no longer sticks/jumps.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When you open/read a conversation, the server now also pushes a notif-clear to your
OWN other sockets. Each device tags notifications by conversation (kind:id), so on
notif-clear it closes the matching page Notification + any Service-Worker (Web Push)
notifications, and drops matching activity-center entries so the bell badge stays in
sync. (Desktop Electron native toasts are transient/auto-expire; the web+PWA surface
is covered.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Settings now shows 'Biz Connect for Desktop · Version x.y.z' with a Check for
updates button (desktop only, feature-detected).
- preload exposes checkForUpdates(); main.js adds the check-updates IPC (returns
available/current/dev/error) and, when a build finishes downloading, shows a
Restart now / Later dialog instead of only the silent on-next-launch install.
- desktop version bumped 0.1.4 -> 0.1.5.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Message action pill gains a Forward button; tapping it enters selection mode
(tap bubbles to multi-select, footer bar shows count + Forward/Cancel, Esc exits).
- Forward picker lists EXISTING conversations only (DMs + groups from the sidebar),
searchable, multi-target. POST /api/messages/forward copies body+attachment into
each target (authorized as participant/member), live-pushed like a normal send.
- /files auth now accepts ANY message carrying an attachment (allByAttachment), so
forwarded images stay viewable for the new recipients.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- #8: reply quotes are now clickable — jump to the original message (paging older
history in if needed), then flash it. reply DTO carries the target's timestamp.
- #3: image lightbox now has ← / → arrows + keyboard nav to flip through all images
in the conversation.
- #5: the composer queues MULTIPLE files (file input is multiple; paste still works);
each is shown as a removable chip and sent as its own message (first carries the
typed text as caption), in order.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- #2/#7/#19: openFromNotif opened chats via a full-page reload (location.assign),
which was slow AND dropped live state — the sidebar hadn't reloaded so the header
showed 'Conversation' with no DP, and an incoming call's Join/invite popup was lost.
Now it opens the chat IN-PLACE (selectChat) since the app is already running;
full-page nav only as a fallback when the app isn't initialised.
- #9: slimmer 6px scrollbar; explicitly hide all webkit stepper-arrow variants.
- #16: 1:1/small call no longer shows a stray scrollbar — meet-grid centers tiles
and clips overflow (call UI fits the viewport).
- #17: caller now hears a gentle ringback while waiting; stops on answer, on any
call exit, and auto-stops after 45s so it never rings forever.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pull-down-at-top called reloadThread (reload newest 500 + scroll to bottom), so
reaching the oldest message yanked the view to the newest — jarring, especially on a
single page. Now the pull gesture calls loadOlder: it pages in older history keeping
scroll position, or no-ops when there's nothing older. Never jumps to the bottom.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Older messages (beyond the newest 500) now load as you scroll to the top — the
thread endpoint takes a ?before=<created_at> cursor, the client prepends the older
page and preserves scroll position (renderThread keepScroll). _hasMoreOlder stops
paging when a short page returns.
Search now covers the ENTIRE thread, not just the loaded window: new
/api/messages/search (DM + group, LIKE with escaped wildcards) returns all matching
message ids; the client debounces the query, and jumping to a hit older than the
loaded window pages history back (ensureLoadedBack) until the match is in view, then
highlights + flashes it. Cap raised to 500.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
THE disappearing-messages root cause. openConvo did: let msgs=[]; try{ msgs=fetch()
}catch{}; THREAD = Array.isArray(msgs)?msgs:[]. On a flaky desktop network a failed
fetch left msgs=[] (still an array) → THREAD=[] AND THREAD_CACHE.set(ckey,[]) — so
reopening a chat blanked it AND overwrote the cache, making messages vanish and stay
gone even though they persist server-side (confirmed: 982 DMs, zero dangling ids).
Now msgs stays null on a failed/non-OK fetch; we only replace THREAD/cache on a real
array response, and otherwise keep the cached render instead of blanking.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Broken images: UPLOADS_DIR/REC_DIR/TRANS_DIR were server/<dir> INSIDE the image,
so every deploy.sh rebuild wiped uploaded files — old images 404'd ('broken
image') though their DB rows survived. Make them env-overridable and point prod
at /data/uploads|recordings|transcripts (persistent volume), matching DB/downloads.
NOTE: files already lost to prior rebuilds can't be recovered; new uploads persist.
- Duplicate notifications: harden connectChatWs — close/detach any prior socket
before opening a new one and keep a single pending reconnect timer, so a flaky
reconnect can't leave two live sockets delivering every event/notification twice.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
batch42's resync-on-reconnect reopened the conversation via openConvo, which
replaces THREAD wholesale. On the desktop app (flaky WS), a reload racing with a
just-sent message could momentarily blank the chat ('messages disappear after
sending', seen on the Manasa chat). resync now MERGES: adds messages missed while
disconnected + refreshes read/delivered/seen/edited/deleted flags, but never
removes messages already on screen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- #10/#18/#7: on WebSocket RE-connect, re-pull sidebar + re-open the current
thread so the desktop/mobile apps recover missed messages/ticks without a
refresh (the old onopen only re-sent chat-hello).
- #11: branded bzConfirm() dialog replaces window.confirm for delete (the OS/
Electron default dialog looked off-brand on desktop).
- #4: global Esc no longer closes the conversation while an image preview is
open — the preview closes first, the conversation on a second Esc.
- #6: live 'typing…' now also shows inside the conversation panel (animated),
not only the header/sidebar.
- #15 (diagnostic): mic/cam failures now report the REAL cause (permission vs
no-device vs in-use) with desktop-specific guidance, and log the raw error,
instead of always saying 'permission required'.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>