update flutter uikit to 1.4.0

This commit is contained in:
anonymous 2023-01-14 09:23:46 +08:00
parent e0bb3127c8
commit 121e8c295f
25 changed files with 480 additions and 91 deletions

View File

@ -1,3 +1,9 @@
## 1.4.0
* Add: Text translation. Long press the text messages and choose `Translate`. This function can be turn off by `showTranslation` from `ToolTipsConfig`.
* Optimize: The long press pop-up location.
* Optimize: keyboard pop-up event.
## 1.3.2 ## 1.3.2
* Fix: Text input field height, after choosing to mention someone. * Fix: Text input field height, after choosing to mention someone.

View File

@ -963,7 +963,7 @@ packages:
name: tencent_cloud_chat_sdk name: tencent_cloud_chat_sdk
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "5.0.7" version: "5.0.8"
tencent_cloud_chat_uikit: tencent_cloud_chat_uikit:
dependency: "direct main" dependency: "direct main"
description: description:
@ -998,14 +998,14 @@ packages:
name: tencent_im_base name: tencent_im_base
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.17" version: "1.0.19"
tencent_im_sdk_plugin_platform_interface: tencent_im_sdk_plugin_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: tencent_im_sdk_plugin_platform_interface name: tencent_im_sdk_plugin_platform_interface
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.3.10" version: "0.3.11"
tencent_im_sdk_plugin_web: tencent_im_sdk_plugin_web:
dependency: "direct main" dependency: "direct main"
description: description:

BIN
images/translate.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

@ -3,10 +3,13 @@ import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
// ignore: unnecessary_import // ignore: unnecessary_import
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter_image_compress/flutter_image_compress.dart'; import 'package:flutter_image_compress/flutter_image_compress.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart';
import 'package:tencent_cloud_chat_uikit/ui/widgets/link_preview/models/link_preview_content.dart';
import 'package:tencent_im_base/tencent_im_base.dart'; import 'package:tencent_im_base/tencent_im_base.dart';
import 'package:tencent_cloud_chat_uikit/business_logic/life_cycle/chat_life_cycle.dart'; import 'package:tencent_cloud_chat_uikit/business_logic/life_cycle/chat_life_cycle.dart';
import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_chat_model_tools.dart'; import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_chat_model_tools.dart';
@ -53,6 +56,7 @@ class TUIChatSeparateViewModel extends ChangeNotifier {
V2TimGroupInfo? _groupInfo; V2TimGroupInfo? _groupInfo;
String groupMemberListSeq = "0"; String groupMemberListSeq = "0";
List<V2TimGroupMemberFullInfo?>? groupMemberList = []; List<V2TimGroupMemberFullInfo?>? groupMemberList = [];
V2TimGroupInfo? get groupInfo => _groupInfo; V2TimGroupInfo? get groupInfo => _groupInfo;
set groupInfo(V2TimGroupInfo? value) { set groupInfo(V2TimGroupInfo? value) {
@ -1214,6 +1218,26 @@ class TUIChatSeparateViewModel extends ChangeNotifier {
} }
} }
translateText(V2TimMessage message) async {
final String originText = message.textElem?.text ?? "";
final String deviceLocale =
WidgetsBinding.instance?.window.locale.toLanguageTag() ?? "en";
final String targetMessage = deviceLocale.split("-")[0];
final translatedText =
await _messageService.translateText(originText, targetMessage);
final LocalCustomDataModel localCustomData = LocalCustomDataModel.fromMap(
json.decode(TencentUtils.checkString(message.localCustomData) ?? "{}"));
localCustomData.translatedText = translatedText;
final result = await TencentImSDKPlugin.v2TIMManager.v2TIMMessageManager
.setLocalCustomData(
msgID: message.msgID!,
localCustomData: json.encode(localCustomData.toMap()));
if (result.code == 0 && TencentUtils.checkString(message.msgID) != null) {
updateMessageFromController(msgID: message.msgID!);
}
}
updateMultiSelectStatus(bool isSelect) { updateMultiSelectStatus(bool isSelect) {
_isMultiSelect = isSelect; _isMultiSelect = isSelect;
if (!isSelect) { if (!isSelect) {

View File

@ -798,4 +798,18 @@ class MessageServiceImpl extends MessageService {
} }
return result; return result;
} }
@override
Future<String> translateText(String text, String target) async {
final result = await TencentImSDKPlugin.v2TIMManager
.getMessageManager()
.translateText(texts: [text], targetLanguage: target);
if (result.code != 0) {
_coreService.callOnCallback(TIMCallback(
type: TIMCallbackType.API_ERROR,
errorMsg: result.desc,
errorCode: result.code));
}
return result.data?[text] ?? "";
}
} }

