- Live chat streaming UX:
FastCommentsLiveChatnow matches the web live-chat widget. Compact Twitch-style rows (small avatar with the activity dot, bold name inlined with the message), periodic day separators, no per-message dates or per-message reply/vote bar, a red "Live" dot, and a divider above the composer. Ctrl/Cmd+Enter submits; the composer shimmers in place during a save instead of showing a spinner or flickering the text. - Online users: a red presence dot, an always-on count, and
OnlineUsersFacepile+OnlineUsersListwidgets. The list has Online/Offline sections (offline paginated viagetOfflineUsers) and afillmode for sidebar layouts. Presence is now applied incrementally from websocket frames (add/remove + a debouncedgetUsersInfoenrich) instead of re-fetching the whole list - matching the web widget. - Feed parity with the Android SDK:
FastCommentsFeedPostCreate- a standalone post composer (AndroidFeedPostCreateViewequivalent): author header, a rich enriched-editor body, media attachments, link attachments, tags (tagSupplier), custom toolbar buttons, and cancel/loading/error. Drive it from the feed's own store viaonStoreReady, or pass it aconfig.- Post rows gain author avatars, rich HTML content, a comment button that opens the post's comments (
urlId = "post:<id>", like Android), share, and a delete menu. - Typed image layouts: single images render full-width at their aspect ratio; multiple render as a navigable carousel; link-only posts render action buttons.
config.hideFeedComposerto suppress the built-in composer.
- Store access:
FastCommentsLiveChat/FastCommentsLiveCommenting/FastCommentsFeedacceptonStoreReady(store), and theFastCommentsStoretype is exported, so hosts can render store-driven widgets (e.g. the online-users list) alongside a widget. - Loaders: every spinner is replaced with a shimmer (a sized
Skeletonor a row-basedListLoadingSkeleton), all driven by one shared animation loop. - Anchored popovers (GIF picker, comment/sort/notification menus) now detect available space and flip above the trigger when there is no room below.
- Reliable scroll-to-bottom on live-chat load (pins the DOM scroller to its true max so the last message isn't clipped), and the per-message reply/vote toolbar no longer adds dead vertical space.
- Submit buttons (reply, SSO login, edit save) share one filled style, and their icons follow the button text color instead of the page background.
- The comment menu is a three-dot kebab in both modes.
- Voting a comment (or any single-comment update) flashed images in other comments: the list re-rendered every row on each store change, which re-ran react-native-render-html and reloaded the images. Comment rows now re-render only when their own layout inputs change (each row already self-subscribes to its content), so unrelated rows and their images stay put. Also removes a lot of wasted render work on every interaction.
Major release: a complete visual redesign on a semantic theme-token layer, the FastCommentsLiveChat widget, first-class web support via react-native-web, and an upgrade of the dev/test/example toolchain to React Native 0.81 / React 19. The SDK source only uses stable RN APIs, so a consumer's integration code is unaffected; the break is the supported RN/React floor.
- Requires React Native 0.81 / React 19 on the toolchain. The Fabric editor (
react-native-enriched) needs the New Architecture, the default since RN 0.76; consumers on older RN cannot take this as a minor. - Migrates to
fastcomments-sdk3.3.1: GIF methods renamed (search/getTrending/getLarge->getGifsSearch/getGifsTrending/getGifLarge),logout->logoutPublic, page reacts ->getV2PageReacts/createV2PageReact/deleteV2PageReact/getV2PageReactUsers,getUserNotificationsgainsurlId,getCommentsForUserreshaped (dropped tenantId/urlId/sso/cursor params),reactFeedPostPublicdroppedurlId. Enum value imports moved tofastcomments-sdk/server(the bare entry is type-only).
-
config.hideTopBar(FastCommentsRNConfig): hide the logged-in user + notification-bell strip above the composer. -
First-class web overlays: the comment/sort menus, GIF popover, and notification list are portaled to the body and anchored under their trigger via shared
useAnchoredPosition/useDismissOnOutsideClickhooks (capture-phase outside-click that excludes the trigger and content; selection closes the menu explicitly). The notification list opens as an anchored popover under the bell instead of a full-screen modal. -
shadow*styles emit CSSboxShadowon web (react-native-web deprecatesshadow*); native keepsshadow*+elevation. -
FastCommentsThemesemantic token layer (colors, spacing, radii, font sizes, font weights, avatar sizes). New optionalthemeprop onFastCommentsLiveCommenting,FastCommentsFeed, andFastCommentsLiveChatgenerates the entire default style tree from tokens;getLightTheme()/getDarkTheme()/resolveTheme()exported.stylespassed together withthemeis merged on top (styles win);stylesalone keeps the legacy full-replace behavior. -
FastCommentsLiveChatwidget (AndroidLiveChatViewparity): chronological messages, composer below the list, live header strip with connection dot + user count, auto-scroll to new messages (pauses while scrolled up, resumes at the bottom or on your own message), older history loads on scroll-to-top, votes and reply threading disabled. All presets overridable viaconfig. -
config.maxReplyDepth: 0now hides reply buttons (flat mode). -
Page reacts (
config.pageReactConfig): the web widget's page-level react bar, rendered above the composer with per-react counts, selected/unselected images, persistence, and theshowUsersreacted-by list. Backed by new typed page-reacts endpoints in fastcomments-sdk. -
config.voteStyle: VoteStyle.Heart: single like toggle per comment instead of up/down, using the newICON_HEART/ICON_HEART_ACTIVEassets (overridable via theassetsprop, e.g. with a star). -
config.useInlineSubmitButton: renders the submit button as an icon inside the comment box instead of the standalone labeled button. -
config.mentionGroupIdsis now forwarded to mention user search. -
config.countAboveToggle(web widget parity): withuseShowCommentsToggle, the widget starts collapsed like the web widget and renders the first N root comments as a teaser above the Show Comments toggle; the toggle reveals the rest. Toggled widgets previously started expanded and the collapsed state dropped the composer entirely. -
Image-upload and GIF toolbar buttons now render by default like the web widget. The GIF button opens the SDK's built-in GifBrowser (in a Modal, so it overlays the list on web too) and the selection inserts into the comment; hosts can still take over via the
pickGIFcallback. The image button uses a DOM file-input fallback on web; on native it appears once the host suppliespickImage(the SDK ships no native file picker). GifBrowser layout cleaned up (contained images, scrim, sized modal). -
Redesigned default look ("modern neutral"): token-driven palette with hairline separators, pill vote buttons and chips, filled primary action buttons, larger rounded avatars without drop shadows, a consistent 14px+ type scale, and themed dark mode. The dark and erebus skins now derive from the dark theme;
setupDarkModeSkinis deprecated in favor of thethemeprop. -
New style keys:
commentTextArea.toolbarRoot/toolbarFormatButton/toolbarFormatButtonActive/toolbarFormatButtonText,mentionPopup.itemSelected,comment.textLinkStyles(per-tag HTML styles, used for link color),liveChat.root/composerWrapper.
- 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). The composer now sends<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 to<br>-separated content before create and edit. - Guest voting was a silent dead end: a stale anon session was treated as authenticated, the 401 was swallowed, and nothing happened on tap. Ghost anon sessions now route to the vote identity form, request failures render a visible error, and the vote auth inputs are themed.
- A ghost anon session (stale cookie, no username) lit up logged-in chrome: an empty username box in the top bar, a Log Out menu, and a notification bell whose requests 401'd into an infinite spinner. The top bar now requires an identified user, and the notification list handles load failures with a visible error instead of spinning forever.
- Reply indentation was always zero (the depth attached by the list was lost in a store merge), making threads render flat. Replies indent again, and the default indent is larger.
- Dark theme: typed editor text was black-on-black, and light-theme PNG icons (bell, vote arrows, menu icons) were used on dark backgrounds. The editor now receives themed text color, font size, and placeholder;
hasDarkBackgroundis derived from the theme's background luminance so dark icon variants apply automatically. - The composer had no placeholder and stole focus on page load (web).
Alert.alertis a no-op under react-native-web, so canceling a reply, confirming a delete/block, and error display were all dead ends in browsers. Dialogs now route through a platform shim (window.confirm/alert on web).- Bold/italic/underline formatting in posted comments rendered as plain text on web; explicit per-tag styles restore inline formatting.
- The chat widget opened scrolled to the oldest message on web; it now pins to the newest message once content lays out. The comment-count/sort header is suppressed in chat mode.
- Comments beyond the tenant character limit were silently truncated client-side; the SDK now shows the COMMENT_TOO_BIG error instead of losing the tail.
- "Subscribe to this page" never persisted:
getUserNotificationsnow sendsurlId, so the server returns the correct per-pageisSubscribedand the checkbox no longer resets each time the list reopens. onScrolltop-pagination (chat mode) could firedoPaginateNextmore than once between renders; a ref guard closes the double-fire window.
- Username now renders above the "Unverified comment" label (identity first, status second).
- The guest name/email form is progressively disclosed on composer focus instead of rendering permanently.
- Touch targets raised toward the 44px guideline (vote pills, reply, sort, bell, three-dot menu); vote icons enlarged; vote counts show 0 instead of hiding; the modal menus gained a scrim; composer and list share one gutter; the notification bell hides its badge at zero and the BackHandler warning is gone on web.
- New web test lane (
npm run test-web): vitest + jsdom inexample-web/, mounting the SDK through react-native-web with the realreact-native-enriched(tiptap) editor against the real backend; covers the.web.tsx/ react-native-web class of regression the node suite cannot reach. - The jest mock for
react-native-enrichednow mirrors the real editor's HTML contract (<p>-wrapped paragraphs). New E2E specs: LiveChat widget (dual-session two-way exchange, header strip, presets, auto-scroll, load-older), the theme prop, guest submit, multi-paragraph comments. New web-lane specs: anchoredModalMenu(select/dismiss/sub-modal) and the notification popover (anchor + outside-click dismiss). - Verified on the RN 0.81 / React 19 stack:
tsc --noEmitclean, 137 unit tests, 12 web-lane tests, and the tests-ui harness renders under React 19 (render-smoke). The full live E2E suite requires a backend key and the native example app (still RN 0.69) was not rebuilt.
State management rewrite. Internal implementation changed from Hookstate to Zustand, with a flat indexed comment store (byId + childrenByParent + rootOrder + pinnedIds) replacing the previous tree-in-state. Delivers O(1) live-event mutations, removes two full-tree JSON deep-clones on every fetch, and drops the global CommentChangeCounter memoization hack in favor of standard selector-based subscriptions.
FastCommentsCallbacks.onCommentsRenderedsignature changed:- Before:
(comment: ImmutableArray<RNComment>) => void - After:
(comments: readonly RNComment[]) => void - Migration: drop any Hookstate-specific access; treat the argument as a plain readonly array.
- Before:
@hookstate/coreis no longer a dependency. If you were reaching into this SDK's internals and importing Hookstate helpers from it, migrate to plain data access.- The
FastCommentsStateshape exported from./typesis now a backwards-compatible facade only. Internal state lives in the Zustand store returned byFastCommentsLiveCommentingService.createStoreFromConfig(...). Consumers that only used the public component API (<FastCommentsLiveCommenting .../>) are not affected.
At 1000 comments (measured locally, synthetic fixtures):
- 100 sequential vote events: 18 ms (was O(n) per event under Hookstate due to tree rebuilds)
- 100 sequential new-reply insertions: 49 ms
- New-comment insertion: O(1) amortized + O(depth) nested count update (was O(n) + full tree rebuild)
- Badge add/remove: O(k) where k = that user's comment count (was O(n) full scan)
- Comment deletion: O(1) + O(subtree) cleanup (was O(n) findIndex twice)
- Visible flat list: derived selector with referential-identity memoization (was full O(n) rebuild on every mutation)
- New
src/store/module:create-store.ts, slices (comments,config,presence,notifications), selectors (visible-list,comment-by-id), and hook helpers. - Per-comment relative-date timers consolidated into a single shared ticker (
src/services/relative-time-ticker.ts). src/services/comment-render-determination.tsdeleted; memoization is now handled by narrow selectors.src/services/comment-trees.tsslimmed to a normalizer + iteration helpers.