init
This commit is contained in:
@@ -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,
|
||||
)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user