Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
dd2d3cc
feat(chat): re-add native LegendList for the message thread
chrisnojima Jul 9, 2026
9712f15
fix(chat): keep native thread pinned to bottom through keyboard dismi…
chrisnojima Jul 17, 2026
b1616d9
fix(chat): land search hits reliably via patched legend-list
chrisnojima Aug 11, 2026
3effec3
fix(chat): rework the legend-list scroll fixes after review
chrisnojima Aug 11, 2026
d7b9fb2
fix(chat): address second-round review of the legend-list patch
chrisnojima Aug 11, 2026
cb8883f
fix(chat): scroll to a search hit once, not on every index shift
chrisnojima Aug 11, 2026
e31d207
test(e2e): drive thread search hits on device
chrisnojima Aug 11, 2026
103105f
test(e2e): make the drag-after-hit case provoke a real page-in
chrisnojima Aug 11, 2026
d4ba158
test(e2e): make the thread-search flow addressable and honest
chrisnojima Aug 11, 2026
0aeeded
test(e2e): provoke the page-in after the reader moves, not before
chrisnojima Aug 12, 2026
6bb595d
test(e2e): drag until the hit is gone, rather than a fixed number of …
chrisnojima Aug 12, 2026
71ff880
test(e2e): stop retrying the thread-search flow
chrisnojima Aug 12, 2026
05e5bbc
test(e2e): make retries opt-in per flow rather than the default
chrisnojima Aug 12, 2026
f7b514c
test(e2e): let the reset escape a keyboard and a modal it could not b…
chrisnojima Aug 12, 2026
7a52280
test(e2e): click the feed row, not the header avatar
chrisnojima Aug 12, 2026
850f575
fix(chat): update legend-list patch, which had stopped reaching searc…
chrisnojima Aug 12, 2026
00510ec
test(e2e): fix what review found in the harness, and read the hit cou…
chrisnojima Aug 12, 2026
bdeb2b9
test(e2e): make the reset's waits mean what they say
chrisnojima Aug 12, 2026
9ee334e
fix(chat): give the native thread the resets the desktop one already had
chrisnojima Aug 12, 2026
7218758
test(e2e): cover the desktop search cases, and stop two checks from l…
chrisnojima Aug 12, 2026
22862ae
test(e2e): stop the search flow from timing out as the thread grows
chrisnojima Aug 12, 2026
340a758
test(e2e): point the search flows at a named conversation
chrisnojima Aug 12, 2026
545c8b5
fix(chat): drop the library change we could not show we needed
chrisnojima Aug 12, 2026
30da1e9
fix(chat): replace the borrowed-room scroll fix with re-issuing the s…
chrisnojima Aug 12, 2026
191de70
fix(chat): apply what the reviewers found
chrisnojima Aug 13, 2026
d1911fa
test(e2e): make the desktop search flow able to fail
chrisnojima Aug 13, 2026
2cbbf2b
fix(chat): one centred-scroll hook for both platforms
chrisnojima Aug 13, 2026
fab97c5
fix(chat): open a thread on its newest message
chrisnojima Aug 13, 2026
8aa43c5
test(e2e): check that opening a thread lands on its newest message
chrisnojima Aug 13, 2026
b239d85
fix(chat): rebuild the legend-list patch against 3.3.7
chrisnojima Aug 18, 2026
2529570
fix(chat): let the list bootstrap own the first centered scroll
chrisnojima Aug 19, 2026
d3ff0dc
fix(chat): mirror the list's own non-empty-data predicate
chrisnojima Aug 19, 2026
cee1628
fix(chat): rebuild the legend-list patch with the header end-anchor t…
chrisnojima Aug 19, 2026
d1c2a2e
test(e2e): cover opening a conversation onto a linked message
chrisnojima Aug 19, 2026
2d428e5
fix(chat): always issue the centred scroll
chrisnojima Aug 19, 2026
dbe74c1
test(e2e): reuse an existing link message in chat-link-jump
chrisnojima Aug 19, 2026
96345f7
test(e2e): open each conversation for real before measuring its end
chrisnojima Aug 19, 2026
459587c
fix(chat): pass maintainVisibleContentPosition as the documented boolean
chrisnojima Aug 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion shared/chat/conversation/header-area/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ const HeaderAreaRight = (props: HeaderConversationProps) => {
noShrink={true}
style={Kb.Styles.collapseStyles([styles.headerRight, {opacity: pendingWaiting ? 0 : 1}])}
>
<Kb.Icon type="iconfont-search" onClick={onToggleThreadSearch} />
<Kb.Icon type="iconfont-search" onClick={onToggleThreadSearch} testID={TestIDs.CHAT_HEADER_SEARCH_BUTTON} />
<Kb.Icon type="iconfont-info" onClick={onShowInfoPanel} testID={TestIDs.CHAT_HEADER_INFO_BUTTON} />
</Kb.Box2>
)
Expand Down
651 changes: 162 additions & 489 deletions shared/chat/conversation/list-area/index.tsx

