This commit is contained in:
2025-06-30 16:24:20 +08:00
commit 68de5eb048
454 changed files with 56921 additions and 0 deletions
@@ -0,0 +1,52 @@
// ignore_for_file: camel_case_types
import 'package:flutter/cupertino.dart';
import 'package:provider/provider.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/view_models/tui_conversation_view_model.dart';
import 'package:tencent_cloud_chat_uikit/data_services/services_locatar.dart';
import 'package:tencent_cloud_chat_uikit/ui/widgets/unread_message.dart';
typedef unreadCountBuilder = Widget Function(int unreadCount);
class TIMUIKitConversationTotalUnread extends TIMUIKitStatelessWidget {
final TUIConversationViewModel model =
serviceLocator<TUIConversationViewModel>();
final int? unreadCount;
final unreadCountBuilder? builder;
final double? width;
final double? height;
TIMUIKitConversationTotalUnread(
{this.width = 22.0,
this.height = 22.0,
this.unreadCount,
this.builder,
Key? key})
: super(key: key);
@override
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
return MultiProvider(
providers: [
ChangeNotifierProvider.value(value: model),
],
child: Consumer<TUIConversationViewModel>(
builder: (context, value, child) {
if (value.totalUnReadCount == 0) {
return Container();
}
if (builder != null) {
return builder!(value.totalUnReadCount);
}
return UnreadMessage(
unreadCount: unreadCount ?? value.totalUnReadCount,
width: width,
height: height);
},
),
);
}
}
@@ -0,0 +1,452 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_easyrefresh/easy_refresh.dart';
import 'package:flutter_slidable_plus_plus/flutter_slidable_plus_plus.dart';
import 'package:provider/provider.dart';
import 'package:scroll_to_index/scroll_to_index.dart';
import 'package:tencent_chat_i18n_tool/tencent_chat_i18n_tool.dart';
import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation.dart'
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_conversation.dart';
import 'package:tencent_cloud_chat_sdk/models/v2_tim_user_status.dart'
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_user_status.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_state.dart';
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_statelesswidget.dart';
import 'package:tencent_cloud_chat_uikit/business_logic/life_cycle/conversation_life_cycle.dart';
import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_conversation_view_model.dart';
import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_friendship_view_model.dart';
import 'package:tencent_cloud_chat_uikit/business_logic/listener_model/tui_group_listener_model.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/services_locatar.dart';
import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart';
import 'package:tencent_cloud_chat_uikit/ui/controller/tim_uikit_conversation_controller.dart';
import 'package:tencent_cloud_chat_uikit/ui/utils/platform.dart';
import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart';
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitConversation/tim_uikit_conversation_item.dart';
import 'package:tencent_cloud_chat_uikit/ui/widgets/customize_ball_pulse_header.dart';
import 'package:tencent_cloud_chat_uikit/ui/widgets/wide_popup.dart';
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_callback.dart';
import 'package:tencent_cloud_chat_uikit/theme/color.dart';
import 'package:tencent_cloud_chat_uikit/theme/tui_theme_view_model.dart';
typedef ConversationItemBuilder = Widget Function(V2TimConversation conversationItem, [V2TimUserStatus? onlineStatus]);
typedef ConversationItemSlideBuilder = List<ConversationItemSlidePanel> Function(V2TimConversation conversationItem);
typedef ConversationItemSecondaryMenuBuilder = Widget Function(
V2TimConversation conversationItem, VoidCallback onClose);
class TIMUIKitConversation extends StatefulWidget {
/// the callback after clicking conversation item
final ValueChanged<V2TimConversation>? onTapItem;
/// conversation controller
final TIMUIKitConversationController? controller;
/// the builder for conversation item
final ConversationItemBuilder? itemBuilder;
/// the builder for Slidable item for each conversation item, shows on narrow screens.
final ConversationItemSlideBuilder? itemSlideBuilder;
/// the widget of secondary tap menu for each conversation item, shows on wide screens.
final ConversationItemSecondaryMenuBuilder? itemSecondaryMenuBuilder;
/// the widget shows when no conversation exists
final Widget Function()? emptyBuilder;
/// the filter for conversation
final bool Function(V2TimConversation? conversation)? conversationCollector;
/// the builder for the second line in each conservation item,
/// usually shows the summary of the last message
final LastMessageBuilder? lastMessageBuilder;
/// The life cycle hooks for `TIMUIKitConversation`
final ConversationLifeCycle? lifeCycle;
/// Control if shows the online status for each user on its avatar.
final bool isShowOnlineStatus;
/// Control if shows the identifier that the conversation has a draft text, inputted in previous.
final bool isShowDraft;
const TIMUIKitConversation(
{Key? key,
this.lifeCycle,
this.onTapItem,
this.controller,
this.itemSecondaryMenuBuilder,
this.itemBuilder,
this.isShowDraft = true,
this.itemSlideBuilder,
this.conversationCollector,
this.emptyBuilder,
this.lastMessageBuilder,
this.isShowOnlineStatus = true})
: super(key: key);
@override
State<StatefulWidget> createState() {
return _TIMUIKitConversationState();
}
}
class ConversationItemSlidePanel extends TIMUIKitStatelessWidget {
ConversationItemSlidePanel({
Key? key,
this.flex = 1,
this.backgroundColor = Colors.white,
this.foregroundColor,
this.autoClose = true,
required this.onPressed,
this.icon,
this.spacing = 4,
this.label,
}) : assert(flex > 0),
assert(icon != null || label != null),
super(key: key);
/// {@macro slidable.actions.flex}
final int flex;
/// {@macro slidable.actions.backgroundColor}
final Color backgroundColor;
/// {@macro slidable.actions.foregroundColor}
final Color? foregroundColor;
/// {@macro slidable.actions.autoClose}
final bool autoClose;
/// {@macro slidable.actions.onPressed}
final SlidableActionCallback? onPressed;
/// An icon to display above the [label].
final IconData? icon;
/// The space between [icon] and [label] if both set.
///
/// Defaults to 4.
final double spacing;
/// A label to display below the [icon].
final String? label;
@override
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
return SlidableAction(
onPressed: onPressed,
flex: flex,
backgroundColor: backgroundColor,
foregroundColor: foregroundColor,
autoClose: autoClose,
label: label,
spacing: spacing,
);
}
}
class _TIMUIKitConversationState extends TIMUIKitState<TIMUIKitConversation> {
final TUIConversationViewModel model = serviceLocator<TUIConversationViewModel>();
late TIMUIKitConversationController _timuiKitConversationController;
final TUIThemeViewModel themeViewModel = serviceLocator<TUIThemeViewModel>();
final TUIFriendShipViewModel friendShipViewModel = serviceLocator<TUIFriendShipViewModel>();
final TUIGroupListenerModel groupListenerModel = serviceLocator<TUIGroupListenerModel>();
late AutoScrollController _autoScrollController;
@override
void initState() {
super.initState();
final controller = getController();
_timuiKitConversationController = controller;
_timuiKitConversationController.model = model;
_autoScrollController = AutoScrollController();
}
TIMUIKitConversationController getController() {
return widget.controller ?? TIMUIKitConversationController();
}
void onTapConvItem(V2TimConversation conversation) {
if (widget.onTapItem != null) {
widget.onTapItem!(conversation);
}
model.setSelectedConversation(conversation);
}
_clearHistory(V2TimConversation conversationItem) {
_timuiKitConversationController.clearHistoryMessage(conversation: conversationItem);
}
_pinConversation(V2TimConversation conversation) {
_timuiKitConversationController.pinConversation(
conversationID: conversation.conversationID, isPinned: !conversation.isPinned!);
}
_deleteConversation(V2TimConversation conversation) {
_timuiKitConversationController.deleteConversation(conversationID: conversation.conversationID);
}
List<V2TimConversation?> getFilteredConversation() {
List<V2TimConversation?> filteredConversationList =
model.conversationList.where((element) => (element?.groupID != null || element?.userID != null)).toList();
if (widget.conversationCollector != null) {
filteredConversationList = filteredConversationList.where(widget.conversationCollector!).toList();
}
return filteredConversationList;
}
_onScrollToConversation(String conversationID) {
final msgList = getFilteredConversation();
bool isFound = false;
int targetIndex = 1;
for (int i = msgList.length - 1; i >= 0; i--) {
final currentConversation = msgList[i];
if (currentConversation?.conversationID == conversationID) {
isFound = true;
targetIndex = i;
break;
}
}
if (isFound) {
_autoScrollController.scrollToIndex(
targetIndex,
preferPosition: AutoScrollPosition.begin,
);
}
}
Widget _defaultSecondaryMenu(V2TimConversation conversationItem, VoidCallback onClose) {
return TUIKitColumnMenu(data: [
if (!PlatformUtils().isWeb)
ColumnMenuItem(
label: TIM_t("清除消息"),
icon: const Icon(Icons.clear_all, size: 16),
onClick: () {
onClose();
_clearHistory(conversationItem);
}),
ColumnMenuItem(
label: conversationItem.isPinned! ? TIM_t("取消置顶") : TIM_t("置顶"),
icon: Icon(conversationItem.isPinned! ? Icons.vertical_align_bottom : Icons.vertical_align_top, size: 16),
onClick: () {
onClose();
_pinConversation(conversationItem);
}),
ColumnMenuItem(
label: TIM_t("删除会话"),
icon: const Icon(Icons.delete_outline, size: 16),
onClick: () {
onClose();
_deleteConversation(conversationItem);
}),
]);
}
List<ConversationItemSlidePanel> _defaultSlideBuilder(
V2TimConversation conversationItem,
) {
final theme = themeViewModel.theme;
return [
if (!PlatformUtils().isWeb)
ConversationItemSlidePanel(
onPressed: (context) {
_clearHistory(conversationItem);
},
backgroundColor: theme.conversationItemSliderClearBgColor ?? CommonColor.primaryColor,
foregroundColor: theme.conversationItemSliderTextColor,
label: TIM_t("清除"),
spacing: 0,
autoClose: true,
),
ConversationItemSlidePanel(
onPressed: (context) {
_pinConversation(conversationItem);
},
backgroundColor: theme.conversationItemSliderPinBgColor ?? CommonColor.infoColor,
foregroundColor: theme.conversationItemSliderTextColor,
label: conversationItem.isPinned! ? TIM_t("取消置顶") : TIM_t("置顶"),
),
ConversationItemSlidePanel(
onPressed: (context) {
_deleteConversation(conversationItem);
},
backgroundColor: theme.conversationItemSliderDeleteBgColor ?? Colors.red,
foregroundColor: theme.conversationItemSliderTextColor,
label: TIM_t("删除"),
)
];
}
Widget _getSecondaryMenu(V2TimConversation conversation, VoidCallback onClose) {
if (widget.itemSecondaryMenuBuilder != null) {
return widget.itemSecondaryMenuBuilder!(conversation, onClose);
}
return _defaultSecondaryMenu(conversation, onClose);
}
ConversationItemSlideBuilder _getSlideBuilder() {
return widget.itemSlideBuilder ?? _defaultSlideBuilder;
}
@override
void dispose() {
super.dispose();
}
@override
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
final theme = value.theme;
final isDesktopScreen = TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop;
return MultiProvider(
providers: [
ChangeNotifierProvider.value(value: model),
ChangeNotifierProvider.value(value: friendShipViewModel),
ChangeNotifierProvider.value(value: groupListenerModel)
],
builder: (BuildContext context, Widget? w) {
final _model = Provider.of<TUIConversationViewModel>(context);
bool haveMoreData = _model.haveMoreData;
final _friendShipViewModel = Provider.of<TUIFriendShipViewModel>(context);
_model.lifeCycle = widget.lifeCycle;
final TUIGroupListenerModel groupListenerModel = Provider.of<TUIGroupListenerModel>(context, listen: true);
final NeedUpdate? needUpdate = groupListenerModel.needUpdate;
if (needUpdate != null) {
groupListenerModel.needUpdate = null;
if (needUpdate.updateType == UpdateType.groupDismissed) {
onTIMCallback(TIMCallback(
type: TIMCallbackType.INFO,
infoRecommendText: "${needUpdate!.extraData}${TIM_t("已解散")}",
infoCode: 6661402));
} else if (needUpdate.updateType == UpdateType.kickedFromGroup) {
onTIMCallback(TIMCallback(
type: TIMCallbackType.INFO,
infoRecommendText: "${TIM_t("您已被踢出")}${needUpdate!.extraData}",
infoCode: 6661402));
}
}
List<V2TimConversation?> filteredConversationList = getFilteredConversation();
if (TencentUtils.checkString(_model.scrollToConversation) != null) {
_onScrollToConversation(_model.scrollToConversation!);
_model.clearScrollToConversation();
}
Widget conversationList() {
return filteredConversationList.isNotEmpty
? ListView.builder(
controller: _autoScrollController,
shrinkWrap: true,
itemCount: filteredConversationList.length,
itemBuilder: (context, index) {
if (index == filteredConversationList.length - 1) {
if (haveMoreData) {
_timuiKitConversationController.loadData();
}
}
final conversationItem = filteredConversationList[index];
final V2TimUserStatus? onlineStatus = _friendShipViewModel.userStatusList.firstWhere(
(item) => item.userID == conversationItem?.userID,
orElse: () => V2TimUserStatus(statusType: 0));
if (widget.itemBuilder != null) {
return widget.itemBuilder!(conversationItem!, onlineStatus);
}
final slideChildren = _getSlideBuilder()(conversationItem!);
final isCurrent = conversationItem.conversationID == model.selectedConversation?.conversationID;
final isPined = conversationItem.isPinned ?? false;
Widget conversationLineItem() {
return Material(
color: (isCurrent && isDesktopScreen)
? theme.conversationItemChooseBgColor
: isPined
? theme.conversationItemPinedBgColor
: theme.conversationItemBgColor,
child: GestureDetector(
child: TIMUIKitConversationItem(
isCurrent: isCurrent,
lastMessageBuilder: widget.lastMessageBuilder,
faceUrl: conversationItem.faceUrl ?? "",
nickName: conversationItem.showName ?? "",
isDisturb:
(conversationItem.groupType == "Meeting" ? false : conversationItem.recvOpt != 0),
lastMsg: conversationItem.lastMessage,
isPined: isPined,
groupAtInfoList: conversationItem.groupAtInfoList ?? [],
unreadCount: conversationItem.unreadCount ?? 0,
draftText: conversationItem.draftText,
onlineStatus: (widget.isShowOnlineStatus &&
conversationItem.userID != null &&
conversationItem.userID!.isNotEmpty)
? onlineStatus
: null,
draftTimestamp: conversationItem.draftTimestamp,
convType: conversationItem.type),
onTap: () => onTapConvItem(conversationItem),
),
);
}
return TUIKitScreenUtils.getDeviceWidget(
context: context,
desktopWidget: AutoScrollTag(
key: ValueKey(conversationItem.conversationID),
controller: _autoScrollController,
index: index,
child: InkWell(
onSecondaryTapDown: (details) {
TUIKitWidePopup.showPopupWindow(
operationKey: TUIKitWideModalOperationKey.conversationSecondaryMenu,
isDarkBackground: false,
borderRadius: const BorderRadius.all(Radius.circular(4)),
context: context,
offset: Offset(
min(details.globalPosition.dx, MediaQuery.of(context).size.width - 80),
min(details.globalPosition.dy, MediaQuery.of(context).size.height - 130)),
child: (onClose) => _getSecondaryMenu(conversationItem, onClose));
},
child: conversationLineItem(),
),
),
defaultWidget: AutoScrollTag(
key: ValueKey(conversationItem.conversationID),
controller: _autoScrollController,
index: index,
child: Slidable(
groupTag: 'conversation-list',
child: conversationLineItem(),
endActionPane: ActionPane(
extentRatio: slideChildren.length > 2 ? 0.77 : 0.5,
motion: const DrawerMotion(),
children: slideChildren)),
));
})
: (widget.emptyBuilder != null ? widget.emptyBuilder!() : Container());
}
return TUIKitScreenUtils.getDeviceWidget(
context: context,
defaultWidget: SlidableAutoCloseBehavior(
child: EasyRefresh(
header: CustomizeBallPulseHeader(color: theme.primaryColor),
onRefresh: () async {
model.refresh();
},
child: conversationList(),
),
),
desktopWidget: Scrollbar(controller: _autoScrollController, child: conversationList()));
});
}
}
@@ -0,0 +1,46 @@
import 'package:flutter/material.dart';
import 'package:tencent_chat_i18n_tool/tencent_chat_i18n_tool.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_base.dart';
import 'package:tencent_cloud_chat_uikit/theme/tui_theme.dart';
class TIMUIKitDraftText extends TIMUIKitStatelessWidget {
final BuildContext context;
final String draftText;
final double fontSize;
TIMUIKitDraftText({
Key? key,
this.fontSize = 14.0,
required this.context,
required this.draftText,
}) : super(key: key);
String _getDraftShowText() {
final draftShowText = TIM_t("草稿");
return '[$draftShowText] ';
}
@override
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
final TUITheme theme = value.theme;
return Row(children: [
Text(_getDraftShowText(),
style: TextStyle(
color: theme.conversationItemDraftTextColor,
)),
Expanded(
child: Text(
draftText,
softWrap: true,
overflow: TextOverflow.ellipsis,
maxLines: 1,
style: TextStyle(
height: 1.5,
color: theme.conversationItemLastMessageTextColor,
fontSize: fontSize),
)),
]);
}
}
@@ -0,0 +1,197 @@
// ignore_for_file: empty_catches
import 'package:flutter/material.dart';
import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_at_info.dart'
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_group_at_info.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/models/v2_tim_user_status.dart'
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_user_status.dart';
import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart';
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_statelesswidget.dart';
import 'package:tencent_cloud_chat_uikit/ui/utils/time_ago.dart';
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitConversation/tim_uikit_conversation_last_msg.dart';
import 'package:tencent_cloud_chat_uikit/ui/widgets/avatar.dart';
import 'package:tencent_cloud_chat_uikit/ui/widgets/unread_message.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';
typedef LastMessageBuilder = Widget? Function(V2TimMessage? lastMsg, List<V2TimGroupAtInfo?> groupAtInfoList);
class TIMUIKitConversationItem extends TIMUIKitStatelessWidget {
final String faceUrl;
final String nickName;
final V2TimMessage? lastMsg;
final int unreadCount;
final bool isPined;
final List<V2TimGroupAtInfo?> groupAtInfoList;
final String? draftText;
final int? draftTimestamp;
final bool isDisturb;
final LastMessageBuilder? lastMessageBuilder;
final V2TimUserStatus? onlineStatus;
final int? convType;
final bool isCurrent;
TIMUIKitConversationItem({
Key? key,
required this.faceUrl,
required this.nickName,
required this.lastMsg,
this.onlineStatus,
required this.isPined,
this.isCurrent = false,
required this.unreadCount,
required this.groupAtInfoList,
required this.isDisturb,
this.draftText,
this.draftTimestamp,
this.lastMessageBuilder,
this.convType,
}) : super(key: key);
Widget _getShowMsgWidget(BuildContext context) {
final isDesktopScreen = TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop;
if (lastMsg != null && lastMessageBuilder != null && lastMessageBuilder!(lastMsg, groupAtInfoList) != null) {
return lastMessageBuilder!(lastMsg, groupAtInfoList)!;
}
if (lastMsg != null || (draftText != null && draftText != "")) {
return TIMUIKitLastMsg(
fontSize: isDesktopScreen ? 12 : 14,
groupAtInfoList: groupAtInfoList,
lastMsg: lastMsg,
isDisturb: isDisturb,
unreadCount: unreadCount,
context: context,
draftText: draftText ?? "",
);
}
return Container(
height: 0,
);
}
bool isHaveSecondLine() {
return (draftText != null && draftText != "") || (lastMsg != null);
}
Widget _getTimeStringForChatWidget(BuildContext context, TUITheme theme) {
try {
if (draftTimestamp != null && draftTimestamp != 0) {
return Text(TimeAgo().getTimeStringForChat(draftTimestamp as int) ?? "",
style: TextStyle(
fontSize: 12,
color: theme.conversationItemTitmeTextColor,
));
} else if (lastMsg != null) {
return Text(TimeAgo().getTimeStringForChat(lastMsg!.timestamp as int) ?? "",
style: TextStyle(
fontSize: 11,
color: theme.conversationItemTitmeTextColor,
));
}
} catch (err) {}
return Container();
}
@override
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
final TUITheme theme = value.theme;
final isDesktopScreen = TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop;
return Container(
padding: const EdgeInsets.only(top: 6, bottom: 6, left: 16, right: 16),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: theme.conversationItemBorderColor ?? CommonColor.weakDividerColor,
width: 1,
),
),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.only(top: 0, bottom: 2, right: 0),
child: SizedBox(
width: isDesktopScreen ? 40 : 44,
height: isDesktopScreen ? 40 : 44,
child: Stack(
fit: StackFit.expand,
clipBehavior: Clip.none,
children: [
Avatar(onlineStatus: onlineStatus, faceUrl: faceUrl, showName: nickName, type: convType),
if (unreadCount != 0)
Positioned(
top: isDisturb ? -2.5 : -4.5,
right: isDisturb ? -2.5 : -4.5,
child: UnconstrainedBox(
child: UnreadMessage(
width: isDisturb ? 10 : 18,
height: isDisturb ? 10 : 18,
unreadCount: isDisturb ? 0 : unreadCount),
),
)
],
),
),
),
Expanded(
child: Container(
height: 60,
margin: EdgeInsets.only(left: isDesktopScreen ? 10 : 12),
padding: const EdgeInsets.only(top: 0, bottom: 0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
nickName,
softWrap: true,
textAlign: TextAlign.left,
overflow: TextOverflow.ellipsis,
maxLines: 1,
style: TextStyle(
height: 1,
color: theme.conversationItemTitleTextColor,
fontSize: isDesktopScreen ? 14 : 18,
fontWeight: FontWeight.w400,
),
)),
_getTimeStringForChatWidget(context, theme),
],
),
if (isHaveSecondLine())
const SizedBox(
height: 6,
),
Row(
children: [
Expanded(child: _getShowMsgWidget(context)),
if (isDisturb)
SizedBox(
width: 18,
height: 18,
child: Icon(
Icons.notifications_off,
color: theme.conversationItemNoNotificationIconColor,
size: isDesktopScreen ? 14 : 16.0,
),
)
],
),
],
),
))
],
),
);
}
}
@@ -0,0 +1,242 @@
// ignore_for_file: unrelated_type_equality_checks
import 'dart:convert';
import 'package:extended_text/extended_text.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:tencent_chat_i18n_tool/tencent_chat_i18n_tool.dart';
import 'package:tencent_cloud_chat_sdk/enum/message_elem_type.dart';
import 'package:tencent_cloud_chat_sdk/enum/message_status.dart';
import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_at_info.dart'
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_group_at_info.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_uikit/base_widgets/tim_ui_kit_base.dart';
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart';
import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart';
import 'package:tencent_cloud_chat_uikit/ui/utils/common_utils.dart';
import 'package:tencent_cloud_chat_uikit/ui/utils/message.dart';
import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart';
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitTextField/special_text/DefaultSpecialTextSpanBuilder.dart';
import 'package:tencent_cloud_chat_uikit/theme/tui_theme.dart';
import 'package:tencent_cloud_chat_uikit/theme/tui_theme_view_model.dart';
class TIMUIKitLastMsg extends StatefulWidget {
final V2TimMessage? lastMsg;
final List<V2TimGroupAtInfo?> groupAtInfoList;
final BuildContext context;
final double fontSize;
bool isDisturb;
int unreadCount;
String draftText;
TIMUIKitLastMsg(
{Key? key,
this.lastMsg,
required this.groupAtInfoList,
this.isDisturb = false,
this.unreadCount = 0,
required this.draftText,
required this.context,
this.fontSize = 14.0})
: super(key: key);
@override
State<TIMUIKitLastMsg> createState() => _TIMUIKitLastMsgState();
}
class _TIMUIKitLastMsgState extends TIMUIKitState<TIMUIKitLastMsg> {
String groupTipsAbstractText = "";
@override
void initState() {
super.initState();
_getMsgElem();
}
@override
void didUpdateWidget(covariant TIMUIKitLastMsg oldWidget) {
super.didUpdateWidget(oldWidget);
if ((oldWidget.lastMsg?.msgID != widget.lastMsg?.msgID) ||
(oldWidget.lastMsg?.id != widget.lastMsg?.id) ||
(oldWidget.lastMsg?.status != widget.lastMsg?.status) ||
(oldWidget.unreadCount != widget.unreadCount)) {
_getMsgElem();
}
}
(bool isRevoke, bool isRevokeByAdmin) isRevokeMessage(V2TimMessage? message) {
if (message == null) {
return (false, false);
}
if (message.status == 6) {
return (true, false);
} else {
try {
final customData = jsonDecode(message.cloudCustomData ?? "{}");
final isRevoke = customData["isRevoke"] ?? false;
final revokeByAdmin = customData["revokeByAdmin"] ?? false;
return (isRevoke, revokeByAdmin);
} catch (e) {
return (false, false);
}
}
}
void _getMsgElem() async {
final revokeStatus = isRevokeMessage(widget.lastMsg);
final isRevokedMessage = revokeStatus.$1;
final isAdminRevoke = revokeStatus.$2;
if (isRevokedMessage) {
final isSelf = widget.lastMsg!.isSelf ?? true;
final option1 =
isAdminRevoke ? TIM_t("管理员") : (isSelf ? TIM_t("您") : widget.lastMsg!.nickName ?? widget.lastMsg?.sender);
if (mounted) {
setState(() {
groupTipsAbstractText = TIM_t_para("{{option1}}撤回了一条消息", "$option1撤回了一条消息")(option1: option1);
});
}
} else {
String msgShowText = await _getLastMsgShowText(widget.lastMsg, widget.context) ?? "";
if (mounted) {
setState(() {
groupTipsAbstractText = msgShowText;
});
}
}
}
String _getDisturbUnreadCountInfo() {
if (widget.isDisturb && widget.unreadCount > 0) {
final option1 = widget.unreadCount.toString();
String unreadCountText = TIM_t_para("[{{option1}} 条]", "[$option1 条]")(option1: option1);
return unreadCountText;
}
return "";
}
Future<String?> _getLastMsgShowText(V2TimMessage? message, BuildContext context) async {
final msgType = message!.elemType;
switch (msgType) {
case MessageElemType.V2TIM_ELEM_TYPE_CUSTOM:
return TIM_t("[自定义]");
case MessageElemType.V2TIM_ELEM_TYPE_SOUND:
return TIM_t("[语音]");
case MessageElemType.V2TIM_ELEM_TYPE_TEXT:
return (widget.lastMsg?.textElem?.text)?.trim() ?? "";
case MessageElemType.V2TIM_ELEM_TYPE_FACE:
return TIM_t("[表情]");
case MessageElemType.V2TIM_ELEM_TYPE_FILE:
final option1 = widget.lastMsg!.fileElem!.fileName;
return TIM_t_para("[文件] {{option1}}", "[文件] $option1")(option1: option1);
case MessageElemType.V2TIM_ELEM_TYPE_GROUP_TIPS:
return await MessageUtils.groupTipsMessageAbstract(widget.lastMsg!.groupTipsElem!, []);
case MessageElemType.V2TIM_ELEM_TYPE_IMAGE:
return TIM_t("[图片]");
case MessageElemType.V2TIM_ELEM_TYPE_VIDEO:
return TIM_t("[视频]");
case MessageElemType.V2TIM_ELEM_TYPE_LOCATION:
return TIM_t("[位置]");
case MessageElemType.V2TIM_ELEM_TYPE_MERGER:
return TIM_t("[聊天记录]");
default:
return null;
}
}
Icon? _getIconByMsgStatus(BuildContext context) {
final msgStatus = widget.lastMsg!.status;
final theme = Provider.of<TUIThemeViewModel>(context).theme;
if (msgStatus == MessageStatus.V2TIM_MSG_STATUS_SEND_FAIL) {
return Icon(Icons.error, color: theme.cautionColor, size: 16);
}
if (msgStatus == MessageStatus.V2TIM_MSG_STATUS_SENDING) {
return Icon(Icons.arrow_back, color: theme.weakTextColor, size: 16);
}
return null;
}
String _getAtMessage() {
bool atMe = false;
bool atAll = false;
String msg = "";
for (var item in widget.groupAtInfoList) {
if (item!.atType == 1) {
atMe = true;
continue;
} else if (item!.atType == 2) {
atAll = true;
continue;
} else if (item!.atType == 3) {
atMe = true;
atAll = true;
continue;
}
}
if (atAll && atMe) {
msg = TIM_t("[@所有人][有人@我]");
} else if (atAll) {
msg = TIM_t("[@所有人]");
} else if (atMe) {
msg = TIM_t("[有人@我]");
}
return msg;
}
String _getDraftShowText() {
final draftShowText = TIM_t("草稿");
return '[$draftShowText]';
}
@override
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
final isDesktopScreen = TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop;
final TUITheme theme = value.theme;
final icon = _getIconByMsgStatus(context);
String disturbUnreadCountInfo = _getDisturbUnreadCountInfo();
return Row(children: [
if (icon != null)
Container(
margin: const EdgeInsets.only(right: 2),
child: icon,
),
if (widget.groupAtInfoList.isNotEmpty)
Text(_getAtMessage(), style: TextStyle(color: theme.cautionColor, fontSize: widget.fontSize)),
if (widget.draftText != null && widget.draftText != "")
Text(_getDraftShowText(),
style: TextStyle(color: theme.conversationItemDraftTextColor, fontSize: widget.fontSize)),
if (disturbUnreadCountInfo != "")
Text(disturbUnreadCountInfo, style: TextStyle(color: theme.weakTextColor, fontSize: widget.fontSize)),
if (widget.draftText != null && widget.draftText != "")
Expanded(
child: ExtendedText(groupTipsAbstractText,
softWrap: true,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(height: 1, color: theme.weakTextColor, fontSize: widget.fontSize),
specialTextSpanBuilder: DefaultSpecialTextSpanBuilder(
isUseQQPackage: true,
isUseTencentCloudChatPackage: true,
showAtBackground: true,
)),
),
if (widget.draftText == null || widget.draftText == "" && TencentUtils.checkString(groupTipsAbstractText) != null)
Expanded(
child: ExtendedText(groupTipsAbstractText,
softWrap: true,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(height: 1, color: theme.weakTextColor, fontSize: widget.fontSize),
specialTextSpanBuilder: DefaultSpecialTextSpanBuilder(
isUseQQPackage: true,
isUseTencentCloudChatPackage: true,
showAtBackground: true,
)),
)
]);
}
}