View File

@ -204,4 +204,6 @@ abstract class MessageService {
required int imageType, // messageType为图片消息是有效 required int imageType, // messageType为图片消息是有效
required bool isSnapshot, // messageType为视频消息是有效 required bool isSnapshot, // messageType为视频消息是有效
}); });
Future<String> translateText(String text, String target);
} }

File diff suppressed because one or more lines are too long

View File

@ -656,5 +656,9 @@
"k_0td1p3f": "保存中…", "k_0td1p3f": "保存中…",
"k_1klqdh1": "仅限汉字、英文、数字和下划线", "k_1klqdh1": "仅限汉字、英文、数字和下划线",
"k_03el5lp": "未填写", "k_03el5lp": "未填写",
"k_1ui0gai": "搜索指定内容" "k_1ui0gai": "搜索指定内容",
"k_003nvk2": "消息",
"k_03agld7": "群提示",
"k_002wkr3": "翻译",
"k_13g4hxv": "翻译完成"
} }

File diff suppressed because one or more lines are too long

View File

@ -245,6 +245,37 @@ class MessageUtils {
margin: const EdgeInsets.symmetric(vertical: 10), child: child); margin: const EdgeInsets.symmetric(vertical: 10), child: child);
} }
static String getAbstractMessageAsync(V2TimMessage message,
List<V2TimGroupMemberFullInfo?> groupMemberList) {
final msgType = message.elemType;
switch (msgType) {
case MessageElemType.V2TIM_ELEM_TYPE_CUSTOM:
return handleCustomMessageString(message);
case MessageElemType.V2TIM_ELEM_TYPE_SOUND:
return TIM_t("[语音]");
case MessageElemType.V2TIM_ELEM_TYPE_TEXT:
return message.textElem!.text as String;
case MessageElemType.V2TIM_ELEM_TYPE_FACE:
return TIM_t("[表情]");
case MessageElemType.V2TIM_ELEM_TYPE_FILE:
final String? option2 = message.fileElem!.fileName ?? "";
return TIM_t_para("[文件] {{option2}}", "[文件] $option2")(
option2: option2);
case MessageElemType.V2TIM_ELEM_TYPE_GROUP_TIPS:
return TIM_t("群提示");
case MessageElemType.V2TIM_ELEM_TYPE_IMAGE:
return TIM_t("[图片]");
case MessageElemType.V2TIM_ELEM_TYPE_VIDEO:
return TIM_t("[视频]");
case MessageElemType.V2TIM_ELEM_TYPE_LOCATION:
return TIM_t("[位置]");
case MessageElemType.V2TIM_ELEM_TYPE_MERGER:
return TIM_t("[聊天记录]");
default:
return TIM_t("未知消息");
}
}
static Future<String> getAbstractMessage(V2TimMessage message, static Future<String> getAbstractMessage(V2TimMessage message,
List<V2TimGroupMemberFullInfo?> groupMemberList) async { List<V2TimGroupMemberFullInfo?> groupMemberList) async {
final msgType = message.elemType; final msgType = message.elemType;

View File

@ -7,6 +7,7 @@ import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:loading_animation_widget/loading_animation_widget.dart'; import 'package:loading_animation_widget/loading_animation_widget.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitMessageItem/tim_uikit_chat_text_translate_elem.dart';
import 'package:tencent_super_tooltip/tencent_super_tooltip.dart'; import 'package:tencent_super_tooltip/tencent_super_tooltip.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_state.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_statelesswidget.dart';
@ -131,6 +132,7 @@ class ToolTipsConfig {
final bool showRecallMessage; final bool showRecallMessage;
final bool showCopyMessage; final bool showCopyMessage;
final bool showForwardMessage; final bool showForwardMessage;
final bool showTranslation;
final Widget? Function(V2TimMessage message, Function() closeTooltip, final Widget? Function(V2TimMessage message, Function() closeTooltip,
[Key? key, BuildContext? context])? additionalItemBuilder; [Key? key, BuildContext? context])? additionalItemBuilder;
@ -139,6 +141,7 @@ class ToolTipsConfig {
this.showMultipleChoiceMessage = true, this.showMultipleChoiceMessage = true,
this.showRecallMessage = true, this.showRecallMessage = true,
this.showReplyMessage = true, this.showReplyMessage = true,
this.showTranslation = true,
this.showCopyMessage = true, this.showCopyMessage = true,
this.showForwardMessage = true, this.showForwardMessage = true,
this.additionalItemBuilder}); this.additionalItemBuilder});
@ -200,6 +203,9 @@ class TIMUIKitHistoryMessageListItem extends StatefulWidget {
/// padding for each message item /// padding for each message item
final EdgeInsetsGeometry? padding; final EdgeInsetsGeometry? padding;
/// The controller for text field.
final TIMUIKitInputTextFieldController? textFieldController;
/// padding for text messagesound messagereply message /// padding for text messagesound messagereply message
final EdgeInsetsGeometry? textPadding; final EdgeInsetsGeometry? textPadding;
@ -253,7 +259,8 @@ class TIMUIKitHistoryMessageListItem extends StatefulWidget {
this.isUseMessageReaction, this.isUseMessageReaction,
this.bottomRowBuilder, this.bottomRowBuilder,
this.isUseDefaultEmoji = false, this.isUseDefaultEmoji = false,
this.customEmojiStickerList = const []}) this.customEmojiStickerList = const [],
this.textFieldController})
: super(key: key); : super(key: key);
@override @override
@ -682,6 +689,7 @@ class _TIMUIKItHistoryMessageListItemState
} else { } else {
if (box != null) { if (box != null) {
double screenWidth = MediaQuery.of(context).size.width; double screenWidth = MediaQuery.of(context).size.width;
double viewInsetsBottom = MediaQuery.of(context).viewInsets.bottom;
Offset offset = box.localToGlobal(Offset.zero); Offset offset = box.localToGlobal(Offset.zero);
double boxWidth = box.size.width; double boxWidth = box.size.width;
if (isSelf) { if (isSelf) {
@ -689,7 +697,10 @@ class _TIMUIKItHistoryMessageListItemState
} else { } else {
left = offset.dx; left = offset.dx;
} }
if (offset.dy < 300 && !isLongMessage) { if (offset.dy < 300 && !isLongMessage && viewInsetsBottom == 0) {
selectEmojiPanelPosition = SelectEmojiPanelPosition.up;
popupDirection = TooltipDirection.down;
} else if(viewInsetsBottom != 0 && offset.dy < 220){
selectEmojiPanelPosition = SelectEmojiPanelPosition.up; selectEmojiPanelPosition = SelectEmojiPanelPosition.up;
popupDirection = TooltipDirection.down; popupDirection = TooltipDirection.down;
} }
@ -1032,11 +1043,18 @@ class _TIMUIKItHistoryMessageListItemState
)), )),
Container( Container(
constraints: BoxConstraints( constraints: BoxConstraints(
// maxWidth: getMaxWidth(false),
// maxWidth: getMaxWidth(false), // maxWidth: getMaxWidth(false),
maxWidth: constraints.maxWidth * 0.8, maxWidth: constraints.maxWidth * 0.8,
), ),
child: Builder(builder: (context) { child: Builder(builder: (context) {
return GestureDetector( return Column(
crossAxisAlignment:
(message.isSelf ?? false)
? CrossAxisAlignment.end
: CrossAxisAlignment.start,
children: [
GestureDetector(
child: IgnorePointer( child: IgnorePointer(
ignoring: model.isMultiSelect, ignoring: model.isMultiSelect,
child: _getMessageItemBuilder( child: _getMessageItemBuilder(
@ -1071,6 +1089,19 @@ class _TIMUIKItHistoryMessageListItemState
widget.onLongPress!(context, message); widget.onLongPress!(context, message);
} }
}, },
),
Container(
color: theme.white,
child: TIMUIKitTextTranslationElem(
message: message,
isUseDefaultEmoji: widget.isUseDefaultEmoji,
customEmojiStickerList: widget.customEmojiStickerList,
isFromSelf: message.isSelf ?? false,
isShowJump: false,
clearJump: () {},
chatModel: model),
)
],
); );
}), }),
), ),

