Skip to content

WIP: version 670#28898

Open
chrisnojima wants to merge 89 commits intomasterfrom
nojima/HOTPOT-next-670-clean
Open

WIP: version 670#28898
chrisnojima wants to merge 89 commits intomasterfrom
nojima/HOTPOT-next-670-clean

Conversation

@chrisnojima
Copy link
Contributor

No description provided.

@chrisnojima chrisnojima mentioned this pull request Feb 13, 2026
# Conflicts:
#	shared/constants/init/index.native.tsx
#	shared/constants/platform-specific/index.desktop.tsx
#	shared/constants/router2/index.tsx
#	shared/stores/archive.tsx
#	shared/stores/config.tsx
* move from box to box2
* try automated perf harness
* rename numbered things back to root
* fix device timeline
* modernize jsi (#28941)
* native level cleanup (#28942)
* encode msgpack on cpp side. rpc on just handle arrays.
chrisnojima and others added 30 commits March 4, 2026 17:50
* Migrate inbox native list from FlatList to LegendList with recycling

* Tune LegendList: drawDistance=250 for best scroll FPS
* unify inbox with legend list

* unify more
* kb.list to legends

* merge desktop and native

* lint
* Add teams list perf testing with Maestro flow and React Profiler wraps
* adopt rn ref api to cleanup
* Fix orange line race condition on desktop

Two issues fixed:
1. On desktop, the NormalWrapper component doesn't remount when switching
   conversations (ChatProvider just changes id prop). The initial load
   useEffect had empty deps [], so loadOrangeLine never fired for
   subsequent conversation switches. Changed to depend on [id, loaded].

2. The getUnreadline RPC raced with markThreadAsRead - both fire to the
   Go service, and if mark-read was processed first, the service returned
   no unread line. Fixed by snapshotting meta.readMsgID during render
   (before any effects) and passing it to the RPC. Also wait for loaded
   state so the Go service has messages in its local cache.

* Fix teams divider badge to only count hidden small team unread

The divider was showing smallTeamBadgeCount which includes all small
teams. Now subtracts badge counts from visible small team rows so the
badge only reflects unread messages in hidden/collapsed conversations.

* Fix lint: use useMemo instead of ref writes during render

Replace ref-during-render pattern with useMemo to snapshot readMsgID
when conversation id changes, satisfying react-hooks/refs lint rule.
trim → .trim(), isArray → Array.isArray, includes → .includes(),
assign → Object.assign, clamp → Math.min/max, range → Array.from,
uniq → [...new Set()]
Guard DOM element behind isMobile check so it only renders on desktop.
* Add onClick/style/children to Avatar2, migrate ~60 Avatar usages

Enhance Avatar2 with onClick (including 'profile'), style, and children
props. Migrate all Avatar consumers that don't use rare props (editable,
imageOverrideUrl, lighterPlaceholders, showFollowingStatus, crop, blocked)
to Avatar2.

Convert borderColor prop to boxShadow style inline (with borderRadius),
drop skipBackground/loadingColor (Avatar2 doesn't need them).

Remaining ~10 Avatar usages with rare props will be migrated in a
follow-up PR.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Migrate remaining Avatar usages to Avatar2, delete old Avatar

- Add imageOverrideUrl, crop, and placeholder-on-error to Avatar2
- Migrate all remaining ~10 Avatar consumers to Avatar2
- Handle editable/showFollowingStatus as Avatar2 children
- Drop lighterPlaceholders (use standard placeholder)
- Delete old Avatar component (hooks, index, CSS)
- Remove dead Avatar import from rich-button
- Add start-hot-debug script to help debug Electron

* Rename Avatar2 to Avatar, move into common-adapters/avatar/

Pure rename: Avatar2 → Avatar across ~75 files. Move implementation
from avatar2.{d.ts,desktop.tsx,native.tsx,css} into avatar/ directory.

* Switch desktop avatar from <img> to background-image to eliminate scroll flicker

<img> tags require async decode on mount even from cache, causing a visible
background flash when virtualized list rows remount during scrolling.
CSS background-image paints synchronously from memory cache.

Also add recyclingKey + cachePolicy to native expo-image for better scroll perf.
* Icon2 enhancements + ImageIcon + first migration batch

- Add onClick, className, padding, Huge/Bigger sizes to Icon2
- Create ImageIcon component for image icons (icon-* types)
- Export ImageIcon from common-adapters barrel
- Migrate 17 files from Kb.Icon to Kb.Icon2/ImageIcon

* Migrate more Icon → Icon2/ImageIcon (batch 2)

- search.tsx, jump-to-recent.tsx, reply-preview.tsx, command-status.tsx
- retention-notice.tsx, special-top-message.tsx
- no-conversation.tsx, you-are-reset.tsx, delete-history-warning.tsx
- reset-user.tsx, announcement.tsx

* Migrate Icon → Icon2/ImageIcon (batch 3: chat, teams, login, signup)

~40 files: chat cards, emoji picker, audio, payments, inbox rows,
team confirm modals, add-members wizard, emojis, channel header,
login/signup flows

* Migrate Icon → Icon2/ImageIcon (batch 4: profile, provision, settings, teams, crypto, devices, wallets, unlock-folders)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Migrate Icon → Icon2/ImageIcon (batch 5: chat, fs, teams, tracker, team-building, menubar, settings, router)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Migrate Icon → Icon2/ImageIcon in common-adapters internal files (batch 6)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add IconAuto (auto-delegates to Icon2/ImageIcon based on type) and className to ImageIcon

- IconAuto checks iconMeta[type].isFont at runtime and renders Icon2 or ImageIcon
- Added className prop to ImageIcon (plumbed to <img> on desktop)
- Migrated all remaining dynamic-type Icon usages to IconAuto
- Fixed start-new-chat hoverColor="inital" (typo, no-op on mobile) → Icon2

* Add hoverColor to Icon2 + migrate all remaining Icon usages (complex phase)

- Added hoverColor prop to Icon2 (desktop: CSS class hover_color_*, native: ignored)
- Migrated 5 hoverColor files: banner, popup-dialog, check-circle, checkbox, search-filter
- Migrated 3 boxStyle files: search-filter (merged into style), checkbox (wrapped in Box2), name-with-icon (wrapped in Box2)
- Migrated 4 dynamic-type files to IconAuto: meta, input3.desktop, input3.native, platform-icon
- Zero remaining `import Icon from './icon'` or `<Kb.Icon` usages

* Fix Icon2 desktop default color: use black_50 instead of inheriting pure black

The old Icon component defaulted to globalColors.black_50 when no color
was specified. Icon2 was missing this default, causing icons to render
as pure black instead of the themed gray.

* Fix Icon2 size and default color regressions

- Remove gridSize from Icon2 size calculation (old Icon never used
  gridSize for font size, only sizeType via typeToFontSize)
- Add explicit color to crown icons (yellowDark for owner, black_35
  for admin) at all call sites that relied on old Icon defaultColor
- Add explicit color={black_20} to iconfont-close Icon2 usages

* Migrate all remaining old Icon usages to Icon2/IconAuto/ImageIcon

* Fix Icon2 CSS specificity: use class-based color instead of inline style

When Icon2's color resolves to a CSS variable (e.g. var(--color-black_50)),
use a CSS class (color_black_50) instead of an inline style. Inline styles
have higher specificity than CSS classes, which broke cases like the sidebar
tab icons where external CSS overrides the default color.

* Fix checkbox alignment: use plain div instead of Box2 for icon wrapper

Box2 adds alignSelf:'center' by default which shifted the checkbox
box position. The old Icon used a plain <div> for boxStyle, so match
that behavior.

* Add visual regression testing scripts for desktop

- visual-diff-take.js: captures all 8 app tabs via CDP
- visual-diff-compare.sh: ImageMagick pixel diff with classification
- Updated PERF-TESTING.md with workflow and diff reading guide

* Remove old Icon, rename Icon2 → Icon

Delete the old Icon component (icon.desktop.tsx, icon.native.tsx, icon.d.ts)
and rename Icon2 to Icon across the entire codebase (~180 consumer files).

- Rename types: Icon2Props → IconProps, SizeType2 → SizeType
- Strip icon.shared.tsx of unused functions (defaultColor, defaultHoverColor,
  fontSize, typeToFontSize, paddingStyles)
- Move iconTypeToImgSet into the new icon.tsx with platform branching
- Remove IconStyle type, urlsToImgSet export (no consumers)
- Update barrel exports (index.d.ts, index-impl.js)

* Move iconTypeToImgSet into avatar/ since it's the only consumer

Extract iconTypeToImgSet and getMultsMap from icon.tsx/icon.shared.tsx into
a dedicated helper at common-adapters/avatar/icon-to-img-set.tsx. This keeps
the icon module focused on the Icon component itself.

* Add hint prop to Icon, rendered as title tooltip on desktop

* Restore lost hint props on download folder icons
* Use satisfies operator for type-safe object literals

Replace type annotations with satisfies on const object literals across
constants/fs.tsx, stores, and other config objects. This preserves
narrower literal types while still validating structure at compile time.

* Use Promise.withResolvers() for cleaner async patterns

Replace new Promise((resolve, reject) => { ... }) constructor pattern
with Promise.withResolvers() in 5 files. This flattens the code by
removing one level of nesting in callback-heavy async code.

* More satisfies conversions: styles, nav options, config objects

Convert type-annotated const objects in button.tsx, icon2.tsx,
avatar, router options, and inbox-rows to use satisfies.

* Final satisfies batch: installer, contacts, menubar

* Fix lint: revert installer satisfies, use undefined for Promise.withResolvers

The installer errorTypes with satisfies narrowed booleans to literal
false, causing lint errors. Reverted to type annotation.

Promise.withResolvers<void>() triggers no-invalid-void-type lint rule.
Changed to <undefined> and updated resolve() calls to resolve(undefined).

* Allow void in generic type arguments, use undefined for withResolvers

Relax no-invalid-void-type to allow void in all generic type arguments
(was only Promise). Promise.withResolvers<void>() still triggers the
rule since it's a method call not a type, so use <undefined> there.

* Revert Promise.withResolvers usage — not supported by Hermes

Reverts back to new Promise((resolve, reject) => {...}) pattern
in all 5 files that were converted.
Default list-common.tsx to index-based keys when keyProperty is absent,
and add keyProperty to 7 Kb.List sites that have natural unique keys.
Enable item recycling for the teams list by moving header (buttons, sort)
and footer into ListHeaderComponent/ListFooterComponent, so the data array
contains only fixed-height team rows. This drops TeamRow mounts from 133
to 18 during scroll, cutting total React render time by 50% (3863ms →
1921ms) and improving worst-case FPS from 25 to 43.

Add ListHeaderComponent, ListFooterComponent, and recycleItems props to
Kb.List. Remove dead banner code and unused firstItem prop from TeamRow.
* Migrate inbox to use Kb.List instead of LegendList directly

Extend Kb.List with props needed by the inbox (onViewableItemsChanged,
viewabilityConfig, getItemType, drawDistance, keyExtractor, perItem
itemHeight variant) and replace direct LegendList usage in both desktop
and native inbox. Also rename desktopRef to ref (React 19 supports ref
as a regular prop) and re-export LegendListRef from common-adapters.
The node engine's NativeTransport.packetize_data forwards all responses
to the renderer without processing them locally, so RPCs sent from the
node process never get responses. The logger's periodic dump was sending
appendGUILogs through the node engine, creating a session that stayed
outstanding forever.

Check process.type to skip log sending in the node (main) process.
…670-clean

# Conflicts:
#	go/libkb/version.go
#	shared/android/app/build.gradle
#	shared/chat/inbox/filter-row.tsx
#	shared/ios/Keybase/Info.plist
#	shared/ios/KeybaseShare/Info.plist
…ontext (#29008)

React 19 supports using <Context value={...}> directly instead of
<Context.Provider value={...}>. Migrate all context providers to the new
syntax and remove the deprecated CanFixOverdrawContext entirely.
* fix copy text clipping

* Replace PopupWrapper and MobilePopup with Modal2 across 17 consumers

Steps 1-2 of modal cleanup: convert all PopupWrapper usages to Modal2
with proper header/onClose patterns. Add popupStyleClipContainer prop
to Modal2 for consumers whose content width differs from the default
400px mode. Convert MobilePopup in join-from-invite to Modal.

* Fix download icon positioning on mobile image/video attachments

Use RNText onPress instead of wrapping in Pressable in Kb.Icon native,
so style props like position/left apply to the actual rendered element.

* Replace direct PopupDialog and MaybePopup uses with Modal2

Step 3 of modal cleanup: convert 7 files that directly used PopupDialog
or MaybePopup to use Modal2 instead. Removes Wrapper shim components
from warning dialogs, converts fullscreen attachment viewer with
popupStyleContainer fill, and migrates proxy settings from
HeaderHocWrapper to Modal2 header.

* Switch ConfirmModal internals from Modal to Modal2

Step 4 of modal cleanup: no consumer changes needed, just swaps the
internal wrapper from deprecated Modal to Modal2.

* Migrate all Kb.Modal consumers to Kb.Modal2 (45 files)

* Remove dead modal components: Modal, PopupWrapper barrel, PopupDialog barrel, MaybePopup

* Add bare prop to Modal2 for router-managed modal screens

The old Modal2 was a bare fragment (header + children + footer with no
wrapping). Our Modal cleanup added PopupDialog/ScrollView wrapping which
broke the 3 pre-existing Modal2 consumers whose router already provides
the outer chrome. bare={true} restores the original behavior.

* Replace HeaderHocWrapper consumers with Modal2 bare

Convert the last 2 HeaderHocWrapper consumers to Modal2 bare and remove
HeaderHocWrapper from barrel exports.

* Delete dead HeaderHocWrapper and header-or-popup

Remove HeaderHocWrapper from header-hoc source files (zero consumers
after barrel export removal) and delete orphaned header-or-popup.tsx.
Clean up unused imports.

* Convert MobilePopup from FloatingBox overlay to BottomSheetModal

Replace the custom FloatingBox+KeyboardAvoidingView+underlay overlay
with @gorhom/bottom-sheet BottomSheetModal, following the same pattern
as FloatingMenu. Add onDismiss prop for swipe-to-dismiss support and
snapPoints for height control. Update all 3 consumers to pass dismiss
callbacks.

* Remove Modal2 wrapper: screens render ModalHeader/ModalFooter directly

- Delete Modal2, PopupDialog, Overlay, MobilePopup components
- Add unified Popup component (desktop/native)
- Router layout defaults modal2=true, provides overlay/close button
- Remove modal2Type system (Wide/SuperWide/etc) — screens set own sizes
- Remove ScrollView from modal chrome — screens manage own scrolling
- Convert all ~100 modal screens to render header/footer directly

* Fix desktop modal layout: sizing, escape key, and lazy-load flash

- Change default modal height from maxHeight to fixed height (560px)
  so virtualized lists get a proper parent height
- Add modalStyle to LayoutOptions for per-route size overrides
- Add modalStyle overrides for modals needing non-default dimensions:
  deviceAdd (620w), profileEdit (350x450), profileGeneric* (560x485),
  profileShowcaseTeamOffer (600x600), teamRename (560x480),
  kextPermission (700w), chatAttachmentFullscreen, chatPDF
- Fix fullHeight on modal container to prevent box2_centered collapse
- Replace stack-based EscapeHandler with capture-phase window listener
  to ensure Escape always closes the topmost modal
- Move React.Suspense outside ModalWrapper to prevent white box flash
  while lazy content loads

* Move modal headers to router getOptions for idiomatic React Navigation

Layout wrappers (desktop + native) now read standard React Navigation
header options (title, headerTitle, headerLeft, headerRight, headerShown)
from getOptions and render ModalHeader. Screens declare headers
declaratively instead of rendering them inline.

Migrated ~25 screens across chat, teams, settings, profile, git, login,
and deeplinks modules. Screens with dynamic/state-dependent headers
remain inline for now. Native layout only renders headers for modal
screens to avoid doubling with RN's native header on regular screens.

* Remove inline headers from modal screens, use native React Navigation headers

- Move header configuration to getOptions in route definitions instead of
  rendering HeaderHocHeader/ModalHeader inline in components
- Remove setOptions usage for headers that can be declared at push time
- Add title: '' to modal group defaults to prevent route names leaking as titles
- Move team-building headers to page.tsx using direct zustand store access
- Remove WalletPopup inline headers, use default modal Cancel
- Add skipMobileHeader to signup modal screens with route-level Skip buttons
- Hide native header for screens with custom layouts (emoji picker, attachment titles)
- Delete dead modal-header-props.tsx
- Move account switcher Sign Out button to route getOptions

* Fix headerShown: target attachment fullscreen, not get-titles

* Rename modal2* layout options to cleaner names and add headerShown:false for fs barePreview

- modal2Style → overlayStyle, modal2AvoidTabs → overlayAvoidTabs
- modal2ClearCover → overlayTransparent, modal2NoClose → overlayNoClose
- modal2Footer → modalFooter, modal2Header → modalHeader
- Delete dead modal2 boolean branch and modalContainer style
- Hide native header for fs/barePreview (custom fullscreen layout)

* Migrate static setOptions to getOptions in route definitions

Move header configuration (title, headerLeft, headerRight, headerStyle)
from runtime setOptions calls in screen components to static getOptions
in route definitions. This follows the idiomatic React Navigation pattern
where static header config lives in the route definition rather than
being set imperatively from inside the screen.

Also fixes desktop info panel not opening (StrictMode double-effect
cleanup was calling showInfoPanel(false) on desktop where the panel
is inline), changes default modal height to maxHeight, and adds
modalStyle overrides for routes that used the old modal2Type system.

* initial skills

* Delete modal2.tsx: inline ModalFooter as local styles, inline ModalHeader into consumers

ModalFooter replaced with <Box2> + local modalFooter style definitions
across 37+ screen files. ModalHeader inlined into its 3 remaining
consumers (signup/common, screen-layout.desktop, login/reset/modal,
provision/troubleshooting). Removed ModalFooter, ModalHeader,
useModalHeaderTitleAndCancel, modalFooterStyle, modalFooterNoBorderStyle
from barrel exports.

* Delete PopupHeaderText, Overlay, MobilePopup from barrel exports

PopupHeaderText inlined as styled Text in its 3 consumers.
Overlay and MobilePopup were unused aliases for Popup — removed
from barrel exports.

* Unify popup abstractions: migrate FloatingBox consumers to Popup, move into popup/ directory

- Extend native Popup with Portal mode (attachTo → Portal, no onHidden → Portal, onHidden → BottomSheet)
- Make onHidden optional on Popup; desktop skips positioned chrome when absent
- Migrate FloatingBox consumers to Popup (role-picker, add-alias, emoji-row, suggestors, selection-popup, toast)
- Remove FloatingBox from barrel exports (now internal to popup/)
- Move popup, floating-box, bottom-sheet, use-popup into common-adapters/popup/ directory

* Clean up HeaderHoc: remove HeaderHocHeader, HeaderLeftBlank, internalize LeftAction

* Unify HeaderLeftArrow/Cancel/Cancel2 into single HeaderLeftButton with mode prop

Replaces 4 overlapping header button components with one unified
HeaderLeftButton that accepts mode ('back'|'cancel') and
autoDetectCanGoBack props. Also cleans up dead LeftAction props
from deleted HeaderHocHeader. Renames header-hoc/ to header-buttons.

* Add desktop stub for bottom-sheet (mobile-only module)

* Add extraData to Kb.List usages so LegendList re-renders items on state change

* fix import cycle

* Improve attachment fullscreen ellipsis menu hit area and simplify Icon

- Add padding to the ellipsis icon for a larger click target
- Add windowDraggingClickable to opt out of the Electron drag region
- Simplify desktop Icon: render a single span instead of div+span wrapper

* fix mobile attach flow

* Fix attachment titles dialog missing white background on desktop

* Fix Avatar crash on iOS for big team channel headers

Size 12 isn't a valid Avatar size and was cast as 16, but the native
Avatar's styleCache doesn't have an entry for it, causing a crash.

* Fix bottom sheet not expandable when pulled up

Restore enableDynamicSizing={true} and default snap points to match
the previous FloatingMenu behavior, allowing sheets to grow when
dragged upward.

* Remove orange focus outline from inbox list on desktop

* Fix info panel header styling

* Fix AudioRecorder crash on iOS when entering a thread

Guard recorder.stop() to only run when a recording was actually started,
preventing a SharedObject error on component unmount.

* Fix suggestion list zero height causing LegendList warning

The suggestion list container had maxHeight but no explicit height,
so LegendList's height:100% resolved to 0. Compute a concrete pixel
height from item count capped at 224px.

* Fix source map CSP violation in Electron dev mode

Add webpack: to the connect-src CSP directive so clicking stack trace
links in DevTools resolves source maps instead of showing a CSP error.

* Fix popups showing as positioned overlays instead of bottom sheets on mobile

Pass undefined for attachTo on mobile so Popup uses bottom sheet behavior.
Previously mobile ignored attachTo, but now it respects it, causing menus
and popups to render as positioned overlays instead of bottom sheets.

* WIP

* Fix PDF modal centering and height on desktop

Center all desktop modals in the overlay and override maxHeight for
the PDF viewer so it actually reaches 80% of the viewport.

* Fix chat message popup menu clipped by adjacent messages on desktop

* move skills, add claude

* moved to .claude

* fix useRpc memoizing. team wizard sizing consistent

* Add "Add" button to team builder nav header on mobile

* Fix button waiting spinner centering on mobile and team builder header reactivity

- Wrap mobile button spinner in absolute-positioned View so it centers over the button
- Add HeaderRightUpdater inside TBProvider to drive header updates via setOptions,
  fixing the Add button not appearing when users are selected

* Remove FloatingModalContext, replace with mode prop on FloatingMenu

* Replace all 17 imperative nav.setOptions calls with declarative header components

Components now write dynamic state to a new ModalHeaderStore (stores/modal-header.tsx),
and header components in routes.tsx files read from it. This eliminates imperative
header mutations from useEffect and removes setOptions from the useNav() hook entirely.

* Fix team info dialog not dismissing after save

* WIP

* WIP

* Fix FloatingRolePicker styling after FloatingBox→Popup migration

Remove the role picker's own border/shadow since Popup now provides
elevation styling. Remove all floatingContainerStyle position hacks
(relative offsets) that caused see-through gaps inside the Popup wrapper.

* Remove duplicate header from emoji modals, use native headers

The emoji Modal component rendered its own title header on desktop,
duplicating the router-provided header. Remove the custom header and
rely on native headers. Add Cancel button on mobile via HeaderLeftButton.

* Remove duplicate headers and redundant hardcoded sizes from modal components

Modal routes already define title and modalStyle dimensions via the
router. Components were duplicating both, causing double headers and
size conflicts that hid content (e.g. missing Save button in Edit
Profile).

- Remove desktop-only header text from: edit-profile, proofs-list,
  showcase-team-offer, add-device
- Remove hardcoded width/height from container styles in: edit-profile,
  enter-username, result, proofs-list, showcase-team-offer
- Fix proofs-list flex layout so it fills the modal properly

* Add cancel button to user avatar upload modal header on iOS

* Clear all modals after successfully adding user to team

navigateUp() only popped the add-to-team modal, leaving the search
dialog with a spinner stuck underneath. Use clearModals() instead.

* Fix team showcase modal list overflowing its container on desktop

* Fix phone number prefix container being too narrow on iOS

* Fix destination picker modal content not displaying on desktop

The Move or Copy modal had no explicit height, causing the folder
list to collapse to zero. Add modalStyle with proper dimensions to
the route definition, replacing the old Kb.Modal wrapper sizing.

* Fix device list overlapping header text on desktop sign-in screen

The virtualized list header height was 60px but the actual content
(3 lines of text + padding) needs ~90px, causing items to overlap.

* Enable native navigation headers for logged-out screens on iOS

Remove the custom ModalHeader from SignupScreen and use React Navigation's
native header instead. Add titles to all logged-out route definitions so
they display properly in the nav header.

* default tab nav stuff

* Fix share extension crash on iOS by passing nil for options

The options dictionary in the unsafeBitCast call to UIApplication.open
was causing a crash because iOS internally calls universalLinksOnly on
the options object, which fails on Swift/NSDictionary types. Passing
nil instead lets ObjC nil-messaging safely return false.
* convostate cleanup. claude cleanup

* fix lint

* fix messageUnchanged dropping isCollapsed and unfurl collapse updates

MessagesUpdated events (e.g. from ToggleMessageCollapse) were silently
dropped by the messageUnchanged fast-path, which didn't check isCollapsed
on messages or compare unfurl content when the unfurl count was unchanged.

* fix messageUnchanged missing isEditable, isDeleteable, inlinePaymentSuccessful

These fields can change independently via MessagesUpdated without any
other content changing, so they must be checked in the fast-path skip.

* replace messageUnchanged with in-place mergeMessage using Immer

Instead of a fragile field-by-field skip check, merge incoming message
fields directly onto the existing draft so Immer's per-property change
detection handles re-render granularity automatically. Maps are updated
in-place (with deletes) for the same reason. HiddenString gets an equals()
method so comparison stays encapsulated without exposing stringValue().

* fix mergeMessage crash when cur is undefined for HiddenString field

val.equals(cur as HiddenString) throws if cur is undefined, e.g. when
decoratedText is absent on the existing message but present on incoming.
Check cur instanceof HiddenString first before calling equals.

* fix mergeMessage to resolve fields from both existing and incoming

Using only Object.entries(incoming) meant fields present on the existing
message but absent from incoming (e.g. submitState: 'pending' on a pending
message that gets confirmed) were never cleared. Now iterating the union of
both objects' keys so absent-on-incoming fields are correctly set to undefined.

* lint -n

* normalize empty reactions/unfurls maps to undefined at construction

reactionMapToReactions returned new Map([]) when r.reactions was a truthy
empty object, and unfurls built an empty Map for empty arrays. Both cases
caused mergeMessage to see a reference change where there was no real change.
Normalize to undefined so Immer detects no diff.

---------

Co-authored-by: chrisnojima <cnojima@keyba.se>
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.

4 participants