init
This commit is contained in:
+63
@@ -0,0 +1,63 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_statelesswidget.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKItMessageList/TIMUIKitTongue/tim_uikit_tongue_item.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart';
|
||||
|
||||
enum MessageListTongueType {
|
||||
none,
|
||||
toLatest,
|
||||
showUnread,
|
||||
atMe,
|
||||
showPrevious,
|
||||
atAll,
|
||||
}
|
||||
|
||||
typedef TongueItemBuilder = Widget Function(
|
||||
VoidCallback onClick, MessageListTongueType valueType, int unreadCount);
|
||||
|
||||
class TIMUIKitHistoryMessageListTongue extends TIMUIKitStatelessWidget {
|
||||
/// the value type currently
|
||||
final MessageListTongueType valueType;
|
||||
|
||||
/// the callback after clicking
|
||||
final VoidCallback onClick;
|
||||
|
||||
/// unread amount currently
|
||||
final int unreadCount;
|
||||
|
||||
/// the builder for tongue item
|
||||
final TongueItemBuilder? tongueItemBuilder;
|
||||
|
||||
/// total amount of messages at me
|
||||
final String atNum;
|
||||
|
||||
final int previousCount;
|
||||
|
||||
TIMUIKitHistoryMessageListTongue({
|
||||
Key? key,
|
||||
required this.valueType,
|
||||
required this.onClick,
|
||||
required this.previousCount,
|
||||
required this.unreadCount,
|
||||
this.tongueItemBuilder,
|
||||
this.atNum = "",
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
late Widget tongueItem;
|
||||
if (tongueItemBuilder != null) {
|
||||
tongueItem = tongueItemBuilder!(onClick, valueType, unreadCount);
|
||||
} else {
|
||||
tongueItem = TIMUIKitTongueItem(
|
||||
onClick: onClick,
|
||||
unreadCount: unreadCount,
|
||||
valueType: valueType,
|
||||
atNum: atNum,
|
||||
previousCount: previousCount,
|
||||
);
|
||||
}
|
||||
return valueType != MessageListTongueType.none ? tongueItem : Container();
|
||||
}
|
||||
}
|
||||
+182
@@ -0,0 +1,182 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:scroll_to_index/scroll_to_index.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_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_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/business_logic/separate_models/tui_chat_separate_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_chat_global_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/services_locatar.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/common_utils.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKItMessageList/TIMUIKitTongue/tim_uikit_chat_history_message_list_tongue.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
class TIMUIKitHistoryMessageListTongueContainer extends StatefulWidget {
|
||||
final Widget Function(void Function(), MessageListTongueType, int)? tongueItemBuilder;
|
||||
final List<V2TimGroupAtInfo?>? groupAtInfoList;
|
||||
final Function(String targetSeq) scrollToIndexBySeq;
|
||||
final AutoScrollController scrollController;
|
||||
final TUIChatSeparateViewModel model;
|
||||
final V2TimConversation conversation;
|
||||
|
||||
const TIMUIKitHistoryMessageListTongueContainer({
|
||||
Key? key,
|
||||
this.tongueItemBuilder,
|
||||
this.groupAtInfoList,
|
||||
required this.conversation,
|
||||
required this.scrollToIndexBySeq,
|
||||
required this.scrollController,
|
||||
required this.model,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _TIMUIKitHistoryMessageListTongueContainerState();
|
||||
}
|
||||
|
||||
class _TIMUIKitHistoryMessageListTongueContainerState extends TIMUIKitState<TIMUIKitHistoryMessageListTongueContainer> {
|
||||
bool isFinishJumpToAt = false;
|
||||
List<V2TimGroupAtInfo?>? groupAtInfoList = [];
|
||||
final TUIChatGlobalModel globalModel = serviceLocator<TUIChatGlobalModel>();
|
||||
bool isClickShowPrevious = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
initScrollListener();
|
||||
groupAtInfoList = widget.groupAtInfoList?.reversed.toList();
|
||||
}
|
||||
|
||||
void changePositionState(HistoryMessagePosition newPosition) {
|
||||
if (globalModel.getMessageListPosition(widget.model.conversationID) != newPosition) {
|
||||
globalModel.setMessageListPosition(widget.model.conversationID, newPosition);
|
||||
}
|
||||
}
|
||||
|
||||
scrollHandler() {
|
||||
final screenHeight = MediaQuery.of(context).size.height;
|
||||
final offset = widget.scrollController.offset;
|
||||
final conversationUnreadCount = widget.model.getConversationUnreadCount();
|
||||
if (offset <= 0.0 && conversationUnreadCount != 0) {
|
||||
widget.model.showLatestUnread();
|
||||
}
|
||||
if (widget.scrollController.offset <= widget.scrollController.position.minScrollExtent &&
|
||||
!widget.scrollController.position.outOfRange &&
|
||||
!widget.model.haveMoreLatestData) {
|
||||
changePositionState(HistoryMessagePosition.bottom);
|
||||
} else if (widget.scrollController.offset <= screenHeight * 1.6 &&
|
||||
widget.scrollController.offset > 0 &&
|
||||
!widget.scrollController.position.outOfRange &&
|
||||
!widget.model.haveMoreLatestData) {
|
||||
changePositionState(HistoryMessagePosition.inTwoScreen);
|
||||
} else if (widget.scrollController.offset > screenHeight * 1.6 &&
|
||||
!widget.scrollController.position.outOfRange &&
|
||||
!widget.model.haveMoreLatestData) {
|
||||
changePositionState(HistoryMessagePosition.awayTwoScreen);
|
||||
}
|
||||
}
|
||||
|
||||
void initScrollListener() {
|
||||
widget.scrollController.addListener(scrollHandler);
|
||||
}
|
||||
|
||||
MessageListTongueType _getTongueValueType(List<V2TimGroupAtInfo?>? groupAtInfoList) {
|
||||
if (globalModel.getMessageListPosition(widget.model.conversationID) == HistoryMessagePosition.notShowLatest) {
|
||||
return MessageListTongueType.none;
|
||||
}
|
||||
if (groupAtInfoList != null && groupAtInfoList.isNotEmpty && !isFinishJumpToAt) {
|
||||
if (groupAtInfoList[0]!.atType == 1) {
|
||||
return MessageListTongueType.atMe;
|
||||
} else {
|
||||
return MessageListTongueType.atAll;
|
||||
}
|
||||
}
|
||||
|
||||
// if ((widget.conversation.unreadCount ?? 0) > 20 && !isClickShowPrevious) {
|
||||
// return MessageListTongueType.showPrevious;
|
||||
// }
|
||||
|
||||
if (globalModel.unreadCountForTongue > 0) {
|
||||
return MessageListTongueType.showUnread;
|
||||
}
|
||||
|
||||
if (globalModel.getMessageListPosition(widget.model.conversationID) == HistoryMessagePosition.awayTwoScreen) {
|
||||
return MessageListTongueType.toLatest;
|
||||
}
|
||||
|
||||
return MessageListTongueType.none;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
widget.scrollController.removeListener(scrollHandler);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
return Selector<TUIChatGlobalModel, Tuple2<HistoryMessagePosition, int>>(
|
||||
builder: (context, value, child) {
|
||||
return Positioned(
|
||||
bottom: _getTongueValueType(groupAtInfoList) != MessageListTongueType.showPrevious ? 16 : null,
|
||||
top: _getTongueValueType(groupAtInfoList) == MessageListTongueType.showPrevious ? 16 : null,
|
||||
right: 16,
|
||||
child: TIMUIKitHistoryMessageListTongue(
|
||||
previousCount: widget.conversation.unreadCount ?? 0,
|
||||
tongueItemBuilder: widget.tongueItemBuilder,
|
||||
unreadCount: value.item2,
|
||||
onClick: () async {
|
||||
if (groupAtInfoList != null && groupAtInfoList!.isNotEmpty) {
|
||||
if (groupAtInfoList?.length == 1) {
|
||||
widget.scrollToIndexBySeq(groupAtInfoList![0]!.seq);
|
||||
|
||||
setState(() {
|
||||
groupAtInfoList = [];
|
||||
isFinishJumpToAt = true;
|
||||
});
|
||||
} else {
|
||||
widget.scrollToIndexBySeq(groupAtInfoList!.removeAt(0)!.seq);
|
||||
}
|
||||
} else if ((widget.conversation.unreadCount ?? 0) > 20 && !isClickShowPrevious) {
|
||||
try {
|
||||
isClickShowPrevious = true;
|
||||
final String? lastSeqString = widget.conversation.lastMessage?.seq;
|
||||
final int? lastSeq =
|
||||
TencentUtils.checkString(lastSeqString) != null ? int.parse(lastSeqString!) : null;
|
||||
final int? previousCount = widget.conversation.unreadCount;
|
||||
if (lastSeq != null && previousCount != null) {
|
||||
final targetSeq = lastSeq - previousCount;
|
||||
await widget.model.loadListForSpecificMessage(seq: targetSeq);
|
||||
// Future.delayed(const Duration(milliseconds: 100), () {
|
||||
// widget.scrollToIndexBySeq((targetSeq).toString());
|
||||
// });
|
||||
}
|
||||
} catch (e) {
|
||||
// TODO: 这里后续加个弹窗提示客户,找消息失败了
|
||||
}
|
||||
// widget.model.loadListForSpecificMessage(seq: count);
|
||||
} else if (value.item1 == HistoryMessagePosition.awayTwoScreen || value.item2 > 0) {
|
||||
widget.model.showLatestUnread();
|
||||
widget.scrollController.animateTo(
|
||||
widget.scrollController.position.minScrollExtent,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.ease,
|
||||
);
|
||||
return;
|
||||
}
|
||||
},
|
||||
atNum: groupAtInfoList?.length.toString() ?? "",
|
||||
valueType: _getTongueValueType(groupAtInfoList),
|
||||
),
|
||||
);
|
||||
},
|
||||
selector: (c, model) {
|
||||
final messageListPosition = model.getMessageListPosition(widget.model.conversationID);
|
||||
final unreadCountForConversation = model.unreadCountForTongue;
|
||||
return Tuple2(messageListPosition, unreadCountForConversation);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
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/ui/views/TIMUIKitChat/TIMUIKItMessageList/TIMUIKitTongue/tim_uikit_chat_history_message_list_tongue.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/color.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/tui_theme.dart';
|
||||
|
||||
class TIMUIKitTongueItem extends TIMUIKitStatelessWidget {
|
||||
/// the callback after clicking
|
||||
final VoidCallback onClick;
|
||||
|
||||
/// the value type currently
|
||||
final MessageListTongueType valueType;
|
||||
|
||||
/// unread amount currently
|
||||
final int unreadCount;
|
||||
|
||||
/// total amount of messages at me
|
||||
final String atNum;
|
||||
|
||||
final int previousCount;
|
||||
|
||||
TIMUIKitTongueItem({
|
||||
Key? key,
|
||||
required this.onClick,
|
||||
required this.valueType,
|
||||
required this.previousCount,
|
||||
required this.unreadCount,
|
||||
required this.atNum,
|
||||
}) : super(key: key);
|
||||
|
||||
Map<MessageListTongueType, String> textType(BuildContext context) {
|
||||
final option1 = unreadCount.toString();
|
||||
final option2 = atNum.toString();
|
||||
// final option3 = previousCount.toString();
|
||||
final String atMeString = option2 != ""
|
||||
? TIM_t_para("有{{option2}}条@我消息", "有$option2条@我消息")(option2: option2)
|
||||
: TIM_t("有人@我");
|
||||
|
||||
return {
|
||||
// MessageListTongueType.showPrevious:
|
||||
// TIM_t_para("{{option3}}条未读消息", "$option3条未读消息")(option3: option3),
|
||||
MessageListTongueType.toLatest: TIM_t("回到最新位置"),
|
||||
MessageListTongueType.showUnread:
|
||||
TIM_t_para("{{option1}}条新消息", "$option1条新消息")(option1: option1),
|
||||
MessageListTongueType.atMe: atMeString,
|
||||
MessageListTongueType.atAll: TIM_t("@所有人"),
|
||||
};
|
||||
}
|
||||
|
||||
final Map<MessageListTongueType, IconData> iconType = {
|
||||
MessageListTongueType.toLatest: Icons.arrow_downward_outlined,
|
||||
MessageListTongueType.showUnread: Icons.arrow_downward_outlined,
|
||||
MessageListTongueType.atMe: Icons.arrow_upward_outlined,
|
||||
MessageListTongueType.atAll: Icons.arrow_upward_outlined,
|
||||
MessageListTongueType.showPrevious: Icons.arrow_upward_outlined,
|
||||
};
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final TUITheme theme = value.theme;
|
||||
return GestureDetector(
|
||||
onTap: onClick,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: Border.all(color: hexToColor("E5E5E5"), width: 1),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: theme.weakDividerColor ?? hexToColor("E6E9EB"),
|
||||
offset: const Offset(0.0, 0.0),
|
||||
blurRadius: 10,
|
||||
spreadRadius: 2),
|
||||
],
|
||||
),
|
||||
padding: const EdgeInsets.all(10),
|
||||
// width: 112,
|
||||
// height: 37,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
margin: const EdgeInsets.only(right: 6),
|
||||
child: Icon(
|
||||
iconType[valueType],
|
||||
color: theme.primaryColor,
|
||||
size: 12,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
textType(context)[valueType] ?? "",
|
||||
style: TextStyle(
|
||||
color: theme.primaryColor,
|
||||
fontSize: 12.0,
|
||||
fontWeight: FontWeight.w400),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+531
@@ -0,0 +1,531 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:loading_animation_widget/loading_animation_widget.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/enum/message_elem_type.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_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/base_widgets/tim_ui_kit_statelesswidget.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_chat_separate_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_chat_global_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/logger.dart';
|
||||
// ignore: unused_import
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/optimize_utils.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKItMessageList/tim_uikit_chat_history_message_list_config.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKItMessageList/utils.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/keepalive_wrapper.dart';
|
||||
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_callback.dart';
|
||||
import 'TIMUIKitTongue/tim_uikit_chat_history_message_list_tongue.dart';
|
||||
import 'TIMUIKitTongue/tim_uikit_chat_history_message_list_tongue_container.dart';
|
||||
|
||||
enum LoadingPlace {
|
||||
none,
|
||||
top,
|
||||
bottom,
|
||||
}
|
||||
|
||||
enum ScrollType { toIndex, toIndexBegin }
|
||||
|
||||
class TIMUIKitHistoryMessageListController extends ChangeNotifier {
|
||||
AutoScrollController? scrollController = AutoScrollController();
|
||||
late ScrollType scrollType;
|
||||
late V2TimMessage targetMessage;
|
||||
|
||||
TIMUIKitHistoryMessageListController({
|
||||
AutoScrollController? scrollController,
|
||||
}) {
|
||||
if (scrollController != null) {
|
||||
this.scrollController = scrollController;
|
||||
}
|
||||
}
|
||||
|
||||
scrollToIndex(V2TimMessage message) {
|
||||
scrollType = ScrollType.toIndex;
|
||||
targetMessage = message;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
scrollToIndexBegin(V2TimMessage message) {
|
||||
scrollType = ScrollType.toIndexBegin;
|
||||
targetMessage = message;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
class TIMUIKitHistoryMessageList extends StatefulWidget {
|
||||
/// message list
|
||||
final List<V2TimMessage?> messageList;
|
||||
|
||||
/// tongue item builder
|
||||
final TongueItemBuilder? tongueItemBuilder;
|
||||
|
||||
/// group at info, it can get from conversation info
|
||||
final List<V2TimGroupAtInfo?>? groupAtInfoList;
|
||||
|
||||
/// use for build message item
|
||||
final Widget Function(BuildContext, V2TimMessage?)? itemBuilder;
|
||||
|
||||
/// can controll message list scroll
|
||||
final TIMUIKitHistoryMessageListController? controller;
|
||||
|
||||
/// use for message jump, if passed will jump to target message.
|
||||
final V2TimMessage? initFindingMsg;
|
||||
|
||||
/// use for load more message
|
||||
final Future<bool> Function(String?, LoadDirection direction, [int?, int?]) onLoadMore;
|
||||
|
||||
/// configuration for list view
|
||||
final TIMUIKitHistoryMessageListConfig? mainHistoryListConfig;
|
||||
|
||||
final TUIChatSeparateViewModel model;
|
||||
|
||||
final bool isAllowScroll;
|
||||
|
||||
final V2TimConversation conversation;
|
||||
|
||||
const TIMUIKitHistoryMessageList(
|
||||
{Key? key,
|
||||
required this.model,
|
||||
required this.messageList,
|
||||
this.itemBuilder,
|
||||
this.controller,
|
||||
required this.onLoadMore,
|
||||
this.tongueItemBuilder,
|
||||
this.groupAtInfoList,
|
||||
this.initFindingMsg,
|
||||
this.isAllowScroll = true,
|
||||
this.mainHistoryListConfig,
|
||||
required this.conversation})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _TIMUIKitHistoryMessageListState();
|
||||
}
|
||||
|
||||
class _TIMUIKitHistoryMessageListState extends TIMUIKitState<TIMUIKitHistoryMessageList> {
|
||||
V2TimMessage? findingMsg;
|
||||
String findingSeq = "";
|
||||
late TIMUIKitHistoryMessageListController _controller;
|
||||
late AutoScrollController _autoScrollController;
|
||||
LoadingPlace loadingPlace = LoadingPlace.none;
|
||||
bool maybeHaveMoreMessageForFind = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = widget.controller ?? TIMUIKitHistoryMessageListController();
|
||||
_autoScrollController = _controller.scrollController ?? AutoScrollController();
|
||||
_controller.addListener(_controllerListener);
|
||||
initFinding();
|
||||
}
|
||||
|
||||
initFinding() async {
|
||||
if (widget.initFindingMsg != null) {
|
||||
await widget.onLoadMore(null, LoadDirection.previous);
|
||||
setState(() {
|
||||
findingMsg = widget.initFindingMsg!;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_controllerListener() {
|
||||
final scrollType = _controller.scrollType;
|
||||
final targetMessage = _controller.targetMessage;
|
||||
switch (scrollType) {
|
||||
case ScrollType.toIndex:
|
||||
_onScrollToIndex(targetMessage);
|
||||
break;
|
||||
case ScrollType.toIndexBegin:
|
||||
_onScrollToIndexBegin(targetMessage);
|
||||
break;
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
Widget _getMessageItemBuilder(V2TimMessage? messageItem) {
|
||||
if (widget.itemBuilder != null) {
|
||||
return widget.itemBuilder!(context, messageItem);
|
||||
}
|
||||
return Container();
|
||||
}
|
||||
|
||||
_getMessageId(int index) {
|
||||
if (widget.messageList[index]!.elemType == 11) {
|
||||
return _getMessageId(index - 1);
|
||||
}
|
||||
return widget.messageList[index]!.msgID;
|
||||
}
|
||||
|
||||
void showCantFindMsg() {
|
||||
findingMsg = null;
|
||||
findingSeq = "";
|
||||
loadingPlace = LoadingPlace.none;
|
||||
onTIMCallback(TIMCallback(type: TIMCallbackType.INFO, infoRecommendText: TIM_t("无法定位到原消息"), infoCode: 6660401));
|
||||
}
|
||||
|
||||
_onScrollToIndex(V2TimMessage targetMsg) async {
|
||||
// This method called by @ messages or messages been searched, aims to jump to target message
|
||||
loadingPlace = LoadingPlace.top;
|
||||
const int singleLoadAmount = kIsWeb ? 15 : 40;
|
||||
final lastTimestamp = widget.messageList[widget.messageList.length - 1]?.timestamp;
|
||||
final msgList = widget.messageList;
|
||||
final targetTimeStamp = targetMsg.timestamp!;
|
||||
|
||||
if (targetTimeStamp >= lastTimestamp!) {
|
||||
// 当前列表里应该有这个消息,试试能不能直接定位到那去
|
||||
bool isFound = false;
|
||||
int targetIndex = 1;
|
||||
for (int i = msgList.length - 1; i >= 0; i--) {
|
||||
final currentMsg = msgList[i];
|
||||
if (currentMsg?.timestamp == targetTimeStamp &&
|
||||
currentMsg?.elemType != 11 &&
|
||||
currentMsg!.msgID == targetMsg.msgID) {
|
||||
// find the target index by timestamp and msgID
|
||||
isFound = true;
|
||||
targetIndex = -i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (isFound && targetIndex != 1) {
|
||||
findingMsg = null;
|
||||
_autoScrollController.scrollToIndex(
|
||||
targetIndex,
|
||||
preferPosition: AutoScrollPosition.middle,
|
||||
);
|
||||
|
||||
// execute twice for accurate position, as the position located firstly can be wrong
|
||||
_autoScrollController.scrollToIndex(targetIndex, preferPosition: AutoScrollPosition.middle);
|
||||
_autoScrollController.scrollToIndex(targetIndex, preferPosition: AutoScrollPosition.middle);
|
||||
|
||||
widget.model.jumpMsgID = targetMsg.msgID!;
|
||||
loadingPlace = LoadingPlace.none;
|
||||
} else {
|
||||
showCantFindMsg();
|
||||
}
|
||||
} else {
|
||||
if (maybeHaveMoreMessageForFind) {
|
||||
// if the target message not in current message list, load more
|
||||
findingMsg = targetMsg;
|
||||
final lastMsgId = _getMessageId(widget.messageList.length - 1);
|
||||
maybeHaveMoreMessageForFind = await widget.onLoadMore(lastMsgId, LoadDirection.previous, singleLoadAmount);
|
||||
} else {
|
||||
showCantFindMsg();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_onScrollToIndexBySeq(String targetSeq) async {
|
||||
// This method called by tongue request jumping to target @ message
|
||||
loadingPlace = LoadingPlace.top;
|
||||
// const int singleLoadAmount = 40;
|
||||
final msgList = widget.messageList;
|
||||
String lastSeq = "";
|
||||
for (int i = msgList.length - 1; i >= 0; i--) {
|
||||
final currentMsg = msgList[i];
|
||||
if (currentMsg!.seq != null && currentMsg.seq != "") {
|
||||
lastSeq = currentMsg.seq!;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (int.parse(lastSeq) <= int.parse(targetSeq)) {
|
||||
bool isFound = false;
|
||||
int targetIndex = 1;
|
||||
String? targetMsgID = "";
|
||||
for (int i = msgList.length - 1; i >= 0; i--) {
|
||||
final currentMsg = msgList[i];
|
||||
if (currentMsg?.seq == targetSeq) {
|
||||
isFound = true;
|
||||
targetMsgID = currentMsg?.msgID;
|
||||
targetIndex = -i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (isFound && targetIndex != 1) {
|
||||
findingSeq = "";
|
||||
_autoScrollController.scrollToIndex(
|
||||
targetIndex,
|
||||
preferPosition: AutoScrollPosition.middle,
|
||||
);
|
||||
_autoScrollController.scrollToIndex(targetIndex, preferPosition: AutoScrollPosition.middle);
|
||||
if (targetMsgID != null && targetMsgID != "") {
|
||||
widget.model.jumpMsgID = targetMsgID;
|
||||
}
|
||||
loadingPlace = LoadingPlace.none;
|
||||
} else {
|
||||
showCantFindMsg();
|
||||
}
|
||||
} else {
|
||||
if (maybeHaveMoreMessageForFind) {
|
||||
findingSeq = targetSeq;
|
||||
int requestCount = int.parse(lastSeq) - int.parse(targetSeq);
|
||||
maybeHaveMoreMessageForFind = await widget.onLoadMore(
|
||||
_getMessageId(widget.messageList.length - 1), LoadDirection.previous, requestCount, int.parse(lastSeq));
|
||||
} else {
|
||||
showCantFindMsg();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_onScrollToIndexBegin(V2TimMessage targetMsg) {
|
||||
final lastTimestamp = widget.messageList[widget.messageList.length - 1]?.timestamp;
|
||||
final msgList = widget.messageList;
|
||||
final int targetTimeStamp = targetMsg.timestamp!;
|
||||
|
||||
if (targetTimeStamp >= lastTimestamp!) {
|
||||
bool isFound = false;
|
||||
int targetIndex = 1;
|
||||
for (int i = msgList.length - 1; i >= 0; i--) {
|
||||
final currentMsg = msgList[i];
|
||||
if (currentMsg?.timestamp == targetTimeStamp &&
|
||||
currentMsg?.elemType != 11 &&
|
||||
currentMsg!.msgID == targetMsg.msgID) {
|
||||
isFound = true;
|
||||
targetIndex = -i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (isFound && targetIndex != 1) {
|
||||
_autoScrollController.scrollToIndex(
|
||||
targetIndex,
|
||||
preferPosition: AutoScrollPosition.end,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<V2TimMessage?> _getReceivedMessageList(int receivedMessageListCount) {
|
||||
if (receivedMessageListCount == 0) {
|
||||
return [];
|
||||
}
|
||||
final haveTimeStampMessage = widget.messageList[receivedMessageListCount]?.elemType == 11;
|
||||
final endPoint = haveTimeStampMessage ? receivedMessageListCount + 1 : receivedMessageListCount;
|
||||
return widget.messageList.sublist(0, endPoint).reversed.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
// center key should generate everytime when build method called.
|
||||
final GlobalKey centerKey = GlobalKey();
|
||||
|
||||
final theme = value.theme;
|
||||
if (widget.messageList.isEmpty) {
|
||||
return Container();
|
||||
}
|
||||
|
||||
final messageList = widget.messageList;
|
||||
final globalModel = context.read<TUIChatGlobalModel>();
|
||||
final receivedNewMessageCount = globalModel.receivedNewMessageCount;
|
||||
final shouldShowUnreadMessage = receivedNewMessageCount > 0;
|
||||
final unreadMessageList = _getReceivedMessageList(receivedNewMessageCount);
|
||||
final readMessageList = messageList.sublist(unreadMessageList.length, messageList.length).toList();
|
||||
|
||||
final throttleFunction = OptimizeUtils.multiThrottle((index, LoadDirection direction) async {
|
||||
final msgID = TIMUIKitChatUtils.getMessageIDWithinIndex(readMessageList, index);
|
||||
await widget.onLoadMore(msgID, direction);
|
||||
}, 20);
|
||||
|
||||
final throttleFunctionWithMsgID = OptimizeUtils.multiThrottle((msgID, LoadDirection direction) async {
|
||||
await widget.onLoadMore(msgID, direction);
|
||||
}, 200);
|
||||
|
||||
if (findingMsg != null) {
|
||||
_onScrollToIndex(findingMsg!);
|
||||
} else if (findingSeq != "") {
|
||||
_onScrollToIndexBySeq(findingSeq);
|
||||
}
|
||||
|
||||
String getMessageIdentifier(V2TimMessage? message, int index) {
|
||||
return "${message?.msgID} - ${message?.timestamp} - ${message?.seq} -${message?.id ?? ""}";
|
||||
}
|
||||
|
||||
return Stack(
|
||||
alignment: Alignment.topCenter,
|
||||
children: [
|
||||
Scrollbar(
|
||||
controller: _autoScrollController,
|
||||
child: CustomScrollView(
|
||||
center: shouldShowUnreadMessage ? centerKey : null,
|
||||
key: widget.mainHistoryListConfig?.key,
|
||||
primary: widget.mainHistoryListConfig?.primary,
|
||||
physics: (widget.isAllowScroll == false)
|
||||
? const NeverScrollableScrollPhysics()
|
||||
: widget.mainHistoryListConfig?.physics,
|
||||
// padding: widget.mainHistoryListConfig?.padding ?? EdgeInsets.zero,
|
||||
// itemExtent: widget.mainHistoryListConfig?.itemExtent,
|
||||
// prototypeItem: widget.mainHistoryListConfig?.prototypeItem,
|
||||
cacheExtent: widget.mainHistoryListConfig?.cacheExtent ?? 1500,
|
||||
semanticChildCount: widget.mainHistoryListConfig?.semanticChildCount,
|
||||
dragStartBehavior: widget.mainHistoryListConfig?.dragStartBehavior ?? DragStartBehavior.start,
|
||||
keyboardDismissBehavior:
|
||||
widget.mainHistoryListConfig?.keyboardDismissBehavior ?? ScrollViewKeyboardDismissBehavior.manual,
|
||||
restorationId: widget.mainHistoryListConfig?.restorationId,
|
||||
clipBehavior: widget.mainHistoryListConfig?.clipBehavior ?? Clip.hardEdge,
|
||||
reverse: true,
|
||||
shrinkWrap: !shouldShowUnreadMessage,
|
||||
controller: _autoScrollController,
|
||||
slivers: [
|
||||
SliverPadding(
|
||||
padding: widget.mainHistoryListConfig?.padding ?? EdgeInsets.zero,
|
||||
sliver: SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(BuildContext context, int index) {
|
||||
final messageItem = unreadMessageList[index];
|
||||
if (index == unreadMessageList.length - 1 && widget.model.haveMoreLatestData == true) {
|
||||
throttleFunctionWithMsgID(messageItem?.msgID ?? "", LoadDirection.latest);
|
||||
}
|
||||
outputLogger.i(
|
||||
"Rendering a unread message: ${getMessageIdentifier(messageItem, 0)}, message Type: ${messageItem?.elemType}");
|
||||
return AutoScrollTag(
|
||||
controller: _autoScrollController,
|
||||
index: -index,
|
||||
key: ValueKey(getMessageIdentifier(messageItem, index)),
|
||||
highlightColor: Colors.black.withOpacity(0.1),
|
||||
child: KeepAliveWrapper(
|
||||
keepAlive: messageItem?.elemType == MessageElemType.V2TIM_ELEM_TYPE_SOUND,
|
||||
child: Container(child: _getMessageItemBuilder(messageItem))),
|
||||
);
|
||||
},
|
||||
childCount: unreadMessageList.length,
|
||||
findChildIndexCallback: (Key key) {
|
||||
final ValueKey<String> valueKey = key as ValueKey<String>;
|
||||
final String data = valueKey.value;
|
||||
final int index =
|
||||
unreadMessageList.indexWhere((element) => getMessageIdentifier(element, 0) == data);
|
||||
return index != -1 ? index : null;
|
||||
})),
|
||||
),
|
||||
SliverPadding(
|
||||
padding: EdgeInsets.zero,
|
||||
key: centerKey,
|
||||
),
|
||||
SliverPadding(
|
||||
padding: widget.mainHistoryListConfig?.padding ?? EdgeInsets.zero,
|
||||
sliver: Selector<TUIChatSeparateViewModel, bool>(
|
||||
selector: (context, model) {
|
||||
return model.haveMoreData;
|
||||
},
|
||||
shouldRebuild: (previous, next) {
|
||||
return previous != next;
|
||||
},
|
||||
builder: (context, haveMoreData, child) {
|
||||
return SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(BuildContext context, int index) {
|
||||
final messageItem = readMessageList[index];
|
||||
if (index == readMessageList.length - 1) {
|
||||
if (haveMoreData) {
|
||||
final lastMessage = globalModel
|
||||
.messageListMap[TencentUtils.checkString(widget.conversation.groupID) ??
|
||||
widget.conversation.userID ??
|
||||
widget.conversation.conversationID]
|
||||
?.last;
|
||||
if (lastMessage != null) {
|
||||
throttleFunctionWithMsgID(lastMessage.msgID ?? "", LoadDirection.previous);
|
||||
} else {
|
||||
throttleFunction(index, messageList);
|
||||
}
|
||||
return Column(
|
||||
children: [
|
||||
LoadingAnimationWidget.staggeredDotsWave(
|
||||
color: theme.weakTextColor ?? Colors.grey,
|
||||
size: 28,
|
||||
),
|
||||
AutoScrollTag(
|
||||
controller: _autoScrollController,
|
||||
index: -index,
|
||||
key: ValueKey(getMessageIdentifier(messageItem, index)),
|
||||
highlightColor: Colors.black.withOpacity(0.1),
|
||||
child: KeepAliveWrapper(
|
||||
keepAlive: messageItem?.elemType == MessageElemType.V2TIM_ELEM_TYPE_SOUND,
|
||||
child: Container(child: _getMessageItemBuilder(messageItem))),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
if (index == 0 &&
|
||||
widget.model.haveMoreLatestData == true &&
|
||||
globalModel.receivedNewMessageCount < 10) {
|
||||
throttleFunction(index, LoadDirection.latest);
|
||||
}
|
||||
outputLogger.i(
|
||||
"Rendering a read message: ${getMessageIdentifier(messageItem, 0)}, message Type: ${messageItem?.elemType}");
|
||||
return AutoScrollTag(
|
||||
controller: _autoScrollController,
|
||||
index: -index,
|
||||
key: ValueKey(getMessageIdentifier(messageItem, index)),
|
||||
highlightColor: Colors.black.withOpacity(0.1),
|
||||
child: KeepAliveWrapper(
|
||||
keepAlive: messageItem?.elemType == MessageElemType.V2TIM_ELEM_TYPE_SOUND,
|
||||
child: Container(child: _getMessageItemBuilder(messageItem))),
|
||||
);
|
||||
},
|
||||
childCount: readMessageList.length,
|
||||
findChildIndexCallback: (Key key) {
|
||||
final ValueKey<String> valueKey = key as ValueKey<String>;
|
||||
final String data = valueKey.value;
|
||||
final int index =
|
||||
readMessageList.indexWhere((element) => getMessageIdentifier(element, 0) == data);
|
||||
return index > -1 ? index : null;
|
||||
}));
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
)),
|
||||
TIMUIKitHistoryMessageListTongueContainer(
|
||||
conversation: widget.conversation,
|
||||
model: widget.model,
|
||||
scrollController: _autoScrollController,
|
||||
scrollToIndexBySeq: _onScrollToIndexBySeq,
|
||||
groupAtInfoList: widget.groupAtInfoList,
|
||||
tongueItemBuilder: widget.tongueItemBuilder,
|
||||
),
|
||||
if (loadingPlace == LoadingPlace.top)
|
||||
Positioned(
|
||||
top: 8,
|
||||
child: LoadingAnimationWidget.staggeredDotsWave(
|
||||
color: theme.weakTextColor ?? Colors.grey,
|
||||
size: 28,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TIMUIKitHistoryMessageListSelector extends TIMUIKitStatelessWidget {
|
||||
final Widget Function(BuildContext, List<V2TimMessage?>, Widget?) builder;
|
||||
final String conversationID;
|
||||
|
||||
TIMUIKitHistoryMessageListSelector({Key? key, required this.builder, required this.conversationID}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
return Selector<TUIChatGlobalModel, List<V2TimMessage?>>(
|
||||
builder: builder,
|
||||
shouldRebuild: (previous, next) {
|
||||
final isEquals = const DeepCollectionEquality.unordered().equals(previous, next);
|
||||
return !isEquals;
|
||||
},
|
||||
selector: (context, model) {
|
||||
final messageList = model.getMessageList(conversationID) ?? [];
|
||||
return messageList;
|
||||
});
|
||||
}
|
||||
}
|
||||
+171
@@ -0,0 +1,171 @@
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
// The definition of the following parameters is the same with [ListView.builder]
|
||||
class TIMUIKitHistoryMessageListConfig {
|
||||
Key? key;
|
||||
|
||||
/// {@template flutter.widgets.scroll_view.scrollDirection}
|
||||
/// The axis along which the scroll view scrolls.
|
||||
///
|
||||
/// Defaults to [Axis.vertical].
|
||||
/// {@endtemplate}
|
||||
Axis? scrollDirection;
|
||||
|
||||
/// {@template flutter.widgets.scroll_view.primary}
|
||||
/// Whether this is the primary scroll view associated with the parent
|
||||
/// [PrimaryScrollController].
|
||||
///
|
||||
/// When this is true, the scroll view is scrollable even if it does not have
|
||||
/// sufficient content to actually scroll. Otherwise, by default the user can
|
||||
/// only scroll the view if it has sufficient content. See [physics].
|
||||
///
|
||||
/// Also when true, the scroll view is used for default [ScrollAction]s. If a
|
||||
/// ScrollAction is not handled by an otherwise focused part of the application,
|
||||
/// the ScrollAction will be evaluated using this scroll view, for example,
|
||||
/// when executing [Shortcuts] key events like page up and down.
|
||||
///
|
||||
/// On iOS, this also identifies the scroll view that will scroll to top in
|
||||
/// response to a tap in the status bar.
|
||||
/// {@endtemplate}
|
||||
///
|
||||
/// Defaults to true when [scrollDirection] is [Axis.vertical] and
|
||||
/// [controller] is null.
|
||||
bool? primary;
|
||||
|
||||
/// {@template flutter.widgets.scroll_view.physics}
|
||||
/// How the scroll view should respond to user input.
|
||||
///
|
||||
/// For example, determines how the scroll view continues to animate after the
|
||||
/// user stops dragging the scroll view.
|
||||
///
|
||||
/// Defaults to matching platform conventions. Furthermore, if [primary] is
|
||||
/// false, then the user cannot scroll if there is insufficient content to
|
||||
/// scroll, while if [primary] is true, they can always attempt to scroll.
|
||||
///
|
||||
/// To force the scroll view to always be scrollable even if there is
|
||||
/// insufficient content, as if [primary] was true but without necessarily
|
||||
/// setting it to true, provide an [AlwaysScrollableScrollPhysics] physics
|
||||
/// object, as in:
|
||||
///
|
||||
/// ```dart
|
||||
/// physics: const AlwaysScrollableScrollPhysics(),
|
||||
/// ```
|
||||
///
|
||||
/// To force the scroll view to use the default platform conventions and not
|
||||
/// be scrollable if there is insufficient content, regardless of the value of
|
||||
/// [primary], provide an explicit [ScrollPhysics] object, as in:
|
||||
///
|
||||
/// ```dart
|
||||
/// physics: const ScrollPhysics(),
|
||||
/// ```
|
||||
///
|
||||
/// The physics can be changed dynamically (by providing a new object in a
|
||||
/// subsequent build), but new physics will only take effect if the _class_ of
|
||||
/// the provided object changes. Merely constructing a new instance with a
|
||||
/// different configuration is insufficient to cause the physics to be
|
||||
/// reapplied. (This is because the final object used is generated
|
||||
/// dynamically, which can be relatively expensive, and it would be
|
||||
/// inefficient to speculatively create this object each frame to see if the
|
||||
/// physics should be updated.)
|
||||
/// {@endtemplate}
|
||||
///
|
||||
/// If an explicit [ScrollBehavior] is provided to [scrollBehavior], the
|
||||
/// [ScrollPhysics] provided by that behavior will take precedence after
|
||||
/// [physics].
|
||||
ScrollPhysics? physics;
|
||||
|
||||
/// The amount of space by which to inset the children.
|
||||
EdgeInsetsGeometry? padding;
|
||||
|
||||
/// {@template flutter.widgets.list_view.itemExtent}
|
||||
/// If non-null, forces the children to have the given extent in the scroll
|
||||
/// direction.
|
||||
///
|
||||
/// Specifying an [itemExtent] is more efficient than letting the children
|
||||
/// determine their own extent because the scrolling machinery can make use of
|
||||
/// the foreknowledge of the children's extent to save work, for example when
|
||||
/// the scroll position changes drastically.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [SliverFixedExtentList], the sliver used internally when this property
|
||||
/// is provided. It constrains its box children to have a specific given
|
||||
/// extent along the main axis.
|
||||
/// * The [prototypeItem] property, which allows forcing the children's
|
||||
/// extent to be the same as the given widget.
|
||||
/// {@endtemplate}
|
||||
double? itemExtent;
|
||||
|
||||
/// {@template flutter.widgets.list_view.prototypeItem}
|
||||
/// If non-null, forces the children to have the same extent as the given
|
||||
/// widget in the scroll direction.
|
||||
///
|
||||
/// Specifying an [prototypeItem] is more efficient than letting the children
|
||||
/// determine their own extent because the scrolling machinery can make use of
|
||||
/// the foreknowledge of the children's extent to save work, for example when
|
||||
/// the scroll position changes drastically.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [SliverPrototypeExtentList], the sliver used internally when this
|
||||
/// property is provided. It constrains its box children to have the same
|
||||
/// extent as a prototype item along the main axis.
|
||||
/// * The [itemExtent] property, which allows forcing the children's extent
|
||||
/// to a given value.
|
||||
/// {@endtemplate}
|
||||
Widget? prototypeItem;
|
||||
|
||||
/// {@macro flutter.rendering.RenderViewportBase.cacheExtent}
|
||||
double? cacheExtent;
|
||||
|
||||
/// The number of children that will contribute semantic information.
|
||||
///
|
||||
/// Some subtypes of [ScrollView] can infer this value automatically. For
|
||||
/// example [ListView] will use the number of widgets in the child list,
|
||||
/// while the [ListView.separated] constructor will use half that amount.
|
||||
///
|
||||
/// For [CustomScrollView] and other types which do not receive a builder
|
||||
/// or list of widgets, the child count must be explicitly provided. If the
|
||||
/// number is unknown or unbounded this should be left unset or set to null.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [SemanticsConfiguration.scrollChildCount], the corresponding semantics property.
|
||||
int? semanticChildCount;
|
||||
|
||||
/// {@macro flutter.widgets.scrollable.dragStartBehavior}
|
||||
DragStartBehavior? dragStartBehavior;
|
||||
|
||||
/// {@template flutter.widgets.scroll_view.keyboardDismissBehavior}
|
||||
/// [ScrollViewKeyboardDismissBehavior] the defines how this [ScrollView] will
|
||||
/// dismiss the keyboard automatically.
|
||||
/// {@endtemplate}
|
||||
ScrollViewKeyboardDismissBehavior? keyboardDismissBehavior;
|
||||
|
||||
/// {@macro flutter.widgets.scrollable.restorationId}
|
||||
String? restorationId;
|
||||
|
||||
/// {@macro flutter.material.Material.clipBehavior}
|
||||
///
|
||||
/// Defaults to [Clip.hardEdge].
|
||||
Clip? clipBehavior;
|
||||
|
||||
bool? shrinkWrap;
|
||||
|
||||
TIMUIKitHistoryMessageListConfig(
|
||||
{this.key,
|
||||
this.scrollDirection,
|
||||
this.primary,
|
||||
this.physics,
|
||||
this.padding,
|
||||
this.itemExtent,
|
||||
this.prototypeItem,
|
||||
this.cacheExtent,
|
||||
this.semanticChildCount,
|
||||
this.dragStartBehavior,
|
||||
this.keyboardDismissBehavior,
|
||||
this.restorationId,
|
||||
this.shrinkWrap,
|
||||
this.clipBehavior});
|
||||
}
|
||||
+1460
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,620 @@
|
||||
// ignore_for_file: non_constant_identifier_names, avoid_print
|
||||
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:open_file/open_file.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:tencent_chat_i18n_tool/tencent_chat_i18n_tool.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/enum/group_member_role.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_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_message.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_message.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_self_info_view_model.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/utils/common_utils.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/message.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitMessageItem/TIMUIKitMessageReaction/tim_uikit_message_reaction_select_emoji.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/business_logic/separate_models/tui_chat_separate_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_chat_global_model.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/TIMUIKitChat/TIMUIKItMessageList/tim_uikit_chat_history_message_list_item.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/forward_message_screen.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
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.dart';
|
||||
|
||||
class TIMUIKitMessageTooltip extends StatefulWidget {
|
||||
/// tool tips panel configuration, long press message will show tool tips panel
|
||||
final ToolTipsConfig? toolTipsConfig;
|
||||
|
||||
/// current message
|
||||
final V2TimMessage message;
|
||||
|
||||
/// allow notifi user when send reply message
|
||||
final bool allowAtUserWhenReply;
|
||||
|
||||
/// the callback for long press event, except myself avatar
|
||||
final Function(String? userId, String? nickName)? onLongPressForOthersHeadPortrait;
|
||||
|
||||
final bool isUseMessageReaction;
|
||||
|
||||
/// direction
|
||||
final SelectEmojiPanelPosition selectEmojiPanelPosition;
|
||||
|
||||
/// on add sticker reaction to a message
|
||||
final ValueChanged<int> onSelectSticker;
|
||||
|
||||
/// on close tooltip area
|
||||
final VoidCallback onCloseTooltip;
|
||||
|
||||
final TUIChatSeparateViewModel model;
|
||||
|
||||
final bool isShowMoreSticker;
|
||||
|
||||
final V2TimGroupMemberFullInfo? groupMemberInfo;
|
||||
|
||||
final bool iSUseDefaultHoverBar;
|
||||
|
||||
const TIMUIKitMessageTooltip(
|
||||
{Key? key,
|
||||
this.toolTipsConfig,
|
||||
this.isUseMessageReaction = true,
|
||||
required this.model,
|
||||
required this.message,
|
||||
required this.allowAtUserWhenReply,
|
||||
this.onLongPressForOthersHeadPortrait,
|
||||
required this.selectEmojiPanelPosition,
|
||||
required this.onCloseTooltip,
|
||||
required this.onSelectSticker,
|
||||
this.isShowMoreSticker = false,
|
||||
this.groupMemberInfo,
|
||||
required this.iSUseDefaultHoverBar})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => TIMUIKitMessageTooltipState();
|
||||
}
|
||||
|
||||
class TIMUIKitMessageTooltipState extends TIMUIKitState<TIMUIKitMessageTooltip> {
|
||||
final TUIChatGlobalModel globalModal = serviceLocator<TUIChatGlobalModel>();
|
||||
final TUISelfInfoViewModel selfInfoViewModel = serviceLocator<TUISelfInfoViewModel>();
|
||||
bool isShowMoreSticker = false;
|
||||
bool fileBeenDownloaded = false;
|
||||
String filePath = "";
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
hasFile();
|
||||
isShowMoreSticker = widget.isShowMoreSticker;
|
||||
}
|
||||
|
||||
hasFile() {
|
||||
if (PlatformUtils().isMobile ||
|
||||
(widget.message.fileElem == null && widget.message.imageElem == null && widget.message.videoElem == null)) {
|
||||
fileBeenDownloaded = false;
|
||||
return;
|
||||
}
|
||||
if (PlatformUtils().isWeb) {
|
||||
fileBeenDownloaded = true;
|
||||
return;
|
||||
}
|
||||
if (PlatformUtils().isDesktop) {
|
||||
if (widget.message.fileElem != null) {
|
||||
String savePath = TencentUtils.checkString(globalModal.getFileMessageLocation(widget.message.msgID)) ??
|
||||
TencentUtils.checkString(widget.message.fileElem!.localUrl) ??
|
||||
widget.message.fileElem?.path ??
|
||||
"";
|
||||
File f = File(savePath);
|
||||
if (f.existsSync() && widget.message.msgID != null) {
|
||||
filePath = savePath;
|
||||
fileBeenDownloaded = true;
|
||||
return;
|
||||
}
|
||||
} else if (widget.message.imageElem != null) {
|
||||
if (TencentUtils.checkString(widget.message.imageElem!.imageList![0]!.localUrl) != null &&
|
||||
File(widget.message.imageElem!.imageList![0]!.localUrl!).existsSync()) {
|
||||
fileBeenDownloaded = true;
|
||||
return;
|
||||
}
|
||||
} else if (widget.message.videoElem != null) {
|
||||
if (TencentUtils.checkString(widget.message.videoElem!.localVideoUrl) != null &&
|
||||
File(widget.message.videoElem!.localVideoUrl!).existsSync()) {
|
||||
fileBeenDownloaded = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
fileBeenDownloaded = false;
|
||||
}
|
||||
|
||||
bool isRevocable(int timestamp, int upperTimeLimit) =>
|
||||
((DateTime.now().millisecondsSinceEpoch / 1000).ceil() - timestamp < upperTimeLimit) &&
|
||||
(widget.message.isSelf ?? true);
|
||||
|
||||
Widget ItemInkWell({
|
||||
Widget? child,
|
||||
GestureTapCallback? onTap,
|
||||
}) {
|
||||
return SizedBox(
|
||||
width: 44,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
splashColor: Colors.white,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(bottom: 6, top: 6),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
bool isAdminCanRecall() {
|
||||
if (widget.model.chatConfig.isGroupAdminRecallEnabled) {
|
||||
final selfMemberInfo = widget.groupMemberInfo ?? widget.model.selfMemberInfo;
|
||||
final selfRole = selfMemberInfo?.role;
|
||||
return selfRole == GroupMemberRoleType.V2TIM_GROUP_MEMBER_ROLE_ADMIN ||
|
||||
selfRole == GroupMemberRoleType.V2TIM_GROUP_MEMBER_ROLE_OWNER;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
_buildLongPressTipItem(TUITheme theme, TUIChatSeparateViewModel model, V2TimMessage message) {
|
||||
final isDesktopScreen = TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop;
|
||||
final isCanRevokeSelf = isRevocable(widget.message.timestamp!, model.chatConfig.upperRecallTime);
|
||||
final shouldShowRevokeAction =
|
||||
(isCanRevokeSelf || isAdminCanRecall()) && widget.message.status != MessageStatus.V2TIM_MSG_STATUS_SEND_FAIL;
|
||||
final shouldShowReplyAction =
|
||||
!(widget.message.customElem?.data != null && MessageUtils.isCallingData(widget.message.customElem!.data!));
|
||||
final shouldShowForwardAction =
|
||||
!(widget.message.customElem?.data != null && MessageUtils.isCallingData(widget.message.customElem!.data!));
|
||||
final tooltipsConfig = widget.toolTipsConfig;
|
||||
final messageCanCopy = widget.message.elemType == MessageElemType.V2TIM_ELEM_TYPE_TEXT ||
|
||||
(isDesktopScreen && widget.message.elemType == MessageElemType.V2TIM_ELEM_TYPE_IMAGE && fileBeenDownloaded);
|
||||
bool showTranslation = true;
|
||||
if (widget.message.localCustomData != null) {
|
||||
final LocalCustomDataModel localCustomData =
|
||||
LocalCustomDataModel.fromMap(json.decode(TencentUtils.checkString(widget.message.localCustomData) ?? "{}"));
|
||||
if (localCustomData.translatedText != null && localCustomData.translatedText != "") {
|
||||
showTranslation = false;
|
||||
}
|
||||
}
|
||||
|
||||
final dynamicQuote = model.chatConfig.isAtWhenReplyDynamic?.call(widget.message);
|
||||
|
||||
final List<MessageToolTipItem> defaultTipsList = [
|
||||
if (fileBeenDownloaded)
|
||||
MessageToolTipItem(
|
||||
label: TIM_t("打开"),
|
||||
id: "open",
|
||||
iconImageAsset: "images/open_in_new.png",
|
||||
onClick: () => _onTap("open", model)),
|
||||
if (fileBeenDownloaded && PlatformUtils().isDesktop)
|
||||
MessageToolTipItem(
|
||||
label: PlatformUtils().isMacOS ? TIM_t("在访达中打开") : TIM_t("查看文件夹"),
|
||||
id: "finder",
|
||||
iconImageAsset: "images/folder_open.png",
|
||||
onClick: () => _onTap("finder", model)),
|
||||
if (messageCanCopy)
|
||||
MessageToolTipItem(
|
||||
label: TIM_t("复制"),
|
||||
id: "copyMessage",
|
||||
iconImageAsset: "images/copy_message.png",
|
||||
onClick: () => _onTap("copyMessage", model)),
|
||||
if (shouldShowForwardAction && !model.isVoteMessage(widget.message))
|
||||
MessageToolTipItem(
|
||||
label: TIM_t("转发"),
|
||||
id: "forwardMessage",
|
||||
iconImageAsset: "images/forward_message.png",
|
||||
onClick: () => _onTap("forwardMessage", model)),
|
||||
if (shouldShowReplyAction)
|
||||
MessageToolTipItem(
|
||||
label: TIM_t((dynamicQuote ?? model.chatConfig.isAtWhenReply) ? "回复" : "引用"),
|
||||
id: "replyMessage",
|
||||
iconImageAsset: "images/reply_message.png",
|
||||
onClick: () => _onTap("replyMessage", model)),
|
||||
MessageToolTipItem(
|
||||
label: TIM_t("多选"),
|
||||
id: "multiSelect",
|
||||
iconImageAsset: "images/multi_message.png",
|
||||
onClick: () => _onTap("multiSelect", model)),
|
||||
MessageToolTipItem(
|
||||
label: TIM_t("删除"),
|
||||
id: "delete",
|
||||
iconImageAsset: "images/delete_message.png",
|
||||
onClick: () => _onTap("delete", model)),
|
||||
if (showTranslation)
|
||||
MessageToolTipItem(
|
||||
label: TIM_t("翻译"),
|
||||
id: "translate",
|
||||
iconImageAsset: "images/translate.png",
|
||||
onClick: () => _onTap("translate", model)),
|
||||
if (shouldShowRevokeAction)
|
||||
MessageToolTipItem(
|
||||
label: TIM_t("撤回"),
|
||||
id: "revoke",
|
||||
iconImageAsset: "images/revoke_message.png",
|
||||
onClick: () => _onTap("revoke", model)),
|
||||
];
|
||||
final defaultTipsIds = defaultTipsList.map((e) => e.id);
|
||||
List<MessageToolTipItem> defaultFormattedTipsList = defaultTipsList;
|
||||
if (tooltipsConfig != null) {
|
||||
defaultFormattedTipsList = defaultTipsList.where((element) {
|
||||
final type = element.id;
|
||||
if (type == "copyMessage") {
|
||||
return tooltipsConfig.showCopyMessage;
|
||||
}
|
||||
if (type == "forwardMessage") {
|
||||
return tooltipsConfig.showForwardMessage && !(isDesktopScreen && widget.iSUseDefaultHoverBar);
|
||||
}
|
||||
if (type == "replyMessage") {
|
||||
return tooltipsConfig.showReplyMessage && !(isDesktopScreen && widget.iSUseDefaultHoverBar);
|
||||
}
|
||||
if (type == "delete") {
|
||||
return (!PlatformUtils().isWeb) && tooltipsConfig.showDeleteMessage;
|
||||
}
|
||||
if (type == "multiSelect") {
|
||||
return tooltipsConfig.showMultipleChoiceMessage;
|
||||
}
|
||||
|
||||
if (type == "revoke") {
|
||||
return tooltipsConfig.showRecallMessage;
|
||||
}
|
||||
if (type == "translate") {
|
||||
return tooltipsConfig.showTranslation && widget.message.elemType == MessageElemType.V2TIM_ELEM_TYPE_TEXT;
|
||||
}
|
||||
return true;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
final List<MessageToolTipItem>? customList = widget.toolTipsConfig?.additionalMessageToolTips != null
|
||||
? (widget.toolTipsConfig?.additionalMessageToolTips!(message, widget.onCloseTooltip))
|
||||
: [];
|
||||
|
||||
List<MessageToolTipItem> formattedTipsList = [
|
||||
...defaultFormattedTipsList,
|
||||
...?customList,
|
||||
];
|
||||
|
||||
List<dynamic> widgetList = [];
|
||||
if (isDesktopScreen) {
|
||||
widgetList = formattedTipsList
|
||||
.map(
|
||||
(item) => Material(
|
||||
color: Colors.white,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
item.onClick();
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(6),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Image.asset(
|
||||
item.iconImageAsset,
|
||||
package: defaultTipsIds.contains(item.id) ? 'tencent_cloud_chat_uikit' : null,
|
||||
width: 20,
|
||||
height: 20,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 4,
|
||||
width: 8,
|
||||
),
|
||||
Text(
|
||||
item.label,
|
||||
style: TextStyle(
|
||||
decoration: TextDecoration.none,
|
||||
color: theme.darkTextColor,
|
||||
fontSize: 12,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
} else {
|
||||
widgetList = formattedTipsList
|
||||
.map(
|
||||
(item) => Material(
|
||||
color: Colors.white,
|
||||
child: ItemInkWell(
|
||||
onTap: () {
|
||||
item.onClick();
|
||||
},
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Image.asset(
|
||||
item.iconImageAsset,
|
||||
package: defaultTipsIds.contains(item.id) ? 'tencent_cloud_chat_uikit' : null,
|
||||
width: 20,
|
||||
height: 20,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 4,
|
||||
width: 60,
|
||||
),
|
||||
Text(
|
||||
item.label,
|
||||
style: TextStyle(
|
||||
decoration: TextDecoration.none,
|
||||
color: theme.darkTextColor,
|
||||
fontSize: 10,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
if (widgetList.isEmpty && widget.isUseMessageReaction == false) {
|
||||
widget.onCloseTooltip();
|
||||
}
|
||||
|
||||
return widgetList;
|
||||
}
|
||||
|
||||
_onOpenDesktop(String path) {
|
||||
try {
|
||||
if (PlatformUtils().isDesktop && !PlatformUtils().isWindows) {
|
||||
launchUrl(Uri.file(path));
|
||||
} else {
|
||||
OpenFile.open(path);
|
||||
}
|
||||
// ignore: empty_catches
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
_onTap(String operation, TUIChatSeparateViewModel model) async {
|
||||
final messageItem = widget.message;
|
||||
final msgID = messageItem.msgID as String;
|
||||
switch (operation) {
|
||||
case "open":
|
||||
if (widget.message.fileElem != null) {
|
||||
_onOpenDesktop(TencentUtils.checkString(globalModal.getFileMessageLocation(widget.message.msgID)) ??
|
||||
TencentUtils.checkString(widget.message.fileElem!.localUrl) ??
|
||||
widget.message.fileElem?.path ??
|
||||
"");
|
||||
} else if (widget.message.imageElem != null) {
|
||||
_onOpenDesktop(TencentUtils.checkString(widget.message.imageElem!.imageList?[0]?.localUrl) ??
|
||||
TencentUtils.checkString(widget.message.imageElem?.path) ??
|
||||
"");
|
||||
} else if (widget.message.videoElem != null) {
|
||||
_onOpenDesktop(TencentUtils.checkString(widget.message.videoElem!.localVideoUrl) ??
|
||||
TencentUtils.checkString(widget.message.videoElem?.videoPath) ??
|
||||
"");
|
||||
}
|
||||
break;
|
||||
case "finder":
|
||||
String savePath = "";
|
||||
if (widget.message.fileElem != null) {
|
||||
savePath = (TencentUtils.checkString(globalModal.getFileMessageLocation(widget.message.msgID)) ??
|
||||
TencentUtils.checkString(widget.message.fileElem!.localUrl) ??
|
||||
widget.message.fileElem?.path ??
|
||||
"");
|
||||
} else if (widget.message.imageElem != null) {
|
||||
savePath = (TencentUtils.checkString(widget.message.imageElem!.imageList?[0]?.localUrl) ??
|
||||
TencentUtils.checkString(widget.message.imageElem?.path) ??
|
||||
"");
|
||||
} else if (widget.message.videoElem != null) {
|
||||
savePath = (TencentUtils.checkString(widget.message.videoElem!.localVideoUrl) ??
|
||||
TencentUtils.checkString(widget.message.videoElem?.videoPath) ??
|
||||
"");
|
||||
}
|
||||
final String fileDir = path.dirname(savePath);
|
||||
_onOpenDesktop(fileDir);
|
||||
break;
|
||||
case "delete":
|
||||
model.deleteMsg(msgID, webMessageInstance: messageItem.messageFromWeb);
|
||||
break;
|
||||
case "revoke":
|
||||
model.revokeMsg(msgID, !isRevocable(widget.message.timestamp!, model.chatConfig.upperRecallTime),
|
||||
messageItem.messageFromWeb);
|
||||
break;
|
||||
case 'translate':
|
||||
model.translateText(widget.message);
|
||||
break;
|
||||
case "multiSelect":
|
||||
model.updateMultiSelectStatus(true);
|
||||
model.setMessageItemChecked(widget.message, true);
|
||||
break;
|
||||
case "forwardMessage":
|
||||
model.setMessageItemChecked(widget.message, true);
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => ForwardMessageScreen(
|
||||
conversationType: ConvType.c2c,
|
||||
model: model,
|
||||
)));
|
||||
break;
|
||||
case "copyMessage":
|
||||
if (widget.message.elemType == MessageElemType.V2TIM_ELEM_TYPE_TEXT) {
|
||||
try {
|
||||
await Clipboard.setData(ClipboardData(text: widget.message.textElem?.text ?? ""));
|
||||
onTIMCallback(TIMCallback(type: TIMCallbackType.INFO, infoRecommendText: TIM_t("已复制"), infoCode: 6660408));
|
||||
// ignore: empty_catches
|
||||
} catch (e) {}
|
||||
}
|
||||
break;
|
||||
case "replyMessage":
|
||||
model.repliedMessage = widget.message;
|
||||
final dynamicQuote = model.chatConfig.isAtWhenReplyDynamic?.call(widget.message);
|
||||
final isSelf = widget.message.isSelf ?? true;
|
||||
final isGroup = TencentUtils.checkString(widget.message.groupID) != null;
|
||||
final isAtWhenReply = !isSelf &&
|
||||
isGroup &&
|
||||
(dynamicQuote ?? widget.allowAtUserWhenReply) &&
|
||||
widget.onLongPressForOthersHeadPortrait != null;
|
||||
|
||||
/// If replying to a self message, do not add a at tag, only requestFocus.
|
||||
widget.onLongPressForOthersHeadPortrait!(
|
||||
!isAtWhenReply ? null : widget.message.sender, !isAtWhenReply ? null : widget.message.nickName);
|
||||
break;
|
||||
default:
|
||||
onTIMCallback(TIMCallback(type: TIMCallbackType.INFO, infoRecommendText: TIM_t("暂未实现"), infoCode: 6660409));
|
||||
}
|
||||
widget.onCloseTooltip();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final TUITheme theme = value.theme;
|
||||
final isDesktopScreen = TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop;
|
||||
return MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider.value(value: widget.model),
|
||||
],
|
||||
builder: (BuildContext context, Widget? w) {
|
||||
final TUIChatSeparateViewModel model = Provider.of<TUIChatSeparateViewModel>(context);
|
||||
final bool haveExtraTipsConfig =
|
||||
widget.toolTipsConfig != null && widget.toolTipsConfig?.additionalItemBuilder != null;
|
||||
Widget? extraTipsActionItem = haveExtraTipsConfig
|
||||
? widget.toolTipsConfig!.additionalItemBuilder!(widget.message, widget.onCloseTooltip, null, context)
|
||||
: null;
|
||||
final message = widget.message;
|
||||
return Container(
|
||||
decoration: isDesktopScreen
|
||||
? BoxDecoration(
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0xCCbebebe),
|
||||
offset: Offset(2, 2),
|
||||
blurRadius: 10,
|
||||
spreadRadius: 1,
|
||||
),
|
||||
],
|
||||
border: Border.all(
|
||||
width: 1,
|
||||
color: hexToColor("dee0e3"),
|
||||
),
|
||||
color: Colors.white,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(10)),
|
||||
)
|
||||
: null,
|
||||
color: isDesktopScreen ? null : Colors.white,
|
||||
padding: EdgeInsets.symmetric(horizontal: 8, vertical: isDesktopScreen ? 8 : 4),
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: min(MediaQuery.of(context).size.width * 0.75, 350),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if ((!isDesktopScreen || widget.isShowMoreSticker) &&
|
||||
widget.isUseMessageReaction &&
|
||||
widget.selectEmojiPanelPosition == SelectEmojiPanelPosition.up)
|
||||
TIMUIKitMessageReactionEmojiSelectPanel(
|
||||
isShowMoreSticker: isShowMoreSticker,
|
||||
onSelect: (int value) => widget.onSelectSticker(value),
|
||||
onClickShowMore: (bool value) {
|
||||
setState(() {
|
||||
isShowMoreSticker = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
if (!isDesktopScreen &&
|
||||
widget.isUseMessageReaction &&
|
||||
widget.selectEmojiPanelPosition == SelectEmojiPanelPosition.up &&
|
||||
isShowMoreSticker == false)
|
||||
Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 6),
|
||||
child: const Divider(
|
||||
thickness: 1,
|
||||
indent: 0,
|
||||
// endIndent: 10,
|
||||
color: Colors.black12)),
|
||||
if (isShowMoreSticker == false)
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (!isDesktopScreen && widget.isUseMessageReaction)
|
||||
Expanded(
|
||||
child: Wrap(
|
||||
direction: Axis.horizontal,
|
||||
alignment: TUIKitScreenUtils.getFormFactor(context) == DeviceType.Mobile
|
||||
? WrapAlignment.start
|
||||
: WrapAlignment.start,
|
||||
spacing: 12,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
..._buildLongPressTipItem(theme, model, message),
|
||||
if (extraTipsActionItem != null) extraTipsActionItem
|
||||
],
|
||||
)),
|
||||
if (!isDesktopScreen && !widget.isUseMessageReaction)
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: min(MediaQuery.of(context).size.width * 0.75, 350),
|
||||
),
|
||||
child: Wrap(
|
||||
direction: Axis.horizontal,
|
||||
alignment: TUIKitScreenUtils.getFormFactor(context) == DeviceType.Mobile
|
||||
? WrapAlignment.spaceBetween
|
||||
: WrapAlignment.start,
|
||||
spacing: 4,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
..._buildLongPressTipItem(theme, model, message),
|
||||
if (extraTipsActionItem != null) extraTipsActionItem
|
||||
],
|
||||
),
|
||||
),
|
||||
if (isDesktopScreen)
|
||||
Table(columnWidths: const <int, TableColumnWidth>{
|
||||
0: IntrinsicColumnWidth(),
|
||||
}, children: <TableRow>[
|
||||
..._buildLongPressTipItem(theme, model, message).map((e) => TableRow(children: <Widget>[e]))
|
||||
])
|
||||
],
|
||||
),
|
||||
if (!isDesktopScreen &&
|
||||
widget.isUseMessageReaction &&
|
||||
widget.selectEmojiPanelPosition == SelectEmojiPanelPosition.down &&
|
||||
isShowMoreSticker == false)
|
||||
Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 6),
|
||||
child: const Divider(
|
||||
thickness: 1,
|
||||
indent: 0,
|
||||
// endIndent: 10,
|
||||
color: Colors.black12)),
|
||||
if ((!isDesktopScreen || widget.isShowMoreSticker) &&
|
||||
widget.isUseMessageReaction &&
|
||||
widget.selectEmojiPanelPosition == SelectEmojiPanelPosition.down)
|
||||
TIMUIKitMessageReactionEmojiSelectPanel(
|
||||
isShowMoreSticker: isShowMoreSticker,
|
||||
onSelect: (int value) => widget.onSelectSticker(value),
|
||||
onClickShowMore: (bool value) {
|
||||
setState(() {
|
||||
isShowMoreSticker = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
));
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
+203
@@ -0,0 +1,203 @@
|
||||
// ignore_for_file: deprecated_member_use_from_same_package
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:scroll_to_index/scroll_to_index.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_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_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_message.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_message.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitTextField/tim_uikit_text_field_controller.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_chat_separate_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_chat_global_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/constants/history_message_constant.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKItMessageList/TIMUIKitTongue/tim_uikit_chat_history_message_list_tongue.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKItMessageList/tim_uikit_chat_history_message_list_config.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKItMessageList/tim_uikit_chat_history_message_list_item.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKItMessageList/tim_uikit_chat_history_message_list.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/tim_uikit_chat_config.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart';
|
||||
import 'package:tim_ui_kit_sticker_plugin/utils/tim_custom_face_data.dart';
|
||||
|
||||
enum LoadingPlace {
|
||||
none,
|
||||
top,
|
||||
bottom,
|
||||
}
|
||||
|
||||
class TIMUIKitHistoryMessageListContainer extends StatefulWidget {
|
||||
final Widget Function(BuildContext, V2TimMessage?)? itemBuilder;
|
||||
final AutoScrollController? scrollController;
|
||||
final String conversationID;
|
||||
final Function(String? userId, String? nickName)? onLongPressForOthersHeadPortrait;
|
||||
final List<V2TimGroupAtInfo?>? groupAtInfoList;
|
||||
final V2TimMessage? initFindingMsg;
|
||||
|
||||
/// message item builder, works for customize all message types and row layout.
|
||||
final MessageItemBuilder? messageItemBuilder;
|
||||
|
||||
/// The controller for text field.
|
||||
final TIMUIKitInputTextFieldController? textFieldController;
|
||||
|
||||
/// the builder for avatar
|
||||
final Widget Function(BuildContext context, V2TimMessage message)? userAvatarBuilder;
|
||||
|
||||
/// the builder for tongue
|
||||
final TongueItemBuilder? tongueItemBuilder;
|
||||
|
||||
final Widget? Function(V2TimMessage message, Function() closeTooltip, [Key? key, BuildContext? context])?
|
||||
extraTipsActionItemBuilder;
|
||||
|
||||
/// conversation type
|
||||
final ConvType conversationType;
|
||||
|
||||
/// Avatar and name in message reaction tap callback.
|
||||
final void Function(String userID, TapDownDetails tapDetails)? onTapAvatar;
|
||||
|
||||
/// Avatar and name in message reaction secondary tap callback.
|
||||
final void Function(String userID, TapDownDetails tapDetails)? onSecondaryTapAvatar;
|
||||
|
||||
@Deprecated(
|
||||
"Nickname will not show in one-to-one chat, if you tend to control it in group chat, please use `isShowSelfNameInGroup` and `isShowOthersNameInGroup` from `config: TIMUIKitChatConfig` instead")
|
||||
final bool showNickName;
|
||||
|
||||
final TIMUIKitHistoryMessageListConfig? mainHistoryListConfig;
|
||||
|
||||
/// tool tips panel configuration, long press message will show tool tips panel
|
||||
final ToolTipsConfig? toolTipsConfig;
|
||||
|
||||
final List<CustomEmojiFaceData> customEmojiStickerList;
|
||||
|
||||
final bool isAllowScroll;
|
||||
|
||||
final V2TimConversation conversation;
|
||||
|
||||
final V2TimGroupMemberFullInfo? groupMemberInfo;
|
||||
|
||||
/// This parameter accepts a custom widget to be displayed when the mouse hovers over a message,
|
||||
/// replacing the default message hover action bar.
|
||||
/// Applicable only on desktop platforms.
|
||||
/// If provided, the default message action functionality will appear in the right-click context menu instead.
|
||||
final Widget? Function(V2TimMessage message)? customMessageHoverBarOnDesktop;
|
||||
|
||||
const TIMUIKitHistoryMessageListContainer({
|
||||
Key? key,
|
||||
this.itemBuilder,
|
||||
this.scrollController,
|
||||
required this.conversationID,
|
||||
required this.conversationType,
|
||||
this.userAvatarBuilder,
|
||||
this.onLongPressForOthersHeadPortrait,
|
||||
this.groupAtInfoList,
|
||||
this.messageItemBuilder,
|
||||
this.tongueItemBuilder,
|
||||
this.extraTipsActionItemBuilder,
|
||||
this.isAllowScroll = true,
|
||||
this.onTapAvatar,
|
||||
@Deprecated(
|
||||
"Nickname will not show in one-to-one chat, if you tend to control it in group chat, please use `isShowSelfNameInGroup` and `isShowOthersNameInGroup` from `config: TIMUIKitChatConfig` instead")
|
||||
this.showNickName = true,
|
||||
this.initFindingMsg,
|
||||
this.mainHistoryListConfig,
|
||||
this.toolTipsConfig,
|
||||
this.customEmojiStickerList = const [],
|
||||
this.textFieldController,
|
||||
required this.conversation,
|
||||
this.onSecondaryTapAvatar,
|
||||
this.groupMemberInfo,
|
||||
this.customMessageHoverBarOnDesktop,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _TIMUIKitHistoryMessageListContainerState();
|
||||
}
|
||||
|
||||
class _TIMUIKitHistoryMessageListContainerState extends TIMUIKitState<TIMUIKitHistoryMessageListContainer> {
|
||||
late TIMUIKitHistoryMessageListController _historyMessageListController;
|
||||
|
||||
List<V2TimMessage?> historyMessageList = [];
|
||||
|
||||
Future<bool> requestForData(String? lastMsgID, LoadDirection direction, TUIChatSeparateViewModel model,
|
||||
[int? count, int? lastSeq]) async {
|
||||
if ((direction == LoadDirection.previous) || (direction == LoadDirection.latest && model.haveMoreLatestData)) {
|
||||
return await model.loadChatRecord(
|
||||
direction: direction,
|
||||
count: count ?? (kIsWeb ? 15 : HistoryMessageDartConstant.getCount),
|
||||
lastMsgID: lastMsgID,
|
||||
lastMsgSeq: lastSeq ?? -1,
|
||||
);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Widget Function(BuildContext, V2TimMessage)? _getTopRowBuilder(TUIChatSeparateViewModel model) {
|
||||
if (widget.messageItemBuilder?.messageNickNameBuilder != null) {
|
||||
return (BuildContext context, V2TimMessage message) {
|
||||
return widget.messageItemBuilder!.messageNickNameBuilder!(context, message, model);
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_historyMessageListController = TIMUIKitHistoryMessageListController(scrollController: widget.scrollController);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final chatConfig = Provider.of<TIMUIKitChatConfig>(context);
|
||||
final TUIChatSeparateViewModel model = Provider.of<TUIChatSeparateViewModel>(context, listen: false);
|
||||
|
||||
return TIMUIKitHistoryMessageListSelector(
|
||||
conversationID: model.conversationID,
|
||||
builder: (context, messageList, child) {
|
||||
return TIMUIKitHistoryMessageList(
|
||||
conversation: widget.conversation,
|
||||
model: model,
|
||||
isAllowScroll: widget.isAllowScroll,
|
||||
controller: _historyMessageListController,
|
||||
groupAtInfoList: widget.groupAtInfoList,
|
||||
mainHistoryListConfig: widget.mainHistoryListConfig,
|
||||
itemBuilder: (context, message) {
|
||||
return TIMUIKitHistoryMessageListItem(
|
||||
customMessageHoverBarOnDesktop: widget.customMessageHoverBarOnDesktop,
|
||||
groupMemberInfo: widget.groupMemberInfo,
|
||||
textFieldController: widget.textFieldController,
|
||||
userAvatarBuilder: widget.userAvatarBuilder,
|
||||
customEmojiStickerList: widget.customEmojiStickerList,
|
||||
topRowBuilder: _getTopRowBuilder(model),
|
||||
onScrollToIndex: _historyMessageListController.scrollToIndex,
|
||||
onScrollToIndexBegin: _historyMessageListController.scrollToIndexBegin,
|
||||
toolTipsConfig:
|
||||
widget.toolTipsConfig ?? ToolTipsConfig(additionalItemBuilder: widget.extraTipsActionItemBuilder),
|
||||
message: message!,
|
||||
showAvatar: chatConfig.isShowAvatar,
|
||||
onSecondaryTapForOthersPortrait: widget.onSecondaryTapAvatar,
|
||||
onTapForOthersPortrait: widget.onTapAvatar,
|
||||
messageItemBuilder: widget.messageItemBuilder,
|
||||
onLongPressForOthersHeadPortrait: widget.onLongPressForOthersHeadPortrait,
|
||||
allowAtUserWhenReply: chatConfig.isAtWhenReply,
|
||||
allowAvatarTap: chatConfig.isAllowClickAvatar,
|
||||
allowLongPress: chatConfig.isAllowLongPressMessage,
|
||||
isUseMessageReaction: chatConfig.isUseMessageReaction);
|
||||
},
|
||||
tongueItemBuilder: widget.tongueItemBuilder,
|
||||
initFindingMsg: widget.initFindingMsg,
|
||||
messageList: messageList,
|
||||
onLoadMore: (String? a, LoadDirection direction, [int? b, int? lastSeq]) async {
|
||||
return await requestForData(a, direction, model, b, lastSeq);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
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/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_message_receipt.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_message_receipt.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_chat_separate_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_chat_global_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/core/tim_uikit_wide_modal_operation_key.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/message_read_receipt.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/tui_theme.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/wide_popup.dart';
|
||||
|
||||
class TIMUIKitMessageReadReceipt extends TIMUIKitStatelessWidget {
|
||||
final V2TimMessage messageItem;
|
||||
final void Function(String, TapDownDetails tapDetails)? onTapAvatar;
|
||||
|
||||
TIMUIKitMessageReadReceipt({Key? key, this.onTapAvatar, required this.messageItem}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final TUITheme theme = value.theme;
|
||||
final TUIChatSeparateViewModel model = Provider.of<TUIChatSeparateViewModel>(context, listen: false);
|
||||
final isDesktopScreen = TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop;
|
||||
|
||||
return Selector<TUIChatGlobalModel, V2TimMessageReceipt?>(
|
||||
builder: (context, value, child) {
|
||||
if (model.conversationType == ConvType.c2c) {
|
||||
bool isPeerRead = false;
|
||||
if (value != null) {
|
||||
isPeerRead = value.isPeerRead ?? false;
|
||||
}
|
||||
return Container(
|
||||
padding: const EdgeInsets.only(bottom: 3),
|
||||
margin: const EdgeInsets.only(right: 6),
|
||||
child: Text(
|
||||
isPeerRead ? TIM_t("已读") : TIM_t("未读"),
|
||||
style: TextStyle(color: theme.chatMessageItemUnreadStatusTextColor, fontSize: 12),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () {
|
||||
if ((value?.readCount ?? 0) > 0) {
|
||||
if (isDesktopScreen) {
|
||||
TUIKitWidePopup.showPopupWindow(
|
||||
operationKey: TUIKitWideModalOperationKey.messageReadDetails,
|
||||
context: context,
|
||||
width: MediaQuery.of(context).size.width * 0.5,
|
||||
height: MediaQuery.of(context).size.height * 0.8,
|
||||
title: TIM_t("消息详情"),
|
||||
child: (onClose) => MessageReadReceipt(
|
||||
model: model,
|
||||
onTapAvatar: onTapAvatar,
|
||||
messageItem: messageItem,
|
||||
unreadCount: value?.unreadCount ?? 0,
|
||||
readCount: value?.readCount ?? 0));
|
||||
} else {
|
||||
if (value?.unreadCount == 0) {
|
||||
return;
|
||||
}
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => MessageReadReceipt(
|
||||
model: model,
|
||||
onTapAvatar: onTapAvatar,
|
||||
messageItem: messageItem,
|
||||
unreadCount: value?.unreadCount ?? 0,
|
||||
readCount: value?.readCount ?? 0)));
|
||||
}
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
padding: EdgeInsets.only(bottom: 3, right: 6, left: 6, top: isDesktopScreen ? 2 : 6),
|
||||
child: ((value?.unreadCount ?? 0) == 0 && (value?.readCount ?? 0) > 0)
|
||||
? Icon(
|
||||
Icons.check_circle_outline,
|
||||
size: 18,
|
||||
color: theme.weakTextColor,
|
||||
)
|
||||
: Container(
|
||||
width: 14,
|
||||
height: 14,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
width: 1.3,
|
||||
color: (value?.readCount ?? 0) > 0 ? theme.primaryColor! : theme.weakTextColor!)),
|
||||
child: (value?.readCount ?? 0) > 0
|
||||
? Text(
|
||||
'${value?.readCount ?? 0}',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 8, color: theme.primaryColor),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
selector: (c, model) {
|
||||
return model.getMessageReadReceipt(messageItem.msgID ?? "");
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
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';
|
||||
|
||||
class TIMUIKitChatUtils {
|
||||
static String? getMessageIDWithinIndex(List<V2TimMessage?> messageList, int index) {
|
||||
if (messageList[index]!.elemType == 11) {
|
||||
if (index > 0) {
|
||||
return getMessageIDWithinIndex(messageList, index - 1);
|
||||
}
|
||||
}
|
||||
return messageList[index]!.msgID;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
// ignore_for_file: deprecated_member_use
|
||||
|
||||
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/V2TimFriendshipListener.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/enum/V2TimGroupListener.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/enum/group_change_info_type.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_chat_separate_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_chat_global_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/friendShip/friendship_services.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/ui/utils/screen_utils.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitAppBar/tim_uikit_appbar_title.dart';
|
||||
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';
|
||||
|
||||
class TIMUIKitAppBar extends StatefulWidget implements PreferredSizeWidget {
|
||||
/// Appbar config
|
||||
final AppBar? config;
|
||||
|
||||
/// Allow show conversation total unread count
|
||||
final bool showTotalUnReadCount;
|
||||
|
||||
/// Conversation id
|
||||
final String conversationID;
|
||||
|
||||
/// conversation name
|
||||
final String conversationShowName;
|
||||
|
||||
/// If allow update the conversation show name automatically.
|
||||
final bool isConversationShowNameFixed;
|
||||
|
||||
final bool showC2cMessageEditStatus;
|
||||
|
||||
final GestureTapDownCallback? onClickTitle;
|
||||
|
||||
const TIMUIKitAppBar({
|
||||
Key? key,
|
||||
this.config,
|
||||
this.isConversationShowNameFixed = false,
|
||||
this.showTotalUnReadCount = true,
|
||||
this.conversationID = "",
|
||||
this.conversationShowName = "",
|
||||
this.showC2cMessageEditStatus = true,
|
||||
this.onClickTitle,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Size get preferredSize =>
|
||||
config?.preferredSize ?? const Size.fromHeight(56.0);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _TIMUIKitAppBarState();
|
||||
}
|
||||
|
||||
class _TIMUIKitAppBarState extends TIMUIKitState<TIMUIKitAppBar> {
|
||||
final FriendshipServices _friendshipServices =
|
||||
serviceLocator<FriendshipServices>();
|
||||
final GroupServices _groupServices = serviceLocator<GroupServices>();
|
||||
|
||||
V2TimFriendshipListener? _friendshipListener;
|
||||
V2TimGroupListener? _groupListener;
|
||||
|
||||
String _conversationShowName = "";
|
||||
|
||||
_addConversationShowNameListener() {
|
||||
_friendshipListener = V2TimFriendshipListener(
|
||||
onFriendInfoChanged: (infoList) {
|
||||
try {
|
||||
final changedInfo = infoList.firstWhere(
|
||||
(element) => element.userID == widget.conversationID,
|
||||
);
|
||||
if (changedInfo.friendRemark != null &&
|
||||
changedInfo.friendRemark!.isNotEmpty) {
|
||||
_conversationShowName = changedInfo.friendRemark!;
|
||||
setState(() {});
|
||||
} else {
|
||||
_conversationShowName = (changedInfo.userProfile?.nickName ??
|
||||
changedInfo.userProfile?.userID) ??
|
||||
"";
|
||||
}
|
||||
// ignore: empty_catches
|
||||
} catch (e) {}
|
||||
},
|
||||
);
|
||||
if (_friendshipListener != null) {
|
||||
_friendshipServices.addFriendListener(listener: _friendshipListener!);
|
||||
}
|
||||
}
|
||||
|
||||
_addGroupListener() {
|
||||
_groupListener = V2TimGroupListener(
|
||||
onGroupInfoChanged: (groupID, changeInfos) {
|
||||
try {
|
||||
if (groupID == widget.conversationID) {
|
||||
final groupNameChangeInfo = changeInfos.firstWhere((element) =>
|
||||
element.type ==
|
||||
GroupChangeInfoType.V2TIM_GROUP_INFO_CHANGE_TYPE_NAME);
|
||||
if (groupNameChangeInfo.value != null) {
|
||||
_conversationShowName = groupNameChangeInfo.value!;
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
// ignore: empty_catches
|
||||
} catch (e) {}
|
||||
},
|
||||
);
|
||||
if (_groupListener != null) {
|
||||
_groupServices.addGroupListener(listener: _groupListener!);
|
||||
}
|
||||
}
|
||||
|
||||
String _getTotalUnReadCount(int unreadCount) {
|
||||
return unreadCount < 99 ? unreadCount.toString() : "99";
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_conversationShowName = widget.conversationShowName;
|
||||
if (!widget.isConversationShowNameFixed) {
|
||||
_addConversationShowNameListener();
|
||||
_addGroupListener();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
if (!widget.isConversationShowNameFixed) {
|
||||
_groupServices.removeGroupListener(listener: _groupListener);
|
||||
_friendshipServices.removeFriendListener(listener: _friendshipListener);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(TIMUIKitAppBar oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.conversationShowName != widget.conversationShowName) {
|
||||
if (widget.conversationShowName.isNotEmpty) {
|
||||
setState(() {
|
||||
_conversationShowName = widget.conversationShowName;
|
||||
});
|
||||
} else {
|
||||
updateTitleFuture();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void updateTitleFuture() async {
|
||||
try {
|
||||
final res = await _friendshipServices
|
||||
.getFriendsInfo(userIDList: [widget.conversationID]);
|
||||
if (res != null && res.isNotEmpty && res.first.resultCode == 0) {
|
||||
setState(() {
|
||||
_conversationShowName = res.first.friendInfo?.userProfile?.nickName ??
|
||||
res.first.friendInfo?.userProfile?.userID ??
|
||||
"";
|
||||
});
|
||||
}
|
||||
// ignore: empty_catches
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final TUITheme theme = value.theme;
|
||||
|
||||
final setAppbar = widget.config;
|
||||
final TUIChatSeparateViewModel chatVM =
|
||||
Provider.of<TUIChatSeparateViewModel>(context);
|
||||
final isDesktopScreen =
|
||||
TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop;
|
||||
return AppBar(
|
||||
backgroundColor: setAppbar?.backgroundColor ??
|
||||
theme.chatHeaderBgColor ??
|
||||
theme.appbarBgColor ??
|
||||
theme.primaryColor,
|
||||
actionsIconTheme: setAppbar?.actionsIconTheme,
|
||||
foregroundColor: setAppbar?.foregroundColor,
|
||||
elevation: setAppbar?.elevation ?? (isDesktopScreen ? 0 : 1),
|
||||
bottom: setAppbar?.bottom,
|
||||
bottomOpacity: setAppbar?.bottomOpacity ?? 1.0,
|
||||
titleSpacing: setAppbar?.titleSpacing,
|
||||
automaticallyImplyLeading: setAppbar?.automaticallyImplyLeading ?? false,
|
||||
shadowColor: setAppbar?.shadowColor ?? theme.weakDividerColor,
|
||||
excludeHeaderSemantics: setAppbar?.excludeHeaderSemantics ?? false,
|
||||
toolbarHeight: setAppbar?.toolbarHeight,
|
||||
titleTextStyle: setAppbar?.titleTextStyle,
|
||||
toolbarOpacity: setAppbar?.toolbarOpacity ?? 1.0,
|
||||
toolbarTextStyle: setAppbar?.toolbarTextStyle,
|
||||
|
||||
// textTheme: setAppbar?.textTheme,
|
||||
iconTheme: setAppbar?.iconTheme ??
|
||||
const IconThemeData(
|
||||
color: Colors.white,
|
||||
),
|
||||
title: TIMUIKitAppBarTitle(
|
||||
title: setAppbar?.title,
|
||||
onClick: widget.onClickTitle,
|
||||
textStyle: TextStyle(
|
||||
color: theme.appbarTextColor ?? hexToColor("010000"), fontSize: 16),
|
||||
conversationShowName: _conversationShowName,
|
||||
showC2cMessageEditStatus: widget.showC2cMessageEditStatus,
|
||||
fromUser: widget.conversationID,
|
||||
),
|
||||
centerTitle: setAppbar?.centerTitle ?? (isDesktopScreen ? false : true),
|
||||
leadingWidth: setAppbar?.leadingWidth ?? (isDesktopScreen ? 8 : 70),
|
||||
leading: Selector<TUIChatGlobalModel, Tuple2<bool, int>>(
|
||||
builder: (context, data, _) {
|
||||
final isMultiSelect = data.item1;
|
||||
final unReadCount = data.item2;
|
||||
return (!isDesktopScreen && isMultiSelect)
|
||||
? TextButton(
|
||||
onPressed: () {
|
||||
chatVM.updateMultiSelectStatus(false);
|
||||
},
|
||||
child: Text(
|
||||
TIM_t('取消'),
|
||||
style: TextStyle(
|
||||
color: theme.appbarTextColor ?? hexToColor("010000"),
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
)
|
||||
: setAppbar?.leading ??
|
||||
(isDesktopScreen
|
||||
? Container()
|
||||
: Row(
|
||||
children: [
|
||||
IconButton(
|
||||
padding: const EdgeInsets.only(left: 16),
|
||||
constraints: const BoxConstraints(),
|
||||
icon: Icon(
|
||||
Icons.arrow_back_ios,
|
||||
color: hexToColor("010000"),
|
||||
size: 17,
|
||||
),
|
||||
onPressed: () async {
|
||||
chatVM.repliedMessage = null;
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
if (widget.showTotalUnReadCount &&
|
||||
unReadCount > 0)
|
||||
Container(
|
||||
width: 22,
|
||||
height: 22,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: theme.cautionColor,
|
||||
),
|
||||
child:
|
||||
Text(_getTotalUnReadCount(unReadCount)),
|
||||
),
|
||||
],
|
||||
));
|
||||
},
|
||||
shouldRebuild: (prev, next) =>
|
||||
prev.item1 != next.item1 || prev.item2 != next.item2,
|
||||
selector: (_, model) =>
|
||||
Tuple2(chatVM.isMultiSelect, model.totalUnReadCount)),
|
||||
actions: setAppbar?.actions,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
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_uikit/business_logic/view_models/tui_chat_global_model.dart';
|
||||
|
||||
class TIMUIKitAppBarTitle extends StatelessWidget {
|
||||
final Widget? title;
|
||||
final String conversationShowName;
|
||||
final bool showC2cMessageEditStatus;
|
||||
final String fromUser;
|
||||
final GestureTapDownCallback? onClick;
|
||||
final TextStyle? textStyle;
|
||||
|
||||
const TIMUIKitAppBarTitle(
|
||||
{Key? key,
|
||||
this.title,
|
||||
this.textStyle,
|
||||
required this.conversationShowName,
|
||||
required this.showC2cMessageEditStatus,
|
||||
required this.fromUser, this.onClick})
|
||||
: super(key: key);
|
||||
|
||||
Widget titleText(String text){
|
||||
return InkWell(
|
||||
onTapDown: onClick,
|
||||
child: Text(
|
||||
text,
|
||||
style: textStyle ??
|
||||
const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 17,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// String conversationShowName;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
int status = Provider.of<TUIChatGlobalModel>(context, listen: true)
|
||||
.getC2cMessageEditStatus(fromUser);
|
||||
if (status == 0) {
|
||||
if (title != null) {
|
||||
return title!;
|
||||
}
|
||||
return titleText(conversationShowName,);
|
||||
} else {
|
||||
if (showC2cMessageEditStatus) {
|
||||
return titleText(
|
||||
TIM_t("对方正在输入中..."),);
|
||||
|
||||
} else {
|
||||
if (title != null) {
|
||||
return title!;
|
||||
}
|
||||
return titleText(
|
||||
conversationShowName,);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
List<Map<String, Object>> messageReactionEmojiData = [
|
||||
{"name": "red heart", "unicode": 10084},
|
||||
{"name": "OK", "unicode": 128076},
|
||||
{"name": "thumbs up", "unicode": 128077},
|
||||
{"name": "flexed biceps", "unicode": 128170},
|
||||
{"name": "waving hand", "unicode": 128079},
|
||||
{"name": "handshake", "unicode": 129309},
|
||||
{"name": "folded hands", "unicode": 128591},
|
||||
{"name": "raised fist", "unicode": 9994},
|
||||
{"name": "thumbs down", "unicode": 128078},
|
||||
{"name": "smiling face with sunglasses", "unicode": 128526},
|
||||
{"name": "GRINNING FACE WITH SMILING EYES", "unicode": 128513},
|
||||
{"name": "SMILING FACE WITH SMILING EYES", "unicode": 128522},
|
||||
{"name": "smiling face with hearts", "unicode": 129392},
|
||||
{"name": "SMILING FACE WITH HEART-SHAPED EYES", "unicode": 128525},
|
||||
{"name": "FACE WITH TEARS OF JOY", "unicode": 128514},
|
||||
{"name": "flushed face", "unicode": 128563},
|
||||
{"name": "SMILING FACE WITH OPEN MOUTH AND COLD SWEAT", "unicode": 128517},
|
||||
{"name": "FACE WITH COLD SWEAT", "unicode": 128531},
|
||||
{"name": "loudly crying face", "unicode": 128557},
|
||||
{"name": "CONFOUNDED FACE", "unicode": 128534},
|
||||
{"name": "KISSING FACE WITH CLOSED EYES", "unicode": 128538},
|
||||
{
|
||||
"name": "FACE WITH STUCK-OUT TONGUE AND TIGHTLY-CLOSED EYES",
|
||||
"unicode": 128541
|
||||
},
|
||||
{"name": "DISAPPOINTED FACE", "unicode": 128542},
|
||||
{"name": "ANGRY FACE", "unicode": 128544},
|
||||
{"name": "POUTING FACE", "unicode": 128545},
|
||||
{"name": "CRYING FACE", "unicode": 128546},
|
||||
{"name": "FACE WITH LOOK OF TRIUMPH", "unicode": 128548},
|
||||
{"name": "FEARFUL FACE", "unicode": 128552},
|
||||
{"name": "SLEEPY FACE", "unicode": 128554},
|
||||
{"name": "FACE WITH NO GOOD GESTURE", "unicode": 128581},
|
||||
{"name": "FACE WITH OK GESTURE", "unicode": 128582},
|
||||
];
|
||||
+181
@@ -0,0 +1,181 @@
|
||||
import 'package:flutter/material.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_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/business_logic/view_models/tui_self_info_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/services_locatar.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/avatar.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/color.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/tui_theme.dart';
|
||||
|
||||
class TIMUIKitMessageReactionDetail extends StatefulWidget {
|
||||
/// the index of the current emoji sticker
|
||||
final int currentStickerIndex;
|
||||
|
||||
/// the list of member
|
||||
final List<V2TimGroupMemberFullInfo?>? memberList;
|
||||
|
||||
/// message reaction map
|
||||
final Map<String, dynamic> messageReaction;
|
||||
|
||||
/// the sticker list from message reaction
|
||||
final List<int> stickerList;
|
||||
|
||||
final Function(String userID, TapDownDetails tapDetails)? onTapAvatar;
|
||||
|
||||
const TIMUIKitMessageReactionDetail(
|
||||
{required this.currentStickerIndex,
|
||||
this.memberList,
|
||||
required this.messageReaction,
|
||||
Key? key,
|
||||
required this.stickerList,
|
||||
this.onTapAvatar})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => TIMUIKitMessageReactionDetailState();
|
||||
}
|
||||
|
||||
class TIMUIKitMessageReactionDetailState extends TIMUIKitState<TIMUIKitMessageReactionDetail>
|
||||
with TickerProviderStateMixin {
|
||||
final TUISelfInfoViewModel selfInfoModel = serviceLocator<TUISelfInfoViewModel>();
|
||||
|
||||
Widget getUserItem(String userID, TUITheme theme, Function(String userID, TapDownDetails tapDetails)? onTapAvatar) {
|
||||
V2TimGroupMemberFullInfo? memberInfo;
|
||||
String showName = userID;
|
||||
try {
|
||||
memberInfo = widget.memberList?.firstWhere((element) => element?.userID == userID);
|
||||
if (memberInfo != null) {
|
||||
if (memberInfo.friendRemark != null && memberInfo.friendRemark!.isNotEmpty) {
|
||||
showName = memberInfo.friendRemark!;
|
||||
} else if (memberInfo.nameCard != null && memberInfo.nameCard!.isNotEmpty) {
|
||||
showName = memberInfo.nameCard!;
|
||||
} else if (memberInfo.nickName != null && memberInfo.nickName!.isNotEmpty) {
|
||||
showName = memberInfo.nickName!;
|
||||
} else {
|
||||
showName = memberInfo.userID;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// e
|
||||
}
|
||||
|
||||
return InkWell(
|
||||
onTapDown: (tapDetails) {
|
||||
if (onTapAvatar != null) {
|
||||
if (userID != selfInfoModel.loginInfo?.userID) {
|
||||
onTapAvatar(userID, tapDetails);
|
||||
}
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border(bottom: BorderSide(color: theme.weakDividerColor ?? CommonColor.weakDividerColor))),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
margin: const EdgeInsets.only(right: 12),
|
||||
child: SizedBox(
|
||||
height: 40,
|
||||
width: 40,
|
||||
child: (memberInfo?.faceUrl != null)
|
||||
? Avatar(faceUrl: memberInfo!.faceUrl!, showName: userID)
|
||||
: Container(),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
padding: const EdgeInsets.only(top: 20, bottom: 20, right: 28),
|
||||
child: Text(
|
||||
showName,
|
||||
style: TextStyle(color: theme.black, fontSize: 18),
|
||||
),
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget stickerItem(int sticker, int length) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(top: 12, bottom: 12),
|
||||
padding: const EdgeInsets.only(left: 10, right: 10, top: 2, bottom: 1),
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0x198a8a8a),
|
||||
borderRadius: BorderRadius.all(Radius.circular(20)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.only(bottom: 3),
|
||||
child: Text(
|
||||
String.fromCharCode(sticker),
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
margin: const EdgeInsets.only(left: 6),
|
||||
child: Text(length.toString()),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget getStickerNameList(
|
||||
int sticker, TUITheme theme, Function(String userID, TapDownDetails tapDetails)? onTapAvatar) {
|
||||
final nameList = widget.messageReaction[sticker.toString()];
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [...nameList.map((e) => getUserItem(e, theme, onTapAvatar))],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final theme = value.theme;
|
||||
return DefaultTabController(
|
||||
initialIndex: widget.currentStickerIndex,
|
||||
length: widget.stickerList.length,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
margin: const EdgeInsets.only(bottom: 4),
|
||||
child: TabBar(
|
||||
isScrollable: true,
|
||||
labelColor: theme.primaryColor,
|
||||
labelStyle: const TextStyle(fontWeight: FontWeight.bold),
|
||||
unselectedLabelColor: hexToColor("62626b"),
|
||||
unselectedLabelStyle: const TextStyle(fontWeight: FontWeight.normal),
|
||||
indicatorSize: TabBarIndicatorSize.label,
|
||||
indicatorColor: theme.primaryColor ?? hexToColor("62626b"),
|
||||
tabs: [
|
||||
...widget.stickerList.map((element) {
|
||||
return stickerItem(element, widget.messageReaction[element.toString()].length);
|
||||
})
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
children: widget.stickerList
|
||||
.map((int sticker) => getStickerNameList(sticker, theme, widget.onTapAvatar))
|
||||
.toList()))
|
||||
],
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
import 'package:flutter/material.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/ui/utils/screen_utils.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitMessageItem/TIMUIKitMessageReaction/message_reaction_emoji.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitTextField/tim_uikit_emoji_panel.dart'
|
||||
as emoji;
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/emoji.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/extended_wrap/extended_wrap.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/color.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/tui_theme.dart';
|
||||
|
||||
enum SelectEmojiPanelPosition { up, down }
|
||||
|
||||
class TIMUIKitMessageReactionEmojiSelectPanel extends StatefulWidget {
|
||||
final ValueChanged<int> onSelect;
|
||||
final bool isShowMoreSticker;
|
||||
final ValueChanged<bool> onClickShowMore;
|
||||
|
||||
const TIMUIKitMessageReactionEmojiSelectPanel(
|
||||
{Key? key,
|
||||
required this.onSelect,
|
||||
required this.isShowMoreSticker,
|
||||
required this.onClickShowMore})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() =>
|
||||
TIMUIKitMessageReactionEmojiSelectPanelState();
|
||||
}
|
||||
|
||||
class TIMUIKitMessageReactionEmojiSelectPanelState
|
||||
extends TIMUIKitState<TIMUIKitMessageReactionEmojiSelectPanel> {
|
||||
bool isShowMore = false;
|
||||
|
||||
_buildSimplePanel(TUITheme theme) {
|
||||
final List<Map<String, Object>> emojiData = messageReactionEmojiData;
|
||||
final isDesktopScreen = TUIKitScreenUtils.getFormFactor() == DeviceType.Desktop;
|
||||
return Material(
|
||||
color: Colors.white,
|
||||
child: ExtendedWrap(
|
||||
maxLines: widget.isShowMoreSticker ? 5 : 1,
|
||||
spacing: 18,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
runSpacing: 24,
|
||||
children: [
|
||||
if(!isDesktopScreen)
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
widget.onClickShowMore(!widget.isShowMoreSticker);
|
||||
},
|
||||
child: SizedBox(
|
||||
height: 34,
|
||||
child: Icon(
|
||||
widget.isShowMoreSticker
|
||||
? Icons.cancel_outlined
|
||||
: Icons.add_circle_outline_outlined,
|
||||
color: hexToColor("444444"),
|
||||
size: 26),
|
||||
),
|
||||
),
|
||||
...emojiData.map(
|
||||
(e) {
|
||||
var item = Emoji.fromJson(e);
|
||||
return SizedBox(
|
||||
// width: 50,
|
||||
child: InkWell(
|
||||
splashColor: Colors.white,
|
||||
onTap: () {
|
||||
widget.onSelect(item.unicode);
|
||||
},
|
||||
child: emoji.EmojiItem(
|
||||
name: item.name,
|
||||
unicode: item.unicode,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
).toList()
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final TUITheme theme = value.theme;
|
||||
|
||||
return Container(
|
||||
child: _buildSimplePanel(theme),
|
||||
);
|
||||
}
|
||||
}
|
||||
+198
@@ -0,0 +1,198 @@
|
||||
// ignore_for_file: unused_field
|
||||
|
||||
import 'package:collection/collection.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/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_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_message_change_info.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_message_change_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_uikit/tencent_cloud_chat_uikit.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_chat_separate_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_self_info_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/message/message_services.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/services_locatar.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/platform.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitMessageItem/TIMUIKitMessageReaction/tim_uikit_message_reaction_utils.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/extended_wrap/extended_wrap.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/color.dart';
|
||||
|
||||
class TIMUIKitMessageReactionShowItem extends TIMUIKitStatelessWidget {
|
||||
/// the unicode of the emoji
|
||||
final int sticker;
|
||||
|
||||
/// the list contains the name who choose the current emoji
|
||||
final List nameList;
|
||||
|
||||
/// current message
|
||||
final V2TimMessage message;
|
||||
|
||||
/// show the details of message reaction
|
||||
final Function(int sticker) onShowDetail;
|
||||
|
||||
/// the member in current chat
|
||||
final List<V2TimGroupMemberFullInfo?> memberList;
|
||||
|
||||
TIMUIKitMessageReactionShowItem(
|
||||
{required this.message,
|
||||
required this.sticker,
|
||||
required this.memberList,
|
||||
required this.onShowDetail,
|
||||
required this.nameList,
|
||||
Key? key})
|
||||
: super(key: key);
|
||||
|
||||
final TUISelfInfoViewModel selfInfoModel = serviceLocator<TUISelfInfoViewModel>();
|
||||
final MessageService _messageService = serviceLocator<MessageService>();
|
||||
|
||||
clickOnCurrentSticker() async {
|
||||
for (int i = 0; i < 5; i++) {
|
||||
final res = await modifySticker();
|
||||
if (res.code == 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<V2TimValueCallback<V2TimMessageChangeInfo>> modifySticker() async {
|
||||
return await Future.delayed(const Duration(milliseconds: 50), () async {
|
||||
return await MessageReactionUtils.clickOnSticker(message, sticker);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final theme = value.theme;
|
||||
final option1 = nameList.length;
|
||||
final TUIChatSeparateViewModel model = Provider.of<TUIChatSeparateViewModel>(context);
|
||||
|
||||
final List<String> userIDs = [];
|
||||
for (final user in nameList) {
|
||||
final V2TimGroupMemberFullInfo? memberInfo =
|
||||
memberList.firstWhereOrNull((element) => element?.userID == user && TencentUtils.checkString(user) != null);
|
||||
if ((memberInfo == null || TencentUtils.checkString(memberInfo.userID) == null) &&
|
||||
TencentUtils.checkString(user.toString()) != null) {
|
||||
userIDs.add(user.toString());
|
||||
}
|
||||
}
|
||||
if (userIDs.isNotEmpty) {
|
||||
model.getUserShowName(userIDs);
|
||||
}
|
||||
|
||||
return LayoutBuilder(builder: (context, constraints) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 10,
|
||||
right: 10,
|
||||
),
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0x19727271),
|
||||
borderRadius: BorderRadius.all(Radius.circular(20)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
InkWell(
|
||||
onTap: clickOnCurrentSticker,
|
||||
child: Container(
|
||||
margin:
|
||||
EdgeInsets.only(bottom: (!PlatformUtils().isIOS) ? 4 : 2, top: (!PlatformUtils().isIOS) ? 4 : 0),
|
||||
child: Text(
|
||||
String.fromCharCode(sticker),
|
||||
style: TextStyle(fontSize: (!PlatformUtils().isIOS) ? 12 : 16, color: hexToColor("f9453d")),
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
margin: const EdgeInsets.only(left: 6, right: 6),
|
||||
child: SizedBox(
|
||||
width: 1,
|
||||
height: 14,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(color: theme.weakTextColor),
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: constraints.maxWidth * 0.8,
|
||||
),
|
||||
child: ExtendedWrap(
|
||||
maxLines: 1,
|
||||
spacing: 8,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
overflowWidget: GestureDetector(
|
||||
onTap: () {
|
||||
onShowDetail(sticker);
|
||||
},
|
||||
child: Text(
|
||||
TIM_t_para("...共{{option1}}人", "...共$option1人")(option1: option1),
|
||||
style: TextStyle(fontSize: 12, color: hexToColor("616669")),
|
||||
),
|
||||
),
|
||||
children: [
|
||||
...nameList.map((e) {
|
||||
String showName = e;
|
||||
if (memberList.isNotEmpty) {
|
||||
try {
|
||||
final V2TimGroupMemberFullInfo? memberInfo =
|
||||
memberList.firstWhere((element) => element?.userID == e);
|
||||
if (memberInfo != null) {
|
||||
if (memberInfo.friendRemark != null && memberInfo.friendRemark!.isNotEmpty) {
|
||||
showName = memberInfo.friendRemark!;
|
||||
} else if (memberInfo.nameCard != null && memberInfo.nameCard!.isNotEmpty) {
|
||||
showName = memberInfo.nameCard!;
|
||||
} else if (memberInfo.nickName != null && memberInfo.nickName!.isNotEmpty) {
|
||||
showName = memberInfo.nickName!;
|
||||
} else {
|
||||
showName = memberInfo.userID;
|
||||
}
|
||||
} else {
|
||||
final String? data = model.groupUserShowName[e];
|
||||
if (TencentUtils.checkString(data) != null) {
|
||||
showName = data ?? e;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
final String? data = model.groupUserShowName[e];
|
||||
if (TencentUtils.checkString(data) != null) {
|
||||
showName = data ?? e;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
final String? data = model.groupUserShowName[e];
|
||||
if (TencentUtils.checkString(data) != null) {
|
||||
showName = data ?? e;
|
||||
}
|
||||
}
|
||||
return InkWell(
|
||||
onTapDown: (tapDetails) {
|
||||
if (model.onTapAvatar != null) {
|
||||
if (e != selfInfoModel.loginInfo?.userID) {
|
||||
model.onTapAvatar!(e, tapDetails);
|
||||
}
|
||||
}
|
||||
},
|
||||
child: Text(
|
||||
showName,
|
||||
style: TextStyle(fontSize: 12, color: hexToColor("616669")),
|
||||
),
|
||||
);
|
||||
})
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
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/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_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_statelesswidget.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_chat_separate_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_chat_global_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_self_info_view_model.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/utils/platform.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitMessageItem/TIMUIKitMessageReaction/tim_uikit_message_reaction_detail.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitMessageItem/TIMUIKitMessageReaction/tim_uikit_message_reaction_show_item.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitMessageItem/TIMUIKitMessageReaction/tim_uikit_message_reaction_utils.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/tim_uikit_cloud_custom_data.dart';
|
||||
|
||||
class TIMUIKitMessageReactionShowPanel extends TIMUIKitStatelessWidget {
|
||||
/// current message
|
||||
final V2TimMessage message;
|
||||
|
||||
TIMUIKitMessageReactionShowPanel({required this.message, Key? key}) : super(key: key);
|
||||
|
||||
final TUISelfInfoViewModel selfInfoModel = serviceLocator<TUISelfInfoViewModel>();
|
||||
|
||||
void showMore(BuildContext context, List<V2TimGroupMemberFullInfo?>? memberList, Map<String, dynamic> messageReaction,
|
||||
int currentSticker, List<int> stickerList, TUIChatSeparateViewModel model) async {
|
||||
_showCustomModalBottomSheet(context, memberList, messageReaction, currentSticker, stickerList, model);
|
||||
}
|
||||
|
||||
Future<Future<int?>> _showCustomModalBottomSheet(
|
||||
context,
|
||||
List<V2TimGroupMemberFullInfo?>? memberList,
|
||||
Map<String, dynamic> messageReaction,
|
||||
int currentSticker,
|
||||
List<int> stickerList,
|
||||
TUIChatSeparateViewModel model) async {
|
||||
return showModalBottomSheet<int>(
|
||||
backgroundColor: Colors.transparent,
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return Container(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(20.0),
|
||||
topRight: Radius.circular(20.0),
|
||||
),
|
||||
),
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: MediaQuery.of(context).size.height * 0.66,
|
||||
minHeight: MediaQuery.of(context).size.height * 0.2,
|
||||
),
|
||||
child: Column(children: [
|
||||
SizedBox(
|
||||
height: 50,
|
||||
child: Stack(
|
||||
textDirection: TextDirection.rtl,
|
||||
children: [
|
||||
Center(
|
||||
child: Text(
|
||||
TIM_t("回应详情"),
|
||||
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16.0),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(height: 1.0),
|
||||
Expanded(
|
||||
child: TIMUIKitMessageReactionDetail(
|
||||
onTapAvatar: model.onTapAvatar,
|
||||
stickerList: stickerList,
|
||||
currentStickerIndex: stickerList.indexWhere((element) => element == currentSticker),
|
||||
memberList: memberList,
|
||||
messageReaction: messageReaction,
|
||||
)),
|
||||
]),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
Map<String, dynamic> messageReaction = {};
|
||||
CloudCustomData messageCloudCustomData = MessageReactionUtils.getCloudCustomData(message);
|
||||
final TUIChatSeparateViewModel model = Provider.of<TUIChatSeparateViewModel>(context);
|
||||
if (messageCloudCustomData.messageReaction != null && messageCloudCustomData.messageReaction!.isNotEmpty) {
|
||||
messageReaction = messageCloudCustomData.messageReaction!;
|
||||
} else {
|
||||
return const SizedBox(width: 0, height: 0);
|
||||
}
|
||||
|
||||
final List<int> messageReactionStickerList = [];
|
||||
|
||||
messageReaction.forEach((key, value) {
|
||||
messageReactionStickerList.add(int.parse(key));
|
||||
});
|
||||
|
||||
final filteredMessageReactionStickerList = messageReactionStickerList.where((sticker) {
|
||||
if (messageReaction[sticker.toString()] == null ||
|
||||
messageReaction[sticker.toString()] is! List ||
|
||||
messageReaction[sticker.toString()].length == 0) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}).toList();
|
||||
|
||||
final ConvType convType = model.conversationType ?? ConvType.c2c;
|
||||
List<V2TimGroupMemberFullInfo?> memberList = [];
|
||||
if (convType == ConvType.group) {
|
||||
memberList = model.groupMemberList ?? [];
|
||||
} else {
|
||||
final V2TimGroupMemberFullInfo selfInfo = V2TimGroupMemberFullInfo(
|
||||
userID: selfInfoModel.loginInfo?.userID ?? "",
|
||||
nickName: selfInfoModel.loginInfo?.nickName,
|
||||
faceUrl: selfInfoModel.loginInfo?.faceUrl,
|
||||
);
|
||||
|
||||
final V2TimGroupMemberFullInfo targetInfo = V2TimGroupMemberFullInfo(
|
||||
userID: model.conversationID,
|
||||
);
|
||||
memberList = [selfInfo, model.currentChatUserInfo ?? targetInfo];
|
||||
}
|
||||
|
||||
return filteredMessageReactionStickerList.isNotEmpty
|
||||
? Container(
|
||||
margin: const EdgeInsets.only(top: 12),
|
||||
child: Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: (!PlatformUtils().isIOS) ? 12 : 8,
|
||||
children: [
|
||||
...filteredMessageReactionStickerList.map((sticker) {
|
||||
return TIMUIKitMessageReactionShowItem(
|
||||
memberList: memberList,
|
||||
message: message,
|
||||
nameList: messageReaction[sticker.toString()],
|
||||
sticker: sticker,
|
||||
onShowDetail: (int sticker) {
|
||||
showMore(
|
||||
context, memberList, messageReaction, sticker, filteredMessageReactionStickerList, model);
|
||||
});
|
||||
}).toList(),
|
||||
],
|
||||
),
|
||||
)
|
||||
: const SizedBox(width: 0, height: 0);
|
||||
}
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
import 'dart:convert';
|
||||
|
||||
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_message_change_info.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_message_change_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_uikit/tencent_cloud_chat_uikit.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_self_info_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/message/message_services.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/services_locatar.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/platform.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/tim_uikit_cloud_custom_data.dart';
|
||||
|
||||
class MessageReactionUtils {
|
||||
static final TUISelfInfoViewModel selfInfoModel = serviceLocator<TUISelfInfoViewModel>();
|
||||
static final MessageService _messageService = serviceLocator<MessageService>();
|
||||
|
||||
static CloudCustomData getCloudCustomData(V2TimMessage message) {
|
||||
CloudCustomData messageCloudCustomData;
|
||||
try {
|
||||
messageCloudCustomData = CloudCustomData.fromJson(
|
||||
json.decode(TencentUtils.checkString(message.cloudCustomData) != null ? message.cloudCustomData! : "{}"));
|
||||
} catch (e) {
|
||||
messageCloudCustomData = CloudCustomData();
|
||||
}
|
||||
|
||||
return messageCloudCustomData;
|
||||
}
|
||||
|
||||
static Map<String, dynamic> getMessageReaction(V2TimMessage message) {
|
||||
return getCloudCustomData(message).messageReaction ?? {};
|
||||
}
|
||||
|
||||
static Future<V2TimValueCallback<V2TimMessageChangeInfo>> clickOnSticker(V2TimMessage message, int sticker) async {
|
||||
final CloudCustomData messageCloudCustomData = getCloudCustomData(message);
|
||||
final Map<String, dynamic> messageReaction = messageCloudCustomData.messageReaction ?? {};
|
||||
List targetList = messageReaction["$sticker"] ?? [];
|
||||
if (targetList.contains(selfInfoModel.loginInfo!.userID!)) {
|
||||
targetList.remove(selfInfoModel.loginInfo!.userID!);
|
||||
} else {
|
||||
targetList = [selfInfoModel.loginInfo!.userID!, ...targetList];
|
||||
}
|
||||
messageReaction["$sticker"] = targetList;
|
||||
|
||||
if (PlatformUtils().isWeb) {
|
||||
final decodedMessage = jsonDecode(message.messageFromWeb!);
|
||||
decodedMessage["cloudCustomData"] = jsonEncode(messageCloudCustomData.toMap());
|
||||
message.messageFromWeb = jsonEncode(decodedMessage);
|
||||
} else {
|
||||
message.cloudCustomData = json.encode(messageCloudCustomData.toMap());
|
||||
}
|
||||
return await _messageService.modifyMessage(message: message);
|
||||
}
|
||||
}
|
||||
+175
@@ -0,0 +1,175 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.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/ui/utils/common_utils.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/business_logic/separate_models/tui_chat_separate_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitMessageItem/TIMUIKitMessageReaction/tim_uikit_message_reaction_show_panel.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/tim_uikit_cloud_custom_data.dart';
|
||||
|
||||
class TIMUIKitMessageReactionWrapper extends StatefulWidget {
|
||||
final Widget child;
|
||||
final V2TimMessage message;
|
||||
final Color? backgroundColor;
|
||||
final bool isFromSelf;
|
||||
final BorderRadius? borderRadius;
|
||||
final bool isShowJump;
|
||||
final VoidCallback? clearJump;
|
||||
final bool isShowMessageReaction;
|
||||
final TUIChatSeparateViewModel chatModel;
|
||||
|
||||
const TIMUIKitMessageReactionWrapper(
|
||||
{Key? key,
|
||||
required this.isShowJump,
|
||||
this.clearJump,
|
||||
required this.isFromSelf,
|
||||
this.backgroundColor,
|
||||
required this.chatModel,
|
||||
required this.message,
|
||||
this.borderRadius,
|
||||
required this.child,
|
||||
this.isShowMessageReaction = true})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _TIMUIKitMessageReactionWrapperState();
|
||||
}
|
||||
|
||||
class _TIMUIKitMessageReactionWrapperState extends TIMUIKitState<TIMUIKitMessageReactionWrapper> {
|
||||
bool isShowJumpState = false;
|
||||
bool isShining = false;
|
||||
bool isShowBorder = false;
|
||||
|
||||
_showJumpColor() {
|
||||
if ((widget.chatModel.jumpMsgID != widget.message.msgID) && (widget.message.msgID?.isNotEmpty ?? true)) {
|
||||
return;
|
||||
}
|
||||
isShining = true;
|
||||
int shineAmount = 6;
|
||||
setState(() {
|
||||
isShowJumpState = true;
|
||||
isShowBorder = true;
|
||||
});
|
||||
Timer.periodic(const Duration(milliseconds: 300), (timer) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
isShowJumpState = shineAmount.isOdd ? true : false;
|
||||
isShowBorder = shineAmount.isOdd ? true : false;
|
||||
});
|
||||
}
|
||||
if (shineAmount == 0 || !mounted) {
|
||||
isShining = false;
|
||||
timer.cancel();
|
||||
}
|
||||
shineAmount--;
|
||||
});
|
||||
if (widget.clearJump != null) {
|
||||
widget.clearJump!();
|
||||
}
|
||||
}
|
||||
|
||||
bool isHaveMessageReaction() {
|
||||
Map<String, dynamic> messageReaction = {};
|
||||
CloudCustomData messageCloudCustomData;
|
||||
try {
|
||||
messageCloudCustomData = CloudCustomData.fromJson(json.decode(
|
||||
TencentUtils.checkString(widget.message.cloudCustomData) != null ? widget.message.cloudCustomData! : "{}"));
|
||||
} catch (e) {
|
||||
messageCloudCustomData = CloudCustomData();
|
||||
}
|
||||
|
||||
if (messageCloudCustomData.messageReaction != null && messageCloudCustomData.messageReaction!.isNotEmpty) {
|
||||
messageReaction = messageCloudCustomData.messageReaction!;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
final List<int> messageReactionStickerList = [];
|
||||
|
||||
messageReaction.forEach((key, value) {
|
||||
messageReactionStickerList.add(int.parse(key));
|
||||
});
|
||||
|
||||
final filteredMessageReactionStickerList = messageReactionStickerList.where((sticker) {
|
||||
if (messageReaction[sticker.toString()] == null ||
|
||||
messageReaction[sticker.toString()] is! List ||
|
||||
messageReaction[sticker.toString()].length == 0) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
if (filteredMessageReactionStickerList.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final theme = value.theme;
|
||||
final borderRadius = widget.isFromSelf
|
||||
? const BorderRadius.only(
|
||||
topLeft: Radius.circular(10),
|
||||
topRight: Radius.circular(2),
|
||||
bottomLeft: Radius.circular(10),
|
||||
bottomRight: Radius.circular(10))
|
||||
: const BorderRadius.only(
|
||||
topLeft: Radius.circular(2),
|
||||
topRight: Radius.circular(10),
|
||||
bottomLeft: Radius.circular(10),
|
||||
bottomRight: Radius.circular(10));
|
||||
|
||||
if (widget.isShowJump) {
|
||||
if (!isShining) {
|
||||
Future.delayed(Duration.zero, () {
|
||||
_showJumpColor();
|
||||
});
|
||||
} else {
|
||||
if ((widget.chatModel.jumpMsgID == widget.message.msgID) && (widget.message.msgID?.isNotEmpty ?? false)) {
|
||||
if (widget.clearJump != null) {
|
||||
widget.clearJump!();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final defaultStyle = widget.isFromSelf ? theme.lightPrimaryMaterialColor.shade50 : theme.weakBackgroundColor;
|
||||
final backgroundColor =
|
||||
isShowJumpState ? const Color.fromRGBO(245, 166, 35, 1) : (widget.backgroundColor ?? defaultStyle);
|
||||
|
||||
if (!widget.isShowMessageReaction || !isHaveMessageReaction()) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(5)),
|
||||
border: Border.all(color: Color.fromRGBO(245, 166, 35, (isShowBorder ? 1 : 0)), width: 2)),
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
borderRadius: widget.borderRadius ?? borderRadius,
|
||||
),
|
||||
constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.7),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(5)),
|
||||
border: Border.all(color: Color.fromRGBO(245, 166, 35, (isShowBorder ? 1 : 0)), width: 2)),
|
||||
child: widget.child,
|
||||
),
|
||||
if (widget.isShowMessageReaction) TIMUIKitMessageReactionShowPanel(message: widget.message)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitMessageItem/tim_uikit_chat_file_elem.dart';
|
||||
export 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitMessageItem/tim_uikit_chat_group_tips_elem.dart';
|
||||
export 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitMessageItem/tim_uikit_chat_image_elem.dart';
|
||||
export 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitMessageItem/tim_uikit_chat_sound_elem.dart';
|
||||
export 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitMessageItem/tim_uikit_chat_text_elem.dart';
|
||||
export 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitMessageItem/tim_uikit_chat_video_elem.dart';
|
||||
export 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitMessageItem/tim_uikit_merger_message_elem.dart';
|
||||
export 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitMessageItem/tim_uikit_chat_reply_elem.dart';
|
||||
@@ -0,0 +1,60 @@
|
||||
// ignore_for_file: unrelated_type_equality_checks
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:tencent_chat_i18n_tool/tencent_chat_i18n_tool.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_custom_elem.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_custom_elem.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_statelesswidget.dart';
|
||||
|
||||
class TIMUIKitCustomElem extends TIMUIKitStatelessWidget {
|
||||
final V2TimCustomElem? customElem;
|
||||
final bool isFromSelf;
|
||||
final TextStyle? messageFontStyle;
|
||||
final BorderRadius? messageBorderRadius;
|
||||
final Color? messageBackgroundColor;
|
||||
final EdgeInsetsGeometry? textPadding;
|
||||
final V2TimMessage message;
|
||||
final bool? isShowMessageReaction;
|
||||
|
||||
TIMUIKitCustomElem({
|
||||
Key? key,
|
||||
required this.message,
|
||||
this.isShowMessageReaction,
|
||||
this.customElem,
|
||||
this.isFromSelf = false,
|
||||
this.messageFontStyle,
|
||||
this.messageBorderRadius,
|
||||
this.messageBackgroundColor,
|
||||
this.textPadding,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final theme = value.theme;
|
||||
final borderRadius = isFromSelf
|
||||
? const BorderRadius.only(
|
||||
topLeft: Radius.circular(10),
|
||||
topRight: Radius.circular(2),
|
||||
bottomLeft: Radius.circular(10),
|
||||
bottomRight: Radius.circular(10))
|
||||
: const BorderRadius.only(
|
||||
topLeft: Radius.circular(2),
|
||||
topRight: Radius.circular(10),
|
||||
bottomLeft: Radius.circular(10),
|
||||
bottomRight: Radius.circular(10));
|
||||
final backgroundColor = isFromSelf ? theme.lightPrimaryMaterialColor.shade50 : theme.weakBackgroundColor;
|
||||
return Container(
|
||||
padding: textPadding ?? const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: messageBackgroundColor ?? backgroundColor,
|
||||
borderRadius: messageBorderRadius ?? borderRadius,
|
||||
),
|
||||
constraints: const BoxConstraints(maxWidth: 240),
|
||||
child: Column(
|
||||
children: [Text(TIM_t("自定义消息"))],
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import 'package:flutter/material.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/ui/utils/screen_utils.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/business_logic/separate_models/tui_chat_separate_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitMessageItem/TIMUIKitMessageReaction/tim_uikit_message_reaction_wrapper.dart';
|
||||
|
||||
class TIMUIKitFaceElem extends StatefulWidget {
|
||||
final String path;
|
||||
final bool isShowJump;
|
||||
final VoidCallback? clearJump;
|
||||
final V2TimMessage message;
|
||||
final bool? isShowMessageReaction;
|
||||
final TUIChatSeparateViewModel model;
|
||||
|
||||
const TIMUIKitFaceElem(
|
||||
{Key? key,
|
||||
required this.path,
|
||||
required this.isShowJump,
|
||||
this.clearJump,
|
||||
required this.message,
|
||||
this.isShowMessageReaction,
|
||||
required this.model})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _TIMUIKitTextElemState();
|
||||
}
|
||||
|
||||
class _TIMUIKitTextElemState extends TIMUIKitState<TIMUIKitFaceElem> {
|
||||
bool isFromNetwork() {
|
||||
return widget.path.startsWith('http');
|
||||
}
|
||||
|
||||
createPathFromNative(String path) {
|
||||
String prefix = "";
|
||||
String suffix = "";
|
||||
if (widget.model.chatConfig.faceURIPrefix != null) {
|
||||
prefix = widget.model.chatConfig.faceURIPrefix!(path);
|
||||
}
|
||||
if (widget.model.chatConfig.faceURISuffix != null) {
|
||||
suffix = widget.model.chatConfig.faceURISuffix!(path);
|
||||
}
|
||||
return "$prefix$path$suffix";
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final isDesktopScreen = TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop;
|
||||
return TIMUIKitMessageReactionWrapper(
|
||||
chatModel: widget.model,
|
||||
isShowJump: widget.isShowJump,
|
||||
isFromSelf: widget.message.isSelf ?? true,
|
||||
clearJump: widget.clearJump,
|
||||
message: widget.message,
|
||||
isShowMessageReaction: widget.isShowMessageReaction ?? true,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * (isDesktopScreen ? 0.1 : 0.3)),
|
||||
child: isFromNetwork() ? Image.network(widget.path) : Image.asset(createPathFromNative(widget.path)),
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,472 @@
|
||||
// ignore_for_file: avoid_print
|
||||
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:loading_animation_widget/loading_animation_widget.dart';
|
||||
import 'package:open_file/open_file.dart';
|
||||
import 'package:tencent_chat_i18n_tool/tencent_chat_i18n_tool.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/enum/V2TimAdvancedMsgListener.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_file_elem.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_file_elem.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_message_download_progress.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_message_download_progress.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/tencent_im_sdk_plugin.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_callback.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/business_logic/separate_models/tui_chat_separate_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_chat_global_model.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/theme/color.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/tui_theme.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/permission.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/platform.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitMessageItem/TIMUIKitMessageReaction/tim_uikit_message_reaction_wrapper.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitMessageItem/tim_uikit_chat_file_icon.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/textSize.dart';
|
||||
import 'package:universal_html/html.dart' as html;
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class TIMUIKitFileElem extends StatefulWidget {
|
||||
final String? messageID;
|
||||
final V2TimFileElem? fileElem;
|
||||
final bool isSelf;
|
||||
final bool isShowJump;
|
||||
final VoidCallback? clearJump;
|
||||
final V2TimMessage message;
|
||||
final bool? isShowMessageReaction;
|
||||
final TUIChatSeparateViewModel chatModel;
|
||||
|
||||
const TIMUIKitFileElem(
|
||||
{Key? key,
|
||||
required this.chatModel,
|
||||
required this.messageID,
|
||||
required this.fileElem,
|
||||
required this.isSelf,
|
||||
required this.isShowJump,
|
||||
this.clearJump,
|
||||
required this.message,
|
||||
this.isShowMessageReaction})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _TIMUIKitFileElemState();
|
||||
}
|
||||
|
||||
class _TIMUIKitFileElemState extends TIMUIKitState<TIMUIKitFileElem> {
|
||||
String filePath = "";
|
||||
bool isWebDownloading = false;
|
||||
final TUIChatGlobalModel model = serviceLocator<TUIChatGlobalModel>();
|
||||
int downloadProgress = 0;
|
||||
V2TimAdvancedMsgListener? advancedMsgListener;
|
||||
final GlobalKey containerKey = GlobalKey();
|
||||
double? containerHeight;
|
||||
bool? _downloadFailed = false;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (advancedMsgListener != null) {
|
||||
TencentImSDKPlugin.v2TIMManager.getMessageManager().removeAdvancedMsgListener(listener: advancedMsgListener);
|
||||
advancedMsgListener = null;
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (!PlatformUtils().isWeb) {
|
||||
Future.delayed(const Duration(microseconds: 10), () {
|
||||
hasFile();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> addAdvancedMsgListenerForDownload() async {
|
||||
if (advancedMsgListener != null) {
|
||||
return false;
|
||||
}
|
||||
advancedMsgListener = V2TimAdvancedMsgListener(
|
||||
onMessageDownloadProgressCallback: (V2TimMessageDownloadProgress messageProgress) async {
|
||||
if (messageProgress.msgID == widget.message.msgID) {
|
||||
if (messageProgress.isError || messageProgress.errorCode != 0) {
|
||||
setState(() {
|
||||
_downloadFailed = true;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (messageProgress.isFinish) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
downloadProgress = 100;
|
||||
});
|
||||
|
||||
if (advancedMsgListener != null) {
|
||||
TencentImSDKPlugin.v2TIMManager
|
||||
.getMessageManager()
|
||||
.removeAdvancedMsgListener(listener: advancedMsgListener);
|
||||
advancedMsgListener = null;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
final currentProgress = (messageProgress.currentSize / messageProgress.totalSize * 100).floor();
|
||||
if (mounted && currentProgress > downloadProgress) {
|
||||
setState(() {
|
||||
downloadProgress = currentProgress;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
await TencentImSDKPlugin.v2TIMManager.getMessageManager().addAdvancedMsgListener(listener: advancedMsgListener!);
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<String> getSavePath() async {
|
||||
String savePathWithAppPath = '/storage/emulated/0/Android/data/com.tencent.flutter.tuikit/cache/' +
|
||||
(widget.message.msgID ?? "") +
|
||||
widget.fileElem!.fileName!;
|
||||
return savePathWithAppPath;
|
||||
}
|
||||
|
||||
Future<bool> hasFile() async {
|
||||
if (PlatformUtils().isWeb) {
|
||||
return true;
|
||||
}
|
||||
String savePath = TencentUtils.checkString(model.getFileMessageLocation(widget.messageID)) ??
|
||||
TencentUtils.checkString(widget.message.fileElem!.localUrl) ??
|
||||
widget.message.fileElem?.path ??
|
||||
'';
|
||||
|
||||
File f = File(savePath);
|
||||
if (widget.messageID != null) {
|
||||
if (f.existsSync()) {
|
||||
filePath = savePath;
|
||||
if (downloadProgress != 100) {
|
||||
setState(() {
|
||||
downloadProgress = 100;
|
||||
});
|
||||
}
|
||||
if (model.getMessageProgress(widget.messageID) != 100) {
|
||||
model.setMessageProgress(widget.messageID!, 100);
|
||||
}
|
||||
if (advancedMsgListener != null) {
|
||||
TencentImSDKPlugin.v2TIMManager.getMessageManager().removeAdvancedMsgListener(listener: advancedMsgListener);
|
||||
advancedMsgListener = null;
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
model.setMessageProgress(widget.messageID!, 0);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
String showFileSize(int fileSize) {
|
||||
if (fileSize < 1024) {
|
||||
return fileSize.toString() + "B";
|
||||
} else if (fileSize < 1024 * 1024) {
|
||||
return (fileSize / 1024).toStringAsFixed(2) + "KB";
|
||||
} else if (fileSize < 1024 * 1024 * 1024) {
|
||||
return (fileSize / 1024 / 1024).toStringAsFixed(2) + "MB";
|
||||
} else {
|
||||
return (fileSize / 1024 / 1024 / 1024).toStringAsFixed(2) + "GB";
|
||||
}
|
||||
}
|
||||
|
||||
addUrlToWaitingPath(TUITheme theme) async {
|
||||
if (widget.messageID != null) {
|
||||
model.addWaitingList(widget.messageID!);
|
||||
}
|
||||
if (model.getWaitingListLength() == 1) {
|
||||
await downloadFile(theme);
|
||||
}
|
||||
}
|
||||
|
||||
checkIsWaiting() {
|
||||
bool res = false;
|
||||
try {
|
||||
if (widget.messageID!.isNotEmpty) {
|
||||
res = model.isWaiting(widget.messageID!);
|
||||
}
|
||||
} catch (err) {
|
||||
// err
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
downloadFile(TUITheme theme) async {
|
||||
if (PlatformUtils().isMobile) {
|
||||
if (PlatformUtils().isIOS) {
|
||||
if (!await Permissions.checkPermission(context, Permission.photosAddOnly.value, theme, false)) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
final DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
|
||||
AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo;
|
||||
if ((androidInfo.version.sdkInt) >= 33) {
|
||||
} else {
|
||||
var storage = await Permissions.checkPermission(
|
||||
context,
|
||||
Permission.storage.value,
|
||||
);
|
||||
if (!storage) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
await model.downloadFile();
|
||||
}
|
||||
|
||||
Future<bool> hasZeroSize(String filePath) async {
|
||||
try {
|
||||
final file = File(filePath);
|
||||
final fileSize = await file.length();
|
||||
return fileSize == 0;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
tryOpenFile(context, theme) async {
|
||||
if (!PlatformUtils().isWeb && (await hasZeroSize(filePath) || widget.message.status == 3)) {
|
||||
onTIMCallback(TIMCallback(type: TIMCallbackType.INFO, infoRecommendText: "不支持 0KB 文件的传输", infoCode: 6660417));
|
||||
return;
|
||||
}
|
||||
if (PlatformUtils().isMobile) {
|
||||
if (PlatformUtils().isIOS) {
|
||||
if (!await Permissions.checkPermission(context, Permission.photosAddOnly.value, theme!, false)) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
final DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
|
||||
AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo;
|
||||
if ((androidInfo.version.sdkInt) >= 33) {
|
||||
} else {
|
||||
var storage = await Permissions.checkPermission(
|
||||
context,
|
||||
Permission.storage.value,
|
||||
);
|
||||
if (!storage) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (PlatformUtils().isDesktop && !PlatformUtils().isWindows) {
|
||||
launchUrl(Uri.file(filePath));
|
||||
} else {
|
||||
OpenFile.open(filePath);
|
||||
}
|
||||
// ignore: empty_catches
|
||||
} catch (e) {
|
||||
OpenFile.open(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
void downloadWebFile(String fileUrl) async {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
isWebDownloading = true;
|
||||
});
|
||||
}
|
||||
String fileName = Uri.parse(fileUrl).pathSegments.last;
|
||||
try {
|
||||
http.Response response = await http.get(
|
||||
Uri.parse(fileUrl),
|
||||
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||||
);
|
||||
|
||||
final html.AnchorElement downloadAnchor = html.document.createElement('a') as html.AnchorElement;
|
||||
|
||||
final html.Blob blob = html.Blob([response.bodyBytes]);
|
||||
|
||||
downloadAnchor.href = html.Url.createObjectUrlFromBlob(blob);
|
||||
downloadAnchor.download = widget.message.fileElem?.fileName ?? fileName;
|
||||
|
||||
downloadAnchor.click();
|
||||
} catch (e) {
|
||||
html.AnchorElement(
|
||||
href: widget.fileElem?.path ?? "",
|
||||
)
|
||||
..setAttribute("download", widget.message.fileElem?.fileName ?? fileName)
|
||||
..setAttribute("target", '_blank')
|
||||
..style.display = "none"
|
||||
..click();
|
||||
}
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
isWebDownloading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final theme = value.theme;
|
||||
final received = downloadProgress;
|
||||
final fileName = widget.fileElem!.fileName ?? "";
|
||||
final fileSize = widget.fileElem!.fileSize;
|
||||
final borderRadius = widget.isSelf
|
||||
? const BorderRadius.only(
|
||||
topLeft: Radius.circular(10),
|
||||
topRight: Radius.circular(2),
|
||||
bottomLeft: Radius.circular(10),
|
||||
bottomRight: Radius.circular(10))
|
||||
: const BorderRadius.only(
|
||||
topLeft: Radius.circular(2),
|
||||
topRight: Radius.circular(10),
|
||||
bottomLeft: Radius.circular(10),
|
||||
bottomRight: Radius.circular(10));
|
||||
String? fileFormat;
|
||||
if (widget.fileElem?.fileName != null && widget.fileElem!.fileName!.isNotEmpty) {
|
||||
final String fileName = widget.fileElem!.fileName!;
|
||||
fileFormat = fileName.split(".")[max(fileName.split(".").length - 1, 0)];
|
||||
}
|
||||
final RenderBox? containerRenderBox = containerKey.currentContext?.findRenderObject() as RenderBox?;
|
||||
if (containerRenderBox != null) {
|
||||
containerHeight = containerRenderBox.size.height;
|
||||
}
|
||||
|
||||
return Row(
|
||||
key: containerKey,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (widget.isSelf && isWebDownloading)
|
||||
Container(
|
||||
margin: const EdgeInsets.only(top: 2),
|
||||
child: LoadingAnimationWidget.threeArchedCircle(
|
||||
color: theme.weakTextColor ?? Colors.grey,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
TIMUIKitMessageReactionWrapper(
|
||||
chatModel: widget.chatModel,
|
||||
isShowJump: widget.isShowJump,
|
||||
clearJump: widget.clearJump,
|
||||
isFromSelf: widget.message.isSelf ?? true,
|
||||
isShowMessageReaction: widget.isShowMessageReaction ?? true,
|
||||
message: widget.message,
|
||||
child: GestureDetector(
|
||||
onTap: () async {
|
||||
try {
|
||||
if (PlatformUtils().isWeb) {
|
||||
if (!isWebDownloading) {
|
||||
downloadWebFile(widget.fileElem?.path ?? "");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
await addAdvancedMsgListenerForDownload();
|
||||
if (await hasFile()) {
|
||||
if (received == 100) {
|
||||
tryOpenFile(context, theme);
|
||||
} else {
|
||||
onTIMCallback(
|
||||
TIMCallback(
|
||||
type: TIMCallbackType.INFO,
|
||||
infoRecommendText: TIM_t("正在下载中"),
|
||||
infoCode: 6660411,
|
||||
),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (checkIsWaiting()) {
|
||||
onTIMCallback(
|
||||
TIMCallback(
|
||||
type: TIMCallbackType.INFO, infoRecommendText: TIM_t("已加入待下载队列,其他文件下载中"), infoCode: 6660413),
|
||||
);
|
||||
return;
|
||||
} else {
|
||||
await addUrlToWaitingPath(theme);
|
||||
}
|
||||
} catch (e) {
|
||||
onTIMCallback(
|
||||
TIMCallback(type: TIMCallbackType.INFO, infoRecommendText: "文件处理异常", infoCode: 6660416));
|
||||
}
|
||||
},
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxHeight: 72),
|
||||
child: Container(
|
||||
width: 170,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: theme.weakDividerColor ?? CommonColor.weakDividerColor,
|
||||
),
|
||||
borderRadius: borderRadius),
|
||||
child: Stack(children: [
|
||||
ClipRRect(
|
||||
borderRadius: borderRadius,
|
||||
child: LinearProgressIndicator(
|
||||
minHeight: ((containerHeight) ?? 72) - 6,
|
||||
value: (received == 100 ? 0 : received) / 100,
|
||||
backgroundColor: received == 100 ? theme.weakBackgroundColor : Colors.white,
|
||||
valueColor: AlwaysStoppedAnimation(theme.lightPrimaryMaterialColor.shade50),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12),
|
||||
child: Row(
|
||||
mainAxisAlignment: widget.isSelf ? MainAxisAlignment.end : MainAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
constraints: const BoxConstraints(maxWidth: 160),
|
||||
child: LayoutBuilder(
|
||||
builder: (buildContext, boxConstraints) {
|
||||
return CustomText(
|
||||
fileName,
|
||||
width: boxConstraints.maxWidth,
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
color: theme.darkTextColor,
|
||||
fontSize: 16,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
if (fileSize != null)
|
||||
Text(
|
||||
showFileSize(fileSize),
|
||||
style: TextStyle(fontSize: 14, color: theme.weakTextColor),
|
||||
)
|
||||
],
|
||||
)),
|
||||
TIMUIKitFileIcon(
|
||||
fileFormat: fileFormat,
|
||||
),
|
||||
])),
|
||||
]),
|
||||
),
|
||||
),
|
||||
)),
|
||||
if (!widget.isSelf && isWebDownloading)
|
||||
Container(
|
||||
margin: const EdgeInsets.only(top: 2),
|
||||
child: LoadingAnimationWidget.threeArchedCircle(
|
||||
color: theme.weakTextColor ?? Colors.grey,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
// ignore_for_file: must_be_immutable
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.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';
|
||||
|
||||
class TIMUIKitFileIcon extends TIMUIKitStatelessWidget {
|
||||
final String? fileFormat;
|
||||
final double? size;
|
||||
|
||||
TIMUIKitFileIcon( {this.size, this.fileFormat, Key? key}) : super(key: key);
|
||||
|
||||
Map fileMap = {
|
||||
"doc": "images/word.png",
|
||||
"docx": "images/word.png",
|
||||
"ppt": "images/ppt.png",
|
||||
"pptx": "images/ppt.png",
|
||||
"xls": "images/excel.png",
|
||||
"xlsx": "images/excel.png",
|
||||
"pdf": "images/pdf.png",
|
||||
"zip": "images/zip.png",
|
||||
"rar": "images/zip.png",
|
||||
"7z": "images/zip.png",
|
||||
"tar": "images/zip.png",
|
||||
"gz": "images/zip.png",
|
||||
"xz": "images/zip.png",
|
||||
"bz2": "images/zip.png",
|
||||
"txt": "images/txt.png",
|
||||
"jpg": "images/image_icon.png",
|
||||
"bmp": "images/image_icon.png",
|
||||
"gif": "images/image_icon.png",
|
||||
"png": "images/image_icon.png",
|
||||
"jpeg": "images/image_icon.png",
|
||||
"tif": "images/image_icon.png",
|
||||
"wmf": "images/image_icon.png",
|
||||
"dib": "images/image_icon.png",
|
||||
"mp4": "images/video_icon.png",
|
||||
"avi": "images/video_icon.png",
|
||||
"mov": "images/video_icon.png",
|
||||
"wmv": "images/video_icon.png",
|
||||
"flv": "images/video_icon.png",
|
||||
};
|
||||
|
||||
Widget _getFileIcon() {
|
||||
return Image.asset(
|
||||
fileMap[fileFormat?.toLowerCase()] ?? "images/unknown.png",
|
||||
package: 'tencent_cloud_chat_uikit',
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
return SizedBox(
|
||||
height: size ?? 50,
|
||||
width: size ?? 50,
|
||||
child: Container(padding: const EdgeInsets.all(4), child: _getFileIcon()),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import 'package:flutter/material.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'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_group_tips_elem.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/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';
|
||||
|
||||
class TIMUIKitGroupTipsElem extends StatefulWidget {
|
||||
final V2TimGroupTipsElem groupTipsElem;
|
||||
final List<V2TimGroupMemberFullInfo?> groupMemberList;
|
||||
|
||||
const TIMUIKitGroupTipsElem({Key? key, required this.groupMemberList, required this.groupTipsElem}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<TIMUIKitGroupTipsElem> createState() => _TIMUIKitGroupTipsElemState();
|
||||
}
|
||||
|
||||
class _TIMUIKitGroupTipsElemState extends TIMUIKitState<TIMUIKitGroupTipsElem> {
|
||||
String groupTipsAbstractText = "";
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
getText();
|
||||
}
|
||||
|
||||
void getText() async {
|
||||
final newText = await MessageUtils.groupTipsMessageAbstract(widget.groupTipsElem, widget.groupMemberList);
|
||||
setState(() {
|
||||
groupTipsAbstractText = newText;
|
||||
});
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,638 @@
|
||||
// ignore_for_file: prefer_typing_uninitialized_variables, unused_import
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:image_gallery_saver_plus/image_gallery_saver_plus.dart';
|
||||
import 'package:loading_animation_widget/loading_animation_widget.dart';
|
||||
import 'package:open_file/open_file.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:tencent_chat_i18n_tool/tencent_chat_i18n_tool.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_image.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_image.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_callback.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/business_logic/separate_models/tui_chat_separate_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_chat_global_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/message/message_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/theme/tui_theme.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/constants/history_message_constant.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/logger.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/message.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/permission.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/TIMUIKitChat/TIMUIKitMessageItem/TIMUIKitMessageReaction/tim_uikit_message_reaction_wrapper.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/image_screen.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/wide_popup.dart';
|
||||
import 'package:transparent_image/transparent_image.dart';
|
||||
import 'package:universal_html/html.dart' as html;
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class TIMUIKitImageElem extends StatefulWidget {
|
||||
final V2TimMessage message;
|
||||
final bool isShowJump;
|
||||
final VoidCallback? clearJump;
|
||||
final String? isFrom;
|
||||
final bool? isShowMessageReaction;
|
||||
final TUIChatSeparateViewModel chatModel;
|
||||
|
||||
const TIMUIKitImageElem(
|
||||
{required this.message,
|
||||
this.isShowJump = false,
|
||||
required this.chatModel,
|
||||
this.clearJump,
|
||||
this.isFrom,
|
||||
Key? key,
|
||||
this.isShowMessageReaction})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _TIMUIKitImageElem();
|
||||
}
|
||||
|
||||
class _TIMUIKitImageElem extends TIMUIKitState<TIMUIKitImageElem> {
|
||||
final TUIChatGlobalModel globalModel = serviceLocator<TUIChatGlobalModel>();
|
||||
final TUIChatGlobalModel model = serviceLocator<TUIChatGlobalModel>();
|
||||
final MessageService _messageService = serviceLocator<MessageService>();
|
||||
Widget? imageItem;
|
||||
|
||||
@override
|
||||
didUpdateWidget(oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
}
|
||||
|
||||
String getOriginImgURL() {
|
||||
// 实际拿的是原图
|
||||
V2TimImage? img =
|
||||
MessageUtils.getImageFromImgList(widget.message.imageElem!.imageList, HistoryMessageDartConstant.oriImgPrior);
|
||||
return img == null ? widget.message.imageElem!.path! : img.url!;
|
||||
}
|
||||
|
||||
Widget errorDisplay(BuildContext context, TUITheme? theme) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(5)),
|
||||
border: Border.all(
|
||||
width: 2,
|
||||
color: theme?.weakDividerColor ?? Colors.grey,
|
||||
)),
|
||||
height: 170,
|
||||
width: 170,
|
||||
child: Center(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
LoadingAnimationWidget.staggeredDotsWave(
|
||||
color: theme?.weakTextColor ?? Colors.grey,
|
||||
size: 28,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget getImage(image, {imageElem}) {
|
||||
Widget res = ClipRRect(
|
||||
clipper: ImageClipper(),
|
||||
child: image,
|
||||
);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
//保存网络图片到本地
|
||||
Future<void> _saveImageToLocal(
|
||||
context,
|
||||
String imageUrl, {
|
||||
bool isLocalResource = true,
|
||||
TUITheme? theme,
|
||||
}) async {
|
||||
if (PlatformUtils().isWeb) {
|
||||
download(imageUrl) async {
|
||||
final http.Response r = await http.get(Uri.parse(imageUrl));
|
||||
final data = r.bodyBytes;
|
||||
final base64data = base64Encode(data);
|
||||
final a = html.AnchorElement(href: 'data:image/jpeg;base64,$base64data');
|
||||
a.download = md5.convert(utf8.encode(imageUrl)).toString();
|
||||
a.click();
|
||||
a.remove();
|
||||
}
|
||||
|
||||
download(imageUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
if (PlatformUtils().isIOS) {
|
||||
if (!await Permissions.checkPermission(context, Permission.photosAddOnly.value, theme!, false)) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
final DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
|
||||
AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo;
|
||||
if (PlatformUtils().isMobile) {
|
||||
if ((androidInfo.version.sdkInt) >= 33) {
|
||||
final photos = await Permissions.checkPermission(
|
||||
context,
|
||||
Permission.photos.value,
|
||||
theme,
|
||||
);
|
||||
if (!photos) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
final storage = await Permissions.checkPermission(
|
||||
context,
|
||||
Permission.storage.value,
|
||||
);
|
||||
if (!storage) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isLocalResource) {
|
||||
if (widget.message.msgID == null || widget.message.msgID!.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (model.getMessageProgress(widget.message.msgID) == 100) {
|
||||
String savePath;
|
||||
if (widget.message.imageElem!.path != null &&
|
||||
widget.message.imageElem!.path != '' &&
|
||||
File(widget.message.imageElem!.path!).existsSync()) {
|
||||
savePath = widget.message.imageElem!.path!;
|
||||
} else {
|
||||
savePath = model.getFileMessageLocation(widget.message.msgID);
|
||||
}
|
||||
File f = File(savePath);
|
||||
if (f.existsSync()) {
|
||||
var result = await ImageGallerySaverPlus.saveFile(savePath);
|
||||
|
||||
if (PlatformUtils().isIOS) {
|
||||
if (result['isSuccess']) {
|
||||
onTIMCallback(
|
||||
TIMCallback(type: TIMCallbackType.INFO, infoRecommendText: TIM_t("图片保存成功"), infoCode: 6660406));
|
||||
} else {
|
||||
onTIMCallback(
|
||||
TIMCallback(type: TIMCallbackType.INFO, infoRecommendText: TIM_t("图片保存失败"), infoCode: 6660407));
|
||||
}
|
||||
} else {
|
||||
if (result != null) {
|
||||
onTIMCallback(
|
||||
TIMCallback(type: TIMCallbackType.INFO, infoRecommendText: TIM_t("图片保存成功"), infoCode: 6660406));
|
||||
} else {
|
||||
onTIMCallback(
|
||||
TIMCallback(type: TIMCallbackType.INFO, infoRecommendText: TIM_t("图片保存失败"), infoCode: 6660407));
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
onTIMCallback(TIMCallback(
|
||||
type: TIMCallbackType.INFO, infoRecommendText: TIM_t("the message is downloading"), infoCode: -1));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var result = await ImageGallerySaverPlus.saveFile(imageUrl);
|
||||
|
||||
if (PlatformUtils().isIOS) {
|
||||
if (result['isSuccess']) {
|
||||
onTIMCallback(TIMCallback(type: TIMCallbackType.INFO, infoRecommendText: TIM_t("图片保存成功"), infoCode: 6660406));
|
||||
} else {
|
||||
onTIMCallback(TIMCallback(type: TIMCallbackType.INFO, infoRecommendText: TIM_t("图片保存失败"), infoCode: 6660407));
|
||||
}
|
||||
} else {
|
||||
if (result != null) {
|
||||
onTIMCallback(TIMCallback(type: TIMCallbackType.INFO, infoRecommendText: TIM_t("图片保存成功"), infoCode: 6660406));
|
||||
} else {
|
||||
onTIMCallback(TIMCallback(type: TIMCallbackType.INFO, infoRecommendText: TIM_t("图片保存失败"), infoCode: 6660407));
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
Future<void> _saveImg(TUITheme theme) async {
|
||||
try {
|
||||
String? imageUrl;
|
||||
bool isAssetBool = false;
|
||||
final imageElem = widget.message.imageElem;
|
||||
|
||||
if (imageElem != null) {
|
||||
final originUrl = getOriginImgURL();
|
||||
final localUrl = imageElem.imageList?.firstOrNull?.localUrl;
|
||||
final filePath = imageElem.path;
|
||||
final isWeb = PlatformUtils().isWeb;
|
||||
|
||||
if (!isWeb && filePath != null && File(filePath).existsSync()) {
|
||||
imageUrl = filePath;
|
||||
isAssetBool = true;
|
||||
} else if (localUrl != null && (!isWeb && File(localUrl).existsSync())) {
|
||||
imageUrl = localUrl;
|
||||
isAssetBool = true;
|
||||
} else {
|
||||
imageUrl = originUrl;
|
||||
isAssetBool = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (imageUrl != null) {
|
||||
return await _saveImageToLocal(
|
||||
context,
|
||||
imageUrl,
|
||||
isLocalResource: isAssetBool,
|
||||
theme: theme,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
onTIMCallback(TIMCallback(infoCode: 6660414, infoRecommendText: TIM_t("正在下载中"), type: TIMCallbackType.INFO));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
V2TimImage? getImageFromList(V2TimImageTypesEnum imgType) {
|
||||
V2TimImage? img = MessageUtils.getImageFromImgList(widget.message.imageElem!.imageList,
|
||||
HistoryMessageDartConstant.imgPriorMap[imgType] ?? HistoryMessageDartConstant.oriImgPrior);
|
||||
|
||||
return img;
|
||||
}
|
||||
|
||||
void launchDesktopFile(String path) {
|
||||
if (PlatformUtils().isWindows) {
|
||||
OpenFile.open(path);
|
||||
} else {
|
||||
launchUrl(Uri.file(path));
|
||||
}
|
||||
}
|
||||
|
||||
Widget errorPage(theme) => Container(
|
||||
height: MediaQuery.of(context).size.height,
|
||||
color: theme.black,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: errorDisplay(context, theme),
|
||||
));
|
||||
|
||||
bool checkIfDownloadSuccess() {
|
||||
final localUrl = TencentUtils.checkString(model.getFileMessageLocation(widget.message.msgID)) ??
|
||||
widget.message.imageElem!.imageList![0]!.localUrl;
|
||||
return TencentUtils.checkString(localUrl) != null && File(localUrl!).existsSync();
|
||||
}
|
||||
|
||||
_onClickOpenImageInNewWindow() {
|
||||
final localUrl = TencentUtils.checkString(model.getFileMessageLocation(widget.message.msgID)) ??
|
||||
widget.message.imageElem!.imageList![0]!.localUrl;
|
||||
Future.delayed(const Duration(milliseconds: 0), () async {
|
||||
final isDownloaded = checkIfDownloadSuccess();
|
||||
if (isDownloaded) {
|
||||
launchDesktopFile(localUrl ?? "");
|
||||
} else {
|
||||
onTIMCallback(
|
||||
TIMCallback(infoCode: 6660414, infoRecommendText: TIM_t("正在下载原始资源,请稍候..."), type: TIMCallbackType.INFO));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_handleOnTapPreviewImageOnDesktop({
|
||||
double? positionRadio,
|
||||
String? originImgUrl,
|
||||
}) {
|
||||
final localUrl = TencentUtils.checkString(model.getFileMessageLocation(widget.message.msgID)) ??
|
||||
widget.message.imageElem!.imageList![0]!.localUrl;
|
||||
if (checkIfDownloadSuccess()) {
|
||||
TUIKitWidePopup.showMedia(
|
||||
aspectRatio: positionRadio,
|
||||
context: context,
|
||||
mediaLocalPath: localUrl ?? "",
|
||||
onClickOrigin: () => _onClickOpenImageInNewWindow());
|
||||
} else {
|
||||
if (TencentUtils.checkString(originImgUrl) != null) {
|
||||
TUIKitWidePopup.showMedia(
|
||||
aspectRatio: positionRadio,
|
||||
context: context,
|
||||
mediaURL: originImgUrl,
|
||||
onClickOrigin: () => _onClickOpenImageInNewWindow());
|
||||
} else {
|
||||
onTIMCallback(TIMCallback(infoCode: 6660414, infoRecommendText: TIM_t("正在下载中"), type: TIMCallbackType.INFO));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<double> calculateAspectRatio(ImageProvider imageProvider) async {
|
||||
Completer<double> completer = Completer<double>();
|
||||
|
||||
final imageStream = imageProvider.resolve(const ImageConfiguration());
|
||||
imageStream.addListener(
|
||||
ImageStreamListener((imageInfo, synchronousCall) {
|
||||
if (imageInfo.image.width != 0 && imageInfo.image.height != 0) {
|
||||
double aspectRatio = imageInfo.image.width / imageInfo.image.height;
|
||||
completer.complete(aspectRatio);
|
||||
} else {
|
||||
// If unable to calculate aspect ratio, return default value of 0.5
|
||||
completer.complete(0.5);
|
||||
}
|
||||
}, onError: (Object exception, StackTrace? stackTrace) {
|
||||
// If there's an error, return default value of 0.5
|
||||
completer.complete(1);
|
||||
}),
|
||||
);
|
||||
|
||||
return await completer.future;
|
||||
}
|
||||
|
||||
void onClickImage({
|
||||
required bool isNetworkImage,
|
||||
dynamic heroTag,
|
||||
required TUITheme theme,
|
||||
String? imgUrl,
|
||||
String? imgPath,
|
||||
}) {
|
||||
if (isNetworkImage) {
|
||||
if (PlatformUtils().isWeb) {
|
||||
TUIKitWidePopup.showMedia(
|
||||
context: context,
|
||||
mediaURL: widget.message.imageElem?.path ?? "",
|
||||
onClickOrigin: () => launchUrl(
|
||||
Uri.parse(widget.message.imageElem?.path ?? ""),
|
||||
mode: LaunchMode.externalApplication,
|
||||
));
|
||||
return;
|
||||
}
|
||||
if (PlatformUtils().isDesktop) {
|
||||
_handleOnTapPreviewImageOnDesktop(
|
||||
originImgUrl: imgUrl,
|
||||
);
|
||||
} else {
|
||||
Navigator.of(context).push(
|
||||
PageRouteBuilder(
|
||||
opaque: false,
|
||||
pageBuilder: (_, __, ___) => ImageScreen(
|
||||
imageProvider: CachedNetworkImageProvider(
|
||||
imgUrl ?? "",
|
||||
cacheKey: widget.message.msgID,
|
||||
),
|
||||
heroTag: heroTag,
|
||||
messageID: widget.message.msgID,
|
||||
downloadFn: () async {
|
||||
return await _saveImg(theme);
|
||||
})),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (PlatformUtils().isDesktop) {
|
||||
TUIKitWidePopup.showMedia(
|
||||
mediaLocalPath: imgPath, context: context, onClickOrigin: () => launchDesktopFile(imgPath ?? ""));
|
||||
} else {
|
||||
Navigator.of(context).push(
|
||||
PageRouteBuilder(
|
||||
opaque: false, // set to false
|
||||
pageBuilder: (_, __, ___) => ImageScreen(
|
||||
imageProvider: FileImage(File(imgPath ?? "")),
|
||||
heroTag: heroTag,
|
||||
messageID: widget.message.msgID,
|
||||
downloadFn: () async {
|
||||
return await _saveImg(theme);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Widget _renderAllImage(
|
||||
{dynamic heroTag,
|
||||
double? positionRadio,
|
||||
required TUITheme theme,
|
||||
bool isNetworkImage = false,
|
||||
String? webPath,
|
||||
V2TimImage? originalImg,
|
||||
V2TimImage? smallImg,
|
||||
String? smallLocalPath,
|
||||
String? originLocalPath}) {
|
||||
Widget getImageWidget() {
|
||||
if (isNetworkImage) {
|
||||
return Hero(
|
||||
tag: heroTag,
|
||||
child: PlatformUtils().isWeb
|
||||
? Image.network(webPath ?? smallImg?.url ?? originalImg!.url!, fit: BoxFit.contain)
|
||||
: CachedNetworkImage(
|
||||
alignment: Alignment.topCenter,
|
||||
imageUrl: webPath ?? smallImg?.url ?? originalImg!.url!,
|
||||
errorWidget: (context, error, stackTrace) => errorPage(theme),
|
||||
fit: BoxFit.contain,
|
||||
cacheKey: smallImg?.uuid ?? originalImg!.uuid,
|
||||
placeholder: (context, url) => Image(image: MemoryImage(kTransparentImage)),
|
||||
fadeInDuration: const Duration(milliseconds: 0),
|
||||
));
|
||||
} else {
|
||||
final imgPath = (TencentUtils.checkString(smallLocalPath) != null ? smallLocalPath : originLocalPath)!;
|
||||
return Hero(tag: heroTag, child: Image.file(File(imgPath), fit: BoxFit.contain));
|
||||
}
|
||||
}
|
||||
|
||||
double? currentPositionRadio;
|
||||
// File imgF = File((TencentUtils.checkString(originLocalPath) != null
|
||||
// ? originLocalPath
|
||||
// : smallLocalPath) ??
|
||||
// "");
|
||||
// bool isExist = imgF.existsSync();
|
||||
//
|
||||
// if (!isExist) {
|
||||
// return errorDisplay(context, theme);
|
||||
// }
|
||||
// Image image = Image.file(imgF);
|
||||
//
|
||||
// image.image
|
||||
// .resolve(const ImageConfiguration())
|
||||
// .addListener(ImageStreamListener((image, synchronousCall) {
|
||||
// if (image.image.width != 0 && image.image.height != 0) {
|
||||
// currentPositionRadio = image.image.width / image.image.height;
|
||||
// }
|
||||
// }));
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () => onClickImage(
|
||||
theme: theme,
|
||||
heroTag: heroTag,
|
||||
isNetworkImage: isNetworkImage,
|
||||
imgUrl: webPath ?? smallImg?.url ?? originalImg?.url ?? "",
|
||||
imgPath: (TencentUtils.checkString(originLocalPath) != null ? originLocalPath : smallLocalPath) ?? ""),
|
||||
child: Stack(
|
||||
children: [
|
||||
if (positionRadio != null)
|
||||
AspectRatio(
|
||||
aspectRatio: (currentPositionRadio ?? positionRadio)!,
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(color: Colors.transparent),
|
||||
),
|
||||
),
|
||||
getImageWidget(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void initImages() async {
|
||||
final zeroImageLocal = TencentUtils.checkString(
|
||||
widget.message.imageElem?.imageList?.firstWhereOrNull((element) => element?.type == 0)?.localUrl);
|
||||
final oneImageLocal = TencentUtils.checkString(
|
||||
widget.message.imageElem?.imageList?.firstWhereOrNull((element) => element?.type == 1)?.localUrl);
|
||||
final twoImageLocal = TencentUtils.checkString(
|
||||
widget.message.imageElem?.imageList?.firstWhereOrNull((element) => element?.type == 2)?.localUrl);
|
||||
|
||||
if (!PlatformUtils().isWeb && TencentUtils.checkString(widget.message.msgID) != null) {
|
||||
if ((widget.message.imageElem?.imageList) == null || widget.message.imageElem!.imageList!.isEmpty) {
|
||||
final response = await _messageService.getMessageOnlineUrl(msgID: widget.message.msgID!);
|
||||
final elem = response.data;
|
||||
if (elem != null && elem.imageElem != null) {
|
||||
widget.message.imageElem = elem.imageElem;
|
||||
}
|
||||
}
|
||||
if (oneImageLocal == null || !File(oneImageLocal).existsSync()) {
|
||||
_messageService.downloadMessage(msgID: widget.message.msgID!, messageType: 3, imageType: 1, isSnapshot: false);
|
||||
}
|
||||
if (twoImageLocal == null || !File(twoImageLocal).existsSync()) {
|
||||
_messageService.downloadMessage(msgID: widget.message.msgID!, messageType: 3, imageType: 2, isSnapshot: false);
|
||||
}
|
||||
if (zeroImageLocal == null || !File(zeroImageLocal).existsSync()) {
|
||||
_messageService.downloadMessage(msgID: widget.message.msgID!, messageType: 3, imageType: 0, isSnapshot: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
initImages();
|
||||
}
|
||||
|
||||
Widget? _renderImage(dynamic heroTag, TUITheme theme, {V2TimImage? originalImg, V2TimImage? smallImg}) {
|
||||
double positionRadio = 1.0;
|
||||
if (smallImg?.width != null && smallImg?.height != null && smallImg?.width != 0 && smallImg?.height != 0) {
|
||||
positionRadio = (smallImg!.width! / smallImg.height!);
|
||||
}
|
||||
|
||||
if (PlatformUtils().isWeb && widget.message.imageElem!.path != null) {
|
||||
// Displaying on Web only
|
||||
return _renderAllImage(
|
||||
heroTag: heroTag,
|
||||
theme: theme,
|
||||
isNetworkImage: true,
|
||||
smallImg: smallImg,
|
||||
originalImg: originalImg,
|
||||
positionRadio: positionRadio,
|
||||
webPath: widget.message.imageElem!.path);
|
||||
}
|
||||
|
||||
try {
|
||||
if ((widget.message.isSelf! &&
|
||||
widget.message.imageElem!.path != null &&
|
||||
widget.message.imageElem!.path!.isNotEmpty &&
|
||||
File(widget.message.imageElem!.path!).existsSync())) {
|
||||
return _renderAllImage(
|
||||
smallLocalPath: widget.message.imageElem!.path!,
|
||||
heroTag: heroTag,
|
||||
theme: theme,
|
||||
positionRadio: positionRadio,
|
||||
originLocalPath: widget.message.imageElem!.path!);
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore: avoid_print
|
||||
outputLogger.i(e.toString());
|
||||
}
|
||||
|
||||
try {
|
||||
if ((TencentUtils.checkString(smallImg?.localUrl) != null && File((smallImg?.localUrl!)!).existsSync()) ||
|
||||
(TencentUtils.checkString(originalImg?.localUrl) != null && File((originalImg?.localUrl!)!).existsSync())) {
|
||||
return _renderAllImage(
|
||||
smallLocalPath: smallImg?.localUrl ?? "",
|
||||
heroTag: heroTag,
|
||||
theme: theme,
|
||||
positionRadio: positionRadio,
|
||||
originLocalPath: originalImg?.localUrl);
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore: avoid_print
|
||||
outputLogger.i(e.toString());
|
||||
return _renderAllImage(
|
||||
heroTag: heroTag,
|
||||
theme: theme,
|
||||
isNetworkImage: true,
|
||||
smallImg: smallImg,
|
||||
positionRadio: positionRadio,
|
||||
originalImg: originalImg);
|
||||
}
|
||||
|
||||
if ((smallImg?.url ?? originalImg?.url) != null && (smallImg?.url ?? originalImg?.url)!.isNotEmpty) {
|
||||
return _renderAllImage(
|
||||
heroTag: heroTag,
|
||||
theme: theme,
|
||||
isNetworkImage: true,
|
||||
positionRadio: positionRadio,
|
||||
smallImg: smallImg,
|
||||
originalImg: originalImg);
|
||||
}
|
||||
|
||||
return errorDisplay(context, theme);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final theme = value.theme;
|
||||
final isDesktopScreen = TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop;
|
||||
final heroTag =
|
||||
"${widget.message.msgID ?? widget.message.id ?? widget.message.timestamp ?? DateTime.now().millisecondsSinceEpoch}${widget.isFrom}";
|
||||
|
||||
V2TimImage? originalImg = getImageFromList(V2TimImageTypesEnum.original);
|
||||
V2TimImage? smallImg = getImageFromList(V2TimImageTypesEnum.small);
|
||||
return TIMUIKitMessageReactionWrapper(
|
||||
chatModel: widget.chatModel,
|
||||
isShowJump: widget.isShowJump,
|
||||
clearJump: widget.clearJump,
|
||||
isFromSelf: widget.message.isSelf ?? true,
|
||||
isShowMessageReaction: widget.isShowMessageReaction ?? true,
|
||||
message: widget.message,
|
||||
child: LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) {
|
||||
return ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: constraints.maxWidth * (isDesktopScreen ? 0.4 : 0.5),
|
||||
minWidth: 64,
|
||||
maxHeight: 256,
|
||||
),
|
||||
child: _renderImage(heroTag, theme, originalImg: originalImg, smallImg: smallImg),
|
||||
);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
class ImageClipper extends CustomClipper<RRect> {
|
||||
@override
|
||||
RRect getClip(Size size) {
|
||||
return RRect.fromRectAndRadius(Rect.fromLTWH(0, 0, size.width, min(size.height, 256)), const Radius.circular(5));
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldReclip(CustomClipper<RRect> oldClipper) {
|
||||
return oldClipper != this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,422 @@
|
||||
// ignore_for_file: unused_import
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.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_message.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_message.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_chat_model_tools.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/common_utils.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/TIMUIKitChat/TIMUIKitTextField/special_text/DefaultSpecialTextSpanBuilder.dart';
|
||||
import 'package:extended_text/extended_text.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_callback.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/business_logic/separate_models/tui_chat_separate_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_chat_global_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/services_locatar.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/tui_theme.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitMessageItem/TIMUIKitMessageReaction/tim_uikit_message_reaction_show_panel.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitMessageItem/main.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitMessageItem/tim_uikit_chat_face_elem.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/tim_uikit_chat_config.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/tim_uikit_cloud_custom_data.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/link_preview/link_preview_entry.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/link_preview/models/link_preview_content.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/link_preview/widgets/link_preview.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/logger.dart';
|
||||
import 'package:tim_ui_kit_sticker_plugin/utils/tim_custom_face_data.dart';
|
||||
|
||||
class TIMUIKitReplyElem extends StatefulWidget {
|
||||
final V2TimMessage message;
|
||||
final Function scrollToIndex;
|
||||
final bool isShowJump;
|
||||
final VoidCallback clearJump;
|
||||
final TextStyle? fontStyle;
|
||||
final BorderRadius? borderRadius;
|
||||
final Color? backgroundColor;
|
||||
final EdgeInsetsGeometry? textPadding;
|
||||
final TUIChatSeparateViewModel chatModel;
|
||||
final bool? isShowMessageReaction;
|
||||
final List<CustomEmojiFaceData> customEmojiStickerList;
|
||||
|
||||
const TIMUIKitReplyElem({
|
||||
Key? key,
|
||||
required this.message,
|
||||
required this.scrollToIndex,
|
||||
this.isShowJump = false,
|
||||
required this.clearJump,
|
||||
this.fontStyle,
|
||||
this.borderRadius,
|
||||
this.isShowMessageReaction,
|
||||
this.backgroundColor,
|
||||
this.textPadding,
|
||||
this.customEmojiStickerList = const [],
|
||||
required this.chatModel,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _TIMUIKitReplyElemState();
|
||||
}
|
||||
|
||||
class _TIMUIKitReplyElemState extends TIMUIKitState<TIMUIKitReplyElem> {
|
||||
MessageRepliedData? repliedMessage;
|
||||
V2TimMessage? rawMessage;
|
||||
bool isShowJumpState = false;
|
||||
bool isShining = false;
|
||||
|
||||
MessageRepliedData? _getRepliedMessage() {
|
||||
try {
|
||||
final CloudCustomData messageCloudCustomData = CloudCustomData.fromJson(json.decode(
|
||||
TencentUtils.checkString(widget.message.cloudCustomData) != null ? widget.message.cloudCustomData! : "{}"));
|
||||
if (messageCloudCustomData.messageReply != null) {
|
||||
final MessageRepliedData repliedMessage = MessageRepliedData.fromJson(messageCloudCustomData.messageReply!);
|
||||
return repliedMessage;
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
_getMessageByMessageID() async {
|
||||
final MessageRepliedData? cloudCustomData = _getRepliedMessage();
|
||||
if (cloudCustomData != null) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
repliedMessage = cloudCustomData;
|
||||
});
|
||||
}
|
||||
|
||||
final messageID = cloudCustomData.messageID;
|
||||
if (PlatformUtils().isWeb) {
|
||||
return;
|
||||
}
|
||||
V2TimMessage? message = await widget.chatModel.findMessage(messageID);
|
||||
if (message == null) {
|
||||
try {
|
||||
final RepliedMessageAbstract repliedMessageAbstract =
|
||||
RepliedMessageAbstract.fromJson(jsonDecode(cloudCustomData.messageAbstract));
|
||||
if (repliedMessageAbstract.isNotEmpty) {
|
||||
message = V2TimMessage(
|
||||
elemType: 0,
|
||||
seq: repliedMessageAbstract.seq,
|
||||
timestamp: repliedMessageAbstract.timestamp,
|
||||
msgID: repliedMessageAbstract.msgID);
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore: avoid_print
|
||||
outputLogger.i(e.toString());
|
||||
}
|
||||
}
|
||||
if (message != null) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
rawMessage = message;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Widget _defaultRawMessageText(String text, TUITheme? theme) {
|
||||
return Text(text,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(fontSize: 12, color: theme?.weakTextColor, fontWeight: FontWeight.w400));
|
||||
}
|
||||
|
||||
_renderMessageSummary(TUITheme? theme) {
|
||||
try {
|
||||
final RepliedMessageAbstract repliedMessageAbstract =
|
||||
RepliedMessageAbstract.fromJson(jsonDecode(repliedMessage?.messageAbstract ?? ""));
|
||||
if (TencentUtils.checkString(repliedMessageAbstract.summary) != null) {
|
||||
return _defaultRawMessageText(repliedMessageAbstract.summary!, theme);
|
||||
}
|
||||
return _defaultRawMessageText(repliedMessage?.messageAbstract ?? TIM_t("[未知消息]"), theme);
|
||||
} catch (e) {
|
||||
return _defaultRawMessageText(repliedMessage?.messageAbstract ?? TIM_t("[未知消息]"), theme);
|
||||
}
|
||||
}
|
||||
|
||||
(bool isRevoke, bool isRevokeByAdmin) isRevokeMessage(V2TimMessage message) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_rawMessageBuilder(V2TimMessage? message, TUITheme? theme) {
|
||||
if (repliedMessage == null) {
|
||||
return const SizedBox(width: 0, height: 12);
|
||||
}
|
||||
if (message == null) {
|
||||
if (repliedMessage?.messageAbstract != null) {
|
||||
return _renderMessageSummary(theme);
|
||||
}
|
||||
return const SizedBox(width: 0, height: 12);
|
||||
}
|
||||
|
||||
final revokeStatus = isRevokeMessage(message);
|
||||
final isRevokedMsg = revokeStatus.$1;
|
||||
final isAdminRevoke = revokeStatus.$2;
|
||||
|
||||
if (isRevokedMsg) {
|
||||
return _defaultRawMessageText(isAdminRevoke ? TIM_t("[消息被管理员撤回]") : TIM_t("[消息被撤回]"), theme);
|
||||
}
|
||||
|
||||
final messageType = message.elemType;
|
||||
final isSelf = message.isSelf ?? true;
|
||||
final customAbstractMessage =
|
||||
widget.chatModel.abstractMessageBuilder != null ? widget.chatModel.abstractMessageBuilder!(message) : null;
|
||||
if (customAbstractMessage != null) {
|
||||
return _defaultRawMessageText(
|
||||
customAbstractMessage,
|
||||
theme,
|
||||
);
|
||||
}
|
||||
switch (messageType) {
|
||||
case MessageElemType.V2TIM_ELEM_TYPE_CUSTOM:
|
||||
return _defaultRawMessageText(TIM_t("[自定义]"), theme);
|
||||
case MessageElemType.V2TIM_ELEM_TYPE_SOUND:
|
||||
return _defaultRawMessageText(TIM_t("[语音消息]"), theme);
|
||||
case MessageElemType.V2TIM_ELEM_TYPE_TEXT:
|
||||
return _defaultRawMessageText(message.textElem?.text ?? "", theme);
|
||||
case MessageElemType.V2TIM_ELEM_TYPE_FACE:
|
||||
return TIMUIKitFaceElem(
|
||||
model: widget.chatModel,
|
||||
isShowJump: false,
|
||||
isShowMessageReaction: false,
|
||||
path: message.faceElem!.data ?? "",
|
||||
message: message,
|
||||
);
|
||||
case MessageElemType.V2TIM_ELEM_TYPE_FILE:
|
||||
return TIMUIKitFileElem(
|
||||
chatModel: widget.chatModel,
|
||||
isShowMessageReaction: false,
|
||||
message: message,
|
||||
messageID: message.msgID,
|
||||
fileElem: message.fileElem,
|
||||
isSelf: isSelf,
|
||||
isShowJump: false);
|
||||
case MessageElemType.V2TIM_ELEM_TYPE_IMAGE:
|
||||
return TIMUIKitImageElem(
|
||||
chatModel: widget.chatModel, message: message, isFrom: "reply", isShowMessageReaction: false);
|
||||
case MessageElemType.V2TIM_ELEM_TYPE_VIDEO:
|
||||
return TIMUIKitVideoElem(message, chatModel: widget.chatModel, isFrom: "reply", isShowMessageReaction: false);
|
||||
case MessageElemType.V2TIM_ELEM_TYPE_LOCATION:
|
||||
return _defaultRawMessageText(TIM_t("[位置]"), theme);
|
||||
case MessageElemType.V2TIM_ELEM_TYPE_MERGER:
|
||||
return TIMUIKitMergerElem(
|
||||
model: widget.chatModel,
|
||||
isShowJump: false,
|
||||
isShowMessageReaction: false,
|
||||
message: message,
|
||||
mergerElem: message.mergerElem!,
|
||||
messageID: message.msgID ?? "",
|
||||
isSelf: isSelf);
|
||||
default:
|
||||
return _renderMessageSummary(theme);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_getMessageByMessageID();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant TIMUIKitReplyElem oldWidget) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((mag) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
_getMessageByMessageID();
|
||||
});
|
||||
}
|
||||
|
||||
_showJumpColor() {
|
||||
if ((widget.chatModel.jumpMsgID != widget.message.msgID) && (widget.message.msgID?.isNotEmpty ?? true)) {
|
||||
return;
|
||||
}
|
||||
isShining = true;
|
||||
int shineAmount = 6;
|
||||
setState(() {
|
||||
isShowJumpState = true;
|
||||
});
|
||||
Timer.periodic(const Duration(milliseconds: 300), (timer) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
isShowJumpState = shineAmount.isOdd ? true : false;
|
||||
});
|
||||
}
|
||||
if (shineAmount == 0 || !mounted) {
|
||||
isShining = false;
|
||||
timer.cancel();
|
||||
}
|
||||
shineAmount--;
|
||||
});
|
||||
widget.clearJump();
|
||||
}
|
||||
|
||||
void _jumpToRawMsg() {
|
||||
if (rawMessage?.status != MessageStatus.V2TIM_MSG_STATUS_LOCAL_REVOKED && rawMessage?.timestamp != null) {
|
||||
widget.scrollToIndex(rawMessage);
|
||||
} else {
|
||||
onTIMCallback(TIMCallback(type: TIMCallbackType.INFO, infoRecommendText: TIM_t("无法定位到原消息")));
|
||||
}
|
||||
}
|
||||
|
||||
Widget? _renderPreviewWidget() {
|
||||
// If the link preview info from [localCustomData] is available, use it to render the preview card.
|
||||
// Otherwise, it will returns null.
|
||||
if (widget.message.localCustomData != null && widget.message.localCustomData!.isNotEmpty) {
|
||||
try {
|
||||
final String localJSON = widget.message.localCustomData!;
|
||||
final LocalCustomDataModel? localPreviewInfo = LocalCustomDataModel.fromMap(json.decode(localJSON));
|
||||
if (localPreviewInfo != null && !localPreviewInfo.isLinkPreviewEmpty()) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(top: 8),
|
||||
child:
|
||||
// You can use this default widget [LinkPreviewWidget] to render preview card, or you can use custom widget.
|
||||
LinkPreviewWidget(linkPreview: localPreviewInfo),
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final theme = value.theme;
|
||||
final isDesktopScreen = TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop;
|
||||
if (widget.isShowJump) {
|
||||
if (!isShining) {
|
||||
Future.delayed(Duration.zero, () {
|
||||
_showJumpColor();
|
||||
});
|
||||
} else {
|
||||
if ((widget.chatModel.jumpMsgID == widget.message.msgID) && (widget.message.msgID?.isNotEmpty ?? false)) {
|
||||
widget.clearJump();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final isFromSelf = widget.message.isSelf ?? true;
|
||||
|
||||
final defaultStyle = isFromSelf
|
||||
? (theme.chatMessageItemFromSelfBgColor ?? theme.lightPrimaryMaterialColor.shade50)
|
||||
: (theme.chatMessageItemFromOthersBgColor);
|
||||
|
||||
final backgroundColor =
|
||||
isShowJumpState ? const Color.fromRGBO(245, 166, 35, 1) : (defaultStyle ?? widget.backgroundColor);
|
||||
|
||||
final borderRadius = isFromSelf
|
||||
? const BorderRadius.only(
|
||||
topLeft: Radius.circular(10),
|
||||
topRight: Radius.circular(2),
|
||||
bottomLeft: Radius.circular(10),
|
||||
bottomRight: Radius.circular(10))
|
||||
: const BorderRadius.only(
|
||||
topLeft: Radius.circular(2),
|
||||
topRight: Radius.circular(10),
|
||||
bottomLeft: Radius.circular(10),
|
||||
bottomRight: Radius.circular(10));
|
||||
final textWithLink = LinkPreviewEntry.getHyperlinksText(
|
||||
widget.message.textElem?.text ?? "", widget.chatModel.chatConfig.isSupportMarkdownForTextMessage,
|
||||
onLinkTap: widget.chatModel.chatConfig.onTapLink,
|
||||
isUseQQPackage: widget.chatModel.chatConfig.stickerPanelConfig?.useQQStickerPackage ?? true,
|
||||
isUseTencentCloudChatPackage:
|
||||
widget.chatModel.chatConfig.stickerPanelConfig?.useTencentCloudChatStickerPackage ?? true,
|
||||
isUseTencentCloudChatPackageOldKeys:
|
||||
widget.chatModel.chatConfig.stickerPanelConfig?.useTencentCloudChatStickerPackageOldKeys ?? false,
|
||||
customEmojiStickerList: widget.customEmojiStickerList,
|
||||
isEnableTextSelection: widget.chatModel.chatConfig.isEnableTextSelection ?? false);
|
||||
return Container(
|
||||
padding: widget.textPadding ?? EdgeInsets.all(isDesktopScreen ? 12 : 10),
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
borderRadius: widget.borderRadius ?? borderRadius,
|
||||
),
|
||||
constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.6),
|
||||
child: GestureDetector(
|
||||
onTap: _jumpToRawMsg,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
// 这里是引用的部分
|
||||
padding: const EdgeInsets.symmetric(vertical: 3, horizontal: 6),
|
||||
constraints: const BoxConstraints(minWidth: 120),
|
||||
decoration: const BoxDecoration(
|
||||
color: Color.fromRGBO(68, 68, 68, 0.05),
|
||||
border: Border(left: BorderSide(color: Color.fromRGBO(68, 68, 68, 0.1), width: 2))),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
repliedMessage != null ? "${repliedMessage!.messageSender}:" : "",
|
||||
style: TextStyle(fontSize: 12, color: theme.weakTextColor, fontWeight: FontWeight.w500),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 4,
|
||||
),
|
||||
_rawMessageBuilder(rawMessage, theme)
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
// If the [elemType] is text message, it will not be null here.
|
||||
// You can render the widget from extension directly, with a [TextStyle] optionally.
|
||||
widget.chatModel.chatConfig.urlPreviewType != UrlPreviewType.none
|
||||
? textWithLink!(
|
||||
style: widget.fontStyle ??
|
||||
TextStyle(
|
||||
fontSize: isDesktopScreen ? 14 : 16,
|
||||
textBaseline: TextBaseline.ideographic,
|
||||
height: widget.chatModel.chatConfig.textHeight))
|
||||
: ExtendedText(widget.message.textElem?.text ?? "",
|
||||
softWrap: true,
|
||||
style: widget.fontStyle ??
|
||||
TextStyle(fontSize: isDesktopScreen ? 14 : 16, height: widget.chatModel.chatConfig.textHeight),
|
||||
specialTextSpanBuilder: DefaultSpecialTextSpanBuilder(
|
||||
isUseQQPackage: widget.chatModel.chatConfig.stickerPanelConfig?.useQQStickerPackage ?? true,
|
||||
isUseTencentCloudChatPackage:
|
||||
widget.chatModel.chatConfig.stickerPanelConfig?.useTencentCloudChatStickerPackage ?? true,
|
||||
isUseTencentCloudChatPackageOldKeys:
|
||||
widget.chatModel.chatConfig.stickerPanelConfig?.useTencentCloudChatStickerPackageOldKeys ??
|
||||
false,
|
||||
customEmojiStickerList: widget.customEmojiStickerList,
|
||||
showAtBackground: true,
|
||||
)),
|
||||
// If the link preview info is available, render the preview card.
|
||||
if (_renderPreviewWidget() != null &&
|
||||
widget.chatModel.chatConfig.urlPreviewType == UrlPreviewType.previewCardAndHyperlink)
|
||||
_renderPreviewWidget()!,
|
||||
if (widget.isShowMessageReaction ?? true) TIMUIKitMessageReactionShowPanel(message: widget.message)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:just_audio/just_audio.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_sound_elem.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_sound_elem.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/business_logic/separate_models/tui_chat_separate_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_chat_global_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/message/message_services.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/services_locatar.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/constants/history_message_constant.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/platform.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/sound_record.dart';
|
||||
import 'TIMUIKitMessageReaction/tim_uikit_message_reaction_show_panel.dart';
|
||||
|
||||
class TIMUIKitSoundElem extends StatefulWidget {
|
||||
final V2TimMessage message;
|
||||
final V2TimSoundElem soundElem;
|
||||
final String msgID;
|
||||
final bool isFromSelf;
|
||||
final int? localCustomInt;
|
||||
final bool isShowJump;
|
||||
final VoidCallback? clearJump;
|
||||
final TextStyle? fontStyle;
|
||||
final BorderRadius? borderRadius;
|
||||
final Color? backgroundColor;
|
||||
final EdgeInsetsGeometry? textPadding;
|
||||
final bool? isShowMessageReaction;
|
||||
final TUIChatSeparateViewModel chatModel;
|
||||
|
||||
const TIMUIKitSoundElem(
|
||||
{Key? key,
|
||||
required this.soundElem,
|
||||
required this.msgID,
|
||||
required this.isFromSelf,
|
||||
this.isShowJump = false,
|
||||
this.clearJump,
|
||||
this.localCustomInt,
|
||||
this.fontStyle,
|
||||
this.borderRadius,
|
||||
this.backgroundColor,
|
||||
this.textPadding,
|
||||
required this.message,
|
||||
this.isShowMessageReaction,
|
||||
required this.chatModel})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _TIMUIKitSoundElemState();
|
||||
}
|
||||
|
||||
class _TIMUIKitSoundElemState extends TIMUIKitState<TIMUIKitSoundElem> {
|
||||
final int charLen = 8;
|
||||
bool isPlaying = false;
|
||||
StreamSubscription<Object>? subscription;
|
||||
bool isShowJumpState = false;
|
||||
bool isShining = false;
|
||||
final TUIChatGlobalModel globalModel = serviceLocator<TUIChatGlobalModel>();
|
||||
final MessageService _messageService = serviceLocator<MessageService>();
|
||||
late V2TimSoundElem stateElement = widget.message.soundElem!;
|
||||
|
||||
_playSound() async {
|
||||
if (!SoundPlayer.isInit) {
|
||||
SoundPlayer.initSoundPlayer();
|
||||
}
|
||||
if (widget.localCustomInt == null || widget.localCustomInt != HistoryMessageDartConstant.read) {
|
||||
globalModel.setLocalCustomInt(widget.msgID, HistoryMessageDartConstant.read, widget.chatModel.conversationID);
|
||||
}
|
||||
if (isPlaying) {
|
||||
SoundPlayer.stop();
|
||||
widget.chatModel.currentPlayedMsgId = "";
|
||||
} else {
|
||||
SoundPlayer.play(url: stateElement.url!);
|
||||
widget.chatModel.currentPlayedMsgId = widget.msgID;
|
||||
|
||||
setState(() {
|
||||
isPlaying = widget.chatModel.currentPlayedMsgId != '' && widget.chatModel.currentPlayedMsgId == widget.msgID;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
downloadMessageDetailAndSave() async {
|
||||
if (widget.message.msgID != null && widget.message.msgID != '') {
|
||||
if (widget.message.soundElem!.url == null || widget.message.soundElem!.url == '') {
|
||||
final response = await _messageService.getMessageOnlineUrl(msgID: widget.message.msgID!);
|
||||
if (response.data != null) {
|
||||
widget.message.soundElem = response.data!.soundElem;
|
||||
Future.delayed(const Duration(microseconds: 10), () {
|
||||
setState(() => stateElement = response.data!.soundElem!);
|
||||
});
|
||||
}
|
||||
}
|
||||
if (!PlatformUtils().isWeb) {
|
||||
if (widget.message.soundElem!.localUrl == null || widget.message.soundElem!.localUrl == '') {
|
||||
_messageService.downloadMessage(
|
||||
msgID: widget.message.msgID!, messageType: 4, imageType: 0, isSnapshot: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
subscription = SoundPlayer.playStateListener(listener: (PlayerState state) {
|
||||
if (state.processingState == ProcessingState.completed) {
|
||||
widget.chatModel.currentPlayedMsgId = "";
|
||||
setState(() {
|
||||
isPlaying = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
downloadMessageDetailAndSave();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (isPlaying) {
|
||||
SoundPlayer.stop();
|
||||
widget.chatModel.currentPlayedMsgId = "";
|
||||
}
|
||||
subscription?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
double _getSoundLen() {
|
||||
double soundLen = 32;
|
||||
if (stateElement.duration != null) {
|
||||
final realSoundLen = stateElement.duration!;
|
||||
int sdLen = 32;
|
||||
if (realSoundLen > 10) {
|
||||
sdLen = 12 * charLen + ((realSoundLen - 10) * charLen / 0.5).floor();
|
||||
} else if (realSoundLen > 2) {
|
||||
sdLen = 2 * charLen + realSoundLen * charLen;
|
||||
}
|
||||
sdLen = min(sdLen, 20 * charLen);
|
||||
soundLen = sdLen.toDouble();
|
||||
}
|
||||
|
||||
return soundLen;
|
||||
}
|
||||
|
||||
_showJumpColor() {
|
||||
if ((widget.chatModel.jumpMsgID != widget.message.msgID) && (widget.message.msgID?.isNotEmpty ?? true)) {
|
||||
return;
|
||||
}
|
||||
isShining = true;
|
||||
int shineAmount = 6;
|
||||
setState(() {
|
||||
isShowJumpState = true;
|
||||
});
|
||||
Timer.periodic(const Duration(milliseconds: 300), (timer) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
isShowJumpState = shineAmount.isOdd ? true : false;
|
||||
});
|
||||
}
|
||||
if (shineAmount == 0 || !mounted) {
|
||||
isShining = false;
|
||||
timer.cancel();
|
||||
}
|
||||
shineAmount--;
|
||||
});
|
||||
widget.clearJump!();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final theme = value.theme;
|
||||
|
||||
final backgroundColor = widget.isFromSelf
|
||||
? (theme.chatMessageItemFromSelfBgColor ?? theme.lightPrimaryMaterialColor.shade50)
|
||||
: (theme.chatMessageItemFromOthersBgColor);
|
||||
|
||||
final borderRadius = widget.isFromSelf
|
||||
? const BorderRadius.only(
|
||||
topLeft: Radius.circular(10),
|
||||
topRight: Radius.circular(2),
|
||||
bottomLeft: Radius.circular(10),
|
||||
bottomRight: Radius.circular(10))
|
||||
: const BorderRadius.only(
|
||||
topLeft: Radius.circular(2),
|
||||
topRight: Radius.circular(10),
|
||||
bottomLeft: Radius.circular(10),
|
||||
bottomRight: Radius.circular(10));
|
||||
if (widget.isShowJump) {
|
||||
if (!isShining) {
|
||||
Future.delayed(Duration.zero, () {
|
||||
_showJumpColor();
|
||||
});
|
||||
} else {
|
||||
if ((widget.chatModel.jumpMsgID == widget.message.msgID) && (widget.message.msgID?.isNotEmpty ?? false)) {
|
||||
widget.clearJump!();
|
||||
}
|
||||
}
|
||||
}
|
||||
return GestureDetector(
|
||||
onTap: () => _playSound(),
|
||||
child: Container(
|
||||
padding: widget.textPadding ?? const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: isShowJumpState ? const Color.fromRGBO(245, 166, 35, 1) : (widget.backgroundColor ?? backgroundColor),
|
||||
borderRadius: widget.borderRadius ?? borderRadius,
|
||||
),
|
||||
constraints: const BoxConstraints(maxWidth: 240),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: widget.isFromSelf
|
||||
? [
|
||||
Container(width: _getSoundLen()),
|
||||
Text(
|
||||
"''${stateElement.duration} ",
|
||||
style: widget.fontStyle,
|
||||
),
|
||||
isPlaying
|
||||
? Image.asset(
|
||||
'images/play_voice_send.gif',
|
||||
package: 'tencent_cloud_chat_uikit',
|
||||
width: 16,
|
||||
height: 16,
|
||||
)
|
||||
: Image.asset(
|
||||
'images/voice_send.png',
|
||||
package: 'tencent_cloud_chat_uikit',
|
||||
width: 16,
|
||||
height: 16,
|
||||
),
|
||||
]
|
||||
: [
|
||||
isPlaying
|
||||
? Image.asset(
|
||||
'images/play_voice_receive.gif',
|
||||
package: 'tencent_cloud_chat_uikit',
|
||||
width: 16,
|
||||
height: 16,
|
||||
)
|
||||
: Image.asset(
|
||||
'images/voice_receive.png',
|
||||
width: 16,
|
||||
height: 16,
|
||||
package: 'tencent_cloud_chat_uikit',
|
||||
),
|
||||
Text(
|
||||
" ${stateElement.duration}''",
|
||||
style: widget.fontStyle,
|
||||
),
|
||||
Container(width: _getSoundLen()),
|
||||
],
|
||||
),
|
||||
if (widget.isShowMessageReaction ?? true)
|
||||
TIMUIKitMessageReactionShowPanel(
|
||||
message: widget.message,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
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/ui/utils/screen_utils.dart';
|
||||
import 'package:extended_text/extended_text.dart';
|
||||
import 'package:flutter/material.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/business_logic/separate_models/tui_chat_separate_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitTextField/special_text/DefaultSpecialTextSpanBuilder.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/link_preview/link_preview_entry.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/link_preview/widgets/link_preview.dart';
|
||||
import 'TIMUIKitMessageReaction/tim_uikit_message_reaction_show_panel.dart';
|
||||
|
||||
class TIMUIKitTextElem extends StatefulWidget {
|
||||
final V2TimMessage message;
|
||||
final bool isFromSelf;
|
||||
final bool isShowJump;
|
||||
final VoidCallback clearJump;
|
||||
final TextStyle? fontStyle;
|
||||
final BorderRadius? borderRadius;
|
||||
final Color? backgroundColor;
|
||||
final EdgeInsetsGeometry? textPadding;
|
||||
final TUIChatSeparateViewModel chatModel;
|
||||
final bool? isShowMessageReaction;
|
||||
final List<CustomEmojiFaceData> customEmojiStickerList;
|
||||
|
||||
const TIMUIKitTextElem(
|
||||
{Key? key,
|
||||
required this.message,
|
||||
required this.isFromSelf,
|
||||
required this.isShowJump,
|
||||
required this.clearJump,
|
||||
this.fontStyle,
|
||||
this.borderRadius,
|
||||
this.isShowMessageReaction,
|
||||
this.backgroundColor,
|
||||
this.textPadding,
|
||||
required this.chatModel,
|
||||
this.customEmojiStickerList = const []})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _TIMUIKitTextElemState();
|
||||
}
|
||||
|
||||
class _TIMUIKitTextElemState extends TIMUIKitState<TIMUIKitTextElem> {
|
||||
bool isShowJumpState = false;
|
||||
bool isShining = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// get the link preview info
|
||||
_getLinkPreview();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(TIMUIKitTextElem oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.message.msgID == null && widget.message.msgID != null) {
|
||||
_getLinkPreview();
|
||||
}
|
||||
}
|
||||
|
||||
_showJumpColor() {
|
||||
if ((widget.chatModel.jumpMsgID != widget.message.msgID) && (widget.message.msgID?.isNotEmpty ?? true)) {
|
||||
return;
|
||||
}
|
||||
isShining = true;
|
||||
int shineAmount = 6;
|
||||
setState(() {
|
||||
isShowJumpState = true;
|
||||
});
|
||||
Timer.periodic(const Duration(milliseconds: 300), (timer) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
isShowJumpState = shineAmount.isOdd ? true : false;
|
||||
});
|
||||
}
|
||||
if (shineAmount == 0 || !mounted) {
|
||||
isShining = false;
|
||||
timer.cancel();
|
||||
}
|
||||
shineAmount--;
|
||||
});
|
||||
Future.delayed(const Duration(milliseconds: 100), () {
|
||||
widget.clearJump();
|
||||
});
|
||||
}
|
||||
|
||||
// get the link preview info
|
||||
_getLinkPreview() {
|
||||
if (widget.chatModel.chatConfig.urlPreviewType != UrlPreviewType.previewCardAndHyperlink) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (widget.message.localCustomData != null && widget.message.localCustomData!.isNotEmpty) {
|
||||
final String localJSON = widget.message.localCustomData!;
|
||||
final LocalCustomDataModel? localPreviewInfo = LocalCustomDataModel.fromMap(json.decode(localJSON));
|
||||
// If [localCustomData] is not empty, check if the link preview info exists
|
||||
if (localPreviewInfo == null || localPreviewInfo.isLinkPreviewEmpty()) {
|
||||
// If not exists, get it
|
||||
_initLinkPreview();
|
||||
}
|
||||
} else {
|
||||
// It [localCustomData] is empty, get the link info
|
||||
_initLinkPreview();
|
||||
}
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
_initLinkPreview() async {
|
||||
// Get the link preview info from extension, let it update the message UI automatically by providing a [onUpdateMessage].
|
||||
// The `onUpdateMessage` can use the `updateMessage()` from the [TIMUIKitChatController] directly.
|
||||
LinkPreviewEntry.getFirstLinkPreviewContent(
|
||||
message: widget.message,
|
||||
onUpdateMessage: (message) {
|
||||
widget.chatModel.updateMessageFromController(msgID: widget.message.msgID!, message: message);
|
||||
});
|
||||
}
|
||||
|
||||
Widget? _renderPreviewWidget() {
|
||||
// If the link preview info from [localCustomData] is available, use it to render the preview card.
|
||||
// Otherwise, it will returns null.
|
||||
if (widget.message.localCustomData != null && widget.message.localCustomData!.isNotEmpty) {
|
||||
try {
|
||||
final String localJSON = widget.message.localCustomData!;
|
||||
final LocalCustomDataModel? localPreviewInfo = LocalCustomDataModel.fromMap(json.decode(localJSON));
|
||||
if (localPreviewInfo != null && !localPreviewInfo.isLinkPreviewEmpty()) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(top: 8),
|
||||
child:
|
||||
// You can use this default widget [LinkPreviewWidget] to render preview card, or you can use custom widget.
|
||||
LinkPreviewWidget(linkPreview: localPreviewInfo),
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final theme = value.theme;
|
||||
final isDesktopScreen = TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop;
|
||||
final textWithLink = LinkPreviewEntry.getHyperlinksText(
|
||||
widget.message.textElem?.text ?? "", widget.chatModel.chatConfig.isSupportMarkdownForTextMessage,
|
||||
onLinkTap: widget.chatModel.chatConfig.onTapLink,
|
||||
isUseQQPackage: widget.chatModel.chatConfig.stickerPanelConfig?.useQQStickerPackage ?? true,
|
||||
isUseTencentCloudChatPackage:
|
||||
widget.chatModel.chatConfig.stickerPanelConfig?.useTencentCloudChatStickerPackage ?? true,
|
||||
isUseTencentCloudChatPackageOldKeys:
|
||||
widget.chatModel.chatConfig.stickerPanelConfig?.useTencentCloudChatStickerPackageOldKeys ?? false,
|
||||
customEmojiStickerList: widget.customEmojiStickerList,
|
||||
isEnableTextSelection: widget.chatModel.chatConfig.isEnableTextSelection ?? false);
|
||||
final borderRadius = widget.isFromSelf
|
||||
? const BorderRadius.only(
|
||||
topLeft: Radius.circular(10),
|
||||
topRight: Radius.circular(2),
|
||||
bottomLeft: Radius.circular(10),
|
||||
bottomRight: Radius.circular(10))
|
||||
: const BorderRadius.only(
|
||||
topLeft: Radius.circular(2),
|
||||
topRight: Radius.circular(10),
|
||||
bottomLeft: Radius.circular(10),
|
||||
bottomRight: Radius.circular(10));
|
||||
if ((widget.chatModel.jumpMsgID == widget.message.msgID)) {}
|
||||
if (widget.isShowJump) {
|
||||
if (!isShining) {
|
||||
Future.delayed(Duration.zero, () {
|
||||
_showJumpColor();
|
||||
});
|
||||
} else {
|
||||
if ((widget.chatModel.jumpMsgID == widget.message.msgID) && (widget.message.msgID?.isNotEmpty ?? false)) {
|
||||
widget.clearJump();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final defaultStyle = widget.isFromSelf
|
||||
? (theme.chatMessageItemFromSelfBgColor ?? theme.lightPrimaryMaterialColor.shade50)
|
||||
: (theme.chatMessageItemFromOthersBgColor);
|
||||
|
||||
final backgroundColor =
|
||||
isShowJumpState ? const Color.fromRGBO(245, 166, 35, 1) : (defaultStyle ?? widget.backgroundColor);
|
||||
|
||||
return Container(
|
||||
padding: widget.textPadding ?? EdgeInsets.all(isDesktopScreen ? 12 : 10),
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
borderRadius: widget.borderRadius ?? borderRadius,
|
||||
),
|
||||
constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.6),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// If the [elemType] is text message, it will not be null here.
|
||||
// You can render the widget from extension directly, with a [TextStyle] optionally.
|
||||
widget.chatModel.chatConfig.urlPreviewType != UrlPreviewType.none
|
||||
? textWithLink!(
|
||||
style: widget.fontStyle ??
|
||||
TextStyle(
|
||||
fontSize: isDesktopScreen ? 14 : 16,
|
||||
textBaseline: TextBaseline.ideographic,
|
||||
height: widget.chatModel.chatConfig.textHeight))
|
||||
: ExtendedText(widget.message.textElem?.text ?? "",
|
||||
softWrap: true,
|
||||
style: widget.fontStyle ??
|
||||
TextStyle(fontSize: isDesktopScreen ? 14 : 16, height: widget.chatModel.chatConfig.textHeight),
|
||||
specialTextSpanBuilder: DefaultSpecialTextSpanBuilder(
|
||||
isUseQQPackage: widget.chatModel.chatConfig.stickerPanelConfig?.useQQStickerPackage ?? true,
|
||||
isUseTencentCloudChatPackage:
|
||||
widget.chatModel.chatConfig.stickerPanelConfig?.useTencentCloudChatStickerPackage ?? true,
|
||||
isUseTencentCloudChatPackageOldKeys:
|
||||
widget.chatModel.chatConfig.stickerPanelConfig?.useTencentCloudChatStickerPackageOldKeys ??
|
||||
false,
|
||||
customEmojiStickerList: widget.customEmojiStickerList,
|
||||
showAtBackground: true,
|
||||
checkHttpLink: true,
|
||||
)),
|
||||
// If the link preview info is available, render the preview card.
|
||||
if (_renderPreviewWidget() != null &&
|
||||
widget.chatModel.chatConfig.urlPreviewType == UrlPreviewType.previewCardAndHyperlink)
|
||||
_renderPreviewWidget()!,
|
||||
if (widget.isShowMessageReaction ?? true) TIMUIKitMessageReactionShowPanel(message: widget.message)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+190
@@ -0,0 +1,190 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:tencent_chat_i18n_tool/tencent_chat_i18n_tool.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/ui/utils/screen_utils.dart';
|
||||
import 'package:extended_text/extended_text.dart';
|
||||
import 'package:flutter/material.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/business_logic/separate_models/tui_chat_separate_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitTextField/special_text/DefaultSpecialTextSpanBuilder.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/link_preview/link_preview_entry.dart';
|
||||
|
||||
class TIMUIKitTextTranslationElem extends StatefulWidget {
|
||||
final V2TimMessage message;
|
||||
final bool isFromSelf;
|
||||
final bool isShowJump;
|
||||
final VoidCallback clearJump;
|
||||
final TextStyle? fontStyle;
|
||||
final BorderRadius? borderRadius;
|
||||
final Color? backgroundColor;
|
||||
final EdgeInsetsGeometry? textPadding;
|
||||
final TUIChatSeparateViewModel chatModel;
|
||||
final bool? isShowMessageReaction;
|
||||
final List<CustomEmojiFaceData> customEmojiStickerList;
|
||||
|
||||
const TIMUIKitTextTranslationElem(
|
||||
{Key? key,
|
||||
required this.message,
|
||||
required this.isFromSelf,
|
||||
required this.isShowJump,
|
||||
required this.clearJump,
|
||||
this.fontStyle,
|
||||
this.borderRadius,
|
||||
this.isShowMessageReaction,
|
||||
this.backgroundColor,
|
||||
this.textPadding,
|
||||
required this.chatModel,
|
||||
this.customEmojiStickerList = const []})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _TIMUIKitTextTranslationElemState();
|
||||
}
|
||||
|
||||
class _TIMUIKitTextTranslationElemState extends TIMUIKitState<TIMUIKitTextTranslationElem> {
|
||||
bool isShowJumpState = false;
|
||||
bool isShining = false;
|
||||
|
||||
_showJumpColor() {
|
||||
if ((widget.chatModel.jumpMsgID != widget.message.msgID) && (widget.message.msgID?.isNotEmpty ?? true)) {
|
||||
return;
|
||||
}
|
||||
isShining = true;
|
||||
int shineAmount = 6;
|
||||
setState(() {
|
||||
isShowJumpState = true;
|
||||
});
|
||||
Timer.periodic(const Duration(milliseconds: 300), (timer) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
isShowJumpState = shineAmount.isOdd ? true : false;
|
||||
});
|
||||
}
|
||||
if (shineAmount == 0 || !mounted) {
|
||||
isShining = false;
|
||||
timer.cancel();
|
||||
}
|
||||
shineAmount--;
|
||||
});
|
||||
Future.delayed(const Duration(milliseconds: 100), () {
|
||||
widget.clearJump();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final theme = value.theme;
|
||||
final isDesktopScreen = TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop;
|
||||
final borderRadius = widget.isFromSelf
|
||||
? const BorderRadius.only(
|
||||
topLeft: Radius.circular(10),
|
||||
topRight: Radius.circular(2),
|
||||
bottomLeft: Radius.circular(10),
|
||||
bottomRight: Radius.circular(10))
|
||||
: const BorderRadius.only(
|
||||
topLeft: Radius.circular(2),
|
||||
topRight: Radius.circular(10),
|
||||
bottomLeft: Radius.circular(10),
|
||||
bottomRight: Radius.circular(10));
|
||||
if ((widget.chatModel.jumpMsgID == widget.message.msgID)) {}
|
||||
if (widget.isShowJump) {
|
||||
if (!isShining) {
|
||||
Future.delayed(Duration.zero, () {
|
||||
_showJumpColor();
|
||||
});
|
||||
} else {
|
||||
if ((widget.chatModel.jumpMsgID == widget.message.msgID) && (widget.message.msgID?.isNotEmpty ?? false)) {
|
||||
widget.clearJump();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final defaultStyle = widget.isFromSelf
|
||||
? (theme.chatMessageItemFromSelfBgColor ?? theme.lightPrimaryMaterialColor.shade50)
|
||||
: (theme.chatMessageItemFromOthersBgColor);
|
||||
|
||||
final backgroundColor =
|
||||
isShowJumpState ? const Color.fromRGBO(245, 166, 35, 1) : (defaultStyle ?? widget.backgroundColor);
|
||||
|
||||
final LocalCustomDataModel localCustomData =
|
||||
LocalCustomDataModel.fromMap(json.decode(TencentUtils.checkString(widget.message.localCustomData) ?? "{}"));
|
||||
final String? translateText = localCustomData.translatedText;
|
||||
|
||||
final textWithLink = LinkPreviewEntry.getHyperlinksText(
|
||||
translateText ?? "", widget.chatModel.chatConfig.isSupportMarkdownForTextMessage,
|
||||
onLinkTap: widget.chatModel.chatConfig.onTapLink,
|
||||
isUseQQPackage: widget.chatModel.chatConfig.stickerPanelConfig?.useQQStickerPackage ?? true,
|
||||
isUseTencentCloudChatPackage:
|
||||
widget.chatModel.chatConfig.stickerPanelConfig?.useTencentCloudChatStickerPackage ?? true,
|
||||
isUseTencentCloudChatPackageOldKeys:
|
||||
widget.chatModel.chatConfig.stickerPanelConfig?.useTencentCloudChatStickerPackageOldKeys ?? false,
|
||||
customEmojiStickerList: widget.customEmojiStickerList,
|
||||
isEnableTextSelection: widget.chatModel.chatConfig.isEnableTextSelection ?? false);
|
||||
|
||||
return TencentUtils.checkString(translateText) != null
|
||||
? Container(
|
||||
margin: const EdgeInsets.only(top: 6),
|
||||
padding: widget.textPadding ?? EdgeInsets.all(isDesktopScreen ? 12 : 10),
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
borderRadius: widget.borderRadius ?? borderRadius,
|
||||
),
|
||||
constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.6),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// If the [elemType] is text message, it will not be null here.
|
||||
// You can render the widget from extension directly, with a [TextStyle] optionally.
|
||||
widget.chatModel.chatConfig.urlPreviewType != UrlPreviewType.none
|
||||
? textWithLink!(
|
||||
style: widget.fontStyle ??
|
||||
TextStyle(
|
||||
fontSize: isDesktopScreen ? 14 : 16,
|
||||
textBaseline: TextBaseline.ideographic,
|
||||
height: widget.chatModel.chatConfig.textHeight))
|
||||
: ExtendedText(translateText!,
|
||||
softWrap: true,
|
||||
style: widget.fontStyle ??
|
||||
TextStyle(
|
||||
fontSize: isDesktopScreen ? 14 : 16, height: widget.chatModel.chatConfig.textHeight),
|
||||
specialTextSpanBuilder: DefaultSpecialTextSpanBuilder(
|
||||
isUseQQPackage: widget.chatModel.chatConfig.stickerPanelConfig?.useQQStickerPackage ?? true,
|
||||
isUseTencentCloudChatPackage:
|
||||
widget.chatModel.chatConfig.stickerPanelConfig?.useTencentCloudChatStickerPackage ?? true,
|
||||
isUseTencentCloudChatPackageOldKeys: widget
|
||||
.chatModel.chatConfig.stickerPanelConfig?.useTencentCloudChatStickerPackageOldKeys ??
|
||||
false,
|
||||
customEmojiStickerList: widget.customEmojiStickerList,
|
||||
showAtBackground: true,
|
||||
)),
|
||||
const SizedBox(height: 6),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.check_circle,
|
||||
color: Color(0x72282c34),
|
||||
size: 12,
|
||||
),
|
||||
const SizedBox(
|
||||
width: 4,
|
||||
),
|
||||
Text(
|
||||
TIM_t("翻译完成"),
|
||||
style: const TextStyle(color: Color(0x72282c34), fontSize: 10),
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
: const SizedBox(width: 0, height: 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:loading_animation_widget/loading_animation_widget.dart';
|
||||
import 'package:open_file/open_file.dart';
|
||||
import 'package:tencent_chat_i18n_tool/tencent_chat_i18n_tool.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/enum/message_status.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_video_elem.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_video_elem.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/base_widgets/tim_callback.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_chat_separate_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/message/message_services.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/services_locatar.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/tui_theme.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/message.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/platform.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitMessageItem/TIMUIKitMessageReaction/tim_uikit_message_reaction_wrapper.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/video_screen.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/wide_popup.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class TIMUIKitVideoElem extends StatefulWidget {
|
||||
final V2TimMessage message;
|
||||
final bool isShowJump;
|
||||
final VoidCallback? clearJump;
|
||||
final String? isFrom;
|
||||
final TUIChatSeparateViewModel chatModel;
|
||||
final bool? isShowMessageReaction;
|
||||
|
||||
const TIMUIKitVideoElem(this.message,
|
||||
{Key? key,
|
||||
this.isShowJump = false,
|
||||
this.clearJump,
|
||||
this.isFrom,
|
||||
this.isShowMessageReaction,
|
||||
required this.chatModel})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _TIMUIKitVideoElemState();
|
||||
}
|
||||
|
||||
class _TIMUIKitVideoElemState extends TIMUIKitState<TIMUIKitVideoElem> {
|
||||
final MessageService _messageService = serviceLocator<MessageService>();
|
||||
late V2TimVideoElem stateElement = widget.message.videoElem!;
|
||||
|
||||
Widget errorDisplay(TUITheme? theme) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
width: 1,
|
||||
color: Colors.black12,
|
||||
)),
|
||||
height: 100,
|
||||
child: Center(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.warning_amber_outlined,
|
||||
color: theme?.cautionColor,
|
||||
size: 16,
|
||||
),
|
||||
Text(
|
||||
TIM_t("视频加载失败"),
|
||||
style: TextStyle(color: theme?.cautionColor),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget generateSnapshot(TUITheme theme, int height) {
|
||||
if (!PlatformUtils().isWeb) {
|
||||
final current = (DateTime.now().millisecondsSinceEpoch / 1000).ceil();
|
||||
final timeStamp = widget.message.timestamp ?? current;
|
||||
if (current - timeStamp < 300) {
|
||||
if (stateElement.snapshotPath != null && stateElement.snapshotPath != '') {
|
||||
File imgF = File(stateElement.snapshotPath!);
|
||||
bool isExist = imgF.existsSync();
|
||||
if (isExist) {
|
||||
return Image.file(File(stateElement.snapshotPath!), fit: BoxFit.fitWidth);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ((stateElement.snapshotUrl == null || stateElement.snapshotUrl == '') &&
|
||||
(stateElement.snapshotPath == null || stateElement.snapshotPath == '')) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(5)),
|
||||
border: Border.all(
|
||||
width: 1,
|
||||
color: Colors.black12,
|
||||
)),
|
||||
height: double.parse(height.toString()),
|
||||
child: Center(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
LoadingAnimationWidget.staggeredDotsWave(
|
||||
color: theme.weakTextColor ?? Colors.grey,
|
||||
size: 28,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return (!PlatformUtils().isWeb && stateElement.snapshotUrl == null ||
|
||||
widget.message.status == MessageStatus.V2TIM_MSG_STATUS_SENDING)
|
||||
? (stateElement.snapshotPath!.isNotEmpty
|
||||
? Image.file(File(stateElement.snapshotPath!), fit: BoxFit.fitWidth)
|
||||
: Image.file(File(stateElement.localSnapshotUrl!), fit: BoxFit.fitWidth))
|
||||
: (PlatformUtils().isWeb || stateElement.localSnapshotUrl == null || stateElement.localSnapshotUrl == "")
|
||||
? Image.network(stateElement.snapshotUrl!, fit: BoxFit.fitWidth)
|
||||
: Image.file(File(stateElement.localSnapshotUrl!), fit: BoxFit.fitWidth);
|
||||
}
|
||||
|
||||
downloadMessageDetailAndSave() async {
|
||||
if (TencentUtils.checkString(widget.message.msgID) != null) {
|
||||
if (TencentUtils.checkString(widget.message.videoElem!.videoUrl) == null) {
|
||||
final response = await _messageService.getMessageOnlineUrl(msgID: widget.message.msgID!);
|
||||
if (response.data != null) {
|
||||
widget.message.videoElem = response.data!.videoElem;
|
||||
Future.delayed(const Duration(microseconds: 10), () {
|
||||
setState(() => stateElement = response.data!.videoElem!);
|
||||
});
|
||||
}
|
||||
}
|
||||
if (!PlatformUtils().isWeb) {
|
||||
if (TencentUtils.checkString(widget.message.videoElem!.localVideoUrl) == null ||
|
||||
!File(widget.message.videoElem!.localVideoUrl!).existsSync()) {
|
||||
_messageService.downloadMessage(
|
||||
msgID: widget.message.msgID!, messageType: 5, imageType: 0, isSnapshot: false);
|
||||
}
|
||||
if (TencentUtils.checkString(widget.message.videoElem!.localSnapshotUrl) == null ||
|
||||
!File(widget.message.videoElem!.localSnapshotUrl!).existsSync()) {
|
||||
_messageService.downloadMessage(msgID: widget.message.msgID!, messageType: 5, imageType: 0, isSnapshot: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
downloadMessageDetailAndSave();
|
||||
}
|
||||
|
||||
void launchDesktopFile(String path) {
|
||||
if (PlatformUtils().isWindows) {
|
||||
OpenFile.open(path);
|
||||
} else {
|
||||
launchUrl(Uri.file(path));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final theme = value.theme;
|
||||
final heroTag =
|
||||
"${widget.message.msgID ?? widget.message.id ?? widget.message.timestamp ?? DateTime.now().millisecondsSinceEpoch}${widget.isFrom}";
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
if (PlatformUtils().isWeb) {
|
||||
final url = widget.message.videoElem?.videoUrl ?? widget.message.videoElem?.videoPath ?? "";
|
||||
TUIKitWidePopup.showMedia(
|
||||
context: context,
|
||||
mediaURL: url,
|
||||
onClickOrigin: () => launchUrl(
|
||||
Uri.parse(url),
|
||||
mode: LaunchMode.externalApplication,
|
||||
));
|
||||
return;
|
||||
}
|
||||
if (PlatformUtils().isDesktop) {
|
||||
final videoElem = widget.message.videoElem;
|
||||
if (videoElem != null) {
|
||||
final localVideoUrl = TencentUtils.checkString(videoElem.localVideoUrl);
|
||||
final videoPath = TencentUtils.checkString(videoElem.videoPath);
|
||||
final videoUrl = videoElem.videoUrl;
|
||||
if (localVideoUrl != null) {
|
||||
launchDesktopFile(localVideoUrl);
|
||||
// todo
|
||||
// TUIKitWidePopup.showMedia(
|
||||
// context: context,
|
||||
// mediaPath: localVideoUrl,
|
||||
// onClickOrigin: () => launchDesktopFile(localVideoUrl));
|
||||
} else if (videoPath != null && File(videoPath).existsSync()) {
|
||||
launchDesktopFile(videoPath);
|
||||
// todo
|
||||
// TUIKitWidePopup.showMedia(
|
||||
// context: context,
|
||||
// mediaPath: videoPath,
|
||||
// onClickOrigin: () => launchDesktopFile(videoPath));
|
||||
} else if (TencentUtils.isTextNotEmpty(videoUrl)) {
|
||||
onTIMCallback(
|
||||
TIMCallback(infoCode: 6660414, infoRecommendText: TIM_t("正在下载中"), type: TIMCallbackType.INFO));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Navigator.of(context).push(
|
||||
PageRouteBuilder(
|
||||
opaque: false, // set to false
|
||||
pageBuilder: (_, __, ___) => VideoScreen(
|
||||
message: widget.message,
|
||||
heroTag: heroTag,
|
||||
videoElement: stateElement,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Hero(
|
||||
tag: heroTag,
|
||||
child: TIMUIKitMessageReactionWrapper(
|
||||
chatModel: widget.chatModel,
|
||||
message: widget.message,
|
||||
isShowJump: widget.isShowJump,
|
||||
isShowMessageReaction: widget.isShowMessageReaction ?? true,
|
||||
clearJump: widget.clearJump,
|
||||
isFromSelf: widget.message.isSelf ?? true,
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(5)),
|
||||
child: LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) {
|
||||
double? positionRadio;
|
||||
if ((stateElement.snapshotWidth) != null &&
|
||||
stateElement.snapshotHeight != null &&
|
||||
stateElement.snapshotWidth != 0 &&
|
||||
stateElement.snapshotHeight != 0) {
|
||||
positionRadio = (stateElement.snapshotWidth! / stateElement.snapshotHeight!);
|
||||
}
|
||||
return ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: PlatformUtils().isWeb ? 300 : constraints.maxWidth * 0.5,
|
||||
maxHeight: min(constraints.maxHeight * 0.8, 300),
|
||||
minHeight: 20,
|
||||
minWidth: 20),
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
if (positionRadio != null &&
|
||||
(stateElement.snapshotUrl != null || stateElement.snapshotUrl != null))
|
||||
AspectRatio(
|
||||
aspectRatio: positionRadio,
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(color: Colors.transparent),
|
||||
),
|
||||
),
|
||||
Row(
|
||||
children: [Expanded(child: generateSnapshot(theme, stateElement.snapshotHeight ?? 100))],
|
||||
),
|
||||
if (widget.message.status != MessageStatus.V2TIM_MSG_STATUS_SENDING &&
|
||||
(stateElement.snapshotUrl != null || stateElement.snapshotPath != null) &&
|
||||
stateElement.videoPath != null ||
|
||||
stateElement.videoUrl != null)
|
||||
Positioned.fill(
|
||||
// alignment: Alignment.center,
|
||||
child: Center(
|
||||
child:
|
||||
Image.asset('images/play.png', package: 'tencent_cloud_chat_uikit', height: 64)),
|
||||
),
|
||||
if (widget.message.videoElem?.duration != null && widget.message.videoElem!.duration! > 0)
|
||||
Positioned(
|
||||
right: 10,
|
||||
bottom: 10,
|
||||
child: Text(
|
||||
MessageUtils.formatVideoTime(widget.message.videoElem!.duration!).toString(),
|
||||
style: const TextStyle(color: Colors.white, fontSize: 12))),
|
||||
],
|
||||
));
|
||||
}),
|
||||
))),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:better_player_plus/better_player_plus.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/enum/message_elem_type.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_message_online_url.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_message_online_url.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/tencent_im_sdk_plugin.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart';
|
||||
|
||||
class TIMUIKitVideoPlayer extends StatefulWidget {
|
||||
final V2TimMessage message;
|
||||
final bool controller;
|
||||
final bool isSending;
|
||||
|
||||
const TIMUIKitVideoPlayer({
|
||||
super.key,
|
||||
required this.message,
|
||||
required this.controller,
|
||||
required this.isSending,
|
||||
});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => TIMUIKitVideoPlayerState();
|
||||
}
|
||||
|
||||
enum CurrentVideoType {
|
||||
online,
|
||||
local,
|
||||
}
|
||||
|
||||
class CurrentVideoInfo {
|
||||
final String path;
|
||||
final CurrentVideoType type;
|
||||
final double aspectRatio;
|
||||
|
||||
CurrentVideoInfo({
|
||||
required this.path,
|
||||
required this.type,
|
||||
required this.aspectRatio,
|
||||
});
|
||||
}
|
||||
|
||||
class TIMUIKitVideoPlayerState extends State<TIMUIKitVideoPlayer> {
|
||||
final String _tag = "TencentCloudChatMessageVideoPlayer";
|
||||
|
||||
BetterPlayerController? _betterPlayerController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initializePlayer();
|
||||
}
|
||||
|
||||
Future<void> _initializePlayer() async {
|
||||
try {
|
||||
final info = await getMessageInfo();
|
||||
if (info != null && mounted) {
|
||||
BetterPlayerDataSource dataSource;
|
||||
if (info.type == CurrentVideoType.online) {
|
||||
dataSource = BetterPlayerDataSource(
|
||||
BetterPlayerDataSourceType.network,
|
||||
info.path,
|
||||
);
|
||||
} else {
|
||||
dataSource = BetterPlayerDataSource(
|
||||
BetterPlayerDataSourceType.file,
|
||||
info.path,
|
||||
);
|
||||
}
|
||||
|
||||
final betterPlayerConfiguration = BetterPlayerConfiguration(
|
||||
aspectRatio: info.aspectRatio,
|
||||
fit: BoxFit.contain,
|
||||
autoPlay: true,
|
||||
allowedScreenSleep: false,
|
||||
fullScreenByDefault: false,
|
||||
controlsConfiguration: const BetterPlayerControlsConfiguration(
|
||||
enableFullscreen: false,
|
||||
enablePlayPause: true,
|
||||
enableProgressBar: true,
|
||||
enableProgressText: true,
|
||||
showControlsOnInitialize: false,
|
||||
enableMute: false,
|
||||
enableOverflowMenu: false,
|
||||
enableSkips: false,
|
||||
),
|
||||
);
|
||||
|
||||
_betterPlayerController = BetterPlayerController(
|
||||
betterPlayerConfiguration,
|
||||
betterPlayerDataSource: dataSource,
|
||||
);
|
||||
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint("Video initialization error: $e");
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_betterPlayerController?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<CurrentVideoInfo?> getMessageInfo() async {
|
||||
if (widget.message.elemType == MessageElemType.V2TIM_ELEM_TYPE_VIDEO) {
|
||||
double aspectRatio = (9 / 16);
|
||||
|
||||
if (widget.isSending) {
|
||||
var lp = widget.message.videoElem!.videoPath ?? "";
|
||||
if (lp.isNotEmpty) {
|
||||
console("view sending message video path");
|
||||
if (File(lp).existsSync() && !kIsWeb) {
|
||||
return CurrentVideoInfo(path: lp, type: CurrentVideoType.local, aspectRatio: aspectRatio);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (widget.message.videoElem!.snapshotWidth != null && widget.message.videoElem!.snapshotHeight != null) {
|
||||
if (widget.message.videoElem!.snapshotHeight != 0) {
|
||||
aspectRatio = (widget.message.videoElem!.snapshotWidth!) / (widget.message.videoElem!.snapshotHeight!);
|
||||
}
|
||||
}
|
||||
|
||||
if (TencentUtils.checkString(widget.message.videoElem!.videoPath) != null) {
|
||||
// 先查本地发送的视频地址
|
||||
if (File(widget.message.videoElem!.videoPath!).existsSync()) {
|
||||
console("video: local video path exists");
|
||||
return CurrentVideoInfo(
|
||||
path: widget.message.videoElem!.videoPath!, type: CurrentVideoType.local, aspectRatio: aspectRatio);
|
||||
}
|
||||
} else if (TencentUtils.checkString(widget.message.videoElem!.localVideoUrl) != null) {
|
||||
// 再查本地下载的视频地址
|
||||
if (File(widget.message.videoElem!.localVideoUrl!).existsSync()) {
|
||||
console("video: local url exists");
|
||||
return CurrentVideoInfo(
|
||||
path: widget.message.videoElem!.localVideoUrl!, type: CurrentVideoType.local, aspectRatio: aspectRatio);
|
||||
}
|
||||
} else {
|
||||
// 最后再查在线地址(todo 使用 getMessageOnlineUrl 查询)
|
||||
if (widget.message.videoElem != null) {
|
||||
if (widget.message.videoElem!.snapshotUrl != null) {
|
||||
console("video: online url ${widget.message.videoElem!.videoUrl}");
|
||||
return CurrentVideoInfo(
|
||||
path: widget.message.videoElem!.videoUrl!,
|
||||
type: CurrentVideoType.online,
|
||||
aspectRatio: aspectRatio,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (!kIsWeb) {
|
||||
V2TimValueCallback<V2TimMessageOnlineUrl> urlres = await TencentImSDKPlugin.v2TIMManager
|
||||
.getMessageManager()
|
||||
.getMessageOnlineUrl(msgID: widget.message.msgID ?? "");
|
||||
if (urlres.data != null) {
|
||||
if (urlres.data?.videoElem != null) {
|
||||
if (TencentUtils.checkString(urlres.data?.videoElem?.videoUrl) != null) {
|
||||
console("view video online url ${urlres.data?.videoElem?.videoUrl}");
|
||||
return CurrentVideoInfo(
|
||||
path: urlres.data!.videoElem!.videoUrl!, type: CurrentVideoType.online, aspectRatio: aspectRatio);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console("The component received a non-video message parameter. please check");
|
||||
}
|
||||
console("has no view video source. please check");
|
||||
return null;
|
||||
}
|
||||
|
||||
console(String log) {
|
||||
print("$_tag, $log");
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.message.hasRiskContent == true) {
|
||||
return const Center(
|
||||
child: Text(
|
||||
"Risk Video",
|
||||
style: TextStyle(color: Colors.white),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (_betterPlayerController == null) {
|
||||
return Container();
|
||||
}
|
||||
|
||||
return AspectRatio(
|
||||
aspectRatio: _betterPlayerController!.videoPlayerController?.value.aspectRatio ?? 9 / 16,
|
||||
child: BetterPlayer(
|
||||
controller: _betterPlayerController!,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:tencent_chat_i18n_tool/tencent_chat_i18n_tool.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_merger_elem.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_merger_elem.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_state.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_chat_separate_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/core/tim_uikit_wide_modal_operation_key.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKItMessageList/tim_uikit_chat_history_message_list_item.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/merger_message_screen.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_callback.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/wide_popup.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/color.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/tui_theme.dart';
|
||||
import 'TIMUIKitMessageReaction/tim_uikit_message_reaction_show_panel.dart';
|
||||
|
||||
class TIMUIKitMergerElem extends StatefulWidget {
|
||||
final V2TimMergerElem mergerElem;
|
||||
final String messageID;
|
||||
final bool isSelf;
|
||||
final bool isShowJump;
|
||||
final VoidCallback? clearJump;
|
||||
final V2TimMessage message;
|
||||
final bool? isShowMessageReaction;
|
||||
final TUIChatSeparateViewModel model;
|
||||
final MessageItemBuilder? messageItemBuilder;
|
||||
|
||||
const TIMUIKitMergerElem(
|
||||
{Key? key,
|
||||
required this.message,
|
||||
required this.model,
|
||||
required this.mergerElem,
|
||||
required this.isSelf,
|
||||
this.isShowMessageReaction,
|
||||
required this.messageID,
|
||||
required this.isShowJump,
|
||||
this.clearJump,
|
||||
this.messageItemBuilder})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => TIMUIKitMergerElemState();
|
||||
}
|
||||
|
||||
class TIMUIKitMergerElemState extends TIMUIKitState<TIMUIKitMergerElem> {
|
||||
bool isShowJumpState = false;
|
||||
late ScrollController _scrollController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_scrollController = ScrollController();
|
||||
}
|
||||
|
||||
_showJumpColor() {
|
||||
int shineAmount = 6;
|
||||
setState(() {
|
||||
isShowJumpState = true;
|
||||
});
|
||||
Future.delayed(const Duration(milliseconds: 100), () {
|
||||
if (widget.clearJump != null) {
|
||||
widget.clearJump!();
|
||||
}
|
||||
});
|
||||
Timer.periodic(const Duration(milliseconds: 300), (timer) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
isShowJumpState = shineAmount.isOdd ? true : false;
|
||||
});
|
||||
}
|
||||
if (shineAmount == 0 || !mounted) {
|
||||
timer.cancel();
|
||||
}
|
||||
shineAmount--;
|
||||
});
|
||||
}
|
||||
|
||||
_handleTap(BuildContext context, TUIChatSeparateViewModel model) async {
|
||||
try {
|
||||
if (widget.messageID != "") {
|
||||
final isDesktopScreen = TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop;
|
||||
|
||||
if (isDesktopScreen) {
|
||||
TUIKitWidePopup.showPopupWindow(
|
||||
operationKey: TUIKitWideModalOperationKey.mergerMessageList,
|
||||
context: context,
|
||||
width: MediaQuery.of(context).size.width * 0.7,
|
||||
title: TIM_t("聊天记录"),
|
||||
height: MediaQuery.of(context).size.height * 0.7,
|
||||
child: (onClose) => Scrollbar(
|
||||
controller: _scrollController,
|
||||
child: MergerMessageScreen(
|
||||
messageItemBuilder: widget.messageItemBuilder, model: model, msgID: widget.messageID),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => MergerMessageScreen(
|
||||
messageItemBuilder: widget.messageItemBuilder, model: model, msgID: widget.messageID),
|
||||
));
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
onTIMCallback(TIMCallback(type: TIMCallbackType.INFO, infoRecommendText: TIM_t("无法定位到原消息"), infoCode: 6660401));
|
||||
}
|
||||
}
|
||||
|
||||
List<String>? _getAbstractList() {
|
||||
final length = widget.mergerElem.abstractList!.length;
|
||||
if (length <= 4) {
|
||||
return widget.mergerElem.abstractList;
|
||||
}
|
||||
return widget.mergerElem.abstractList!.getRange(0, 4).toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final TUITheme theme = value.theme;
|
||||
if (widget.isShowJump) {
|
||||
Future.delayed(Duration.zero, () {
|
||||
_showJumpColor();
|
||||
});
|
||||
}
|
||||
final isDesktopScreen = TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop;
|
||||
return Container(
|
||||
constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * (isDesktopScreen ? 0.3 : 0.6)),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: widget.isSelf ? const Radius.circular(10) : Radius.zero,
|
||||
bottomLeft: const Radius.circular(10),
|
||||
topRight: widget.isSelf ? Radius.zero : const Radius.circular(10),
|
||||
bottomRight: const Radius.circular(10),
|
||||
),
|
||||
border: Border.all(
|
||||
color: isShowJumpState
|
||||
? const Color.fromRGBO(245, 166, 35, 1)
|
||||
: (theme.weakDividerColor ?? CommonColor.weakDividerColor),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
_handleTap(context, widget.model);
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
widget.mergerElem.title!,
|
||||
softWrap: true,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.left,
|
||||
maxLines: 1,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: 4,
|
||||
),
|
||||
// const Divider(),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: _getAbstractList()!
|
||||
.map(
|
||||
(e) => Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
e,
|
||||
textAlign: TextAlign.left,
|
||||
softWrap: true,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 2,
|
||||
style: TextStyle(
|
||||
color: theme.weakTextColor,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 4,
|
||||
),
|
||||
const Divider(),
|
||||
Text(
|
||||
TIM_t("聊天记录"),
|
||||
style: TextStyle(
|
||||
color: theme.weakTextColor,
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
if (widget.isShowMessageReaction ?? true) TIMUIKitMessageReactionShowPanel(message: widget.message)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:scroll_to_index/scroll_to_index.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_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/business_logic/separate_models/tui_chat_separate_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/avatar.dart';
|
||||
|
||||
class AtMemberPanel extends StatefulWidget {
|
||||
/// messageList widget scroll controller
|
||||
final AutoScrollController atMemberPanelScroll;
|
||||
|
||||
final ValueChanged<V2TimGroupMemberFullInfo> onSelectMember;
|
||||
|
||||
// final TextFieldWebController textFieldWebController;
|
||||
const AtMemberPanel(
|
||||
// this.textFieldWebController,
|
||||
{Key? key,
|
||||
required this.atMemberPanelScroll,
|
||||
required this.onSelectMember})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() {
|
||||
return _AtMemberPanelState();
|
||||
}
|
||||
}
|
||||
|
||||
_getShowName(V2TimGroupMemberFullInfo? item) {
|
||||
return TencentUtils.checkStringWithoutSpace(item?.nameCard) ??
|
||||
TencentUtils.checkStringWithoutSpace(item?.nickName) ??
|
||||
TencentUtils.checkStringWithoutSpace(item?.userID);
|
||||
}
|
||||
|
||||
class _AtMemberPanelState extends TIMUIKitState<AtMemberPanel> {
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final theme = value.theme;
|
||||
final chatModal = Provider.of<TUIChatSeparateViewModel>(context);
|
||||
final List<V2TimGroupMemberFullInfo?> groupMemberList = chatModal.showAtMemberList;
|
||||
final double positionX = chatModal.atPositionX;
|
||||
final double positionY = chatModal.atPositionY;
|
||||
final int activeIndex = chatModal.activeAtIndex;
|
||||
|
||||
if (groupMemberList.isEmpty) {
|
||||
return Container();
|
||||
}
|
||||
return Positioned(
|
||||
left: positionX,
|
||||
bottom: positionY,
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxHeight: 170, maxWidth: 170),
|
||||
padding: const EdgeInsets.symmetric(vertical: 5),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
border: Border.all(color: const Color(0xFFE5E6E9))),
|
||||
child: Scrollbar(
|
||||
controller: widget.atMemberPanelScroll,
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: groupMemberList.length,
|
||||
controller: widget.atMemberPanelScroll,
|
||||
itemBuilder: ((context, index) {
|
||||
final memberItem = groupMemberList[index];
|
||||
if (memberItem == null) {
|
||||
return AutoScrollTag(key: ValueKey(index), controller: widget.atMemberPanelScroll, index: index);
|
||||
}
|
||||
final showName = _getShowName(memberItem);
|
||||
final isAtAll = memberItem.userID == "__kImSDK_MesssageAtALL__";
|
||||
return AutoScrollTag(
|
||||
key: ValueKey(index),
|
||||
controller: widget.atMemberPanelScroll,
|
||||
index: index,
|
||||
child: Material(
|
||||
color: theme.wideBackgroundColor,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
chatModal.activeAtIndex = index;
|
||||
widget.onSelectMember(memberItem);
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
color: activeIndex == index ? theme.weakBackgroundColor : theme.wideBackgroundColor,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 24,
|
||||
width: 24,
|
||||
child: Avatar(faceUrl: memberItem.faceUrl ?? "", type: 1, showName: showName),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 8,
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
isAtAll ? "$showName(${groupMemberList.length - 1})" : showName,
|
||||
softWrap: false,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
fontWeight: memberItem.role == 400 || memberItem.role == 300
|
||||
? FontWeight.w500
|
||||
: FontWeight.normal,
|
||||
color: memberItem.role == 400 || memberItem.role == 300
|
||||
? theme.primaryColor
|
||||
: theme.darkTextColor),
|
||||
))
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
})),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
// ignore_for_file: file_names
|
||||
|
||||
import 'package:extended_text_field/extended_text_field.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitTextField/special_text/http_text.dart';
|
||||
import 'package:tim_ui_kit_sticker_plugin/utils/tim_custom_face_data.dart';
|
||||
|
||||
import 'emoji_text.dart';
|
||||
|
||||
class DefaultSpecialTextSpanBuilder extends SpecialTextSpanBuilder {
|
||||
DefaultSpecialTextSpanBuilder({
|
||||
this.isUseQQPackage = false,
|
||||
this.isUseTencentCloudChatPackage = false,
|
||||
this.isUseTencentCloudChatPackageOldKeys = false,
|
||||
this.customEmojiStickerList = const [],
|
||||
this.showAtBackground = false,
|
||||
this.checkHttpLink = true,
|
||||
});
|
||||
|
||||
/// whether show background for @somebody
|
||||
final bool showAtBackground;
|
||||
|
||||
final bool isUseQQPackage;
|
||||
|
||||
final bool isUseTencentCloudChatPackage;
|
||||
|
||||
final bool isUseTencentCloudChatPackageOldKeys;
|
||||
|
||||
final bool checkHttpLink;
|
||||
|
||||
final List<CustomEmojiFaceData> customEmojiStickerList;
|
||||
|
||||
@override
|
||||
SpecialText? createSpecialText(String flag,
|
||||
{TextStyle? textStyle, SpecialTextGestureTapCallback? onTap, int? index}) {
|
||||
if (flag == '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
///index is end index of start flag, so text start index should be index-(flag.length-1)
|
||||
if (isStart(flag, EmojiText.flag)) {
|
||||
return EmojiText(textStyle,
|
||||
isUseTencentCloudChatPackage: isUseTencentCloudChatPackage,
|
||||
isUseTencentCloudChatPackageOldKeys: isUseTencentCloudChatPackageOldKeys,
|
||||
isUseQQPackage: isUseQQPackage,
|
||||
start: index! - (EmojiText.flag.length - 1),
|
||||
customEmojiStickerList: customEmojiStickerList);
|
||||
} else if (isStart(flag, HttpText.flag) && checkHttpLink) {
|
||||
return HttpText(textStyle, onTap, start: index! - (HttpText.flag.length - 1));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:extended_text/extended_text.dart';
|
||||
import 'package:tim_ui_kit_sticker_plugin/constant/emoji.dart';
|
||||
import 'package:tim_ui_kit_sticker_plugin/utils/tim_custom_face_data.dart';
|
||||
|
||||
///emoji/image text
|
||||
class EmojiText extends SpecialText {
|
||||
EmojiText(TextStyle? textStyle,
|
||||
{this.start,
|
||||
this.isUseQQPackage = false,
|
||||
this.isUseTencentCloudChatPackage = false,
|
||||
this.isUseTencentCloudChatPackageOldKeys = false,
|
||||
this.customEmojiStickerList = const []})
|
||||
: super(EmojiText.flag, ']', textStyle);
|
||||
static const String flag = '[';
|
||||
final int? start;
|
||||
final bool isUseQQPackage;
|
||||
final bool isUseTencentCloudChatPackage;
|
||||
final bool isUseTencentCloudChatPackageOldKeys;
|
||||
final List<CustomEmojiFaceData> customEmojiStickerList;
|
||||
|
||||
@override
|
||||
InlineSpan finishText() {
|
||||
final String key = toString();
|
||||
final EmojiUtil emojiUtil = EmojiUtil(
|
||||
isUseQQPackage: isUseQQPackage,
|
||||
isUseTencentCloudChatPackage: isUseTencentCloudChatPackage,
|
||||
isUseTencentCloudChatPackageOldKeys: isUseTencentCloudChatPackageOldKeys,
|
||||
customEmojiStickerList: customEmojiStickerList);
|
||||
|
||||
if (emojiUtil.emojiMap.containsKey(key)) {
|
||||
double size = 16;
|
||||
|
||||
final TextStyle ts = textStyle!;
|
||||
if (ts.fontSize != null) {
|
||||
size = ts.fontSize! * 1.44;
|
||||
}
|
||||
|
||||
if (isUseQQPackage == true && (emojiUtil.emojiKeyCategoryMap["4349"]?.contains(key) ?? false)) {
|
||||
return ImageSpan(AssetImage(emojiUtil.emojiMap[key]!, package: "tim_ui_kit_sticker_plugin"),
|
||||
actualText: key,
|
||||
imageWidth: size,
|
||||
imageHeight: size,
|
||||
start: start!,
|
||||
// fit: BoxFit.cover,
|
||||
margin: const EdgeInsets.all(0));
|
||||
} else if (isUseTencentCloudChatPackage == true &&
|
||||
(emojiUtil.emojiKeyCategoryMap["tcc1"]?.contains(key) ?? false)) {
|
||||
return ImageSpan(AssetImage(emojiUtil.emojiMap[key]!, package: "tim_ui_kit_sticker_plugin"),
|
||||
actualText: key,
|
||||
imageWidth: size,
|
||||
imageHeight: size,
|
||||
start: start!,
|
||||
// fit: BoxFit.cover,
|
||||
margin: const EdgeInsets.all(0));
|
||||
} else {
|
||||
return ImageSpan(AssetImage(emojiUtil.emojiMap[key]!),
|
||||
actualText: key,
|
||||
imageWidth: size,
|
||||
imageHeight: size,
|
||||
start: start!,
|
||||
// fit: BoxFit.cover,
|
||||
margin: const EdgeInsets.all(0));
|
||||
}
|
||||
}
|
||||
|
||||
return TextSpan(text: toString(), style: textStyle);
|
||||
}
|
||||
}
|
||||
|
||||
class EmojiUtil {
|
||||
// Private constructor initializing the emoji data
|
||||
EmojiUtil._internal(
|
||||
{required this.isUseQQPackage,
|
||||
required this.isUseTencentCloudChatPackage,
|
||||
required this.isUseTencentCloudChatPackageOldKeys,
|
||||
required this.customEmojiStickerList}) {
|
||||
_emojiMap.addAll(loadDefaultEmojis());
|
||||
|
||||
final customEmojis = loadCustomEmojis();
|
||||
_emojiMap.addAll(customEmojis.$1);
|
||||
_emojiKeyCategoryMap["custom"] = customEmojis.$2;
|
||||
}
|
||||
|
||||
final bool isUseQQPackage;
|
||||
final bool isUseTencentCloudChatPackage;
|
||||
final bool isUseTencentCloudChatPackageOldKeys;
|
||||
final List<CustomEmojiFaceData> customEmojiStickerList;
|
||||
|
||||
// Load the default emojis into a Map
|
||||
Map<String, String> loadDefaultEmojis() {
|
||||
Map<String, String> defaultEmojiMap = {};
|
||||
for (final emojiGroup in TUIKitStickerConstData.emojiList) {
|
||||
final groupName = emojiGroup.name;
|
||||
final keyList = [];
|
||||
if (isUseQQPackage && groupName == "4349") {
|
||||
for (final emoji in emojiGroup.list) {
|
||||
String emojiName = emoji.split('.png')[0];
|
||||
defaultEmojiMap['[$emojiName]'] = '$_emojiFilePath/$groupName/$emojiName.png';
|
||||
keyList.add('[$emojiName]');
|
||||
|
||||
final zhKey = TUIKitStickerConstData.emoji4349ZhMapList[emojiName];
|
||||
defaultEmojiMap['[$zhKey]'] = '$_emojiFilePath/$groupName/$emojiName.png';
|
||||
keyList.add('[$zhKey]');
|
||||
}
|
||||
_emojiKeyCategoryMap[groupName] = keyList;
|
||||
}
|
||||
|
||||
if (isUseTencentCloudChatPackage && groupName == "tcc1") {
|
||||
for (final emoji in emojiGroup.list) {
|
||||
String emojiName = emoji.split('.png')[0];
|
||||
String compatibleEmojiName = emojiName;
|
||||
if (isUseTencentCloudChatPackageOldKeys) {
|
||||
// use old emoji keys in 3.x version
|
||||
compatibleEmojiName = getCompatibleEmojiName(emojiName);
|
||||
}
|
||||
|
||||
defaultEmojiMap['[$compatibleEmojiName]'] = '$_emojiFilePath/$groupName/$emojiName.png';
|
||||
keyList.add('[$compatibleEmojiName]');
|
||||
}
|
||||
_emojiKeyCategoryMap[groupName] = keyList;
|
||||
}
|
||||
}
|
||||
return defaultEmojiMap;
|
||||
}
|
||||
|
||||
// Load the custom emojis into a Map
|
||||
(Map<String, String>, List<String>) loadCustomEmojis() {
|
||||
Map<String, String> customEmojiMap = {};
|
||||
List<String> keyList = [];
|
||||
for (final customEmojiGroup in customEmojiStickerList) {
|
||||
for (final customEmoji in customEmojiGroup.list) {
|
||||
String customEmojiName = customEmoji.split('.png')[0];
|
||||
customEmojiMap['[$customEmojiName]'] = '$_emojiFilePath/${customEmojiGroup.name}/$customEmojiName.png';
|
||||
keyList.add('[$customEmojiName]');
|
||||
}
|
||||
}
|
||||
return (customEmojiMap, keyList);
|
||||
}
|
||||
|
||||
// A Map instance variable to store the emojis and their paths
|
||||
final Map<String, String> _emojiMap = <String, String>{};
|
||||
|
||||
// A getter method for _emojiMap
|
||||
Map<String, String> get emojiMap => _emojiMap;
|
||||
|
||||
// A Map instance variable to store the emojis and their paths
|
||||
final Map<String, List> _emojiKeyCategoryMap = <String, List>{};
|
||||
|
||||
// A getter method for _emojiMap
|
||||
Map<String, List> get emojiKeyCategoryMap => _emojiKeyCategoryMap;
|
||||
|
||||
// An instance variable to store the emoji file path
|
||||
final String _emojiFilePath = 'assets/custom_face_resource';
|
||||
|
||||
// Singleton pattern to avoid creating multiple instances of EmojiUtil
|
||||
static EmojiUtil? _instance;
|
||||
|
||||
// Factory constructor to return the singleton instance of EmojiUtil with custom parameters
|
||||
factory EmojiUtil(
|
||||
{bool isUseQQPackage = false,
|
||||
bool isUseTencentCloudChatPackage = false,
|
||||
bool isUseTencentCloudChatPackageOldKeys = false,
|
||||
List<CustomEmojiFaceData> customEmojiStickerList = const []}) {
|
||||
return _instance ??= EmojiUtil._internal(
|
||||
isUseQQPackage: isUseQQPackage,
|
||||
customEmojiStickerList: customEmojiStickerList,
|
||||
isUseTencentCloudChatPackage: isUseTencentCloudChatPackage,
|
||||
isUseTencentCloudChatPackageOldKeys: isUseTencentCloudChatPackageOldKeys);
|
||||
}
|
||||
|
||||
static String getCompatibleEmojiName(String emojiName) {
|
||||
String compatibleEmojiName = emojiName;
|
||||
try {
|
||||
compatibleEmojiName = emojiName.split('_')[1];
|
||||
// 对特殊字符串 Ok 进行处理
|
||||
if (compatibleEmojiName == 'Ok') {
|
||||
compatibleEmojiName = 'OK';
|
||||
}
|
||||
} catch (e) {
|
||||
print(e);
|
||||
}
|
||||
|
||||
return compatibleEmojiName;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/link_preview/common/utils.dart';
|
||||
import 'package:extended_text/extended_text.dart';
|
||||
|
||||
class HttpText extends SpecialText {
|
||||
HttpText(TextStyle? textStyle, SpecialTextGestureTapCallback? onTap,
|
||||
{this.start})
|
||||
: super(flag, flag, textStyle, onTap: onTap);
|
||||
static const String flag = '!@TURL#*&\$';
|
||||
final int? start;
|
||||
@override
|
||||
InlineSpan finishText() {
|
||||
final String text = getContent();
|
||||
|
||||
return SpecialTextSpan(
|
||||
text: text,
|
||||
actualText: toString(),
|
||||
start: start!,
|
||||
|
||||
///caret can move into special text
|
||||
deleteAll: true,
|
||||
style: TextStyle(color: LinkUtils.hexToColor("015fff")),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () {
|
||||
if (onTap != null) {
|
||||
onTap!(toString());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:tencent_chat_i18n_tool/tencent_chat_i18n_tool.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_info.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_group_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';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_member_search_param.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_group_member_search_param.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_member_search_result.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_group_member_search_result.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_user_full_info.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_user_full_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_uikit/base_widgets/tim_ui_kit_state.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_self_info_view_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/ui/utils/platform.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitGroupProfile/widgets/tim_ui_group_member_search.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/group_member_list.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 AtText extends StatefulWidget {
|
||||
final String? groupID;
|
||||
final V2TimGroupInfo? groupInfo;
|
||||
final List<V2TimGroupMemberFullInfo?>? groupMemberList;
|
||||
final VoidCallback? closeFunc;
|
||||
final Function(List<V2TimGroupMemberFullInfo> memberInfo)? onChooseMember;
|
||||
final bool canAtAll;
|
||||
|
||||
// some Group type cant @all
|
||||
final String? groupType;
|
||||
|
||||
const AtText({
|
||||
this.groupID,
|
||||
this.groupType,
|
||||
Key? key,
|
||||
this.groupInfo,
|
||||
this.groupMemberList,
|
||||
this.closeFunc,
|
||||
this.onChooseMember,
|
||||
this.canAtAll = false,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _AtTextState();
|
||||
}
|
||||
|
||||
class _AtTextState extends TIMUIKitState<AtText> {
|
||||
final GroupServices _groupServices = serviceLocator<GroupServices>();
|
||||
final TUISelfInfoViewModel _selfInfoViewModel = serviceLocator<TUISelfInfoViewModel>();
|
||||
|
||||
List<V2TimGroupMemberFullInfo?>? groupMemberList;
|
||||
List<V2TimGroupMemberFullInfo?>? searchMemberList;
|
||||
|
||||
List<V2TimGroupMemberFullInfo> selectedGroupMemberList = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
groupMemberList = widget.groupMemberList;
|
||||
searchMemberList = groupMemberList;
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _submitAtMemberList() {
|
||||
if (widget.closeFunc != null) {
|
||||
widget.closeFunc!();
|
||||
}
|
||||
|
||||
if (widget.onChooseMember != null) {
|
||||
widget.onChooseMember!(selectedGroupMemberList);
|
||||
} else {
|
||||
Navigator.pop(context, selectedGroupMemberList);
|
||||
}
|
||||
}
|
||||
|
||||
Future<V2TimValueCallback<V2GroupMemberInfoSearchResult>> searchGroupMember(
|
||||
V2TimGroupMemberSearchParam searchParam) async {
|
||||
final res = await _groupServices.searchGroupMembers(searchParam: searchParam);
|
||||
|
||||
if (res.code == 0) {}
|
||||
return res;
|
||||
}
|
||||
|
||||
handleSearchGroupMembers(String searchText, context) async {
|
||||
final res = await searchGroupMember(V2TimGroupMemberSearchParam(
|
||||
keywordList: [searchText],
|
||||
groupIDList: [widget.groupID!],
|
||||
));
|
||||
|
||||
if (res.code == 0) {
|
||||
List<V2TimGroupMemberFullInfo?> list = [];
|
||||
final searchResult = res.data!.groupMemberSearchResultItems!;
|
||||
searchResult.forEach((key, value) {
|
||||
if (value is List) {
|
||||
for (V2TimGroupMemberFullInfo item in value) {
|
||||
list.add(item);
|
||||
}
|
||||
}
|
||||
});
|
||||
searchMemberList = list;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
searchMemberList = isSearchTextExist(searchText) ? searchMemberList : groupMemberList;
|
||||
});
|
||||
}
|
||||
|
||||
bool isSearchTextExist(String? searchText) {
|
||||
return searchText != null && searchText != "";
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final TUITheme theme = value.theme;
|
||||
|
||||
V2TimUserFullInfo? loginUserInfo = _selfInfoViewModel.loginInfo;
|
||||
if (loginUserInfo != null) {
|
||||
searchMemberList?.removeWhere((memberInfo) {
|
||||
return memberInfo?.userID == loginUserInfo.userID;
|
||||
});
|
||||
}
|
||||
|
||||
Widget mentionedMembersBody() {
|
||||
return GroupProfileMemberList(
|
||||
groupType: widget.groupType ?? "",
|
||||
memberList: searchMemberList ?? [],
|
||||
canAtAll: widget.canAtAll,
|
||||
canSelectMember: true,
|
||||
canSlideDelete: false,
|
||||
onSelectedMemberChange: (selectedMemberList) {
|
||||
selectedGroupMemberList = selectedMemberList;
|
||||
bool isAtAllSelected = selectedGroupMemberList.where((element) {
|
||||
return element.userID == GroupProfileMemberList.AT_ALL_USER_ID;
|
||||
}).isNotEmpty;
|
||||
|
||||
if (isAtAllSelected) {
|
||||
_submitAtMemberList();
|
||||
}
|
||||
},
|
||||
touchBottomCallBack: () {
|
||||
// Get all by once, unnecessary to load more
|
||||
},
|
||||
customTopArea: PlatformUtils().isWeb
|
||||
? null
|
||||
: GroupMemberSearchTextField(
|
||||
onTextChange: (text) => handleSearchGroupMembers(text, context),
|
||||
));
|
||||
}
|
||||
|
||||
return TUIKitScreenUtils.getDeviceWidget(
|
||||
context: context,
|
||||
desktopWidget: mentionedMembersBody(),
|
||||
defaultWidget: Scaffold(
|
||||
appBar: AppBar(
|
||||
shadowColor: theme.weakBackgroundColor,
|
||||
iconTheme: IconThemeData(
|
||||
color: theme.appbarTextColor,
|
||||
),
|
||||
backgroundColor: theme.appbarBgColor ?? theme.primaryColor,
|
||||
leading: Row(
|
||||
children: [
|
||||
IconButton(
|
||||
padding: const EdgeInsets.only(left: 16),
|
||||
constraints: const BoxConstraints(),
|
||||
icon: Image.asset(
|
||||
'images/arrow_back.png',
|
||||
package: 'tencent_cloud_chat_uikit',
|
||||
height: 34,
|
||||
width: 34,
|
||||
color: theme.appbarTextColor,
|
||||
),
|
||||
onPressed: () async {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
centerTitle: true,
|
||||
leadingWidth: 100,
|
||||
title: Text(
|
||||
TIM_t("选择提醒人"),
|
||||
style: TextStyle(
|
||||
color: theme.appbarTextColor,
|
||||
fontSize: 17,
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
_submitAtMemberList();
|
||||
},
|
||||
child: Text(
|
||||
TIM_t("确定"),
|
||||
style: TextStyle(
|
||||
color: theme.appbarTextColor,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
body: mentionedMembersBody()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:loading_animation_widget/loading_animation_widget.dart';
|
||||
import 'package:tencent_chat_i18n_tool/tencent_chat_i18n_tool.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/enum/group_member_filter_enum.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_member_search_param.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_group_member_search_param.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_member_search_result.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_group_member_search_result.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_uikit/base_widgets/tim_ui_kit_state.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/ui/utils/platform.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitGroupProfile/widgets/tim_ui_group_member_search.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/group_member_list.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 SelectCallInviter extends StatefulWidget {
|
||||
final String? groupID;
|
||||
const SelectCallInviter({
|
||||
this.groupID,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _SelectCallInviterState();
|
||||
}
|
||||
|
||||
class _SelectCallInviterState extends TIMUIKitState<SelectCallInviter> {
|
||||
final CoreServicesImpl _coreServicesImpl = serviceLocator<CoreServicesImpl>();
|
||||
final GroupServices _groupServices = serviceLocator<GroupServices>();
|
||||
List<V2TimGroupMemberFullInfo> selectedMember = [];
|
||||
List<V2TimGroupMemberFullInfo?>? _groupMemberList = [];
|
||||
String _groupMemberListSeq = "0";
|
||||
List<V2TimGroupMemberFullInfo?>? searchMemberList;
|
||||
String? searchText;
|
||||
bool loading = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.groupID != null) {
|
||||
_loadGroupMemberList(groupID: widget.groupID!);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
bool isSearchTextExist(String? searchText) {
|
||||
return searchText != null && searchText != "";
|
||||
}
|
||||
|
||||
Future<void> _loadGroupMemberList({required String groupID, int count = 100, String? seq}) async {
|
||||
if (seq == null || seq == "" || seq == "0") {
|
||||
_groupMemberList = [];
|
||||
}
|
||||
final String? nextSeq = await _loadGroupMemberListFunction(groupID: groupID, seq: seq, count: count);
|
||||
if (nextSeq != null && nextSeq != "0" && nextSeq != "") {
|
||||
return await _loadGroupMemberList(groupID: groupID, count: count, seq: nextSeq);
|
||||
} else {
|
||||
setState(() {
|
||||
_groupMemberList = _groupMemberList;
|
||||
searchMemberList = _groupMemberList;
|
||||
loading = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<String?> _loadGroupMemberListFunction({required String groupID, int count = 100, String? seq}) async {
|
||||
if (seq == "0") {
|
||||
_groupMemberList?.clear();
|
||||
}
|
||||
final res = await _groupServices.getGroupMemberList(
|
||||
groupID: widget.groupID!,
|
||||
filter: GroupMemberFilterTypeEnum.V2TIM_GROUP_MEMBER_FILTER_ALL,
|
||||
count: count,
|
||||
nextSeq: seq ?? _groupMemberListSeq);
|
||||
final groupMemberListRes = res.data;
|
||||
if (res.code == 0 && groupMemberListRes != null) {
|
||||
final groupMemberListTemp = groupMemberListRes.memberInfoList ?? [];
|
||||
_groupMemberList = [...?_groupMemberList, ...groupMemberListTemp];
|
||||
_groupMemberListSeq = groupMemberListRes.nextSeq ?? "0";
|
||||
}
|
||||
return groupMemberListRes?.nextSeq;
|
||||
}
|
||||
|
||||
Future<V2TimValueCallback<V2GroupMemberInfoSearchResult>> searchGroupMember(
|
||||
V2TimGroupMemberSearchParam searchParam) async {
|
||||
final res = await _groupServices.searchGroupMembers(searchParam: searchParam);
|
||||
|
||||
if (res.code == 0) {}
|
||||
return res;
|
||||
}
|
||||
|
||||
handleSearchGroupMembers(String searchText, context) async {
|
||||
loading = true;
|
||||
if (widget.groupID == null || widget.groupID!.isEmpty) {
|
||||
return;
|
||||
}
|
||||
List<V2TimGroupMemberFullInfo?> currentGroupMember = [];
|
||||
final res = await searchGroupMember(V2TimGroupMemberSearchParam(
|
||||
keywordList: [searchText],
|
||||
groupIDList: [widget.groupID!],
|
||||
));
|
||||
|
||||
if (res.code == 0) {
|
||||
List<V2TimGroupMemberFullInfo?> list = [];
|
||||
final searchResult = res.data!.groupMemberSearchResultItems!;
|
||||
searchResult.forEach((key, value) {
|
||||
if (value is List) {
|
||||
for (V2TimGroupMemberFullInfo item in value) {
|
||||
list.add(item);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
currentGroupMember = list;
|
||||
} else {
|
||||
currentGroupMember = [];
|
||||
}
|
||||
setState(() {
|
||||
loading = false;
|
||||
searchMemberList = isSearchTextExist(searchText) ? currentGroupMember : _groupMemberList;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final TUITheme theme = value.theme;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
shadowColor: theme.weakBackgroundColor,
|
||||
iconTheme: IconThemeData(
|
||||
color: theme.appbarTextColor,
|
||||
),
|
||||
backgroundColor: theme.appbarBgColor ?? theme.primaryColor,
|
||||
leading: TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: Text(
|
||||
TIM_t("取消"),
|
||||
style: TextStyle(
|
||||
color: theme.appbarTextColor,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
if (selectedMember.isNotEmpty) {
|
||||
Navigator.pop(context, selectedMember);
|
||||
}
|
||||
},
|
||||
child: Text(
|
||||
TIM_t("完成"),
|
||||
style: TextStyle(
|
||||
color: theme.appbarTextColor,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
centerTitle: true,
|
||||
leadingWidth: 80,
|
||||
title: Text(
|
||||
TIM_t("发起呼叫"),
|
||||
style: TextStyle(
|
||||
color: theme.appbarTextColor,
|
||||
fontSize: 17,
|
||||
),
|
||||
),
|
||||
),
|
||||
body: ((searchMemberList ?? []).isNotEmpty || loading == false)
|
||||
? GroupProfileMemberList(
|
||||
customTopArea: PlatformUtils().isWeb
|
||||
? null
|
||||
: GroupMemberSearchTextField(
|
||||
onTextChange: (text) => handleSearchGroupMembers(text, context),
|
||||
),
|
||||
memberList: (searchMemberList ?? [])
|
||||
.where((element) => element?.userID != _coreServicesImpl.loginInfo.userID)
|
||||
.toList(),
|
||||
canSlideDelete: false,
|
||||
canSelectMember: true,
|
||||
onSelectedMemberChange: (member) {
|
||||
selectedMember = member;
|
||||
setState(() {});
|
||||
},
|
||||
)
|
||||
: Center(
|
||||
child: LoadingAnimationWidget.staggeredDotsWave(
|
||||
color: theme.primaryColor ?? Colors.grey,
|
||||
size: 40,
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
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/ui/utils/platform.dart';
|
||||
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/logger.dart';
|
||||
|
||||
import 'package:tencent_cloud_chat_uikit/theme/color.dart';
|
||||
|
||||
class EmojiPanel extends TIMUIKitStatelessWidget {
|
||||
final void Function(int unicode) onTapEmoji;
|
||||
final void Function() onSubmitted;
|
||||
final void Function() delete;
|
||||
final bool showBottomContainer;
|
||||
|
||||
EmojiPanel({
|
||||
Key? key,
|
||||
required this.onTapEmoji,
|
||||
required this.onSubmitted,
|
||||
required this.delete,
|
||||
this.showBottomContainer = true, // 可选参数,是否展示下方的底部导航栏
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
// ignore: avoid_print
|
||||
outputLogger.i(TIM_t(
|
||||
"暂未安装表情包插件,如需使用表情相关功能,请根据本文档安装:https://cloud.tencent.com/document/product/269/70746"));
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
height: showBottomContainer ? 190 : 248,
|
||||
// color: theme.weakBackgroundColor,
|
||||
padding: const EdgeInsets.fromLTRB(24, 16, 24, 16),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(TIM_t("暂无表情包")),
|
||||
],
|
||||
),
|
||||
),
|
||||
showBottomContainer
|
||||
? Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
SingleChildScrollView(
|
||||
child: Container(
|
||||
// color: Colors.white,
|
||||
margin: const EdgeInsets.only(right: 25),
|
||||
// height: MediaQuery.of(context).padding.bottom,
|
||||
child: ElevatedButton(
|
||||
child: Text(TIM_t("发送")),
|
||||
style: ElevatedButton.styleFrom(),
|
||||
onPressed: () {
|
||||
onSubmitted();
|
||||
})),
|
||||
),
|
||||
],
|
||||
)
|
||||
: Container()
|
||||
],
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
class EmojiItem extends TIMUIKitStatelessWidget {
|
||||
EmojiItem({Key? key, required this.name, required this.unicode})
|
||||
: super(key: key);
|
||||
final String name;
|
||||
final int unicode;
|
||||
|
||||
// final String toUser;
|
||||
// final int type;
|
||||
// final Function close;
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
return DefaultTextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: (PlatformUtils().isAndroid) ? 20 : 26,
|
||||
color: hexToColor("f9453d")
|
||||
),
|
||||
child: Text(
|
||||
String.fromCharCode(unicode),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,741 @@
|
||||
// ignore_for_file: unused_field, avoid_print, unused_import
|
||||
|
||||
import 'dart:io';
|
||||
import 'package:better_player_plus/better_player_plus.dart';
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:fc_native_video_thumbnail/fc_native_video_thumbnail.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:tencent_chat_i18n_tool/tencent_chat_i18n_tool.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_uikit/tencent_cloud_chat_uikit.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitTextField/tim_uikit_call_invite_list.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_chat_separate_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_chat_global_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_self_info_view_model.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/services_locatar.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/message.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/permission.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/platform.dart';
|
||||
import 'package:wechat_assets_picker/wechat_assets_picker.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart';
|
||||
|
||||
// ignore: unnecessary_import
|
||||
import 'dart:typed_data';
|
||||
import 'package:universal_html/html.dart' as html;
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/logger.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.dart';
|
||||
|
||||
class MorePanelConfig {
|
||||
static final int FILE_MAX_SIZE = 100 * 1024 * 1024;
|
||||
static final int VIDEO_MAX_SIZE = 100 * 1024 * 1024;
|
||||
static final int IMAGE_MAX_SIZE = 28 * 1024 * 1024;
|
||||
|
||||
final bool showGalleryPickAction;
|
||||
final bool showCameraAction;
|
||||
final bool showFilePickAction;
|
||||
final bool showWebImagePickAction;
|
||||
final bool showWebVideoPickAction;
|
||||
final bool showVoiceCall;
|
||||
final bool showVideoCall;
|
||||
final List<MorePanelItem>? extraAction;
|
||||
final Widget Function(MorePanelItem item)? actionBuilder;
|
||||
|
||||
MorePanelConfig({
|
||||
this.showFilePickAction = true,
|
||||
this.showGalleryPickAction = true,
|
||||
this.showCameraAction = true,
|
||||
this.showWebImagePickAction = true,
|
||||
this.showWebVideoPickAction = true,
|
||||
this.showVoiceCall = true,
|
||||
this.showVideoCall = true,
|
||||
this.extraAction,
|
||||
this.actionBuilder,
|
||||
});
|
||||
}
|
||||
|
||||
class MorePanelItem {
|
||||
final String title;
|
||||
final String id;
|
||||
final Widget icon;
|
||||
final Function(BuildContext context)? onTap;
|
||||
|
||||
MorePanelItem({this.onTap, required this.icon, required this.id, required this.title});
|
||||
}
|
||||
|
||||
class MorePanel extends StatefulWidget {
|
||||
/// 会话ID
|
||||
final String conversationID;
|
||||
|
||||
/// 会话类型
|
||||
final ConvType conversationType;
|
||||
|
||||
final MorePanelConfig? morePanelConfig;
|
||||
|
||||
const MorePanel({required this.conversationID, required this.conversationType, Key? key, this.morePanelConfig})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _MorePanelState();
|
||||
}
|
||||
|
||||
class _MorePanelState extends TIMUIKitState<MorePanel> {
|
||||
final ImagePicker _picker = ImagePicker();
|
||||
final TUISelfInfoViewModel _selfInfoViewModel = serviceLocator<TUISelfInfoViewModel>();
|
||||
Uint8List? fileContent;
|
||||
String? fileName;
|
||||
File? tempFile;
|
||||
final _tUICore = TUICore();
|
||||
final _tUILogin = TUILogin();
|
||||
bool isInstallCallkit = false;
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
final DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
|
||||
|
||||
late BetterPlayerController _betterPlayerController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (PlatformUtils().isMobile) {
|
||||
_tUICore.getService(TUICALLKIT_SERVICE_NAME).then((value) {
|
||||
setState(() {
|
||||
isInstallCallkit = value;
|
||||
});
|
||||
});
|
||||
_betterPlayerController = BetterPlayerController(const BetterPlayerConfiguration());
|
||||
}
|
||||
}
|
||||
|
||||
List<MorePanelItem> itemList(TUIChatSeparateViewModel model, TUITheme theme) {
|
||||
final config = widget.morePanelConfig ?? MorePanelConfig();
|
||||
return [
|
||||
if (!PlatformUtils().isWeb)
|
||||
MorePanelItem(
|
||||
id: "photo",
|
||||
title: TIM_t("照片"),
|
||||
onTap: (c) {
|
||||
_onFeatureTap(
|
||||
"photo",
|
||||
c,
|
||||
model,
|
||||
theme,
|
||||
);
|
||||
},
|
||||
icon: Container(
|
||||
height: 64,
|
||||
width: 64,
|
||||
margin: const EdgeInsets.only(bottom: 4),
|
||||
decoration: const BoxDecoration(color: Colors.white, borderRadius: BorderRadius.all(Radius.circular(5))),
|
||||
child: SvgPicture.asset(
|
||||
"images/photo.svg",
|
||||
package: 'tencent_cloud_chat_uikit',
|
||||
height: 64,
|
||||
width: 64,
|
||||
),
|
||||
)),
|
||||
if (PlatformUtils().isMobile)
|
||||
MorePanelItem(
|
||||
id: "take_photo",
|
||||
title: TIM_t("拍照"),
|
||||
onTap: (c) {
|
||||
_onFeatureTap("take_photo", c, model, theme);
|
||||
},
|
||||
icon: Container(
|
||||
height: 64,
|
||||
width: 64,
|
||||
margin: const EdgeInsets.only(bottom: 4),
|
||||
decoration: const BoxDecoration(color: Colors.white, borderRadius: BorderRadius.all(Radius.circular(5))),
|
||||
child: SvgPicture.asset(
|
||||
"images/screen.svg",
|
||||
package: 'tencent_cloud_chat_uikit',
|
||||
height: 64,
|
||||
width: 64,
|
||||
),
|
||||
)),
|
||||
if (PlatformUtils().isMobile)
|
||||
MorePanelItem(
|
||||
id: "take_video",
|
||||
title: TIM_t("录像"),
|
||||
onTap: (c) {
|
||||
_onFeatureTap("take_video", c, model, theme);
|
||||
},
|
||||
icon: Container(
|
||||
height: 64,
|
||||
width: 64,
|
||||
margin: const EdgeInsets.only(bottom: 4),
|
||||
decoration: const BoxDecoration(color: Colors.white, borderRadius: BorderRadius.all(Radius.circular(5))),
|
||||
child: Image.asset(
|
||||
"images/take_video.png",
|
||||
package: 'tencent_cloud_chat_uikit',
|
||||
height: 64,
|
||||
width: 64,
|
||||
),
|
||||
)),
|
||||
if (PlatformUtils().isWeb)
|
||||
MorePanelItem(
|
||||
id: "image",
|
||||
title: TIM_t("图片"),
|
||||
onTap: (c) {
|
||||
_onFeatureTap(
|
||||
"image",
|
||||
c,
|
||||
model,
|
||||
theme,
|
||||
);
|
||||
},
|
||||
icon: Container(
|
||||
height: 64,
|
||||
width: 64,
|
||||
margin: const EdgeInsets.only(bottom: 4),
|
||||
decoration: const BoxDecoration(color: Colors.white, borderRadius: BorderRadius.all(Radius.circular(5))),
|
||||
child: SvgPicture.asset(
|
||||
"images/photo.svg",
|
||||
package: 'tencent_cloud_chat_uikit',
|
||||
height: 64,
|
||||
width: 64,
|
||||
),
|
||||
)),
|
||||
if (PlatformUtils().isWeb)
|
||||
MorePanelItem(
|
||||
id: "video",
|
||||
title: TIM_t("视频"),
|
||||
onTap: (c) {
|
||||
_onFeatureTap(
|
||||
"video",
|
||||
c,
|
||||
model,
|
||||
theme,
|
||||
);
|
||||
},
|
||||
icon: Container(
|
||||
height: 64,
|
||||
width: 64,
|
||||
margin: const EdgeInsets.only(bottom: 4),
|
||||
decoration: const BoxDecoration(color: Colors.white, borderRadius: BorderRadius.all(Radius.circular(5))),
|
||||
child: Icon(Icons.video_file, color: hexToColor("5c6168"), size: 26),
|
||||
)),
|
||||
MorePanelItem(
|
||||
id: "file",
|
||||
title: TIM_t("文件"),
|
||||
onTap: (c) {
|
||||
_onFeatureTap(
|
||||
"file",
|
||||
c,
|
||||
model,
|
||||
theme,
|
||||
);
|
||||
},
|
||||
icon: Container(
|
||||
height: 64,
|
||||
width: 64,
|
||||
margin: const EdgeInsets.only(bottom: 4),
|
||||
decoration: const BoxDecoration(color: Colors.white, borderRadius: BorderRadius.all(Radius.circular(5))),
|
||||
child: SvgPicture.asset(
|
||||
"images/file.svg",
|
||||
package: 'tencent_cloud_chat_uikit',
|
||||
height: 64,
|
||||
width: 64,
|
||||
),
|
||||
)),
|
||||
if (isInstallCallkit && PlatformUtils().isMobile)
|
||||
MorePanelItem(
|
||||
id: "videoCall",
|
||||
title: TIM_t("视频通话"),
|
||||
onTap: (c) {
|
||||
_onFeatureTap(
|
||||
"videoCall",
|
||||
c,
|
||||
model,
|
||||
theme,
|
||||
);
|
||||
},
|
||||
icon: Container(
|
||||
height: 64,
|
||||
width: 64,
|
||||
margin: const EdgeInsets.only(bottom: 4),
|
||||
decoration: const BoxDecoration(color: Colors.white, borderRadius: BorderRadius.all(Radius.circular(5))),
|
||||
child: SvgPicture.asset(
|
||||
"images/video-call.svg",
|
||||
package: 'tencent_cloud_chat_uikit',
|
||||
height: 64,
|
||||
width: 64,
|
||||
),
|
||||
)),
|
||||
if (isInstallCallkit && PlatformUtils().isMobile)
|
||||
MorePanelItem(
|
||||
id: "voiceCall",
|
||||
title: TIM_t("语音通话"),
|
||||
onTap: (c) {
|
||||
_onFeatureTap(
|
||||
"voiceCall",
|
||||
c,
|
||||
model,
|
||||
theme,
|
||||
);
|
||||
},
|
||||
icon: Container(
|
||||
height: 64,
|
||||
width: 64,
|
||||
margin: const EdgeInsets.only(bottom: 4),
|
||||
decoration: const BoxDecoration(color: Colors.white, borderRadius: BorderRadius.all(Radius.circular(5))),
|
||||
child: SvgPicture.asset(
|
||||
"images/voice-call.svg",
|
||||
package: 'tencent_cloud_chat_uikit',
|
||||
height: 64,
|
||||
width: 64,
|
||||
),
|
||||
)),
|
||||
if (config.extraAction != null) ...?config.extraAction,
|
||||
].where((element) {
|
||||
if (element.id == "screen") {
|
||||
return config.showCameraAction;
|
||||
}
|
||||
|
||||
if (element.id == "file") {
|
||||
return config.showFilePickAction;
|
||||
}
|
||||
|
||||
if (element.id == "photo") {
|
||||
return config.showGalleryPickAction;
|
||||
}
|
||||
|
||||
if (element.id == "image") {
|
||||
return config.showWebImagePickAction;
|
||||
}
|
||||
|
||||
if (element.id == "video") {
|
||||
return config.showWebVideoPickAction;
|
||||
}
|
||||
if (element.id == "voiceCall") {
|
||||
return config.showVoiceCall;
|
||||
}
|
||||
if (element.id == "videoCall") {
|
||||
return config.showVideoCall;
|
||||
}
|
||||
return true;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
_sendVideoMessage(String originFilePath, int duration, int size, TUIChatSeparateViewModel model) async {
|
||||
if (size >= MorePanelConfig.VIDEO_MAX_SIZE) {
|
||||
onTIMCallback(TIMCallback(type: TIMCallbackType.INFO, infoRecommendText: TIM_t("文件大小超出了限制")));
|
||||
return;
|
||||
}
|
||||
|
||||
final plugin = FcNativeVideoThumbnail();
|
||||
|
||||
final convID = widget.conversationID;
|
||||
final convType = widget.conversationType;
|
||||
|
||||
String tempPath = (await getTemporaryDirectory()).path + p.basename(originFilePath) + ".jpeg";
|
||||
|
||||
await plugin.getVideoThumbnail(
|
||||
srcFile: originFilePath,
|
||||
destFile: tempPath,
|
||||
format: 'jpeg',
|
||||
width: 1280,
|
||||
quality: 100,
|
||||
height: 1280,
|
||||
);
|
||||
MessageUtils.handleMessageError(
|
||||
model.sendVideoMessage(
|
||||
videoPath: originFilePath, duration: duration, snapshotPath: tempPath, convID: convID, convType: convType),
|
||||
context);
|
||||
}
|
||||
|
||||
_sendImageMessage(TUIChatSeparateViewModel model, TUITheme theme) async {
|
||||
try {
|
||||
if (PlatformUtils().isMobile) {
|
||||
if (PlatformUtils().isAndroid) {
|
||||
AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo;
|
||||
if ((androidInfo.version.sdkInt) >= 33) {
|
||||
final videos = await Permissions.checkPermission(
|
||||
context,
|
||||
Permission.videos.value,
|
||||
theme,
|
||||
);
|
||||
final photos = await Permissions.checkPermission(
|
||||
context,
|
||||
Permission.photos.value,
|
||||
theme,
|
||||
);
|
||||
if (!videos && !photos) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
final storage = await Permissions.checkPermission(
|
||||
context,
|
||||
Permission.storage.value,
|
||||
theme,
|
||||
);
|
||||
if (!storage) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
final photos = await Permissions.checkPermission(
|
||||
context,
|
||||
Permission.photos.value,
|
||||
theme,
|
||||
);
|
||||
if (!photos) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final convID = widget.conversationID;
|
||||
final convType = widget.conversationType;
|
||||
|
||||
if (PlatformUtils().isMobile) {
|
||||
final pickedAssets = await AssetPicker.pickAssets(context);
|
||||
|
||||
if (pickedAssets != null) {
|
||||
for (var asset in pickedAssets) {
|
||||
final originFile = await asset.originFile;
|
||||
final filePath = originFile?.path;
|
||||
final type = asset.type;
|
||||
final size = await originFile!.length();
|
||||
if (filePath != null) {
|
||||
if (type == AssetType.image) {
|
||||
if (size >= MorePanelConfig.IMAGE_MAX_SIZE) {
|
||||
onTIMCallback(TIMCallback(type: TIMCallbackType.INFO, infoRecommendText: TIM_t("文件大小超出了限制")));
|
||||
return;
|
||||
}
|
||||
|
||||
MessageUtils.handleMessageError(
|
||||
model.sendImageMessage(imagePath: filePath, convID: convID, convType: convType), context);
|
||||
}
|
||||
|
||||
if (type == AssetType.video) {
|
||||
_sendVideoMessage(originFile!.path, asset.videoDuration.inSeconds, size, model);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
FilePickerResult? result = await FilePicker.platform.pickFiles(type: FileType.media);
|
||||
if (result != null && result.files.isNotEmpty) {
|
||||
File file = File(result.files.single.path!);
|
||||
final String savePath = file.path;
|
||||
final String type =
|
||||
TencentUtils.getFileType(savePath.split(".")[savePath.split(".").length - 1]).split("/")[0];
|
||||
|
||||
if (type == "image") {
|
||||
MessageUtils.handleMessageError(
|
||||
model.sendImageMessage(imagePath: savePath, convID: convID, convType: convType), context);
|
||||
} else if (type == "video") {
|
||||
MessageUtils.handleMessageError(
|
||||
model.sendVideoMessage(videoPath: savePath, convID: convID, convType: convType), context);
|
||||
}
|
||||
} else {
|
||||
throw TypeError();
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
outputLogger.i("err: $err");
|
||||
}
|
||||
}
|
||||
|
||||
_sendImageFromCamera(TUIChatSeparateViewModel model, TUITheme theme, {required isVideo}) async {
|
||||
try {
|
||||
if (!await Permissions.checkPermission(
|
||||
context,
|
||||
Permission.camera.value,
|
||||
theme,
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
await Permissions.checkPermission(
|
||||
context,
|
||||
Permission.microphone.value,
|
||||
theme,
|
||||
);
|
||||
|
||||
final convID = widget.conversationID;
|
||||
final convType = widget.conversationType;
|
||||
final ImagePicker picker = ImagePicker();
|
||||
XFile? originFile;
|
||||
if (isVideo) {
|
||||
originFile = await picker.pickVideo(source: ImageSource.camera);
|
||||
} else {
|
||||
originFile = await picker.pickImage(source: ImageSource.camera);
|
||||
}
|
||||
final size = await originFile!.length();
|
||||
if (!isVideo) {
|
||||
if (size >= MorePanelConfig.IMAGE_MAX_SIZE) {
|
||||
onTIMCallback(TIMCallback(type: TIMCallbackType.INFO, infoRecommendText: TIM_t("文件大小超出了限制")));
|
||||
return;
|
||||
}
|
||||
|
||||
MessageUtils.handleMessageError(
|
||||
model.sendImageMessage(imagePath: originFile.path, convID: convID, convType: convType), context);
|
||||
} else {
|
||||
// 监听视频准备完成事件
|
||||
_betterPlayerController.addEventsListener((event) {
|
||||
if (event.betterPlayerEventType == BetterPlayerEventType.initialized) {
|
||||
// 获取视频时长(单位:秒)
|
||||
int durationInSeconds = _betterPlayerController.videoPlayerController?.value.duration?.inSeconds ?? 0;
|
||||
_sendVideoMessage(originFile!.path, durationInSeconds, size, model);
|
||||
}
|
||||
});
|
||||
|
||||
// 加载视频源
|
||||
_betterPlayerController.setupDataSource(
|
||||
BetterPlayerDataSource(
|
||||
BetterPlayerDataSourceType.file,
|
||||
originFile.path, // 替换为你的视频 URL
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
outputLogger.i("err: $error");
|
||||
}
|
||||
}
|
||||
|
||||
_sendImageFileOnWeb(TUIChatSeparateViewModel model) async {
|
||||
try {
|
||||
final pickedFile = await _picker.pickImage(source: ImageSource.gallery);
|
||||
final imageContent = await pickedFile!.readAsBytes();
|
||||
fileName = pickedFile.name;
|
||||
tempFile = File(pickedFile.path);
|
||||
fileContent = imageContent;
|
||||
|
||||
html.Node? inputElem;
|
||||
inputElem = html.document.getElementById("__image_picker_web-file-input")?.querySelector("input");
|
||||
final convID = widget.conversationID;
|
||||
final convType = widget.conversationType;
|
||||
MessageUtils.handleMessageError(
|
||||
model.sendImageMessage(
|
||||
inputElement: inputElem, imagePath: tempFile?.path, convID: convID, convType: convType),
|
||||
context);
|
||||
} catch (e) {
|
||||
outputLogger.i("_sendFileErr: ${e.toString()}");
|
||||
}
|
||||
}
|
||||
|
||||
_sendVideoFileOnWeb(TUIChatSeparateViewModel model) async {
|
||||
try {
|
||||
final pickedFile = await _picker.pickVideo(source: ImageSource.gallery);
|
||||
final videoContent = await pickedFile!.readAsBytes();
|
||||
fileName = pickedFile.name;
|
||||
tempFile = File(pickedFile.path);
|
||||
fileContent = videoContent;
|
||||
|
||||
if (fileName!.split(".")[fileName!.split(".").length - 1] != "mp4") {
|
||||
onTIMCallback(
|
||||
TIMCallback(type: TIMCallbackType.INFO, infoRecommendText: TIM_t("视频消息仅限 mp4 格式"), infoCode: 6660412));
|
||||
return;
|
||||
}
|
||||
|
||||
html.Node? inputElem;
|
||||
inputElem = html.document.getElementById("__image_picker_web-file-input")?.querySelector("input");
|
||||
final convID = widget.conversationID;
|
||||
final convType = widget.conversationType;
|
||||
MessageUtils.handleMessageError(
|
||||
model.sendVideoMessage(
|
||||
inputElement: inputElem, videoPath: tempFile?.path, convID: convID, convType: convType),
|
||||
context);
|
||||
} catch (e) {
|
||||
outputLogger.i("_sendFileErr: ${e.toString()}");
|
||||
}
|
||||
}
|
||||
|
||||
_sendFile(
|
||||
TUIChatSeparateViewModel model,
|
||||
TUITheme theme,
|
||||
) async {
|
||||
try {
|
||||
final convID = widget.conversationID;
|
||||
final convType = widget.conversationType;
|
||||
FilePickerResult? result = await FilePicker.platform.pickFiles();
|
||||
if (result != null && result.files.isNotEmpty) {
|
||||
if (PlatformUtils().isWeb) {
|
||||
html.Node? inputElem;
|
||||
inputElem = html.document.getElementById("__file_picker_web-file-input")?.querySelector("input");
|
||||
fileName = result.files.single.name;
|
||||
|
||||
MessageUtils.handleMessageError(
|
||||
model.sendFileMessage(inputElement: inputElem, fileName: fileName, convID: convID, convType: convType),
|
||||
context);
|
||||
return;
|
||||
}
|
||||
|
||||
String? option2 = result.files.single.path ?? "";
|
||||
outputLogger.i(TIM_t_para("选择成功{{option2}}", "选择成功$option2")(option2: option2));
|
||||
|
||||
File file = File(result.files.single.path!);
|
||||
final int size = file.lengthSync();
|
||||
if (size >= MorePanelConfig.FILE_MAX_SIZE) {
|
||||
onTIMCallback(TIMCallback(type: TIMCallbackType.INFO, infoRecommendText: TIM_t("文件大小超出了限制")));
|
||||
return;
|
||||
}
|
||||
|
||||
final String savePath = file.path;
|
||||
|
||||
MessageUtils.handleMessageError(
|
||||
model.sendFileMessage(filePath: savePath, size: size, convID: convID, convType: convType), context);
|
||||
} else {
|
||||
throw TypeError();
|
||||
}
|
||||
} catch (e) {
|
||||
outputLogger.i("_sendFileErr: ${e.toString()}");
|
||||
}
|
||||
}
|
||||
|
||||
_onFeatureTap(
|
||||
String id,
|
||||
BuildContext context,
|
||||
TUIChatSeparateViewModel model,
|
||||
TUITheme theme,
|
||||
) async {
|
||||
switch (id) {
|
||||
case "photo":
|
||||
_sendImageMessage(model, theme);
|
||||
break;
|
||||
case "take_photo":
|
||||
_sendImageFromCamera(model, theme, isVideo: false);
|
||||
break;
|
||||
case "take_video":
|
||||
_sendImageFromCamera(model, theme, isVideo: true);
|
||||
break;
|
||||
case "file":
|
||||
_sendFile(model, theme);
|
||||
break;
|
||||
case "image":
|
||||
// only for web
|
||||
_sendImageFileOnWeb(model);
|
||||
break;
|
||||
case "video":
|
||||
// only for web
|
||||
_sendVideoFileOnWeb(model);
|
||||
break;
|
||||
case "voiceCall":
|
||||
_goToVideoUI(TYPE_AUDIO);
|
||||
break;
|
||||
case "videoCall":
|
||||
_goToVideoUI(TYPE_VIDEO);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_goToVideoUI(String type) async {
|
||||
if (!PlatformUtils().isWeb) {
|
||||
bool hasCameraPermission = false;
|
||||
bool hasMicrophonePermission = false;
|
||||
if (type == TYPE_VIDEO) {
|
||||
hasCameraPermission = await Permissions.checkPermission(context, Permission.camera.value);
|
||||
hasMicrophonePermission = await Permissions.checkPermission(context, Permission.microphone.value);
|
||||
if (!hasCameraPermission || !hasMicrophonePermission) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
hasMicrophonePermission = await Permissions.checkPermission(context, Permission.microphone.value);
|
||||
if (!hasMicrophonePermission) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final isGroup = widget.conversationType == ConvType.group;
|
||||
if (isGroup) {
|
||||
List<V2TimGroupMemberFullInfo>? selectedMember = await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => SelectCallInviter(
|
||||
groupID: widget.conversationID,
|
||||
),
|
||||
),
|
||||
);
|
||||
if (selectedMember != null) {
|
||||
final inviteMember = selectedMember.map((e) => e.userID).toList();
|
||||
_tUICore.callService(TUICALLKIT_SERVICE_NAME, METHOD_NAME_CALL, {
|
||||
PARAM_NAME_TYPE: type,
|
||||
PARAM_NAME_USERIDS: inviteMember,
|
||||
PARAM_NAME_GROUPID: widget.conversationType == ConvType.group ? widget.conversationID : ""
|
||||
});
|
||||
}
|
||||
} else {
|
||||
_tUICore.callService(TUICALLKIT_SERVICE_NAME, METHOD_NAME_CALL, {
|
||||
PARAM_NAME_TYPE: type,
|
||||
PARAM_NAME_USERIDS: [widget.conversationID],
|
||||
PARAM_NAME_GROUPID: ""
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_betterPlayerController?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final TUITheme theme = value.theme;
|
||||
final TUIChatSeparateViewModel model = Provider.of<TUIChatSeparateViewModel>(context);
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
return Container(
|
||||
height: 248,
|
||||
decoration: BoxDecoration(
|
||||
// color: hexToColor("EBF0F6"),
|
||||
border: Border(
|
||||
top: BorderSide(width: 1, color: Colors.grey.shade300),
|
||||
),
|
||||
),
|
||||
padding: const EdgeInsets.only(top: 20, left: 20, right: 20),
|
||||
width: screenWidth,
|
||||
child: Scrollbar(
|
||||
controller: _scrollController,
|
||||
child: SingleChildScrollView(
|
||||
controller: _scrollController,
|
||||
child: Wrap(
|
||||
spacing: (screenWidth - (23 * 2) - 64 * 4) / 3,
|
||||
runSpacing: 20,
|
||||
children: itemList(model, theme)
|
||||
.map((item) => InkWell(
|
||||
onTap: () {
|
||||
if (item.onTap != null) {
|
||||
item.onTap!(context);
|
||||
}
|
||||
},
|
||||
child: widget.morePanelConfig?.actionBuilder != null
|
||||
? widget.morePanelConfig?.actionBuilder!(item)
|
||||
: SizedBox(
|
||||
height: 94,
|
||||
width: 64,
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
height: 64,
|
||||
width: 64,
|
||||
margin: const EdgeInsets.only(bottom: 4),
|
||||
decoration: const BoxDecoration(borderRadius: BorderRadius.all(Radius.circular(5))),
|
||||
child: item.icon,
|
||||
),
|
||||
Text(
|
||||
item.title,
|
||||
style: TextStyle(fontSize: 12, color: theme.darkTextColor),
|
||||
)
|
||||
],
|
||||
),
|
||||
)))
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,322 @@
|
||||
// ignore_for_file: avoid_print, unused_import
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:tencent_chat_i18n_tool/tencent_chat_i18n_tool.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_chat_separate_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_chat_global_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/services_locatar.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/message.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/permission.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/sound_record.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/logger.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_callback.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/tui_theme.dart';
|
||||
|
||||
class SendSoundMessage extends StatefulWidget {
|
||||
/// conversation ID
|
||||
final String conversationID;
|
||||
|
||||
/// control the list to bottom
|
||||
final VoidCallback onDownBottom;
|
||||
|
||||
/// the conversation type
|
||||
final ConvType conversationType;
|
||||
|
||||
const SendSoundMessage(
|
||||
{required this.conversationID,
|
||||
required this.conversationType,
|
||||
Key? key,
|
||||
required this.onDownBottom})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _SendSoundMessageState();
|
||||
}
|
||||
|
||||
class _SendSoundMessageState extends TIMUIKitState<SendSoundMessage> {
|
||||
final TUIChatGlobalModel model = serviceLocator<TUIChatGlobalModel>();
|
||||
String soundTipsText = "";
|
||||
bool isRecording = false;
|
||||
bool isInit = false;
|
||||
bool isCancelSend = false;
|
||||
DateTime startTime = DateTime.now();
|
||||
List<StreamSubscription<Object>> subscriptions = [];
|
||||
|
||||
OverlayEntry? overlayEntry;
|
||||
String voiceIcon = "images/voice_volume_1.png";
|
||||
double volume = 0.1;
|
||||
|
||||
buildOverLayView(BuildContext context) {
|
||||
if (overlayEntry == null) {
|
||||
overlayEntry = OverlayEntry(builder: (content) {
|
||||
return Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: MediaQuery.of(context).size.width,
|
||||
height: MediaQuery.of(context).size.height,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
type: MaterialType.canvas,
|
||||
child: Center(
|
||||
child: Opacity(
|
||||
opacity: 0.8,
|
||||
child: Container(
|
||||
width: 160,
|
||||
height: 160,
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xff77797A),
|
||||
borderRadius: BorderRadius.all(Radius.circular(20.0)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
const SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
margin: const EdgeInsets.only(top: 10),
|
||||
child: Image.asset(
|
||||
"images/microphone.png",
|
||||
width: 50,
|
||||
height: 60,
|
||||
package: 'flutter_plugin_record_plus',
|
||||
),
|
||||
),
|
||||
ClipRect(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
child: Align(
|
||||
heightFactor: max(min(volume, 1), 0.1),
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: SizedBox(
|
||||
width: 50,
|
||||
height: 60,
|
||||
child: Image.asset(
|
||||
"images/voice_volume_total.png",
|
||||
width: 50,
|
||||
height: 60,
|
||||
package: 'flutter_plugin_record_plus',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
Text(
|
||||
soundTipsText,
|
||||
style: const TextStyle(
|
||||
fontStyle: FontStyle.normal,
|
||||
color: Colors.white,
|
||||
fontSize: 14,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
Overlay.of(context).insert(overlayEntry!);
|
||||
}
|
||||
}
|
||||
|
||||
onLongPressStart(_) {
|
||||
if (isInit) {
|
||||
setState(() {
|
||||
soundTipsText = TIM_t("手指上滑,取消发送");
|
||||
});
|
||||
startTime = DateTime.now();
|
||||
SoundPlayer.startRecord();
|
||||
buildOverLayView(context);
|
||||
}
|
||||
}
|
||||
|
||||
onLongPressUpdate(e) {
|
||||
double height = MediaQuery.of(context).size.height * 0.5 - 240;
|
||||
double dy = e.localPosition.dy;
|
||||
|
||||
if (dy.abs() > height) {
|
||||
if (mounted && soundTipsText != TIM_t("松开取消")) {
|
||||
setState(() {
|
||||
soundTipsText = TIM_t("松开取消");
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (mounted && soundTipsText == TIM_t("松开取消")) {
|
||||
setState(() {
|
||||
soundTipsText = TIM_t("手指上滑,取消发送");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onLongPressEnd(e) {
|
||||
double dy = e.localPosition.dy;
|
||||
// 此高度为 160为录音取消组件距离顶部的预留距离
|
||||
double height = MediaQuery.of(context).size.height * 0.5 - 240;
|
||||
if (dy.abs() > height) {
|
||||
isCancelSend = true;
|
||||
} else {
|
||||
isCancelSend = false;
|
||||
}
|
||||
if (overlayEntry != null) {
|
||||
overlayEntry!.remove();
|
||||
overlayEntry = null;
|
||||
}
|
||||
// Did not receive onStop from FlutterPluginRecord if the duration is too short.
|
||||
if (DateTime.now().difference(startTime).inSeconds < 1) {
|
||||
isCancelSend = true;
|
||||
onTIMCallback(TIMCallback(
|
||||
type: TIMCallbackType.INFO,
|
||||
infoRecommendText: TIM_t("说话时间太短"),
|
||||
infoCode: 6660404));
|
||||
}
|
||||
stop();
|
||||
}
|
||||
|
||||
onLonePressCancel() {
|
||||
if (isRecording) {
|
||||
isCancelSend = true;
|
||||
if (overlayEntry != null) {
|
||||
overlayEntry!.remove();
|
||||
overlayEntry = null;
|
||||
}
|
||||
stop();
|
||||
}
|
||||
}
|
||||
|
||||
void stop() {
|
||||
setState(() {
|
||||
isRecording = false;
|
||||
});
|
||||
SoundPlayer.stopRecord();
|
||||
setState(() {
|
||||
soundTipsText = TIM_t("手指上滑,取消发送");
|
||||
});
|
||||
}
|
||||
|
||||
sendSound(
|
||||
{required String path,
|
||||
required int duration,
|
||||
required TUIChatSeparateViewModel model}) {
|
||||
final convID = widget.conversationID;
|
||||
final convType = widget.conversationType;
|
||||
|
||||
if (duration > 0) {
|
||||
if (!isCancelSend) {
|
||||
MessageUtils.handleMessageError(
|
||||
model.sendSoundMessage(
|
||||
soundPath: path,
|
||||
duration: duration,
|
||||
convID: convID,
|
||||
convType: convType),
|
||||
context);
|
||||
widget.onDownBottom();
|
||||
} else {
|
||||
isCancelSend = false;
|
||||
}
|
||||
} else {
|
||||
onTIMCallback(TIMCallback(
|
||||
type: TIMCallbackType.INFO,
|
||||
infoRecommendText: TIM_t("说话时间太短"),
|
||||
infoCode: 6660404));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
dispose() {
|
||||
for (var subscription in subscriptions) {
|
||||
subscription.cancel();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
initRecordSound(TUIChatSeparateViewModel model) {
|
||||
final responseSubscription = SoundPlayer.responseListener((recordResponse) {
|
||||
final status = recordResponse.msg;
|
||||
if (status == "onStop") {
|
||||
if (!isCancelSend) {
|
||||
final soundPath = recordResponse.path;
|
||||
final recordDuration = recordResponse.audioTimeLength;
|
||||
sendSound(
|
||||
path: soundPath!, duration: recordDuration!.ceil(), model: model);
|
||||
}
|
||||
} else if (status == "onStart") {
|
||||
outputLogger.i("start record");
|
||||
setState(() {
|
||||
isRecording = true;
|
||||
});
|
||||
} else {
|
||||
outputLogger.i(status.toString());
|
||||
}
|
||||
});
|
||||
final amplitudesResponseSubscription =
|
||||
SoundPlayer.responseFromAmplitudeListener((recordResponse) {
|
||||
setState(() {
|
||||
volume = double.parse(recordResponse.msg!) * 1.1;
|
||||
if (overlayEntry != null) {
|
||||
overlayEntry!.markNeedsBuild();
|
||||
}
|
||||
});
|
||||
});
|
||||
subscriptions = [responseSubscription, amplitudesResponseSubscription];
|
||||
SoundPlayer.initSoundPlayer();
|
||||
isInit = true;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final TUITheme theme = value.theme;
|
||||
final TUIChatSeparateViewModel model =
|
||||
Provider.of<TUIChatSeparateViewModel>(context);
|
||||
return GestureDetector(
|
||||
onTapDown: (detail) async {
|
||||
if (!isInit) {
|
||||
bool hasMicrophonePermission = await Permissions.checkPermission(
|
||||
context,
|
||||
Permission.microphone.value,
|
||||
theme,
|
||||
);
|
||||
if (!hasMicrophonePermission) {
|
||||
return;
|
||||
}
|
||||
initRecordSound(model);
|
||||
}
|
||||
},
|
||||
onLongPressStart: onLongPressStart,
|
||||
onLongPressMoveUpdate: onLongPressUpdate,
|
||||
onLongPressEnd: onLongPressEnd,
|
||||
onLongPressCancel: onLonePressCancel,
|
||||
child: Container(
|
||||
height: 35,
|
||||
color: isRecording ? theme.weakBackgroundColor : Colors.white,
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
TIM_t("按住说话"),
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
color: theme.darkTextColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,50 @@
|
||||
import 'package:flutter/material.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';
|
||||
|
||||
enum ActionType { hideAllPanel, longPressToAt, setTextField, requestFocus, handleAtMember }
|
||||
|
||||
class TIMUIKitInputTextFieldController extends ChangeNotifier {
|
||||
TextEditingController? textEditingController = TextEditingController();
|
||||
ActionType? actionType;
|
||||
String? atUserName;
|
||||
String? atUserID;
|
||||
String inputText = "";
|
||||
V2TimGroupMemberFullInfo? groupMemberFullInfo;
|
||||
|
||||
TIMUIKitInputTextFieldController([TextEditingController? controller]) {
|
||||
if (controller != null) {
|
||||
textEditingController = controller;
|
||||
}
|
||||
}
|
||||
|
||||
/// text field unfocused and hide all panel
|
||||
hideAllPanel() {
|
||||
actionType = ActionType.hideAllPanel;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
longPressToAt(String? userName, String? userID) {
|
||||
actionType = ActionType.longPressToAt;
|
||||
atUserName = userName;
|
||||
atUserID = userID;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
setTextField(String text) {
|
||||
inputText = text;
|
||||
actionType = ActionType.setTextField;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
requestFocus() {
|
||||
actionType = ActionType.requestFocus;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
handleAtMember(V2TimGroupMemberFullInfo? memberInfo) {
|
||||
actionType = ActionType.handleAtMember;
|
||||
groupMemberFullInfo = memberInfo;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,632 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:extended_text_field/extended_text_field.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:tencent_chat_i18n_tool/tencent_chat_i18n_tool.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/business_logic/separate_models/tui_chat_separate_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_chat_global_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_setting_model.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/theme/color.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/tui_theme.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/message.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/optimize_utils.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/permission.dart';
|
||||
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_keyboard_visibility/tencent_keyboard_visibility.dart';
|
||||
|
||||
GlobalKey<_TIMUIKitTextFieldLayoutNarrowState> narrowTextFieldKey = GlobalKey();
|
||||
|
||||
class TIMUIKitTextFieldLayoutNarrow extends StatefulWidget {
|
||||
/// sticker panel customization
|
||||
final CustomStickerPanel? customStickerPanel;
|
||||
|
||||
final VoidCallback onEmojiSubmitted;
|
||||
final Function(int, String) onCustomEmojiFaceSubmitted;
|
||||
final Function(String, bool) handleSendEditStatus;
|
||||
final VoidCallback backSpaceText;
|
||||
final ValueChanged<String> addStickerToText;
|
||||
|
||||
final ValueChanged<String> handleAtText;
|
||||
|
||||
/// Whether to use the default emoji
|
||||
final bool isUseDefaultEmoji;
|
||||
|
||||
final bool isUseTencentCloudChatPackageOldKeys;
|
||||
|
||||
final TUIChatSeparateViewModel model;
|
||||
|
||||
/// background color
|
||||
final Color? backgroundColor;
|
||||
|
||||
/// control input field behavior
|
||||
final TIMUIKitInputTextFieldController? controller;
|
||||
|
||||
/// config for more panel
|
||||
final MorePanelConfig? morePanelConfig;
|
||||
|
||||
final String languageType;
|
||||
|
||||
final TextEditingController textEditingController;
|
||||
|
||||
/// conversation id
|
||||
final String conversationID;
|
||||
|
||||
/// conversation type
|
||||
final ConvType conversationType;
|
||||
|
||||
final FocusNode focusNode;
|
||||
|
||||
/// show more panel
|
||||
final bool showMorePanel;
|
||||
|
||||
/// hint text for textField widget
|
||||
final String? hintText;
|
||||
|
||||
final int? currentCursor;
|
||||
|
||||
final ValueChanged<int?> setCurrentCursor;
|
||||
|
||||
final VoidCallback onCursorChange;
|
||||
|
||||
/// show send audio icon
|
||||
final bool showSendAudio;
|
||||
|
||||
final VoidCallback handleSoftKeyBoardDelete;
|
||||
|
||||
/// on text changed
|
||||
final void Function(String)? onChanged;
|
||||
|
||||
final V2TimMessage? repliedMessage;
|
||||
|
||||
final void Function(String)? onDeleteText;
|
||||
|
||||
/// show send emoji icon
|
||||
final bool showSendEmoji;
|
||||
|
||||
final VoidCallback onSubmitted;
|
||||
|
||||
final VoidCallback goDownBottom;
|
||||
|
||||
final List<CustomEmojiFaceData> customEmojiStickerList;
|
||||
|
||||
final List<CustomStickerPackage> stickerPackageList;
|
||||
|
||||
const TIMUIKitTextFieldLayoutNarrow(
|
||||
{Key? key,
|
||||
this.customStickerPanel,
|
||||
required this.onEmojiSubmitted,
|
||||
required this.onCustomEmojiFaceSubmitted,
|
||||
required this.backSpaceText,
|
||||
required this.addStickerToText,
|
||||
required this.isUseDefaultEmoji,
|
||||
this.isUseTencentCloudChatPackageOldKeys = false,
|
||||
required this.languageType,
|
||||
required this.textEditingController,
|
||||
this.morePanelConfig,
|
||||
required this.conversationID,
|
||||
required this.conversationType,
|
||||
required this.focusNode,
|
||||
this.currentCursor,
|
||||
required this.setCurrentCursor,
|
||||
required this.onCursorChange,
|
||||
required this.model,
|
||||
this.backgroundColor,
|
||||
this.onChanged,
|
||||
this.onDeleteText,
|
||||
required this.handleSendEditStatus,
|
||||
required this.handleAtText,
|
||||
required this.handleSoftKeyBoardDelete,
|
||||
this.repliedMessage,
|
||||
required this.onSubmitted,
|
||||
required this.goDownBottom,
|
||||
required this.showSendAudio,
|
||||
required this.showSendEmoji,
|
||||
required this.showMorePanel,
|
||||
this.hintText,
|
||||
required this.customEmojiStickerList,
|
||||
this.controller,
|
||||
required this.stickerPackageList})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<TIMUIKitTextFieldLayoutNarrow> createState() => _TIMUIKitTextFieldLayoutNarrowState();
|
||||
}
|
||||
|
||||
class _TIMUIKitTextFieldLayoutNarrowState extends TIMUIKitState<TIMUIKitTextFieldLayoutNarrow> {
|
||||
final TUISettingModel settingModel = serviceLocator<TUISettingModel>();
|
||||
|
||||
bool showMore = false;
|
||||
bool showMoreButton = true;
|
||||
bool showSendSoundText = false;
|
||||
bool showEmojiPanel = false;
|
||||
bool showKeyboard = false;
|
||||
Function? setKeyboardHeight;
|
||||
double? bottomPadding;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.controller != null) {
|
||||
widget.controller?.addListener(
|
||||
() {
|
||||
final actionType = widget.controller?.actionType;
|
||||
if (actionType == ActionType.hideAllPanel) {
|
||||
hideAllPanel();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void setSendButton() {
|
||||
final value = widget.textEditingController.text;
|
||||
if (isWebDevice() || isAndroidDevice()) {
|
||||
if (value.isEmpty && showMoreButton != true) {
|
||||
setState(() {
|
||||
showMoreButton = true;
|
||||
});
|
||||
} else if (value.isNotEmpty && showMoreButton == true) {
|
||||
setState(() {
|
||||
showMoreButton = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hideAllPanel() {
|
||||
widget.focusNode.unfocus();
|
||||
widget.currentCursor == null;
|
||||
if (showKeyboard != false || showMore != false || showEmojiPanel != false) {
|
||||
setState(() {
|
||||
showKeyboard = false;
|
||||
showMore = false;
|
||||
showEmojiPanel = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Widget _getBottomContainer(TUITheme theme) {
|
||||
if (showEmojiPanel) {
|
||||
return widget.customStickerPanel != null
|
||||
? widget.customStickerPanel!(
|
||||
sendTextMessage: () {
|
||||
widget.onEmojiSubmitted();
|
||||
setSendButton();
|
||||
},
|
||||
sendFaceMessage: widget.onCustomEmojiFaceSubmitted,
|
||||
deleteText: () {
|
||||
widget.backSpaceText();
|
||||
setSendButton();
|
||||
},
|
||||
addText: (int unicode) {
|
||||
final newText = String.fromCharCode(unicode);
|
||||
widget.addStickerToText(newText);
|
||||
setSendButton();
|
||||
// handleSetDraftText();
|
||||
},
|
||||
addCustomEmojiText: ((String singleEmojiName) {
|
||||
String? emojiName = singleEmojiName.split('.png')[0];
|
||||
String compatibleEmojiName = emojiName;
|
||||
if (widget.isUseTencentCloudChatPackageOldKeys) {
|
||||
compatibleEmojiName = EmojiUtil.getCompatibleEmojiName(emojiName);
|
||||
}
|
||||
|
||||
String newText = '[$compatibleEmojiName]';
|
||||
widget.addStickerToText(newText);
|
||||
setSendButton();
|
||||
}),
|
||||
defaultCustomEmojiStickerList: widget.isUseDefaultEmoji ? TUIKitStickerConstData.emojiList : [])
|
||||
: StickerPanel(
|
||||
isWideScreen: false,
|
||||
sendTextMsg: () {
|
||||
widget.onEmojiSubmitted();
|
||||
setSendButton();
|
||||
},
|
||||
sendFaceMsg: widget.onCustomEmojiFaceSubmitted,
|
||||
deleteText: () {
|
||||
widget.backSpaceText();
|
||||
setSendButton();
|
||||
},
|
||||
addText: (int unicode) {
|
||||
final newText = String.fromCharCode(unicode);
|
||||
widget.addStickerToText(newText);
|
||||
setSendButton();
|
||||
// handleSetDraftText();
|
||||
},
|
||||
addCustomEmojiText: ((String singleEmojiName) {
|
||||
String? emojiName = singleEmojiName.split('.png')[0];
|
||||
String compatibleEmojiName = emojiName;
|
||||
if (widget.isUseTencentCloudChatPackageOldKeys) {
|
||||
compatibleEmojiName = EmojiUtil.getCompatibleEmojiName(emojiName);
|
||||
}
|
||||
|
||||
String newText = '[$compatibleEmojiName]';
|
||||
widget.addStickerToText(newText);
|
||||
setSendButton();
|
||||
}),
|
||||
customStickerPackageList: widget.stickerPackageList,
|
||||
lightPrimaryColor: theme.lightPrimaryColor);
|
||||
}
|
||||
|
||||
if (showMore) {
|
||||
return MorePanel(
|
||||
morePanelConfig: widget.morePanelConfig,
|
||||
conversationID: widget.conversationID,
|
||||
conversationType: widget.conversationType);
|
||||
}
|
||||
|
||||
return const SizedBox(height: 0);
|
||||
}
|
||||
|
||||
double _getBottomHeight() {
|
||||
if (showKeyboard) {
|
||||
final currentKeyboardHeight = MediaQuery.of(context).viewInsets.bottom;
|
||||
double originHeight = settingModel.keyboardHeight;
|
||||
if (currentKeyboardHeight != 0) {
|
||||
if (currentKeyboardHeight >= originHeight) {
|
||||
originHeight = currentKeyboardHeight;
|
||||
}
|
||||
if (setKeyboardHeight != null) {
|
||||
setKeyboardHeight!(currentKeyboardHeight);
|
||||
}
|
||||
}
|
||||
final height = originHeight != 0 ? originHeight : currentKeyboardHeight;
|
||||
return height;
|
||||
} else if (showMore || showEmojiPanel) {
|
||||
return 248.0 + (bottomPadding ?? 0.0);
|
||||
} else if (widget.textEditingController.text.length >= 46 && showKeyboard == false) {
|
||||
return 25 + (bottomPadding ?? 0.0);
|
||||
} else {
|
||||
return bottomPadding ?? 0;
|
||||
}
|
||||
}
|
||||
|
||||
_openMore() {
|
||||
if (!showMore) {
|
||||
widget.focusNode.unfocus();
|
||||
widget.setCurrentCursor(null);
|
||||
}
|
||||
setState(() {
|
||||
showKeyboard = false;
|
||||
showEmojiPanel = false;
|
||||
showSendSoundText = false;
|
||||
showMore = !showMore;
|
||||
});
|
||||
}
|
||||
|
||||
_openEmojiPanel() {
|
||||
widget.onCursorChange();
|
||||
showKeyboard = showEmojiPanel;
|
||||
if (showEmojiPanel) {
|
||||
widget.focusNode.requestFocus();
|
||||
} else {
|
||||
widget.focusNode.unfocus();
|
||||
}
|
||||
|
||||
setState(() {
|
||||
showMore = false;
|
||||
showSendSoundText = false;
|
||||
showEmojiPanel = !showEmojiPanel;
|
||||
});
|
||||
}
|
||||
|
||||
_debounce(
|
||||
Function(String text) fun, [
|
||||
Duration delay = const Duration(milliseconds: 30),
|
||||
]) {
|
||||
Timer? timer;
|
||||
return (String text) {
|
||||
if (timer != null) {
|
||||
timer?.cancel();
|
||||
}
|
||||
|
||||
timer = Timer(delay, () {
|
||||
fun(text);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
String getAbstractMessage(V2TimMessage message) {
|
||||
final String? customAbstractMessage =
|
||||
widget.model.abstractMessageBuilder != null ? widget.model.abstractMessageBuilder!(message) : null;
|
||||
return customAbstractMessage ?? MessageUtils.getAbstractMessageAsync(message, widget.model.groupMemberList ?? []);
|
||||
}
|
||||
|
||||
_buildRepliedMessage(V2TimMessage? repliedMessage) {
|
||||
final haveRepliedMessage = repliedMessage != null;
|
||||
if (haveRepliedMessage) {
|
||||
final String text = "${MessageUtils.getDisplayName(repliedMessage)}:${getAbstractMessage(repliedMessage)}";
|
||||
return Container(
|
||||
color: widget.backgroundColor ?? hexToColor("f5f5f6"),
|
||||
alignment: Alignment.centerLeft,
|
||||
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 16),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
text,
|
||||
softWrap: true,
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(color: hexToColor("8f959e"), fontSize: 14),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 16,
|
||||
),
|
||||
InkWell(
|
||||
onTap: () {
|
||||
widget.model.repliedMessage = null;
|
||||
},
|
||||
child: Icon(Icons.clear, color: hexToColor("8f959e"), size: 18),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
return Container();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final theme = value.theme;
|
||||
|
||||
setKeyboardHeight ??= OptimizeUtils.debounce((height) {
|
||||
settingModel.keyboardHeight = height;
|
||||
}, const Duration(seconds: 1));
|
||||
|
||||
final debounceFunc = _debounce((value) {
|
||||
if (isWebDevice() || isAndroidDevice()) {
|
||||
if (value.isEmpty && showMoreButton != true) {
|
||||
setState(() {
|
||||
showMoreButton = true;
|
||||
});
|
||||
} else if (value.isNotEmpty && showMoreButton == true) {
|
||||
setState(() {
|
||||
showMoreButton = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
if (widget.onChanged != null) {
|
||||
widget.onChanged!(value);
|
||||
}
|
||||
widget.handleAtText(value);
|
||||
widget.handleSendEditStatus(value, true);
|
||||
final isEmpty = value.isEmpty;
|
||||
if (isEmpty) {
|
||||
widget.handleSoftKeyBoardDelete();
|
||||
}
|
||||
}, const Duration(milliseconds: 80));
|
||||
|
||||
final MediaQueryData data = MediaQuery.of(context);
|
||||
EdgeInsets padding = data.padding;
|
||||
if (bottomPadding == null || padding.bottom > bottomPadding!) {
|
||||
bottomPadding = padding.bottom;
|
||||
}
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {},
|
||||
child: Column(
|
||||
children: [
|
||||
_buildRepliedMessage(widget.repliedMessage),
|
||||
Container(
|
||||
color: widget.backgroundColor ?? hexToColor("f5f5f6"),
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
|
||||
constraints: const BoxConstraints(minHeight: 50),
|
||||
child: Row(
|
||||
children: [
|
||||
if (PlatformUtils().isMobile && widget.showSendAudio)
|
||||
InkWell(
|
||||
onTap: () async {
|
||||
showKeyboard = showSendSoundText;
|
||||
if (showSendSoundText) {
|
||||
widget.focusNode.requestFocus();
|
||||
}
|
||||
if (await Permissions.checkPermission(
|
||||
context,
|
||||
Permission.microphone.value,
|
||||
theme,
|
||||
)) {
|
||||
setState(() {
|
||||
showEmojiPanel = false;
|
||||
showMore = false;
|
||||
showSendSoundText = !showSendSoundText;
|
||||
});
|
||||
}
|
||||
},
|
||||
child: SvgPicture.asset(
|
||||
showSendSoundText ? 'images/keyboard.svg' : 'images/voice.svg',
|
||||
package: 'tencent_cloud_chat_uikit',
|
||||
color: const Color.fromRGBO(68, 68, 68, 1),
|
||||
height: 28,
|
||||
width: 28,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 10,
|
||||
),
|
||||
Expanded(
|
||||
child: showSendSoundText
|
||||
? SendSoundMessage(
|
||||
onDownBottom: widget.goDownBottom,
|
||||
conversationID: widget.conversationID,
|
||||
conversationType: widget.conversationType)
|
||||
: Stack(children: [
|
||||
Center(
|
||||
child: KeyboardVisibility(
|
||||
child: ExtendedTextField(
|
||||
maxLines: 4,
|
||||
minLines: 1,
|
||||
focusNode: widget.focusNode,
|
||||
onChanged: debounceFunc,
|
||||
onTap: () {
|
||||
showKeyboard = true;
|
||||
widget.goDownBottom();
|
||||
setState(() {
|
||||
showEmojiPanel = false;
|
||||
showMore = false;
|
||||
});
|
||||
},
|
||||
keyboardType: TextInputType.multiline,
|
||||
textInputAction: PlatformUtils().isAndroid
|
||||
? TextInputAction.newline
|
||||
: TextInputAction.send,
|
||||
onEditingComplete: () {
|
||||
widget.onSubmitted();
|
||||
if (showKeyboard) {
|
||||
widget.focusNode.requestFocus();
|
||||
}
|
||||
setState(() {
|
||||
if (widget.textEditingController.text.isEmpty) {
|
||||
showMoreButton = true;
|
||||
}
|
||||
});
|
||||
},
|
||||
textAlignVertical: TextAlignVertical.top,
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
hintStyle: const TextStyle(
|
||||
// fontSize: 10,
|
||||
color: Color(0xffAEA4A3),
|
||||
),
|
||||
fillColor: Colors.white,
|
||||
filled: true,
|
||||
isDense: true,
|
||||
hintText: widget.hintText ?? ''),
|
||||
controller: widget.textEditingController,
|
||||
specialTextSpanBuilder: PlatformUtils().isWeb
|
||||
? null
|
||||
: DefaultSpecialTextSpanBuilder(
|
||||
isUseQQPackage:
|
||||
widget.model.chatConfig.stickerPanelConfig?.useQQStickerPackage ??
|
||||
true,
|
||||
isUseTencentCloudChatPackage: widget.model.chatConfig
|
||||
.stickerPanelConfig?.useTencentCloudChatStickerPackage ??
|
||||
true,
|
||||
isUseTencentCloudChatPackageOldKeys: widget
|
||||
.model
|
||||
.chatConfig
|
||||
.stickerPanelConfig
|
||||
?.useTencentCloudChatStickerPackageOldKeys ??
|
||||
false,
|
||||
customEmojiStickerList: widget.customEmojiStickerList,
|
||||
showAtBackground: true,
|
||||
checkHttpLink: false,
|
||||
)),
|
||||
onChanged: (bool visibility) {
|
||||
if (showKeyboard != visibility) {
|
||||
setState(() {
|
||||
showKeyboard = visibility;
|
||||
});
|
||||
}
|
||||
}),
|
||||
),
|
||||
RawKeyboardListener(
|
||||
autofocus: true,
|
||||
focusNode: FocusNode(),
|
||||
onKey: (key) {
|
||||
if (key is RawKeyDownEvent && key.logicalKey == LogicalKeyboardKey.backspace) {
|
||||
if (widget.onDeleteText != null) {
|
||||
widget.onDeleteText!(widget.textEditingController.text);
|
||||
}
|
||||
}
|
||||
},
|
||||
child: Container(),
|
||||
),
|
||||
]),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 10,
|
||||
),
|
||||
if (widget.showSendEmoji)
|
||||
InkWell(
|
||||
onTap: () {
|
||||
_openEmojiPanel();
|
||||
widget.goDownBottom();
|
||||
},
|
||||
child: PlatformUtils().isWeb
|
||||
? Icon(showEmojiPanel ? Icons.keyboard_alt_outlined : Icons.mood_outlined,
|
||||
color: hexToColor("5c6168"), size: 32)
|
||||
: SvgPicture.asset(
|
||||
showEmojiPanel ? 'images/keyboard.svg' : 'images/face.svg',
|
||||
package: 'tencent_cloud_chat_uikit',
|
||||
color: const Color.fromRGBO(68, 68, 68, 1),
|
||||
height: 28,
|
||||
width: 28,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 10,
|
||||
),
|
||||
if (widget.showMorePanel && showMoreButton)
|
||||
InkWell(
|
||||
onTap: () {
|
||||
// model.sendCustomMessage(data: "a", convID: model.currentSelectedConv, convType: model.currentSelectedConvType == 1 ? ConvType.c2c : ConvType.group);
|
||||
_openMore();
|
||||
widget.goDownBottom();
|
||||
},
|
||||
child: PlatformUtils().isWeb
|
||||
? Icon(Icons.add_circle_outline_outlined, color: hexToColor("5c6168"), size: 32)
|
||||
: SvgPicture.asset(
|
||||
'images/add.svg',
|
||||
package: 'tencent_cloud_chat_uikit',
|
||||
color: const Color.fromRGBO(68, 68, 68, 1),
|
||||
height: 28,
|
||||
width: 28,
|
||||
),
|
||||
),
|
||||
if ((isAndroidDevice() || isWebDevice()) && !showMoreButton)
|
||||
SizedBox(
|
||||
height: 32.0,
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
widget.onSubmitted();
|
||||
if (showKeyboard) {
|
||||
widget.focusNode.requestFocus();
|
||||
}
|
||||
if (widget.textEditingController.text.isEmpty) {
|
||||
setState(() {
|
||||
showMoreButton = true;
|
||||
});
|
||||
}
|
||||
},
|
||||
child: Text(TIM_t("发送")),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
AnimatedContainer(
|
||||
duration: Duration(milliseconds: (showKeyboard && PlatformUtils().isAndroid) ? 200 : 340),
|
||||
curve: Curves.fastOutSlowIn,
|
||||
height: max(_getBottomHeight(), 0.0),
|
||||
child: ListView(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
children: [_getBottomContainer(theme)],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,994 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
// ignore: unnecessary_import
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:extended_text_field/extended_text_field.dart';
|
||||
import 'package:fc_native_video_thumbnail/fc_native_video_thumbnail.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:pasteboard/pasteboard.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.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_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/business_logic/separate_models/tui_chat_separate_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_chat_global_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_setting_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/utils/logger.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/message.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/optimize_utils.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/platform.dart';
|
||||
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/wide_popup.dart';
|
||||
import 'package:universal_html/html.dart' as html;
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
import 'package:wechat_assets_picker/wechat_assets_picker.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.dart';
|
||||
|
||||
class DesktopControlBarItem {
|
||||
final String item;
|
||||
final IconData? icon;
|
||||
final String? imgPath;
|
||||
final String? svgPath;
|
||||
final Color? color;
|
||||
final ValueChanged<Offset?> onClick;
|
||||
final String? showName;
|
||||
final double? size;
|
||||
|
||||
DesktopControlBarItem(
|
||||
{required this.item,
|
||||
this.icon,
|
||||
this.color,
|
||||
this.imgPath,
|
||||
this.svgPath,
|
||||
required this.onClick,
|
||||
this.showName,
|
||||
this.size})
|
||||
: assert(icon != null || TencentUtils.checkString(imgPath) != null || TencentUtils.checkString(svgPath) != null);
|
||||
}
|
||||
|
||||
class DesktopControlBarConfig {
|
||||
final bool showStickerPanel;
|
||||
final bool showScreenshotButton;
|
||||
final bool showSendFileButton;
|
||||
final bool showSendImageButton;
|
||||
final bool showSendVideoButton;
|
||||
final bool showMessageHistoryButton;
|
||||
|
||||
DesktopControlBarConfig({
|
||||
this.showStickerPanel = true,
|
||||
this.showScreenshotButton = true,
|
||||
this.showSendFileButton = true,
|
||||
this.showSendImageButton = true,
|
||||
this.showSendVideoButton = true,
|
||||
this.showMessageHistoryButton = true,
|
||||
});
|
||||
}
|
||||
|
||||
class TIMUIKitTextFieldLayoutWide extends StatefulWidget {
|
||||
/// sticker panel customization
|
||||
final CustomStickerPanel? customStickerPanel;
|
||||
final VoidCallback onEmojiSubmitted;
|
||||
final Function(int, String) onCustomEmojiFaceSubmitted;
|
||||
final Function(String, bool) handleSendEditStatus;
|
||||
final VoidCallback backSpaceText;
|
||||
final ValueChanged<String> addStickerToText;
|
||||
final TUITheme theme;
|
||||
final ValueChanged<String> handleAtText;
|
||||
|
||||
/// Whether to use the default emoji
|
||||
final bool isUseDefaultEmoji;
|
||||
|
||||
final bool isCompatibleWithTencentCloudChatPackageOldKeys;
|
||||
|
||||
final TUIChatSeparateViewModel model;
|
||||
|
||||
/// background color
|
||||
final Color? backgroundColor;
|
||||
|
||||
/// control input field behavior
|
||||
final TIMUIKitInputTextFieldController? controller;
|
||||
|
||||
/// config for more panel
|
||||
final MorePanelConfig? morePanelConfig;
|
||||
|
||||
final String languageType;
|
||||
|
||||
final TextEditingController textEditingController;
|
||||
|
||||
/// conversation id
|
||||
final String conversationID;
|
||||
|
||||
/// conversation type
|
||||
final ConvType conversationType;
|
||||
|
||||
final FocusNode focusNode;
|
||||
|
||||
/// show more panel
|
||||
final bool showMorePanel;
|
||||
|
||||
/// hint text for textField widget
|
||||
final String? hintText;
|
||||
|
||||
final int? currentCursor;
|
||||
|
||||
final ValueChanged<int?> setCurrentCursor;
|
||||
|
||||
final VoidCallback onCursorChange;
|
||||
|
||||
/// show send audio icon
|
||||
final bool showSendAudio;
|
||||
|
||||
final TIMUIKitChatConfig chatConfig;
|
||||
|
||||
/// on text changed
|
||||
final void Function(String)? onChanged;
|
||||
|
||||
final V2TimMessage? repliedMessage;
|
||||
|
||||
/// show send emoji icon
|
||||
final bool showSendEmoji;
|
||||
|
||||
final VoidCallback onSubmitted;
|
||||
|
||||
final VoidCallback goDownBottom;
|
||||
|
||||
final List<CustomEmojiFaceData> customEmojiStickerList;
|
||||
|
||||
/// Conversation need search
|
||||
final V2TimConversation currentConversation;
|
||||
|
||||
final List<CustomStickerPackage> stickerPackageList;
|
||||
|
||||
const TIMUIKitTextFieldLayoutWide(
|
||||
{Key? key,
|
||||
this.customStickerPanel,
|
||||
required this.onEmojiSubmitted,
|
||||
required this.onCustomEmojiFaceSubmitted,
|
||||
required this.backSpaceText,
|
||||
required this.addStickerToText,
|
||||
required this.isUseDefaultEmoji,
|
||||
this.isCompatibleWithTencentCloudChatPackageOldKeys = false,
|
||||
required this.languageType,
|
||||
required this.textEditingController,
|
||||
this.morePanelConfig,
|
||||
required this.conversationID,
|
||||
required this.conversationType,
|
||||
required this.focusNode,
|
||||
this.currentCursor,
|
||||
required this.setCurrentCursor,
|
||||
required this.onCursorChange,
|
||||
required this.model,
|
||||
this.backgroundColor,
|
||||
this.onChanged,
|
||||
required this.handleSendEditStatus,
|
||||
required this.handleAtText,
|
||||
this.repliedMessage,
|
||||
required this.onSubmitted,
|
||||
required this.goDownBottom,
|
||||
required this.showSendAudio,
|
||||
required this.showSendEmoji,
|
||||
required this.showMorePanel,
|
||||
this.hintText,
|
||||
required this.customEmojiStickerList,
|
||||
this.controller,
|
||||
required this.currentConversation,
|
||||
required this.theme,
|
||||
required this.chatConfig,
|
||||
required this.stickerPackageList})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<TIMUIKitTextFieldLayoutWide> createState() => _TIMUIKitTextFieldLayoutWideState();
|
||||
}
|
||||
|
||||
class _TIMUIKitTextFieldLayoutWideState extends TIMUIKitState<TIMUIKitTextFieldLayoutWide> {
|
||||
final TUISettingModel settingModel = serviceLocator<TUISettingModel>();
|
||||
OverlayEntry? entry;
|
||||
final ImagePicker _picker = ImagePicker();
|
||||
Uint8List? fileContent;
|
||||
String? fileName;
|
||||
File? tempFile;
|
||||
Function? setKeyboardHeight;
|
||||
double? bottomPadding;
|
||||
late ScrollController _scrollController;
|
||||
late FocusNode textFocusNode;
|
||||
late List<DesktopControlBarItem> defaultControlBarItems;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.controller != null) {
|
||||
widget.controller?.addListener(() {
|
||||
final actionType = widget.controller?.actionType;
|
||||
if (actionType == ActionType.hideAllPanel) {
|
||||
hideAllPanel();
|
||||
}
|
||||
});
|
||||
}
|
||||
textFocusNode = FocusNode();
|
||||
widget.focusNode.requestFocus();
|
||||
_scrollController = ScrollController();
|
||||
try {
|
||||
if (PlatformUtils().isWeb) {
|
||||
html.window.addEventListener('paste', (event) {
|
||||
_handlePaste(event as html.ClipboardEvent);
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore: avoid_print
|
||||
outputLogger.i(e.toString());
|
||||
}
|
||||
generateDefaultControlBarItems();
|
||||
}
|
||||
|
||||
Future<void> _handlePaste(html.ClipboardEvent event) async {
|
||||
try {
|
||||
if (event.clipboardData!.files!.isNotEmpty) {
|
||||
html.File imageFile = event.clipboardData!.files![0];
|
||||
sendFileUseJs(imageFile);
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore: avoid_print
|
||||
outputLogger.i("Paste image failed: ${e.toString()}");
|
||||
}
|
||||
}
|
||||
|
||||
hideAllPanel() {
|
||||
widget.focusNode.unfocus();
|
||||
widget.currentCursor == null;
|
||||
}
|
||||
|
||||
_debounce(
|
||||
Function(String text) fun, [
|
||||
Duration delay = const Duration(milliseconds: 30),
|
||||
]) {
|
||||
Timer? timer;
|
||||
return (String text) {
|
||||
if (timer != null) {
|
||||
timer?.cancel();
|
||||
}
|
||||
|
||||
timer = Timer(delay, () {
|
||||
fun(text);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
String getAbstractMessage(V2TimMessage message) {
|
||||
final String? customAbstractMessage = widget.model.abstractMessageBuilder != null
|
||||
? widget.model.abstractMessageBuilder!(widget.model.repliedMessage!)
|
||||
: null;
|
||||
return customAbstractMessage ??
|
||||
MessageUtils.getAbstractMessageAsync(widget.model.repliedMessage!, widget.model.groupMemberList ?? []);
|
||||
}
|
||||
|
||||
_buildRepliedMessage(V2TimMessage? repliedMessage) {
|
||||
final haveRepliedMessage = repliedMessage != null;
|
||||
if (haveRepliedMessage) {
|
||||
return Container(
|
||||
color: widget.backgroundColor ?? hexToColor("f5f5f6"),
|
||||
alignment: Alignment.centerLeft,
|
||||
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 16),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
TIM_t("回复 "),
|
||||
style: TextStyle(color: hexToColor("8f959e"), fontSize: 14),
|
||||
),
|
||||
Text(
|
||||
MessageUtils.getDisplayName(widget.model.repliedMessage!),
|
||||
softWrap: true,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(color: hexToColor("8f959e"), fontSize: 14, fontWeight: FontWeight.bold),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
": ${getAbstractMessage(repliedMessage)}",
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: hexToColor("8f959e"),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 8,
|
||||
),
|
||||
InkWell(
|
||||
onTap: () {
|
||||
widget.model.repliedMessage = null;
|
||||
},
|
||||
child: Icon(Icons.cancel, color: hexToColor("8f959e"), size: 18),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
return Container();
|
||||
}
|
||||
|
||||
_sendEmoji(Offset? offset, TUITheme theme) {
|
||||
widget.onCursorChange();
|
||||
if (entry != null) {
|
||||
entry?.remove();
|
||||
entry = null;
|
||||
} else {
|
||||
entry = OverlayEntry(builder: (BuildContext context) {
|
||||
return TUIKitDragArea(
|
||||
closeFun: () {
|
||||
if (entry != null) {
|
||||
entry?.remove();
|
||||
entry = null;
|
||||
}
|
||||
},
|
||||
initOffset: offset != null
|
||||
? Offset(offset.dx, max(offset.dy, 16))
|
||||
: Offset(MediaQuery.of(context).size.height * 0.5 + 20, MediaQuery.of(context).size.height * 0.5 - 100),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
color: theme.wideBackgroundColor,
|
||||
border: Border.all(
|
||||
width: 2,
|
||||
color: theme.weakBackgroundColor ?? const Color(0xFFbebebe),
|
||||
),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0xFFbebebe),
|
||||
offset: Offset(5, 5),
|
||||
blurRadius: 10,
|
||||
spreadRadius: 1,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Container(
|
||||
child: widget.customStickerPanel != null
|
||||
? widget.customStickerPanel!(
|
||||
height: widget.chatConfig.desktopStickerPanelHeight,
|
||||
width: 350,
|
||||
sendTextMessage: () {
|
||||
widget.onEmojiSubmitted();
|
||||
},
|
||||
sendFaceMessage: widget.onCustomEmojiFaceSubmitted,
|
||||
deleteText: () {
|
||||
widget.backSpaceText();
|
||||
},
|
||||
addText: (int unicode) {
|
||||
final newText = String.fromCharCode(unicode);
|
||||
widget.addStickerToText(newText);
|
||||
entry?.remove();
|
||||
entry = null;
|
||||
},
|
||||
addCustomEmojiText: ((String singleEmojiName) {
|
||||
String? emojiName = singleEmojiName.split('.png')[0];
|
||||
String compatibleEmojiName = emojiName;
|
||||
if (widget.isCompatibleWithTencentCloudChatPackageOldKeys) {
|
||||
compatibleEmojiName = EmojiUtil.getCompatibleEmojiName(emojiName);
|
||||
}
|
||||
|
||||
String newText = '[$compatibleEmojiName]';
|
||||
widget.addStickerToText(newText);
|
||||
entry?.remove();
|
||||
entry = null;
|
||||
}),
|
||||
defaultCustomEmojiStickerList: widget.isUseDefaultEmoji ? TUIKitStickerConstData.emojiList : [])
|
||||
: Material(
|
||||
color: Colors.transparent,
|
||||
child: StickerPanel(
|
||||
isWideScreen: true,
|
||||
height: widget.chatConfig.desktopStickerPanelHeight,
|
||||
width: 350,
|
||||
sendTextMsg: null,
|
||||
sendFaceMsg: (_, __) {
|
||||
widget.onCustomEmojiFaceSubmitted(_, __);
|
||||
entry?.remove();
|
||||
entry = null;
|
||||
},
|
||||
deleteText: () {
|
||||
widget.backSpaceText();
|
||||
},
|
||||
addText: (int unicode) {
|
||||
final newText = String.fromCharCode(unicode);
|
||||
widget.addStickerToText(newText);
|
||||
entry?.remove();
|
||||
entry = null;
|
||||
},
|
||||
addCustomEmojiText: ((String singleEmojiName) {
|
||||
String? emojiName = singleEmojiName.split('.png')[0];
|
||||
String compatibleEmojiName = emojiName;
|
||||
if (widget.isCompatibleWithTencentCloudChatPackageOldKeys) {
|
||||
compatibleEmojiName = EmojiUtil.getCompatibleEmojiName(emojiName);
|
||||
}
|
||||
|
||||
String newText = '[$compatibleEmojiName]';
|
||||
widget.addStickerToText(newText);
|
||||
entry?.remove();
|
||||
entry = null;
|
||||
}),
|
||||
customStickerPackageList: widget.stickerPackageList,
|
||||
bottomColor: theme.weakBackgroundColor,
|
||||
backgroundColor: theme.wideBackgroundColor,
|
||||
lightPrimaryColor: theme.lightPrimaryColor),
|
||||
),
|
||||
),
|
||||
));
|
||||
});
|
||||
Overlay.of(context).insert(entry!);
|
||||
}
|
||||
}
|
||||
|
||||
_addGreyOverlay() {
|
||||
if (entry != null) {
|
||||
_removeOverlay();
|
||||
return;
|
||||
} else {
|
||||
entry = OverlayEntry(builder: (BuildContext context) {
|
||||
return Container(
|
||||
color: const Color(0x7F000000),
|
||||
);
|
||||
});
|
||||
Overlay.of(context).insert(entry!);
|
||||
}
|
||||
}
|
||||
|
||||
_removeOverlay() {
|
||||
entry?.remove();
|
||||
entry = null;
|
||||
}
|
||||
|
||||
_sendFile(
|
||||
TUIChatSeparateViewModel model,
|
||||
TUITheme theme,
|
||||
) async {
|
||||
if (!PlatformUtils().isWeb) {
|
||||
_addGreyOverlay();
|
||||
}
|
||||
try {
|
||||
final convID = widget.conversationID;
|
||||
final convType = widget.conversationType;
|
||||
FilePickerResult? result = await FilePicker.platform.pickFiles();
|
||||
_removeOverlay();
|
||||
if (result != null && result.files.isNotEmpty) {
|
||||
if (PlatformUtils().isWeb) {
|
||||
html.Node? inputElem;
|
||||
inputElem = html.document.getElementById("__file_picker_web-file-input")?.querySelector("input");
|
||||
fileName = result.files.single.name;
|
||||
|
||||
MessageUtils.handleMessageError(
|
||||
model.sendFileMessage(inputElement: inputElem, fileName: fileName, convID: convID, convType: convType),
|
||||
context);
|
||||
} else {
|
||||
File file = File(result.files.single.path!);
|
||||
final int size = file.lengthSync();
|
||||
final String savePath = file.path;
|
||||
|
||||
MessageUtils.handleMessageError(
|
||||
model.sendFileMessage(filePath: savePath, size: size, convID: convID, convType: convType), context);
|
||||
}
|
||||
} else {
|
||||
throw TypeError();
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore: avoid_print
|
||||
outputLogger.i("_sendFileErr: ${e.toString()}");
|
||||
}
|
||||
}
|
||||
|
||||
List<Widget> generateBarIcons(List<DesktopControlBarItem> items, TUITheme theme) {
|
||||
final defaultItems = defaultControlBarItems.map((e) => e.item);
|
||||
return items.map((e) {
|
||||
final GlobalKey key = GlobalKey();
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(right: 10),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
final alignBox = key.currentContext?.findRenderObject() as RenderBox?;
|
||||
var offset = alignBox?.localToGlobal(Offset.zero);
|
||||
final double? dx = (offset?.dx != null) ? offset!.dx : null;
|
||||
final double? dy = (offset?.dy != null && alignBox?.size.height != null)
|
||||
? offset!.dy - (widget.chatConfig.desktopStickerPanelHeight + 20)
|
||||
: null;
|
||||
e.onClick((dx != null && dy != null) ? Offset(dx, dy) : null);
|
||||
},
|
||||
child: Tooltip(
|
||||
preferBelow: false,
|
||||
textStyle: TextStyle(fontSize: 12, color: theme.white),
|
||||
message: e.showName,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(borderRadius: BorderRadius.circular(2)),
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: () {
|
||||
if (TencentUtils.checkString(e.svgPath) != null) {
|
||||
return SvgPicture.asset(
|
||||
e.svgPath!,
|
||||
package: defaultItems.contains(e.item) ? 'tencent_cloud_chat_uikit' : null,
|
||||
key: key,
|
||||
width: e.size ?? 16,
|
||||
height: e.size ?? 16,
|
||||
);
|
||||
}
|
||||
if (TencentUtils.checkString(e.imgPath) != null) {
|
||||
return Image.asset(
|
||||
e.imgPath!,
|
||||
package: defaultItems.contains(e.item) ? 'tencent_cloud_chat_uikit' : null,
|
||||
key: key,
|
||||
width: e.size ?? 16,
|
||||
height: e.size ?? 16,
|
||||
);
|
||||
}
|
||||
return Icon(
|
||||
e.icon,
|
||||
key: key,
|
||||
color: e.color ?? hexToColor("646a73"),
|
||||
size: e.size ?? 20,
|
||||
);
|
||||
}(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
_sendImageFileOnWeb(TUIChatSeparateViewModel model) async {
|
||||
try {
|
||||
final pickedFile = await _picker.pickImage(source: ImageSource.gallery);
|
||||
final imageContent = await pickedFile!.readAsBytes();
|
||||
fileName = pickedFile.name;
|
||||
tempFile = File(pickedFile.path);
|
||||
fileContent = imageContent;
|
||||
|
||||
html.Node? inputElem;
|
||||
inputElem = html.document.getElementById("__image_picker_web-file-input")?.querySelector("input");
|
||||
final convID = widget.conversationID;
|
||||
final convType = widget.conversationType;
|
||||
MessageUtils.handleMessageError(
|
||||
model.sendImageMessage(
|
||||
inputElement: inputElem, imagePath: tempFile?.path, convID: convID, convType: convType),
|
||||
context);
|
||||
} catch (e) {
|
||||
// ignore: avoid_print
|
||||
outputLogger.i("_sendFileErr: ${e.toString()}");
|
||||
}
|
||||
}
|
||||
|
||||
_sendVideoFileOnWeb(TUIChatSeparateViewModel model) async {
|
||||
try {
|
||||
final pickedFile = await _picker.pickVideo(source: ImageSource.gallery);
|
||||
final videoContent = await pickedFile!.readAsBytes();
|
||||
fileName = pickedFile.name;
|
||||
tempFile = File(pickedFile.path);
|
||||
fileContent = videoContent;
|
||||
|
||||
if (fileName!.split(".")[fileName!.split(".").length - 1] != "mp4") {
|
||||
onTIMCallback(
|
||||
TIMCallback(type: TIMCallbackType.INFO, infoRecommendText: TIM_t("视频消息仅限 mp4 格式"), infoCode: 6660412));
|
||||
return;
|
||||
}
|
||||
|
||||
html.Node? inputElem;
|
||||
inputElem = html.document.getElementById("__image_picker_web-file-input")?.querySelector("input");
|
||||
final convID = widget.conversationID;
|
||||
final convType = widget.conversationType;
|
||||
MessageUtils.handleMessageError(
|
||||
model.sendVideoMessage(
|
||||
inputElement: inputElem, videoPath: tempFile?.path, convID: convID, convType: convType),
|
||||
context);
|
||||
} catch (e) {
|
||||
// ignore: avoid_print
|
||||
outputLogger.i("_sendFileErr: ${e.toString()}");
|
||||
}
|
||||
}
|
||||
|
||||
_sendVideoMessage(AssetEntity asset, TUIChatSeparateViewModel model) async {
|
||||
try {
|
||||
final plugin = FcNativeVideoThumbnail();
|
||||
final originFile = await asset.originFile;
|
||||
final size = await originFile!.length();
|
||||
if (size >= 104857600) {
|
||||
onTIMCallback(
|
||||
TIMCallback(type: TIMCallbackType.INFO, infoRecommendText: TIM_t("发送失败,视频不能大于100MB"), infoCode: 6660405));
|
||||
return;
|
||||
}
|
||||
|
||||
final duration = asset.videoDuration.inSeconds;
|
||||
final filePath = originFile.path;
|
||||
final convID = widget.conversationID;
|
||||
final convType = widget.conversationType;
|
||||
|
||||
String tempPath = (await getTemporaryDirectory()).path + p.extension(originFile.path, 3) + ".jpeg";
|
||||
|
||||
await plugin.getVideoThumbnail(
|
||||
srcFile: originFile.path,
|
||||
destFile: tempPath,
|
||||
format: 'jpeg',
|
||||
width: 128,
|
||||
quality: 100,
|
||||
height: 128,
|
||||
);
|
||||
MessageUtils.handleMessageError(
|
||||
model.sendVideoMessage(
|
||||
videoPath: filePath, duration: duration, snapshotPath: tempPath, convID: convID, convType: convType),
|
||||
context);
|
||||
} catch (e) {
|
||||
onTIMCallback(TIMCallback(type: TIMCallbackType.INFO, infoRecommendText: TIM_t("视频文件异常"), infoCode: 6660415));
|
||||
}
|
||||
}
|
||||
|
||||
_sendMediaMessage(TUIChatSeparateViewModel model, TUITheme theme, FileType fileType) async {
|
||||
try {
|
||||
final convID = widget.conversationID;
|
||||
final convType = widget.conversationType;
|
||||
|
||||
if (PlatformUtils().isMobile) {
|
||||
final pickedAssets = await AssetPicker.pickAssets(context);
|
||||
|
||||
if (pickedAssets != null) {
|
||||
for (var asset in pickedAssets) {
|
||||
final originFile = await asset.originFile;
|
||||
final filePath = originFile?.path;
|
||||
final type = asset.type;
|
||||
if (filePath != null) {
|
||||
if (type == AssetType.image) {
|
||||
MessageUtils.handleMessageError(
|
||||
model.sendImageMessage(imagePath: filePath, convID: convID, convType: convType), context);
|
||||
}
|
||||
|
||||
if (type == AssetType.video) {
|
||||
_sendVideoMessage(asset, model);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
final plugin = FcNativeVideoThumbnail();
|
||||
_addGreyOverlay();
|
||||
FilePickerResult? result = await FilePicker.platform.pickFiles(type: fileType);
|
||||
_removeOverlay();
|
||||
if (result != null && result.files.isNotEmpty) {
|
||||
File file = File(result.files.single.path!);
|
||||
final String savePath = file.path;
|
||||
final String type =
|
||||
TencentUtils.getFileType((savePath.split(".")[savePath.split(".").length - 1]).toLowerCase())
|
||||
.split("/")[0];
|
||||
|
||||
if (type == "image") {
|
||||
MessageUtils.handleMessageError(
|
||||
model.sendImageMessage(imagePath: savePath, convID: convID, convType: convType), context);
|
||||
} else if (type == "video") {
|
||||
String tempPath = (await getTemporaryDirectory()).path + p.basename(savePath) + ".jpeg";
|
||||
await plugin.getVideoThumbnail(
|
||||
srcFile: savePath,
|
||||
destFile: tempPath,
|
||||
format: 'jpeg',
|
||||
width: 128,
|
||||
quality: 100,
|
||||
height: 128,
|
||||
);
|
||||
MessageUtils.handleMessageError(
|
||||
model.sendVideoMessage(videoPath: savePath, convID: convID, convType: convType, snapshotPath: tempPath),
|
||||
context);
|
||||
}
|
||||
} else {
|
||||
throw TypeError();
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// ignore: avoid_print
|
||||
outputLogger.i("send media err: $err");
|
||||
onTIMCallback(TIMCallback(type: TIMCallbackType.INFO, infoRecommendText: TIM_t("视频文件异常"), infoCode: 6660415));
|
||||
}
|
||||
}
|
||||
|
||||
_sendImageWithConfirmation({String? fileName, Size? fileSize, required String filePath}) async {
|
||||
final option1 =
|
||||
widget.currentConversation.showName ?? (widget.conversationType == ConvType.group ? TIM_t("群聊") : TIM_t("对方"));
|
||||
final size = fileSize ?? await ScreenshotHelper.getImageSize(filePath);
|
||||
|
||||
TUIKitWidePopup.showPopupWindow(
|
||||
operationKey: TUIKitWideModalOperationKey.beforeSendScreenShot,
|
||||
context: context,
|
||||
isDarkBackground: false,
|
||||
width: 500,
|
||||
height: min(500, size.height / 2 + 140),
|
||||
title: TIM_t_para("发送给{{option1}}", "发送给$option1")(option1: option1),
|
||||
child: (closeFunc) => Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: min(360, size.height / 2),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
launchUrl(PlatformUtils().isWeb ? Uri.parse(filePath) : Uri.file(filePath));
|
||||
},
|
||||
child: PlatformUtils().isWeb
|
||||
? Image.network(
|
||||
filePath,
|
||||
height: min(360, size.height / 2),
|
||||
)
|
||||
: Image.file(
|
||||
File(filePath),
|
||||
height: min(360, size.height / 2),
|
||||
),
|
||||
),
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
OutlinedButton(
|
||||
onPressed: () {
|
||||
closeFunc();
|
||||
},
|
||||
child: Text(TIM_t("取消"))),
|
||||
const SizedBox(
|
||||
width: 20,
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
MessageUtils.handleMessageError(
|
||||
widget.model.sendImageMessage(
|
||||
imagePath: filePath,
|
||||
imageName: fileName,
|
||||
convID: widget.conversationID,
|
||||
convType: widget.conversationType),
|
||||
context);
|
||||
closeFunc();
|
||||
},
|
||||
child: Text(TIM_t("发送")))
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_sendScreenShot() async {
|
||||
final file = await ScreenshotHelper.captureScreen();
|
||||
if (file != null) {
|
||||
_sendImageWithConfirmation(filePath: file);
|
||||
} else {}
|
||||
}
|
||||
|
||||
generateDefaultControlBarItems() {
|
||||
final DesktopControlBarConfig config = widget.chatConfig.desktopControlBarConfig ?? DesktopControlBarConfig();
|
||||
final List<DesktopControlBarItem> itemsList = [
|
||||
if (config.showStickerPanel)
|
||||
DesktopControlBarItem(
|
||||
item: "face",
|
||||
showName: TIM_t("表情"),
|
||||
onClick: (offset) {
|
||||
_sendEmoji(offset, widget.theme);
|
||||
},
|
||||
imgPath: "images/svg/send_face.png"),
|
||||
if (config.showScreenshotButton && PlatformUtils().isDesktop)
|
||||
DesktopControlBarItem(
|
||||
item: "screenShot",
|
||||
showName: TIM_t("截图"),
|
||||
onClick: (offset) {
|
||||
_sendScreenShot();
|
||||
},
|
||||
svgPath: "images/svg/send_screenshot.svg"),
|
||||
if (config.showSendFileButton)
|
||||
DesktopControlBarItem(
|
||||
item: "file",
|
||||
showName: TIM_t("文件"),
|
||||
onClick: (offset) {
|
||||
_sendFile(widget.model, widget.theme);
|
||||
},
|
||||
svgPath: "images/svg/send_file.svg"),
|
||||
if (config.showSendImageButton)
|
||||
DesktopControlBarItem(
|
||||
item: "photo",
|
||||
showName: TIM_t("图片"),
|
||||
onClick: (offset) {
|
||||
if (PlatformUtils().isWeb) {
|
||||
_sendImageFileOnWeb(widget.model);
|
||||
} else {
|
||||
_sendMediaMessage(widget.model, widget.theme, FileType.image);
|
||||
}
|
||||
},
|
||||
svgPath: "images/svg/send_image.svg"),
|
||||
if (config.showSendVideoButton)
|
||||
DesktopControlBarItem(
|
||||
item: "video",
|
||||
showName: TIM_t("视频"),
|
||||
onClick: (offset) {
|
||||
if (PlatformUtils().isWeb) {
|
||||
_sendVideoFileOnWeb(widget.model);
|
||||
} else {
|
||||
_sendMediaMessage(widget.model, widget.theme, FileType.video);
|
||||
}
|
||||
},
|
||||
svgPath: "images/svg/send_video.svg"),
|
||||
if (config.showMessageHistoryButton)
|
||||
DesktopControlBarItem(
|
||||
item: "history",
|
||||
showName: TIM_t("消息历史"),
|
||||
onClick: (offset) {
|
||||
TUIKitWidePopup.showPopupWindow(
|
||||
operationKey: TUIKitWideModalOperationKey.chatHistory,
|
||||
context: context,
|
||||
width: MediaQuery.of(context).size.width * 0.5,
|
||||
height: MediaQuery.of(context).size.width * 0.5,
|
||||
child: (onClose) => TIMUIKitSearchMsgDetail(
|
||||
currentConversation: widget.currentConversation,
|
||||
keyword: '',
|
||||
initMessageList: widget.model
|
||||
.getOriginMessageList()
|
||||
.getRange(0, min(widget.model.getOriginMessageList().length, 100))
|
||||
.toList(),
|
||||
onTapConversation: (V2TimConversation conversation, V2TimMessage? message) {},
|
||||
),
|
||||
theme: widget.theme);
|
||||
},
|
||||
svgPath: "images/svg/message_history.svg"),
|
||||
];
|
||||
defaultControlBarItems = itemsList;
|
||||
}
|
||||
|
||||
List<Widget> generateControlBar(TUIChatSeparateViewModel model, TUITheme theme) {
|
||||
final List<DesktopControlBarItem> itemsList = [
|
||||
...defaultControlBarItems,
|
||||
...(widget.chatConfig.additionalDesktopControlBarItems ?? [])
|
||||
];
|
||||
|
||||
return generateBarIcons(itemsList, theme);
|
||||
}
|
||||
|
||||
sendFileUseJs(html.File file) {
|
||||
final mimeType = file.type.split('/');
|
||||
final type = mimeType[0];
|
||||
final blobUrl = html.Url.createObjectUrl(file);
|
||||
if (type == 'image') {
|
||||
_sendImageWithConfirmation(filePath: blobUrl, fileName: file.name, fileSize: const Size(500, 500));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _handleKeyEvent(RawKeyEvent event) async {
|
||||
if (PlatformUtils().isDesktop &&
|
||||
((event.isKeyPressed(LogicalKeyboardKey.controlLeft) && event.logicalKey == LogicalKeyboardKey.keyV) ||
|
||||
(event.isMetaPressed && event.logicalKey == LogicalKeyboardKey.keyV))) {
|
||||
final bytes = await Pasteboard.image;
|
||||
if (bytes != null) {
|
||||
String directory;
|
||||
if (PlatformUtils().isWindows) {
|
||||
final String documentsDirectoryPath = "${Platform.environment['USERPROFILE']}";
|
||||
PackageInfo packageInfo = await PackageInfo.fromPlatform();
|
||||
String pkgName = packageInfo.packageName;
|
||||
directory = p.join(documentsDirectoryPath, "Documents", ".TencentCloudChat", pkgName, "screenshots");
|
||||
} else {
|
||||
final dic = await getApplicationSupportDirectory();
|
||||
directory = dic.path;
|
||||
}
|
||||
const uuid = Uuid();
|
||||
final fileName = 'paste_image_${uuid.v4()}.png';
|
||||
final scDirectory = Directory(directory);
|
||||
final filePath = '${scDirectory.path}${PlatformUtils().isWindows ? "\\" : "/"}$fileName';
|
||||
final file = File(filePath);
|
||||
if (!await scDirectory.exists()) {
|
||||
await scDirectory.create(recursive: true);
|
||||
}
|
||||
await file.writeAsBytes(bytes.toList());
|
||||
_sendImageWithConfirmation(filePath: filePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final theme = value.theme;
|
||||
|
||||
setKeyboardHeight ??= OptimizeUtils.debounce((height) {
|
||||
settingModel.keyboardHeight = height;
|
||||
}, const Duration(seconds: 1));
|
||||
|
||||
final debounceFunc = _debounce((value) {
|
||||
if (widget.onChanged != null) {
|
||||
widget.onChanged!(value);
|
||||
}
|
||||
widget.handleAtText(value);
|
||||
widget.handleSendEditStatus(value, true);
|
||||
}, const Duration(milliseconds: 80));
|
||||
|
||||
final MediaQueryData data = MediaQuery.of(context);
|
||||
EdgeInsets padding = data.padding;
|
||||
if (bottomPadding == null || padding.bottom > bottomPadding!) {
|
||||
bottomPadding = padding.bottom;
|
||||
}
|
||||
|
||||
return RawKeyboardListener(
|
||||
focusNode: textFocusNode,
|
||||
onKey: _handleKeyEvent,
|
||||
child: Container(
|
||||
color: widget.backgroundColor ?? theme.desktopChatMessageInputBgColor,
|
||||
child: Column(
|
||||
children: [
|
||||
_buildRepliedMessage(widget.repliedMessage),
|
||||
SizedBox(height: 1, child: Container(color: theme.weakDividerColor ?? Colors.black12)),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 12),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: generateControlBar(widget.model, theme),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 6),
|
||||
constraints: const BoxConstraints(minHeight: 50),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ExtendedTextField(
|
||||
scrollController: _scrollController,
|
||||
autofocus: true,
|
||||
maxLines: widget.chatConfig.desktopMessageInputFieldLines,
|
||||
minLines: widget.chatConfig.desktopMessageInputFieldLines,
|
||||
focusNode: widget.focusNode,
|
||||
onChanged: debounceFunc,
|
||||
keyboardType: TextInputType.multiline,
|
||||
onEditingComplete: () {
|
||||
// // widget.onSubmitted();
|
||||
},
|
||||
textAlignVertical: TextAlignVertical.top,
|
||||
style: const TextStyle(fontSize: 14),
|
||||
decoration: InputDecoration(
|
||||
hoverColor: Colors.transparent,
|
||||
border: InputBorder.none,
|
||||
hintStyle: const TextStyle(
|
||||
color: Color(0xffAEA4A3),
|
||||
),
|
||||
fillColor:
|
||||
widget.backgroundColor ?? theme.desktopChatMessageInputBgColor ?? hexToColor("fafafa"),
|
||||
filled: true,
|
||||
isDense: true,
|
||||
hintText: widget.hintText ?? '',
|
||||
),
|
||||
controller: widget.textEditingController,
|
||||
specialTextSpanBuilder: PlatformUtils().isWeb
|
||||
? null
|
||||
: DefaultSpecialTextSpanBuilder(
|
||||
isUseTencentCloudChatPackage:
|
||||
widget.model.chatConfig.stickerPanelConfig?.useTencentCloudChatStickerPackage ??
|
||||
true,
|
||||
customEmojiStickerList: widget.customEmojiStickerList,
|
||||
showAtBackground: true,
|
||||
)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,703 @@
|
||||
// ignore_for_file: must_be_immutable, avoid_print
|
||||
|
||||
import 'package:desktop_drop/desktop_drop.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:provider/single_child_widget.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_group_application.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_group_application.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_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_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/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/business_logic/life_cycle/chat_life_cycle.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/listener_model/tui_group_listener_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_chat_separate_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_chat_global_model.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_self_info_view_model.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/constants/history_message_constant.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/controller/tim_uikit_chat_controller.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/frame.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/logger.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/platform.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitTextField/at_member_panel.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/tim_uikit_multi_select_panel.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/tim_uikit_send_file.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/theme/tui_theme_view_model.dart';
|
||||
import 'TIMUIKItMessageList/TIMUIKitTongue/tim_uikit_chat_history_message_list_tongue.dart';
|
||||
import 'TIMUIKItMessageList/tim_uikit_chat_history_message_list_config.dart';
|
||||
import 'TIMUIKItMessageList/tim_uikit_history_message_list_container.dart';
|
||||
|
||||
class TIMUIKitChat extends StatefulWidget {
|
||||
int startTime = 0;
|
||||
int endTime = 0;
|
||||
|
||||
/// The chat controller you tend to used.
|
||||
/// You have to provide this before using it since tencent_cloud_chat_uikit 0.1.4.
|
||||
final TIMUIKitChatController? controller;
|
||||
|
||||
/// [Update] It is suggested to provide the `V2TimConversation` once directly, since tencent_cloud_chat_uikit 1.5.0.
|
||||
/// `conversationID` / `conversationType` / `groupAtInfoList` / `conversationShowName` are not necessary to be provided, unless you want to cover these fields manually.
|
||||
final V2TimConversation conversation;
|
||||
|
||||
/// The ID of the Group that the topic belongs to, only need for topic.
|
||||
final String? groupID;
|
||||
|
||||
/// Conversation id, use for load history message list.
|
||||
/// This field is not necessary to be provided, when `conversation` is provided, unless you want to cover this field manually.
|
||||
final String? conversationID;
|
||||
|
||||
/// Conversation type.
|
||||
/// This field is not necessary to be provided, when `conversation` is provided, unless you want to cover this field manually.
|
||||
final ConvType? conversationType;
|
||||
|
||||
/// use for customize avatar
|
||||
final Widget Function(BuildContext context, V2TimMessage message)? userAvatarBuilder;
|
||||
|
||||
/// Use for show conversation name.
|
||||
/// This field is not necessary to be provided, when `conversation` is provided, unless you want to cover this field manually.
|
||||
final String? conversationShowName;
|
||||
|
||||
/// Avatar and name in message reaction tap callback.
|
||||
final void Function(String userID, TapDownDetails tapDetails)? onTapAvatar;
|
||||
|
||||
/// Avatar and name in message reaction secondary tap callback.
|
||||
final void Function(String userID, TapDownDetails tapDetails)? onSecondaryTapAvatar;
|
||||
|
||||
@Deprecated(
|
||||
"Nickname will not shows in one-to-one chat, if you tend to control it in group chat, please use `isShowSelfNameInGroup` and `isShowOthersNameInGroup` from `config: TIMUIKitChatConfig` instead")
|
||||
|
||||
/// Should show the nick name.
|
||||
final bool showNickName;
|
||||
|
||||
/// Message item builder, can customize partial message item for different types or the layout for the whole line.
|
||||
final MessageItemBuilder? messageItemBuilder;
|
||||
|
||||
/// Is show unread message count, default value is false
|
||||
final bool showTotalUnReadCount;
|
||||
|
||||
/// Deprecated("Please use [extraTipsActionItemBuilder] instead")
|
||||
final Widget? Function(V2TimMessage message, Function() closeTooltip, [Key? key, BuildContext? context])?
|
||||
exteraTipsActionItemBuilder;
|
||||
|
||||
/// The builder for extra tips action.
|
||||
final Widget? Function(V2TimMessage message, Function() closeTooltip, [Key? key, BuildContext? context])?
|
||||
extraTipsActionItemBuilder;
|
||||
|
||||
/// The text of draft shows in TextField.
|
||||
/// [Recommend]: You can specify this field with the draftText from V2TimConversation.
|
||||
final String? draftText;
|
||||
|
||||
/// The target message been jumped just after entering the chat page.
|
||||
final V2TimMessage? initFindingMsg;
|
||||
|
||||
/// The hint text shows at input field.
|
||||
final String? textFieldHintText;
|
||||
|
||||
/// The configuration for appbar.
|
||||
final AppBar? appBarConfig;
|
||||
|
||||
/// The configuration for historical message list.
|
||||
final TIMUIKitHistoryMessageListConfig? mainHistoryListConfig;
|
||||
|
||||
/// The configuration for more panel, can customize actions.
|
||||
final MorePanelConfig? morePanelConfig;
|
||||
|
||||
/// The builder for the tongue on the right bottom.
|
||||
/// Used for back to bottom, shows the count of unread new messages,
|
||||
/// and prompts the messages that @ user.
|
||||
final TongueItemBuilder? tongueItemBuilder;
|
||||
|
||||
/// The `groupAtInfoList` from `V2TimConversation`.
|
||||
/// This field is not necessary to be provided, when `conversation` is provided,
|
||||
/// unless you want to cover this field manually.
|
||||
final List<V2TimGroupAtInfo?>? groupAtInfoList;
|
||||
|
||||
/// The configuration for the whole `TIMUIKitChat` widget.
|
||||
final TIMUIKitChatConfig? config;
|
||||
|
||||
/// The callback for jumping to the page for `TIMUIKitGroupApplicationList`
|
||||
/// or other pages to deal with enter group application for group administrator manually,
|
||||
/// in the case of [public group].
|
||||
/// The parameter here is `String groupID`
|
||||
final ValueChanged<String>? onDealWithGroupApplication;
|
||||
|
||||
/// The generator for the abstract summary preview of a message,
|
||||
/// typically used in replied and forwarded messages.
|
||||
/// Returns `null` to use the default message summary.
|
||||
final String? Function(V2TimMessage message)? abstractMessageBuilder;
|
||||
|
||||
/// The configuration for tool tips panel, long press messages will show this panel.
|
||||
final ToolTipsConfig? toolTipsConfig;
|
||||
|
||||
/// The life cycle for chat business logic.
|
||||
final ChatLifeCycle? lifeCycle;
|
||||
|
||||
/// The top fixed widget.
|
||||
final Widget? topFixWidget;
|
||||
|
||||
/// Specify the custom small png emoji packages.
|
||||
final List<CustomEmojiFaceData> customEmojiStickerList;
|
||||
|
||||
final Widget? customAppBar;
|
||||
|
||||
final Widget? inputTopBuilder;
|
||||
|
||||
/// Custom emoji panel.
|
||||
final CustomStickerPanel? customStickerPanel;
|
||||
|
||||
/// This parameter accepts a custom widget to be displayed when the mouse hovers over a message,
|
||||
/// replacing the default message hover action bar.
|
||||
/// Applicable only on desktop platforms.
|
||||
/// If provided, the default message action functionality will appear in the right-click context menu instead.
|
||||
/// Returns `null` to use default hover bar.
|
||||
final Widget? Function(V2TimMessage message)? customMessageHoverBarOnDesktop;
|
||||
|
||||
/// Custom text field
|
||||
final Widget Function(BuildContext context)? textFieldBuilder;
|
||||
|
||||
/// An optional parameter `groupMemberList` can be provided.
|
||||
/// `groupMemberList` accepts a list of nullable `V2TimGroupMemberFullInfo` objects.
|
||||
/// The purpose of this parameter is to allow the client to supply a pre-fetched list
|
||||
/// of group member information. If this list is provided, it will not make
|
||||
/// additional network requests to fetch the group member information internally.
|
||||
List<V2TimGroupMemberFullInfo?>? groupMemberList;
|
||||
|
||||
TIMUIKitChat(
|
||||
{Key? key,
|
||||
this.groupID,
|
||||
required this.conversation,
|
||||
this.conversationID,
|
||||
this.conversationType,
|
||||
this.groupMemberList,
|
||||
this.conversationShowName,
|
||||
this.abstractMessageBuilder,
|
||||
this.onTapAvatar,
|
||||
@Deprecated(
|
||||
"Nickname will not show in one-to-one chat, if you tend to control it in group chat, please use `isShowSelfNameInGroup` and `isShowOthersNameInGroup` from `config: TIMUIKitChatConfig` instead")
|
||||
this.showNickName = false,
|
||||
this.showTotalUnReadCount = false,
|
||||
this.messageItemBuilder,
|
||||
@Deprecated("Please use [extraTipsActionItemBuilder] instead") this.exteraTipsActionItemBuilder,
|
||||
this.extraTipsActionItemBuilder,
|
||||
this.draftText,
|
||||
this.textFieldHintText,
|
||||
this.initFindingMsg,
|
||||
this.userAvatarBuilder,
|
||||
this.appBarConfig,
|
||||
this.controller,
|
||||
this.morePanelConfig,
|
||||
this.customStickerPanel,
|
||||
this.config = const TIMUIKitChatConfig(),
|
||||
this.tongueItemBuilder,
|
||||
this.groupAtInfoList,
|
||||
this.mainHistoryListConfig,
|
||||
this.onDealWithGroupApplication,
|
||||
this.toolTipsConfig,
|
||||
this.lifeCycle,
|
||||
this.topFixWidget = const SizedBox(),
|
||||
this.textFieldBuilder,
|
||||
this.customEmojiStickerList = const [],
|
||||
this.customAppBar,
|
||||
this.inputTopBuilder,
|
||||
this.onSecondaryTapAvatar,
|
||||
this.customMessageHoverBarOnDesktop})
|
||||
: super(key: key) {
|
||||
startTime = DateTime.now().millisecondsSinceEpoch;
|
||||
}
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _TUIChatState();
|
||||
}
|
||||
|
||||
class _TUIChatState extends TIMUIKitState<TIMUIKitChat> {
|
||||
TUIChatSeparateViewModel model = TUIChatSeparateViewModel();
|
||||
final TUISelfInfoViewModel selfInfoViewModel = serviceLocator<TUISelfInfoViewModel>();
|
||||
final TUIThemeViewModel themeViewModel = serviceLocator<TUIThemeViewModel>();
|
||||
final TUIConversationViewModel conversationViewModel = serviceLocator<TUIConversationViewModel>();
|
||||
TIMUIKitInputTextFieldController textFieldController = TIMUIKitInputTextFieldController();
|
||||
bool isInit = false;
|
||||
final TUIChatGlobalModel chatGlobalModel = serviceLocator<TUIChatGlobalModel>();
|
||||
bool _dragging = false;
|
||||
|
||||
final GlobalKey alignKey = GlobalKey();
|
||||
final GlobalKey listContainerKey = GlobalKey();
|
||||
|
||||
late AutoScrollController autoController = AutoScrollController(
|
||||
viewportBoundaryGetter: () => Rect.fromLTRB(0, 0, 0, MediaQuery.of(context).padding.bottom),
|
||||
axis: Axis.vertical,
|
||||
);
|
||||
|
||||
late AutoScrollController atMemberPanelScroll = AutoScrollController(
|
||||
viewportBoundaryGetter: () => Rect.fromLTRB(0, 0, 0, MediaQuery.of(context).padding.bottom),
|
||||
axis: Axis.vertical,
|
||||
);
|
||||
|
||||
Widget? _joinInGroupCallWidget;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (kProfileMode) {
|
||||
Frame.init();
|
||||
}
|
||||
model.abstractMessageBuilder = widget.abstractMessageBuilder;
|
||||
model.onTapAvatar = widget.onTapAvatar;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
widget.endTime = DateTime.now().millisecondsSinceEpoch;
|
||||
int timeSpend = widget.endTime - widget.startTime;
|
||||
outputLogger.i("Page render time:$timeSpend ms");
|
||||
});
|
||||
Future.delayed(const Duration(milliseconds: 500), () {
|
||||
updateDraft();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
if (kProfileMode) {
|
||||
Frame.destroy();
|
||||
}
|
||||
model.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(TIMUIKitChat oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.conversationID != oldWidget.conversationID) {
|
||||
isInit = false;
|
||||
chatGlobalModel.clearCurrentConversation();
|
||||
model = TUIChatSeparateViewModel();
|
||||
model.abstractMessageBuilder = widget.abstractMessageBuilder;
|
||||
model.onTapAvatar = widget.onTapAvatar;
|
||||
Future.delayed(const Duration(milliseconds: 50), () {
|
||||
updateDraft();
|
||||
textFieldController.requestFocus();
|
||||
try {
|
||||
autoController.jumpTo(
|
||||
autoController.position.minScrollExtent,
|
||||
);
|
||||
autoController.jumpTo(
|
||||
autoController.position.minScrollExtent,
|
||||
);
|
||||
// ignore: empty_catches
|
||||
} catch (e) {}
|
||||
});
|
||||
}
|
||||
if (oldWidget.textFieldBuilder != null && widget.textFieldBuilder == null) {
|
||||
textFieldController = TIMUIKitInputTextFieldController();
|
||||
}
|
||||
if (oldWidget.groupMemberList != widget.groupMemberList) {
|
||||
model.groupMemberList = widget.groupMemberList;
|
||||
}
|
||||
}
|
||||
|
||||
updateDraft() async {
|
||||
final isTopic = widget.conversation.conversationID.contains("@TOPIC#");
|
||||
if (isTopic) {
|
||||
final topicInfoList = await TencentImSDKPlugin.v2TIMManager
|
||||
.getGroupManager()
|
||||
.getTopicInfoList(groupID: widget.groupID!, topicIDList: [widget.conversation.conversationID]);
|
||||
final topicInfo = topicInfoList.data?.first.topicInfo;
|
||||
final draftText = topicInfo?.draftText;
|
||||
if (TencentUtils.checkString(draftText) != null) {
|
||||
textFieldController.setTextField(draftText!);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Widget _renderJoinGroupApplication(int amount, TUITheme theme) {
|
||||
String option1 = amount.toString();
|
||||
return Container(
|
||||
height: 36,
|
||||
decoration: BoxDecoration(color: hexToColor("f6eabc")),
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
if (widget.onDealWithGroupApplication != null) {
|
||||
widget.onDealWithGroupApplication!(_getConvID());
|
||||
}
|
||||
},
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
TIM_t_para("{{option1}} 条入群请求", "$option1 条入群请求")(option1: option1),
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
margin: const EdgeInsets.only(left: 12),
|
||||
child: Text(
|
||||
TIM_t("去处理"),
|
||||
style: TextStyle(fontSize: 12, color: theme.primaryColor),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _getTitle() {
|
||||
return TencentUtils.checkString(widget.conversationShowName) ?? widget.conversation.showName ?? "Chat";
|
||||
}
|
||||
|
||||
String _getConvID() {
|
||||
return TencentUtils.checkString(widget.conversationID) ??
|
||||
(widget.conversation.type == 1 ? widget.conversation.userID : widget.conversation.groupID) ??
|
||||
"";
|
||||
}
|
||||
|
||||
ConvType _getConvType() {
|
||||
return widget.conversation.type == 1 ? ConvType.c2c : ConvType.group;
|
||||
}
|
||||
|
||||
_updateJoinInGroupCallWidget() async {
|
||||
if (_getConvType() != ConvType.group) {
|
||||
return;
|
||||
}
|
||||
final w = await TUICore.instance.raiseExtension(TUIExtensionID.joinInGroup, {GROUP_ID: widget.conversationID!});
|
||||
if (w != _joinInGroupCallWidget) {
|
||||
setState(() {
|
||||
_joinInGroupCallWidget = w;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final TUITheme theme = value.theme;
|
||||
final isBuild = isInit;
|
||||
isInit = true;
|
||||
_updateJoinInGroupCallWidget();
|
||||
|
||||
return TIMUIKitChatProviderScope(
|
||||
model: model,
|
||||
groupID: widget.groupID,
|
||||
scrollController: autoController,
|
||||
textFieldController: textFieldController,
|
||||
conversationID: _getConvID(),
|
||||
groupMemberList: widget.groupMemberList,
|
||||
conversationType: _getConvType(),
|
||||
lifeCycle: widget.lifeCycle,
|
||||
config: widget.config,
|
||||
isBuild: isBuild,
|
||||
providers: [
|
||||
Provider(create: (_) => widget.config),
|
||||
],
|
||||
builder: (context, model, w) {
|
||||
final TUIChatGlobalModel chatGlobalModel = Provider.of<TUIChatGlobalModel>(context, listen: true);
|
||||
|
||||
widget.controller?.model = model;
|
||||
widget.controller?.textFieldController = textFieldController;
|
||||
widget.controller?.scrollController = autoController;
|
||||
List<V2TimGroupApplication> filteredApplicationList = [];
|
||||
if (widget.conversationType == ConvType.group && widget.onDealWithGroupApplication != null) {
|
||||
filteredApplicationList = chatGlobalModel.groupApplicationList.where((item) {
|
||||
return (item.groupID == widget.conversationID) && item.handleStatus == 0;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
final selfUserID = selfInfoViewModel.loginInfo?.userID;
|
||||
final TUIGroupListenerModel groupListenerModel = Provider.of<TUIGroupListenerModel>(context, listen: true);
|
||||
final NeedUpdate? needUpdate = groupListenerModel.needUpdate;
|
||||
if (needUpdate != null && needUpdate.groupID == widget.conversationID) {
|
||||
groupListenerModel.needUpdate = null;
|
||||
switch (needUpdate.updateType) {
|
||||
case UpdateType.groupInfo:
|
||||
model.loadGroupInfo(_getConvID());
|
||||
break;
|
||||
case UpdateType.memberList:
|
||||
if (widget.groupMemberList == null) {
|
||||
model.loadGroupMemberList(groupID: _getConvID());
|
||||
}
|
||||
model.loadGroupInfo(_getConvID());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
List<CustomEmojiFaceData> customImageSmallPngEmojiPackages = [];
|
||||
if (widget.config?.stickerPanelConfig?.customStickerPackages != null &&
|
||||
widget.config!.stickerPanelConfig!.customStickerPackages.isNotEmpty) {
|
||||
customImageSmallPngEmojiPackages = widget.config!.stickerPanelConfig!.customStickerPackages
|
||||
.where((element) => element.isEmoji == true)
|
||||
.map((e) {
|
||||
return CustomEmojiFaceData(
|
||||
name: e.name,
|
||||
isEmoji: true,
|
||||
icon: e.menuItem.url ?? "",
|
||||
list: e.stickerList.map((e) => e.url ?? "").toList());
|
||||
}).toList();
|
||||
}
|
||||
if (customImageSmallPngEmojiPackages.isEmpty) {
|
||||
customImageSmallPngEmojiPackages.addAll(widget.customEmojiStickerList);
|
||||
}
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
textFieldController.hideAllPanel();
|
||||
},
|
||||
child: Scaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
appBar: (widget.customAppBar == null)
|
||||
? TIMUIKitAppBar(
|
||||
showTotalUnReadCount: widget.showTotalUnReadCount,
|
||||
config: widget.appBarConfig,
|
||||
conversationShowName: _getTitle(),
|
||||
conversationID: _getConvID(),
|
||||
showC2cMessageEditStatus: widget.config?.showC2cMessageEditStatus ?? true,
|
||||
)
|
||||
: null,
|
||||
body: DropTarget(
|
||||
onDragDone: (detail) {
|
||||
setState(() {
|
||||
_dragging = false;
|
||||
sendFileWithConfirmation(
|
||||
files: detail.files,
|
||||
conversation: widget.conversation,
|
||||
conversationType: _getConvType(),
|
||||
model: model,
|
||||
theme: theme,
|
||||
context: context);
|
||||
});
|
||||
},
|
||||
onDragEntered: (detail) {
|
||||
setState(() {
|
||||
_dragging = true;
|
||||
});
|
||||
},
|
||||
onDragExited: (detail) {
|
||||
setState(() {
|
||||
_dragging = false;
|
||||
});
|
||||
},
|
||||
child: Stack(
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (widget.customAppBar != null) widget.customAppBar!,
|
||||
if (filteredApplicationList.isNotEmpty)
|
||||
_renderJoinGroupApplication(filteredApplicationList.length, theme),
|
||||
if (widget.topFixWidget != null) widget.topFixWidget!,
|
||||
if (_joinInGroupCallWidget != null) Center(child: _joinInGroupCallWidget!),
|
||||
Expanded(
|
||||
child: Container(
|
||||
color: theme.chatBgColor,
|
||||
child: Align(
|
||||
key: alignKey,
|
||||
alignment: Alignment.topCenter,
|
||||
child: Listener(
|
||||
child: TIMUIKitHistoryMessageListContainer(
|
||||
customMessageHoverBarOnDesktop: widget.customMessageHoverBarOnDesktop,
|
||||
conversation: widget.conversation,
|
||||
groupMemberInfo: model.groupMemberList
|
||||
?.firstWhere((element) => element?.userID == selfUserID, orElse: () => null),
|
||||
textFieldController: textFieldController,
|
||||
customEmojiStickerList: widget.customEmojiStickerList,
|
||||
key: listContainerKey,
|
||||
isAllowScroll: true,
|
||||
userAvatarBuilder: widget.userAvatarBuilder,
|
||||
toolTipsConfig: widget.toolTipsConfig,
|
||||
groupAtInfoList: widget.groupAtInfoList,
|
||||
tongueItemBuilder: widget.tongueItemBuilder,
|
||||
onLongPressForOthersHeadPortrait: (String? userId, String? nickName) {
|
||||
textFieldController.longPressToAt(nickName, userId);
|
||||
},
|
||||
mainHistoryListConfig: widget.mainHistoryListConfig,
|
||||
initFindingMsg: widget.initFindingMsg,
|
||||
extraTipsActionItemBuilder:
|
||||
widget.extraTipsActionItemBuilder ?? widget.exteraTipsActionItemBuilder,
|
||||
conversationType: _getConvType(),
|
||||
scrollController: autoController,
|
||||
onSecondaryTapAvatar: widget.onSecondaryTapAvatar,
|
||||
onTapAvatar: widget.onTapAvatar,
|
||||
// ignore: deprecated_member_use_from_same_package
|
||||
showNickName: widget.showNickName,
|
||||
messageItemBuilder: widget.messageItemBuilder,
|
||||
conversationID: _getConvID(),
|
||||
),
|
||||
)),
|
||||
)),
|
||||
widget.inputTopBuilder ?? Container(),
|
||||
Selector<TUIChatSeparateViewModel, bool>(
|
||||
builder: (context, value, child) {
|
||||
return value
|
||||
? MultiSelectPanel(
|
||||
conversationType: _getConvType(),
|
||||
)
|
||||
: (widget.textFieldBuilder != null
|
||||
? widget.textFieldBuilder!(context)
|
||||
: TIMUIKitInputTextField(
|
||||
chatConfig: widget.config,
|
||||
groupID: widget.groupID,
|
||||
atMemberPanelScroll: atMemberPanelScroll,
|
||||
groupType: widget.conversation.groupType,
|
||||
currentConversation: widget.conversation,
|
||||
model: model,
|
||||
controller: textFieldController,
|
||||
customEmojiStickerList: customImageSmallPngEmojiPackages,
|
||||
customStickerPanel: widget.customStickerPanel,
|
||||
morePanelConfig: widget.morePanelConfig,
|
||||
scrollController: autoController,
|
||||
conversationID: _getConvID(),
|
||||
conversationType: _getConvType(),
|
||||
initText: TencentUtils.checkString(widget.draftText) ??
|
||||
(PlatformUtils().isWeb
|
||||
? TencentUtils.checkString(conversationViewModel.getWebDraft(
|
||||
conversationID: widget.conversation.conversationID))
|
||||
: TencentUtils.checkString(widget.conversation.draftText)),
|
||||
hintText: widget.textFieldHintText,
|
||||
showMorePanel: widget.config?.isAllowShowMorePanel ?? true,
|
||||
showSendAudio: widget.config?.isAllowSoundMessage ?? true,
|
||||
showSendEmoji: widget.config?.isAllowEmojiPanel ?? true,
|
||||
));
|
||||
},
|
||||
selector: (c, model) {
|
||||
return model.isMultiSelect;
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
if (_dragging)
|
||||
TIMUIKitSendFile(
|
||||
conversation: widget.conversation,
|
||||
),
|
||||
AtMemberPanel(
|
||||
atMemberPanelScroll: atMemberPanelScroll,
|
||||
onSelectMember: (member) => textFieldController.handleAtMember(member),
|
||||
)
|
||||
],
|
||||
),
|
||||
)),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class TIMUIKitChatProviderScope extends StatelessWidget {
|
||||
final TUIChatGlobalModel globalModel = serviceLocator<TUIChatGlobalModel>();
|
||||
TUIChatSeparateViewModel? model;
|
||||
final TUIGroupListenerModel groupListenerModel = serviceLocator<TUIGroupListenerModel>();
|
||||
final TUIThemeViewModel themeViewModel = serviceLocator<TUIThemeViewModel>();
|
||||
final Widget? child;
|
||||
|
||||
/// You could get the model from here, and transfer it to other widget from TUIKit.
|
||||
final Widget Function(BuildContext, TUIChatSeparateViewModel, Widget?) builder;
|
||||
final List<SingleChildWidget>? providers;
|
||||
|
||||
/// `TIMUIKitChatController` needs to be provided if you use it outside.
|
||||
final TIMUIKitChatController? controller;
|
||||
|
||||
/// The global config for TIMUIKitChat.
|
||||
final TIMUIKitChatConfig? config;
|
||||
|
||||
/// Conversation id, use for get history message list.
|
||||
final String conversationID;
|
||||
|
||||
final String? groupID;
|
||||
|
||||
/// Conversation type
|
||||
final ConvType conversationType;
|
||||
|
||||
/// The life cycle for chat business logic.
|
||||
final ChatLifeCycle? lifeCycle;
|
||||
|
||||
/// The controller for text field.
|
||||
final TIMUIKitInputTextFieldController? textFieldController;
|
||||
|
||||
final bool? isBuild;
|
||||
|
||||
final AutoScrollController? scrollController;
|
||||
|
||||
/// An optional parameter `groupMemberList` can be provided.
|
||||
/// `groupMemberList` accepts a list of nullable `V2TimGroupMemberFullInfo` objects.
|
||||
/// The purpose of this parameter is to allow the client to supply a pre-fetched list
|
||||
/// of group member information. If this list is provided, it will not make
|
||||
/// additional network requests to fetch the group member information internally.
|
||||
List<V2TimGroupMemberFullInfo?>? groupMemberList;
|
||||
|
||||
TIMUIKitChatProviderScope(
|
||||
{Key? key,
|
||||
this.child,
|
||||
this.providers,
|
||||
this.groupMemberList,
|
||||
this.textFieldController,
|
||||
required this.builder,
|
||||
this.model,
|
||||
this.groupID,
|
||||
this.isBuild,
|
||||
required this.conversationID,
|
||||
required this.conversationType,
|
||||
this.controller,
|
||||
this.config,
|
||||
this.lifeCycle,
|
||||
this.scrollController})
|
||||
: super(key: key) {
|
||||
if (isBuild ?? false) {
|
||||
return;
|
||||
}
|
||||
model ??= TUIChatSeparateViewModel();
|
||||
controller?.model = model;
|
||||
controller?.textFieldController = textFieldController;
|
||||
controller?.scrollController = scrollController;
|
||||
if (config != null) {
|
||||
model?.chatConfig = config!;
|
||||
}
|
||||
model?.lifeCycle = lifeCycle;
|
||||
model?.initForEachConversation(
|
||||
conversationType,
|
||||
conversationID,
|
||||
(String value) {
|
||||
textFieldController?.textEditingController?.text = value;
|
||||
},
|
||||
preGroupMemberList: groupMemberList,
|
||||
groupID: groupID,
|
||||
);
|
||||
model?.showC2cMessageEditStatus =
|
||||
(conversationType == ConvType.c2c ? config?.showC2cMessageEditStatus ?? true : false);
|
||||
loadData();
|
||||
}
|
||||
|
||||
loadData() {
|
||||
// if (model!.haveMoreData) {
|
||||
model!.loadChatRecord(count: kIsWeb ? 15 : HistoryMessageDartConstant.getCount);
|
||||
// }
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider.value(value: model),
|
||||
ChangeNotifierProvider.value(value: globalModel),
|
||||
ChangeNotifierProvider.value(value: themeViewModel),
|
||||
ChangeNotifierProvider.value(value: groupListenerModel),
|
||||
Provider(create: (_) => const TIMUIKitChatConfig()),
|
||||
...?providers
|
||||
],
|
||||
child: child,
|
||||
builder: (context, w) => builder(context, model!, w),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,306 @@
|
||||
import 'package:tencent_cloud_chat_sdk/enum/offlinePushInfo.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/business_logic/view_models/tui_chat_global_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart';
|
||||
|
||||
import 'TIMUIKitTextField/tim_uikit_text_field_layout/wide.dart';
|
||||
|
||||
enum GroupReceptAllowType { work, public, meeting }
|
||||
|
||||
enum GroupReceiptAllowType { work, public, meeting }
|
||||
|
||||
enum UrlPreviewType { none, onlyHyperlink, previewCardAndHyperlink }
|
||||
|
||||
class TimeDividerConfig {
|
||||
/// Defines the interval of adding a time divider among the two messages.
|
||||
/// [Unit]: second.
|
||||
/// [Default]: 300.
|
||||
final int? timeInterval;
|
||||
|
||||
/// Defines the parser of a specific timestamp,
|
||||
/// transform it into a semantic time description.
|
||||
final String Function(int timeStamp)? timestampParser;
|
||||
|
||||
TimeDividerConfig({this.timeInterval, this.timestampParser});
|
||||
}
|
||||
|
||||
/// StickerPanelConfig is a configuration class for the sticker panel component.
|
||||
/// It allows customization of specific features such as display options for the
|
||||
/// message area, sticker packages, unicode emoji lists, and custom sticker packages.
|
||||
class StickerPanelConfig {
|
||||
/// Determines whether to use the QQ Sticker Package.
|
||||
/// Default value: true
|
||||
final bool useQQStickerPackage;
|
||||
|
||||
/// Determines whether to use the Tencent Cloud Chat Sticker Package.
|
||||
/// Default value: true
|
||||
final bool useTencentCloudChatStickerPackage;
|
||||
|
||||
/// Determines whether to compatible with the Tencent Cloud Chat Sticker Package 3.x version.
|
||||
/// Default value : false
|
||||
final bool useTencentCloudChatStickerPackageOldKeys;
|
||||
|
||||
/// A list of unicode emoji, represented as integers.
|
||||
/// Default value: a list of common Unicode Emojis.
|
||||
/// To exclude Unicode Emoji from the display, pass an empty list.
|
||||
final List<int> unicodeEmojiList;
|
||||
|
||||
/// A list of CustomStickerPackage instances, where each instance represents a sticker package.
|
||||
/// Default value: an empty list.
|
||||
final List<CustomStickerPackage> customStickerPackages;
|
||||
|
||||
StickerPanelConfig({
|
||||
this.useQQStickerPackage = true,
|
||||
this.useTencentCloudChatStickerPackage = true,
|
||||
this.useTencentCloudChatStickerPackageOldKeys = false,
|
||||
this.unicodeEmojiList = TUIKitStickerConstData.defaultUnicodeEmojiList,
|
||||
this.customStickerPackages = const [],
|
||||
});
|
||||
}
|
||||
|
||||
class TIMUIKitChatConfig {
|
||||
/// A StickerPanelConfig instance to configure the sticker panel's behavior and appearance.
|
||||
/// This includes options such as display settings, usage of specific sticker packages,
|
||||
/// unicode emoji lists, and custom sticker packages.
|
||||
final StickerPanelConfig? stickerPanelConfig;
|
||||
|
||||
/// Customize the time divider among the two messages.
|
||||
final TimeDividerConfig? timeDividerConfig;
|
||||
|
||||
/// Control if allowed to show reading status.
|
||||
/// [Default]: true.
|
||||
final bool isShowReadingStatus;
|
||||
|
||||
/// Control if allowed to show reading status for group.
|
||||
/// [Default]: true.
|
||||
/// [Deprecated: ] Please use [isShowReadingStatus] instead.
|
||||
final bool isShowGroupReadingStatus;
|
||||
|
||||
/// Control if allowed to report reading status for group.
|
||||
/// [Default]: true.
|
||||
/// [Deprecated: ] Please use [isShowReadingStatus] instead.
|
||||
final bool isReportGroupReadingStatus;
|
||||
|
||||
/// Control if allowed to show the message operation menu after long pressing message.
|
||||
/// [Default]: true.
|
||||
final bool isAllowLongPressMessage;
|
||||
|
||||
/// Control if allowed to callback after clicking the avatar.
|
||||
/// [Default]: true.
|
||||
final bool isAllowClickAvatar;
|
||||
|
||||
/// Control if allowed to show emoji face message panel.
|
||||
/// [Default]: true.
|
||||
final bool isAllowEmojiPanel;
|
||||
|
||||
/// Control if allowed to show more plus panel.
|
||||
/// [Default]: true.
|
||||
final bool isAllowShowMorePanel;
|
||||
|
||||
/// Control if allowed to send voice sound message.
|
||||
/// [Default]: true.
|
||||
final bool isAllowSoundMessage;
|
||||
|
||||
/// Control if allowed to at when reply automatically.
|
||||
/// [Default]: true.
|
||||
final bool isAtWhenReply;
|
||||
|
||||
/// Control if allowed to at when reply automatically.
|
||||
/// [Default]: true.
|
||||
final bool Function(V2TimMessage message)? isAtWhenReplyDynamic;
|
||||
|
||||
/// The main switch of the group read receipt.
|
||||
/// [Deprecated: ] Please use [isShowReadingStatus] instead.
|
||||
final bool isShowGroupMessageReadReceipt;
|
||||
|
||||
/// [Deprecated: ] not support.
|
||||
final List<GroupReceptAllowType>? groupReadReceiptPermisionList;
|
||||
|
||||
/// Control which group can send message read receipt.
|
||||
/// [Deprecated: ] not support.
|
||||
final List<GroupReceiptAllowType>? groupReadReceiptPermissionList;
|
||||
|
||||
/// Control if show self name in group chat.
|
||||
/// [Default]: false.
|
||||
final bool isShowSelfNameInGroup;
|
||||
|
||||
/// Control if others name in group chat.
|
||||
/// [Default]: true.
|
||||
final bool isShowOthersNameInGroup;
|
||||
|
||||
/// Configuration for offline push.
|
||||
/// If this field is specified, `notificationTitle`, `notificationOPPOChannelID`, `notificationIOSSound`, `notificationAndroidSound`, `notificationBody` and `notificationExt` will not work.
|
||||
final OfflinePushInfo? Function(V2TimMessage message, String convID, ConvType convType)? offlinePushInfo;
|
||||
|
||||
/// The title shows in push notification
|
||||
final String notificationTitle;
|
||||
|
||||
/// The channel ID for OPPO in push notification.
|
||||
final String notificationOPPOChannelID;
|
||||
|
||||
/// The notification sound in iOS devices.
|
||||
/// When `iOSSound` = `kIOSOfflinePushNoSound`, the sound will not play when message received. When `iOSSound` = `kIOSOfflinePushDefaultSound`, the system sound is played when message received. If you want to customize `iOSSound`, you need to link the voice file into the Xcode project, and then set the voice file name (with a suffix) to iOSSound.
|
||||
final String notificationIOSSound;
|
||||
|
||||
/// The notification sound in Android devices.
|
||||
final String notificationAndroidSound;
|
||||
|
||||
///Used to set the line height of text messages
|
||||
final double textHeight;
|
||||
|
||||
/// The body content shows in push notification.
|
||||
/// Returning `null` means using default body in this case.
|
||||
final String? Function(V2TimMessage message, String convID, ConvType convType)? notificationBody;
|
||||
|
||||
/// External information (String) for notification message, recommend used for jumping to target conversation with JSON format,
|
||||
/// Returning `null` means using default ext in this case.
|
||||
final String? Function(V2TimMessage message, String convID, ConvType convType)? notificationExt;
|
||||
|
||||
/// The type of URL preview level, none preview, only hyperlink in text, or shows a preview card for website.
|
||||
/// [Default]: UrlPreviewType.previewCardAndHyperlink.
|
||||
final UrlPreviewType urlPreviewType;
|
||||
|
||||
/// Whether to display the sending status of c2c messages
|
||||
/// [Default]: true.
|
||||
final bool showC2cMessageEditStatus;
|
||||
|
||||
/// Control if take emoji stickers as message reaction.
|
||||
/// [Default]: true.
|
||||
final bool isUseMessageReaction;
|
||||
|
||||
/// Determine how long a message is allowed to be recalled after it is sent.
|
||||
/// You must modify the configuration on control dashboard synchronized at: https://console.cloud.tencent.com/im/login-message.
|
||||
/// [Unit]: second.
|
||||
/// [Default]: 120.
|
||||
final int upperRecallTime;
|
||||
|
||||
/// The prefix of face sticker URI.
|
||||
final String Function(String data)? faceURIPrefix;
|
||||
|
||||
/// The suffix of face sticker URI.
|
||||
final String Function(String data)? faceURISuffix;
|
||||
|
||||
/// Controls whether text and replied messages can be displayed with Markdown formatting.
|
||||
/// Also, when enabled, `isEnableTextSelection` will not works.
|
||||
/// [Default]: false.
|
||||
final bool isSupportMarkdownForTextMessage;
|
||||
|
||||
/// The callback after user clicking the URL link in text messages.
|
||||
/// The default action is opening the link with the default browser of system.
|
||||
final void Function(String url)? onTapLink;
|
||||
|
||||
/// Whether shows avatar on history message list.
|
||||
/// [Default]: true.
|
||||
final bool isShowAvatar;
|
||||
|
||||
/// This list contains additional operation items that are displayed on the hover bar
|
||||
/// of a message on desktop (macOS, Windows, and desktop version of Web). These items
|
||||
/// are in addition to the default ones and do not affect them.
|
||||
final List<MessageHoverControlItem>? additionalDesktopMessageHoverBarItem;
|
||||
|
||||
/// This list contains additional items that are displayed
|
||||
/// on the message sending area control bar on desktop (macOS, Windows, and desktop version of Web).
|
||||
/// Use `desktopControlBarConfig` to configure whether or not to show the default control items.
|
||||
final List<DesktopControlBarItem>? additionalDesktopControlBarItems;
|
||||
|
||||
/// This configuration is used for the control bar
|
||||
/// on desktop (macOS, Windows, and desktop version of Web).
|
||||
/// Use `desktopControlBarConfig` to add additional items to the desktop message sending area control bar, in addition to the default ones.
|
||||
final DesktopControlBarConfig? desktopControlBarConfig;
|
||||
|
||||
/// Controls whether users are allowed to mention another user in the group by long-pressing on their avatar.
|
||||
/// [Default]: true.
|
||||
final bool isAllowLongPressAvatarToAt;
|
||||
|
||||
/// Controls whether auto report message read status when new messages come.
|
||||
/// [Default]: true.
|
||||
final bool isAutoReportRead;
|
||||
|
||||
/// Controls whether enable text selection.
|
||||
/// [Default]: true on Desktop while false on Mobile.
|
||||
final bool? isEnableTextSelection;
|
||||
|
||||
/// Controls whether enable the control bar shows when hovering a message on Desktop.
|
||||
/// [Default]: true.
|
||||
final bool isUseMessageHoverBarOnDesktop;
|
||||
|
||||
/// Define the lines in the text message input field on Desktop.
|
||||
final int desktopMessageInputFieldLines;
|
||||
|
||||
/// Specifies whether to use the draft feature on the Web, as the Chat SDK does not support this functionality.
|
||||
/// If enabled, draft data will be stored in TUIKit's memory.
|
||||
/// Note that the draft text will be lost upon refreshing the website.
|
||||
/// [Default]: true.
|
||||
final bool isUseDraftOnWeb;
|
||||
|
||||
/// Determines whether a group administrator is allowed to recall any
|
||||
/// message from any group member. If this capability is enabled,
|
||||
/// recalled messages will not be interoperable with Native clients
|
||||
/// and will only take effect on other Flutter clients.
|
||||
///
|
||||
/// [Default]: false
|
||||
final bool isGroupAdminRecallEnabled;
|
||||
|
||||
/// Defines the height of the sticker panel on desktop platforms.
|
||||
/// If the height of the sticker list exceeds this container height,
|
||||
/// the sticker list will automatically become scrollable.
|
||||
///
|
||||
/// [Default]: 400
|
||||
final double desktopStickerPanelHeight;
|
||||
|
||||
/// Determine whether the normal members can @All in a group chat.
|
||||
/// If enabled, normal members can @All in a group chat.
|
||||
/// If disabled, only the group owner or administrators can @All.
|
||||
///
|
||||
/// [Default]: false
|
||||
final bool isMemberCanAtAll;
|
||||
|
||||
const TIMUIKitChatConfig(
|
||||
{this.onTapLink,
|
||||
this.timeDividerConfig,
|
||||
this.desktopStickerPanelHeight = 400,
|
||||
this.stickerPanelConfig,
|
||||
this.isGroupAdminRecallEnabled = false,
|
||||
this.isAutoReportRead = true,
|
||||
this.faceURIPrefix,
|
||||
this.faceURISuffix,
|
||||
this.textHeight = 1.3,
|
||||
this.desktopMessageInputFieldLines = 6,
|
||||
this.isAtWhenReply = true,
|
||||
this.notificationAndroidSound = "",
|
||||
this.isUseMessageHoverBarOnDesktop = true,
|
||||
this.isSupportMarkdownForTextMessage = false,
|
||||
this.notificationExt,
|
||||
this.isUseMessageReaction = true,
|
||||
this.isShowAvatar = true,
|
||||
this.isShowSelfNameInGroup = false,
|
||||
this.isAtWhenReplyDynamic,
|
||||
this.offlinePushInfo,
|
||||
@Deprecated("Please use [isShowReadingStatus] instead") this.isShowGroupMessageReadReceipt = true,
|
||||
this.upperRecallTime = 120,
|
||||
this.isShowOthersNameInGroup = true,
|
||||
this.urlPreviewType = UrlPreviewType.onlyHyperlink,
|
||||
this.notificationBody,
|
||||
this.notificationOPPOChannelID = "",
|
||||
this.notificationTitle = "",
|
||||
this.notificationIOSSound = "",
|
||||
this.isAllowSoundMessage = true,
|
||||
@Deprecated("not support") this.groupReadReceiptPermisionList,
|
||||
@Deprecated("not support") this.groupReadReceiptPermissionList,
|
||||
this.isAllowEmojiPanel = true,
|
||||
this.isAllowShowMorePanel = true,
|
||||
this.isShowReadingStatus = true,
|
||||
this.desktopControlBarConfig,
|
||||
this.isAllowLongPressMessage = true,
|
||||
this.isUseDraftOnWeb = true,
|
||||
this.isAllowClickAvatar = true,
|
||||
this.isEnableTextSelection,
|
||||
this.additionalDesktopMessageHoverBarItem,
|
||||
this.isShowGroupReadingStatus = true,
|
||||
@Deprecated("Please use [isShowReadingStatus] instead") this.isReportGroupReadingStatus = true,
|
||||
this.showC2cMessageEditStatus = true,
|
||||
this.additionalDesktopControlBarItems,
|
||||
this.isAllowLongPressAvatarToAt = true,
|
||||
this.isMemberCanAtAll = false});
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart';
|
||||
|
||||
class MessageRepliedData {
|
||||
late String messageAbstract;
|
||||
late String messageSender;
|
||||
late String messageID;
|
||||
|
||||
MessageRepliedData.fromJson(Map messageReply) {
|
||||
messageAbstract = messageReply["messageAbstract"];
|
||||
messageSender = messageReply["messageSender"] ?? "";
|
||||
messageID = messageReply["messageID"];
|
||||
}
|
||||
}
|
||||
|
||||
class RepliedMessageAbstract {
|
||||
final int? elemType;
|
||||
final String? msgID;
|
||||
final int? timestamp;
|
||||
final String? seq;
|
||||
final String? summary;
|
||||
|
||||
RepliedMessageAbstract(
|
||||
{this.elemType, this.msgID, this.timestamp, this.seq, this.summary});
|
||||
|
||||
// fromJson constructor
|
||||
RepliedMessageAbstract.fromJson(Map<String, dynamic> json)
|
||||
: elemType = json['elemType'],
|
||||
msgID = json['msgID'],
|
||||
timestamp = json['timestamp'],
|
||||
seq = json['seq'],
|
||||
summary = json['summary'];
|
||||
|
||||
// toJson function
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'summary': summary,
|
||||
'elemType': elemType,
|
||||
'msgID': msgID,
|
||||
'timestamp': timestamp,
|
||||
'seq': seq,
|
||||
};
|
||||
}
|
||||
|
||||
// isNotEmpty method
|
||||
bool get isNotEmpty =>
|
||||
TencentUtils.checkString(msgID) != null &&
|
||||
TencentUtils.checkString(timestamp.toString()) != null &&
|
||||
TencentUtils.checkString(seq) != null;
|
||||
}
|
||||
|
||||
class CloudCustomData {
|
||||
Map<String, dynamic>? messageReply;
|
||||
Map<String, dynamic>? messageReaction = {};
|
||||
|
||||
CloudCustomData.fromJson(Map jsonMap) {
|
||||
messageReply = jsonMap["messageReply"];
|
||||
messageReaction = jsonMap["messageReaction"] ?? {};
|
||||
}
|
||||
|
||||
Map<String, Map?> toMap() {
|
||||
final Map<String, Map?> data = {};
|
||||
if (messageReply != null) {
|
||||
data['messageReply'] = messageReply;
|
||||
}
|
||||
data['messageReaction'] = messageReaction ?? {};
|
||||
return data;
|
||||
}
|
||||
|
||||
CloudCustomData();
|
||||
}
|
||||
@@ -0,0 +1,317 @@
|
||||
import 'package:flutter/cupertino.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_status.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_chat_separate_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_chat_global_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/core/tim_uikit_wide_modal_operation_key.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/forward_message_screen.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/wide_popup.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.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.dart';
|
||||
|
||||
class MultiSelectPanel extends TIMUIKitStatelessWidget {
|
||||
final int forwardMsgNumLimit = 30;
|
||||
|
||||
final ConvType conversationType;
|
||||
|
||||
MultiSelectPanel({Key? key, required this.conversationType})
|
||||
: super(key: key);
|
||||
|
||||
_handleForwardMessage(BuildContext context, bool isMergerForward,
|
||||
TUIChatSeparateViewModel model) {
|
||||
|
||||
// 是否有选中消息
|
||||
if (model.getSelectedMessageList().isEmpty) {
|
||||
onTIMCallback(TIMCallback(
|
||||
type: TIMCallbackType.INFO,
|
||||
infoRecommendText: TIM_t("请选择要操作的消息!")));
|
||||
return;
|
||||
}
|
||||
|
||||
for (var v2TimMessage in model.getSelectedMessageList()) {
|
||||
// 失败消息不支持转发
|
||||
if (v2TimMessage.status == MessageStatus.V2TIM_MSG_STATUS_SEND_FAIL) {
|
||||
onTIMCallback(TIMCallback(
|
||||
type: TIMCallbackType.INFO,
|
||||
infoRecommendText: TIM_t("发送失败消息不支持转发!")));
|
||||
return;
|
||||
}
|
||||
|
||||
// 投票消息不支持转发
|
||||
if (model.isVoteMessage(v2TimMessage)) {
|
||||
onTIMCallback(TIMCallback(
|
||||
type: TIMCallbackType.INFO,
|
||||
infoRecommendText: TIM_t("投票消息不支持转发!")));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 逐条转发限制在 30 条以内
|
||||
if (!isMergerForward && model.getSelectedMessageList().length > forwardMsgNumLimit) {
|
||||
_showForwardLimitDialog(context);
|
||||
return;
|
||||
}
|
||||
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => ForwardMessageScreen(
|
||||
model: model,
|
||||
isMergerForward: isMergerForward,
|
||||
conversationType: conversationType,
|
||||
)));
|
||||
}
|
||||
|
||||
// 弹出逐条转发超限的对话框
|
||||
Future<bool?> _showForwardLimitDialog(BuildContext context) {
|
||||
return showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return CupertinoAlertDialog(
|
||||
title: Text(TIM_t("转发消息过多,暂不支持逐条转发")),
|
||||
actions: [
|
||||
CupertinoDialogAction(
|
||||
child: Text(TIM_t("确定")),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(true);
|
||||
},
|
||||
),
|
||||
CupertinoDialogAction(
|
||||
child: Text(TIM_t("取消")),
|
||||
isDestructiveAction: true,
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
_handleForwardMessageWide(BuildContext context, bool isMergerForward,
|
||||
TUIChatSeparateViewModel model) {
|
||||
TUIKitWidePopup.showPopupWindow(
|
||||
operationKey: TUIKitWideModalOperationKey.forward,
|
||||
context: context,
|
||||
isDarkBackground: false,
|
||||
title: TIM_t("转发"),
|
||||
submitWidget: Text(TIM_t("发送")),
|
||||
width: MediaQuery.of(context).size.width * 0.5,
|
||||
height: MediaQuery.of(context).size.height * 0.8,
|
||||
onSubmit: (){
|
||||
forwardMessageScreenKey.currentState?.handleForwardMessage();
|
||||
},
|
||||
child: (onClose) => Container(
|
||||
padding: const EdgeInsets.symmetric( horizontal: 10),
|
||||
child: ForwardMessageScreen(
|
||||
model: model,
|
||||
key: forwardMessageScreenKey,
|
||||
onClose: onClose,
|
||||
isMergerForward: isMergerForward,
|
||||
conversationType: conversationType,
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final TUITheme theme = value.theme;
|
||||
final TUIChatSeparateViewModel model =
|
||||
Provider.of<TUIChatSeparateViewModel>(context);
|
||||
|
||||
return TUIKitScreenUtils.getDeviceWidget(
|
||||
context: context,
|
||||
desktopWidget: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: theme.selectPanelBgColor ?? theme.primaryColor,
|
||||
border: Border(
|
||||
top: BorderSide(
|
||||
color: theme.weakDividerColor ?? Colors.grey,
|
||||
width: 1.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(vertical: 32),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Wrap(
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
alignment: WrapAlignment.center,
|
||||
spacing: 64,
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: Image.asset('images/forward.png',
|
||||
package: 'tencent_cloud_chat_uikit',
|
||||
color: theme.selectPanelTextIconColor),
|
||||
iconSize: 30,
|
||||
onPressed: () {
|
||||
_handleForwardMessageWide(context, false, model);
|
||||
},
|
||||
),
|
||||
Text(TIM_t("逐条转发"),
|
||||
style: TextStyle(
|
||||
color: hexToColor("646a73"), fontSize: 12))
|
||||
],
|
||||
),
|
||||
Column(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: Image.asset('images/merge_forward.png',
|
||||
package: 'tencent_cloud_chat_uikit',
|
||||
color: theme.selectPanelTextIconColor),
|
||||
iconSize: 30,
|
||||
onPressed: () {
|
||||
_handleForwardMessageWide(context, true, model);
|
||||
},
|
||||
),
|
||||
Text(
|
||||
TIM_t("合并转发"),
|
||||
style:
|
||||
TextStyle(color: theme.selectPanelTextIconColor, fontSize: 12),
|
||||
)
|
||||
],
|
||||
),
|
||||
Column(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: Image.asset('images/delete.png',
|
||||
package: 'tencent_cloud_chat_uikit',
|
||||
color: theme.selectPanelTextIconColor),
|
||||
iconSize: 30,
|
||||
onPressed: () {
|
||||
TUIKitWidePopup.showSecondaryConfirmDialog(
|
||||
operationKey: TUIKitWideModalOperationKey.confirmDeleteMessages,
|
||||
context: context,
|
||||
text: TIM_t("确定删除已选消息"),
|
||||
theme: theme,
|
||||
onCancel: () {},
|
||||
onConfirm: () async {
|
||||
model.deleteSelectedMsg();
|
||||
model.updateMultiSelectStatus(false);
|
||||
});
|
||||
},
|
||||
),
|
||||
Text(TIM_t("删除"),
|
||||
style: TextStyle(
|
||||
color: theme.selectPanelTextIconColor, fontSize: 12))
|
||||
],
|
||||
),
|
||||
InkWell(
|
||||
onTap: (){
|
||||
model.updateMultiSelectStatus(false);
|
||||
},
|
||||
child: Icon(Icons.close, color: theme.darkTextColor,),
|
||||
)
|
||||
],
|
||||
))
|
||||
],
|
||||
),
|
||||
),
|
||||
defaultWidget: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
top: BorderSide(
|
||||
color: theme.weakDividerColor ??
|
||||
CommonColor.weakDividerColor)),
|
||||
color: theme.selectPanelBgColor ?? theme.primaryColor,
|
||||
),
|
||||
padding: const EdgeInsets.only(top: 12, bottom: 48),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: Image.asset('images/forward.png',
|
||||
package: 'tencent_cloud_chat_uikit', color: theme.selectPanelTextIconColor),
|
||||
iconSize: 40,
|
||||
onPressed: () {
|
||||
_handleForwardMessage(context, false, model);
|
||||
},
|
||||
),
|
||||
Text(TIM_t("逐条转发"),
|
||||
style: TextStyle(color: theme.selectPanelTextIconColor, fontSize: 12))
|
||||
],
|
||||
),
|
||||
Column(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: Image.asset('images/merge_forward.png',
|
||||
package: 'tencent_cloud_chat_uikit', color: theme.selectPanelTextIconColor),
|
||||
iconSize: 40,
|
||||
onPressed: () {
|
||||
_handleForwardMessage(context, true, model);
|
||||
},
|
||||
),
|
||||
Text(
|
||||
TIM_t("合并转发"),
|
||||
style: TextStyle(color: theme.selectPanelTextIconColor, fontSize: 12),
|
||||
)
|
||||
],
|
||||
),
|
||||
Column(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: Image.asset('images/delete.png',
|
||||
package: 'tencent_cloud_chat_uikit', color: theme.selectPanelTextIconColor),
|
||||
iconSize: 40,
|
||||
onPressed: () {
|
||||
showCupertinoModalPopup<String>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return CupertinoActionSheet(
|
||||
title: Text(TIM_t("确定删除已选消息")),
|
||||
cancelButton: CupertinoActionSheetAction(
|
||||
onPressed: () {
|
||||
Navigator.pop(
|
||||
context,
|
||||
"cancel",
|
||||
);
|
||||
},
|
||||
child: Text(TIM_t("取消")),
|
||||
isDefaultAction: false,
|
||||
),
|
||||
actions: [
|
||||
CupertinoActionSheetAction(
|
||||
onPressed: () {
|
||||
model.deleteSelectedMsg();
|
||||
model.updateMultiSelectStatus(false);
|
||||
Navigator.pop(
|
||||
context,
|
||||
"cancel",
|
||||
);
|
||||
},
|
||||
child: Text(
|
||||
TIM_t("删除"),
|
||||
style: TextStyle(color: theme.cautionColor),
|
||||
),
|
||||
isDefaultAction: false,
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
Text(TIM_t("删除"),
|
||||
style: TextStyle(color: theme.selectPanelTextIconColor, fontSize: 12))
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
import 'dart:io';
|
||||
import 'package:dotted_border/dotted_border.dart';
|
||||
import 'package:flutter/material.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_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_chat_separate_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_chat_global_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/core/tim_uikit_wide_modal_operation_key.dart';
|
||||
import 'package:cross_file/cross_file.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/message.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/platform.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/wide_popup.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/tui_theme.dart';
|
||||
import 'TIMUIKitMessageItem/tim_uikit_chat_file_icon.dart';
|
||||
|
||||
String _getConvID(V2TimConversation conversation) {
|
||||
return (conversation.type == 1 ? conversation.userID : conversation.groupID) ?? "";
|
||||
}
|
||||
|
||||
sendFileWithConfirmation(
|
||||
{required List<XFile> files,
|
||||
required V2TimConversation conversation,
|
||||
required ConvType conversationType,
|
||||
required TUIChatSeparateViewModel model,
|
||||
required TUITheme theme,
|
||||
required BuildContext context}) async {
|
||||
bool isCanSend = true;
|
||||
|
||||
if (!PlatformUtils().isWeb) {
|
||||
files.map((e) => e.path).any((filePath) {
|
||||
final directory = Directory(filePath);
|
||||
final isDirectoryExists = directory.existsSync();
|
||||
if (isDirectoryExists) {
|
||||
isCanSend = false;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
} else {
|
||||
files.map((e) => e.name).any((fileName) {
|
||||
String fileExtension = path.extension(fileName);
|
||||
bool hasNoExtension = fileExtension.isEmpty;
|
||||
if (hasNoExtension) {
|
||||
isCanSend = false;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
if (!isCanSend) {
|
||||
TUIKitWidePopup.showSecondaryConfirmDialog(
|
||||
text: TIM_t("无法发送,包含文件夹"),
|
||||
onConfirm: () {},
|
||||
operationKey: TUIKitWideModalOperationKey.unableToSendDueToFolders,
|
||||
context: context,
|
||||
theme: theme);
|
||||
return;
|
||||
}
|
||||
|
||||
final option1 = conversation.showName ?? (conversationType == ConvType.group ? TIM_t("群聊") : TIM_t("对方"));
|
||||
TUIKitWidePopup.showPopupWindow(
|
||||
operationKey: TUIKitWideModalOperationKey.beforeSendScreenShot,
|
||||
context: context,
|
||||
isDarkBackground: false,
|
||||
width: 600,
|
||||
height: files.length < 4 ? 300 : 500,
|
||||
title: TIM_t_para("发送给{{option1}}", "发送给$option1")(option1: option1),
|
||||
child: (closeFunc) => Container(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Scrollbar(
|
||||
child: ListView.separated(
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final file = files[index];
|
||||
final fileName = PlatformUtils().isWeb ? file.name : path.basename(file.path);
|
||||
return Material(
|
||||
color: theme.wideBackgroundColor,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
launchUrl(Uri.file(file.path));
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 20),
|
||||
child: Row(
|
||||
children: [
|
||||
TIMUIKitFileIcon(
|
||||
size: 44,
|
||||
fileFormat: fileName.split(".")[fileName.split(".").length - 1],
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Text(
|
||||
fileName,
|
||||
style: TextStyle(fontSize: 16, color: theme.darkTextColor),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
separatorBuilder: (BuildContext context, int index) {
|
||||
return Divider(
|
||||
height: 1,
|
||||
thickness: 1,
|
||||
color: theme.weakDividerColor,
|
||||
);
|
||||
},
|
||||
itemCount: files.length,
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 16, top: 16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
OutlinedButton(
|
||||
onPressed: () {
|
||||
closeFunc();
|
||||
},
|
||||
child: Text(TIM_t("取消"))),
|
||||
const SizedBox(
|
||||
width: 20,
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
sendFiles(files, model, conversation, conversationType, context);
|
||||
closeFunc();
|
||||
},
|
||||
child: Text(TIM_t("发送")))
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
Future<void> sendFiles(List<XFile> files, TUIChatSeparateViewModel model, V2TimConversation conversation,
|
||||
ConvType conversationType, BuildContext context) async {
|
||||
for (final file in files) {
|
||||
final fileName = file.name;
|
||||
final filePath = file.path;
|
||||
await MessageUtils.handleMessageError(
|
||||
model.sendFileMessage(
|
||||
fileName: fileName, filePath: filePath, convID: _getConvID(conversation), convType: conversationType),
|
||||
context);
|
||||
await Future.delayed(const Duration(microseconds: 300));
|
||||
}
|
||||
}
|
||||
|
||||
class TIMUIKitSendFile extends TIMUIKitStatelessWidget {
|
||||
final V2TimConversation conversation;
|
||||
|
||||
TIMUIKitSendFile({required this.conversation, Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final theme = value.theme;
|
||||
final conversationType = conversation.type;
|
||||
final option1 = conversation.showName ?? (conversationType == 2 ? TIM_t("群聊") : TIM_t("会话"));
|
||||
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Opacity(
|
||||
opacity: 0.85,
|
||||
child: Container(
|
||||
color: theme.wideBackgroundColor,
|
||||
padding: const EdgeInsets.all(40),
|
||||
child: DottedBorder(
|
||||
borderType: BorderType.RRect,
|
||||
radius: const Radius.circular(20),
|
||||
color: theme.primaryColor ?? theme.weakTextColor!,
|
||||
dashPattern: const [6, 3],
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.file_copy_outlined,
|
||||
size: 60,
|
||||
color: theme.primaryColor,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 40,
|
||||
),
|
||||
Text(
|
||||
TIM_t_para("发送给{{option1}}", "发送给$option1")(option1: option1),
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: theme.darkTextColor),
|
||||
)
|
||||
],
|
||||
))
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
))
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user