This commit is contained in:
2025-06-30 16:24:20 +08:00
commit 68de5eb048
454 changed files with 56921 additions and 0 deletions
@@ -0,0 +1,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),
))
],
),
),
),
),
);
})),
),
),
);
}
}
@@ -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,
)),
),
],
),
),
],
),
));
}
}