Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
dc68e4c
feat(mobile): run the chat list top to bottom under the native header
claude Oct 19, 2026
9517d2e
docs(mobile): describe the native chat header and top-to-bottom list
claude Oct 19, 2026
377f0d5
chore(changes): number the native chat header fragment
claude Oct 19, 2026
f9ee355
feat(mobile): glass chat header items, open at the newest message, ke…
claude Oct 19, 2026
685b454
test(mobile): iOS 26 native chat header screenshots
DoodleBears Oct 19, 2026
d9b69df
test(mobile): re-shoot iOS 26 native chat header on 685b4540c
DoodleBears Oct 19, 2026
0fe97c5
fix(mobile): chat list first under the header, bottom safe area once,…
claude Oct 19, 2026
9cd5deb
test(mobile): re-shoot native chat on 0fe97c5a4: edge, overscroll, dr…
DoodleBears Oct 19, 2026
f077b11
feat(mobile): native compose bar for the chat composer
claude Oct 19, 2026
82808b8
docs(changes): name the native composer fragment after #2355
claude Oct 19, 2026
ec1f976
Merge main into native chat header
claude Oct 19, 2026
f2c0e00
test(mobile): iOS 26 native composer screenshots
DoodleBears Oct 19, 2026
51e4fbe
docs: link the native composer change to #2355
claude Oct 19, 2026
69c7880
test(mobile): native chat on ec1f976eb at AX5, Reduce Transparency an…
DoodleBears Oct 19, 2026
dcdf2c4
fix(mobile): native bar buttons and system label colour in the iOS ch…
claude Oct 19, 2026
3e5fc42
Merge remote-tracking branch 'origin/main' into claude/project-thread…
claude Oct 19, 2026
e6f84a0
Merge main into native composer
claude Oct 19, 2026
15e758f
Merge native chat header into native composer
claude Oct 19, 2026
aa80e84
test(mobile): native composer with header on 15e758ff3
DoodleBears Oct 19, 2026
779bc64
feat(mobile): native iOS 26 scroll edge effect under the chat composer
claude Oct 19, 2026
112181f
feat(mobile): Grok-style send button and trailing glass jump button
claude Oct 19, 2026
ebed880
feat(mobile): filled mic circle at rest in the iOS composer
claude Oct 19, 2026
03b43b2
test(mobile): native composer with scroll edge element on ebed880ed
DoodleBears Oct 19, 2026
eae7899
feat(mobile): near-opaque white composer glass like Grok
claude Oct 19, 2026
2c4867a
feat(mobile): Grok-sized composer, right jump button and app-themed k…
claude Oct 19, 2026
90a4998
test(mobile): native composer glass on eae7899b7
DoodleBears Oct 19, 2026
d2563e0
fix(mobile): native bottom edge for a tall composer and whiter glass
claude Oct 19, 2026
5e38f82
test(mobile): native composer on 2c4867a60, edge by keyboard and draf…
DoodleBears Oct 19, 2026
eb14ff5
test(mobile): native composer on d2563e0a3 rebuild
DoodleBears Oct 19, 2026
c17b5b1
fix(mobile): re-add the scroll edge interaction when the composer moves
claude Oct 19, 2026
d46fb68
test(mobile): native composer on c17b5b1d4 rebuild
DoodleBears Oct 19, 2026
a82b07f
fix(mobile): re-add the scroll edge interaction after the keyboard se…
claude Oct 19, 2026
1783638
feat(mobile): Liquid Glass @ mention panel on iOS 26
claude Oct 19, 2026
a07e9ba
test(mobile): native composer rebuild on 1783638b9 with @ mention panel
DoodleBears Oct 19, 2026
8b6538d
revert: keyboard-settled re-add of the scroll edge interaction
claude Oct 19, 2026
2d7988f
Merge main into the native composer after #2344
claude Oct 20, 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
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.
20 changes: 11 additions & 9 deletions apps/mobile/DESIGN.md

Large diffs are not rendered by default.

15 changes: 15 additions & 0 deletions apps/mobile/modules/scroll-edge-element/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# iOS scroll edge element

This local Expo module lets a floating React Native view take part in UIKit's
scroll edge effect on iOS 26, the way a system toolbar does. It wraps its
children in a view that carries a `UIScrollEdgeElementContainerInteraction`
for the nearest sibling scroll view, so UIKit draws the soft edge under it. UIKit shapes that edge when the
interaction is added, so the `revision` prop re-adds it whenever the
container's height or the keyboard changes.

