Files
customer-im-client-dev/apps/im_app/lib/app/router/app_router.dart
pp-bot e8f58212e6
Some checks failed
CI / Lint (push) Has been cancelled
feat(chat): 正在输入指示器 — 点对点复刻 iOS + 性能改进
## 新增
- TypingIndicatorManager: 内存态管理器,精准Timer替代iOS 1s轮询
- TypingInputSender: per-chatId 节流(3s)/防抖(2s),修复iOS跨chat竞态
- WS chat_input/chat_typing 帧处理(mode2 + ctl 双路径)

## UI
- ChatDetailPage AppBar 绿色副标题显示"正在输入…"
- ChatPage 列表 snippet 绿色输入状态优先于 lastMsg
- 群聊不发送 typing 事件(对齐 iOS gate)

## 改进 (vs iOS)
- Timer 仅在有 entry 时启动,空时 null(零空转)
- per-chatId 隔离节流/防抖(iOS 全局共享有竞态 bug)
- msgIdx 守卫防止乱序帧覆盖 lastMsg

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 14:59:30 +09:00

206 lines
7.9 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:im_app/features/chat/view/chat_db_test_page.dart';
import 'package:im_app/features/app_tab/view/app_tab.dart';
import 'package:im_app/features/chat/view/chat_detail_page.dart';
import 'package:im_app/features/chat/view/chat_page.dart';
import 'package:im_app/features/contact/view/contact_page.dart';
import 'package:im_app/features/login/view/login_page.dart';
import 'package:im_app/features/settings/view/settings_page.dart';
import 'package:im_app/features/settings/view/theme_view.dart';
import 'package:im_app/features/settings/view/edit_profile_page.dart';
import 'package:im_app/features/settings/view/blocklist_page.dart';
import 'package:im_app/features/settings/view/language_page.dart';
import 'package:im_app/features/settings/view/network_diagnostics_page.dart';
import 'package:im_app/features/settings/view/about_page.dart';
import 'package:im_app/features/settings/view/favorites_page.dart';
import 'package:im_app/features/settings/view/recent_calls_page.dart';
import 'package:im_app/app/di/app_providers.dart';
import 'package:im_app/app/router/app_route_name.dart';
import 'package:im_app/app/router/guards/auth_guard.dart';
/// 应用路由 Provider
///
/// 路由结构:
/// ```
/// StatefulShellRoute底部导航栏持久容器
/// ├── /chat ChatPage
/// ├── /contact ContactPage
/// └── /settings SettingsPage
///
/// ── 全屏页面无底部导航栏parentNavigatorKey = _rootKey──
/// /chat/detail ChatDetailPageextra 传参)
/// /chat/:id ChatDetailPage路径参数
/// /settings/theme ThemeView
/// /login LoginPage
/// ```
///
/// ## Shell 内 vs Shell 外
///
/// Shell 内路由Tab 根路由)始终显示底部导航栏。
/// Shell 外路由(详情页 / 子功能页无底部导航栏push 进入后有返回按钮。
/// 这与 iOS / Android 主流 IM App 的交互一致(会话详情、设置子页均全屏)。
///
/// ## parentNavigatorKey 的作用
///
/// go_router push 时,路由默认放到"最近的 Navigator 祖先"上。
/// 在 StatefulShellBranch 内 push最近的 Navigator 是 Branch Navigator
/// 而不是 Root NavigatorShell 不会被盖住TabBar 仍然可见。
///
/// 设置 `parentNavigatorKey: _rootKey` 后,路由强制放到 Root Navigator
/// 盖住整个 ShellTabBar 消失,表现为真正的全屏页面。
///
/// ## 登录守卫
///
/// [authGuard] 检查 [AuthNotifier.isLoggedIn],未登录时重定向到 /login。
/// 登录 / 退出后 [AuthNotifier.notifyListeners] 触发 [refreshListenable]
/// go_router 自动重新执行 redirect无需手动跳转。
///
/// ## Tab 状态保持
///
/// [StatefulShellRoute.indexedStack] 为每个 Tab 维护独立的 Navigator 栈,
/// 切换 Tab 时页面状态(滚动位置、输入内容等)不丢失。
// Root Navigator Key供全屏路由声明 parentNavigatorKey确保覆盖整个 Shell
final _rootKey = GlobalKey<NavigatorState>();
final routerProvider = Provider<GoRouter>((ref) {
final authNotifier = ref.read(authNotifierProvider);
return GoRouter(
// Root Navigator 的 Key供全屏路由声明 parentNavigatorKey 使用,
// 确保 push 时覆盖整个 Shell包括 TabBar
navigatorKey: _rootKey,
// 冷启动默认落地页authGuard 会在进入前检查登录状态并按需重定向
initialLocation: AppRouteName.chat.path,
// 在控制台打印每次路由变化,方便开发期间调试;上线前设为 false
debugLogDiagnostics: true,
// 监听 authNotifier 变化:登录 / 退出时自动触发 redirect 重新执行,
// 无需在业务代码里手动 context.go守卫统一负责跳转
refreshListenable: authNotifier,
redirect: (context, state) => authGuard(authNotifier, state),
routes: [
// ── Shell 内:底部导航栏始终可见 ─────────────────────────────────────
StatefulShellRoute.indexedStack(
builder: (context, state, navigationShell) {
return AppTab(navigationShell: navigationShell);
},
branches: [
StatefulShellBranch(
routes: [
GoRoute(
path: AppRouteName.chat.path,
builder: (context, state) => const ChatPage(),
),
],
),
StatefulShellBranch(
routes: [
GoRoute(
path: AppRouteName.contact.path,
builder: (context, state) => const ContactPage(),
),
],
),
StatefulShellBranch(
routes: [
GoRoute(
path: AppRouteName.settings.path,
builder: (context, state) => const SettingsPage(),
),
],
),
],
),
// ── Shell 外:全屏页面,无底部导航栏 ─────────────────────────────────
// parentNavigatorKey: _rootKey 确保路由覆盖 ShellTabBar 消失
//
// extra 传参:接收 ({String conversationId, String title})
GoRoute(
parentNavigatorKey: _rootKey,
path: AppRouteName.chatDBTest.path,
builder: (context, state) {
return const ChatDbTestPage();
},
),
GoRoute(
parentNavigatorKey: _rootKey,
path: AppRouteName.chatDetail.path,
builder: (context, state) {
final extra = state.extra
as ({String conversationId, String title, int chatType});
return ChatDetailPage(
conversationId: extra.conversationId,
title: extra.title,
chatType: extra.chatType,
);
},
),
// 路径参数id 内嵌在 URL 中(/chat/:id
GoRoute(
parentNavigatorKey: _rootKey,
path: AppRouteName.chatDetailById.path,
builder: (context, state) {
final id = state.pathParameters['id']!;
return ChatDetailPage(
conversationId: id,
title: '路径参数详情',
);
},
),
GoRoute(
parentNavigatorKey: _rootKey,
path: AppRouteName.settingsTheme.path,
builder: (context, state) => const ThemeView(),
),
GoRoute(
parentNavigatorKey: _rootKey,
path: AppRouteName.settingsEditProfile.path,
builder: (context, state) => const EditProfilePage(),
),
GoRoute(
parentNavigatorKey: _rootKey,
path: AppRouteName.settingsBlocklist.path,
builder: (context, state) => const BlocklistPage(),
),
GoRoute(
parentNavigatorKey: _rootKey,
path: AppRouteName.settingsLanguage.path,
builder: (context, state) => const LanguagePage(),
),
GoRoute(
parentNavigatorKey: _rootKey,
path: AppRouteName.settingsNetworkDiagnostics.path,
builder: (context, state) => const NetworkDiagnosticsPage(),
),
GoRoute(
parentNavigatorKey: _rootKey,
path: AppRouteName.settingsAbout.path,
builder: (context, state) => const AboutPage(),
),
GoRoute(
parentNavigatorKey: _rootKey,
path: AppRouteName.settingsFavorites.path,
builder: (context, state) => const FavoritesPage(),
),
GoRoute(
parentNavigatorKey: _rootKey,
path: AppRouteName.settingsRecentCalls.path,
builder: (context, state) => const RecentCallsPage(),
),
GoRoute(
parentNavigatorKey: _rootKey,
path: AppRouteName.login.path,
builder: (context, state) => const LoginPage(),
),
],
);
});