feat(group): 新增退出/踢出群组时保留本地会话消息的功能

This commit is contained in:
Zeew 2025-08-19 07:22:08 +08:00
parent 2c03ed7f1c
commit c3aed3d14a
10 changed files with 652 additions and 159 deletions

View File

@ -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'; 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' 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'; 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_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/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/group/group_services.dart';
import 'package:tencent_cloud_chat_uikit/data_services/services_locatar.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/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 } enum UpdateType { groupInfo, memberList, joinApplicationList, groupDismissed, kickedFromGroup }
@ -54,12 +57,19 @@ class TUIGroupListenerModel extends ChangeNotifier {
} }
TUIGroupListenerModel() { TUIGroupListenerModel() {
_groupListener = V2TimGroupListener(onMemberInvited: (groupID, opUser, memberList) { _groupListener = V2TimGroupListener(onMemberInvited: (groupID, opUser, memberList) async {
// 退/
await GroupChatStatusManager.instance.clearGroupChatStatus(groupID);
_needUpdate = NeedUpdate(groupID, UpdateType.memberList, ""); _needUpdate = NeedUpdate(groupID, UpdateType.memberList, "");
notifyListeners(); notifyListeners();
}, onMemberKicked: (groupID, opUser, memberList) async { }, onMemberKicked: (groupID, opUser, memberList) async {
if (_isLoginUserKickedFromGroup(groupID, memberList)) { if (_isLoginUserKickedFromGroup(groupID, memberList)) {
_deleteGroupConversation(groupID); //
await GroupChatStatusManager.instance
.setGroupChatStatus(groupID, GroupChatStatus.userKicked);
//
await _insertKickedOutMessage(groupID, opUser);
final groupName = await _getGroupName(groupID); final groupName = await _getGroupName(groupID);
_needUpdate = NeedUpdate(groupID, UpdateType.kickedFromGroup, groupName); _needUpdate = NeedUpdate(groupID, UpdateType.kickedFromGroup, groupName);
@ -69,7 +79,16 @@ class TUIGroupListenerModel extends ChangeNotifier {
_needUpdate = NeedUpdate(groupID, UpdateType.memberList, ""); _needUpdate = NeedUpdate(groupID, UpdateType.memberList, "");
notifyListeners(); 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, ""); _needUpdate = NeedUpdate(groupID, UpdateType.memberList, "");
notifyListeners(); notifyListeners();
}, onMemberLeave: (String groupID, V2TimGroupMemberInfo member) { }, onMemberLeave: (String groupID, V2TimGroupMemberInfo member) {
@ -170,4 +189,91 @@ class TUIGroupListenerModel extends ChangeNotifier {
} }
return false; 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}");
}
}
} }

View File

@ -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;
}
}

View File

