From d4ffbf2165217c22c40d743e2909fa70319b38a5 Mon Sep 17 00:00:00 2001 From: Zeew Date: Sat, 9 Aug 2025 17:07:47 +0800 Subject: [PATCH] =?UTF-8?q?feat(group):=20=E7=BE=A4=E6=B6=88=E6=81=AF?= =?UTF-8?q?=E6=8F=90=E7=A4=BA=E4=B8=AD=E6=96=B0=E5=A2=9E=E5=A7=93=E5=90=8D?= =?UTF-8?q?=E9=AB=98=E4=BA=AE=E6=98=BE=E7=A4=BA=E5=8F=8A=E7=82=B9=E5=87=BB?= =?UTF-8?q?=E8=B7=B3=E8=BD=AC=E7=94=A8=E6=88=B7=E8=AF=A6=E6=83=85=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/ui/utils/message.dart | 265 ++++++++++++++++++ ..._uikit_chat_history_message_list_item.dart | 10 +- ..._uikit_history_message_list_container.dart | 7 +- .../tim_uikit_chat_group_tips_elem.dart | 68 ++++- lib/ui/views/TIMUIKitChat/tim_uikit_chat.dart | 7 +- 5 files changed, 343 insertions(+), 14 deletions(-) diff --git a/lib/ui/utils/message.dart b/lib/ui/utils/message.dart index b8f9550..9ca16bb 100644 --- a/lib/ui/utils/message.dart +++ b/lib/ui/utils/message.dart @@ -30,6 +30,26 @@ import 'package:tencent_cloud_chat_uikit/ui/utils/common_utils.dart'; import 'package:tencent_cloud_chat_uikit/ui/utils/logger.dart'; import 'package:tencent_cloud_chat_uikit/theme/tui_theme.dart'; +/// 群提示消息文本片段数据类 +class TipsTextSegment { + final String text; + final String? userID; + final bool isClickable; + + TipsTextSegment({ + required this.text, + this.userID, + required this.isClickable, + }); +} + +/// 群提示消息富文本数据类 +class GroupTipsRichTextData { + final List segments; + + GroupTipsRichTextData({required this.segments}); +} + class MessageUtils { // 判断CallingData的方式和Trtc的方法一致 static isCallingData(String data) { @@ -241,6 +261,251 @@ class MessageUtils { return displayMessage; } + /// 生成群提示消息的富文本数据,支持点击用户名跳转 + static Future groupTipsMessageRichText( + V2TimGroupTipsElem groupTipsElem, List groupMemberList) async { + final operationType = groupTipsElem.type; + final operationMember = groupTipsElem.opMember; + final memberList = groupTipsElem.memberList; + final opUserNickName = _getOpUserNick(operationMember); + + // 获取当前登录用户ID + final currentUserID = (await TencentImSDKPlugin.v2TIMManager.getLoginUser()).data; + + List segments = []; + + switch (operationType) { + case GroupTipsElemType.V2TIM_GROUP_TIPS_TYPE_GROUP_INFO_CHANGE: + final userName = opUserNickName ?? ""; + final userID = operationMember?.userID; + final isCurrentUser = userID == currentUserID; + final displayName = isCurrentUser ? "你" : userName; + + if (userID != null && !isCurrentUser) { + segments.add(TipsTextSegment(text: displayName, userID: userID, isClickable: true)); + } else { + segments.add(TipsTextSegment(text: displayName, isClickable: false)); + } + + final groupChangeInfoList = groupTipsElem.groupChangeInfoList ?? []; + String changedInfoString = ""; + bool changedValue = false; + for (V2TimGroupChangeInfo? element in groupChangeInfoList) { + final newText = await _getGroupChangeType(element!, groupMemberList); + changedInfoString += (changedInfoString.isEmpty ? "" : " / ") + newText; + changedValue = element!.boolValue ?? false; + } + if (changedInfoString.isEmpty) { + changedInfoString = TIM_t("群资料"); + } + if (changedInfoString == TIM_t("全员禁言状态")) { + changedInfoString = TIM_t("全员禁言"); + segments.add(TipsTextSegment(text: changedValue == false ? " 取消" + changedInfoString : " 开启" + changedInfoString, isClickable: false)); + } else { + segments.add(TipsTextSegment(text: "修改" + changedInfoString, isClickable: false)); + } + break; + + case GroupTipsElemType.V2TIM_GROUP_TIPS_TYPE_QUIT: + final userName = opUserNickName ?? ""; + final userID = operationMember?.userID; + final isCurrentUser = userID == currentUserID; + final displayName = isCurrentUser ? "你" : userName; + + if (userID != null && !isCurrentUser) { + segments.add(TipsTextSegment(text: displayName, userID: userID, isClickable: true)); + } else { + segments.add(TipsTextSegment(text: displayName, isClickable: false)); + } + segments.add(TipsTextSegment(text: "退出群聊", isClickable: false)); + break; + + case GroupTipsElemType.V2TIM_GROUP_TIPS_TYPE_INVITE: + final inviteUser = _getOpUserNick(operationMember); + final opUserID = operationMember?.userID; + final isOpCurrentUser = opUserID == currentUserID; + final opDisplayName = isOpCurrentUser ? "你" : (inviteUser ?? ""); + + if (opUserID != null && !isOpCurrentUser) { + segments.add(TipsTextSegment(text: opDisplayName, userID: opUserID, isClickable: true)); + } else { + segments.add(TipsTextSegment(text: opDisplayName, isClickable: false)); + } + segments.add(TipsTextSegment(text: "邀请", isClickable: false)); + + // 添加被邀请的用户 + for (int i = 0; i < memberList!.length; i++) { + final member = memberList[i]!; + final memberName = _getMemberNickName(member); + final memberUserID = member.userID; + final isMemberCurrentUser = memberUserID == currentUserID; + final memberDisplayName = isMemberCurrentUser ? "你" : memberName!; + + if (memberUserID != null && !isMemberCurrentUser) { + segments.add(TipsTextSegment(text: memberDisplayName, userID: memberUserID, isClickable: true)); + } else { + segments.add(TipsTextSegment(text: memberDisplayName, isClickable: false)); + } + if (i < memberList.length - 1) { + segments.add(TipsTextSegment(text: "、", isClickable: false)); + } + } + segments.add(TipsTextSegment(text: "加入群组", isClickable: false)); + break; + + case GroupTipsElemType.V2TIM_GROUP_TIPS_TYPE_KICKED: + final kickUser = _getOpUserNick(operationMember); + final opUserID = operationMember?.userID; + final isOpCurrentUser = opUserID == currentUserID; + final opDisplayName = isOpCurrentUser ? "你" : (kickUser ?? ""); + + if (opUserID != null && !isOpCurrentUser) { + segments.add(TipsTextSegment(text: opDisplayName, userID: opUserID, isClickable: true)); + } else { + segments.add(TipsTextSegment(text: opDisplayName, isClickable: false)); + } + segments.add(TipsTextSegment(text: "将", isClickable: false)); + + // 添加被踢的用户 + for (int i = 0; i < memberList!.length; i++) { + final member = memberList[i]!; + final memberName = _getMemberNickName(member); + final memberUserID = member.userID; + final isMemberCurrentUser = memberUserID == currentUserID; + final memberDisplayName = isMemberCurrentUser ? "你" : memberName!; + + if (memberUserID != null && !isMemberCurrentUser) { + segments.add(TipsTextSegment(text: memberDisplayName, userID: memberUserID, isClickable: true)); + } else { + segments.add(TipsTextSegment(text: memberDisplayName, isClickable: false)); + } + if (i < memberList.length - 1) { + segments.add(TipsTextSegment(text: "、", isClickable: false)); + } + } + segments.add(TipsTextSegment(text: "踢出群组", isClickable: false)); + break; + + case GroupTipsElemType.V2TIM_GROUP_TIPS_TYPE_JOIN: + segments.add(TipsTextSegment(text: "用户", isClickable: false)); + // 添加加入的用户 + for (int i = 0; i < memberList!.length; i++) { + final member = memberList[i]!; + final memberName = _getMemberNickName(member); + final memberUserID = member.userID; + final isMemberCurrentUser = memberUserID == currentUserID; + final memberDisplayName = isMemberCurrentUser ? "你" : memberName!; + + if (memberUserID != null && !isMemberCurrentUser) { + segments.add(TipsTextSegment(text: memberDisplayName, userID: memberUserID, isClickable: true)); + } else { + segments.add(TipsTextSegment(text: memberDisplayName, isClickable: false)); + } + if (i < memberList.length - 1) { + segments.add(TipsTextSegment(text: "、", isClickable: false)); + } + } + segments.add(TipsTextSegment(text: "加入了群聊", isClickable: false)); + break; + + case GroupTipsElemType.V2TIM_GROUP_TIPS_TYPE_MEMBER_INFO_CHANGE: + for (int i = 0; i < groupTipsElem.memberList!.length; i++) { + final member = groupTipsElem.memberList![i]!; + final changedMember = + groupTipsElem.memberChangeInfoList!.firstWhere((element) => element!.userID == member.userID); + final isMute = changedMember!.muteTime != 0; + final memberName = _getMemberNickName(member); + final memberUserID = member.userID; + final isMemberCurrentUser = memberUserID == currentUserID; + final memberDisplayName = isMemberCurrentUser ? "你" : memberName!; + + if (memberUserID != null && !isMemberCurrentUser) { + segments.add(TipsTextSegment(text: memberDisplayName, userID: memberUserID, isClickable: true)); + } else { + segments.add(TipsTextSegment(text: memberDisplayName, isClickable: false)); + } + segments.add(TipsTextSegment(text: " 被" + (isMute ? TIM_t("禁言") : TIM_t("解除禁言")), isClickable: false)); + if (i < groupTipsElem.memberList!.length - 1) { + segments.add(TipsTextSegment(text: "、", isClickable: false)); + } + } + break; + + case GroupTipsElemType.V2TIM_GROUP_TIPS_TYPE_SET_ADMIN: + final opMember = _getOpUserNick(operationMember); + final opUserID = operationMember?.userID; + final isOpCurrentUser = opUserID == currentUserID; + final opDisplayName = isOpCurrentUser ? "你" : (opMember ?? ""); + + if (opUserID != null && !isOpCurrentUser) { + segments.add(TipsTextSegment(text: opDisplayName, userID: opUserID, isClickable: true)); + } else { + segments.add(TipsTextSegment(text: opDisplayName, isClickable: false)); + } + segments.add(TipsTextSegment(text: "将 ", isClickable: false)); + + // 添加被设置为管理员的用户 + for (int i = 0; i < memberList!.length; i++) { + final member = memberList[i]!; + final memberName = _getMemberNickName(member); + final memberUserID = member.userID; + final isMemberCurrentUser = memberUserID == currentUserID; + final memberDisplayName = isMemberCurrentUser ? "你" : memberName!; + + if (memberUserID != null && !isMemberCurrentUser) { + segments.add(TipsTextSegment(text: memberDisplayName, userID: memberUserID, isClickable: true)); + } else { + segments.add(TipsTextSegment(text: memberDisplayName, isClickable: false)); + } + if (i < memberList.length - 1) { + segments.add(TipsTextSegment(text: "、", isClickable: false)); + } + } + segments.add(TipsTextSegment(text: " 设置为管理员", isClickable: false)); + break; + + case GroupTipsElemType.V2TIM_GROUP_TIPS_TYPE_CANCEL_ADMIN: + final opMember = _getOpUserNick(operationMember); + final opUserID = operationMember?.userID; + final isOpCurrentUser = opUserID == currentUserID; + final opDisplayName = isOpCurrentUser ? "你" : (opMember ?? ""); + + if (opUserID != null && !isOpCurrentUser) { + segments.add(TipsTextSegment(text: opDisplayName, userID: opUserID, isClickable: true)); + } else { + segments.add(TipsTextSegment(text: opDisplayName, isClickable: false)); + } + segments.add(TipsTextSegment(text: "将 ", isClickable: false)); + + // 添加被取消管理员的用户 + for (int i = 0; i < memberList!.length; i++) { + final member = memberList[i]!; + final memberName = _getMemberNickName(member); + final memberUserID = member.userID; + final isMemberCurrentUser = memberUserID == currentUserID; + final memberDisplayName = isMemberCurrentUser ? "你" : memberName!; + + if (memberUserID != null && !isMemberCurrentUser) { + segments.add(TipsTextSegment(text: memberDisplayName, userID: memberUserID, isClickable: true)); + } else { + segments.add(TipsTextSegment(text: memberDisplayName, isClickable: false)); + } + if (i < memberList.length - 1) { + segments.add(TipsTextSegment(text: "、", isClickable: false)); + } + } + segments.add(TipsTextSegment(text: " 取消管理员", isClickable: false)); + break; + + default: + final String option2 = operationType.toString(); + segments.add(TipsTextSegment(text: TIM_t_para("系统消息 {{option2}}", "系统消息 $option2")(option2: option2), isClickable: false)); + break; + } + + return GroupTipsRichTextData(segments: segments); + } + static String formatVideoTime(int time) { List times = []; if (time <= 0) return '0:01'; diff --git a/lib/ui/views/TIMUIKitChat/TIMUIKItMessageList/tim_uikit_chat_history_message_list_item.dart b/lib/ui/views/TIMUIKitChat/TIMUIKItMessageList/tim_uikit_chat_history_message_list_item.dart index 6d59062..35c99ba 100644 --- a/lib/ui/views/TIMUIKitChat/TIMUIKItMessageList/tim_uikit_chat_history_message_list_item.dart +++ b/lib/ui/views/TIMUIKitChat/TIMUIKItMessageList/tim_uikit_chat_history_message_list_item.dart @@ -283,6 +283,9 @@ class TIMUIKitHistoryMessageListItem extends StatefulWidget { /// If provided, the default message action functionality will appear in the right-click context menu instead. final Widget? Function(V2TimMessage message)? customMessageHoverBarOnDesktop; + /// Callback for tapping user names in group tips messages + final Function(String userID, int conversationType)? onTapUserName; + const TIMUIKitHistoryMessageListItem( {Key? key, required this.message, @@ -312,7 +315,8 @@ class TIMUIKitHistoryMessageListItem extends StatefulWidget { this.textFieldController, this.onSecondaryTapForOthersPortrait, this.groupMemberInfo, - this.customMessageHoverBarOnDesktop}) + this.customMessageHoverBarOnDesktop, + this.onTapUserName}) : super(key: key); @override @@ -627,7 +631,9 @@ class _TIMUIKItHistoryMessageListItemState extends TIMUIKitState groupMemberList; + final Function(String userID, int conversationType)? onTapUserName; - const TIMUIKitGroupTipsElem({Key? key, required this.groupMemberList, required this.groupTipsElem}) : super(key: key); + const TIMUIKitGroupTipsElem({ + Key? key, + required this.groupMemberList, + required this.groupTipsElem, + this.onTapUserName, + }) : super(key: key); @override State createState() => _TIMUIKitGroupTipsElemState(); @@ -21,6 +28,7 @@ class TIMUIKitGroupTipsElem extends StatefulWidget { class _TIMUIKitGroupTipsElemState extends TIMUIKitState { String groupTipsAbstractText = ""; + GroupTipsRichTextData? richTextData; @override void initState() { @@ -30,23 +38,63 @@ class _TIMUIKitGroupTipsElemState extends TIMUIKitState { void getText() async { final newText = await MessageUtils.groupTipsMessageAbstract(widget.groupTipsElem, widget.groupMemberList); + final richData = await MessageUtils.groupTipsMessageRichText(widget.groupTipsElem, widget.groupMemberList); setState(() { groupTipsAbstractText = newText; + richTextData = richData; }); } + Widget _buildRichText(TUITheme theme) { + if (richTextData == null || widget.onTapUserName == null) { + // 如果没有富文本数据或回调函数,使用原来的简单文本 + return Text( + groupTipsAbstractText, + maxLines: 1, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + style: TextStyle(fontSize: 12, fontWeight: FontWeight.w400, color: hexToColor("888888")), + ); + } + + final List spans = richTextData!.segments.map((segment) { + if (segment.isClickable && segment.userID != null) { + return TextSpan( + text: segment.text, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w400, + color: const Color(0xFF006CFF), // 蓝色 + ), + recognizer: TapGestureRecognizer() + ..onTap = () { + widget.onTapUserName!(segment.userID!, 1); // 1 表示群组对话 + }, + ); + } else { + return TextSpan( + text: segment.text, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w400, + color: hexToColor("888888"), + ), + ); + } + }).toList(); + + return RichText( + maxLines: 1, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + text: TextSpan(children: spans), + ); + } + @override Widget tuiBuild(BuildContext context, TUIKitBuildValue value) { final TUITheme theme = value.theme; - return MessageUtils.wrapMessageTips( - Text( - groupTipsAbstractText, - maxLines: 1, - overflow: TextOverflow.ellipsis, - textAlign: TextAlign.center, - style: TextStyle(fontSize: 12, fontWeight: FontWeight.w400, color: hexToColor("888888")), - ), - theme); + return MessageUtils.wrapMessageTips(_buildRichText(theme), theme); } } diff --git a/lib/ui/views/TIMUIKitChat/tim_uikit_chat.dart b/lib/ui/views/TIMUIKitChat/tim_uikit_chat.dart index 934fc21..88de4a2 100644 --- a/lib/ui/views/TIMUIKitChat/tim_uikit_chat.dart +++ b/lib/ui/views/TIMUIKitChat/tim_uikit_chat.dart @@ -178,6 +178,9 @@ class TIMUIKitChat extends StatefulWidget { /// additional network requests to fetch the group member information internally. List? groupMemberList; + /// Callback for tapping user names in group tips messages + final Function(String userID, int conversationType)? onTapUserName; + TIMUIKitChat( {Key? key, this.groupID, @@ -216,7 +219,8 @@ class TIMUIKitChat extends StatefulWidget { this.customAppBar, this.inputTopBuilder, this.onSecondaryTapAvatar, - this.customMessageHoverBarOnDesktop}) + this.customMessageHoverBarOnDesktop, + this.onTapUserName}) : super(key: key) { startTime = DateTime.now().millisecondsSinceEpoch; } @@ -536,6 +540,7 @@ class _TUIChatState extends TIMUIKitState { showNickName: widget.showNickName, messageItemBuilder: widget.messageItemBuilder, conversationID: _getConvID(), + onTapUserName: widget.onTapUserName, ), )), )),