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/ui/utils/logger.dart';
import 'package:tencent_cloud_chat_uikit/theme/tui_theme.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 { class MessageUtils {
// CallingData的方式和Trtc的方法一致 // CallingData的方式和Trtc的方法一致
static isCallingData(String data) { static isCallingData(String data) {
@ -241,6 +261,251 @@ class MessageUtils {
return displayMessage; 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) { static String formatVideoTime(int time) {
List<int> times = []; List<int> times = [];
if (time <= 0) return '0:01'; 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. /// If provided, the default message action functionality will appear in the right-click context menu instead.
final Widget? Function(V2TimMessage message)? customMessageHoverBarOnDesktop; final Widget? Function(V2TimMessage message)? customMessageHoverBarOnDesktop;
/// Callback for tapping user names in group tips messages
final Function(String userID, int conversationType)? onTapUserName;
const TIMUIKitHistoryMessageListItem( const TIMUIKitHistoryMessageListItem(
{Key? key, {Key? key,
required this.message, required this.message,
@ -312,7 +315,8 @@ class TIMUIKitHistoryMessageListItem extends StatefulWidget {
this.textFieldController, this.textFieldController,
this.onSecondaryTapForOthersPortrait, this.onSecondaryTapForOthersPortrait,
this.groupMemberInfo, this.groupMemberInfo,
this.customMessageHoverBarOnDesktop}) this.customMessageHoverBarOnDesktop,
this.onTapUserName})
: super(key: key); : super(key: key);
@override @override
@ -627,7 +631,9 @@ class _TIMUIKItHistoryMessageListItemState extends TIMUIKitState<TIMUIKitHistory
return Container( return Container(
padding: const EdgeInsets.only(bottom: 20), padding: const EdgeInsets.only(bottom: 20),
child: TIMUIKitGroupTipsElem( child: TIMUIKitGroupTipsElem(
groupTipsElem: messageItem.groupTipsElem!, groupMemberList: model.groupMemberList ?? [])); groupTipsElem: messageItem.groupTipsElem!,
groupMemberList: model.groupMemberList ?? [],
onTapUserName: widget.onTapUserName));
} }
Widget _selfRevokeEditMessageBuilder(theme, TUIChatSeparateViewModel model) { 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. /// If provided, the default message action functionality will appear in the right-click context menu instead.
final Widget? Function(V2TimMessage message)? customMessageHoverBarOnDesktop; final Widget? Function(V2TimMessage message)? customMessageHoverBarOnDesktop;
/// Callback for tapping user names in group tips messages
final Function(String userID, int conversationType)? onTapUserName;
const TIMUIKitHistoryMessageListContainer({ const TIMUIKitHistoryMessageListContainer({
Key? key, Key? key,
this.itemBuilder, this.itemBuilder,
@ -112,6 +115,7 @@ class TIMUIKitHistoryMessageListContainer extends StatefulWidget {
this.onSecondaryTapAvatar, this.onSecondaryTapAvatar,
this.groupMemberInfo, this.groupMemberInfo,
this.customMessageHoverBarOnDesktop, this.customMessageHoverBarOnDesktop,
this.onTapUserName,
}) : super(key: key); }) : super(key: key);
@override @override
@ -188,7 +192,8 @@ class _TIMUIKitHistoryMessageListContainerState extends TIMUIKitState<TIMUIKitHi
allowAtUserWhenReply: chatConfig.isAtWhenReply, allowAtUserWhenReply: chatConfig.isAtWhenReply,
allowAvatarTap: chatConfig.isAllowClickAvatar, allowAvatarTap: chatConfig.isAllowClickAvatar,
allowLongPress: chatConfig.isAllowLongPressMessage, allowLongPress: chatConfig.isAllowLongPressMessage,
isUseMessageReaction: chatConfig.isUseMessageReaction); isUseMessageReaction: chatConfig.isUseMessageReaction,
onTapUserName: widget.onTapUserName);
}, },
tongueItemBuilder: widget.tongueItemBuilder, tongueItemBuilder: widget.tongueItemBuilder,
initFindingMsg: widget.initFindingMsg, initFindingMsg: widget.initFindingMsg,

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/gestures.dart';
import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_member_full_info.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'; 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' 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 { class TIMUIKitGroupTipsElem extends StatefulWidget {
final V2TimGroupTipsElem groupTipsElem; final V2TimGroupTipsElem groupTipsElem;
final List<V2TimGroupMemberFullInfo?> groupMemberList; 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 @override
State<TIMUIKitGroupTipsElem> createState() => _TIMUIKitGroupTipsElemState(); State<TIMUIKitGroupTipsElem> createState() => _TIMUIKitGroupTipsElemState();
@ -21,6 +28,7 @@ class TIMUIKitGroupTipsElem extends StatefulWidget {
class _TIMUIKitGroupTipsElemState extends TIMUIKitState<TIMUIKitGroupTipsElem> { class _TIMUIKitGroupTipsElemState extends TIMUIKitState<TIMUIKitGroupTipsElem> {
String groupTipsAbstractText = ""; String groupTipsAbstractText = "";
GroupTipsRichTextData? richTextData;
@override @override
void initState() { void initState() {
@ -30,23 +38,63 @@ class _TIMUIKitGroupTipsElemState extends TIMUIKitState<TIMUIKitGroupTipsElem> {
void getText() async { void getText() async {
final newText = await MessageUtils.groupTipsMessageAbstract(widget.groupTipsElem, widget.groupMemberList); final newText = await MessageUtils.groupTipsMessageAbstract(widget.groupTipsElem, widget.groupMemberList);
final richData = await MessageUtils.groupTipsMessageRichText(widget.groupTipsElem, widget.groupMemberList);
setState(() { setState(() {
groupTipsAbstractText = newText; 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 @override
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) { Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
final TUITheme theme = value.theme; final TUITheme theme = value.theme;
return MessageUtils.wrapMessageTips( return MessageUtils.wrapMessageTips(_buildRichText(theme), theme);
Text(
groupTipsAbstractText,
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
style: TextStyle(fontSize: 12, fontWeight: FontWeight.w400, color: hexToColor("888888")),
),
theme);
} }
} }

View File

@ -178,6 +178,9 @@ class TIMUIKitChat extends StatefulWidget {
/// additional network requests to fetch the group member information internally. /// additional network requests to fetch the group member information internally.
List<V2TimGroupMemberFullInfo?>? groupMemberList; List<V2TimGroupMemberFullInfo?>? groupMemberList;
/// Callback for tapping user names in group tips messages
final Function(String userID, int conversationType)? onTapUserName;
TIMUIKitChat( TIMUIKitChat(
{Key? key, {Key? key,
this.groupID, this.groupID,
@ -216,7 +219,8 @@ class TIMUIKitChat extends StatefulWidget {
this.customAppBar, this.customAppBar,
this.inputTopBuilder, this.inputTopBuilder,
this.onSecondaryTapAvatar, this.onSecondaryTapAvatar,
this.customMessageHoverBarOnDesktop}) this.customMessageHoverBarOnDesktop,
this.onTapUserName})
: super(key: key) { : super(key: key) {
startTime = DateTime.now().millisecondsSinceEpoch; startTime = DateTime.now().millisecondsSinceEpoch;
} }
@ -536,6 +540,7 @@ class _TUIChatState extends TIMUIKitState<TIMUIKitChat> {
showNickName: widget.showNickName, showNickName: widget.showNickName,
messageItemBuilder: widget.messageItemBuilder, messageItemBuilder: widget.messageItemBuilder,
conversationID: _getConvID(), conversationID: _getConvID(),
onTapUserName: widget.onTapUserName,
), ),
)), )),
)), )),