The chat composer uses it for the bottom edge; the native header already gives
the top edge. Before iOS 26 the view is a plain container.

The JavaScript binding lives in `src/ui/primitives/scroll-edge-element.ios.tsx`.
Builds without this module keep the drawn `EdgeMaterial` fade. **Adding this
module requires a native rebuild**; an OTA update alone cannot install it.
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"platforms": ["apple"],
"apple": {
"modules": ["ScrollEdgeElementModule"]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
Pod::Spec.new do |s|
s.name = 'ScrollEdgeElement'
s.version = '1.0.0'
s.summary = 'Registers a floating React Native view as a UIKit scroll edge element.'
s.description = 'Registers a floating React Native view as a UIKit scroll edge element.'
s.license = 'MIT'
s.author = 'BotHarness'
s.homepage = 'https://github.com/BotHarness/DeepSeekBot'
s.platforms = { :ios => '16.4' }
s.swift_version = '5.9'
s.source = { git: 'https://github.com/BotHarness/DeepSeekBot.git' }
s.static_framework = true

s.dependency 'ExpoModulesCore'

s.source_files = '**/*.{h,m,swift}'
s.pod_target_xcconfig = {
'DEFINES_MODULE' => 'YES',
'SWIFT_COMPILATION_MODE' => 'wholemodule'
}
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import ExpoModulesCore
import UIKit

public final class ScrollEdgeElementModule: Module {
public func definition() -> ModuleDefinition {
Name("ScrollEdgeElement")

View(ScrollEdgeElementView.self) {
Prop("edge") { (view: ScrollEdgeElementView, edge: String) in
view.edge = edge == "top" ? .top : .bottom
}
Prop("revision") { (view: ScrollEdgeElementView, _: String) in
view.refresh()
}
}
}
}

final class ScrollEdgeElementView: ExpoView {
var edge: UIRectEdge = .bottom {
didSet { attach() }
}

private var interaction: UIInteraction?

override func didMoveToWindow() {
super.didMoveToWindow()
attach()
}

override func layoutSubviews() {
super.layoutSubviews()
attach()
}

func refresh() {
if let interaction { removeInteraction(interaction) }
interaction = nil
DispatchQueue.main.async { [weak self] in self?.attach() }
DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) { [weak self] in
guard let self, let interaction = self.interaction else { return }
self.removeInteraction(interaction)
self.interaction = nil
self.attach()
}
}

private func attach() {
guard #available(iOS 26.0, *) else { return }
guard window != nil, let scrollView = nearestScrollView() else { return }
let current = interaction as? UIScrollEdgeElementContainerInteraction
if let current, current.scrollView === scrollView, current.edge == edge { return }
if let current { removeInteraction(current) }
let next = UIScrollEdgeElementContainerInteraction()
next.scrollView = scrollView
next.edge = edge
addInteraction(next)
interaction = next
}

private func nearestScrollView() -> UIScrollView? {
var child: UIView = self
while let parent = child.superview {
for sibling in parent.subviews where sibling !== child {
if let found = largestScrollView(in: sibling) { return found }
}
child = parent
}
return nil
}

private func largestScrollView(in root: UIView) -> UIScrollView? {
var best: UIScrollView?
var queue: [UIView] = [root]
while !queue.isEmpty {
let view = queue.removeFirst()
if let scrollView = view as? UIScrollView, !(view is UITextView) {
if best == nil || scrollView.bounds.height > best!.bounds.height { best = scrollView }
continue
}
queue.append(contentsOf: view.subviews)
}
return best
}
}
126 changes: 126 additions & 0 deletions apps/mobile/src/ui/chat/composer-chrome.ios.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { Circle, Host, RoundedRectangle } from '@expo/ui/swift-ui';
import { accessibilityHidden, foregroundStyle, glassEffect } from '@expo/ui/swift-ui/modifiers';
import type { ReactNode } from 'react';
import { Pressable, StyleSheet, View } from 'react-native';

import { pressTint, useDesignTheme, type DesignTheme } from '../design/theme';
import { edge, radius, space } from '../design/tokens';
import { useLiquidGlass } from '../primitives/drawer-material';

