Skip to content

4.1.0: theme tokens, modern restyle, FastCommentsLiveChat widget, content-HTML fixes, web test lane#14

Open
winrid wants to merge 10 commits into
mainfrom
redesign-theme-tokens-live-chat
Open

4.1.0: theme tokens, modern restyle, FastCommentsLiveChat widget, content-HTML fixes, web test lane#14
winrid wants to merge 10 commits into
mainfrom
redesign-theme-tokens-live-chat

Conversation

@winrid

@winrid winrid commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Summary

Visual redesign + Android-SDK parity work, two real content-HTML bug fixes, and a test-framework credibility overhaul. Purely additive API; bumps to 4.1.0.

Theming

  • New FastCommentsTheme semantic token layer (colors, spacing, radii, font sizes, font weights, avatar sizes; token names mirror the Android SDK's FastCommentsTheme). The entire default style tree is generated from tokens.
  • New optional theme prop on FastCommentsLiveCommenting, FastCommentsFeed, and FastCommentsLiveChat; getLightTheme() / getDarkTheme() / resolveTheme() exported. Dark mode is one token set.
  • Back-compat: styles together with theme merges on top (styles win); styles alone keeps the legacy full-replace behavior. The dark/erebus skins now derive from the dark theme; setupDarkModeSkin is deprecated in favor of the theme prop.

Visual redesign ("modern neutral")

Hairline separators, pill vote buttons and chips, filled primary buttons, 40px shadow-free round avatars, consistent 14px+ type scale, themed link colors through render-html tagsStyles. The editor toolbar and mention popup no longer hardcode colors; new style keys: commentTextArea.toolbar*, mentionPopup.itemSelected, comment.textLinkStyles, liveChat.*.

New widget: FastCommentsLiveChat (Android LiveChatView parity)

Chronological messages, composer below the list, live header strip (connection dot + user count), auto-scroll to new messages (pauses while scrolled up, resumes at the bottom or on your own message), older history on scroll-to-top, votes and reply threading disabled. All presets overridable via config. config.maxReplyDepth: 0 now hides reply buttons.

Bug fixes

  • Feed posts composed in the SDK rendered with literal <p> text: the composer wrapped content in <p>, which the server entity-escapes (not an allowed tag). Now sends <br>-separated content like the web feed widget. This was the root cause of the 5 feed E2E suites failing on main.
  • Multi-paragraph comments glued together: the editor emits <p>-wrapped paragraphs, which the server strips. Editor HTML is now normalized (editorHtmlToServerHtml) before comment create and edit.

Test framework credibility

The suite previously stayed green while real flows were broken. Changes so that cannot recur for these classes:

  • The react-native-enriched jest mock now mirrors the real editor's HTML contract (<p>-wrapped paragraphs), so the normalization path is actually exercised.
  • New web lane npm run test-web (vitest + jsdom in example-web/): mounts the SDK through react-native-web with the REAL tiptap editor against the real backend; covers the .web.tsx / react-native-web regression class the node suite cannot execute.
  • New E2E specs: LiveChat widget (dual-session two-way exchange, header strip, presets, auto-scroll, load-older), theme tokens (flow, precedence, skin back-compat), guest submit (mutation-checked: goes red with the old bug re-introduced), multi-paragraph comments.
  • tests-ui/README.md now documents exactly what each remaining mock does NOT cover.

Dependency note

Migrates to fastcomments-sdk 3.2.4 (renamed logout/getTrending/search/getLarge; restored getCommentsForUser and the react urlId param that a spec regen dropped). The fastcomments-sdk-js working tree carries that regen + restore and needs its own commit/release; the durable fix is declaring urlId on the tsoa react endpoint and porting /comments-for-user to a tsoa controller so regens stop dropping them.

Testing

  • Full E2E suite vs production backend: 21/21 suites, 57 tests (main has 5 feed suites failing).
  • Unit: 125 passing, including new theme-styles and editor-normalization tests (red-green).
  • Web lane: 3 passing (real react-native-web + tiptap mount, loading-layout regression guard).
  • tsc --noEmit clean (main does not compile against the regenerated SDK).
  • Verified visually in example-web: light, dark (?theme=dark), and chat widget (?widget=chat) all render correctly.

Not covered here: on-device visual pass for native iOS/Android and the erebus skin.

winrid added 2 commits June 10, 2026 09:14
…xes, web test lane

Theming and visual redesign:
- New FastCommentsTheme semantic token layer (colors, spacing, radii, font
  sizes, font weights, avatar sizes; names mirror the Android SDK). New
  optional `theme` prop on all widgets generates the entire default style
  tree from tokens; getLightTheme/getDarkTheme/resolveTheme exported.
  `styles` together with `theme` merges on top (styles win); `styles` alone
  keeps the legacy full-replace behavior so existing integrations and skins
  are unaffected. Dark and erebus skins now derive from the dark theme;
  setupDarkModeSkin is deprecated in favor of the theme prop.
- Redesigned "modern neutral" default look: hairline separators, pill vote
  buttons and chips, filled primary action buttons, 40px shadow-free round
  avatars, a consistent 14px+ type scale, themed link colors wired through
  react-native-render-html tagsStyles. De-hardcoded the editor toolbar and
  mention popup into new style keys (commentTextArea.toolbar*,
  mentionPopup.itemSelected, comment.textLinkStyles, liveChat.*).

New FastCommentsLiveChat widget (Android LiveChatView parity):
- Chat preset over FastCommentsLiveCommenting: chronological messages with
  the composer below the list, live header strip (connection dot + user
  count), auto-scroll to new messages that pauses while scrolled up and
  resumes at the bottom or on your own message, older history loaded on
  scroll-to-top, votes and reply threading disabled. All presets
  overridable via config. config.maxReplyDepth: 0 now hides reply buttons.

Bug fixes:
- Feed posts composed in the SDK rendered with literal <p> tags: the
  composer wrapped content in <p>, which the server entity-escapes (not an
  allowed tag). Send <br>-separated content like the web feed widget.
- Multi-paragraph comments lost their line breaks: the editor emits
  <p>-wrapped paragraphs, which the server strips, gluing paragraphs
  together. Editor HTML is now normalized via editorHtmlToServerHtml()
  before comment create and edit.

Test framework credibility work:
- The jest mock for react-native-enriched now mirrors the real editor's
  HTML contract (each line arrives as a <p> block), so the normalization
  path is actually exercised.
- New web test lane (npm run test-web): vitest + jsdom in example-web,
  mounting the SDK through react-native-web with the real tiptap editor
  against the real backend. Covers the .web.tsx / react-native-web class
  of regression the node suite cannot execute.
- New E2E specs: live-chat-widget (dual-session two-way exchange, header
  strip, presets, auto-scroll, load-older), theme (token flow, precedence,
  skin back-compat), guest submit (verified red with the old bug
  re-introduced), multi-paragraph comments. tests-ui/README.md documents
  what each remaining mock does NOT cover.
- New theme-styles unit tests lock in token flow and the typography rules
  (no sub-base font sizes, no text opacity, no uppercase transforms).

Also migrates to fastcomments-sdk 3.2.4 (renamed logout/getTrending/
search/getLarge methods; restored getCommentsForUser and the react urlId
param) and adds dom to tsconfig lib for the web code paths.
Functional fixes:
- Guest voting dead end: a stale anon session (no username/email) was
  treated as authenticated, so the vote 401'd silently with no prompt and
  no feedback. New user-auth-state helpers gate the vote auth form, a
  rejected session vote falls back to collecting identity, other failures
  render a visible themed error, and the vote auth inputs gained testIDs
  and proper input styling.
- Ghost anon session chrome: the top bar rendered an empty username box,
  a Log Out menu, and a notification bell that 401'd into an infinite
  spinner for users who never logged in. The top bar requires an
  identified user; the notification list catches load failures and shows
  an error; the bell badge hides at zero.
- Reply indentation: depth was attached to the list's prop copy but the
  store object won the merge, so every thread rendered flat. Depth now
  comes from the prop; default indent raised; regression assertion added
  to the threading spec (red before the fix).
- Web dialogs: Alert.alert is a no-op under react-native-web, making
  reply-cancel, delete/block confirms, and error display dead ends in
  browsers. All dialogs route through a new platform shim
  (window.confirm/alert on web, Alert.alert on native).
- Editor theming: typed text was black-on-near-black in dark mode and
  there was no placeholder. The editor now receives themed color, font
  size, placeholder text/color, and no longer autofocuses on mount
  (which also stole focus from host pages). Covered by a web-lane test.
- Dark icon variants: hasDarkBackground is now derived from the resolved
  theme's background luminance, so theme={getDarkTheme()} gets white
  bell/vote/menu icons without setting a second config flag.
- Inline formatting: b/strong/i/em/u/s/code lost their default styles on
  web and posted bold/italic rendered as plain text; explicit tag styles
  added to comment.textLinkStyles (unit-tested).
- Chat: opens pinned to the newest message (onContentSizeChange; the
  mount effect fired before web layout settled) and no longer renders
  the comment-count/sort header, which contradicted chronological order.
- Comment length: the editor silently truncated at the character limit,
  losing everything past it with no warning; the SDK now blocks submit
  with the translated COMMENT_TOO_BIG error instead.

UX polish:
- Identity first: username renders above the "Unverified comment" label.
- Progressive disclosure: the guest name/email form appears on composer
  focus (or submit) instead of rendering as a permanent registration
  form. Guest spec updated to drive focus explicitly.
- Touch targets raised toward 44px (vote pills 36px+ with 16px icons,
  reply/sort/bell/three-dot padding); vote counts show 0 with a muted
  zero style instead of hiding; modal menus gained a scrim; the composer
  and list share one 16px gutter; BackHandler no longer warns on web.
@winrid

winrid commented Jun 10, 2026

Copy link
Copy Markdown
Contributor Author

Added a second commit fixing the usability defects surfaced by a UX + QA review of the web example:

Functional: guest vote dead end (silent 401, no auth prompt), ghost anon-session chrome (empty username box, Log Out for guests, bell spinning forever on 401), flat reply threads (depth lost in a store merge), Alert.alert no-ops on web (reply cancel, delete/block confirms, error display), black-on-black editor text in dark mode + missing placeholder + mount focus steal, light icons on dark backgrounds (hasDarkBackground now derived from the theme), bold/italic rendering as plain text on web, chat opening at the oldest message, and silent comment truncation at the character limit (now a visible COMMENT_TOO_BIG error).

Polish: username above the "Unverified comment" label, progressively disclosed guest form, 36px+ touch targets, visible zero vote counts, menu scrim, aligned gutters, no bell badge at zero, no BackHandler warning on web.

All lanes green after the changes: 21/21 E2E suites (58 tests), 134 unit tests, 4 web-lane tests, tsc clean. Verified visually in the browser: light, dark, and the chat widget on a 390px viewport.

winrid added 8 commits June 10, 2026 13:27
Every spec already authenticates with Secure SSO tokens implicitly; this
adds explicit assertions for the SSO-specific behaviors:
- Secure SSO: signed user lands in the top bar, no guest form on focus,
  comments attributed to the SSO identity.
- Simple SSO: an unsigned { username, email } user is recognized as the
  session identity and can comment.
- Login gate: with an SSO config but no user, the composer is replaced
  by a login button that fires the host loginCallback.
- Logout: the Log Out menu item fires the host logoutCallback.

Adds testIDs to the SSO login button and the modal-menu open button.
…mit, mention groups

Closes the feature gaps customers hit when migrating from the iframe-based
react wrapper (config surface: pageReactConfig, voteStyle, mentionGroupIds,
plus an arrow-in-the-box submit layout):

- Page reacts (config.pageReactConfig): new PageReactsBar above the
  composer renders each configured react as an image button (src /
  selectedSrc) with a count; tapping toggles the react optimistically and
  persists via the page-reacts API; with showUsers, tapping the count
  reveals who reacted. New src/services/page-reacts.ts wraps the new
  typed endpoints (GetPageReacts / AddPageReact / DeletePageReact /
  GetPageReactUsers, added to the fastcomments-sdk spec). New pageReacts
  styles section.
- voteStyle: VoteStyle.Heart renders a single like toggle per comment
  (no up/down pair): like = vote up, tap again to remove. Uses new
  ICON_HEART / ICON_HEART_ACTIVE image assets (appended to the asset
  enum to keep existing numeric values stable), overridable through the
  assets prop, e.g. with a star.
- config.useInlineSubmitButton anchors an icon-only send button inside
  the comment box and suppresses the standalone labeled button.
- config.mentionGroupIds is forwarded to the mention user search.

Covered by tests-ui/specs/idcolab-parity.test.tsx (heart toggle round
trip, page react toggle + persistence across reload + count, inline
submit posting and clearing the editor), written red before each
implementation. Full suite green: 23 E2E suites / 64 tests, 134 unit
tests, 4 web-lane tests.
The submit guard checked the raw editor value, but the editor (tiptap web
and native) reports an empty box as '<p><br></p>' or '<p></p>', never '' -
so tapping send on an empty composer POSTed and the server stored a blank
"<br />" comment. New isEditorHtmlEffectivelyEmpty() strips paragraph
wrappers, <br>s, &nbsp; and whitespace before deciding (image-only content
still counts as non-empty); the submit guard and the missing-identity
check both use it. Caught by a new E2E spec driving the real empty-editor
shapes (red before the fix) plus unit coverage; verified in the browser
that no request fires on empty submit.
countAboveToggle (web widget parity):
- Widgets with useShowCommentsToggle now start collapsed like the web
  widget, keeping the full scaffold (composer, page reacts, toggle)
  instead of replacing everything with a bare toggle button.
- New config.countAboveToggle renders the first N root comments as a
  teaser above the Show Comments toggle while collapsed (replies
  excluded); the toggle reveals the rest. Tapping a teaser's replies
  toggle expands the whole list, matching the web widget. The legacy
  hideCommentsUnderCountTextFormat mode keeps its bare-link behavior.
- Collapsed state suppresses the empty-state message and pagination.

Default media buttons (web widget parity):
- The GIF toolbar button now renders without any host callback and opens
  the SDK's built-in GifBrowser; the selection inserts into the comment.
  Hosts can still take over via pickGIF. The browser is wrapped in a
  Modal: rendered inline it sat underneath later comment rows on web
  (virtualized-list stacking, same class as the mention popup).
- The image button renders by default on web using a DOM file-input
  fallback feeding the existing upload path; on native it appears once
  the host supplies pickImage (the SDK ships no native file picker).
- GifBrowser cleanup: scrim, sized/centered modal, images contained to
  the modal width (previously sized to the window and overflowing),
  rounded corners, testIDs, no BackHandler warning on web.

Tests (red first): countAboveToggle collapse/teaser/expand round trip in
idcolab-parity, new editor-toolbar-media spec (default GIF button ->
built-in browser -> trending from the real backend -> selection lands in
the editor; image button + http-URL insert), and a web-lane assertion
that both buttons render with no callbacks. Full suite green: 24 E2E
suites / 68 tests, 136 unit tests, 5 web-lane tests.
…ll-width modal

The built-in GIF picker opened as a near-full-width centered modal, which
on desktop was huge and visually disconnected from the button that opened
it. It is now a 340px dropdown anchored directly under the toolbar GIF
button, like the web widget's dropped-content panel:

- On web the panel is portaled to document.body (the virtualized list
  clips/overpaints inline overlays) and positioned with page coordinates
  measured off the button, repositioning on scroll/resize and clamped to
  the viewport edge. On native it anchors below the toolbar inline.
- Panel styling: bordered, rounded, shadowed, maxHeight 420 with internal
  scrolling and overflow clipped (content previously bled past the
  rounded border). New gifBrowser.popover style key carries the sizing.
- gifBrowser.centeredView is no longer a full-screen absolute overlay;
  the example app that mounts GifBrowser standalone now provides its own
  overlay wrapper.

Verified on a 1440px viewport: panel sits 4px below the button at the
button's x, selection still inserts and closes. Suite green (the single
live-vote timeout in the full run reproduces green in isolation and is
unrelated WS flake): 24 E2E suites, 136 unit tests, 5 web-lane tests.
Typing in the GIF picker now searches automatically: each keystroke
re-arms a 700ms trailing debounce (the web extension's delay), the final
term searches without pressing Enter, clearing the box falls back to
trending, and identical terms are deduped. Submit/Enter still searches
immediately.

Fixes two latent bugs in the old handler: it sent the PREVIOUS
keystroke's text (stale state read), and it reused the pagination page
from infinite scroll so a new term could start mid-pagination; new
searches now always start at page 0.

Covered by a new E2E test (type with no submit -> first result changes
via the real search API) plus a browser check confirming exactly one
request fires while typing 15 characters. Full suite green: 24 E2E
suites / 69 tests, 136 unit tests, 5 web-lane tests.
… toggle; snug page reacts

Web image/GIF attachment (the web editor cannot hold images at all:
react-native-enriched's web setImage is a no-op stub and its tiptap
schema has no image node, so inline insertion silently vanished):
- Picked GIFs / uploaded images on web attach as preview chips under the
  editor (remove buttons, testIDs) and are appended to the comment as
  [img]src[/img] tokens at read time - the wire format the web widget
  itself submits. Editing on web extracts existing tokens back into
  chips. Native keeps true inline insertion via setImage, converted to
  tokens at submit (the server strips raw <img> from user submissions,
  so even native inserts were silently dropped server-side before).
- The content is otherwise untouched: no <br> injection, no markup
  rewriting. Block layout is a renderer concern, like the web widget's
  `.inline-image { display: block }` CSS.

Render-side (operates on commentHTML, the field we render):
- A domVisitor retags the server's <a class="inline-image"> wrapper to a
  block custom element, so uploaded images render on their own line on
  every platform; a custom renderer preserves tap-to-open. 10px vertical
  margins and left alignment ship via the existing classesStyles channel
  ('inline-image' / 'comment-image').

UI polish from review:
- Show/hide comments toggle is now a quiet, compact centered text
  control (2-3px padding, neutral colors) instead of a full-width blue
  bar, matching the web widget's treatment.
- Page react counts hug their icon (2px gap); the shrunken buttons keep
  a usable touch target via hitSlop.

Tests: media E2E asserts the STORED comment HTML contains the image
(the gap that hid the server-side strip), web-lane test drives the real
gif-pick -> attachment chip flow, normalizer units cover tokenization
with clean content. Suite green: 24 E2E suites / 69 tests, 137 unit
tests, 6 web-lane tests. Verified in the browser: clean [img] payload,
image on its own line at the text gutter with 10px margins.
…y-reply close

- Buttons (Submit, Reply, pagination, banners) now use the web widget's
  off-black (#222) instead of blue; dark theme inverts to light-on-dark
  like the web widget's .dark button treatment. Content hyperlinks stay
  blue. Vote/like buttons lose their grey pill backgrounds (bare icons,
  same touch targets).
- Vote auth form no longer rearranges the vote buttons: the options row
  is flex-start (space-between scattered it across the form's width) and
  the comment toolbar top-aligns so controls don't re-center against the
  expanded form.
- Closing an EMPTY reply no longer asks to discard: the open composer
  registers a dirty check (store.replyDirtyCheck) the close flow
  consults; half-written replies still confirm. New E2E covers both
  directions; verified on web with real window.confirm dialogs.
- The sort selector and the per-comment three-dot menu render as real
  dropdowns on web, anchored under their triggers (portaled to the body,
  scroll-tracked, outside-click close, selected sort highlighted) instead
  of centered modals; native keeps the modal pattern, and sub-flows like
  the edit form remain modals everywhere. The comment menu's trigger rect
  is captured at press time and threaded through OpenCommentMenuRequest.
- Show/hide comments toggle is a quiet compact text control; page react
  counts hug their icons (touch targets preserved via hitSlop).

Suite green: 24 E2E suites / 70 tests, 137 unit tests, 6 web-lane tests.
Browser-verified: anchored sort + comment menus (geometry asserted),
off-black reply text, vote layout stable with the auth form open.
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.

1 participant