feat(group): 群消息提示中新增姓名高亮显示及点击跳转用户详情功能

This commit is contained in:
Zeew 2025-08-09 17:07:47 +08:00
parent e427047752
commit d4ffbf2165
5 changed files with 343 additions and 14 deletions

View File

@ -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<TipsTextSegment> segments;
GroupTipsRichTextData({required this.segments});
}
class MessageUtils {
// CallingData的方式和Trtc的方法一致
static isCallingData(String data) {
@ -241,6 +261,251 @@ class MessageUtils {
return displayMessage;
}
///
static Future<GroupTipsRichTextData> groupTipsMessageRichText(
V2TimGroupTipsElem groupTipsElem, List<V2TimGroupMemberFullInfo?> 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<TipsTextSegment> 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<int> times = [];
if (time <= 0) return '0:01';

View File

@ -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<TIMUIKitHistory
return Container(
padding: const EdgeInsets.only(bottom: 20),
child: TIMUIKitGroupTipsElem(
groupTipsElem: messageItem.groupTipsElem!, groupMemberList: model.groupMemberList ?? []));
groupTipsElem: messageItem.groupTipsElem!,
groupMemberList: model.groupMemberList ?? [],
onTapUserName: widget.onTapUserName));
}
Widget _selfRevokeEditMessageBuilder(theme, TUIChatSeparateViewModel model) {

View File

@ -86,6 +86,9 @@ class TIMUIKitHistoryMessageListContainer 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 TIMUIKitHistoryMessageListContainer({
Key? key,
this.itemBuilder,
@ -112,6 +115,7 @@ class TIMUIKitHistoryMessageListContainer extends StatefulWidget {
this.onSecondaryTapAvatar,
this.groupMemberInfo,
this.customMessageHoverBarOnDesktop,
this.onTapUserName,
}) : super(key: key);
@override
@ -188,7 +192,8 @@ class _TIMUIKitHistoryMessageListContainerState extends TIMUIKitState<TIMUIKitHi
allowAtUserWhenReply: chatConfig.isAtWhenReply,
allowAvatarTap: chatConfig.isAllowClickAvatar,
allowLongPress: chatConfig.isAllowLongPressMessage,
isUseMessageReaction: chatConfig.isUseMessageReaction);
isUseMessageReaction: chatConfig.isUseMessageReaction,
onTapUserName: widget.onTapUserName);
},
tongueItemBuilder: widget.tongueItemBuilder,
initFindingMsg: widget.initFindingMsg,

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter/gestures.dart';
import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_member_full_info.dart'
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_group_member_full_info.dart';
import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_tips_elem.dart'
@ -12,8 +13,14 @@ import 'package:tencent_cloud_chat_uikit/theme/tui_theme.dart';
class TIMUIKitGroupTipsElem extends StatefulWidget {
final V2TimGroupTipsElem groupTipsElem;
final List<V2TimGroupMemberFullInfo?> 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<TIMUIKitGroupTipsElem> createState() => _TIMUIKitGroupTipsElemState();
@ -21,6 +28,7 @@ class TIMUIKitGroupTipsElem extends StatefulWidget {
class _TIMUIKitGroupTipsElemState extends TIMUIKitState<TIMUIKitGroupTipsElem> {
String groupTipsAbstractText = "";
GroupTipsRichTextData? richTextData;
@override
void initState() {
@ -30,23 +38,63 @@ class _TIMUIKitGroupTipsElemState extends TIMUIKitState<TIMUIKitGroupTipsElem> {
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<TextSpan> 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);
}
}

View File

@ -178,6 +178,9 @@ class TIMUIKitChat extends StatefulWidget {
/// additional network requests to fetch the group member information internally.
List<V2TimGroupMemberFullInfo?>? 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<TIMUIKitChat> {
showNickName: widget.showNickName,
messageItemBuilder: widget.messageItemBuilder,
conversationID: _getConvID(),
onTapUserName: widget.onTapUserName,
),
)),
)),