Skip to content

Concord V2-#71

Open
alexgleason wants to merge 428 commits into
VectorPrivacy:masterfrom
alexgleason:v2
Open

Concord V2-#71
alexgleason wants to merge 428 commits into
VectorPrivacy:masterfrom
alexgleason:v2

Conversation

@alexgleason

Copy link
Copy Markdown

This is a few prompts of Fable 5, which I have not tested at all. This is an idea of how it might look.

JSKitty and others added 30 commits May 20, 2026 00:55
In-panel square cropper replaces the square-or-reject rejection for
non-square uploads. Static formats (PNG/JPEG/static-WebP) decode +
re-encode in place; animated formats (GIF, animated WebP) decode
per-frame, crop each, and re-encode preserving per-frame durations.
Animated WebP cumulative timestamps are reconstructed from the
decoder's per-frame deltas.

UX: drag-pan inside the box, 1:1-locked corner resize, marquee draw
from any image-rect pointerdown, live preview chip that animates the
post-crop result. Pointer capture on the stage unifies all three
gestures into one event channel. Esc cancels, Enter confirms.

Emoji cells gain a long-press / right-click context menu (Rename /
Delete) and the hover-× delete badge is hidden on mobile — the gap
that previously left no path to delete emojis on touch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Kind 10030 is now published with an empty public `tags` array — the
entire subscription set lives inside the event's NIP-44-self-encrypted
`content` as a JSON array of `["a", "30030:pk:d"]` tuples. Only the
user's own signer can decrypt and reconstruct the pack list, so peers
scraping relays see opaque ciphertext instead of a public list of
which emoji packs the user follows.

The read path mirrors the write path: kind 10030 with empty content,
or content that fails to decrypt, is treated as no subscriptions
rather than failing the whole refresh. No public-tag fallback —
Vector treats the emoji list as fully private by design.

Encrypted-items schema matches what NIP-51-aware clients (amethyst,
snort) use for mute lists, keeping cross-client compatibility for any
client that adopts emoji-pack support.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
square thumbs

- Picker hover (native CSS + canvas): background highlight alpha
  dropped 25%, scale upscale raised 25% (1.10 → 1.125). More reactive
  feel, lighter wash.
- Stranded tooltip on keyboard close: a MutationObserver on the
  picker's `class` attribute dismisses the shared emoji tooltip
  whenever `visible` comes off, so Enter/Esc-triggered closes don't
  leave a tooltip floating with no mouseleave to dismiss it.
- Favorites section hidden until implemented — both the sidebar
  category button and the main-area section. Explicit `[hidden]`
  rules override `display: flex` on the button and a JS path that
  was forcing `display: block` on every `.emoji-section` after
  search clear.
- `object-fit: fill` → `contain` on the in-chat pack preview thumbs
  and the Pack Details modal thumbs. The preview-grid cells are
  wider than tall (`repeat(6, 1fr)` × 28px), so `fill` was squashing
  square emojis horizontally. `contain` preserves the natural aspect.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- message_new / mls_message_new handlers double-pushed into chat.messages
  when it aliased eventCache's entry.events after openChat. The duplicate
  sat at the tail; on reopen, findIndex(lastReadOnOpen) returned the first
  copy and the loop then saw the second copy as "another unread" — the
  divider anchored above a message the user had already read.
- Expose eventCache.getEventsRef() so the message handlers can detect the
  shared-array case and skip the redundant insertion.
- Tighten the read-state policy across all paths:
  - Open chat → mark synchronously (OS badge clears immediately), with
    last_read snapshotted first so the divider can still anchor above the
    first missed message.
  - Receive while pinned → mark + clear divider.
  - Receive while scrolled up → don't mark, divider stays.
  - Scroll back to pin → mark (badge clears), divider stays.
  - Close while pinned → mark.
  - Close while scrolled up → don't mark; honestly preserves unread state.
- Remove the focus-gated markAsRead inside updateChat — the IPC could
  hang/fail and leave last_read stuck. Direct site-level marks above
  cover every case more reliably.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Back stack
- src/js/back-stack.js: lightweight pushBack/popBack/runBack/clearBack
  stack, deduped by id. window.__vectorOnAndroidBack is the JS entry
  point Kotlin calls via WebView.evaluateJavascript.