@ -431,6 +431,17 @@ class TUIChatSeparateViewModel extends ChangeNotifier {
msgList, msgList,
needResetNewMessageCount: false, 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 { } else {
// //
List<V2TimMessage> receivedList = List<V2TimMessage> receivedList =
@ -605,6 +616,9 @@ class TUIChatSeparateViewModel extends ChangeNotifier {
nextSeq: seq ?? groupMemberListSeq); nextSeq: seq ?? groupMemberListSeq);
final groupMemberListRes = res.data; final groupMemberListRes = res.data;
if (res.code == 0 && groupMemberListRes != null) { if (res.code == 0 && groupMemberListRes != null) {
//
isGroupExist = true;
isNotAMember = false;
final groupMemberListTemp = groupMemberListRes.memberInfoList ?? []; final groupMemberListTemp = groupMemberListRes.memberInfoList ?? [];
groupMemberList = [...?groupMemberList, ...groupMemberListTemp]; groupMemberList = [...?groupMemberList, ...groupMemberListTemp];
groupMemberListSeq = groupMemberListRes.nextSeq ?? "0"; groupMemberListSeq = groupMemberListRes.nextSeq ?? "0";

View File

@ -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/base_widgets/tim_ui_kit_base.dart';
import 'package:tencent_cloud_chat_uikit/theme/color.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/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 { class TIMUIKitAppBar extends StatefulWidget implements PreferredSizeWidget {
/// Appbar config /// Appbar config
@ -50,6 +51,8 @@ class TIMUIKitAppBar extends StatefulWidget implements PreferredSizeWidget {
this.onClickTitle, this.onClickTitle,
}) : super(key: key); }) : super(key: key);
@override @override
Size get preferredSize => Size get preferredSize =>
config?.preferredSize ?? const Size.fromHeight(56.0); config?.preferredSize ?? const Size.fromHeight(56.0);
@ -68,6 +71,25 @@ class _TIMUIKitAppBarState extends TIMUIKitState<TIMUIKitAppBar> {
String _conversationShowName = ""; 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() { _addConversationShowNameListener() {
_friendshipListener = V2TimFriendshipListener( _friendshipListener = V2TimFriendshipListener(
onFriendInfoChanged: (infoList) { onFriendInfoChanged: (infoList) {
@ -202,7 +224,7 @@ class _TIMUIKitAppBarState extends TIMUIKitState<TIMUIKitAppBar> {
), ),
title: TIMUIKitAppBarTitle( title: TIMUIKitAppBarTitle(
title: setAppbar?.title, title: setAppbar?.title,
onClick: widget.onClickTitle, onClick: _handleTitleClick,
textStyle: TextStyle( textStyle: TextStyle(
color: theme.appbarTextColor ?? hexToColor("010000"), fontSize: 16), color: theme.appbarTextColor ?? hexToColor("010000"), fontSize: 16),
conversationShowName: _conversationShowName, conversationShowName: _conversationShowName,

View File

@ -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_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/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/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 } enum MuteStatus { none, me, all }
@ -930,13 +931,22 @@ class _InputTextFieldState extends TIMUIKitState<TIMUIKitInputTextField> {
_getMuteType(model); _getMuteType(model);
return Selector<TUIChatSeparateViewModel, V2TimMessage?>( return Consumer<TUIChatSeparateViewModel>(
builder: ((context, value, child) { builder: ((context, vm, child) {
String? getForbiddenText() { String? getForbiddenText(GroupChatStatus? groupStatus) {
if (!(model.isGroupExist)) { // 退
if (widget.conversationType == ConvType.group &&
widget.groupID != null && groupStatus != null) {
if (groupStatus == GroupChatStatus.userLeft ||
groupStatus == GroupChatStatus.userKicked) {
return "无法在已退出的群聊中发送消息";
}
}
if (!(vm.isGroupExist)) {
return "群组不存在"; return "群组不存在";
} else if (model.isNotAMember) { } else if (vm.isNotAMember) {
return "您不是群成员"; return "无法在已退出的群聊中发送消息";
} else if (muteStatus == MuteStatus.all) { } else if (muteStatus == MuteStatus.all) {
return "全员禁言中"; return "全员禁言中";
} else if (muteStatus == MuteStatus.me) { } else if (muteStatus == MuteStatus.me) {
@ -945,84 +955,105 @@ class _InputTextFieldState extends TIMUIKitState<TIMUIKitInputTextField> {
return null; return null;
} }
final forbiddenText = getForbiddenText(); //
return LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) { if (widget.conversationType == ConvType.group && widget.groupID != null) {
inputWidth = constraints.maxWidth; return StreamBuilder<Map<String, GroupChatStatus>>(
return TUIKitScreenUtils.getDeviceWidget( stream: GroupChatStatusManager.instance.statusStream,
context: context, builder: (context, statusSnapshot) {
defaultWidget: TIMUIKitTextFieldLayoutNarrow( //
stickerPackageList: stickerPackageList, final statusMap = statusSnapshot.data ?? {};
onEmojiSubmitted: _onEmojiSubmitted, final groupStatus = statusMap[widget.groupID!] ?? GroupChatStatus.normal;
onCustomEmojiFaceSubmitted: _onCustomEmojiFaceSubmitted, final forbiddenText = getForbiddenText(groupStatus);
backSpaceText: _deleteStickerFromText, return _buildInputWidget(forbiddenText, vm.repliedMessage, vm, theme);
addStickerToText: _addStickerToText, },
customStickerPanel: widget.customStickerPanel, );
onChanged: widget.onChanged, } else {
onDeleteText: _onDeleteText, //
backgroundColor: widget.backgroundColor, final forbiddenText = getForbiddenText(null);
morePanelConfig: widget.morePanelConfig, return _buildInputWidget(forbiddenText, vm.repliedMessage, vm, theme);
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));
});
}), }),
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));
});
} }
} }

View File

@ -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/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/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/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'; import 'package:tencent_keyboard_visibility/tencent_keyboard_visibility.dart';
GlobalKey<_TIMUIKitTextFieldLayoutNarrowState> narrowTextFieldKey = GlobalKey(); GlobalKey<_TIMUIKitTextFieldLayoutNarrowState> narrowTextFieldKey = GlobalKey();
@ -103,6 +104,8 @@ class TIMUIKitTextFieldLayoutNarrow extends StatefulWidget {
final List<CustomStickerPackage> stickerPackageList; final List<CustomStickerPackage> stickerPackageList;
final String? forbiddenText;
const TIMUIKitTextFieldLayoutNarrow( const TIMUIKitTextFieldLayoutNarrow(
{Key? key, {Key? key,
this.customStickerPanel, this.customStickerPanel,
@ -137,7 +140,8 @@ class TIMUIKitTextFieldLayoutNarrow extends StatefulWidget {
this.hintText, this.hintText,
required this.customEmojiStickerList, required this.customEmojiStickerList,
this.controller, this.controller,
required this.stickerPackageList}) required this.stickerPackageList,
this.forbiddenText})
: super(key: key); : super(key: key);
@override @override
@ -199,7 +203,7 @@ class _TIMUIKitTextFieldLayoutNarrowState extends TIMUIKitState<TIMUIKitTextFiel
} }
Widget _getBottomContainer(TUITheme theme) { Widget _getBottomContainer(TUITheme theme) {
if (showEmojiPanel) { if (showEmojiPanel && widget.forbiddenText == null) {
return widget.customStickerPanel != null return widget.customStickerPanel != null
? widget.customStickerPanel!( ? widget.customStickerPanel!(
sendTextMessage: () { sendTextMessage: () {
@ -261,7 +265,7 @@ class _TIMUIKitTextFieldLayoutNarrowState extends TIMUIKitState<TIMUIKitTextFiel
lightPrimaryColor: theme.lightPrimaryColor); lightPrimaryColor: theme.lightPrimaryColor);
} }
if (showMore) { if (showMore && widget.forbiddenText == null) {
return MorePanel( return MorePanel(
morePanelConfig: widget.morePanelConfig, morePanelConfig: widget.morePanelConfig,
conversationID: widget.conversationID, conversationID: widget.conversationID,
@ -292,9 +296,9 @@ class _TIMUIKitTextFieldLayoutNarrowState extends TIMUIKitState<TIMUIKitTextFiel
} }
final height = originHeight != 0 ? originHeight : currentKeyboardHeight; final height = originHeight != 0 ? originHeight : currentKeyboardHeight;
return height; return height;
} else if (showEmojiPanel) { } else if (showEmojiPanel && widget.forbiddenText == null) {
return 248.0 + (bottomPadding ?? 0.0); return 248.0 + (bottomPadding ?? 0.0);
} else if (showMore) { } else if (showMore && widget.forbiddenText == null) {
final double inner = morePanelDynamicHeight ?? 120; final double inner = morePanelDynamicHeight ?? 120;
return inner + (bottomPadding ?? 0.0); return inner + (bottomPadding ?? 0.0);
} else if (widget.textEditingController.text.length >= 46 && showKeyboard == false) { } else if (widget.textEditingController.text.length >= 46 && showKeyboard == false) {
@ -399,6 +403,14 @@ class _TIMUIKitTextFieldLayoutNarrowState extends TIMUIKitState<TIMUIKitTextFiel
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) { Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
final theme = value.theme; final theme = value.theme;
//
if (widget.forbiddenText != null) {
return DisabledInputWidget(
text: widget.forbiddenText!,
icon: Icons.info_outline,
);
}
setKeyboardHeight ??= OptimizeUtils.debounce((height) { setKeyboardHeight ??= OptimizeUtils.debounce((height) {
settingModel.keyboardHeight = height; settingModel.keyboardHeight = height;
}, const Duration(seconds: 1)); }, const Duration(seconds: 1));
@ -446,7 +458,7 @@ class _TIMUIKitTextFieldLayoutNarrowState extends TIMUIKitState<TIMUIKitTextFiel
constraints: const BoxConstraints(minHeight: 50), constraints: const BoxConstraints(minHeight: 50),
child: Row( child: Row(
children: [ children: [
if (PlatformUtils().isMobile && widget.showSendAudio) if (PlatformUtils().isMobile && widget.showSendAudio && widget.forbiddenText == null)
InkWell( InkWell(
onTap: () async { onTap: () async {
showKeyboard = showSendSoundText; showKeyboard = showSendSoundText;
@ -479,7 +491,7 @@ class _TIMUIKitTextFieldLayoutNarrowState extends TIMUIKitState<TIMUIKitTextFiel
width: 10, width: 10,
), ),
Expanded( Expanded(
child: showSendSoundText child: showSendSoundText && widget.forbiddenText == null
? SendSoundMessage( ? SendSoundMessage(
onDownBottom: widget.goDownBottom, onDownBottom: widget.goDownBottom,
conversationID: widget.conversationID, conversationID: widget.conversationID,
@ -490,6 +502,7 @@ class _TIMUIKitTextFieldLayoutNarrowState extends TIMUIKitState<TIMUIKitTextFiel
child: ExtendedTextField( child: ExtendedTextField(
maxLines: 4, maxLines: 4,
minLines: 1, minLines: 1,
enabled: widget.forbiddenText == null,
focusNode: widget.focusNode, focusNode: widget.focusNode,
onChanged: debounceFunc, onChanged: debounceFunc,
onTap: () { onTap: () {
@ -538,7 +551,7 @@ class _TIMUIKitTextFieldLayoutNarrowState extends TIMUIKitState<TIMUIKitTextFiel
filled: true, filled: true,
isDense: true, isDense: true,
contentPadding: const EdgeInsets.symmetric(vertical: 12, horizontal: 12), contentPadding: const EdgeInsets.symmetric(vertical: 12, horizontal: 12),
hintText: widget.hintText ?? ''), hintText: widget.forbiddenText ?? widget.hintText ?? ''),
controller: widget.textEditingController, controller: widget.textEditingController,
specialTextSpanBuilder: PlatformUtils().isWeb specialTextSpanBuilder: PlatformUtils().isWeb
? null ? null
@ -584,7 +597,7 @@ class _TIMUIKitTextFieldLayoutNarrowState extends TIMUIKitState<TIMUIKitTextFiel
const SizedBox( const SizedBox(
width: 10, width: 10,
), ),
if (widget.showSendEmoji) if (widget.showSendEmoji && widget.forbiddenText == null)
InkWell( InkWell(
onTap: () { onTap: () {
_openEmojiPanel(); _openEmojiPanel();
@ -606,7 +619,7 @@ class _TIMUIKitTextFieldLayoutNarrowState extends TIMUIKitState<TIMUIKitTextFiel
const SizedBox( const SizedBox(
width: 10, width: 10,
), ),
if (widget.showMorePanel && showMoreButton) if (widget.showMorePanel && showMoreButton && widget.forbiddenText == null)
InkWell( InkWell(
onTap: () { onTap: () {
// model.sendCustomMessage(data: "a", convID: model.currentSelectedConv, convType: model.currentSelectedConvType == 1 ? ConvType.c2c : ConvType.group); // 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, width: 28,
), ),
), ),
if ((isAndroidDevice() || isWebDevice()) && !showMoreButton) if ((isAndroidDevice() || isWebDevice()) && !showMoreButton && widget.forbiddenText == null)
SizedBox( SizedBox(
height: 32.0, height: 32.0,
child: ElevatedButton( child: ElevatedButton(

View File

@ -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/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/special_text/emoji_text.dart';
import 'package:tencent_cloud_chat_uikit/ui/widgets/drag_widget.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:tencent_cloud_chat_uikit/ui/widgets/wide_popup.dart';
import 'package:universal_html/html.dart' as html; import 'package:universal_html/html.dart' as html;
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
@ -160,6 +161,7 @@ class TIMUIKitTextFieldLayoutWide extends StatefulWidget {
final V2TimConversation currentConversation; final V2TimConversation currentConversation;
final List<CustomStickerPackage> stickerPackageList; final List<CustomStickerPackage> stickerPackageList;
final String? forbiddenText;
const TIMUIKitTextFieldLayoutWide( const TIMUIKitTextFieldLayoutWide(
{Key? key, {Key? key,
@ -196,7 +198,8 @@ class TIMUIKitTextFieldLayoutWide extends StatefulWidget {
required this.currentConversation, required this.currentConversation,
required this.theme, required this.theme,
required this.chatConfig, required this.chatConfig,
required this.stickerPackageList}) required this.stickerPackageList,
this.forbiddenText})
: super(key: key); : super(key: key);
@override @override
@ -908,6 +911,13 @@ class _TIMUIKitTextFieldLayoutWideState extends TIMUIKitState<TIMUIKitTextFieldL
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) { Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
final theme = value.theme; final theme = value.theme;
if (widget.forbiddenText != null) {
return DisabledInputWidget(
text: widget.forbiddenText!,
icon: Icons.info_outline,
);
}
setKeyboardHeight ??= OptimizeUtils.debounce((height) { setKeyboardHeight ??= OptimizeUtils.debounce((height) {
settingModel.keyboardHeight = height; settingModel.keyboardHeight = height;
}, const Duration(seconds: 1)); }, const Duration(seconds: 1));

View File

@ -253,6 +253,7 @@ class _TUIChatState extends TIMUIKitState<TIMUIKitChat> {
); );
Widget? _joinInGroupCallWidget; Widget? _joinInGroupCallWidget;
bool _isPullingLatest = false;
@override @override
void initState() { void initState() {
@ -434,6 +435,16 @@ class _TUIChatState extends TIMUIKitState<TIMUIKitChat> {
model.loadGroupMemberList(groupID: _getConvID()); model.loadGroupMemberList(groupID: _getConvID());
} }
model.loadGroupInfo(_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; break;
default: default:
break; break;
@ -555,7 +566,7 @@ class _TUIChatState extends TIMUIKitState<TIMUIKitChat> {
? widget.textFieldBuilder!(context) ? widget.textFieldBuilder!(context)
: TIMUIKitInputTextField( : TIMUIKitInputTextField(
chatConfig: widget.config, chatConfig: widget.config,
groupID: widget.groupID, groupID: _getConvType() == ConvType.group ? _getConvID() : null,
atMemberPanelScroll: atMemberPanelScroll, atMemberPanelScroll: atMemberPanelScroll,
groupType: widget.conversation.groupType, groupType: widget.conversation.groupType,
currentConversation: widget.conversation, 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 { class TIMUIKitChatProviderScope extends StatelessWidget {

View File

@ -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_base.dart';
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_statelesswidget.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/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/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/tencent_cloud_chat_uikit.dart';
import 'package:tencent_cloud_chat_uikit/ui/controller/tim_uikit_chat_controller.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; final isDesktopScreen = TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop;
if (isDesktopScreen) { if (isDesktopScreen) {
TUIKitWidePopup.showSecondaryConfirmDialog( _showQuitGroupDialog(context, theme);
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();
}
}
});
} else { } else {
showCupertinoModalPopup<String>( _showQuitGroupDialog(context, theme);
context: context, }
builder: (BuildContext context) { }
return CupertinoActionSheet(
title: Text(TIM_t("退出后不会接收到此群聊消息")), _showQuitGroupDialog(BuildContext context, TUITheme theme) {
cancelButton: CupertinoActionSheetAction( bool clearChatHistory = true; //
onPressed: () {
Navigator.pop( showDialog(
context, context: context,
); builder: (BuildContext context) {
}, return StatefulBuilder(
child: Text(TIM_t("取消")), builder: (context, setState) {
isDefaultAction: false, return AlertDialog(
), title: Text(TIM_t("退出群聊")),
actions: [ content: Column(
CupertinoActionSheetAction( mainAxisSize: MainAxisSize.min,
onPressed: () async { crossAxisAlignment: CrossAxisAlignment.start,
Navigator.pop( children: [
context, Text(TIM_t("退出后不会接收到此群聊消息")),
); const SizedBox(height: 16),
final res = await sdkInstance.quitGroup(groupID: groupID); Row(
if (res.code == 0) { children: [
final deleteConvRes = Checkbox(
await sdkInstance.getConversationManager().deleteConversation(conversationID: "group_$groupID"); value: clearChatHistory,
if (deleteConvRes.code == 0) { onChanged: (bool? value) {
model.lifeCycle?.didLeaveGroup(); setState(() {
} clearChatHistory = value ?? true;
onTIMCallback(TIMCallback( });
type: TIMCallbackType.INFO, },
infoRecommendText: "${TIM_t("您已退出")}${model.groupInfo?.groupName}", ),
infoCode: 6661402)); Expanded(
} child: Text(TIM_t("退出群聊同时清空聊天记录")),
}, ),
child: Text( ],
TIM_t("确定"), ),
style: TextStyle(color: theme.cautionColor), ],
),
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));
} }
} }

View File

@ -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,
),
),
],
),
);
}
}