feat(group): 新增退出/踢出群组时保留本地会话消息的功能
This commit is contained in:
parent
2c03ed7f1c
commit
c3aed3d14a
|
|
@ -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<V2TimGroupMemberInfo> memberList) {
|
||||
}, onMemberEnter: (String groupID, List<V2TimGroupMemberInfo> 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<void> _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<V2TimMessage?> _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<void> _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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Map<String, GroupChatStatus>> _statusController =
|
||||
StreamController<Map<String, GroupChatStatus>>.broadcast();
|
||||
|
||||
// 内存中的状态缓存
|
||||
final Map<String, GroupChatStatus> _statusCache = {};
|
||||
|
||||
GroupChatStatusManager._internal();
|
||||
|
||||
static GroupChatStatusManager get instance {
|
||||
_instance ??= GroupChatStatusManager._internal();
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
/// 获取状态变化流
|
||||
Stream<Map<String, GroupChatStatus>> get statusStream => _statusController.stream;
|
||||
|
||||
/// 初始化状态缓存(从 SharedPreferences 加载)
|
||||
Future<void> _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<Map<String, GroupChatStatus>> getCurrentStatusMap() async {
|
||||
if (_statusCache.isEmpty) {
|
||||
await _initializeCache();
|
||||
}
|
||||
return Map.from(_statusCache);
|
||||
}
|
||||
|
||||
/// 设置群聊状态
|
||||
Future<void> 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<GroupChatStatus> 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<void> 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<bool> isGroupLeft(String groupID) async {
|
||||
final status = await getGroupChatStatus(groupID);
|
||||
return status == GroupChatStatus.userLeft || status == GroupChatStatus.userKicked;
|
||||
}
|
||||
|
||||
/// 检查群聊是否被踢出
|
||||
Future<bool> isGroupKicked(String groupID) async {
|
||||
final status = await getGroupChatStatus(groupID);
|
||||
return status == GroupChatStatus.userKicked;
|
||||
}
|
||||
|
||||
/// 检查群聊是否主动退出
|
||||
Future<bool> isGroupUserLeft(String groupID) async {
|
||||
final status = await getGroupChatStatus(groupID);
|
||||
return status == GroupChatStatus.userLeft;
|
||||
}
|
||||
}
|
||||
|
|
@ -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<TUIChatGlobalModel>();
|
||||
// 通过全局状态触发列表位置维护为底部
|
||||
global.setMessageListPosition(conversationID, HistoryMessagePosition.bottom);
|
||||
} catch (_) {}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// 处理新获取的消息列表后回调
|
||||
List<V2TimMessage> 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";
|
||||
|
|
|
|||
|
|
@ -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<TIMUIKitAppBar> {
|
|||
|
||||
String _conversationShowName = "";
|
||||
|
||||
/// 处理标题点击事件,检查群聊状态
|
||||
Future<void> _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<TIMUIKitAppBar> {
|
|||
),
|
||||
title: TIMUIKitAppBarTitle(
|
||||
title: setAppbar?.title,
|
||||
onClick: widget.onClickTitle,
|
||||
onClick: _handleTitleClick,
|
||||
textStyle: TextStyle(
|
||||
color: theme.appbarTextColor ?? hexToColor("010000"), fontSize: 16),
|
||||
conversationShowName: _conversationShowName,
|
||||
|
|
|
|||
|
|
@ -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<TIMUIKitInputTextField> {
|
|||
|
||||
_getMuteType(model);
|
||||
|
||||
return Selector<TUIChatSeparateViewModel, V2TimMessage?>(
|
||||
builder: ((context, value, child) {
|
||||
String? getForbiddenText() {
|
||||
if (!(model.isGroupExist)) {
|
||||
return Consumer<TUIChatSeparateViewModel>(
|
||||
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,7 +955,28 @@ class _InputTextFieldState extends TIMUIKitState<TIMUIKitInputTextField> {
|
|||
return null;
|
||||
}
|
||||
|
||||
final forbiddenText = getForbiddenText();
|
||||
// 如果是群聊,监听群聊状态变化
|
||||
if (widget.conversationType == ConvType.group && widget.groupID != null) {
|
||||
return StreamBuilder<Map<String, GroupChatStatus>>(
|
||||
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);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInputWidget(String? forbiddenText, V2TimMessage? value, TUIChatSeparateViewModel model, dynamic theme) {
|
||||
return LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) {
|
||||
inputWidth = constraints.maxWidth;
|
||||
return TUIKitScreenUtils.getDeviceWidget(
|
||||
|
|
@ -984,7 +1015,8 @@ class _InputTextFieldState extends TIMUIKitState<TIMUIKitInputTextField> {
|
|||
showSendAudio: widget.showSendAudio,
|
||||
showSendEmoji: widget.showSendEmoji,
|
||||
showMorePanel: widget.showMorePanel,
|
||||
customEmojiStickerList: widget.customEmojiStickerList),
|
||||
customEmojiStickerList: widget.customEmojiStickerList,
|
||||
forbiddenText: forbiddenText),
|
||||
desktopWidget: TIMUIKitTextFieldLayoutWide(
|
||||
stickerPackageList: stickerPackageList,
|
||||
chatConfig: widget.chatConfig ?? widget.model.chatConfig,
|
||||
|
|
@ -1020,9 +1052,8 @@ class _InputTextFieldState extends TIMUIKitState<TIMUIKitInputTextField> {
|
|||
showSendAudio: widget.showSendAudio,
|
||||
showSendEmoji: widget.showSendEmoji,
|
||||
showMorePanel: widget.showMorePanel,
|
||||
customEmojiStickerList: widget.customEmojiStickerList));
|
||||
customEmojiStickerList: widget.customEmojiStickerList,
|
||||
forbiddenText: forbiddenText));
|
||||
});
|
||||
}),
|
||||
selector: (c, model) => model.repliedMessage);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<CustomStickerPackage> 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<TIMUIKitTextFiel
|
|||
}
|
||||
|
||||
Widget _getBottomContainer(TUITheme theme) {
|
||||
if (showEmojiPanel) {
|
||||
if (showEmojiPanel && widget.forbiddenText == null) {
|
||||
return widget.customStickerPanel != null
|
||||
? widget.customStickerPanel!(
|
||||
sendTextMessage: () {
|
||||
|
|
@ -261,7 +265,7 @@ class _TIMUIKitTextFieldLayoutNarrowState extends TIMUIKitState<TIMUIKitTextFiel
|
|||
lightPrimaryColor: theme.lightPrimaryColor);
|
||||
}
|
||||
|
||||
if (showMore) {
|
||||
if (showMore && widget.forbiddenText == null) {
|
||||
return MorePanel(
|
||||
morePanelConfig: widget.morePanelConfig,
|
||||
conversationID: widget.conversationID,
|
||||
|
|
@ -292,9 +296,9 @@ class _TIMUIKitTextFieldLayoutNarrowState extends TIMUIKitState<TIMUIKitTextFiel
|
|||
}
|
||||
final height = originHeight != 0 ? originHeight : currentKeyboardHeight;
|
||||
return height;
|
||||
} else if (showEmojiPanel) {
|
||||
} else if (showEmojiPanel && widget.forbiddenText == null) {
|
||||
return 248.0 + (bottomPadding ?? 0.0);
|
||||
} else if (showMore) {
|
||||
} else if (showMore && widget.forbiddenText == null) {
|
||||
final double inner = morePanelDynamicHeight ?? 120;
|
||||
return inner + (bottomPadding ?? 0.0);
|
||||
} else if (widget.textEditingController.text.length >= 46 && showKeyboard == false) {
|
||||
|
|
@ -399,6 +403,14 @@ class _TIMUIKitTextFieldLayoutNarrowState extends TIMUIKitState<TIMUIKitTextFiel
|
|||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final theme = value.theme;
|
||||
|
||||
// 如果有禁用文本,显示禁用输入组件
|
||||
if (widget.forbiddenText != null) {
|
||||
return DisabledInputWidget(
|
||||
text: widget.forbiddenText!,
|
||||
icon: Icons.info_outline,
|
||||
);
|
||||
}
|
||||
|
||||
setKeyboardHeight ??= OptimizeUtils.debounce((height) {
|
||||
settingModel.keyboardHeight = height;
|
||||
}, const Duration(seconds: 1));
|
||||
|
|
@ -446,7 +458,7 @@ class _TIMUIKitTextFieldLayoutNarrowState extends TIMUIKitState<TIMUIKitTextFiel
|
|||
constraints: const BoxConstraints(minHeight: 50),
|
||||
child: Row(
|
||||
children: [
|
||||
if (PlatformUtils().isMobile && widget.showSendAudio)
|
||||
if (PlatformUtils().isMobile && widget.showSendAudio && widget.forbiddenText == null)
|
||||
InkWell(
|
||||
onTap: () async {
|
||||
showKeyboard = showSendSoundText;
|
||||
|
|
@ -479,7 +491,7 @@ class _TIMUIKitTextFieldLayoutNarrowState extends TIMUIKitState<TIMUIKitTextFiel
|
|||
width: 10,
|
||||
),
|
||||
Expanded(
|
||||
child: showSendSoundText
|
||||
child: showSendSoundText && widget.forbiddenText == null
|
||||
? SendSoundMessage(
|
||||
onDownBottom: widget.goDownBottom,
|
||||
conversationID: widget.conversationID,
|
||||
|
|
@ -490,6 +502,7 @@ class _TIMUIKitTextFieldLayoutNarrowState extends TIMUIKitState<TIMUIKitTextFiel
|
|||
child: ExtendedTextField(
|
||||
maxLines: 4,
|
||||
minLines: 1,
|
||||
enabled: widget.forbiddenText == null,
|
||||
focusNode: widget.focusNode,
|
||||
onChanged: debounceFunc,
|
||||
onTap: () {
|
||||
|
|
@ -538,7 +551,7 @@ class _TIMUIKitTextFieldLayoutNarrowState extends TIMUIKitState<TIMUIKitTextFiel
|
|||
filled: true,
|
||||
isDense: true,
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 12, horizontal: 12),
|
||||
hintText: widget.hintText ?? ''),
|
||||
hintText: widget.forbiddenText ?? widget.hintText ?? ''),
|
||||
controller: widget.textEditingController,
|
||||
specialTextSpanBuilder: PlatformUtils().isWeb
|
||||
? null
|
||||
|
|
@ -584,7 +597,7 @@ class _TIMUIKitTextFieldLayoutNarrowState extends TIMUIKitState<TIMUIKitTextFiel
|
|||
const SizedBox(
|
||||
width: 10,
|
||||
),
|
||||
if (widget.showSendEmoji)
|
||||
if (widget.showSendEmoji && widget.forbiddenText == null)
|
||||
InkWell(
|
||||
onTap: () {
|
||||
_openEmojiPanel();
|
||||
|
|
@ -606,7 +619,7 @@ class _TIMUIKitTextFieldLayoutNarrowState extends TIMUIKitState<TIMUIKitTextFiel
|
|||
const SizedBox(
|
||||
width: 10,
|
||||
),
|
||||
if (widget.showMorePanel && showMoreButton)
|
||||
if (widget.showMorePanel && showMoreButton && widget.forbiddenText == null)
|
||||
InkWell(
|
||||
onTap: () {
|
||||
// model.sendCustomMessage(data: "a", convID: model.currentSelectedConv, convType: model.currentSelectedConvType == 1 ? ConvType.c2c : ConvType.group);
|
||||
|
|
@ -625,7 +638,7 @@ class _TIMUIKitTextFieldLayoutNarrowState extends TIMUIKitState<TIMUIKitTextFiel
|
|||
width: 28,
|
||||
),
|
||||
),
|
||||
if ((isAndroidDevice() || isWebDevice()) && !showMoreButton)
|
||||
if ((isAndroidDevice() || isWebDevice()) && !showMoreButton && widget.forbiddenText == null)
|
||||
SizedBox(
|
||||
height: 32.0,
|
||||
child: ElevatedButton(
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ import 'package:tencent_cloud_chat_uikit/ui/utils/screen_shot.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/widgets/drag_widget.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/disabled_input_widget.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/wide_popup.dart';
|
||||
import 'package:universal_html/html.dart' as html;
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
|
@ -160,6 +161,7 @@ class TIMUIKitTextFieldLayoutWide extends StatefulWidget {
|
|||
final V2TimConversation currentConversation;
|
||||
|
||||
final List<CustomStickerPackage> 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<TIMUIKitTextFieldL
|
|||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final theme = value.theme;
|
||||
|
||||
if (widget.forbiddenText != null) {
|
||||
return DisabledInputWidget(
|
||||
text: widget.forbiddenText!,
|
||||
icon: Icons.info_outline,
|
||||
);
|
||||
}
|
||||
|
||||
setKeyboardHeight ??= OptimizeUtils.debounce((height) {
|
||||
settingModel.keyboardHeight = height;
|
||||
}, const Duration(seconds: 1));
|
||||
|
|
|
|||
|
|
@ -253,6 +253,7 @@ class _TUIChatState extends TIMUIKitState<TIMUIKitChat> {
|
|||
);
|
||||
|
||||
Widget? _joinInGroupCallWidget;
|
||||
bool _isPullingLatest = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
|
@ -434,6 +435,16 @@ class _TUIChatState extends TIMUIKitState<TIMUIKitChat> {
|
|||
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<TIMUIKitChat> {
|
|||
? 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<TIMUIKitChat> {
|
|||
);
|
||||
});
|
||||
}
|
||||
|
||||
void _pullLatestMessagesDebounced() {
|
||||
if (_isPullingLatest) return;
|
||||
_isPullingLatest = true;
|
||||
_pullLatestMessagesOnce();
|
||||
Future.delayed(const Duration(milliseconds: 600), () {
|
||||
_isPullingLatest = false;
|
||||
});
|
||||
}
|
||||
|
||||
void _pullLatestMessagesOnce() {
|
||||
try {
|
||||
final List<V2TimMessage> 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 {
|
||||
|
|
|
|||
|
|
@ -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,48 +107,115 @@ 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<String>(
|
||||
_showQuitGroupDialog(context, theme);
|
||||
}
|
||||
}
|
||||
|
||||
_showQuitGroupDialog(BuildContext context, TUITheme theme) {
|
||||
bool clearChatHistory = true; // 默认勾选
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return CupertinoActionSheet(
|
||||
title: Text(TIM_t("退出后不会接收到此群聊消息")),
|
||||
cancelButton: CupertinoActionSheetAction(
|
||||
onPressed: () {
|
||||
Navigator.pop(
|
||||
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;
|
||||
});
|
||||
},
|
||||
child: Text(TIM_t("取消")),
|
||||
isDefaultAction: false,
|
||||
),
|
||||
Expanded(
|
||||
child: Text(TIM_t("退出群聊同时清空聊天记录")),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
CupertinoActionSheetAction(
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: Text(TIM_t("取消")),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
Navigator.pop(
|
||||
context,
|
||||
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) {
|
||||
final deleteConvRes =
|
||||
await sdkInstance.getConversationManager().deleteConversation(conversationID: "group_$groupID");
|
||||
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(
|
||||
|
|
@ -155,18 +223,6 @@ class GroupProfileButtonArea extends TIMUIKitStatelessWidget {
|
|||
infoRecommendText: "${TIM_t("您已退出")}${model.groupInfo?.groupName}",
|
||||
infoCode: 6661402));
|
||||
}
|
||||
},
|
||||
child: Text(
|
||||
TIM_t("确定"),
|
||||
style: TextStyle(color: theme.cautionColor),
|
||||
),
|
||||
isDefaultAction: false,
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_dismissGroup(BuildContext context, theme) async {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue