Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
37 changes: 37 additions & 0 deletions apps/mobile/e2e/flows/phase4a-work-rows.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -219,3 +219,40 @@ env:
- assertVisible:
id: "timeline-workflow-usage"
- takeScreenshot: phase4a-work-rows-workflow-failed
# The closing assistant message carries a table wider than the screen. It
# must scroll sideways under the message's long-press Pressables (new
# architecture: a ScrollView will not scroll while an ancestor is the JS
# responder), and a long-press on it must still open the message actions.
- scrollUntilVisible:
element:
text: "(?s).*Summary of the changes.*"
direction: DOWN
timeout: 30000
speed: 40
# The table is the last content on the page: bottom the list out so the
# whole table is on screen on any device height. (Cells inside the nested
# horizontal ScrollView report as visible before they are, so they are not
# reliable scroll targets.)
- scroll
- scroll
- scroll
- assertNotVisible: "(?s).*scroll sideways inside the timeline.*"
- swipe:
from:
text: ".*horizontal scroll.*"
direction: LEFT
duration: 600
- extendedWaitUntil:
visible: "(?s).*scroll sideways inside the timeline.*"
timeout: 10000
- takeScreenshot: phase4a-work-rows-table-scrolled
# The Reviewer column is fully on screen after the swipe; long-press lands
# inside the table regardless of how far the fling carried it.
- longPressOn: "sawyer"
- extendedWaitUntil:
visible: "Copy text"
timeout: 10000
- tapOn: "Copy text"
- extendedWaitUntil:
visible: "Copied"
timeout: 10000
3 changes: 2 additions & 1 deletion apps/mobile/src/composer/Composer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
ActionSheet,
Button,
Icon,
LONG_PRESS_DELAY_MS,
SheetPresenceContext,
Spinner,
useOverlayBounds,
Expand Down Expand Up @@ -625,7 +626,7 @@ export const Composer = forwardRef<ComposerHandle, ComposerProps>(
? () => submit("steer")
: undefined
}
delayLongPress={350}
delayLongPress={LONG_PRESS_DELAY_MS}
testID={`${testID}-submit`}
style={({ pressed }) => ({
width: 36,
Expand Down
70 changes: 40 additions & 30 deletions apps/mobile/src/markdown/CodeBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Pressable, ScrollView, Text as RNText, View } from "react-native";
import { FONT_FAMILIES } from "@/theme/fonts";
import { nativeTypography } from "@/theme/theme.native";
import { Icon } from "@/ui/Icon";
import { LONG_PRESS_DELAY_MS } from "@/ui/long-press";
import { toast } from "@/ui/Toast";
import {
codeTokenColor,
Expand Down Expand Up @@ -105,38 +106,47 @@ export const CodeBlock = memo(function CodeBlock({
horizontal
showsHorizontalScrollIndicator={false}
nestedScrollEnabled
contentContainerStyle={{
paddingHorizontal: 12,
paddingBottom: 10,
paddingTop: 2,
}}
>
<RNText
selectable={ctx.selectable}
style={{
fontFamily: FONT_FAMILIES.mono.regular,
fontWeight: "400",
fontSize: mono.fontSize,
lineHeight: mono.lineHeight,
color: tokens.foreground,
}}
{/*
The body gets its own Pressable inside the ScrollView, and the
body padding sits on it so a drag that starts in a gutter hits it
too. With only the outer Pressable (an ancestor of the ScrollView)
as responder, the new architecture never lets the ScrollView scroll
sideways. See MarkdownTable.
*/}
<Pressable
accessible={false}
onLongPress={copy}
delayLongPress={LONG_PRESS_DELAY_MS}
style={{ paddingHorizontal: 12, paddingBottom: 10, paddingTop: 2 }}
>
{lines.map((line, lineIndex) => (
<RNText key={lineIndex}>
{line.map((span, spanIndex) => (
<RNText
key={spanIndex}
style={{
color: codeTokenColor(span.type, mode, tokens),
}}
>
{span.text}
</RNText>
))}
{lineIndex < lines.length - 1 ? "\n" : null}
</RNText>
))}
</RNText>
<RNText
selectable={ctx.selectable}
style={{
fontFamily: FONT_FAMILIES.mono.regular,
fontWeight: "400",
fontSize: mono.fontSize,
lineHeight: mono.lineHeight,
color: tokens.foreground,
}}
>
{lines.map((line, lineIndex) => (
<RNText key={lineIndex}>
{line.map((span, spanIndex) => (
<RNText
key={spanIndex}
style={{
color: codeTokenColor(span.type, mode, tokens),
}}
>
{span.text}
</RNText>
))}
{lineIndex < lines.length - 1 ? "\n" : null}
</RNText>
))}
</RNText>
</Pressable>
</ScrollView>
</Pressable>
);
Expand Down
2 changes: 2 additions & 0 deletions apps/mobile/src/markdown/Markdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ function MarkdownComponent({
onThreadPress,
onMentionPress,
onBlockLongPress,
onLongPress,
renderDirective,
resolveImageSource,
}: MarkdownProps) {
Expand Down Expand Up @@ -232,6 +233,7 @@ function MarkdownComponent({
onThreadPress,
onMentionPress,
onBlockLongPress,
onLongPress,
renderDirective,
resolveImageSource,
});
Expand Down
10 changes: 10 additions & 0 deletions apps/mobile/src/markdown/MarkdownContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,13 @@ export interface MarkdownCallbacks {
* blocks are memoized on the context.
*/
onBlockLongPress?: (block: MarkdownBlockPress) => void;
/**
* Long-pressed a block that owns its touch target (a table: its target
* sits inside the horizontal ScrollView, so a Pressable around the whole
* body never sees the press) and that has no `onBlockLongPress`. Pass the
* body's message-level long-press here so such blocks are not dead zones.
*/
onLongPress?: () => void;
/**
* Directive cards. Return a node to render a card, or null to fall back to
* the literal directive source.
Expand Down Expand Up @@ -140,6 +147,7 @@ export function useMarkdownContextValue(
onThreadPress,
onMentionPress,
onBlockLongPress,
onLongPress,
renderDirective,
resolveImageSource,
} = inputs;
Expand All @@ -160,6 +168,7 @@ export function useMarkdownContextValue(
onThreadPress,
onMentionPress,
onBlockLongPress,
onLongPress,
renderDirective,
resolveImageSource,
}),
Expand All @@ -179,6 +188,7 @@ export function useMarkdownContextValue(
onThreadPress,
onMentionPress,
onBlockLongPress,
onLongPress,
renderDirective,
resolveImageSource,
],
Expand Down
28 changes: 25 additions & 3 deletions apps/mobile/src/markdown/MarkdownTable.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import type { AlignType, Table, TableCell } from "mdast";
import { toString as mdastToString } from "mdast-util-to-string";
import { memo, useMemo } from "react";
import { ScrollView, Text as RNText, View } from "react-native";
import { Pressable, ScrollView, Text as RNText, View } from "react-native";
import { FONT_FAMILIES } from "@/theme/fonts";
import { nativeTypography } from "@/theme/theme.native";
import { LONG_PRESS_DELAY_MS } from "@/ui/long-press";
import { buildTableModel } from "./blocks";
import { useMarkdownContext } from "./MarkdownContext";
import { renderInline } from "./render-inline";
Expand Down Expand Up @@ -59,8 +60,14 @@ function textAlign(align: AlignType): "left" | "center" | "right" {

export const MarkdownTable = memo(function MarkdownTable({
table,
onLongPress,
}: {
table: Table;
/**
* Long-press on the table body: the quote-this-block shortcut, or the
* body-level message actions when no block handler exists.
*/
onLongPress?: () => void;
}) {
const ctx = useMarkdownContext();
const { tokens } = ctx;
Expand Down Expand Up @@ -118,7 +125,22 @@ export const MarkdownTable = memo(function MarkdownTable({
showsHorizontalScrollIndicator={false}
nestedScrollEnabled
>
<View
{/*
The long-press target lives INSIDE the ScrollView on purpose. On the
new architecture a ScrollView refuses to take over a touch while one
of its ancestors is the JS responder (RCTScrollViewComponentView
`_shouldDisableScrollInteraction`), and a Pressable claims the
responder as soon as a touch starts. Timeline messages wrap their
markdown in Pressables, so a table under them could not scroll
sideways. A Pressable descendant claims the responder first, and the
ScrollView cancels a descendant normally once the drag starts. Since
it claims every touch, it must also carry the body-level long-press
when there is no block handler (render-blocks passes the fallback).
*/}
<Pressable
accessible={false}
onLongPress={onLongPress}
delayLongPress={LONG_PRESS_DELAY_MS}
style={{
borderWidth: 1,
borderRightWidth: 0,
Expand Down Expand Up @@ -153,7 +175,7 @@ export const MarkdownTable = memo(function MarkdownTable({
)}
</View>
))}
</View>
</Pressable>
</ScrollView>
);
});
31 changes: 24 additions & 7 deletions apps/mobile/src/markdown/render-blocks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Pressable, Text as RNText, View, type TextStyle } from "react-native";
import { FONT_FAMILIES, FONT_WEIGHT_VALUES } from "@/theme/fonts";
import { nativeTypography } from "@/theme/theme.native";
import { Icon } from "@/ui/Icon";
import { LONG_PRESS_DELAY_MS } from "@/ui/long-press";
import { getNodeSource, splitParagraphSegments } from "./blocks";
import { CodeBlock } from "./CodeBlock";
import type { MarkdownContextValue } from "./MarkdownContext";
Expand Down Expand Up @@ -409,8 +410,6 @@ function renderBlockContent(
return <CodeBlock code={node.value} language={node.lang ?? null} />;
case "math":
return <CodeBlock code={node.value} language="math" />;
case "table":
return <MarkdownTable table={node} />;
case "html":
return (
<RNText
Expand Down Expand Up @@ -517,20 +516,38 @@ const MarkdownBlock = memo(function MarkdownBlock({
isLast,
keyPrefix,
}: MarkdownBlockProps) {
const onBlockLongPress = ctx.onBlockLongPress;
const onLongPress =
onBlockLongPress !== undefined && source !== null
? () => onBlockLongPress({ source })
: undefined;
const style = blockMargins(node, isFirst, isLast, options);
if (node.type === "table") {
// A table owns its long-press target (inside its ScrollView; see
// MarkdownTable), so it gets no Pressable wrapper. Without a block
// handler it falls back to the body-level one. Code blocks also own
// their target, but theirs copies the code instead of quoting it.
return (
<View style={style}>
<MarkdownTable
table={node}
onLongPress={onLongPress ?? ctx.onLongPress}
/>
</View>
);
}
const rendered = renderBlockContent(node, ctx, content, options, keyPrefix);
if (rendered === null) {
return null;
}
const style = blockMargins(node, isFirst, isLast, options);
const onBlockLongPress = ctx.onBlockLongPress;
if (onBlockLongPress !== undefined && source !== null) {
if (onLongPress !== undefined) {
// Not an accessibility element of its own: the text inside stays the
// screen-reader target; long-press is a shortcut to quote this block.
return (
<Pressable
accessible={false}
onLongPress={() => onBlockLongPress({ source })}
delayLongPress={350}
onLongPress={onLongPress}
delayLongPress={LONG_PRESS_DELAY_MS}
style={style}
>
{rendered}
Expand Down
12 changes: 11 additions & 1 deletion apps/mobile/src/screens/dev/work-row-fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -808,7 +808,17 @@ export function buildWorkRowFixtureSections(): WorkRowFixtureSection[] {
taskStatus: "stopped",
completedAt: T0 + 9_000,
}),
assistant("a-done", "All done."),
assistant(
"a-done",
[
"All done. Summary of the changes:",
"",
"| File | Change | Lines | Reviewer | Notes |",
"| --- | --- | ---: | --- | --- |",
"| src/markdown/MarkdownTable.tsx | horizontal scroll | 12 | sawyer | wide tables scroll sideways inside the timeline |",
"| src/markdown/CodeBlock.tsx | inner press target | 8 | bee | code bodies scroll sideways too |",
].join("\n"),
),
],
},
];
Expand Down
4 changes: 2 additions & 2 deletions apps/mobile/src/screens/files/FilePathRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { memo } from "react";
import { Pressable, View } from "react-native";
import { buildHighlightSegments, splitPathForRow } from "@/data/files";
import { useTheme } from "@/theme";
import { cn, Icon, Text, type IconName } from "@/ui";
import { cn, Icon, LONG_PRESS_DELAY_MS, Text, type IconName } from "@/ui";

interface FilePathRowProps {
/** Root-relative (or absolute) path shown split into name + directory. */
Expand Down Expand Up @@ -53,7 +53,7 @@ export const FilePathRow = memo(function FilePathRow({
accessibilityLabel={path}
onPress={onPress}
onLongPress={onLongPress}
delayLongPress={350}
delayLongPress={LONG_PRESS_DELAY_MS}
testID={testID}
className="min-h-[44px] flex-row items-center gap-3 px-4 py-2 active:bg-state-hover"
>
Expand Down
6 changes: 3 additions & 3 deletions apps/mobile/src/screens/sidebar/SidebarRows.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { memo } from "react";
import { Pressable, View } from "react-native";
import { getThreadDisplayTitle } from "@/data/threads";
import { useTheme } from "@/theme";
import { Icon, Text, cn } from "@/ui";
import { Icon, LONG_PRESS_DELAY_MS, Text, cn } from "@/ui";
import {
getCollapsedActivityIndicatorState,
type SidebarEmptyRow,
Expand Down Expand Up @@ -113,7 +113,7 @@ export const SidebarThreadRowView = memo(function SidebarThreadRowView({
accessibilityHint={subtitleText(subtitle)}
onPress={() => onPress(row)}
onLongPress={() => onLongPress(row)}
delayLongPress={350}
delayLongPress={LONG_PRESS_DELAY_MS}
className="flex-row items-center gap-1 active:bg-state-hover"
style={{
minHeight: ROW_MIN_HEIGHT,
Expand Down Expand Up @@ -219,7 +219,7 @@ export const SidebarHeaderRowView = memo(function SidebarHeaderRowView({
accessibilityState={{ expanded: !row.collapsed }}
onPress={() => onToggleCollapsed(row)}
onLongPress={() => onLongPress(row)}
delayLongPress={350}
delayLongPress={LONG_PRESS_DELAY_MS}
className="flex-row items-center gap-1 active:bg-state-hover"
style={{
minHeight: HEADER_MIN_HEIGHT,
Expand Down
Loading
Loading