View File

@ -73,15 +73,15 @@ class TIMUIKitMessageTooltipState
Widget ItemInkWell({ Widget ItemInkWell({
Widget? child, Widget? child,
GestureTapCallback? onTap, GestureTapCallback? onTap
}) { }) {
return SizedBox( return SizedBox(
width: 50, width: 40,
child: InkWell( child: InkWell(
onTap: onTap, onTap: onTap,
splashColor: Colors.white, splashColor: Colors.white,
child: Container( child: Container(
padding: const EdgeInsets.only(bottom: 8, top: 8), padding: const EdgeInsets.only(bottom: 6, top: 6),
child: child, child: child,
), ),
), ),
@ -127,6 +127,11 @@ class TIMUIKitMessageTooltipState
"id": "delete", "id": "delete",
"icon": "images/delete_message.png" "icon": "images/delete_message.png"
}, },
{
"label": TIM_t("翻译"),
"id": "translate",
"icon": "images/translate.png"
},
if (shouldShowRevokeAction) if (shouldShowRevokeAction)
{ {
"label": TIM_t("撤回"), "label": TIM_t("撤回"),
@ -134,15 +139,12 @@ class TIMUIKitMessageTooltipState
"icon": "images/revoke_message.png" "icon": "images/revoke_message.png"
} }
]; ];
if (widget.message.elemType != MessageElemType.V2TIM_ELEM_TYPE_TEXT) {
defaultTipsList.removeAt(0);
}
List formatedTipsList = defaultTipsList; List formatedTipsList = defaultTipsList;
if (tooltipsConfig != null) { if (tooltipsConfig != null) {
formatedTipsList = defaultTipsList.where((element) { formatedTipsList = defaultTipsList.where((element) {
final type = element["id"]; final type = element["id"];
if (type == "copyMessage") { if (type == "copyMessage") {
return tooltipsConfig.showCopyMessage; return tooltipsConfig.showCopyMessage && widget.message.elemType == MessageElemType.V2TIM_ELEM_TYPE_TEXT;
} }
if (type == "forwardMessage") { if (type == "forwardMessage") {
return tooltipsConfig.showForwardMessage; return tooltipsConfig.showForwardMessage;
@ -156,7 +158,9 @@ class TIMUIKitMessageTooltipState
if (type == "multiSelect") { if (type == "multiSelect") {
return tooltipsConfig.showMultipleChoiceMessage; return tooltipsConfig.showMultipleChoiceMessage;
} }
if (type == "translate") {
return tooltipsConfig.showTranslation && widget.message.elemType == MessageElemType.V2TIM_ELEM_TYPE_TEXT;
}
if (type == "revoke") { if (type == "revoke") {
return tooltipsConfig.showRecallMessage; return tooltipsConfig.showRecallMessage;
} }
@ -212,6 +216,9 @@ class TIMUIKitMessageTooltipState
model.updateMultiSelectStatus(true); model.updateMultiSelectStatus(true);
model.addToMultiSelectedMessageList(widget.message); model.addToMultiSelectedMessageList(widget.message);
break; break;
case 'translate':
model.translateText(widget.message);
break;
case "forwardMessage": case "forwardMessage":
model.addToMultiSelectedMessageList(widget.message); model.addToMultiSelectedMessageList(widget.message);
Navigator.push( Navigator.push(
@ -313,10 +320,10 @@ class TIMUIKitMessageTooltipState
direction: Axis.horizontal, direction: Axis.horizontal,
alignment: ScreenUtils.getFormFactor(context) == alignment: ScreenUtils.getFormFactor(context) ==
ScreenType.Handset ScreenType.Handset
? WrapAlignment.spaceAround ? WrapAlignment.spaceBetween
: WrapAlignment.start, : WrapAlignment.start,
spacing: 4, spacing: 4,
runSpacing: 16, runSpacing: 8,
children: [ children: [
..._buildLongPressTipItem(theme, model), ..._buildLongPressTipItem(theme, model),
if (extraTipsActionItem != null) if (extraTipsActionItem != null)

View File

@ -2,6 +2,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:scroll_to_index/scroll_to_index.dart'; import 'package:scroll_to_index/scroll_to_index.dart';
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitTextField/tim_uikit_text_field_controller.dart';
import 'package:tencent_im_base/tencent_im_base.dart'; import 'package:tencent_im_base/tencent_im_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_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/separate_models/tui_chat_separate_view_model.dart';
@ -61,6 +62,9 @@ class TIMUIKitHistoryMessageListContainer extends StatefulWidget {
final List customEmojiStickerList; final List customEmojiStickerList;
/// The controller for text field.
final TIMUIKitInputTextFieldController? textFieldController;
final bool isAllowScroll; final bool isAllowScroll;
const TIMUIKitHistoryMessageListContainer({ const TIMUIKitHistoryMessageListContainer({
@ -84,6 +88,7 @@ class TIMUIKitHistoryMessageListContainer extends StatefulWidget {
this.toolTipsConfig, this.toolTipsConfig,
this.isUseDefaultEmoji = false, this.isUseDefaultEmoji = false,
this.customEmojiStickerList = const [], this.customEmojiStickerList = const [],
this.textFieldController,
}) : super(key: key); }) : super(key: key);
@override @override
@ -155,6 +160,7 @@ class _TIMUIKitHistoryMessageListContainerState
message: message!, message: message!,
showAvatar: chatConfig.isShowAvatar, showAvatar: chatConfig.isShowAvatar,
onTapForOthersPortrait: widget.onTapAvatar, onTapForOthersPortrait: widget.onTapAvatar,
textFieldController: widget.textFieldController,
messageItemBuilder: widget.messageItemBuilder, messageItemBuilder: widget.messageItemBuilder,
onLongPressForOthersHeadPortrait: onLongPressForOthersHeadPortrait:
widget.onLongPressForOthersHeadPortrait, widget.onLongPressForOthersHeadPortrait,

View File

@ -226,9 +226,9 @@ class _TIMUIKitReplyElemState extends TIMUIKitState<TIMUIKitReplyElem> {
if (widget.message.localCustomData != null && if (widget.message.localCustomData != null &&
widget.message.localCustomData!.isNotEmpty) { widget.message.localCustomData!.isNotEmpty) {
final String localJSON = widget.message.localCustomData!; final String localJSON = widget.message.localCustomData!;
final LinkPreviewModel? localPreviewInfo = final LocalCustomDataModel? localPreviewInfo =
LinkPreviewModel.fromMap(json.decode(localJSON)); LocalCustomDataModel.fromMap(json.decode(localJSON));
if (localPreviewInfo != null && !localPreviewInfo.isEmpty()) { if (localPreviewInfo != null && !localPreviewInfo.isLinkPreviewEmpty()) {
return Container( return Container(
margin: const EdgeInsets.only(top: 8), margin: const EdgeInsets.only(top: 8),
child: child:
@ -290,7 +290,7 @@ class _TIMUIKitReplyElemState extends TIMUIKitState<TIMUIKitReplyElem> {
bottomLeft: Radius.circular(10), bottomLeft: Radius.circular(10),
bottomRight: Radius.circular(10)); bottomRight: Radius.circular(10));
final textWithLink = LinkPreviewEntry.getHyperlinksText( final textWithLink = LinkPreviewEntry.getHyperlinksText(
widget.message, widget.message.textElem?.text ?? "",
widget.chatModel.chatConfig.isSupportMarkdownForTextMessage, widget.chatModel.chatConfig.isSupportMarkdownForTextMessage,
widget.chatModel.chatConfig.onTapLink); widget.chatModel.chatConfig.onTapLink);
return Container( return Container(

View File

@ -102,10 +102,10 @@ class _TIMUIKitTextElemState extends TIMUIKitState<TIMUIKitTextElem> {
if (widget.message.localCustomData != null && if (widget.message.localCustomData != null &&
widget.message.localCustomData!.isNotEmpty) { widget.message.localCustomData!.isNotEmpty) {
final String localJSON = widget.message.localCustomData!; final String localJSON = widget.message.localCustomData!;
final LinkPreviewModel? localPreviewInfo = final LocalCustomDataModel? localPreviewInfo =
LinkPreviewModel.fromMap(json.decode(localJSON)); LocalCustomDataModel.fromMap(json.decode(localJSON));
// If [localCustomData] is not empty, check if the link preview info exists // If [localCustomData] is not empty, check if the link preview info exists
if (localPreviewInfo == null || localPreviewInfo.isEmpty()) { if (localPreviewInfo == null || localPreviewInfo.isLinkPreviewEmpty()) {
// If not exists, get it // If not exists, get it
_initLinkPreview(); _initLinkPreview();
} }
@ -136,9 +136,9 @@ class _TIMUIKitTextElemState extends TIMUIKitState<TIMUIKitTextElem> {
widget.message.localCustomData!.isNotEmpty) { widget.message.localCustomData!.isNotEmpty) {
try { try {
final String localJSON = widget.message.localCustomData!; final String localJSON = widget.message.localCustomData!;
final LinkPreviewModel? localPreviewInfo = final LocalCustomDataModel? localPreviewInfo =
LinkPreviewModel.fromMap(json.decode(localJSON)); LocalCustomDataModel.fromMap(json.decode(localJSON));
if (localPreviewInfo != null && !localPreviewInfo.isEmpty()) { if (localPreviewInfo != null && !localPreviewInfo.isLinkPreviewEmpty()) {
return Container( return Container(
margin: const EdgeInsets.only(top: 8), margin: const EdgeInsets.only(top: 8),
child: child:
@ -160,7 +160,7 @@ class _TIMUIKitTextElemState extends TIMUIKitState<TIMUIKitTextElem> {
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) { Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
final theme = value.theme; final theme = value.theme;
final textWithLink = LinkPreviewEntry.getHyperlinksText( final textWithLink = LinkPreviewEntry.getHyperlinksText(
widget.message, widget.message.textElem?.text ?? "",
widget.chatModel.chatConfig.isSupportMarkdownForTextMessage, widget.chatModel.chatConfig.isSupportMarkdownForTextMessage,
widget.chatModel.chatConfig.onTapLink, widget.chatModel.chatConfig.onTapLink,
widget.isUseDefaultEmoji, widget.isUseDefaultEmoji,

View File

@ -0,0 +1,259 @@
import 'dart:async';
import 'dart:convert';
import 'package:tencent_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 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 bool isUseDefaultEmoji;
final List 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.isUseDefaultEmoji = false,
this.customEmojiStickerList = const []})
: super(key: key);
@override
State<StatefulWidget> createState() => _TIMUIKitTextTranslationElemState();
}
class _TIMUIKitTextTranslationElemState extends TIMUIKitState<TIMUIKitTextTranslationElem> {
bool isShowJumpState = false;
bool isShining = false;
@override
void initState() {
super.initState();
// get the link preview info
_getLinkPreview();
}
@override
void didUpdateWidget(TIMUIKitTextTranslationElem 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: () {
widget.chatModel
.updateMessageFromController(msgID: widget.message.msgID!);
});
}
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 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.lightPrimaryMaterialColor.shade50
: theme.chatMessageItemFromOthersBgColor;
final backgroundColor = isShowJumpState
? const Color.fromRGBO(245, 166, 35, 1)
: (widget.backgroundColor ?? defaultStyle);
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,
widget.chatModel.chatConfig.onTapLink,
widget.isUseDefaultEmoji,
widget.customEmojiStickerList,
);
return TencentUtils.checkString(translateText) != null ? Container(
margin: const EdgeInsets.only(top: 6),
padding: widget.textPadding ?? const EdgeInsets.all(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: 16,
textBaseline: TextBaseline.ideographic,
height: widget.chatModel.chatConfig.textHight))
: ExtendedText(translateText!,
softWrap: true,
style: widget.fontStyle ??
TextStyle(
fontSize: 16,
height: widget.chatModel.chatConfig.textHight),
specialTextSpanBuilder: DefaultSpecialTextSpanBuilder(
isUseDefaultEmoji: widget.isUseDefaultEmoji,
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);
}
}

View File

@ -279,7 +279,7 @@ class _InputTextFieldState extends TIMUIKitState<TIMUIKitInputTextField> {
final haveRepliedMessage = repliedMessage != null; final haveRepliedMessage = repliedMessage != null;
if (haveRepliedMessage) { if (haveRepliedMessage) {
final text = final text =
"${MessageUtils.getDisplayName(widget.model.repliedMessage!)}:${widget.model.abstractMessageBuilder != null ? widget.model.abstractMessageBuilder!(widget.model.repliedMessage!) : MessageUtils.getAbstractMessage(widget.model.repliedMessage!, widget.model.groupMemberList ?? [])}"; "${MessageUtils.getDisplayName(widget.model.repliedMessage!)}:${widget.model.abstractMessageBuilder != null ? widget.model.abstractMessageBuilder!(widget.model.repliedMessage!) : MessageUtils.getAbstractMessageAsync(widget.model.repliedMessage!, widget.model.groupMemberList ?? [])}";
return Container( return Container(
color: widget.backgroundColor ?? hexToColor("f5f5f6"), color: widget.backgroundColor ?? hexToColor("f5f5f6"),
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,

View File

@ -337,6 +337,7 @@ class _TUIChatState extends TIMUIKitState<TIMUIKitChat> {
child: Listener( child: Listener(
onPointerMove: closePanel, onPointerMove: closePanel,
child: TIMUIKitHistoryMessageListContainer( child: TIMUIKitHistoryMessageListContainer(
textFieldController: textFieldController,
customEmojiStickerList: customEmojiStickerList:
widget.customEmojiStickerList, widget.customEmojiStickerList,
isUseDefaultEmoji: isUseDefaultEmoji:

View File

@ -52,7 +52,7 @@ class _ForwardMessageScreenState extends TIMUIKitState<ForwardMessageScreen> {
final sender = (e.nickName != null && e.nickName!.isNotEmpty) final sender = (e.nickName != null && e.nickName!.isNotEmpty)
? e.nickName ? e.nickName
: e.sender; : e.sender;
return "$sender: ${model.abstractMessageBuilder != null ? model.abstractMessageBuilder!(e) : MessageUtils.getAbstractMessage(e, [])}"; return "$sender: ${model.abstractMessageBuilder != null ? model.abstractMessageBuilder!(e) : MessageUtils.getAbstractMessageAsync(e, [])}";
}).toList(); }).toList();
} }

View File

@ -45,10 +45,10 @@ class LinkUtils {
} }
/// Get the URL preview information /// Get the URL preview information
static Future<List<LinkPreviewModel>> getURLPreview( static Future<List<LocalCustomDataModel>> getURLPreview(
List<String> urlMatches) async { List<String> urlMatches) async {
// Request for preview information for all URL links synchronously // Request for preview information for all URL links synchronously
final List<LinkPreviewModel> urlPreview = final List<LocalCustomDataModel> urlPreview =
await Future.wait(urlMatches.map((e) async { await Future.wait(urlMatches.map((e) async {
String url = e; String url = e;
if (!e.contains("http")) { if (!e.contains("http")) {
@ -56,7 +56,7 @@ class LinkUtils {
} }
final WebInfo info = await LinkPreview.scrapeFromURL(url); final WebInfo info = await LinkPreview.scrapeFromURL(url);
return LinkPreviewModel( return LocalCustomDataModel(
url: e, url: e,
title: info.title, title: info.title,
image: info.image, image: info.image,
@ -68,7 +68,7 @@ class LinkUtils {
/// save the link info to local and call updating the message on UI, only works with [onUpdateMessage] /// save the link info to local and call updating the message on UI, only works with [onUpdateMessage]
static Future<void> saveToLocalAndUpdate(V2TimMessage message, static Future<void> saveToLocalAndUpdate(V2TimMessage message,
LinkPreviewModel previewItem, VoidCallback onUpdateMessage) async { LocalCustomDataModel previewItem, VoidCallback onUpdateMessage) async {
if (message.msgID != null) { if (message.msgID != null) {
String saveInfo = LinkPreviewEntry.linkInfoToString(previewItem); String saveInfo = LinkPreviewEntry.linkInfoToString(previewItem);
final currentInfo = message.localCustomData; final currentInfo = message.localCustomData;

View File

@ -9,11 +9,10 @@ import 'models/link_preview_content.dart';
class LinkPreviewEntry { class LinkPreviewEntry {
/// get the text message with hyperlinks /// get the text message with hyperlinks
static LinkPreviewText? getHyperlinksText( static LinkPreviewText? getHyperlinksText(
V2TimMessage message, bool isMarkdown, String messageText, bool isMarkdown,
[Function(String)? onLinkTap, [Function(String)? onLinkTap,
bool isUseDefaultEmoji = false, bool isUseDefaultEmoji = false,
List customEmojiStickerList = const []]) { List customEmojiStickerList = const []]) {
final String? messageText = message.textElem!.text;
if (messageText == null) { if (messageText == null) {
return null; return null;
@ -46,10 +45,10 @@ class LinkPreviewEntry {
return null; return null;
} }
final List<LinkPreviewModel?> previewItemList = final List<LocalCustomDataModel?> previewItemList =
await LinkUtils.getURLPreview([urlMatches[0]]); await LinkUtils.getURLPreview([urlMatches[0]]);
if (previewItemList.isNotEmpty) { if (previewItemList.isNotEmpty) {
final LinkPreviewModel previewItem = previewItemList.first!; final LocalCustomDataModel previewItem = previewItemList.first!;
if (onUpdateMessage != null) { if (onUpdateMessage != null) {
LinkUtils.saveToLocalAndUpdate(message, previewItem, onUpdateMessage); LinkUtils.saveToLocalAndUpdate(message, previewItem, onUpdateMessage);
} }
@ -75,7 +74,7 @@ class LinkPreviewEntry {
return []; return [];
} }
final List<LinkPreviewModel> previewItemList = final List<LocalCustomDataModel> previewItemList =
await LinkUtils.getURLPreview([urlMatches[0]]); await LinkUtils.getURLPreview([urlMatches[0]]);
if (previewItemList.isNotEmpty) { if (previewItemList.isNotEmpty) {
final List<LinkPreviewContent?> resultList = previewItemList final List<LinkPreviewContent?> resultList = previewItemList
@ -91,7 +90,7 @@ class LinkPreviewEntry {
} }
} }
static String linkInfoToString(LinkPreviewModel linkInfo) { static String linkInfoToString(LocalCustomDataModel linkInfo) {
return linkInfo.toString(); return linkInfo.toString();
} }

View File

@ -4,14 +4,15 @@ import 'package:flutter/cupertino.dart';
typedef LinkPreviewText = Widget Function({TextStyle? style}); typedef LinkPreviewText = Widget Function({TextStyle? style});
class LinkPreviewModel { class LocalCustomDataModel {
final String? description; final String? description;
final String? image; final String? image;
final String url; final String? url;
final String? title; final String? title;
String? translatedText;
LinkPreviewModel( LocalCustomDataModel(
{this.description, this.image, required this.url, this.title}); {this.description, this.image, this.url, this.title, this.translatedText});
Map<String, String?> toMap() { Map<String, String?> toMap() {
final Map<String, String?> data = {}; final Map<String, String?> data = {};
@ -19,13 +20,15 @@ class LinkPreviewModel {
data['image'] = image; data['image'] = image;
data['title'] = title; data['title'] = title;
data['description'] = description; data['description'] = description;
data['translatedText'] = translatedText;
return data; return data;
} }
LinkPreviewModel.fromMap(Map map) LocalCustomDataModel.fromMap(Map map)
: description = map['description'], : description = map['description'],
image = map['image'], image = map['image'],
url = map['url'], url = map['url'],
translatedText = map['translatedText'],
title = map['title']; title = map['title'];
@override @override
@ -33,7 +36,7 @@ class LinkPreviewModel {
return json.encode(toMap()); return json.encode(toMap());
} }
bool isEmpty() { bool isLinkPreviewEmpty() {
if ((image == null || image!.isEmpty) && if ((image == null || image!.isEmpty) &&
(title == null || title!.isEmpty) && (title == null || title!.isEmpty) &&
(description == null || description!.isEmpty)) { (description == null || description!.isEmpty)) {
@ -49,6 +52,6 @@ class LinkPreviewContent {
this.linkPreviewWidget, this.linkPreviewWidget,
}); });
final LinkPreviewModel? linkInfo; final LocalCustomDataModel? linkInfo;
final Widget? linkPreviewWidget; final Widget? linkPreviewWidget;
} }

View File

@ -4,19 +4,21 @@ import 'package:tencent_cloud_chat_uikit/ui/widgets/link_preview/common/utils.da
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/models/link_preview_content.dart';
class LinkPreviewWidget extends TIMStatelessWidget { class LinkPreviewWidget extends TIMStatelessWidget {
final LinkPreviewModel linkPreview; final LocalCustomDataModel linkPreview;
const LinkPreviewWidget({Key? key, required this.linkPreview}) const LinkPreviewWidget({Key? key, required this.linkPreview})
: super(key: key); : super(key: key);
@override @override
Widget timBuild(BuildContext context) { Widget timBuild(BuildContext context) {
if (linkPreview.isEmpty()) { if (linkPreview.isLinkPreviewEmpty()) {
return Container(); return Container();
} }
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
LinkUtils.launchURL(context, linkPreview.url); if(linkPreview.url != null){
LinkUtils.launchURL(context, linkPreview.url!);
}
}, },
child: Container( child: Container(
padding: const EdgeInsets.only(top: 8, bottom: 8, left: 8, right: 8), padding: const EdgeInsets.only(top: 8, bottom: 8, left: 8, right: 8),

View File

@ -1063,7 +1063,7 @@ packages:
name: tencent_cloud_chat_sdk name: tencent_cloud_chat_sdk
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "5.0.7" version: "5.0.8"
tencent_extended_text: tencent_extended_text:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1091,14 +1091,14 @@ packages:
name: tencent_im_base name: tencent_im_base
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.17" version: "1.0.19"
tencent_im_sdk_plugin_platform_interface: tencent_im_sdk_plugin_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: tencent_im_sdk_plugin_platform_interface name: tencent_im_sdk_plugin_platform_interface
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.3.10" version: "0.3.11"
tencent_keyboard_visibility: tencent_keyboard_visibility:
dependency: "direct main" dependency: "direct main"
description: description:

View File

@ -1,6 +1,6 @@
name: tencent_cloud_chat_uikit name: tencent_cloud_chat_uikit
description: UI components library and basic chat business logic for Tencent Cloud Chat service, helping you build In-APP Chat module easily. description: UI components library and basic chat business logic for Tencent Cloud Chat service, helping you build In-APP Chat module easily.
version: 1.3.2 version: 1.4.0
homepage: https://www.tencentcloud.com/products/im?from=pub homepage: https://www.tencentcloud.com/products/im?from=pub
repository: https://github.com/TencentCloud/tc-chat-uikit-flutter repository: https://github.com/TencentCloud/tc-chat-uikit-flutter
documentation: https://comm.qq.com/im/doc/flutter/en/TUIKit/readme.html documentation: https://comm.qq.com/im/doc/flutter/en/TUIKit/readme.html
@ -62,7 +62,7 @@ dependencies:
url_launcher: ^6.1.4 url_launcher: ^6.1.4
universal_html: ^2.0.8 universal_html: ^2.0.8
link_preview_generator: ^1.2.0 link_preview_generator: ^1.2.0
tencent_im_base: ^1.0.17 tencent_im_base: ^1.0.19
disk_space: ^0.2.1 disk_space: ^0.2.1
http: ^0.13.5 http: ^0.13.5
crypto: ^3.0.2 crypto: ^3.0.2