diff --git a/docs/acceptance/b171-batch2.md b/docs/acceptance/b171-batch2.md index 4ad6c48..5c8d9c4 100644 --- a/docs/acceptance/b171-batch2.md +++ b/docs/acceptance/b171-batch2.md @@ -1,6 +1,6 @@ # B-185 / B-171 第二批交互补齐验收与回退 -分支:`agent/agent/7b089146`(本工作副本本地名为 `agent/agent/1a87b790`,基于该远程分支 `a4d21ef`) +分支:`agent/agent/7b089146` 基线:`fb58da6`(B-171 第一批完成点) 架构:B-90;视觉:B-91 `changlian-visual-spec-v2.md` @@ -8,7 +8,7 @@ ## 提交主题(可逐项 `git revert`) -从新到旧回退:先 B-188 验收补记与 `d9d5853`,再原第二批验收提交,再按下表从下往上 `git revert`。 +从新到旧回退:先 B-189,再 B-188 验收补记与 `d9d5853`,再原第二批验收提交,再按下表从下往上 `git revert`。 | 主题 | SHA | 主要文件 | |---|---|---| @@ -19,17 +19,34 @@ | 工号查询防抖、控制器生命周期、开关、资料页工号 | `26622676ed4f4898385d8e9e686296eb56adf4be` | `add_colleague_page.dart`、`app_pages.dart`、`company_login_page.dart`、`my_info_view.dart`、`personal_info_view.dart` | | 断网横幅恢复后 300ms 收起、底栏 12px | `d1657ea29f0fc59f248f2108d7cfb9bcc239921e` | `home_view.dart` | | PC 第二批登录/会话/聊天/添加同事/职务行/i18n | `a44537da4e8ce7b5b17b491c05592ad21cdd0bca` | `pc-client/src/pages/**`、`ApplicationItem`、`i18n`、`global.scss`、`docs/acceptance/pc-boundary.md` | -| 本验收记录 | 本文件当次提交 | `docs/acceptance/b171-batch2.md` | +| 第二批验收记录 | `a4d21ef36777440812c6746cd792eeaf7685cd5c` | `docs/acceptance/b171-batch2.md` | | B-188 定点返工:PC 会话置顶改 `setConversation` | `d9d5853f73aa8060c9ed768c326f00d691847e3e` | `pc-client/src/pages/chat/ConversationSider/ConversationItem.tsx` | -| B-188 验收补记 | 本文件当次提交 | `docs/acceptance/b171-batch2.md` | +| B-188 验收补记 | `e40ece1` | `docs/acceptance/b171-batch2.md` | -回退 B-188:先 `git revert` 本验收补记提交,再 `git revert d9d5853f73aa8060c9ed768c326f00d691847e3e`。 +回退 B-188:先 `git revert e40ece1`,再 `git revert d9d5853f73aa8060c9ed768c326f00d691847e3e`。 + +## B-189 语音区返工(相对 `e40ece1`) + +依据:B-187 打回三项阻塞 + 输入框/`按住 说话` 底色 `searchBg`。不处理其余黄色建议。 + +| 主题 | SHA | 主要文件 | +|---|---|---| +| 未听红点、录音浮层入场/取消区、50–60s 时限、searchBg | 见当次提交 | `chat_voice_*.dart`、`chat_item_view.dart`、`chat_input_box.dart`、`apply_official_styles.dart`、`styles.dart` | + +回退 B-189:`git revert <当次 SHA>` + +静态核对(对照代码,非运行): + +- 未听红点:接收且 `message.isRead == false` 时在气泡右侧 8px `danger`;播放走既有 `_playVoiceMessage` + `_markMessageAsRead`,不另造状态。 +- 浮层入场:150ms ease-out 透明度 0→1,`Styles.motionOf` 接到 `AppMotion.reduce` / `AppDuration.overlay`;取消区保持 `rgba(0,0,0,0.7)` 黑卡,仅垃圾桶与提示改 `danger`;切换时长走 `AppDuration.overlaySwitch`。 +- 50s:计时改 `warning` 文案「还可说 N 秒」;60s:既有 `ChatRecordVoiceView` 中断→dispose→一次 `onCompleted`,toast「已达最长 60 秒,已自动发送」。实例级 `_completed` 避免重复发送;timer 在 dispose 取消。 +- 输入框与「按住 说话」底色改为 `Styles.c_F2F3F5` ← `AppColors.searchBg`,不再用 `c_F0F2F6`/`chatBg`。 ## 检查 - `dart format` / `flutter analyze` / Android debug+release:**未跑。** 本环境 PATH 中无 `flutter`/`dart`/`java`,不推定通过。 -- PC `npm run lint` / `vite build`:**未跑。** 仓库内无 `pc-client/node_modules`,不推定通过。 -- 运行截图:无测试账号、无客户端进程,**未截图。** +- PC `npm run lint` / `vite build`:B-189 未改 PC、未重跑。B-188 当时 `npm run build` 通过,见下节。 +- 运行截图:无 Flutter 工具链、无测试账号、无客户端进程,**未截图、未实测**未听红点/播放清除/取消手势/50–60s 倒计时/到限自动发送与 toast。不得推定通过。 ### B-188 定点返工检查(2026-08-20) @@ -51,8 +68,6 @@ - `src/pages/common/UserCardModal/index.tsx` TS2345 - `src/utils/imCommon.ts` TS2322 ×11(`string | undefined` → `string`) -本卡不自行宣布整批通过。 - ## 越界比对(对比 `e26abad`) 零改动:`mobile/`、`pc-client/src/store/`、`pc-client/src/layout/MainContentWrap.tsx`、`pc-client/src/pages/chat/queryChat/ChatFooter/useSendMessage.ts`、`pc-client/src/pages/chat/queryChat/useHistoryMessageList.tsx`、`account-service/`。 @@ -61,6 +76,8 @@ `pc-boundary.md` 已扩到与 B-90 / B-185 一致:登录窗、chat **展示**、`AddColleague.tsx` 列入可改;发送/历史/store/SDK 初始化仍冻结。PC 聊天顶栏去掉「语音通话」入口;RTC 死代码未删。 +B-189 改动限于 `mobile-next/openim_common` 语音/输入部件、Token 接线、`mobile-next/lib/pages/chat/chat_view.dart` 自定义语音气泡防双重气泡,以及本验收记录。 + ## 未验证(缺账号环境,禁止推定通过) - HTTP 正确密码登录 @@ -68,9 +85,10 @@ - 服务端好友申请 `status=2` - LiveKit / 语音通话(本阶段不迁、PC 不建设) - 登录窗独立 360×480:受 electron 冻结限制,本批只改登录页展示,未改窗口尺寸 +- B-189 全部录音/未听红点运行时表现 ## 品牌 未换标。手机登录仍按开关保留 OpenIM 标识。不增删品牌资源。 -功能质检与视觉审核须重新执行,本卡不自行宣布通过。 +功能质检与视觉审核须重新执行,本卡不自行宣布整批通过。 diff --git a/mobile-next/lib/company/brand/apply_official_styles.dart b/mobile-next/lib/company/brand/apply_official_styles.dart index 3846948..22b9f81 100644 --- a/mobile-next/lib/company/brand/apply_official_styles.dart +++ b/mobile-next/lib/company/brand/apply_official_styles.dart @@ -17,7 +17,12 @@ void applyOfficialStylesFromTokens() { Styles.c_F4F5F7 = AppColors.chatBg; Styles.c_F8F9FA = AppColors.pageBg; Styles.c_F0F2F6 = AppColors.chatBg; + Styles.c_F2F3F5 = AppColors.searchBg; Styles.c_F2F8FF = AppColors.primaryActive; + Styles.durationOverlay = AppDuration.overlay; + Styles.durationOverlaySwitch = AppDuration.overlaySwitch; + Styles.reduceMotion = AppMotion.reduce; + Styles.motionOf = AppMotion.of; Styles.c_18E875 = AppColors.success; Styles.c_FFB300 = AppColors.warning; Styles.c_6085B1 = AppColors.avatarGray; @@ -91,10 +96,18 @@ void applyOfficialStylesFromTokens() { color: AppColors.danger, fontSize: AppFont.body, ); + Styles.ts_FF381F_12sp = TextStyle( + color: AppColors.danger, + fontSize: AppFont.small, + ); Styles.ts_FF381F_10sp = TextStyle( color: AppColors.danger, fontSize: AppFont.small, ); + Styles.ts_FFB300_12sp = TextStyle( + color: AppColors.warning, + fontSize: AppFont.small, + ); Styles.ts_6085B1_17sp_medium = TextStyle( color: AppColors.avatarGray, fontSize: AppFont.body, diff --git a/mobile-next/lib/pages/chat/chat_view.dart b/mobile-next/lib/pages/chat/chat_view.dart index f190d1e..b474545 100644 --- a/mobile-next/lib/pages/chat/chat_view.dart +++ b/mobile-next/lib/pages/chat/chat_view.dart @@ -195,7 +195,9 @@ class ChatPage extends StatelessWidget { soundUrl: soundElem.sourceUrl, duration: soundElem.duration, isPlaying: logic.isPlaySound(message), + isUnread: !isISend && message.isRead == false, ), + false, ); } } diff --git a/mobile-next/openim_common/lib/src/res/lang/en_US.dart b/mobile-next/openim_common/lib/src/res/lang/en_US.dart index 16ee70f..999a4e5 100644 --- a/mobile-next/openim_common/lib/src/res/lang/en_US.dart +++ b/mobile-next/openim_common/lib/src/res/lang/en_US.dart @@ -535,6 +535,8 @@ const Map en_US = { "nWeek": "%s weeks", "nMonth": "%s months", "talkTooShort": "Talk too short", + "remainingRecordSeconds": "%s seconds left", + "autoSendMaxVoiceDuration": "Reached the 60-second limit, sent automatically", 'quoteContentBeRevoked': 'Quote content be revoked', 'tapTooShort': 'Tap too short', 'createGroupTips': 'The group name cannot exceed 16 characters', diff --git a/mobile-next/openim_common/lib/src/res/lang/zh_CN.dart b/mobile-next/openim_common/lib/src/res/lang/zh_CN.dart index 110fda5..3124979 100644 --- a/mobile-next/openim_common/lib/src/res/lang/zh_CN.dart +++ b/mobile-next/openim_common/lib/src/res/lang/zh_CN.dart @@ -535,6 +535,8 @@ const Map zh_CN = { "nWeek": "%s周", "nMonth": "%s个月", "talkTooShort": "说话时间太短", + "remainingRecordSeconds": "还可说 %s 秒", + "autoSendMaxVoiceDuration": "已达最长 60 秒,已自动发送", 'quoteContentBeRevoked': '引用内容已被删除', 'tapTooShort': '点击时间太短', 'createGroupTips': '群名称不能超过16个字符', diff --git a/mobile-next/openim_common/lib/src/res/strings.dart b/mobile-next/openim_common/lib/src/res/strings.dart index 60db3b3..2279cc3 100644 --- a/mobile-next/openim_common/lib/src/res/strings.dart +++ b/mobile-next/openim_common/lib/src/res/strings.dart @@ -1090,6 +1090,10 @@ class StrRes { static String get talkTooShort => 'talkTooShort'.tr; + static String get remainingRecordSeconds => 'remainingRecordSeconds'.tr; + + static String get autoSendMaxVoiceDuration => 'autoSendMaxVoiceDuration'.tr; + static String get quoteContentBeRevoked => 'quoteContentBeRevoked'.tr; static String get tapTooShort => 'tapTooShort'.tr; diff --git a/mobile-next/openim_common/lib/src/res/styles.dart b/mobile-next/openim_common/lib/src/res/styles.dart index 024833d..fec8907 100644 --- a/mobile-next/openim_common/lib/src/res/styles.dart +++ b/mobile-next/openim_common/lib/src/res/styles.dart @@ -12,7 +12,18 @@ class Styles { static Color c_FFFFFF = const Color(0xFFFFFFFF); // 警告色 static Color c_18E875 = const Color(0xFF18E875); // 在线 static Color c_F0F2F6 = const Color(0xFFF0F2F6); // 聊天页底部 + static Color c_F2F3F5 = const Color(0xFFF2F3F5); // 搜索/输入框底 searchBg static Color c_000000 = const Color(0xFF000000); // + + static Duration durationOverlay = const Duration(milliseconds: 150); + static Duration durationOverlaySwitch = const Duration(milliseconds: 100); + + static bool Function(BuildContext) reduceMotion = + (BuildContext context) => MediaQuery.maybeOf(context)?.disableAnimations ?? false; + + static Duration Function(BuildContext, Duration) motionOf = + (BuildContext context, Duration normal) => reduceMotion(context) ? Duration.zero : normal; + static Color c_92B3E0 = const Color(0xFF92B3E0); static Color c_F2F8FF = const Color(0xFFF2F8FF); // 同步成功背景色 static Color c_F8F9FA = const Color(0xFFF8F9FA); // 默认背景 @@ -264,6 +275,11 @@ class Styles { fontSize: 10.sp, ); + static TextStyle ts_FFB300_12sp = TextStyle( + color: c_FFB300, + fontSize: 12.sp, + ); + static TextStyle ts_6085B1_17sp_medium = TextStyle( color: c_6085B1, fontSize: 17.sp, diff --git a/mobile-next/openim_common/lib/src/widgets/chat/chat_input_box.dart b/mobile-next/openim_common/lib/src/widgets/chat/chat_input_box.dart index d72cdeb..251bf3e 100644 --- a/mobile-next/openim_common/lib/src/widgets/chat/chat_input_box.dart +++ b/mobile-next/openim_common/lib/src/widgets/chat/chat_input_box.dart @@ -204,7 +204,7 @@ class _ChatInputBoxState extends State /*with TickerProviderStateM constraints: const BoxConstraints(minHeight: 40), margin: EdgeInsets.only(top: 8.h, bottom: _showQuoteView ? 4.h : 8.h), decoration: BoxDecoration( - color: Styles.c_F0F2F6, + color: Styles.c_F2F3F5, borderRadius: BorderRadius.circular(8.r), ), child: ChatTextField( diff --git a/mobile-next/openim_common/lib/src/widgets/chat/chat_item_view.dart b/mobile-next/openim_common/lib/src/widgets/chat/chat_item_view.dart index 7807eaa..e3ebb94 100644 --- a/mobile-next/openim_common/lib/src/widgets/chat/chat_item_view.dart +++ b/mobile-next/openim_common/lib/src/widgets/chat/chat_item_view.dart @@ -251,7 +251,7 @@ class _ChatItemViewState extends State { message: _message, ); } else if (_message.isVoiceType) { - isBubbleBg = true; + isBubbleBg = false; final sound = _message.soundElem; child = ChatVoiceView( isISend: _isISend, @@ -259,6 +259,7 @@ class _ChatItemViewState extends State { soundUrl: sound?.sourceUrl, duration: sound?.duration, isPlaying: widget.isPlayingSound, + isUnread: !_isISend && _message.isRead == false, ); } else if (_message.isVideoType) { child = widget.mediaItemBuilder?.call(context, _message) ?? diff --git a/mobile-next/openim_common/lib/src/widgets/chat/chat_voice_read_status_view.dart b/mobile-next/openim_common/lib/src/widgets/chat/chat_voice_read_status_view.dart index dc3162c..edfaf6e 100644 --- a/mobile-next/openim_common/lib/src/widgets/chat/chat_voice_read_status_view.dart +++ b/mobile-next/openim_common/lib/src/widgets/chat/chat_voice_read_status_view.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:openim_common/openim_common.dart'; class ChatVoiceReadStatusView extends StatelessWidget { @@ -7,8 +6,8 @@ class ChatVoiceReadStatusView extends StatelessWidget { @override Widget build(BuildContext context) => Container( - width: 6.w, - height: 6.w, + width: 8, + height: 8, decoration: BoxDecoration( color: Styles.c_FF381F, shape: BoxShape.circle, diff --git a/mobile-next/openim_common/lib/src/widgets/chat/chat_voice_record_bar.dart b/mobile-next/openim_common/lib/src/widgets/chat/chat_voice_record_bar.dart index f82ecde..f6f3639 100644 --- a/mobile-next/openim_common/lib/src/widgets/chat/chat_voice_record_bar.dart +++ b/mobile-next/openim_common/lib/src/widgets/chat/chat_voice_record_bar.dart @@ -107,7 +107,7 @@ class _ChatVoiceRecordBarState extends State { alignment: Alignment.center, decoration: BoxDecoration( color: widget.speakBarColor ?? - (_pressing ? Styles.c_F2F8FF : Styles.c_F0F2F6), + (_pressing ? Styles.c_F2F8FF : Styles.c_F2F3F5), borderRadius: BorderRadius.circular(8.r), ), child: Text( diff --git a/mobile-next/openim_common/lib/src/widgets/chat/chat_voice_record_layout.dart b/mobile-next/openim_common/lib/src/widgets/chat/chat_voice_record_layout.dart index 45b472c..b23e5f6 100644 --- a/mobile-next/openim_common/lib/src/widgets/chat/chat_voice_record_layout.dart +++ b/mobile-next/openim_common/lib/src/widgets/chat/chat_voice_record_layout.dart @@ -5,6 +5,7 @@ import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:lottie/lottie.dart'; import 'package:openim_common/openim_common.dart'; import 'package:rxdart/rxdart.dart'; +import 'package:sprintf/sprintf.dart'; typedef SpeakViewChildBuilder = Widget Function(ChatVoiceRecordBar recordBar); @@ -36,9 +37,12 @@ class _ChatVoiceRecordLayoutState extends State { bool _showVoiceRecordView = false; RecordBarStatus _status = RecordBarStatus.holdTalk; bool _isCancelSend = false; + bool _autoSendByLimit = false; void _completed(int sec, String path) { - if (_isCancelSend) { + final autoSend = _autoSendByLimit; + _autoSendByLimit = false; + if (_isCancelSend && !autoSend) { File(path).delete(); _status = RecordBarStatus.holdTalk; _isCancelSend = false; @@ -48,6 +52,9 @@ class _ChatVoiceRecordLayoutState extends State { IMViews.showToast(StrRes.talkTooShort); } else { widget.onCompleted?.call(sec, path); + if (autoSend) { + IMViews.showToast(StrRes.autoSendMaxVoiceDuration); + } } } } @@ -83,6 +90,8 @@ class _ChatVoiceRecordLayoutState extends State { }, onLongPressStart: (details) { setState(() { + _autoSendByLimit = false; + _isCancelSend = false; _showVoiceRecordView = true; }); }, @@ -93,61 +102,106 @@ class _ChatVoiceRecordLayoutState extends State { return Stack( children: [ widget.builder(_createSpeakBar), - Visibility( - visible: _showVoiceRecordView, - child: ChatRecordVoiceView( - onCompleted: _completed, - onInterrupt: () { - setState(() { - _interruptSub.add(true); - _showVoiceRecordView = false; - }); - }, - builder: (_, sec) => Material( - color: Colors.transparent, - child: Center( - child: AnimatedContainer( - duration: const Duration(milliseconds: 100), - curve: Curves.easeOut, - width: 160.w, - height: 140.h, - padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 12.h), - decoration: BoxDecoration( - color: _isCancelSend - ? Styles.c_FF381F_opacity70 - : Styles.c_000000_opacity70, - borderRadius: BorderRadius.circular(12.r), - ), - child: Column( - children: [ - IMUtils.seconds2HMS(sec).toText - ..style = Styles.ts_FFFFFF_12sp, - Expanded( - child: _isCancelSend - ? Icon(Icons.delete_outline, - size: 48.w, color: Styles.c_FFFFFF) - : _lottieAnimWidget, - ), - (_isCancelSend - ? StrRes.liftFingerToCancelSend - : StrRes.releaseToSendSwipeUpToCancel) - .toText - ..style = Styles.ts_FFFFFF_12sp - ..maxLines = 1, - ], + if (_showVoiceRecordView) + _VoiceRecordOverlayFade( + child: ChatRecordVoiceView( + maxRecordSec: widget.maxRecordSec, + onCompleted: _completed, + onInterrupt: () { + if (_autoSendByLimit) return; + _autoSendByLimit = true; + setState(() { + _interruptSub.add(true); + _showVoiceRecordView = false; + }); + }, + builder: (_, sec) => Material( + color: Colors.transparent, + child: Center( + child: AnimatedContainer( + duration: Styles.motionOf(context, Styles.durationOverlaySwitch), + curve: Curves.easeOut, + width: 160.w, + height: 140.h, + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 12.h), + decoration: BoxDecoration( + color: Styles.c_000000_opacity70, + borderRadius: BorderRadius.circular(12.r), + ), + child: Column( + children: [ + _timerText(sec), + Expanded( + child: _isCancelSend + ? Icon(Icons.delete_outline, + size: 48.w, color: Styles.c_FF381F) + : _lottieAnimWidget, + ), + (_isCancelSend + ? StrRes.liftFingerToCancelSend + : StrRes.releaseToSendSwipeUpToCancel) + .toText + ..style = _isCancelSend + ? Styles.ts_FF381F_12sp + : Styles.ts_FFFFFF_12sp + ..maxLines = 1, + ], + ), ), ), ), ), ), - ), ], ); } + Widget _timerText(int sec) { + final remaining = widget.maxRecordSec - sec; + final nearLimit = sec >= widget.maxRecordSec - 10 && remaining > 0; + final text = nearLimit + ? sprintf(StrRes.remainingRecordSeconds, [remaining]) + : IMUtils.seconds2HMS(sec); + return text.toText + ..style = nearLimit ? Styles.ts_FFB300_12sp : Styles.ts_FFFFFF_12sp; + } + Widget get _lottieAnimWidget => Lottie.asset( 'assets/anim/voice_record.json', fit: BoxFit.contain, package: 'openim_common', ); } + +class _VoiceRecordOverlayFade extends StatefulWidget { + const _VoiceRecordOverlayFade({required this.child}); + + final Widget child; + + @override + State<_VoiceRecordOverlayFade> createState() => _VoiceRecordOverlayFadeState(); +} + +class _VoiceRecordOverlayFadeState extends State<_VoiceRecordOverlayFade> { + double _opacity = 0; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + setState(() => _opacity = 1); + }); + } + + @override + Widget build(BuildContext context) { + final reduce = Styles.reduceMotion(context); + return AnimatedOpacity( + opacity: reduce ? 1 : _opacity, + duration: Styles.motionOf(context, Styles.durationOverlay), + curve: Curves.easeOut, + child: widget.child, + ); + } +} diff --git a/mobile-next/openim_common/lib/src/widgets/chat/chat_voice_record_view.dart b/mobile-next/openim_common/lib/src/widgets/chat/chat_voice_record_view.dart index 2291ce3..c312d2e 100644 --- a/mobile-next/openim_common/lib/src/widgets/chat/chat_voice_record_view.dart +++ b/mobile-next/openim_common/lib/src/widgets/chat/chat_voice_record_view.dart @@ -24,11 +24,13 @@ class ChatRecordVoiceView extends StatefulWidget { class _ChatRecordVoiceViewState extends State { static const _dir = "voice"; static const _ext = ".m4a"; - late String _path; + String? _path; int _startTimestamp = 0; final _audioRecorder = AudioRecorder(); Timer? _timer; int _duration = 0; + bool _interrupted = false; + bool _completed = false; static int _now() => DateTime.now().millisecondsSinceEpoch; @@ -37,16 +39,28 @@ class _ChatRecordVoiceViewState extends State { (() async { if (await _audioRecorder.hasPermission()) { _path = '${await IMUtils.createTempDir(dir: _dir)}/${_now()}$_ext'; - await _audioRecorder.start(RecordConfig(), path: _path); + await _audioRecorder.start(RecordConfig(), path: _path!); + if (!mounted) { + await _audioRecorder.stop(); + return; + } _startTimestamp = _now(); _timer?.cancel(); - _timer = null; - _timer = Timer.periodic(const Duration(seconds: 1), (timer) async { + _timer = Timer.periodic(const Duration(seconds: 1), (timer) { + if (!mounted) { + timer.cancel(); + return; + } + final duration = ((_now() - _startTimestamp) ~/ 1000); + if (duration >= widget.maxRecordSec) { + timer.cancel(); + _timer = null; + _duration = duration; + _interruptByLimit(); + return; + } setState(() { - _duration = ((_now() - _startTimestamp) ~/ 1000); - if (_duration >= widget.maxRecordSec) { - widget.onInterrupt?.call(); - } + _duration = duration; }); }); } @@ -54,17 +68,32 @@ class _ChatRecordVoiceViewState extends State { super.initState(); } - @override - void dispose() { - (() async { - _timer?.cancel(); - _timer = null; + void _interruptByLimit() { + if (_interrupted) return; + _interrupted = true; + widget.onInterrupt?.call(); + } + Future _stopAndComplete() async { + if (_completed) return; + _completed = true; + _timer?.cancel(); + _timer = null; + try { if (await _audioRecorder.isRecording()) { await _audioRecorder.stop(); } - widget.onCompleted?.call(_duration, _path); - })(); + } catch (_) {} + final path = _path; + if (path == null) return; + widget.onCompleted?.call(_duration, path); + } + + @override + void dispose() { + _timer?.cancel(); + _timer = null; + unawaited(_stopAndComplete()); super.dispose(); } diff --git a/mobile-next/openim_common/lib/src/widgets/chat/chat_voice_view.dart b/mobile-next/openim_common/lib/src/widgets/chat/chat_voice_view.dart index 6170b3e..a9b0c6c 100644 --- a/mobile-next/openim_common/lib/src/widgets/chat/chat_voice_view.dart +++ b/mobile-next/openim_common/lib/src/widgets/chat/chat_voice_view.dart @@ -8,6 +8,7 @@ class ChatVoiceView extends StatelessWidget { final String? soundUrl; final int? duration; final bool isPlaying; + final bool isUnread; const ChatVoiceView({ Key? key, @@ -16,6 +17,7 @@ class ChatVoiceView extends StatelessWidget { this.soundUrl, this.duration, this.isPlaying = false, + this.isUnread = false, }) : super(key: key); double get _bubbleWidth { @@ -56,9 +58,22 @@ class ChatVoiceView extends StatelessWidget { @override Widget build(BuildContext context) { - return SizedBox( - width: _bubbleWidth, - child: _buildVoiceAnimView(), + final bubble = ChatBubble( + bubbleType: isISend ? BubbleType.send : BubbleType.receiver, + child: SizedBox( + width: _bubbleWidth, + child: _buildVoiceAnimView(), + ), + ); + if (!isUnread) return bubble; + return Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + bubble, + 8.horizontalSpace, + const ChatVoiceReadStatusView(), + ], ); } }