makecindy / makecindy/cindy

feat(mobile): 以原生 Shell 提升系统级交互体验

Open
#231 0 comments 0 reactions 0 assignees View on GitHub
enhancement
Dominant language
TypeScript
Stars
2.7k
Forks
401
Avg merge
21h 48m
Merged PRs (30d)
776

Description

## 使用场景 / Use case

Cindy Mobile 当前的业务内容、流式消息和 Composer 已经建立在 React Native 上,但导航栏、工具栏、菜单和大部分 Sheet 仍由应用自绘。用户在 iOS / Android 上能感受到这些交互与系统应用在转场、手势、材质、菜单行为和无障碍适配上的差异。

目标不是复制 Flighty 的视觉,也不是重写为 SwiftUI,而是采用「**系统原生 Shell + React Native 内容**」:让系统负责最擅长的导航 Chrome、菜单、页面级 Sheet 和平台材质,继续保留 Cindy 已成熟的消息渲染、Markdown、流式输出、Composer 与跨端业务逻辑。

参考实现与官方能力:

- Nathan Schroeder 的 Expo Flighty Clone:https://github.com/SchroederNathan/expo-flightly-clone
- Expo `/expo-native-ui` 指南:https://github.com/expo/skills/blob/main/plugins/expo/skills/expo-native-ui/SKILL.md
- Expo Router Native Tabs:https://docs.expo.dev/router/advanced/native-tabs/
- Expo Router Stack / `Stack.Toolbar`:https://docs.expo.dev/router/advanced/stack/
- `@expo/ui` SwiftUI:https://docs.expo.dev/versions/latest/sdk/ui/swift-ui/
- `expo-glass-effect`:https://docs.expo.dev/versions/latest/sdk/glass-effect/

公开参考项目并非纯 SwiftUI:当前约 60 个 TSX 文件中,50 个仍使用普通 React Native,仅 2 个使用 `@expo/ui`,且没有自写 Swift / Objective-C 源码。它的“原生感”主要来自原生导航、Toolbar、菜单、Form Sheet、SF Symbols、系统材质和触觉反馈。

## 当前问题 / Current limitation

- `apps/mobile/app/_layout.tsx` 全局设置 `headerShown: false`,页面导航 Chrome 基本由业务组件自绘。
- 底部浮窗主要由 `SheetModal` + `SheetSurface` 负责,拖动、动画、遮罩、键盘和叠层语义均由 RN 自行编排。
- `expo-glass-effect` 已安装但尚未形成明确使用边界;`@expo/ui` 未安装。
- 当前移动端规范要求内容图标统一使用 Lucide、底部浮窗统一复用 `SheetSurface`。若系统 Chrome 也严格套用这两条规则,就无法获得真正的平台原生菜单、符号与 Sheet 行为。
- 主会话页包含高频流式更新、长列表、Markdown、横向滚动和复杂 Composer;直接全量切换风险高,也没有必要。

## 期望方案 / Proposed solution

### 总体原则

1. **原生 Shell,RN 内容**:导航、菜单、简单页面级 Sheet 优先交给 Expo Router / 系统;消息列表和业务内容继续使用 React Native。
2. **平台原生,不做像素级伪统一**:iOS 使用 UIKit / SwiftUI 语义,Android 使用对应 Material 语义;共享任务语义,不强求两端外观完全一致。
3. **渐进式试点**:先做一个低风险纵切,验证收益和稳定性,再决定是否扩大范围。
4. **双模式同时交付**:Light / Dark 必须在同一项工作中实现和验证;系统材质和 Cindy token 都必须有清晰的适用边界。
5. **所有 alpha / unstable 能力都必须有降级路径**,不得让新系统版本特效成为功能正确性的前提。

### Phase 0:先明确设计与架构边界

在移动端规范中记录以下窄例外:

- 系统导航 Chrome 可以使用 SF Symbols / Material Symbols;Cindy 品牌内容和跨端业务图标继续使用 Lucide。
- 简单的页面级弹层可以使用 Expo Router `formSheet`;需要双层状态、复杂键盘联动、固定 header/footer 或业务定制拖动的浮窗继续使用 `SheetSurface`。
- Liquid Glass / Material 材质只用于系统 Chrome 或明确的浮动控制面,不扩散为 Cindy 内容区的装饰风格,不引入渐变、装饰阴影或高饱和配色。

### Phase 1:不增加 native 依赖的纵切试点

首选以 `apps/mobile/app/settings.tsx` 为试点,避免先触碰会话热路径:

- 为 Settings 路由启用原生 Stack Header、系统返回手势和安全区联动,替代当前自绘 `ScreenHeader`。
- 在确有多个相关操作的入口使用 `Stack.Toolbar.Menu`,获得原生菜单、无障碍和平台符号行为;不要为了展示能力凭空增加菜单。
- 选择一个简单低风险弹层(候选:设备名称编辑或关于/调试信息)改为 Router `presentation: "formSheet"`,验证 detents、键盘、关闭手势和主题表现。
- 在合适的浮动系统 Chrome 上试用现有 `expo-glass-effect`;通过 `isLiquidGlassAvailable()` 提供旧 iOS / Android 的 Cindy token 降级样式。
- 保持会话消息列表、Markdown、流式输出、Composer、模型选择器和任务队列不变。

