From c3aed3d14a5196ce3b66d90013f02d7b47a92dcd Mon Sep 17 00:00:00 2001 From: Zeew Date: Tue, 19 Aug 2025 07:22:08 +0800 Subject: [PATCH] =?UTF-8?q?feat(group):=20=E6=96=B0=E5=A2=9E=E9=80=80?= =?UTF-8?q?=E5=87=BA/=E8=B8=A2=E5=87=BA=E7=BE=A4=E7=BB=84=E6=97=B6?= =?UTF-8?q?=E4=BF=9D=E7=95=99=E6=9C=AC=E5=9C=B0=E4=BC=9A=E8=AF=9D=E6=B6=88?= =?UTF-8?q?=E6=81=AF=E7=9A=84=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tui_group_listener_model.dart | 112 +++++++++- .../group_chat_status_manager.dart | 135 ++++++++++++ .../tui_chat_separate_view_model.dart | 14 ++ .../TIMUIKitAppBar/tim_uikit_appbar.dart | 24 ++- .../tim_uikit_text_field.dart | 199 ++++++++++-------- .../tim_uikit_text_field_layout/narrow.dart | 35 ++- .../tim_uikit_text_field_layout/wide.dart | 12 +- lib/ui/views/TIMUIKitChat/tim_uikit_chat.dart | 53 ++++- .../widgets/tim_uikit_group_button_area.dart | 172 ++++++++++----- lib/ui/widgets/disabled_input_widget.dart | 55 +++++ 10 files changed, 652 insertions(+), 159 deletions(-) create mode 100644 lib/business_logic/separate_models/group_chat_status_manager.dart create mode 100644 lib/ui/widgets/disabled_input_widget.dart diff --git a/lib/business_logic/listener_model/tui_group_listener_model.dart b/lib/business_logic/listener_model/tui_group_listener_model.dart index 055bd80..aabe54c 100644 --- a/lib/business_logic/listener_model/tui_group_listener_model.dart +++ b/lib/business_logic/listener_model/tui_group_listener_model.dart @@ -19,11 +19,14 @@ import 'package:tencent_cloud_chat_sdk/models/v2_tim_topic_info.dart' if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_topic_info.dart'; import 'package:tencent_cloud_chat_sdk/models/v2_tim_value_callback.dart' if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_value_callback.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart' + if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_message.dart'; import 'package:tencent_cloud_chat_sdk/tencent_im_sdk_plugin.dart'; import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_chat_global_model.dart'; import 'package:tencent_cloud_chat_uikit/data_services/group/group_services.dart'; import 'package:tencent_cloud_chat_uikit/data_services/services_locatar.dart'; import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart'; +import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/group_chat_status_manager.dart'; enum UpdateType { groupInfo, memberList, joinApplicationList, groupDismissed, kickedFromGroup } @@ -54,12 +57,19 @@ class TUIGroupListenerModel extends ChangeNotifier { } TUIGroupListenerModel() { - _groupListener = V2TimGroupListener(onMemberInvited: (groupID, opUser, memberList) { + _groupListener = V2TimGroupListener(onMemberInvited: (groupID, opUser, memberList) async { + // 受邀进入群:清除“已退出/被踢出”状态 + await GroupChatStatusManager.instance.clearGroupChatStatus(groupID); _needUpdate = NeedUpdate(groupID, UpdateType.memberList, ""); notifyListeners(); }, onMemberKicked: (groupID, opUser, memberList) async { if (_isLoginUserKickedFromGroup(groupID, memberList)) { - _deleteGroupConversation(groupID); + // 不再删除会话,而是标记为被踢出状态 + await GroupChatStatusManager.instance + .setGroupChatStatus(groupID, GroupChatStatus.userKicked); + + // 为被踢出的用户创建本地提示消息 + await _insertKickedOutMessage(groupID, opUser); final groupName = await _getGroupName(groupID); _needUpdate = NeedUpdate(groupID, UpdateType.kickedFromGroup, groupName); @@ -69,7 +79,16 @@ class TUIGroupListenerModel extends ChangeNotifier { _needUpdate = NeedUpdate(groupID, UpdateType.memberList, ""); notifyListeners(); } - }, onMemberEnter: (String groupID, List memberList) { + }, onMemberEnter: (String groupID, List memberList) async { + // 检查当前用户是否重新加入了群聊 + final currentUserID = coreInstance.loginInfo.userID; + final isCurrentUserJoined = memberList.any((member) => member.userID == currentUserID); + + if (isCurrentUserJoined) { + // 当前用户重新加入群聊,清除之前的退出/被踢出状态 + await GroupChatStatusManager.instance.clearGroupChatStatus(groupID); + } + _needUpdate = NeedUpdate(groupID, UpdateType.memberList, ""); notifyListeners(); }, onMemberLeave: (String groupID, V2TimGroupMemberInfo member) { @@ -170,4 +189,91 @@ class TUIGroupListenerModel extends ChangeNotifier { } return false; } + + // 常量定义 + static const String _centerTipType = 'center_tip'; + static const String _kickedMessageTemplate = '你被{operator}踢出了群组'; + + /// 为被踢出的用户创建本地提示消息 + Future _insertKickedOutMessage( + String groupID, V2TimGroupMemberInfo? opUser) async { + if (groupID.isEmpty) { + debugPrint("群组ID为空,无法插入踢出提示消息"); + return; + } + + try { + final operatorName = _getOperatorDisplayName(opUser); + final message = await _createKickedOutMessage(operatorName); + + if (message != null) { + await _insertMessageToLocal(message, groupID); + } + } catch (e) { + debugPrint("插入被踢出提示消息失败: $e"); + } + } + + /// 获取操作者显示名称 + String _getOperatorDisplayName(V2TimGroupMemberInfo? opUser) { + if (opUser == null) return "管理员"; + + // 优先使用昵称,其次用户ID,最后使用默认值 + if (opUser.nickName?.isNotEmpty == true) { + return opUser.nickName!; + } + + if (opUser.userID?.isNotEmpty == true) { + return opUser.userID!; + } + + return "管理员"; + } + + /// 创建被踢出提示消息 + Future _createKickedOutMessage(String operatorName) async { + final messageText = + _kickedMessageTemplate.replaceAll('{operator}', operatorName); + final messageData = { + 'type': _centerTipType, + 'text': messageText, + }; + + final textMsgRes = await sdkInstance + .getMessageManager() + .createCustomMessage(data: jsonEncode(messageData)); + + if (textMsgRes.code == 0 && textMsgRes.data?.messageInfo != null) { + return textMsgRes.data!.messageInfo!; + } + + debugPrint("创建自定义消息失败: ${textMsgRes.desc}"); + return null; + } + + /// 将消息插入到本地存储 + Future _insertMessageToLocal( + V2TimMessage message, String groupID) async { + // 获取当前用户ID + final loginUserRes = await sdkInstance.getLoginUser(); + final currentUserID = loginUserRes.data; + + if (currentUserID?.isEmpty != false) { + debugPrint("无法获取当前用户ID,插入消息失败"); + return; + } + + // 插入到本地存储 + final insertRes = await sdkInstance + .getMessageManager() + .insertGroupMessageToLocalStorageV2( + message: message, + groupID: groupID, + senderID: currentUserID!, + ); + + if (insertRes.code != 0) { + debugPrint("插入消息到本地存储失败: ${insertRes.desc}"); + } + } } diff --git a/lib/business_logic/separate_models/group_chat_status_manager.dart b/lib/business_logic/separate_models/group_chat_status_manager.dart new file mode 100644 index 0000000..0c98dde --- /dev/null +++ b/lib/business_logic/separate_models/group_chat_status_manager.dart @@ -0,0 +1,135 @@ +import 'dart:async'; +import 'package:shared_preferences/shared_preferences.dart'; + +/// 群聊状态枚举 +enum GroupChatStatus { + /// 正常状态 + normal, + /// 用户主动退出 + userLeft, + /// 用户被踢出 + userKicked, +} + +/// 群聊状态管理器 +class GroupChatStatusManager { + static const String _keyPrefix = 'group_chat_status_'; + static GroupChatStatusManager? _instance; + + // 状态变化流控制器 + final StreamController> _statusController = + StreamController>.broadcast(); + + // 内存中的状态缓存 + final Map _statusCache = {}; + + GroupChatStatusManager._internal(); + + static GroupChatStatusManager get instance { + _instance ??= GroupChatStatusManager._internal(); + return _instance!; + } + + /// 获取状态变化流 + Stream> get statusStream => _statusController.stream; + + /// 初始化状态缓存(从 SharedPreferences 加载) + Future _initializeCache() async { + final prefs = await SharedPreferences.getInstance(); + final keys = prefs.getKeys().where((key) => key.startsWith(_keyPrefix)); + + for (final key in keys) { + final groupID = key.substring(_keyPrefix.length); + final statusString = prefs.getString(key); + + if (statusString != null) { + GroupChatStatus status; + switch (statusString) { + case 'userLeft': + status = GroupChatStatus.userLeft; + break; + case 'userKicked': + status = GroupChatStatus.userKicked; + break; + default: + status = GroupChatStatus.normal; + } + _statusCache[groupID] = status; + } + } + } + + /// 获取当前状态映射(如果缓存为空则初始化) + Future> getCurrentStatusMap() async { + if (_statusCache.isEmpty) { + await _initializeCache(); + } + return Map.from(_statusCache); + } + + /// 设置群聊状态 + Future setGroupChatStatus(String groupID, GroupChatStatus status) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setString('${_keyPrefix}$groupID', status.name); + + // 更新缓存 + _statusCache[groupID] = status; + + // 通知状态变化,发送完整的状态映射 + _statusController.add(Map.from(_statusCache)); + } + + /// 获取群聊状态 + Future getGroupChatStatus(String groupID) async { + final prefs = await SharedPreferences.getInstance(); + final statusString = prefs.getString('${_keyPrefix}$groupID'); + + if (statusString == null) { + return GroupChatStatus.normal; + } + + switch (statusString) { + case 'userLeft': + return GroupChatStatus.userLeft; + case 'userKicked': + return GroupChatStatus.userKicked; + default: + return GroupChatStatus.normal; + } + } + + /// 清除群聊状态(恢复为正常状态) + Future clearGroupChatStatus(String groupID) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.remove('${_keyPrefix}$groupID'); + + // 更新缓存为正常状态 + _statusCache[groupID] = GroupChatStatus.normal; + + // 通知状态变化,发送完整的状态映射 + _statusController.add(Map.from(_statusCache)); + } + + /// 释放资源 + void dispose() { + _statusController.close(); + } + + /// 检查群聊是否已退出(包括主动退出和被踢出) + Future isGroupLeft(String groupID) async { + final status = await getGroupChatStatus(groupID); + return status == GroupChatStatus.userLeft || status == GroupChatStatus.userKicked; + } + + /// 检查群聊是否被踢出 + Future isGroupKicked(String groupID) async { + final status = await getGroupChatStatus(groupID); + return status == GroupChatStatus.userKicked; + } + + /// 检查群聊是否主动退出 + Future isGroupUserLeft(String groupID) async { + final status = await getGroupChatStatus(groupID); + return status == GroupChatStatus.userLeft; + } +} \ No newline at end of file diff --git a/lib/business_logic/separate_models/tui_chat_separate_view_model.dart b/lib/business_logic/separate_models/tui_chat_separate_view_model.dart index 511c3b7..602653b 100644 --- a/lib/business_logic/separate_models/tui_chat_separate_view_model.dart +++ b/lib/business_logic/separate_models/tui_chat_separate_view_model.dart @@ -431,6 +431,17 @@ class TUIChatSeparateViewModel extends ChangeNotifier { msgList, needResetNewMessageCount: false, ); + + // 若为向最新方向增量拉取,通知界面保持在底部 + if (direction == LoadDirection.latest) { + Future.delayed(const Duration(milliseconds: 50), () { + try { + final TUIChatGlobalModel global = serviceLocator(); + // 通过全局状态触发列表位置维护为底部 + global.setMessageListPosition(conversationID, HistoryMessagePosition.bottom); + } catch (_) {} + }); + } } else { // 处理新获取的消息列表后回调 List receivedList = @@ -605,6 +616,9 @@ class TUIChatSeparateViewModel extends ChangeNotifier { nextSeq: seq ?? groupMemberListSeq); final groupMemberListRes = res.data; if (res.code == 0 && groupMemberListRes != null) { + // 拉取成员列表成功,认为群存在且当前用户为成员 + isGroupExist = true; + isNotAMember = false; final groupMemberListTemp = groupMemberListRes.memberInfoList ?? []; groupMemberList = [...?groupMemberList, ...groupMemberListTemp]; groupMemberListSeq = groupMemberListRes.nextSeq ?? "0"; diff --git a/lib/ui/views/TIMUIKitChat/TIMUIKitAppBar/tim_uikit_appbar.dart b/lib/ui/views/TIMUIKitChat/TIMUIKitAppBar/tim_uikit_appbar.dart index b572032..0e286e9 100644 --- a/lib/ui/views/TIMUIKitChat/TIMUIKitAppBar/tim_uikit_appbar.dart +++ b/lib/ui/views/TIMUIKitChat/TIMUIKitAppBar/tim_uikit_appbar.dart @@ -18,6 +18,7 @@ import 'package:tuple/tuple.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart'; import 'package:tencent_cloud_chat_uikit/theme/color.dart'; import 'package:tencent_cloud_chat_uikit/theme/tui_theme.dart'; +import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/group_chat_status_manager.dart'; class TIMUIKitAppBar extends StatefulWidget implements PreferredSizeWidget { /// Appbar config @@ -50,6 +51,8 @@ class TIMUIKitAppBar extends StatefulWidget implements PreferredSizeWidget { this.onClickTitle, }) : super(key: key); + + @override Size get preferredSize => config?.preferredSize ?? const Size.fromHeight(56.0); @@ -68,6 +71,25 @@ class _TIMUIKitAppBarState extends TIMUIKitState { String _conversationShowName = ""; + /// 处理标题点击事件,检查群聊状态 + Future _handleTitleClick(TapDownDetails details) async { + // 检查是否为群聊 + if (widget.conversationID.startsWith('group_')) { + final groupID = widget.conversationID.substring(6); // 移除 'group_' 前缀 + final isGroupLeft = await GroupChatStatusManager.instance.isGroupLeft(groupID); + + if (isGroupLeft) { + // 群聊已退出,不允许跳转 + return; + } + } + + // 如果有自定义点击处理,则调用 + if (widget.onClickTitle != null) { + widget.onClickTitle!(details); + } + } + _addConversationShowNameListener() { _friendshipListener = V2TimFriendshipListener( onFriendInfoChanged: (infoList) { @@ -202,7 +224,7 @@ class _TIMUIKitAppBarState extends TIMUIKitState { ), title: TIMUIKitAppBarTitle( title: setAppbar?.title, - onClick: widget.onClickTitle, + onClick: _handleTitleClick, textStyle: TextStyle( color: theme.appbarTextColor ?? hexToColor("010000"), fontSize: 16), conversationShowName: _conversationShowName, diff --git a/lib/ui/views/TIMUIKitChat/TIMUIKitTextField/tim_uikit_text_field.dart b/lib/ui/views/TIMUIKitChat/TIMUIKitTextField/tim_uikit_text_field.dart index aeee4f4..ecc7715 100644 --- a/lib/ui/views/TIMUIKitChat/TIMUIKitTextField/tim_uikit_text_field.dart +++ b/lib/ui/views/TIMUIKitChat/TIMUIKitTextField/tim_uikit_text_field.dart @@ -31,6 +31,7 @@ import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitTextField import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitTextField/tim_uikit_at_text.dart'; import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitTextField/tim_uikit_text_field_layout/narrow.dart'; import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitTextField/tim_uikit_text_field_layout/wide.dart'; +import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/group_chat_status_manager.dart'; enum MuteStatus { none, me, all } @@ -930,13 +931,22 @@ class _InputTextFieldState extends TIMUIKitState { _getMuteType(model); - return Selector( - builder: ((context, value, child) { - String? getForbiddenText() { - if (!(model.isGroupExist)) { + return Consumer( + builder: ((context, vm, child) { + String? getForbiddenText(GroupChatStatus? groupStatus) { + // 检查群聊状态(退出或被踢出) + if (widget.conversationType == ConvType.group && + widget.groupID != null && groupStatus != null) { + if (groupStatus == GroupChatStatus.userLeft || + groupStatus == GroupChatStatus.userKicked) { + return "无法在已退出的群聊中发送消息"; + } + } + + if (!(vm.isGroupExist)) { return "群组不存在"; - } else if (model.isNotAMember) { - return "您不是群成员"; + } else if (vm.isNotAMember) { + return "无法在已退出的群聊中发送消息"; } else if (muteStatus == MuteStatus.all) { return "全员禁言中"; } else if (muteStatus == MuteStatus.me) { @@ -945,84 +955,105 @@ class _InputTextFieldState extends TIMUIKitState { return null; } - final forbiddenText = getForbiddenText(); - return LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) { - inputWidth = constraints.maxWidth; - return TUIKitScreenUtils.getDeviceWidget( - context: context, - defaultWidget: TIMUIKitTextFieldLayoutNarrow( - stickerPackageList: stickerPackageList, - onEmojiSubmitted: _onEmojiSubmitted, - onCustomEmojiFaceSubmitted: _onCustomEmojiFaceSubmitted, - backSpaceText: _deleteStickerFromText, - addStickerToText: _addStickerToText, - customStickerPanel: widget.customStickerPanel, - onChanged: widget.onChanged, - onDeleteText: _onDeleteText, - backgroundColor: widget.backgroundColor, - morePanelConfig: widget.morePanelConfig, - repliedMessage: value, - currentCursor: currentCursor, - hintText: widget.hintText, - isUseDefaultEmoji: widget.isUseDefaultEmoji, - languageType: languageType, - textEditingController: textEditingController, - conversationID: widget.conversationID, - conversationType: widget.conversationType, - focusNode: focusNode, - controller: widget.controller, - setCurrentCursor: _setCurrentCursor, - onCursorChange: _onCursorChange, - model: model, - handleSendEditStatus: _handleSendEditStatus, - handleAtText: (text) { - _handleAtText(text, model); - }, - handleSoftKeyBoardDelete: _handleSoftKeyBoardDelete, - onSubmitted: onSubmitted, - goDownBottom: goDownBottom, - showSendAudio: widget.showSendAudio, - showSendEmoji: widget.showSendEmoji, - showMorePanel: widget.showMorePanel, - customEmojiStickerList: widget.customEmojiStickerList), - desktopWidget: TIMUIKitTextFieldLayoutWide( - stickerPackageList: stickerPackageList, - chatConfig: widget.chatConfig ?? widget.model.chatConfig, - theme: theme, - currentConversation: widget.currentConversation, - onEmojiSubmitted: _onEmojiSubmitted, - onCustomEmojiFaceSubmitted: _onCustomEmojiFaceSubmitted, - backSpaceText: _deleteStickerFromText, - addStickerToText: _addStickerToText, - customStickerPanel: widget.customStickerPanel, - onChanged: widget.onChanged, - backgroundColor: widget.backgroundColor, - morePanelConfig: widget.morePanelConfig, - repliedMessage: value, - currentCursor: currentCursor, - hintText: widget.hintText, - isUseDefaultEmoji: widget.isUseDefaultEmoji, - languageType: languageType, - textEditingController: textEditingController, - conversationID: widget.conversationID, - conversationType: widget.conversationType, - focusNode: focusNode, - controller: widget.controller, - setCurrentCursor: _setCurrentCursor, - onCursorChange: _onCursorChange, - model: model, - handleSendEditStatus: _handleSendEditStatus, - handleAtText: (text) { - _handleAtText(text, model); - }, - onSubmitted: onSubmitted, - goDownBottom: goDownBottom, - showSendAudio: widget.showSendAudio, - showSendEmoji: widget.showSendEmoji, - showMorePanel: widget.showMorePanel, - customEmojiStickerList: widget.customEmojiStickerList)); - }); + // 如果是群聊,监听群聊状态变化 + if (widget.conversationType == ConvType.group && widget.groupID != null) { + return StreamBuilder>( + stream: GroupChatStatusManager.instance.statusStream, + builder: (context, statusSnapshot) { + // 兜底:初始为空时同步读取一次缓存 + final statusMap = statusSnapshot.data ?? {}; + final groupStatus = statusMap[widget.groupID!] ?? GroupChatStatus.normal; + final forbiddenText = getForbiddenText(groupStatus); + return _buildInputWidget(forbiddenText, vm.repliedMessage, vm, theme); + }, + ); + } else { + // 非群聊情况,直接构建输入控件 + final forbiddenText = getForbiddenText(null); + return _buildInputWidget(forbiddenText, vm.repliedMessage, vm, theme); + } }), - selector: (c, model) => model.repliedMessage); + ); + } + + Widget _buildInputWidget(String? forbiddenText, V2TimMessage? value, TUIChatSeparateViewModel model, dynamic theme) { + return LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) { + inputWidth = constraints.maxWidth; + return TUIKitScreenUtils.getDeviceWidget( + context: context, + defaultWidget: TIMUIKitTextFieldLayoutNarrow( + stickerPackageList: stickerPackageList, + onEmojiSubmitted: _onEmojiSubmitted, + onCustomEmojiFaceSubmitted: _onCustomEmojiFaceSubmitted, + backSpaceText: _deleteStickerFromText, + addStickerToText: _addStickerToText, + customStickerPanel: widget.customStickerPanel, + onChanged: widget.onChanged, + onDeleteText: _onDeleteText, + backgroundColor: widget.backgroundColor, + morePanelConfig: widget.morePanelConfig, + repliedMessage: value, + currentCursor: currentCursor, + hintText: widget.hintText, + isUseDefaultEmoji: widget.isUseDefaultEmoji, + languageType: languageType, + textEditingController: textEditingController, + conversationID: widget.conversationID, + conversationType: widget.conversationType, + focusNode: focusNode, + controller: widget.controller, + setCurrentCursor: _setCurrentCursor, + onCursorChange: _onCursorChange, + model: model, + handleSendEditStatus: _handleSendEditStatus, + handleAtText: (text) { + _handleAtText(text, model); + }, + handleSoftKeyBoardDelete: _handleSoftKeyBoardDelete, + onSubmitted: onSubmitted, + goDownBottom: goDownBottom, + showSendAudio: widget.showSendAudio, + showSendEmoji: widget.showSendEmoji, + showMorePanel: widget.showMorePanel, + customEmojiStickerList: widget.customEmojiStickerList, + forbiddenText: forbiddenText), + desktopWidget: TIMUIKitTextFieldLayoutWide( + stickerPackageList: stickerPackageList, + chatConfig: widget.chatConfig ?? widget.model.chatConfig, + theme: theme, + currentConversation: widget.currentConversation, + onEmojiSubmitted: _onEmojiSubmitted, + onCustomEmojiFaceSubmitted: _onCustomEmojiFaceSubmitted, + backSpaceText: _deleteStickerFromText, + addStickerToText: _addStickerToText, + customStickerPanel: widget.customStickerPanel, + onChanged: widget.onChanged, + backgroundColor: widget.backgroundColor, + morePanelConfig: widget.morePanelConfig, + repliedMessage: value, + currentCursor: currentCursor, + hintText: widget.hintText, + isUseDefaultEmoji: widget.isUseDefaultEmoji, + languageType: languageType, + textEditingController: textEditingController, + conversationID: widget.conversationID, + conversationType: widget.conversationType, + focusNode: focusNode, + controller: widget.controller, + setCurrentCursor: _setCurrentCursor, + onCursorChange: _onCursorChange, + model: model, + handleSendEditStatus: _handleSendEditStatus, + handleAtText: (text) { + _handleAtText(text, model); + }, + onSubmitted: onSubmitted, + goDownBottom: goDownBottom, + showSendAudio: widget.showSendAudio, + showSendEmoji: widget.showSendEmoji, + showMorePanel: widget.showMorePanel, + customEmojiStickerList: widget.customEmojiStickerList, + forbiddenText: forbiddenText)); + }); } } diff --git a/lib/ui/views/TIMUIKitChat/TIMUIKitTextField/tim_uikit_text_field_layout/narrow.dart b/lib/ui/views/TIMUIKitChat/TIMUIKitTextField/tim_uikit_text_field_layout/narrow.dart index a70baed..6691797 100644 --- a/lib/ui/views/TIMUIKitChat/TIMUIKitTextField/tim_uikit_text_field_layout/narrow.dart +++ b/lib/ui/views/TIMUIKitChat/TIMUIKitTextField/tim_uikit_text_field_layout/narrow.dart @@ -24,6 +24,7 @@ import 'package:tencent_cloud_chat_uikit/ui/utils/platform.dart'; import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitTextField/special_text/DefaultSpecialTextSpanBuilder.dart'; import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitTextField/special_text/emoji_text.dart'; import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitTextField/tim_uikit_send_sound_message.dart'; +import 'package:tencent_cloud_chat_uikit/ui/widgets/disabled_input_widget.dart'; import 'package:tencent_keyboard_visibility/tencent_keyboard_visibility.dart'; GlobalKey<_TIMUIKitTextFieldLayoutNarrowState> narrowTextFieldKey = GlobalKey(); @@ -103,6 +104,8 @@ class TIMUIKitTextFieldLayoutNarrow extends StatefulWidget { final List stickerPackageList; + final String? forbiddenText; + const TIMUIKitTextFieldLayoutNarrow( {Key? key, this.customStickerPanel, @@ -137,7 +140,8 @@ class TIMUIKitTextFieldLayoutNarrow extends StatefulWidget { this.hintText, required this.customEmojiStickerList, this.controller, - required this.stickerPackageList}) + required this.stickerPackageList, + this.forbiddenText}) : super(key: key); @override @@ -199,7 +203,7 @@ class _TIMUIKitTextFieldLayoutNarrowState extends TIMUIKitState= 46 && showKeyboard == false) { @@ -399,6 +403,14 @@ class _TIMUIKitTextFieldLayoutNarrowState extends TIMUIKitState stickerPackageList; + final String? forbiddenText; const TIMUIKitTextFieldLayoutWide( {Key? key, @@ -196,7 +198,8 @@ class TIMUIKitTextFieldLayoutWide extends StatefulWidget { required this.currentConversation, required this.theme, required this.chatConfig, - required this.stickerPackageList}) + required this.stickerPackageList, + this.forbiddenText}) : super(key: key); @override @@ -908,6 +911,13 @@ class _TIMUIKitTextFieldLayoutWideState extends TIMUIKitState { ); Widget? _joinInGroupCallWidget; + bool _isPullingLatest = false; @override void initState() { @@ -434,6 +435,16 @@ class _TUIChatState extends TIMUIKitState { model.loadGroupMemberList(groupID: _getConvID()); } model.loadGroupInfo(_getConvID()); + // 增量拉取最新一批消息,避免清空历史 + _pullLatestMessagesDebounced(); + // 服务器可能存在延迟,再次尝试拉取 + Future.delayed(const Duration(milliseconds: 1200), _pullLatestMessagesDebounced); + break; + case UpdateType.kickedFromGroup: + // 本地插入的“被踢出群组”提示消息需要主动刷新一次列表 + Future.delayed(const Duration(milliseconds: 120), _pullLatestMessagesOnce); + // 确保列表滚动至底部显示最新提示 + Future.delayed(const Duration(milliseconds: 240), _scrollToBottom); break; default: break; @@ -555,7 +566,7 @@ class _TUIChatState extends TIMUIKitState { ? widget.textFieldBuilder!(context) : TIMUIKitInputTextField( chatConfig: widget.config, - groupID: widget.groupID, + groupID: _getConvType() == ConvType.group ? _getConvID() : null, atMemberPanelScroll: atMemberPanelScroll, groupType: widget.conversation.groupType, currentConversation: widget.conversation, @@ -598,6 +609,46 @@ class _TUIChatState extends TIMUIKitState { ); }); } + + void _pullLatestMessagesDebounced() { + if (_isPullingLatest) return; + _isPullingLatest = true; + _pullLatestMessagesOnce(); + Future.delayed(const Duration(milliseconds: 600), () { + _isPullingLatest = false; + }); + } + + void _pullLatestMessagesOnce() { + try { + final List currentList = model.getOriginMessageList(); + if (currentList.isEmpty) return; + final String? lastMsgID = currentList.first.msgID; + if (lastMsgID == null || lastMsgID.isEmpty) return; + model.loadChatRecord( + count: 20, + lastMsgID: lastMsgID, + direction: LoadDirection.latest, + ).then((_) { + // 拉取完成后滚动到底部 + _scrollToBottom(); + }); + } catch (e) { + // ignore + } + } + + void _scrollToBottom() { + try { + autoController.animateTo( + autoController.position.minScrollExtent, + duration: const Duration(milliseconds: 200), + curve: Curves.ease, + ); + } catch (e) { + // ignore + } + } } class TIMUIKitChatProviderScope extends StatelessWidget { diff --git a/lib/ui/views/TIMUIKitGroupProfile/widgets/tim_uikit_group_button_area.dart b/lib/ui/views/TIMUIKitGroupProfile/widgets/tim_uikit_group_button_area.dart index 7b14376..38142d8 100644 --- a/lib/ui/views/TIMUIKitGroupProfile/widgets/tim_uikit_group_button_area.dart +++ b/lib/ui/views/TIMUIKitGroupProfile/widgets/tim_uikit_group_button_area.dart @@ -6,6 +6,7 @@ import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_member_full_info.dart import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_statelesswidget.dart'; import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_group_profile_model.dart'; +import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/group_chat_status_manager.dart'; import 'package:tencent_cloud_chat_uikit/data_services/core/tim_uikit_wide_modal_operation_key.dart'; import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart'; import 'package:tencent_cloud_chat_uikit/ui/controller/tim_uikit_chat_controller.dart'; @@ -106,66 +107,121 @@ class GroupProfileButtonArea extends TIMUIKitStatelessWidget { final isDesktopScreen = TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop; if (isDesktopScreen) { - TUIKitWidePopup.showSecondaryConfirmDialog( - operationKey: TUIKitWideModalOperationKey.confirmExitGroup, - context: context, - text: TIM_t("退出后不会接收到此群聊消息"), - theme: theme, - onCancel: () {}, - onConfirm: () async { - final res = await sdkInstance.quitGroup(groupID: groupID); - if (res.code == 0) { - final deleteConvRes = - await sdkInstance.getConversationManager().deleteConversation(conversationID: "group_$groupID"); - if (deleteConvRes.code == 0) { - model.lifeCycle?.didLeaveGroup(); - } - } - }); + _showQuitGroupDialog(context, theme); } else { - showCupertinoModalPopup( - context: context, - builder: (BuildContext context) { - return CupertinoActionSheet( - title: Text(TIM_t("退出后不会接收到此群聊消息")), - cancelButton: CupertinoActionSheetAction( - onPressed: () { - Navigator.pop( - context, - ); - }, - child: Text(TIM_t("取消")), - isDefaultAction: false, - ), - actions: [ - CupertinoActionSheetAction( - onPressed: () async { - Navigator.pop( - context, - ); - final res = await sdkInstance.quitGroup(groupID: groupID); - if (res.code == 0) { - final deleteConvRes = - await sdkInstance.getConversationManager().deleteConversation(conversationID: "group_$groupID"); - if (deleteConvRes.code == 0) { - model.lifeCycle?.didLeaveGroup(); - } - onTIMCallback(TIMCallback( - type: TIMCallbackType.INFO, - infoRecommendText: "${TIM_t("您已退出")}${model.groupInfo?.groupName}", - infoCode: 6661402)); - } - }, - child: Text( - TIM_t("确定"), - style: TextStyle(color: theme.cautionColor), + _showQuitGroupDialog(context, theme); + } + } + + _showQuitGroupDialog(BuildContext context, TUITheme theme) { + bool clearChatHistory = true; // 默认勾选 + + showDialog( + context: context, + builder: (BuildContext context) { + return StatefulBuilder( + builder: (context, setState) { + return AlertDialog( + title: Text(TIM_t("退出群聊")), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(TIM_t("退出后不会接收到此群聊消息")), + const SizedBox(height: 16), + Row( + children: [ + Checkbox( + value: clearChatHistory, + onChanged: (bool? value) { + setState(() { + clearChatHistory = value ?? true; + }); + }, + ), + Expanded( + child: Text(TIM_t("退出群聊同时清空聊天记录")), + ), + ], + ), + ], + ), + actions: [ + TextButton( + onPressed: () { + Navigator.pop(context); + }, + child: Text(TIM_t("取消")), ), - isDefaultAction: false, - ) - ], - ); - }, - ); + TextButton( + onPressed: () async { + Navigator.pop(context); + await _performQuitGroup(clearChatHistory); + }, + child: Text( + TIM_t("确定"), + style: TextStyle(color: theme.cautionColor), + ), + ), + ], + ); + }, + ); + }, + ); + } + + _performQuitGroup(bool clearChatHistory) async { + final res = await sdkInstance.quitGroup(groupID: groupID); + if (res.code == 0) { + if (clearChatHistory) { + // 原有逻辑:删除会话和聊天记录 + final deleteConvRes = await sdkInstance + .getConversationManager() + .deleteConversation(conversationID: "group_$groupID"); + if (deleteConvRes.code == 0) { + // 清除群聊状态 + await GroupChatStatusManager.instance.clearGroupChatStatus(groupID); + model.lifeCycle?.didLeaveGroup(); + } + } else { + // 新逻辑:保留会话但标记为已退出状态 + await GroupChatStatusManager.instance + .setGroupChatStatus(groupID, GroupChatStatus.userLeft); + // 插入本地退出提示消息 + try { + // 创建自定义消息 + final textMsgRes = await sdkInstance + .getMessageManager() + .createCustomMessage( + data: '{"type":"center_tip","text":"你已退出该群组"}', + ); + if (textMsgRes.code == 0 && textMsgRes.data != null) { + final message = textMsgRes.data!.messageInfo!; + // 获取当前用户ID + final loginUserRes = await sdkInstance.getLoginUser(); + final currentUserID = loginUserRes.data ?? ""; + + // 插入到本地存储 + await sdkInstance + .getMessageManager() + .insertGroupMessageToLocalStorageV2( + message: message, + groupID: groupID, + senderID: currentUserID, + ); + } + } catch (e) { + // 插入消息失败,不影响退出流程 + debugPrint("插入退出提示消息失败: $e"); + } + + model.lifeCycle?.didLeaveGroup(); + } + onTIMCallback(TIMCallback( + type: TIMCallbackType.INFO, + infoRecommendText: "${TIM_t("您已退出")}${model.groupInfo?.groupName}", + infoCode: 6661402)); } } diff --git a/lib/ui/widgets/disabled_input_widget.dart b/lib/ui/widgets/disabled_input_widget.dart new file mode 100644 index 0000000..330dcbd --- /dev/null +++ b/lib/ui/widgets/disabled_input_widget.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; +import 'package:tencent_cloud_chat_uikit/theme/color.dart'; + +class DisabledInputWidget extends StatelessWidget { + final String text; + final IconData? icon; + + const DisabledInputWidget({ + Key? key, + required this.text, + this.icon, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + width: double.infinity, + padding: EdgeInsets.only( + left: 16, + right: 16, + top: 16, + bottom: 16 + MediaQuery.of(context).padding.bottom, + ), + decoration: BoxDecoration( + color: hexToColor("F5F5F5"), + border: Border( + top: BorderSide( + color: hexToColor("E5E5E5"), + width: 1, + ), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (icon != null) ...[ + Icon( + icon, + color: hexToColor("999999"), + size: 16, + ), + const SizedBox(width: 8), + ], + Text( + text, + style: TextStyle( + color: hexToColor("999999"), + fontSize: 14, + ), + ), + ], + ), + ); + } +} \ No newline at end of file