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.
8 changes: 4 additions & 4 deletions apps/mobile/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,9 @@ Build a ⋯ menu with `ActionMenuButton` (`src/ui/primitives/action-menu-button`
## Activity query controls

- Activity has two header entries: **Filter and sort** and **Search Activity**. Sorting is part of the same options sheet as category, unread-only, Bot and conversation filters.
- iOS uses native navigation toolbar buttons: the system supplies Liquid Glass on iOS 26 and its supported appearance on earlier versions. Active search uses a native input in a measured floating surface outside the fixed-height navigation bar, so accessibility text can grow without clipping. Android uses opaque Material-style icon actions and its native text input.
- iOS uses native navigation toolbar buttons: the system supplies Liquid Glass on iOS 26 and its supported appearance on earlier versions. Active search replaces the title, Filter and Search buttons with the same header search field as Chats, in the measured header row, so accessibility text can grow without clipping. Android uses opaque Material-style icon actions and its native text input.
- The iOS options sheet uses system page-sheet presentation with swipe dismissal, inset grouped rows and trailing checkmarks. Android uses the modal bottom-sheet surface, leading radio indicators and system Back dismissal. Unread-only uses a native switch on each platform. Long option lists virtualize and scroll; footer actions remain reachable.
- Options apply immediately and remain selected after swipe dismissal or Android Back; there are no Cancel/Done actions. Reset all immediately restores default ordering and the Needs you tab, with all Bots/conversations and unread-only off. Search is independent of this reset. The iOS search surface reserves equal outer gutters; iOS search keeps its built-in clear control; tapping blank list space dismisses search without a second close button and preserves the query for reopening. The focused Activity screen owns status-bar contrast for its current appearance.
- Options apply immediately and remain selected after swipe dismissal or Android Back; there are no Cancel/Done actions. Reset all immediately restores default ordering and the Needs you tab, with all Bots/conversations and unread-only off. Search is independent of this reset. The search field keeps the header's outer gutters; iOS search keeps its built-in clear control. Tapping blank list space or ending editing returns the header to its title and buttons and preserves the query for reopening, with the Search button tinted while a query applies, so Filter and sort stay reachable during a search. The close button (iOS) or back arrow and system Back (Android) clears the query. The focused Activity screen owns status-bar contrast for its current appearance.

References: [Apple toolbars](https://developer.apple.com/design/human-interface-guidelines/toolbars), [Apple sheets](https://developer.apple.com/design/human-interface-guidelines/sheets), [Material bottom sheets](https://m3.material.io/components/bottom-sheets/guidelines), [Android radio selection](https://developer.android.com/develop/ui/compose/components/radio-button).

Expand Down Expand Up @@ -94,8 +94,8 @@ The shell is set by [ADR-0211](../../docs/adr/0211-the-mobile-app-has-two-tabs-w
- **Chats** shows the current Host:
- Conversation rows keep the title and timestamp on one horizontal line at every system text size. The title takes the remaining width and truncates with tail ellipsis; the timestamp keeps its own single-line slot. Preview/status and count stay on a second line. A conversation without a timestamp does not reserve an empty metadata line. Preserve native font scaling and the complete accessible title/preview.
- The header has a compact Host title on the left and a Settings gear on the right, on both iOS and Android. This row stays horizontal at every system text size: the Host chip shrinks, its name occupies one line with tail ellipsis, and the Settings target stays at the trailing edge. Keep native font scaling and the full accessible Host name; both targets retain the platform minimum touch size. Other Host-name presentations may wrap. The Host picker stays mounted between visits; its translation follows a critically damped UI-thread spring and its scrim is clamped. Hidden contents leave the accessibility tree.
- **+** sits at the right of the channel search field. It opens the native **New PersonaBot** / **New Channel** menu and uses the app's create flow, or the full client where the app has none yet.
- A **search field** floats just above the bottom navigation island, within reach of the thumb, as iOS 26 places search. It is a capsule on the shared navigation glass (`DrawerMaterial`; opaque raised on older iOS, Reduce Transparency and Android) inset like the island, rides above the keyboard while typing, and hides and returns with the navigation on scroll. While it is focused or holds a query the navigation stays visible, and a circular close button beside it (the island's glass circle) clears the query and dismisses the keyboard. The field is a native UIKit text field with the system clear button, not a SwiftUI field, which has none. The list reserves the field's height under its last row. There are no shortcut cards: Activity already gathers what needs the Human.
- **+** is the circle beside the bottom navigation island. It opens the native **New PersonaBot** / **New Channel** menu and uses the app's create flow, or the full client where the app has none yet.
- **Search** is a magnifying-glass button just left of the Settings gear. Tapping it turns the whole header row into the search field and hides the Host title and Settings, as a Material top app bar opens its search and as iOS expands a toolbar search from its button. The field is the shared navigation glass capsule (`DrawerMaterial`; opaque raised on older iOS, Reduce Transparency and Android) and focuses at once. iOS closes it with the native circular close button at the trailing end and keeps the system clear button inside the field. Android leads with a back arrow, adds its own clear button while there is a query, and system Back closes search. Closing clears the query and restores the title and Settings. Ending editing with an empty query closes search; with a query, the field stays in the header so the filtered list keeps showing what it matches. Navigation stays visible while the field is focused or holds a query. Nothing floats above the bottom navigation, and the list reserves only the navigation's height. There are no shortcut cards: Activity already gathers what needs the Human.
- One list of PersonaBot DMs and Channels together, grouped by the Host's **Channel sections** as on the desktop sidebar:
- Sections come in the desktop's order. Ungrouped DMs and Channels sit between them in the desktop's top-level order.
- A section header shows its name and Channel count and collapses or expands on tap. A collapsed section still shows a "Needs you" mark when something inside needs the Human. The collapse state is kept on the phone, per Host.
Expand Down
45 changes: 33 additions & 12 deletions apps/mobile/app/(tabs)/(home)/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,7 @@ import { conversations, searchKey, type Conversation } from '../../../src/ui/cha
import { CloudConversationRow } from '../../../src/ui/chat/cloud-conversation-row';
import { useConversationFilter } from '../../../src/host/native-conversation-filter';
import { ConnectionBanner } from '../../../src/ui/connection-banner';
import { useDesignTheme } from '../../../src/ui/design/theme';
import { space } from '../../../src/ui/design/tokens';
import { BottomSearch, bottomSearchHeight } from '../../../src/ui/shell/bottom-search';
import { EnvironmentPager } from '../../../src/ui/shell/environment-pager';
import {
conversationEnvironments,
Expand All @@ -34,6 +32,7 @@ import {
} from '../../../src/host/conversation-environments';
import { shownHostName } from '../../../src/ui/design/host-look';
import { Text } from '../../../src/ui/primitives/text';
import { HeaderSearch, HeaderSearchButton } from '../../../src/ui/shell/header-search';
import { HomeHeader } from '../../../src/ui/shell/home-header';
import { HostDrawerEdge } from '../../../src/ui/shell/host-drawer';
import { SettingsButton } from '../../../src/ui/shell/settings-button';
Expand Down Expand Up @@ -101,10 +100,10 @@ const HomeContent = memo(function HomeContent({
);
const [query, setQuery] = useState('');
const [searching, setSearching] = useState(false);
const onScroll = useChromeScroll(true, searching || query !== '');
const [searchFocused, setSearchFocused] = useState(false);
const onScroll = useChromeScroll(true, searchFocused || query !== '');
const chrome = useScrollChrome();
const theme = useDesignTheme();
const bottom = useFloatingTabInset() + bottomSearchHeight(theme.touch);
const bottom = useFloatingTabInset();
const look = useHostLook(saved?.hostId);
const cloud = useCloudSnapshot();
const directory = useCloudConversations();
Expand Down Expand Up @@ -266,7 +265,35 @@ const HomeContent = memo(function HomeContent({
<>
<Stack.Screen options={{ title: t.allConversations, headerShown: false }} />
<HostDrawerEdge>
<HomeHeader actions={<SettingsButton onPress={() => router.push('/settings')} />}>
<HomeHeader
search={
searching ? (
<HeaderSearch
value={query}
placeholder={t.searchPlaceholder}
onChangeText={setQuery}
onFocusChange={(next) => {
setSearchFocused(next);
if (!next && query === '') setSearching(false);
}}
onClose={() => {
setSearchFocused(false);
setSearching(false);
}}
/>
) : undefined
}
actions={
<>
<HeaderSearchButton
label={t.openSearch}
testID="search-button"
onPress={() => setSearching(true)}
/>
<SettingsButton onPress={() => router.push('/settings')} />
</>
}
>
<EnvironmentPager
routes={filters}
selected={effectiveFilter}
Expand Down Expand Up @@ -344,12 +371,6 @@ const HomeContent = memo(function HomeContent({
)}
/>
</HomeHeader>
<BottomSearch
value={query}
placeholder={t.searchPlaceholder}
onChangeText={setQuery}
onFocusChange={setSearching}
/>
</HostDrawerEdge>
</>
);
Expand Down
116 changes: 54 additions & 62 deletions apps/mobile/src/ui/shell/activity-toolbar-base.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { useImperativeHandle, useState, type ReactNode, type Ref } from 'react';
import { Stack } from 'expo-router';
import { Keyboard, Platform, Pressable, View } from 'react-native';
import { Keyboard, Pressable, View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';

import { useDesignTheme } from '../design/theme';
import { radius, space } from '../design/tokens';
import { SearchField } from '../primitives/search-field';
import { Text } from '../primitives/text';
import { t } from '../strings';
import { ActivityFilterIcon, ActivitySearchIcon, CloseIcon } from './icons';
import { HeaderSearch } from './header-search';
import { ActivityFilterIcon, ActivitySearchIcon } from './icons';

export type ActivityToolbarHandle = { dismissSearch(): void };

Expand Down Expand Up @@ -70,68 +70,60 @@ export function ActivityToolbar({
paddingHorizontal: space.x4,
}}
>
<Text
variant="title"
strong
accessibilityRole="header"
numberOfLines={1}
style={{ flex: 1 }}
>
{t.activity}
</Text>
{!unavailable ? (
<ActivityIconButton label={t.activityOptions} onPress={onFilter} selected={filtered}>
<ActivityFilterIcon
color={filtered ? theme.color.accent : theme.color.fg}
size={theme.type.title.size}
/>
</ActivityIconButton>
) : null}
{!unavailable && !searching ? (
<ActivityIconButton
label={t.activitySearch}
onPress={() => {
onSearchFocus(true);
setSearching(true);
{!unavailable && searching ? (
<HeaderSearch
value={search}
placeholder={t.activitySearch}
onChangeText={(value) => onSearch(value.slice(0, 200))}
onFocusChange={(focused) => {
onSearchFocus(focused);
if (!focused) setSearching(false);
}}
>
<ActivitySearchIcon
color={search ? theme.color.accent : theme.color.fg}
size={theme.type.title.size}
/>
</ActivityIconButton>
) : null}
</View>
{!unavailable && searching ? (
<SearchField
trailing={
search && Platform.OS === 'android' ? (
<Pressable
accessibilityRole="button"
accessibilityLabel={t.activityClearSearch}
onPress={() => onSearch('')}
style={{
width: theme.touch,
minHeight: theme.touch,
alignItems: 'center',
justifyContent: 'center',
onClose={() => {
onSearchFocus(false);
setSearching(false);
}}
/>
) : (
<>
<Text
variant="title"
strong
accessibilityRole="header"
numberOfLines={1}
style={{ flex: 1 }}
>
{t.activity}
</Text>
{!unavailable ? (
<ActivityIconButton
label={t.activityOptions}
onPress={onFilter}
selected={filtered}
>
<ActivityFilterIcon
color={filtered ? theme.color.accent : theme.color.fg}
size={theme.type.title.size}
/>
</ActivityIconButton>
) : null}
{!unavailable ? (
<ActivityIconButton
label={t.activitySearch}
onPress={() => {
onSearchFocus(true);
setSearching(true);
}}
>
<CloseIcon color={theme.color.muted} size={theme.type.title.size} />
</Pressable>
) : null
}
autoFocus
onBlur={() => {
setSearching(false);
onSearchFocus(false);
}}
value={search}
inset={space.x4}
placeholder={t.activitySearch}
onChangeText={(value) => onSearch(value.slice(0, 200))}
/>
) : null}
<ActivitySearchIcon
color={search ? theme.color.accent : theme.color.fg}
size={theme.type.title.size}
/>
</ActivityIconButton>
) : null}
</>
)}
</View>
</View>
</>
);
Expand Down
91 changes: 0 additions & 91 deletions apps/mobile/src/ui/shell/bottom-search.tsx

This file was deleted.

Loading
Loading