Large diffs are not rendered by default.

9 changes: 8 additions & 1 deletion shared/chat/conversation/messages/special-top-message.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
import {useConversationParticipantsSelector} from '../data-hooks'
import * as FS from '@/constants/fs'
import {useCurrentUserState} from '@/stores/current-user'
import * as TestIDs from '@/tests/e2e/shared/test-ids'

const ErrorMessage = () => {
const styles = useStyles()
Expand Down Expand Up @@ -153,7 +154,13 @@ function SpecialTopMessage() {
}

return (
<Kb.Box2 direction="vertical" fullWidth={true} style={styles.container}>
<Kb.Box2
direction="vertical"
fullWidth={true}
style={styles.container}
testID={TestIDs.CHAT_THREAD_TOP}
collapsable={false}
>
{hasLoadedEver && loadMoreType === 'noMoreToLoad' && showRetentionNotice && <RetentionNotice />}
<Kb.Box2 direction="vertical" style={styles.spacer} />
{hasOlderResetConversation && <ProfileResetNotice />}
Expand Down
17 changes: 13 additions & 4 deletions shared/chat/conversation/messages/wrapper/long-pressable/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ type Props = {
import {useConversationThreadToggleSearch} from '../../../thread-context'
import Swipeable, {type SwipeableMethods} from '@/common-adapters/swipeable-row'
import {ThreadRefsContext} from '@/chat/conversation/normal/context'
import {useAdaptiveRender} from '@legendapp/list/react-native'

function ReplyIcon({progress}: {progress: Animated.Value}) {
const styles = useStyles()
Expand All @@ -28,16 +29,23 @@ function ReplyIcon({progress}: {progress: Animated.Value}) {
}

function LongPressable(props: Props & {ref?: React.Ref<Kb.MeasureRef>}) {
if (!isMobile) {
return <Kb.Box2 direction="horizontal" fullWidth={true} {...props} />
}
return <LongPressableMobile {...props} />
}

function LongPressableMobile(props: Props & {ref?: React.Ref<Kb.MeasureRef>}) {
const styles = useStyles()
const toggleThreadSearch = useConversationThreadToggleSearch()
const setReplyTo = InputState.useConversationInputDispatch(s => s.setReplyTo)
const ordinal = useOrdinal()
const {focusInput} = React.useContext(ThreadRefsContext)
const swipeRef = React.useRef<SwipeableMethods | null>(null)

if (!isMobile) {
return <Kb.Box2 direction="horizontal" fullWidth={true} {...props} />
}
// Velocity-driven signal from LegendList: during fast scroll it flips to "light". We keep the
// Swipeable mounted (toggling its tree would remount children and flash images) and instead just
// disable its pan handlers in light mode, shedding the per-row touch evaluation during the fling.
const adaptiveMode = useAdaptiveRender()

const {children, onLongPress, style} = props

Expand Down Expand Up @@ -68,6 +76,7 @@ function LongPressable(props: Props & {ref?: React.Ref<Kb.MeasureRef>}) {
return (
<Swipeable
ref={swipeRef}
enabled={adaptiveMode !== 'light'}
renderRightActions={makeAction}
onSwipeableWillOpen={onSwipeableWillOpen}
>
Expand Down
10 changes: 5 additions & 5 deletions shared/chat/conversation/messages/wrapper/sent.native.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import type * as React from 'react'
import Animated, {FadeInDown} from 'react-native-reanimated'

// Slide-up + fade for a message you just sent. The thread list is an inverted
// FlatList (cells are flipped with scaleY: -1), so FadeInDown renders on screen
// as sliding up from below. Runs entirely on the UI thread with no re-renders.
// The entering animation only plays when this Animated.View MOUNTS — callers must
// key it per message (recycled containers reuse instances).
// Slide-up + fade for a message you just sent. The thread list (LegendList) is
// NOT inverted, so FadeInDown (enters from 25px below, sliding up into place)
// reads as the row rising from the input bar. Runs entirely on the UI thread
// with no re-renders. The entering animation only plays when this Animated.View
// MOUNTS — callers must key it per message (recycled containers reuse instances).
export function Sent(p: {children: React.ReactNode}) {
return (
<Animated.View entering={FadeInDown.duration(200)} style={styles.container}>
Expand Down
15 changes: 13 additions & 2 deletions shared/chat/conversation/messages/wrapper/wrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {emptyParticipantInfo} from '../../data-hooks'
import {useInboxMetadataState} from '@/chat/inbox/metadata'
import type {ConversationInputState} from '../../input-area/input-state'
import {useChatTeamMemberRole} from '../../team-hooks'
import * as TestIDs from '@/tests/e2e/shared/test-ids'

type AccountsInfoMap = ReadonlyMap<T.RPCChat.MessageID, T.Chat.ChatRequestInfo | T.Chat.ChatPaymentInfo>
type PaymentStatusMap = ReadonlyMap<T.Wallets.PaymentID, T.Chat.ChatPaymentInfo>
Expand Down Expand Up @@ -905,7 +906,11 @@ function RightSide(p: RProps) {
)}
>
<Kb.Box2 direction="vertical">
<Kb.Icon type="iconfont-ellipsis" onClick={showPopup} />
<Kb.Icon
type="iconfont-ellipsis"
onClick={showPopup}
testID={TestIDs.CHAT_MESSAGE_MENU_BUTTON}
/>
</Kb.Box2>
</Kb.Box2>
)
Expand Down Expand Up @@ -1018,7 +1023,13 @@ export function WrapperMessage(p: WrapperMessageProps) {
const messageContext = {isHighlighted: showCenteredHighlight, ordinal}

const row = (
<Kb.Box2 direction="vertical" relative={true} fullWidth={true}>
<Kb.Box2
direction="vertical"
relative={true}
fullWidth={true}
collapsable={false}
testID={showCenteredHighlight ? TestIDs.CHAT_SEARCH_HIT : undefined}
>
<AuthorHeader
author={author}
botAlias={botAlias}
Expand Down
40 changes: 36 additions & 4 deletions shared/chat/conversation/search.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
} from './thread-context'
import {useThreadSearchRoute} from './thread-search-route'
import {ThreadSearchOverlayContext} from './thread-search-overlay-context'
import * as TestIDs from '@/tests/e2e/shared/test-ids'

type OwnProps = {style?: Kb.Styles.StylesCrossPlatform}
type CommonProps = OwnProps & {
Expand Down Expand Up @@ -434,8 +435,15 @@ const ThreadSearchDesktopInner = function ThreadSearchDesktopInner(p: CommonProp
</Kb.Box2>
<Kb.Box2 direction="horizontal" gap="tiny" noShrink={true}>
{inProgress && <Kb.ProgressIndicator style={styles.progress} />}
{/* collapsable={false}: Android view flattening would drop this testID'd wrapper and
leave the count unreadable to the e2e suite. */}
{hasResults && (
<Kb.Box2 direction="horizontal" gap="tiny">
<Kb.Box2
direction="horizontal"
gap="tiny"
collapsable={false}
testID={TestIDs.CHAT_THREAD_SEARCH_COUNT}
>
<Kb.Text type="BodySmall" style={styles.results}>
{noResults ? 'No results' : `${selectedIndex + 1} of ${hits.length}`}
</Kb.Text>
Expand Down Expand Up @@ -506,13 +514,28 @@ const ThreadSearchMobileInner = function ThreadSearchMobileInner(p: CommonProps)
return (
<Kb.Box2 direction="vertical" fullWidth={true} style={styles.mobileContainer} onLayout={onLayout}>
<Kb.Box2 direction="horizontal" fullWidth={true} justifyContent="space-between" padding="tiny" style={styles.outerContainer} gap="tiny">
<Kb.Box2 direction="horizontal" centerChildren={true} noShrink={true}>
<Kb.Box2
direction="horizontal"
centerChildren={true}
noShrink={true}
collapsable={false}
testID={TestIDs.CHAT_THREAD_SEARCH_CANCEL}
>
<Kb.Text type="BodySemibold" style={styles.done} onClick={onToggleThreadSearch}>
Cancel
</Kb.Text>
</Kb.Box2>
<Kb.Box2 direction="horizontal" justifyContent="space-between" style={styles.inputContainer}>
<Kb.Box2 direction="horizontal" gap="xtiny" flex={1} centerChildren={true}>
{/* collapsable={false}: keep this testID'd wrapper (and the EditText under it) as a real
view on Android, where view flattening would otherwise render it as an empty leaf. */}
<Kb.Box2
direction="horizontal"
gap="xtiny"
flex={1}
centerChildren={true}
collapsable={false}
testID={TestIDs.CHAT_THREAD_SEARCH_INPUT}
>
<Kb.Input3
ref={inputRef}
autoFocus={false}
Expand All @@ -527,8 +550,15 @@ const ThreadSearchMobileInner = function ThreadSearchMobileInner(p: CommonProps)
</Kb.Box2>
<Kb.Box2 direction="horizontal" gap="tiny" noShrink={true}>
{inProgress && <Kb.ProgressIndicator style={styles.progress} />}
{/* collapsable={false}: Android view flattening would drop this testID'd wrapper and
leave the count unreadable to the e2e suite. */}
{hasResults && (
<Kb.Box2 direction="horizontal" gap="tiny">
<Kb.Box2
direction="horizontal"
gap="tiny"
collapsable={false}
testID={TestIDs.CHAT_THREAD_SEARCH_COUNT}
>
<Kb.Text type="BodySmall" style={styles.results}>
{status === 'done' && numHits === 0 ? 'No results' : `${selectedIndex + 1} of ${numHits}`}
</Kb.Text>
Expand All @@ -541,11 +571,13 @@ const ThreadSearchMobileInner = function ThreadSearchMobileInner(p: CommonProps)
color={numHits > 0 ? theme.blue : theme.black_50}
onClick={onUp}
type="iconfont-arrow-up"
testID={TestIDs.CHAT_THREAD_SEARCH_PREV}
/>
<Kb.Icon
color={numHits > 0 ? theme.blue : theme.black_50}
onClick={onDown}
type="iconfont-arrow-down"
testID={TestIDs.CHAT_THREAD_SEARCH_NEXT}
/>
</Kb.Box2>
</Kb.Box2>
Expand Down
8 changes: 5 additions & 3 deletions shared/chat/conversation/thread-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,11 @@ ConversationThreadIDContext.displayName = 'ConversationThreadIDContext'

export type ConversationThreadState = {
accountsInfoMap: Map<T.RPCChat.MessageID, T.Chat.ChatRequestInfo | T.Chat.ChatPaymentInfo>
// Bumped on every messagesClear. The desktop list remounts on it: LegendList cannot recover
// from a non-empty -> empty -> non-empty data transition (it resets its layout state and waits
// for a container layout event that never comes), so the thread renders blank forever.
// Bumped on every messagesClear, and fed to the list as its dataKey (not as a React key - the
// list is not remounted). LegendList cannot recover from a non-empty -> empty -> non-empty data
// transition on its own (it resets its layout state and waits for a container layout event that
// never comes), so the thread renders blank forever; the dataKey change is what tells it this is
// a new dataset and makes it reset rather than wait.
clearVersion: number
explodingMode: number
flipStatusMap: Map<string, T.RPCChat.UICoinFlipStatus>
Expand Down
8 changes: 7 additions & 1 deletion shared/chat/inbox-and-conversation-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {navToPath} from '@/constants/fs'
import {showConversationInfoPanel, toggleConversationThreadSearch} from '@/chat/conversation/thread-context'
import {muteConversation} from '@/chat/conversation/status-actions'
import AccountSwitchHeaderAvatar from '@/router-v2/account-switch-header-avatar'
import * as TestIDs from '@/tests/e2e/shared/test-ids'

const emptyMeta = Chat.makeConversationMeta()
const emptyParticipantInfo = Chat.uiParticipantsToParticipantInfo([])
Expand Down Expand Up @@ -245,7 +246,12 @@ const Header = () => {
direction="vertical"
tooltip={`Search in this chat (${C.shortcutSymbol}F)`}
>
<Kb.Icon style={styles.clickable} type="iconfont-search" onClick={onToggleThreadSearch} />
<Kb.Icon
style={styles.clickable}
type="iconfont-search"
onClick={onToggleThreadSearch}
testID={TestIDs.CHAT_HEADER_SEARCH_BUTTON}
/>
</Kb.Box2>
<Kb.Box2
className="tooltip-left"
Expand Down
9 changes: 8 additions & 1 deletion shared/chat/inbox/row/big-team-channel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type * as React from 'react'
import * as Kb from '@/common-adapters'
import * as RowSizes from './sizes'
import * as T from '@/constants/types'
import * as TestIDs from '@/tests/e2e/shared/test-ids'
import {useInboxRowBig} from '@/chat/inbox/rows-state'
type Props = {
conversationIDKey: string
Expand Down Expand Up @@ -92,7 +93,13 @@ const BigTeamChannel = (props: Props) => {
) : null

return (
<Kb.ClickableBox direction="vertical" fullWidth={true} onClick={onSelectConversation} style={styles.container}>
<Kb.ClickableBox
direction="vertical"
fullWidth={true}
onClick={onSelectConversation}
style={styles.container}
testID={TestIDs.CHAT_INBOX_CHANNEL_ROW}
>
<Kb.Box2 direction="horizontal" fullHeight={true} style={styles.rowContainer}>
<Kb.Box2
className="hover_background_color_blueGreyDark"
Expand Down
Loading