export function composerPrimary(theme: DesignTheme): { fill: string; ink: string } {
return { fill: theme.color.fg, ink: theme.color.base };
}

export function composerBar(theme: DesignTheme): number {
return theme.touch - space.x2;
}

export const micAlwaysFilled = true;

export const rippleFeedback = false;

function Glass({ cornerRadius }: { cornerRadius?: number }) {
const theme = useDesignTheme();
return (
<Host
key={theme.scheme}
style={StyleSheet.absoluteFill}
colorScheme={theme.scheme}
pointerEvents="none"
>
{cornerRadius === undefined ? (
<Circle
modifiers={[
foregroundStyle('transparent'),
glassEffect({
glass: { variant: 'regular', tint: theme.color.surface },
shape: 'circle',
}),
accessibilityHidden(),
]}
/>
) : (
<RoundedRectangle
cornerRadius={cornerRadius}
modifiers={[
foregroundStyle('transparent'),
glassEffect({
glass: { variant: 'regular', tint: theme.color.surface },
shape: 'roundedRectangle',
cornerRadius,
}),
accessibilityHidden(),
]}
/>
)}
</Host>
);
}

export function ComposerCircleButton({
label,
testID,
size,
onPress,
children,
}: {
label: string;
testID?: string;
size: number;
onPress(): void;
children: ReactNode;
}) {
const theme = useDesignTheme();
const glass = useLiquidGlass();
return (
<Pressable
accessibilityRole="button"
accessibilityLabel={label}
testID={testID}
onPress={onPress}
hitSlop={Math.max(0, (theme.touch - size) / 2)}
style={({ pressed }) => ({
width: size,
height: size,
borderRadius: radius.round,
alignItems: 'center',
justifyContent: 'center',
...(glass
? { backgroundColor: pressed ? pressTint(theme.color.fg) : 'transparent' }
: {
borderWidth: edge.hairline,
borderColor: theme.color.borderStrong,
backgroundColor: pressed ? theme.layer.raised.pressed : theme.color.surface,
}),
})}
>
{glass ? <Glass /> : null}
{children}
</Pressable>
);
}

export function ComposerFieldMaterial({ cornerRadius }: { cornerRadius: number }) {
const theme = useDesignTheme();
const glass = useLiquidGlass();
return (
<View
pointerEvents="none"
accessibilityElementsHidden
importantForAccessibility="no-hide-descendants"
style={[
StyleSheet.absoluteFill,
glass
? undefined
: {
borderRadius: cornerRadius,
borderWidth: edge.hairline,
borderColor: theme.color.borderStrong,
backgroundColor: theme.color.surface,
},
]}
>
{glass ? <Glass cornerRadius={cornerRadius} /> : null}
</View>
);
}
66 changes: 66 additions & 0 deletions apps/mobile/src/ui/chat/composer-chrome.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import type { ReactNode } from 'react';
import { Pressable, StyleSheet, View } from 'react-native';

import { pressTint, useDesignTheme, type DesignTheme } from '../design/theme';
import { radius } from '../design/tokens';

export function composerPrimary(theme: DesignTheme): { fill: string; ink: string } {
return { fill: theme.color.accent, ink: theme.color.onAccent };
}

export function composerBar(theme: DesignTheme): number {
return theme.touch;
}

export const micAlwaysFilled = false;

export const rippleFeedback = true;

export function ComposerCircleButton({
label,
testID,
size,
onPress,
children,
}: {
label: string;
testID?: string;
size: number;
onPress(): void;
children: ReactNode;
}) {
const theme = useDesignTheme();
return (
<Pressable
accessibilityRole="button"
accessibilityLabel={label}
testID={testID}
onPress={onPress}
android_ripple={{ color: pressTint(theme.color.fg), borderless: true, radius: size / 2 }}
style={{
width: size,
height: size,
borderRadius: radius.round,
alignItems: 'center',
justifyContent: 'center',
}}
>
{children}
</Pressable>
);
}

export function ComposerFieldMaterial({ cornerRadius }: { cornerRadius: number }) {
const theme = useDesignTheme();
return (
<View
pointerEvents="none"
accessible={false}
importantForAccessibility="no-hide-descendants"
style={[
StyleSheet.absoluteFill,
{ borderRadius: cornerRadius, backgroundColor: theme.layer.raised.fill },
]}
/>
);
}
Loading
Loading