- MainActivity.kt: onBackPressed forwards into JS; the async callback
  hits moveTaskToBack(true) when the stack reports unhandled, so the
  activity hides instead of tearing down (preserves relay/MLS state).
- Screens wired: chat, new-chat, profile (chat-aware close), settings,
  invites, create-group, group-overview, invite-member modal. openChatlist
  clears the stack since it's the root.
- Transient surfaces wired: popupConfirm (back = cancel/accept by fNotice),
  emoji picker + attachment panel via MutationObserver on .visible,
  Mini Apps sub-view, Marketplace + App Details + Permission Prompt,
  and the Mini App launch dialog.

Read-state policy alongside
- Tabbed-out / backgrounded receives no longer auto-mark while open and
  pinned. Refocus / foreground catches up when pinned via
  onFocusChanged + visibilitychange.

UX polish
- Mini profile popup now drops a backdrop on desktop too — focused-modal
  feel without losing the click anchor.
- Reaction custom emojis can't be dragged or selected; pointer-events
  passes the click through to the reaction chip.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wallpapers: pick an image, AES-256-GCM encrypt, upload to Blossom, and
publish a NIP-17 gift-wrapped kind-30078 (d=vector-wallpaper) rumor to the
peer. Receiver downloads, decrypts, caches, and applies it. Latest-write-
wins by rumor created_at; blur + brightness sliders persisted in tags;
animated images get first-frame extraction; auto-brightness from image
luma. Replacing a wallpaper DELETEs the previous Blossom blob, but only
when we were the original uploader (tracked via wallpaper_url +
wallpaper_uploader, migration 31).

Read-badge fix: a backend ACTIVE_CHAT marker (set by the frontend when the
chat is open + pinned + focused) lets the inbound handler advance last_read
on arrival, so the dock badge never bumps for messages the user is watching.

Send path: normalise relay URLs before pool lookup (trailing slash / case
drift was dropping visually-identical inbox relays), and split send-failure
handling so "relays rejected" and "couldn't attempt" report distinct,
actionable errors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Custom emoji: hold a transparent pixel (not a src-less img) during the
loading phase so Android WebView stops painting its broken-image glyph
behind the shimmer. When an emoji's bytes can't be fetched (uncacheable
URL or a 404 on a deleted blob), fall back per context — inline messages
show the literal :shortcode:, reactions show a twemoji'd question mark.

Chat header overflow menu: derive the button's visibility from the same
builder that populates it, so chats with no options (groups, which have no
per-chat settings yet) hide the button instead of opening an empty menu.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fixing Avatar Upload + Truncated Status on Chat Page
JSKitty and others added 29 commits June 20, 2026 16:56
The synced Community List is a single NIP-44 event (65 KB plaintext) and
overflowed once an account held ~23 communities — the encrypted republish then
failed ("message too long") and cross-device sync stalled.

Slim each entry: drop `current` when it equals `seed` (the no-rekey case),
strip the icon from list blobs (a rehydrating device folds it from the
community metadata; the icon still rides the real invite bundle), and skip
serializing None/empty/zero fields. Real 21-community account: ~2,128 ->
~1,181 bytes/entry (-44%), raising the ceiling from ~23 to ~55. from_json
re-expands `current` so the in-memory form is uniform and the roundtrip is
identical; the icon strip also keeps canonical() (the cross-device tiebreak)
icon-invariant.

Cap memberships at 50 (MAX_COMMUNITIES): create_community and the invite-accept
paths reject a NEW membership past the limit (re-accepts are exempt), with a
clear message shown through the existing join/create error popups.

Fully backward-compatible: every change rides existing Option/default serde
semantics, no wire or schema break.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ear "Playing" on close

- A broken .xdc no longer fails silently. The open error (e.g. "Invalid Mini
  App package: Missing index.html") is shown as a toast instead of a dead
  "Click to Play"; the throw happens before any optimistic status, so the card
  stays clickable.
- A solo app (no realtime channel) no longer shows "1 online". The peer badge
  used the real session-peer count (which already includes self once a channel
  is joined) instead of fabricating self+1 from "window open".
- A solo app's "Playing" now clears on close. The desktop WindowEvent::Destroyed
  handler and Android miniapp_close emit is_active:false for the app's topic even
  when no channel was joined (the clear was previously gated on a channel a solo
  app never creates), so the card resets live without a chat re-render.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…c loader)

