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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion apps/mobile/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ The shell is set by [ADR-0211](../../docs/adr/0211-the-mobile-app-has-two-tabs-w
- Android: the header floats over the messages like iOS's (`headerTransparent`, no bar fill, no shadow), with the same three shapes as Grok's chat header: a round Back, the title chip at the start (`headerTitleAlign: 'left'`) as its own capsule island, and a round computer button. Each shape uses the opaque-iOS fallback of the header buttons (`headerGlassFill`: frosted `raised` fill with a `raised` hairline), so the chip reads as an island, not a label in a bar. Back is always this round button (it replaces the native navigation icon; system and predictive back still work). The list pads its own top by the header height, and messages scroll under the header through the drawn top fade (see below). Android has no system scroll edge effect: Material 3 only tints the top app bar on scroll, and the View fading edge (`fadingEdgeLength`) fades content linearly at the scroll view's own bounds, so it cannot keep what passes under the floating chrome hidden.
- The chip holds, in one row, the Bot's avatar (28pt, round, no state ring) and its name in `body` strong; a group shows its mark and name. It is `theme.touch` tall with padding concentric with its corners. The Bot's state is read out in its accessibility label only. Pressing shows the `raised` pressed fill. Icons in the header are `fg`, never the Bot's accent.
- iOS: messages run under the transparent header and the status bar, and UIKit's scroll edge effect (left on `automatic`) softens them there. The list uses `contentInsetAdjustmentBehavior="automatic"`, so UIKit, not a manual padding, keeps the header's height free above the oldest message at rest; the edge effect needs that automatic inset. The edge effect and React Native Screens both find the content scroll view through the first-subview chain of the screen, so the message list is the first thing in that chain: a default or colour chat background is the frame's own `backgroundColor`, not a view in front of the list. An image background still needs a view behind the list, and with one the soft edge does not reach the list. UIKit also adds the home-indicator safe area to the bottom inset while the list reaches the screen bottom (keyboard closed); the list cancels it with a negative `contentInset.bottom`, because the composer's height already includes that padding. Without this, scrolling to the very end left 34pt more space below the newest message than the resting position. There is no painted top gradient. A transparent header dimmed the whole message list on iOS 26 while the list was inverted (a scaleY flip); the list now runs top to bottom, which this header depends on.
- The list runs to the bottom of the screen, and the composer floats over it with no bar of its own; messages pass around and under its **+** button and field capsule. On iOS 26 with Reduce Transparency off both shapes are Liquid Glass, like iOS Messages; older iOS and Reduce Transparency give them an opaque `surface` fill with a `borderStrong` hairline, and Android gives the field the opaque `raised` fill, so on those nothing shows through where the Human types. The bottom of the screen fades out with the same light gradient over the composer and `edgeFade.bottom` above it. The list keeps that much space below the newest message, so at rest it sits just above the composer; scrolled up, messages pass behind the composer. Dragging the list dismisses the keyboard: interactively on iOS (the keyboard follows the finger once it reaches it), on drag on Android. On iOS 26 the bottom edge is UIKit's own scroll edge effect, like the top: the composer's container carries a `UIScrollEdgeElementContainerInteraction` for the message list (the local `scroll-edge-element` module; UIKit shapes the edge once when the interaction is added, so the container re-adds it whenever the composer's height or the keyboard changes), so UIKit softens messages under the composer the way it does under a system toolbar, and adapts it to Reduce Transparency itself. No fade is drawn there. A build without that module, older iOS and Android keep the drawn bottom fade over the composer's measured height (with the soft `EdgeMaterial` blur on iOS 26 with Reduce Transparency off). On Android the fade is a mask layer between the list and the floating chrome: nearly opaque (`edgeFade.cover`) under the composer and under the header, then the light `edgeFade` curve over `edgeFade.bottom` / `edgeFade.top` beyond them, painted in the chat's own background colour (`base` with an image background). With the keyboard open the composer's pill sits `space.x3` above it: the keyboard already covers the home indicator, so the chat frame takes the composer's safe-area padding back out of the keyboard offset instead of counting it twice.
- The list runs to the bottom of the screen, and the composer floats over it with no bar of its own; messages pass around and under its **+** button and field capsule. On iOS 26 with Reduce Transparency off both shapes are Liquid Glass, like iOS Messages; older iOS and Reduce Transparency give them an opaque `surface` fill with a `borderStrong` hairline, and Android gives the field the opaque `raised` fill, so on those nothing shows through where the Human types. The bottom of the screen fades out with the same light gradient over the composer and `edgeFade.bottom` above it. The list keeps that much space below the newest message, so at rest it sits just above the composer; scrolled up, messages pass behind the composer. Dragging the list dismisses the keyboard: interactively on iOS (the keyboard follows the finger once it reaches it), on drag on Android. On iOS and Android the bottom edge is a drawn mask layer between the list and the composer, like Grok's chat: nearly opaque (`edgeFade.cover`) under the composer's measured height, then the light `edgeFade` curve over `edgeFade.bottom` above it, painted in the chat's own background colour (`base` with an image background). Android uses the same mask under its floating header. iOS 26 hides UIKit's bottom scroll edge effect for the chat (`scrollEdgeEffects.bottom: 'hidden'`) and does not use a `UIScrollEdgeElementContainerInteraction` on the composer: UIKit picked the hard style there, an opaque band in a different colour from the list with a hairline across its top, at a height that did not follow a wrapped draft, and neither re-adding the interaction nor pinning the `soft` style fixed it. The top edge stays UIKit's automatic scroll edge effect. With the keyboard open the composer's pill sits `space.x3` above it: the keyboard already covers the home indicator, so the chat frame takes the composer's safe-area padding back out of the keyboard offset instead of counting it twice.
- Messages:
- Activity rows show Bot DMs, posts elsewhere, memory commits, Bot creation, departures and stopped turns as one left-aligned meta line with a kind icon and tail ellipsis. They use the same `bubbleRow(true)` inset and single `space.x4` (12pt) top gap as a new message group, with no bottom padding or vertically centred minimum-height box: bubble → activity, activity → activity and activity → new message group keep the same boundary gap. Icons and avatars match the meta line height. In groups, the actor avatar occupies the existing speaker gutter, followed by the icon at the bubble's left edge and the description. In a Human DM, omit the current Bot's avatar and redundant name; Bot-DM actions show the other Bot's avatar after the icon. Missing avatars use the person icon.
- Activity rows do not navigate on tap. Long-press (or the accessibility details action) opens the message sheet with the complete plain-text content in a scrollable, selectable block capped at `messageSheet.textHeightRatio` of the window height, then Copy/Share and an explicit Open conversation, Open memory or Open new Bot action when a destination exists; there are no reactions. Consecutive posts by the same Bot into the same Channel on the same day fold into a counted row whose menu opens the latest post. Memory opens the Bot's memory files, not a commit diff. Bot-to-Bot DMs remain read-only. Keep full actor/action/detail text in the accessibility label and menu even when the compact memory line uses only the icon and commit subject.
Expand Down
15 changes: 0 additions & 15 deletions apps/mobile/modules/scroll-edge-element/README.md

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

135 changes: 67 additions & 68 deletions apps/mobile/src/ui/chat/conversation-screen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,6 @@ import { ChatTitle } from './chat-title';
import { Composer, type ComposerInput } from './composer';
import { ClipboardImageOffer, useClipboardImage } from './clipboard-image';
import { EdgeFade, HeaderBackButton, titleChipFill } from './header-chrome';
import { ScrollEdgeElement, nativeScrollEdge } from '../primitives/scroll-edge-element';
import {
barLabelStyle,
floatingHeader,
Expand Down Expand Up @@ -812,6 +811,9 @@ function ChatConversation({
headerShadowVisible: false,
headerBackButtonDisplayMode: 'minimal',
...(ios && !glassHeader ? { headerBlurEffect: 'systemChromeMaterial' as const } : {}),
...(glassHeader
? { scrollEdgeEffects: { top: 'automatic' as const, bottom: 'hidden' as const } }
: {}),
...(ios
? {
title: '',
Expand Down Expand Up @@ -865,66 +867,64 @@ function ChatConversation({
}
onComposerLayout={(event) => setComposerHeight(event.nativeEvent.layout.height)}
composer={
<ScrollEdgeElement edge="bottom">
{botDm ? (
<View style={{ padding: space.x5, alignItems: 'center' }}>
<Text variant="meta" tone="muted">
{t.activityRow.botDm}
</Text>
</View>
) : (
<Composer
value={draft}
placeholder={
bot !== undefined
? t.messageBot(bot.displayName)
: channel !== undefined
? t.messageBot(`#${channel.name}`)
: t.messagePlaceholder
}
disabled={conversation === undefined || host.connection.status !== 'connected'}
files={files}
canAttach={files.length < MAX_PICKED}
dictation={dictation.state.status}
canDictate={conversation !== undefined}
onChange={edit}
selection={selection}
onSelection={(next) => {
setSelection(undefined);
setCaret(next);
}}
{...(candidates.length > 0
? { above: <MentionSuggestions candidates={candidates} onChoose={mention} /> }
: clipboard.offered
? {
above: (
<ClipboardImageOffer
onUnreadable={clipboard.used}
onPaste={(data) => {
clipboard.used();
setAttachError(false);
setFiles((current) =>
[...current, fromClipboardImage(data)].slice(0, MAX_PICKED),
);
}}
/>
),
}
: {})}
onSend={send}
onPick={pick}
onRemove={(id) => setFiles((current) => current.filter((file) => file.id !== id))}
onDictate={() => {
setAttachError(false);
dictation.start();
}}
onStopDictation={dictation.stop}
onCancelDictation={dictation.cancel}
inputRef={composerInput}
onFocusChange={setTyping}
/>
)}
</ScrollEdgeElement>
botDm ? (
<View style={{ padding: space.x5, alignItems: 'center' }}>
<Text variant="meta" tone="muted">
{t.activityRow.botDm}
</Text>
</View>
) : (
<Composer
value={draft}
placeholder={
bot !== undefined
? t.messageBot(bot.displayName)
: channel !== undefined
? t.messageBot(`#${channel.name}`)
: t.messagePlaceholder
}
disabled={conversation === undefined || host.connection.status !== 'connected'}
files={files}
canAttach={files.length < MAX_PICKED}
dictation={dictation.state.status}
canDictate={conversation !== undefined}
onChange={edit}
selection={selection}
onSelection={(next) => {
setSelection(undefined);
setCaret(next);
}}
{...(candidates.length > 0
? { above: <MentionSuggestions candidates={candidates} onChoose={mention} /> }
: clipboard.offered
? {
above: (
<ClipboardImageOffer
onUnreadable={clipboard.used}
onPaste={(data) => {
clipboard.used();
setAttachError(false);
setFiles((current) =>
[...current, fromClipboardImage(data)].slice(0, MAX_PICKED),
);
}}
/>
),
}
: {})}
onSend={send}
onPick={pick}
onRemove={(id) => setFiles((current) => current.filter((file) => file.id !== id))}
onDictate={() => {
setAttachError(false);
dictation.start();
}}
onStopDictation={dictation.stop}
onCancelDictation={dictation.cancel}
inputRef={composerInput}
onFocusChange={setTyping}
/>
)
}
>
{notice === undefined ? null : (
Expand Down Expand Up @@ -1081,13 +1081,12 @@ function ChatConversation({
color={fadeColor}
/>
) : null}
{nativeScrollEdge ? null : (
<EdgeFade
edge="bottom"
height={composerHeight + edgeFade.bottom}
{...(floatingHeader ? { cover: composerHeight, color: fadeColor } : {})}
/>
)}
<EdgeFade
edge="bottom"
height={composerHeight + edgeFade.bottom}
cover={composerHeight}
color={fadeColor}
/>
{composable && Platform.OS === 'ios' ? (
<PullToComposeHint
progress={pull.progress}
Expand Down
6 changes: 3 additions & 3 deletions apps/mobile/src/ui/chat/header-chrome.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,10 +113,10 @@ export function EdgeFade({
...(top ? { top: 0 } : { bottom: 0 }),
}}
>
{glass ? (
{glass && cover === 0 ? (
<EdgeMaterial edge={side} />
) : (
<Svg width="100%" height="100%" preserveAspectRatio="none">
<Svg width="100%" height={height} preserveAspectRatio="none">
<Defs>
<LinearGradient id={id} x1="0" y1="0" x2="0" y2="1">
{(top
Expand All @@ -132,7 +132,7 @@ export function EdgeFade({
))}
</LinearGradient>
</Defs>
<Rect x="0" y="0" width="100%" height="100%" fill={`url(#${id})`} />
<Rect x="0" y="0" width="100%" height={height} fill={`url(#${id})`} />
</Svg>
)}
</View>
Expand Down
Loading
Loading