mirror of
https://github.com/bulwarkmail/native.git
synced 2026-11-04 16:06:41 +00:00
feat: search settings from the Settings screen
The webmail's Settings screen has a search field; the app had none, so finding a setting meant opening panes one by one. Settings now has a search field above the pane list that narrows the list to the panes that match and lists up to six matching settings under each. The index follows the webmail's `lib/settings-search.ts`: per pane, the translation keys it renders plus a few English keywords, walked for label/description pairs. The paths list what the native panes show rather than the webmail's, the walker prefers the `*_mobile` variants the panes render, and every label goes through t() for the current language. A test checks that each result is a key its pane renders, so results do not point at webmail-only settings. Tapping a result opens the pane, scrolls to the matching row and tints it briefly. Back clears the search before leaving Settings.
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
import { createContext, useCallback, useContext, useEffect, useRef, useState } from 'react';
|
||||
import type { LayoutChangeEvent, View } from 'react-native';
|
||||
|
||||
/**
|
||||
* A settings search result the user tapped: the pane that just opened shows
|
||||
* the row whose label matches, scrolled into view and briefly highlighted
|
||||
* (webmail: settings-search-highlight).
|
||||
*/
|
||||
export interface SearchHighlight {
|
||||
label: string;
|
||||
/** Scroll `view` into view; called once, by the first row that matches. */
|
||||
reveal: (view: View) => void;
|
||||
}
|
||||
|
||||
export const SearchHighlightContext = createContext<SearchHighlight | null>(null);
|
||||
|
||||
const HIGHLIGHT_MS = 1800;
|
||||
|
||||
/**
|
||||
* For a settings row with `label`: attach `ref` and `onLayout` to its outer
|
||||
* View and paint the highlight while `highlighted` is true.
|
||||
*/
|
||||
export function useSearchHighlight(label: string) {
|
||||
const target = useContext(SearchHighlightContext);
|
||||
const ref = useRef<View>(null);
|
||||
const [highlighted, setHighlighted] = useState(false);
|
||||
|
||||
const onLayout = useCallback((_e: LayoutChangeEvent) => {
|
||||
if (!target || target.label.trim() !== label.trim() || !ref.current) return;
|
||||
target.reveal(ref.current);
|
||||
setHighlighted(true);
|
||||
}, [target, label]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!highlighted) return undefined;
|
||||
const timer = setTimeout(() => setHighlighted(false), HIGHLIGHT_MS);
|
||||
return () => clearTimeout(timer);
|
||||
}, [highlighted]);
|
||||
|
||||
return { ref, onLayout, highlighted };
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import { spacing, radius, typography, type ThemePalette } from '../../theme/toke
|
||||
import { useColors } from '../../theme/colors';
|
||||
import ToggleSwitchComponent from '../ToggleSwitch';
|
||||
import { useLocaleStore } from '../../stores/locale-store';
|
||||
import { useSearchHighlight } from './search-highlight';
|
||||
|
||||
/**
|
||||
* Mirrors webmail settings-section.tsx:
|
||||
@@ -41,6 +42,7 @@ export function SettingsSection({
|
||||
const c = useColors();
|
||||
const styles = React.useMemo(() => makeStyles(c), [c]);
|
||||
const t = useLocaleStore((s) => s.t);
|
||||
const highlight = useSearchHighlight(title);
|
||||
return (
|
||||
<View style={styles.section}>
|
||||
{experimental && (
|
||||
@@ -54,7 +56,7 @@ export function SettingsSection({
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
<View>
|
||||
<View ref={highlight.ref} onLayout={highlight.onLayout} style={highlight.highlighted && styles.searchHighlight}>
|
||||
<Text style={styles.sectionTitle}>{title}</Text>
|
||||
{description && <Text style={styles.sectionDescription}>{description}</Text>}
|
||||
</View>
|
||||
@@ -80,8 +82,13 @@ export function SettingItem({
|
||||
}: SettingItemProps) {
|
||||
const c = useColors();
|
||||
const styles = React.useMemo(() => makeStyles(c), [c]);
|
||||
const highlight = useSearchHighlight(label);
|
||||
return (
|
||||
<View style={[styles.settingItem, !noBorder && styles.settingItemBorder, locked && styles.locked]}>
|
||||
<View
|
||||
ref={highlight.ref}
|
||||
onLayout={highlight.onLayout}
|
||||
style={[styles.settingItem, !noBorder && styles.settingItemBorder, locked && styles.locked, highlight.highlighted && styles.searchHighlight]}
|
||||
>
|
||||
<View style={styles.settingContent}>
|
||||
<View style={styles.settingLabelRow}>
|
||||
<Text style={styles.settingLabel}>{label}</Text>
|
||||
@@ -282,6 +289,14 @@ function makeStyles(c: ThemePalette) {
|
||||
settingRight: {
|
||||
flexShrink: 0,
|
||||
},
|
||||
// A row opened from Settings search. The negative margin keeps the
|
||||
// content where it was while the tint gets some room around it.
|
||||
searchHighlight: {
|
||||
backgroundColor: c.selection,
|
||||
borderRadius: radius.sm,
|
||||
marginHorizontal: -spacing.sm,
|
||||
paddingHorizontal: spacing.sm,
|
||||
},
|
||||
radioGroup: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
|
||||
Reference in New Issue
Block a user