A .xdc whose entry is "Index.html" (or any off-case file) failed to open even
though the bytes were fine. Resolve archive entries case-insensitively at every
zip read: validation (index.html, manifest.toml), the runtime serve (get_file,
the single chokepoint for every asset request), and the info-card manifest/icon.
A mis-cased app now just runs. Exact match always wins first, so correctly-cased
apps are unaffected; genuinely-broken packages still get the descriptive
open-error toast.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…another device)

A backgrounded message notification stayed floating after the chat was read by
opening the app and navigating to it (instead of tapping the notification), or
after answering on another device. There was no per-chat cancel: notifications
only cleared on tap/swipe/Mark-Read, and onResume wiped the id map without ever
cancelling the OS notifications.

- VectorNotificationService.cancelNotification(context, chatId): dismiss the
  per-chat notification (no-op if none is showing) + drop its history.
- clearAllMessageHistory (onResume) now KEEPS chatNotificationIds so a later
  per-chat cancel can still find a notification still showing after resume;
  opening the app no longer wipes the ability to dismiss them on read.
- cancel_notification_jni + cancel_chat_notification: JNI bridge + platform
  helper (Android cancels; desktop no-op).
- Cancel at every read chokepoint: mark_as_read / mark_as_read_headless (in-app
  open, window refocus, notification Mark-Read), and on_dm_received /
  on_file_received for own cross-device messages (mark read + cancel, in both
  the foreground handler and background-sync commit path).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…at edge case)

A message for the chat the user already had open, arriving while softly
backgrounded, was auto-marked read but still posted a notification (the activity
wasn't foreground). Returning to the app landed straight back in that chat, which
never re-opens, so no read path re-fired a cancel and the notification lingered.

Two parts:
- nativeOnResume now cancels the active chat's notification on resume (only the
  active chat; others stay until read). It flips foreground=true first, so a post
  still in flight at the moment of resume is suppressed rather than re-shown.
- Notification IDs are now deterministic (chatId.hashCode()) instead of a mutable
  counter map. The map made cancel unreliable: a message-arrival race between the
  read-path cancel (which removed the map entry) and the re-post (which won) left
  the notification showing with its id forgotten, so the later resume cancel found
  nothing. A deterministic id is recomputed on every cancel, no map state needed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…list

Both overlays opened without registering a back-stack entry, so Android's back
button skipped them and ran the underlying screen's back action: the image viewer
left the conversation for the home view (viewer still open), and the account list
stayed open over the profile screen.

Register both via the existing pushBack/popBack convention:
- image viewer (previewer.js): pushBack on open, popBack on close.
- My Profile account list (profileSwitcher): pushBack on open, popBack on close.

popBack is idempotent, so closing via the X / backdrop / Escape / a real
back-press all keep the stack in sync.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`---` (or more dashes, plus ***/___) now renders as a divider. Three layers
needed fixing, each masking the next:
- message-row.js: the content-append guard counted text/img/video but not <hr>,
  so a divider-only message's span was dropped entirely (rendered nothing).
- styles.css: an <hr> inside the inline .dmsg-text had a zero-width containing
  block and collapsed; pin a visible block width (expands with surrounding text).