### Phase 2:按收益决定是否引入 `@expo/ui`

只有纵切证明系统 Shell 的体验和稳定性达到预期后,再考虑增加与 Expo SDK 56 匹配的 `@expo/ui`:

- 仅用于 Settings 中适合系统控件的简单 picker、segmented control、filter chip 等局部区域。
- 不以 `@expo/ui` 重写页面结构,不迁移消息列表和复杂表单。
- 新依赖会改变 native fingerprint,必须使用冷构建验证,不能仅依赖 OTA。

### Phase 3:基于信息架构决定是否采用 `NativeTabs`

`NativeTabs` 只有在 Cindy 确实形成稳定的多个一级移动端入口时才采用。首期不为了 Liquid Glass 外观强行加入底部 Tabs。

## 非目标 / Non-goals

- 不把 Cindy Mobile 全量重写成 Swift / SwiftUI / Compose。
- 不复制 Flighty 的品牌视觉、地图结构或信息架构。
- 不在首期迁移主聊天页、消息列表、Composer、模型选择器或双层 Sheet。
- 不为了展示原生能力新增无业务价值的底部 Tabs、菜单或动效。
- 不修改服务端、device-link wire protocol 或桌面端行为。

## 风险与约束

- 参考项目当前基于 Expo SDK 57 / RN 0.86;Cindy 当前是 Expo SDK 56 / RN 0.85,虽然目标 API 已存在,但具体 bug 和行为可能不同。
- `Stack.Toolbar`、`NativeTabs`、`@expo/ui`、Glass Effect 中仍有 alpha / unstable 能力,需要限制影响面。
- 参考源码已经存在以下 workaround,试点必须逐项回归:
- Form Sheet 展示过程中挂载底部 Toolbar 可能出现空白 UIBarButtonItem;
- `formSheet → NativeTabs → Stack` 下 large title 与 ScrollView 可能布局异常;
- `react-native-screens` 可能改写 Sheet 内 ScrollView frame,需要验证 native hierarchy;
- SwiftUI `Host` 的 intrinsic size / `Spacer` 在 RN 布局中可能需要显式尺寸。
- Cindy 当前对复杂 Sheet 已有经过实机打磨的关闭、键盘和叠层语义,不应以“更原生”为理由无差别替换。

## 验收标准 / Acceptance criteria

- [ ] 完成一个 Settings 纵切:原生 Header / 返回手势,以及至少一个有真实业务用途的原生菜单或页面级 Form Sheet。
- [ ] iOS 26+ 使用系统材质;较旧受支持 iOS 有完整、可读、可操作的降级样式。
- [ ] Android 使用对应平台行为和 Cindy token,不伪造 iOS Liquid Glass。
- [ ] Light / Dark 两种模式分别验证默认、pressed、disabled、弹层和遮罩状态。
- [ ] VoiceOver / TalkBack、Dynamic Type、Reduce Motion、44pt/48dp 触控区域通过验证。
- [ ] 系统边缘返回、Sheet dismiss、键盘升降、滚动边界和安全区无冲突。
- [ ] 主会话消息滚动、流式输出和 Composer 性能没有回退。
- [ ] 原生 Chrome 图标与内容区 Lucide、原生 Form Sheet 与 `SheetSurface` 的边界写入移动端设计/开发规则。
- [ ] 增补相关单测,并更新 iOS / Android、Light / Dark 的视觉基线或等价实机证据。
- [ ] 若发现 SDK 56 上游缺陷无法安全降级,试点可以停止在现有实现,不以 workaround 堆叠换取表面效果。

## 已考虑的替代方案 / Alternatives considered

1. **全量 SwiftUI / Compose 重写**:成本高、破坏现有跨端业务与流式渲染优势,收益与风险不匹配。
2. **继续全部自绘,只模仿系统外观**:可以接近截图,但无法获得系统转场、菜单、无障碍、动态字体和新系统材质的真实行为。
3. **一次性替换全部 Sheet 和 Header**:回归面过大,尤其会威胁主会话热路径和复杂双层 Sheet;不采用。
4. **直接加入 NativeTabs**:当前没有明确的信息架构需求,属于为了视觉改变产品结构;不采用。

Contributor guide

Open the contributing guide

Research direction

Start with apps/mobile/app/_layout.tsx and apps/mobile/app/settings.tsx, then inspect the existing SheetModal and SheetSurface boundaries. Implement and validate one Settings vertical slice with native header/back behavior plus a real menu or formSheet, while leaving the conversation content unchanged. Done requires light/dark, iOS/Android, accessibility, keyboard, safe-area, gesture, and performance validation, along with updated tests and design rules.

Written by the indexing model from the issue text.

Assessment

Tech stack
android, ios, react-native, typescript
Domain
accessibility, design, frontend, mobile
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.