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';
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}");
}
}
}

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,
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";

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/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,

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_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);
}
}

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/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(

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/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));

View File

@ -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 {

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_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 {

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