- markdown.js: disable setext headings so "text" then "---" renders as text + a
  divider instead of turning the line above into a heading (Vector uses # headings).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… send)

Community typing was wired end to end except two gaps:
- realtime.rs: COMMUNITY_TYPING (3311) was missing from the realtime sub filter,
  so typing signals were published (start_typing already routes hex channel ids to
  send_community_typing) but never received. Add it. Like presence, typing takes
  the no-persist dispatch branch, so it adds no DB writes per signal.
- main.js: the message_new typing-clear filtered active_typers by chat.id, which
  is the contact npub for DMs but the channel hex for communities, so it never
  matched and the indicator lingered the full 30s window. Resolve the sender's
  npub and refresh the header/preview immediately so it clears the instant their
  message lands.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tplace failures

Untrusted-content audit (HIGH batch):
- marketplace: validate the attacker-controlled d-tag app id (length + charset,
  no traversal) at parse and at install/update/uninstall/check_installed
- emoji decode + image cache: SSRF guard + streamed byte caps (1 MB emoji,
  10 MB avatar/banner) that hold for chunked / no-Content-Length responses
- site metadata: hard 512 KB ceiling on the </head> scan (lossy decode)
- get_or_cache_image propagates the failure reason instead of returning Ok(None)

Emoji failures:
- picker/panel hide oversized/unavailable emoji; inline shows a tappable
  :shortcode: chip explaining why; the editor shows a removable broken cell that
  explains + guides re-upload; reactions keep their glyph

Marketplace icons:
- placeholder centers in the slot, CSP-safe fallback, and self-heals (re-fetches)
  after a manual cache wipe

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The shared .context-menu had `padding: 4px 0`, leaving a slice above the first /
below the last item on every showContextMenu dropdown (message right-click,
chat/wallpaper menu, picker long-press). Set to 0 to match .profile-more-dropdown;
items keep their own padding and the rounded corners clip the first/last hover.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replaces the flat popupConfirm badge popup with a tilting, holographic card.

- Tilt by capability: native device-tilt on Android (gravity/accelerometer via a
  JS bridge; baseline = first reading; low-pass + deadzone), mouse on desktop,
  finger-drag as the universal fallback
- White specular sheen that sweeps with the tilt; spring open/close; reduced-motion aware
- Perks section (V for Vector shows its emoji-pack bonuses)
- Sensor runs only while the card is open: stopped on close + background, re-armed
  on resume; Android back button + Escape + backdrop all close it

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ead server

A Cloudflare/origin gateway timeout (524/522/520/504) on a large upload won't
resolve by retrying the same server, so route to the next Blossom server
immediately rather than burning the per-server retries (~2 min each). + a parse test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Register the mini profile with the nav stack (pushBack/popBack) so hardware Back
closes the popup instead of navigating the page beneath it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
On-device the gravity X/Y components are effectively swapped vs the naive mapping:
Y tracks forward/back tilt, X tracks side-to-side. Drive the card's forward/back
lean from Y and side-to-side from X (Z / screen rotation stays ignored), so the
card now tilts to physically match the phone.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sending a file attachment now shows "Uploading · X/s" where the size sits,
mirroring the download-speed readout and computed from the cumulative bytes the
backend reports (bytesSent on attachment_upload_progress). Cancelling an upload
is now reachable from the hover toolbar (warning-colour x) and the right-click /
long-press menu, both gated to attachment uploads (plain text sends have no
cancel path). The speed tracker is torn down on completion, failure, and cancel,
and never spins for uploads without a file-box rate target.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Some recent additions (distorted face, fight cloud, ...) shipped without a
curated shortcode, so they never normalised to a :code:. The runtime EmojiDEX
pass now derives one from the display name (lowercase, non-alphanumerics to
underscores) for any entry missing it, covering the 9 current gaps and future
Unicode additions. The curated shortcode still wins when present.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Share sheet:
- Route every entry point (cold-start poll, live event, resume hooks, and a re-poll right after
  init completes) through the backend's atomic get_pending_share, so a foreground/resume share is
  handled exactly once and isn't stranded until a later tap (which previously yanked the user back
  to the chat list mid-navigation).
- Reveal Send when shared text pre-fills the composer (a programmatic value set fires no input).

Unread badge:
- Re-derive the DB-sourced chat-list count when a message is removed, so deleting an unread
  message drops the in-app badge, not just the OS one.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Favorites section was permanently hidden (category button + section had `hidden`
with nothing to un-hide them) and its only writer was a right-click handler that saved
to localStorage with a brief gold flash but never displayed anything. Dead weight, and
localStorage made it global-across-accounts unlike the per-account SQL settings KV. Drop
the whole path: arrFavoriteEmojis, toggleFavoriteEmoji, loadFavoritesSection, the
contextmenu handler, and the markup/CSS.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Collapses ~40 hand-inlined `nickname || name || npub` resolutions into one shared
getName(profileOrId) helper that also consults the Nostr `display_name` field. Fixes
the divergence where a profile that set only `display_name` (no `name`) showed their
real name in reactions but their npub everywhere else, author, @mention, profile view,
chat header, chatlist. Group/community names and the profile-edit placeholder are left
as-is (not person names). The reaction code now uses the shared helper too.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bios already turn bare npubs into mention chips, but a pasted
vectorapp.io/profile/<npub> link stayed plain text — the bare-npub regex skips an
npub sitting right after a `/`. Add a vectorapp.io-specific branch to renderMentions'
allowBare pattern, tried first so it claims the whole URL. Scoped to that domain only;
other URLs (and other vectorapp.io paths) are untouched, and messages still linkify
their URLs as before.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…text

The attachment reply preview was display:flex with no align-items, so the type
icon and its label ("Picture", "Video", ...) aligned to the top and the text sat
high. Center them.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Top stop #232527 -> #171717 for a deeper, less grey card backdrop.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the tail-anchored offset pager (O(distance-from-now); it loaded 2000+
messages just to reach a week-old read marker) with a random-access model.

- Anchored loading: new get_messages_around(chat, anchor, before, after) runs two
  bounded (created_at, received_at, rowid)-cursor queries, O(window) at any depth.
  The tuple cursor pages PAST same-timestamp bursts (closes the DB-read half of
  #300). save_message switched to an UPSERT so re-saves (reactions, edits) preserve
  rowid and the cursor can't silently drop burst rows.
- DOM windowing: bound the rendered rows (~80) over a non-congruent, tail-pinned
  RAM cache. The buffer may hold [seek-region, GAP, tail]; the newest 500 rows are
  never evicted, so chat-list preview/sort always see the true latest. The
  scroll-extends, jump-to-bottom and reply-jump are all gap-aware.
- Jump-to-unread: a pill plus a "NEW" divider anchored to the last-read row that
  survives the scroll-up trim (manual scroll-up reveals it too). Join/leave
  system-event reveal re-wired into the windowed scroll-up path.
- Polish: viewport-fill on open (short messages no longer leave the view
  unscrollable), intent-aware scroll pin (a slight scroll-up no longer yanks you
  back during sync), and a media-settle re-scroll on jump-to-bottom.

Backend hardening: SessionGuard on the new and existing message-window commands
and the realtime/headless mark-read paths (multi-account swap safety); a window
clamp on get_messages_around (hostile-frontend DoS); community accept reordered to
claim, reset floors, then history-start.

Fixes folded in: banned-on-join no longer destroys the UI (openChatlist hides the
chat view) and shows a toast; the back-chevron unread dot now matches the chatlist
rows; avatarless mini-app lobby players fall back to the default avatar.

Verified by two deep code reviews (every Critical/High/Medium/Low resolved), 965
vector-core tests, and a new independent gap-state-machine fuzz harness
(tests/event-cache/, run via npm run test:cache) that loads event-cache.js
unmodified and asserts the cache invariants across every transition.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A reply to a media message whose target isn't loaded in memory showed a
generic "Attachment" in the quote, while in-memory targets already showed the
type. The backend-resolved reply context only carried a has_attachment bool.

Thread the replied-to attachment's extension through the reply-context
resolution: get_reply_contexts now reads the stored kind-15 event's
"attachments" tag JSON (each entry carries its own extension) into a new
Message.replied_to_attachment_extension, and the quote renderer maps it through
getFileTypeInfo so off-screen replies show "Picture"/"Video"/etc. with the
right icon instead of the generic label.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The debounced refreshUnreadCounts only re-rendered the UI when no chat was
open, so a background message updated chat.unread but never re-evaluated the
in-chat back-chevron dot, leaving it dark despite the OS badge. Refresh the
dot (it reads the chat.unread just updated) when a chat is open; the chatlist
rows still refresh otherwise. Keeps the DB-authoritative chat.unread source,
without the diverging unread walk returning.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…surface

- concord2_* tables (migration 63) + TRANSPORT_CONCORD2 dedup discriminator
- v2 service: create/boot-sync/paging/send/typing/metadata/leave/dissolve,
  authors-routed realtime subscription and dispatch, SessionGuard throughout
- Tauri commands branch by id: create_community now founds v2 ONLY; list/get
  combine both generations; send/sync/typing/leave/delete/metadata route v2
- subscription loop seams v2 wraps by plane-author match before the DM path
- session-swap clears v2 routes; boot orphan sweep keeps v2 channels
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants