init
This commit is contained in:
Vendored
BIN
Binary file not shown.
@@ -0,0 +1,32 @@
|
||||
enum V2TimImageTypesEnum {
|
||||
original,
|
||||
big,
|
||||
small,
|
||||
}
|
||||
|
||||
class HistoryMessageDartConstant {
|
||||
static const getCount = 20;
|
||||
|
||||
// ignore: constant_identifier_names
|
||||
static const V2_TIM_IMAGE_TYPES = {
|
||||
'ORIGINAL': 0,
|
||||
'BIG': 1,
|
||||
'SMALL': 2,
|
||||
};
|
||||
|
||||
static Map<V2TimImageTypesEnum, List<String>> imgPriorMap = {
|
||||
V2TimImageTypesEnum.original: oriImgPrior,
|
||||
V2TimImageTypesEnum.big: bigImgPrior,
|
||||
V2TimImageTypesEnum.small: smallImgPrior,
|
||||
};
|
||||
|
||||
// 缩略图优先,大图次之,最后是原图
|
||||
static const smallImgPrior = ['ORIGINAL', 'BIG', 'SMALL'];
|
||||
// 大图优先,原图次之,最后是缩略图
|
||||
static const bigImgPrior = ['SMALL', 'ORIGINAL', 'BIG'];
|
||||
// 原图优先,大图次之,最后是缩略图
|
||||
static const oriImgPrior = ['SMALL', 'BIG', 'ORIGINAL'];
|
||||
|
||||
// 视频、音频已读状态
|
||||
static const int read = 1;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// ignore_for_file: constant_identifier_names
|
||||
|
||||
class TimeConst {
|
||||
static const DAY_SEC = 86400;
|
||||
static const HOUR_SEC = 3600;
|
||||
static const MIN_SEC = 60;
|
||||
|
||||
static const SEC_SERIES = [HOUR_SEC, MIN_SEC];
|
||||
}
|
||||
@@ -0,0 +1,378 @@
|
||||
// ignore_for_file: avoid_print, empty_catches
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:scroll_to_index/scroll_to_index.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/enum/history_msg_get_type_enum.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/enum/message_priority_enum.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/enum/offlinePushInfo.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_conversation.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_member_full_info.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_group_member_full_info.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_message.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_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/business_logic/separate_models/tui_chat_separate_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_chat_global_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/services_locatar.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart';
|
||||
|
||||
class TIMUIKitChatController {
|
||||
late TUIChatSeparateViewModel? model;
|
||||
late TIMUIKitInputTextFieldController? textFieldController;
|
||||
late AutoScrollController? scrollController;
|
||||
final TUIChatGlobalModel globalChatModel = serviceLocator<TUIChatGlobalModel>();
|
||||
|
||||
TIMUIKitChatController({TUIChatSeparateViewModel? viewModel}) {
|
||||
if (viewModel != null) {
|
||||
model = viewModel;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> loadHistoryMessageList(
|
||||
{HistoryMsgGetTypeEnum getType = HistoryMsgGetTypeEnum.V2TIM_GET_CLOUD_OLDER_MSG,
|
||||
String? userID,
|
||||
String? groupID,
|
||||
int lastMsgSeq = -1,
|
||||
required int count,
|
||||
String? lastMsgID,
|
||||
LoadDirection direction = LoadDirection.previous}) async {
|
||||
return await model?.loadChatRecord(
|
||||
count: count,
|
||||
getType: getType,
|
||||
lastMsgID: lastMsgID,
|
||||
lastMsgSeq: lastMsgSeq,
|
||||
) ??
|
||||
false;
|
||||
}
|
||||
|
||||
/// Clear the current conversation;
|
||||
@Deprecated("No need to dispose after tencent_cloud_chat_uikit 0.1.4")
|
||||
dispose() {}
|
||||
|
||||
/// Clear the history of current conversation;
|
||||
/// Please provide `convID`, if you use `TIMUIKitChatController` without specifying to a `TIMUIKitChat`.
|
||||
clearHistory([String? convID]) {
|
||||
if (convID != null) {
|
||||
return globalChatModel.setMessageList(convID, []);
|
||||
}
|
||||
return model?.clearHistory();
|
||||
}
|
||||
|
||||
/// Refresh the history message list manually;
|
||||
Future<bool> refreshCurrentHistoryList([String? convID, ConvType? convType]) async {
|
||||
if (model != null) {
|
||||
try {
|
||||
scrollController?.animateTo(
|
||||
scrollController!.position.minScrollExtent,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.ease,
|
||||
);
|
||||
} catch (e) {}
|
||||
return model!.loadDataFromController();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Update single message at UI model
|
||||
/// Please provide `convID`, if you use `TIMUIKitChatController` without specifying to a `TIMUIKitChat`.
|
||||
Future<void> updateMessage(
|
||||
{
|
||||
/// The ID of the target conversation
|
||||
String? convID,
|
||||
|
||||
/// The type of the target conversation
|
||||
ConvType? convType,
|
||||
|
||||
/// message ID
|
||||
required String msgID}) async {
|
||||
if (convID != null && convType != null) {
|
||||
return globalChatModel.updateMessageFromController(
|
||||
msgID: msgID, conversationID: convID, conversationType: convType);
|
||||
} else if (model != null) {
|
||||
return model!.updateMessageFromController(msgID: msgID);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
/// Sends a message to the specified conversation, or to the current conversation specified on `TIMUIKitChat`.
|
||||
/// You must provide `convType` and either `userID` or `groupID`, only if you use `TIMUIKitChat` without specifying a `TIMUIKitChatController`, you must provide these parameters.
|
||||
Future<V2TimValueCallback<V2TimMessage>?>? sendMessage({
|
||||
required V2TimMessage? messageInfo,
|
||||
|
||||
/// The type of the target conversation: either ConvType.group or ConvType.c2c. Required if using `TIMUIKitChat` without specifying a `TIMUIKitChatController`.
|
||||
ConvType? convType,
|
||||
|
||||
/// The user ID of the target one-to-one conversation. Required if convType is ConvType.c2c.
|
||||
String? userID,
|
||||
|
||||
/// The target group ID. Required if convType is ConvType.group.
|
||||
String? groupID,
|
||||
|
||||
/// A callback function to update the input field when message sending fails.
|
||||
ValueChanged<String>? setInputField,
|
||||
|
||||
/// Offline push information.
|
||||
OfflinePushInfo? offlinePushInfo,
|
||||
|
||||
/// Whether automatically scrolling to the bottom of the message list after sending a message.
|
||||
/// This field solely works when `TIMUIKitChatController` is specified for use within a `TIMUIKitChat`.
|
||||
bool isNavigateToMessageListBottom = true,
|
||||
|
||||
/// Message priorities. This field is valid only for group chat messages.
|
||||
/// You are advised to set higher priorities for important messages (such as red packet and gift messages)
|
||||
/// and set lower priorities for frequent but unimportant messages (such as like messages).
|
||||
MessagePriorityEnum priority = MessagePriorityEnum.V2TIM_PRIORITY_NORMAL,
|
||||
|
||||
/// Whether the message can be received only by online users.
|
||||
/// If this field is set to true, the message cannot be pulled in recipient historical message pulling.
|
||||
/// This field is often used to implement weak notification features such as "The other party is typing" or unimportant notifications in the group. This field is not supported by audio-video groups (AVChatRoom).
|
||||
bool? onlineUserOnly,
|
||||
|
||||
/// Whether the message is excluded from the conversation unread message count.
|
||||
bool? isExcludedFromUnreadCount,
|
||||
|
||||
/// Whether a read receipt is required.
|
||||
bool? needReadReceipt,
|
||||
|
||||
/// Cloud custom data (saved in the cloud, will be sent to the peer end,
|
||||
/// and can still be pulled after the app is uninstalled and reinstalled)
|
||||
String? cloudCustomData,
|
||||
|
||||
/// Local custom message data (saved locally, will not be sent to the peer end,
|
||||
/// and will become invalid after the app is uninstalled and reinstalled).
|
||||
String? localCustomData,
|
||||
}) {
|
||||
if (convType != null) {
|
||||
/// Sends a message to the specified conversation.
|
||||
assert((groupID == null) != (userID == null));
|
||||
assert(groupID != null || convType != ConvType.group);
|
||||
assert(userID != null || convType != ConvType.c2c);
|
||||
if (isNavigateToMessageListBottom && scrollController != null) {
|
||||
try {
|
||||
scrollController?.animateTo(
|
||||
scrollController!.position.minScrollExtent,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.ease,
|
||||
);
|
||||
} catch (e) {}
|
||||
}
|
||||
return globalChatModel.sendMessageFromController(
|
||||
priority: priority,
|
||||
onlineUserOnly: onlineUserOnly,
|
||||
isExcludedFromUnreadCount: isExcludedFromUnreadCount,
|
||||
needReadReceipt: needReadReceipt,
|
||||
cloudCustomData: cloudCustomData,
|
||||
localCustomData: localCustomData,
|
||||
messageInfo: messageInfo,
|
||||
convType: convType,
|
||||
convID: (convType == ConvType.group ? groupID : userID) ?? "",
|
||||
setInputField: setInputField,
|
||||
offlinePushInfo: offlinePushInfo);
|
||||
} else if (model != null) {
|
||||
/// Sends a message to the current conversation specified on `TIMUIKitChat`. 发送到 `TIMUIKitChat` 中指定的当前对话。
|
||||
if (isNavigateToMessageListBottom && scrollController != null) {
|
||||
try {
|
||||
scrollController?.animateTo(
|
||||
scrollController!.position.minScrollExtent,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.ease,
|
||||
);
|
||||
} catch (e) {}
|
||||
}
|
||||
return model!.sendMessageFromController(
|
||||
priority: priority,
|
||||
onlineUserOnly: onlineUserOnly,
|
||||
isExcludedFromUnreadCount: isExcludedFromUnreadCount,
|
||||
needReadReceipt: needReadReceipt,
|
||||
cloudCustomData: cloudCustomData,
|
||||
localCustomData: localCustomData,
|
||||
messageInfo: messageInfo,
|
||||
offlinePushInfo: offlinePushInfo);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Sends a message, replying to another message, to the specified conversation, or to the current conversation specified on `TIMUIKitChat`.
|
||||
/// You must provide `convType` and either `userID` or `groupID`, only if you use `TIMUIKitChat` without specifying a `TIMUIKitChatController`, you must provide these parameters.
|
||||
Future<V2TimValueCallback<V2TimMessage>?>? sendReplyMessage({
|
||||
required String messageText,
|
||||
required V2TimMessage messageBeenReplied,
|
||||
|
||||
/// The type of the target conversation: either ConvType.group or ConvType.c2c. Required if using `TIMUIKitChat` without specifying a `TIMUIKitChatController`.
|
||||
ConvType? convType,
|
||||
|
||||
/// The user ID of the target one-to-one conversation. Required if convType is ConvType.c2c.
|
||||
String? userID,
|
||||
|
||||
/// The target group ID. Required if convType is ConvType.group.
|
||||
String? groupID,
|
||||
|
||||
/// A callback function to update the input field when message sending fails.
|
||||
ValueChanged<String>? setInputField,
|
||||
|
||||
/// Offline push information.
|
||||
OfflinePushInfo? offlinePushInfo,
|
||||
|
||||
/// Whether automatically scrolling to the bottom of the message list after sending a message.
|
||||
/// This field solely works when `TIMUIKitChatController` is specified for use within a `TIMUIKitChat`.
|
||||
bool isNavigateToMessageListBottom = true,
|
||||
|
||||
/// Message priorities. This field is valid only for group chat messages.
|
||||
/// You are advised to set higher priorities for important messages (such as red packet and gift messages)
|
||||
/// and set lower priorities for frequent but unimportant messages (such as like messages).
|
||||
MessagePriorityEnum priority = MessagePriorityEnum.V2TIM_PRIORITY_NORMAL,
|
||||
|
||||
/// Whether the message can be received only by online users.
|
||||
/// If this field is set to true, the message cannot be pulled in recipient historical message pulling.
|
||||
/// This field is often used to implement weak notification features such as "The other party is typing" or unimportant notifications in the group. This field is not supported by audio-video groups (AVChatRoom).
|
||||
bool? onlineUserOnly,
|
||||
|
||||
/// Whether the message is excluded from the conversation unread message count.
|
||||
bool? isExcludedFromUnreadCount,
|
||||
|
||||
/// Whether a read receipt is required.
|
||||
bool? needReadReceipt,
|
||||
|
||||
/// Local custom message data (saved locally, will not be sent to the peer end,
|
||||
/// and will become invalid after the app is uninstalled and reinstalled).
|
||||
String? localCustomData,
|
||||
}) {
|
||||
if (convType != null) {
|
||||
/// Sends a message to the specified conversation.
|
||||
assert((groupID == null) != (userID == null));
|
||||
assert(groupID != null || convType != ConvType.group);
|
||||
assert(userID != null || convType != ConvType.c2c);
|
||||
if (isNavigateToMessageListBottom && scrollController != null) {
|
||||
try {
|
||||
scrollController?.animateTo(
|
||||
scrollController!.position.minScrollExtent,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.ease,
|
||||
);
|
||||
} catch (e) {}
|
||||
}
|
||||
return globalChatModel.sendReplyMessageFromController(
|
||||
text: messageText,
|
||||
messageBeenReplied: messageBeenReplied,
|
||||
priority: priority,
|
||||
onlineUserOnly: onlineUserOnly,
|
||||
isExcludedFromUnreadCount: isExcludedFromUnreadCount,
|
||||
needReadReceipt: needReadReceipt,
|
||||
localCustomData: localCustomData,
|
||||
convType: convType,
|
||||
convID: (convType == ConvType.group ? groupID : userID) ?? "",
|
||||
setInputField: setInputField,
|
||||
offlinePushInfo: offlinePushInfo);
|
||||
} else if (model != null) {
|
||||
/// Sends a message to the current conversation specified on `TIMUIKitChat`. 发送到 `TIMUIKitChat` 中指定的当前对话。
|
||||
if (isNavigateToMessageListBottom && scrollController != null) {
|
||||
scrollController?.animateTo(
|
||||
scrollController!.position.minScrollExtent,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.ease,
|
||||
);
|
||||
}
|
||||
return globalChatModel.sendReplyMessageFromController(
|
||||
text: messageText,
|
||||
messageBeenReplied: messageBeenReplied,
|
||||
priority: priority,
|
||||
onlineUserOnly: onlineUserOnly,
|
||||
isExcludedFromUnreadCount: isExcludedFromUnreadCount,
|
||||
needReadReceipt: needReadReceipt,
|
||||
localCustomData: localCustomData,
|
||||
convType: model!.conversationType ?? ConvType.group,
|
||||
convID: model!.conversationID,
|
||||
setInputField: setInputField,
|
||||
offlinePushInfo: offlinePushInfo);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Send forward message;
|
||||
/// This function solely works when `TIMUIKitChatController` is specified for use within a `TIMUIKitChat`.
|
||||
sendForwardMessage({
|
||||
required List<V2TimConversation> conversationList,
|
||||
}) async {
|
||||
model?.sendForwardMessage(conversationList: conversationList);
|
||||
}
|
||||
|
||||
/// Send merger message;
|
||||
/// This method needs use with TIMUIKitChat directly or model been initialized.
|
||||
Future<V2TimValueCallback<V2TimMessage>?> sendMergerMessage({
|
||||
required List<V2TimConversation> conversationList,
|
||||
required String title,
|
||||
required List<String> abstractList,
|
||||
required BuildContext context,
|
||||
}) async {
|
||||
return model?.sendMergerMessage(
|
||||
conversationList: conversationList, title: title, abstractList: abstractList, context: context);
|
||||
}
|
||||
|
||||
/// Set local custom data; returns the bool shows if succeed.
|
||||
/// Please provide `convID`, if you use `TIMUIKitChatController` without specifying to a `TIMUIKitChat`.
|
||||
Future<bool> setLocalCustomData(String msgID, String localCustomData, [String? convID]) async {
|
||||
final String? conversationID = convID ?? model?.conversationID;
|
||||
if (conversationID == null) {
|
||||
return false;
|
||||
}
|
||||
return globalChatModel.setLocalCustomData(msgID, localCustomData, conversationID);
|
||||
}
|
||||
|
||||
/// Set local custom int; returns the bool shows if succeed.
|
||||
/// Please provide `convID`, if you use `TIMUIKitChatController` without specifying to a `TIMUIKitChat`.
|
||||
Future<bool> setLocalCustomInt(String msgID, int localCustomInt, [String? convID]) async {
|
||||
final String? conversationID = convID ?? model?.conversationID;
|
||||
if (conversationID == null) {
|
||||
return false;
|
||||
}
|
||||
return globalChatModel.setLocalCustomInt(msgID, localCustomInt, conversationID);
|
||||
}
|
||||
|
||||
/// Get current conversation, returns UserID or GroupID if in the chat page, returns "" if not.
|
||||
String getCurrentConversation() {
|
||||
return globalChatModel.currentSelectedConv;
|
||||
}
|
||||
|
||||
/// Hide all bottom panels, including the sticker panel and the additional functions panel, on mobile devices.
|
||||
/// This function solely works when `TIMUIKitChatController` is specified for use within a `TIMUIKitChat`.
|
||||
void hideAllBottomPanelOnMobile() {
|
||||
textFieldController?.hideAllPanel();
|
||||
}
|
||||
|
||||
/// Mention or @ other members in a group manually.
|
||||
/// This function solely works when `TIMUIKitChatController` is specified for use within a `TIMUIKitChat`.
|
||||
void mentionOtherMemberInGroup({required String showNameInMessage, required String userID}) {
|
||||
textFieldController?.longPressToAt(showNameInMessage, userID);
|
||||
}
|
||||
|
||||
/// Set the content within the message input text field.
|
||||
/// This function solely works when `TIMUIKitChatController` is specified for use within a `TIMUIKitChat`.
|
||||
void setInputTextField(String text) {
|
||||
textFieldController?.setTextField(text);
|
||||
}
|
||||
|
||||
/// Returns the list of group members of current group chat based on the provided keyword.
|
||||
///
|
||||
/// This method filters the group members based on the given keyword. If the keyword is not provided,
|
||||
/// it returns the entire list of group members. The filtering is performed by checking if the keyword
|
||||
/// is contained within the userID, nickName, or friendRemark properties of each group member.
|
||||
///
|
||||
/// [keyword] (optional) - The keyword to filter the group members. If not provided, the entire list of group members is returned.
|
||||
/// This function solely works when `TIMUIKitChatController` is specified for use within a `TIMUIKitChat`.
|
||||
List<V2TimGroupMemberFullInfo> getGroupMemberList({String? keyword}) {
|
||||
final List<V2TimGroupMemberFullInfo> memberList =
|
||||
(model?.groupMemberList ?? []).whereType<V2TimGroupMemberFullInfo>().toList();
|
||||
|
||||
return TencentUtils.checkString(keyword) == null
|
||||
? memberList
|
||||
: memberList.where((e) {
|
||||
final userID = e.userID;
|
||||
final nickName = e.nickName ?? "";
|
||||
final friendRemark = e.friendRemark ?? "";
|
||||
return userID.contains(keyword!) || nickName.contains(keyword) || friendRemark.contains(keyword);
|
||||
}).toList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_callback.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_callback.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_conversation.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_conversation_view_model.dart';
|
||||
|
||||
class TIMUIKitConversationController {
|
||||
late TUIConversationViewModel model;
|
||||
|
||||
/// Get the selected conversation currently
|
||||
V2TimConversation? get selectedConversation {
|
||||
return model.selectedConversation;
|
||||
}
|
||||
|
||||
/// Set the selected conversation currently
|
||||
set selectedConversation(V2TimConversation? conversation) {
|
||||
model.selectedConversation = conversation;
|
||||
}
|
||||
|
||||
/// Get the conversation list
|
||||
List<V2TimConversation?> get conversationList {
|
||||
return model.conversationList;
|
||||
}
|
||||
|
||||
/// Set the conversation list
|
||||
set conversationList(List<V2TimConversation?> conversationList) {
|
||||
model.conversationList = conversationList;
|
||||
}
|
||||
|
||||
/// Load the conversation list to UI
|
||||
loadData({int count = 40}) {
|
||||
model.loadData(count: count);
|
||||
}
|
||||
|
||||
/// Reload the conversation list to UI
|
||||
reloadData({int count = 100}) {
|
||||
model.refresh(count: count);
|
||||
}
|
||||
|
||||
/// Pin one conversation to the top
|
||||
Future<V2TimCallback> pinConversation({required String conversationID, required bool isPinned}) {
|
||||
return model.pinConversation(conversationID: conversationID, isPinned: isPinned);
|
||||
}
|
||||
|
||||
/// Set the draft for a conversation
|
||||
Future<V2TimCallback> setConversationDraft({required String conversationID, String? draftText}) {
|
||||
return model.setConversationDraft(conversationID: conversationID, draftText: draftText);
|
||||
}
|
||||
|
||||
/// Clear the historical message in a specific conversation
|
||||
Future<V2TimCallback?>? clearHistoryMessage({required V2TimConversation conversation}) {
|
||||
final convType = conversation.type;
|
||||
final convID = convType == 1 ? conversation.userID : conversation.groupID;
|
||||
if (convType != null && convID != null) {
|
||||
return model.clearHistoryMessage(convID: convID, convType: convType);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Delete a conversation
|
||||
Future<V2TimCallback?> deleteConversation({required String conversationID}) {
|
||||
return model.deleteConversation(conversationID: conversationID);
|
||||
}
|
||||
|
||||
/// Clear the conversation list from UI
|
||||
dispose() {
|
||||
model.clearData();
|
||||
}
|
||||
|
||||
/// Scroll to a specific conversation, this conversation must be existed in conversation list.
|
||||
/// If not exist, invoking `loadData` recursively, until find the target conversation.
|
||||
scrollToConversation(String conversationID) {
|
||||
model.scrollToConversation = conversationID;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_callback.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_callback.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_friend_operation_result.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_friend_operation_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_uikit/business_logic/separate_models/tui_profile_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/text_input_bottom_sheet.dart';
|
||||
|
||||
import 'package:tencent_cloud_chat_uikit/theme/tui_theme.dart';
|
||||
|
||||
class TIMUIKitProfileController {
|
||||
late TUIProfileViewModel model;
|
||||
|
||||
/// Remove a user from friend or contact
|
||||
Future<V2TimFriendOperationResult?> deleteFriend(String userID) {
|
||||
return model.deleteFriend(userID);
|
||||
}
|
||||
|
||||
/// Deprecated: Please use [pinConversation] instead. Pin the conversation to the top
|
||||
@Deprecated("Please use [pinConversation] instead")
|
||||
Future<V2TimCallback> pinedConversation(bool isPined, String convID) {
|
||||
return model.pinedConversation(isPined, convID);
|
||||
}
|
||||
|
||||
/// pin the conversation to the top
|
||||
Future<V2TimCallback> pinConversation(bool isPined, String convID) {
|
||||
return model.pinedConversation(isPined, convID);
|
||||
}
|
||||
|
||||
/// add a user to block list
|
||||
Future<List<V2TimFriendOperationResult>?> addUserToBlackList(bool shouldAdd, String userID) {
|
||||
return model.addToBlackList(shouldAdd, userID);
|
||||
}
|
||||
|
||||
/// Change the friend adding request verification method,
|
||||
/// 0 represents "Accept all friend request",
|
||||
/// 1 represents "Require approval for friend requests",
|
||||
/// 2 represents "reject all friend requests".
|
||||
Future<V2TimCallback> changeFriendVerificationMethod(int allowType) {
|
||||
return model.changeFriendVerificationMethod(allowType);
|
||||
}
|
||||
|
||||
/// update the remarks for other users,
|
||||
Future<V2TimCallback> updateRemarks(String userID, String remark) {
|
||||
return model.updateRemarks(userID, remark);
|
||||
}
|
||||
|
||||
/// set the message from a specific user as not disturb, mute notification
|
||||
Future<V2TimCallback> setMessageDisturb(String userID, bool isDisturb) {
|
||||
return model.setMessageDisturb(userID, isDisturb);
|
||||
}
|
||||
|
||||
/// Show the text input bottom sheet
|
||||
showTextInputBottomSheet({
|
||||
required BuildContext context,
|
||||
required String title,
|
||||
required String tips,
|
||||
required Function(String text) onSubmitted,
|
||||
required TUITheme theme,
|
||||
Offset? initOffset,
|
||||
String? initText,
|
||||
}) {
|
||||
TextInputBottomSheet.showTextInputBottomSheet(
|
||||
context: context,
|
||||
title: title,
|
||||
tips: tips,
|
||||
onSubmitted: onSubmitted,
|
||||
theme: theme,
|
||||
initOffset: initOffset,
|
||||
initText: initText);
|
||||
}
|
||||
|
||||
/// Load the profile data
|
||||
loadData(String userID) {
|
||||
model.loadData(userID: userID);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
model.dispose();
|
||||
}
|
||||
|
||||
/// Add a user as friend or contact
|
||||
Future<V2TimFriendOperationResult?> addFriend(String userID) {
|
||||
return model.addFriend(userID);
|
||||
}
|
||||
|
||||
Future<V2TimCallback> updateSelfSignature(String selfSignature) {
|
||||
V2TimUserFullInfo userFullInfo = V2TimUserFullInfo();
|
||||
userFullInfo.selfSignature = selfSignature;
|
||||
return model.updateSelfInfo(userFullInfo);
|
||||
}
|
||||
|
||||
Future<V2TimCallback> updateNickName(String nickName) {
|
||||
V2TimUserFullInfo userFullInfo = V2TimUserFullInfo();
|
||||
userFullInfo.nickName = nickName;
|
||||
return model.updateSelfInfo(userFullInfo);
|
||||
}
|
||||
|
||||
/// 1:男 2:女
|
||||
Future<V2TimCallback> updateGender(int gender) {
|
||||
V2TimUserFullInfo userFullInfo = V2TimUserFullInfo();
|
||||
userFullInfo.gender = gender;
|
||||
return model.updateSelfInfo(userFullInfo);
|
||||
}
|
||||
|
||||
Future<V2TimCallback> updateBirthday(int birthday) {
|
||||
V2TimUserFullInfo userFullInfo = V2TimUserFullInfo();
|
||||
userFullInfo.birthday = birthday;
|
||||
return model.updateSelfInfo(userFullInfo);
|
||||
}
|
||||
|
||||
Future<V2TimCallback> updateAvatar(String url) {
|
||||
V2TimUserFullInfo userFullInfo = V2TimUserFullInfo();
|
||||
userFullInfo.faceUrl = url;
|
||||
return model.updateSelfInfo(userFullInfo);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class MessageThemeData {
|
||||
/// Text style for message text
|
||||
final TextStyle? messageTextStyle;
|
||||
|
||||
/// Text style for user nick name
|
||||
final TextStyle? nickNameTextStyle;
|
||||
|
||||
/// Text style for timeline
|
||||
final TextStyle? timelineTextStyle;
|
||||
|
||||
/// Color for messageBackgroundColor
|
||||
final Color? messageBackgroundColor;
|
||||
|
||||
/// border radius for text message
|
||||
final BorderRadius? messageBorderRadius;
|
||||
|
||||
final BorderRadius? avatarBorderRadius;
|
||||
|
||||
MessageThemeData({
|
||||
this.messageTextStyle,
|
||||
this.messageBackgroundColor,
|
||||
this.messageBorderRadius,
|
||||
this.nickNameTextStyle,
|
||||
this.timelineTextStyle,
|
||||
this.avatarBorderRadius,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
class TencentUtils{
|
||||
static bool isTextNotEmpty(String? text){
|
||||
return text != null && text.isNotEmpty;
|
||||
}
|
||||
|
||||
static String? checkString(String? text){
|
||||
return (text != null && text.isEmpty) ? null : text;
|
||||
}
|
||||
|
||||
static String? checkStringWithoutSpace(String? text){
|
||||
if (text == null || text.trim().isEmpty || text.contains(' ')) {
|
||||
return null;
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
static String getFileType(String fileType) {
|
||||
switch (fileType) {
|
||||
case "3gp":
|
||||
return "video/3gpp";
|
||||
case "torrent":
|
||||
return "application/x-bittorrent";
|
||||
case "kml":
|
||||
return "application/vnd.google-earth.kml+xml";
|
||||
case "gpx":
|
||||
return "application/gpx+xml";
|
||||
case "asf":
|
||||
return "video/x-ms-asf";
|
||||
case "avi":
|
||||
return "video/x-msvideo";
|
||||
case "bin":
|
||||
case "class":
|
||||
case "exe":
|
||||
return "application/octet-stream";
|
||||
case "bmp":
|
||||
return "image/bmp";
|
||||
case "c":
|
||||
return "text/plain";
|
||||
case "conf":
|
||||
return "text/plain";
|
||||
case "cpp":
|
||||
return "text/plain";
|
||||
case "doc":
|
||||
return "application/msword";
|
||||
case "docx":
|
||||
return "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
|
||||
case "xls":
|
||||
case "csv":
|
||||
return "application/vnd.ms-excel";
|
||||
case "xlsx":
|
||||
return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
|
||||
case "gif":
|
||||
return "image/gif";
|
||||
case "gtar":
|
||||
return "application/x-gtar";
|
||||
case "gz":
|
||||
return "application/x-gzip";
|
||||
case "h":
|
||||
return "text/plain";
|
||||
case "htm":
|
||||
return "text/html";
|
||||
case "html":
|
||||
return "text/html";
|
||||
case "jar":
|
||||
return "application/java-archive";
|
||||
case "java":
|
||||
return "text/plain";
|
||||
case "jpeg":
|
||||
return "image/jpeg";
|
||||
case "jpg":
|
||||
return "image/jpeg";
|
||||
case "js":
|
||||
return "application/x-javascript";
|
||||
case "log":
|
||||
return "text/plain";
|
||||
case "m3u":
|
||||
return "audio/x-mpegurl";
|
||||
case "m4a":
|
||||
return "audio/mp4a-latm";
|
||||
case "m4b":
|
||||
return "audio/mp4a-latm";
|
||||
case "m4p":
|
||||
return "audio/mp4a-latm";
|
||||
case "m4u":
|
||||
return "video/vnd.mpegurl";
|
||||
case "m4v":
|
||||
return "video/x-m4v";
|
||||
case "mov":
|
||||
return "video/quicktime";
|
||||
case "mp2":
|
||||
return "audio/x-mpeg";
|
||||
case "mp3":
|
||||
return "audio/x-mpeg";
|
||||
case "mp4":
|
||||
return "video/mp4";
|
||||
case "mpc":
|
||||
return "application/vnd.mpohun.certificate";
|
||||
case "mpe":
|
||||
return "video/mpeg";
|
||||
case "mpeg":
|
||||
return "video/mpeg";
|
||||
case "mpg":
|
||||
return "video/mpeg";
|
||||
case "mpg4":
|
||||
return "video/mp4";
|
||||
case "mpga":
|
||||
return "audio/mpeg";
|
||||
case "msg":
|
||||
return "application/vnd.ms-outlook";
|
||||
case "ogg":
|
||||
return "audio/ogg";
|
||||
case "pdf":
|
||||
return "application/pdf";
|
||||
case "png":
|
||||
return "image/png";
|
||||
case "pps":
|
||||
return "application/vnd.ms-powerpoint";
|
||||
case "ppt":
|
||||
return "application/vnd.ms-powerpoint";
|
||||
case "pptx":
|
||||
return "application/vnd.openxmlformats-officedocument.presentationml.presentation";
|
||||
case "prop":
|
||||
return "text/plain";
|
||||
case "rc":
|
||||
return "text/plain";
|
||||
case "rmvb":
|
||||
return "audio/x-pn-realaudio";
|
||||
case "rtf":
|
||||
return "application/rtf";
|
||||
case "sh":
|
||||
return "text/plain";
|
||||
case "tar":
|
||||
return "application/x-tar";
|
||||
case "tgz":
|
||||
return "application/x-compressed";
|
||||
case "txt":
|
||||
return "text/plain";
|
||||
case "wav":
|
||||
return "audio/x-wav";
|
||||
case "wma":
|
||||
return "audio/x-ms-wma";
|
||||
case "wmv":
|
||||
return "audio/x-ms-wmv";
|
||||
case "wps":
|
||||
return "application/vnd.ms-works";
|
||||
case "xml":
|
||||
return "text/plain";
|
||||
case "z":
|
||||
return "application/x-compress";
|
||||
case "zip":
|
||||
return "application/x-zip-compressed";
|
||||
default:
|
||||
return "*/*";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
class CustomSticker {
|
||||
const CustomSticker(
|
||||
{required this.name, required this.index, this.url, this.unicode});
|
||||
|
||||
final int? unicode;
|
||||
final String name;
|
||||
final int index;
|
||||
final String? url;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import 'package:tencent_chat_i18n_tool/tencent_chat_i18n_tool.dart';
|
||||
|
||||
class ErrorMessageConverter {
|
||||
static Map<int, String> errorCodeMap = {
|
||||
10007: TIM_t("操作权限不足"),
|
||||
20007: TIM_t("发送单聊消息,被对方拉黑,禁止发送。"),
|
||||
30010: TIM_t("您的好友数已达系统上限"),
|
||||
30014: TIM_t("对方的好友数已达系统上限"),
|
||||
30015: TIM_t("对方已是您的好友"),
|
||||
30515: TIM_t("被加好友在自己的黑名单中"),
|
||||
30516: TIM_t("对方已禁止加好友"),
|
||||
30525: TIM_t("您已被被对方设置为黑名单"),
|
||||
30539: TIM_t("等待好友审核同意"),
|
||||
};
|
||||
|
||||
static String getErrorMessage(int code) {
|
||||
return errorCodeMap[code] ?? "";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
// ignore_for_file: avoid_print, prefer_typing_uninitialized_variables
|
||||
|
||||
import 'dart:ui';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/logger.dart';
|
||||
|
||||
class Frame {
|
||||
static var orginalCallback;
|
||||
|
||||
static init() {
|
||||
orginalCallback = window.onReportTimings;
|
||||
window.onReportTimings = onReportTimings;
|
||||
}
|
||||
|
||||
// 仅缓存最近 25 帧绘制耗时
|
||||
static const maxframes = 25;
|
||||
static final List<FrameTiming> lastFrames = [];
|
||||
// 基准 VSync 信号周期
|
||||
static const frameInterval =
|
||||
Duration(microseconds: Duration.microsecondsPerSecond ~/ 60);
|
||||
|
||||
static void onReportTimings(List<FrameTiming> timings) {
|
||||
lastFrames.addAll(timings);
|
||||
// 仅保留 25 帧
|
||||
if (lastFrames.length > maxframes) {
|
||||
lastFrames.removeRange(0, lastFrames.length - maxframes);
|
||||
}
|
||||
// 如果有原始帧回调函数,则执行
|
||||
if (orginalCallback != null) {
|
||||
orginalCallback(timings);
|
||||
}
|
||||
outputLogger.i("fps: $fps");
|
||||
}
|
||||
|
||||
static double get fps {
|
||||
int sum = 0;
|
||||
for (FrameTiming timing in lastFrames) {
|
||||
// 计算渲染耗时
|
||||
int duration = timing.timestampInMicroseconds(FramePhase.rasterFinish) -
|
||||
timing.timestampInMicroseconds(FramePhase.buildStart);
|
||||
// 判断耗时是否在 Vsync 信号周期内
|
||||
if (duration < frameInterval.inMicroseconds) {
|
||||
sum += 1;
|
||||
} else {
|
||||
// 有丢帧,向上取整
|
||||
int count = (duration / frameInterval.inMicroseconds).ceil();
|
||||
sum += count;
|
||||
}
|
||||
}
|
||||
return lastFrames.length / sum * 60;
|
||||
}
|
||||
|
||||
static destroy() {
|
||||
window.onReportTimings = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import 'package:tencent_cloud_chat_sdk/tencent_im_sdk_plugin.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/platform.dart';
|
||||
|
||||
final outputLogger = TencentCloudChatLog();
|
||||
|
||||
class TencentCloudChatLog{
|
||||
void i(String text){
|
||||
if(!PlatformUtils().isWeb){
|
||||
TencentImSDKPlugin.v2TIMManager
|
||||
.uikitTrace(trace: text);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,346 @@
|
||||
// ignore_for_file: unrelated_type_equality_checks, avoid_print
|
||||
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:tencent_chat_i18n_tool/tencent_chat_i18n_tool.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/enum/group_change_info_type.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/enum/group_tips_elem_type.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/enum/message_elem_type.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_change_info.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_group_change_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_info.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_group_member_info.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_tips_elem.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_group_tips_elem.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_image.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_image.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_message.dart';
|
||||
import 'package:tencent_cloud_chat_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_sdk/tencent_im_sdk_plugin.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/constants/history_message_constant.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/constants/time.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/common_utils.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/logger.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/tui_theme.dart';
|
||||
|
||||
class MessageUtils {
|
||||
// 判断CallingData的方式和Trtc的方法一致
|
||||
static isCallingData(String data) {
|
||||
try {
|
||||
Map<String, dynamic> customMap = jsonDecode(data);
|
||||
|
||||
if (customMap.containsKey('businessID') && customMap['businessID'] == 1) {
|
||||
return true;
|
||||
}
|
||||
} catch (e) {
|
||||
outputLogger.i("isCallingData json parse error");
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 是否是群组TRTC信息
|
||||
static isGroupCallingMessage(V2TimMessage message) {
|
||||
final isGroup = message.groupID != null;
|
||||
final isCustomMessage = message.elemType == MessageElemType.V2TIM_ELEM_TYPE_CUSTOM;
|
||||
if (isCustomMessage) {
|
||||
final customElemData = message.customElem?.data ?? "";
|
||||
return isCallingData(customElemData) && isGroup;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static String getCustomGroupCreatedOrDismissedString(V2TimMessage message) {
|
||||
try {
|
||||
final isGroup = message.groupID != null;
|
||||
final isCustomMessage = message.elemType == MessageElemType.V2TIM_ELEM_TYPE_CUSTOM;
|
||||
if (isCustomMessage && isGroup) {
|
||||
final data = message.customElem?.data ?? "";
|
||||
Map<String, dynamic> customMap = jsonDecode(data);
|
||||
if (customMap.containsKey('businessID') && customMap['businessID'] == "group_create") {
|
||||
final content = "${customMap['opUser']}${customMap['content']}";
|
||||
return content;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
return "";
|
||||
} catch (e) {
|
||||
outputLogger.i("getCustomGroupCreatedOrDismissedString json parse error");
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
static Future<String> _getGroupChangeType(
|
||||
V2TimGroupChangeInfo info, List<V2TimGroupMemberFullInfo?> groupMemberList) async {
|
||||
int? type = info.type;
|
||||
var value = info.value;
|
||||
String s = TIM_t('群资料信息');
|
||||
switch (type) {
|
||||
case GroupChangeInfoType.V2TIM_GROUP_INFO_CHANGE_TYPE_CUSTOM:
|
||||
s = TIM_t("自定义字段");
|
||||
break;
|
||||
case GroupChangeInfoType.V2TIM_GROUP_INFO_CHANGE_TYPE_FACE_URL:
|
||||
s = TIM_t("群头像");
|
||||
break;
|
||||
case GroupChangeInfoType.V2TIM_GROUP_INFO_CHANGE_TYPE_INTRODUCTION:
|
||||
s = TIM_t("群简介");
|
||||
break;
|
||||
case GroupChangeInfoType.V2TIM_GROUP_INFO_CHANGE_TYPE_NAME:
|
||||
s = TIM_t("群名称");
|
||||
break;
|
||||
case GroupChangeInfoType.V2TIM_GROUP_INFO_CHANGE_TYPE_NOTIFICATION:
|
||||
s = TIM_t("群公告");
|
||||
break;
|
||||
case GroupChangeInfoType.V2TIM_GROUP_INFO_CHANGE_TYPE_OWNER:
|
||||
s = TIM_t("群主");
|
||||
final V2TimGroupMemberFullInfo? groupMemberInfo =
|
||||
groupMemberList.firstWhereOrNull((element) => element?.userID == value);
|
||||
if (groupMemberInfo != null) {
|
||||
value = TencentUtils.checkString(groupMemberInfo.friendRemark) ??
|
||||
TencentUtils.checkString(groupMemberInfo.nameCard) ??
|
||||
TencentUtils.checkString(groupMemberInfo.nickName) ??
|
||||
TencentUtils.checkString(groupMemberInfo.userID);
|
||||
} else {
|
||||
final res = await TencentImSDKPlugin.v2TIMManager.getUsersInfo(userIDList: [value ?? ""]);
|
||||
if (res.code == 0) {
|
||||
final List<V2TimUserFullInfo> data = res.data ?? [];
|
||||
if (data.isNotEmpty) {
|
||||
final firstPerson = data[0];
|
||||
value = TencentUtils.checkString(firstPerson.nickName) ?? TencentUtils.checkString(firstPerson.userID);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case GroupChangeInfoType.V2TIM_GROUP_INFO_CHANGE_TYPE_SHUT_UP_ALL:
|
||||
s = TIM_t("全员禁言状态");
|
||||
break;
|
||||
case GroupChangeInfoType.V2TIM_GROUP_INFO_CHANGE_TYPE_RECEIVE_MESSAGE_OPT:
|
||||
s = TIM_t("消息接收方式");
|
||||
break;
|
||||
case GroupChangeInfoType.V2TIM_GROUP_INFO_CHANGE_TYPE_GROUP_ADD_OPT:
|
||||
s = TIM_t("加群方式");
|
||||
break;
|
||||
}
|
||||
|
||||
final String option8 = s;
|
||||
if (value != null && value.isNotEmpty) {
|
||||
return TIM_t_para("{{option8}}为 ", "$option8为 ")(option8: option8) + value;
|
||||
} else {
|
||||
return option8;
|
||||
}
|
||||
}
|
||||
|
||||
static String? _getOpUserNick(V2TimGroupMemberInfo? opUser) {
|
||||
if (opUser == null) {
|
||||
return "";
|
||||
}
|
||||
return TencentUtils.checkString(opUser.friendRemark) ??
|
||||
TencentUtils.checkString(opUser.nickName) ??
|
||||
TencentUtils.checkString(opUser.userID);
|
||||
}
|
||||
|
||||
static String? _getMemberNickName(V2TimGroupMemberInfo e) {
|
||||
final friendRemark = e.friendRemark;
|
||||
final nameCard = e.nameCard;
|
||||
final nickName = e.nickName;
|
||||
final userID = e.userID;
|
||||
|
||||
if (friendRemark != null && friendRemark != "") {
|
||||
return friendRemark;
|
||||
} else if (nameCard != null && nameCard != "") {
|
||||
return nameCard;
|
||||
} else if (nickName != null && nickName != "") {
|
||||
return nickName;
|
||||
} else {
|
||||
return userID;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<String> groupTipsMessageAbstract(
|
||||
V2TimGroupTipsElem groupTipsElem, List<V2TimGroupMemberFullInfo?> groupMemberList) async {
|
||||
String displayMessage;
|
||||
final operationType = groupTipsElem.type;
|
||||
final operationMember = groupTipsElem.opMember;
|
||||
final memberList = groupTipsElem.memberList;
|
||||
final opUserNickName = _getOpUserNick(operationMember);
|
||||
switch (operationType) {
|
||||
case GroupTipsElemType.V2TIM_GROUP_TIPS_TYPE_GROUP_INFO_CHANGE:
|
||||
final String? option7 = opUserNickName ?? "";
|
||||
final groupChangeInfoList = groupTipsElem.groupChangeInfoList ?? [];
|
||||
String changedInfoString = "";
|
||||
bool changedValue = false;
|
||||
for (V2TimGroupChangeInfo? element in groupChangeInfoList) {
|
||||
final newText = await _getGroupChangeType(element!, groupMemberList);
|
||||
changedInfoString += (changedInfoString.isEmpty ? "" : " / ") + newText;
|
||||
changedValue = element!.boolValue ?? false;
|
||||
}
|
||||
if (changedInfoString.isEmpty) {
|
||||
changedInfoString = TIM_t("群资料");
|
||||
}
|
||||
if (changedInfoString == TIM_t("全员禁言状态")) {
|
||||
changedInfoString = TIM_t("全员禁言");
|
||||
displayMessage = changedValue == false
|
||||
? TIM_t_para("{{option7}} 取消", "$option7 取消")(option7: option7) + changedInfoString
|
||||
: TIM_t_para("{{option7}} 开启", "$option7 开启")(option7: option7) + changedInfoString;
|
||||
} else {
|
||||
displayMessage = TIM_t_para("{{option7}}修改", "$option7修改")(option7: option7) + changedInfoString;
|
||||
}
|
||||
break;
|
||||
case GroupTipsElemType.V2TIM_GROUP_TIPS_TYPE_QUIT:
|
||||
final String? option6 = opUserNickName ?? "";
|
||||
displayMessage = TIM_t_para("{{option6}}退出群聊", "$option6退出群聊")(option6: option6);
|
||||
break;
|
||||
case GroupTipsElemType.V2TIM_GROUP_TIPS_TYPE_INVITE:
|
||||
final option5 = memberList!.map((e) => _getMemberNickName(e!).toString()).join("、");
|
||||
final inviteUser = _getOpUserNick(operationMember);
|
||||
displayMessage = '$inviteUser' + TIM_t_para("邀请{{option5}}加入群组", "邀请$option5加入群组")(option5: option5);
|
||||
break;
|
||||
case GroupTipsElemType.V2TIM_GROUP_TIPS_TYPE_KICKED:
|
||||
final option4 = memberList!.map((e) => _getMemberNickName(e!).toString()).join("、");
|
||||
final kickUser = _getOpUserNick(operationMember);
|
||||
displayMessage = '$kickUser' + TIM_t_para("将{{option4}}踢出群组", "将$option4踢出群组")(option4: option4);
|
||||
break;
|
||||
case GroupTipsElemType.V2TIM_GROUP_TIPS_TYPE_JOIN:
|
||||
final option3 = memberList!.map((e) => _getMemberNickName(e!).toString()).join("、");
|
||||
displayMessage = TIM_t_para("用户{{option3}}加入了群聊", "用户$option3加入了群聊")(option3: option3);
|
||||
break;
|
||||
case GroupTipsElemType.V2TIM_GROUP_TIPS_TYPE_MEMBER_INFO_CHANGE:
|
||||
displayMessage = groupTipsElem.memberList!.map((e) {
|
||||
final changedMember =
|
||||
groupTipsElem.memberChangeInfoList!.firstWhere((element) => element!.userID == e!.userID);
|
||||
final isMute = changedMember!.muteTime != 0;
|
||||
final option2 = _getMemberNickName(e!);
|
||||
final displayMessage = isMute ? TIM_t("禁言") : TIM_t("解除禁言");
|
||||
return TIM_t_para("{{option2}} 被", "$option2 被")(option2: option2) + displayMessage;
|
||||
}).join("、");
|
||||
break;
|
||||
case GroupTipsElemType.V2TIM_GROUP_TIPS_TYPE_SET_ADMIN:
|
||||
final adminMember = memberList!.map((e) => _getMemberNickName(e!).toString()).join("、");
|
||||
final opMember = _getOpUserNick(operationMember);
|
||||
final option1 = adminMember;
|
||||
displayMessage = '$opMember' + TIM_t_para("将 {{option1}} 设置为管理员", "将 $option1 设置为管理员")(option1: option1);
|
||||
break;
|
||||
case GroupTipsElemType.V2TIM_GROUP_TIPS_TYPE_CANCEL_ADMIN:
|
||||
final adminMember = memberList!.map((e) => _getMemberNickName(e!).toString()).join("、");
|
||||
final opMember = _getOpUserNick(operationMember);
|
||||
final option1 = adminMember;
|
||||
displayMessage = '$opMember' + TIM_t_para("将 {{option1}} 取消管理员", "将 $option1 取消管理员")(option1: option1);
|
||||
break;
|
||||
default:
|
||||
final String option2 = operationType.toString();
|
||||
displayMessage = TIM_t_para("系统消息 {{option2}}", "系统消息 $option2")(option2: option2);
|
||||
break;
|
||||
}
|
||||
return displayMessage;
|
||||
}
|
||||
|
||||
static String formatVideoTime(int time) {
|
||||
List<int> times = [];
|
||||
if (time <= 0) return '0:01';
|
||||
if (time >= TimeConst.DAY_SEC) return '1d+';
|
||||
for (int idx = 0; idx < TimeConst.SEC_SERIES.length; idx++) {
|
||||
int sec = TimeConst.SEC_SERIES[idx];
|
||||
if (time >= sec) {
|
||||
times.add((time / sec).floor());
|
||||
time = time % sec;
|
||||
} else if (idx > 0) {
|
||||
times.add(0);
|
||||
}
|
||||
}
|
||||
times.add(time);
|
||||
String formatTime = times[0].toString();
|
||||
for (int idx = 1; idx < times.length; idx++) {
|
||||
if (times[idx] < 10) {
|
||||
formatTime += ':0${times[idx].toString()}';
|
||||
} else {
|
||||
formatTime += ':${times[idx].toString()}';
|
||||
}
|
||||
}
|
||||
return formatTime;
|
||||
}
|
||||
|
||||
static String handleCustomMessageString(V2TimMessage message) {
|
||||
return TIM_t("消息");
|
||||
}
|
||||
|
||||
static Widget wrapMessageTips(Widget child, TUITheme? theme) {
|
||||
return Container(margin: const EdgeInsets.symmetric(vertical: 10, horizontal: 30), child: child);
|
||||
}
|
||||
|
||||
static String getAbstractMessageAsync(V2TimMessage message, List<V2TimGroupMemberFullInfo?> groupMemberList) {
|
||||
final msgType = message.elemType;
|
||||
switch (msgType) {
|
||||
case MessageElemType.V2TIM_ELEM_TYPE_CUSTOM:
|
||||
return handleCustomMessageString(message);
|
||||
case MessageElemType.V2TIM_ELEM_TYPE_SOUND:
|
||||
return TIM_t("[语音]");
|
||||
case MessageElemType.V2TIM_ELEM_TYPE_TEXT:
|
||||
return message.textElem!.text as String;
|
||||
case MessageElemType.V2TIM_ELEM_TYPE_FACE:
|
||||
return TIM_t("[表情]");
|
||||
case MessageElemType.V2TIM_ELEM_TYPE_FILE:
|
||||
final String? option2 = message.fileElem!.fileName ?? "";
|
||||
return TIM_t_para("[文件] {{option2}}", "[文件] $option2")(option2: option2);
|
||||
case MessageElemType.V2TIM_ELEM_TYPE_GROUP_TIPS:
|
||||
return TIM_t("群提示");
|
||||
case MessageElemType.V2TIM_ELEM_TYPE_IMAGE:
|
||||
return TIM_t("[图片]");
|
||||
case MessageElemType.V2TIM_ELEM_TYPE_VIDEO:
|
||||
return TIM_t("[视频]");
|
||||
case MessageElemType.V2TIM_ELEM_TYPE_LOCATION:
|
||||
return TIM_t("[位置]");
|
||||
case MessageElemType.V2TIM_ELEM_TYPE_MERGER:
|
||||
return TIM_t("[聊天记录]");
|
||||
default:
|
||||
return TIM_t("未知消息");
|
||||
}
|
||||
}
|
||||
|
||||
static V2TimImage? getImageFromImgList(List<V2TimImage?>? list, List<String> order) {
|
||||
V2TimImage? img;
|
||||
try {
|
||||
for (String type in order) {
|
||||
img =
|
||||
list?.firstWhere((e) => e?.type == HistoryMessageDartConstant.V2_TIM_IMAGE_TYPES[type], orElse: () => null);
|
||||
}
|
||||
} catch (e) {
|
||||
outputLogger.i('getImageFromImgList error ${e.toString()}');
|
||||
}
|
||||
return img;
|
||||
}
|
||||
|
||||
static String getDisplayName(V2TimMessage message) {
|
||||
final friendRemark = message.friendRemark ?? "";
|
||||
final nameCard = message.nameCard ?? "";
|
||||
final nickName = message.nickName ?? "";
|
||||
final sender = message.sender ?? "";
|
||||
final displayName = friendRemark.isNotEmpty
|
||||
? friendRemark
|
||||
: nameCard.isNotEmpty
|
||||
? nameCard
|
||||
: nickName.isNotEmpty
|
||||
? nickName
|
||||
: sender;
|
||||
return displayName.toString();
|
||||
}
|
||||
|
||||
static Future<V2TimValueCallback<V2TimMessage>?> handleMessageError(
|
||||
Future<V2TimValueCallback<V2TimMessage>?> fun, BuildContext context) async {
|
||||
final res = await fun;
|
||||
return handleMessageErrorCode(res, context);
|
||||
}
|
||||
|
||||
static V2TimValueCallback<V2TimMessage>? handleMessageErrorCode(
|
||||
V2TimValueCallback<V2TimMessage>? sendMsgRes, BuildContext context) {
|
||||
if (sendMsgRes == null) return null;
|
||||
|
||||
return sendMsgRes;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import 'dart:async';
|
||||
|
||||
class OptimizeUtils {
|
||||
static throttle(Function func, int milliseconds) {
|
||||
bool enable = true;
|
||||
return (val) {
|
||||
if (enable == true) {
|
||||
enable = false;
|
||||
Future.delayed(Duration(milliseconds: milliseconds), () {
|
||||
enable = true;
|
||||
func(val);
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static multiThrottle(Function func, int milliseconds) {
|
||||
bool enable = true;
|
||||
return (val1, val2) {
|
||||
if (enable == true) {
|
||||
enable = false;
|
||||
Future.delayed(Duration(milliseconds: milliseconds), () {
|
||||
enable = true;
|
||||
func(val1, val2);
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static debounce(
|
||||
Function func, [
|
||||
Duration delay = const Duration(milliseconds: 100),
|
||||
]) {
|
||||
Timer? timer;
|
||||
return (val) {
|
||||
if (timer != null) {
|
||||
timer?.cancel();
|
||||
}
|
||||
|
||||
timer = Timer(delay, () {
|
||||
func(val);
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,366 @@
|
||||
// ignore_for_file: unused_import
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:tencent_chat_i18n_tool/tencent_chat_i18n_tool.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/platform.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/tui_theme.dart';
|
||||
|
||||
class PermissionRequestInfo extends StatefulWidget {
|
||||
final Function removeOverLay;
|
||||
final int permissionType;
|
||||
final String appName;
|
||||
|
||||
const PermissionRequestInfo(
|
||||
{Key? key,
|
||||
required this.removeOverLay,
|
||||
required this.permissionType,
|
||||
required this.appName})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _PermissionRequestInfo();
|
||||
}
|
||||
|
||||
class _PermissionRequestInfo extends TIMUIKitState<PermissionRequestInfo>
|
||||
with WidgetsBindingObserver {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.removeOverLay();
|
||||
super.dispose();
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
super.didChangeAppLifecycleState(state);
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
widget.removeOverLay();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final theme = value.theme;
|
||||
|
||||
final permission = {
|
||||
1: {
|
||||
"name": TIM_t("相机"),
|
||||
"icon": "images/chat_permission_icon_camera.png",
|
||||
"text": TIM_t("为方便您将所拍摄的照片或视频发送给朋友,以及进行视频通话,请允许我们访问摄像头进行拍摄照片和视频。")
|
||||
},
|
||||
7: {
|
||||
"name": TIM_t("麦克风"),
|
||||
"icon": "images/chat_permission_icon_mic.png",
|
||||
"text": TIM_t("为方便您发送语音消息、拍摄视频以及音视频通话,请允许我们使用麦克风进行录音。")
|
||||
},
|
||||
9: {
|
||||
"name": TIM_t("相册"),
|
||||
"icon": "images/chat_permission_icon_file.png",
|
||||
"text": TIM_t("为方便您查看和选择相册里的图片视频发送给朋友,以及保存内容到设备,请允许我们访问您设备上的照片、媒体内容。")
|
||||
},
|
||||
15: {
|
||||
"name": TIM_t("存储"),
|
||||
"icon": "images/chat_permission_icon_file.png",
|
||||
"text": TIM_t("为方便您查看和选择相册里的图片视频发送给朋友,以及保存内容到设备,请允许我们访问您设备上的照片、媒体内容。")
|
||||
},
|
||||
32: {
|
||||
"name": TIM_t("相册"),
|
||||
"icon": "images/chat_permission_icon_file.png",
|
||||
"text": TIM_t("为方便您查看和选择相册里的图片视频发送给朋友,以及保存内容到设备,请允许我们访问您设备上的照片、媒体内容。")
|
||||
},
|
||||
}[widget.permissionType];
|
||||
final option2 = permission?["name"] ?? "";
|
||||
return Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
child: SafeArea(
|
||||
child: Opacity(
|
||||
opacity: 0.7,
|
||||
child: Container(
|
||||
color: theme.black,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 40),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const SizedBox(
|
||||
height: 50,
|
||||
),
|
||||
SizedBox(
|
||||
height: 50,
|
||||
width: 50,
|
||||
child: Image.asset(
|
||||
permission?["icon"] ?? "",
|
||||
package: "tencent_cloud_chat_uikit",
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
Text(
|
||||
TIM_t(widget.appName) +
|
||||
TIM_t_para(" 申请获取{{option2}}", " 申请获取$option2")(
|
||||
option2: option2) +
|
||||
TIM_t("权限"),
|
||||
style: TextStyle(color: theme.white, fontSize: 18),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
Text(
|
||||
permission?["text"] ?? "",
|
||||
style: TextStyle(color: theme.white, fontSize: 16),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
top: 0,
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Permissions {
|
||||
|
||||
static OverlayEntry? _entry;
|
||||
|
||||
static List<String> _names(BuildContext context) {
|
||||
return <String>[
|
||||
TIM_t("日历"),
|
||||
TIM_t("相机"),
|
||||
TIM_t("联系人"),
|
||||
TIM_t("位置"),
|
||||
'locationAlways',
|
||||
'locationWhenInUse',
|
||||
'mediaLibrary',
|
||||
TIM_t("麦克风"),
|
||||
'phone',
|
||||
TIM_t("照片"),
|
||||
TIM_t("相册写入"),
|
||||
'reminders',
|
||||
'sensors',
|
||||
'sms',
|
||||
'speech',
|
||||
TIM_t("文件"),
|
||||
'ignoreBatteryOptimizations',
|
||||
'notification',
|
||||
'access_media_location',
|
||||
'activity_recognition',
|
||||
'unknown',
|
||||
'bluetooth',
|
||||
'manageExternalStorage',
|
||||
'systemAlertWindow',
|
||||
'requestInstallPackages',
|
||||
'appTrackingTransparency',
|
||||
'criticalAlerts',
|
||||
'accessNotificationPolicy',
|
||||
'bluetoothScan',
|
||||
'bluetoothAdvertise',
|
||||
'bluetoothConnect',
|
||||
'nearbyWifiDevices',
|
||||
TIM_t("视频"),
|
||||
'audio',
|
||||
'scheduleExactAlarm'
|
||||
];
|
||||
}
|
||||
|
||||
static String _permissionText(
|
||||
BuildContext context, String appName, int value) {
|
||||
final _prefix = TIM_t("需要授予");
|
||||
final _postfixList = <String>[
|
||||
TIM_t("日历"),
|
||||
TIM_t(" 相机权限,以正常使用拍摄图片视频、视频通话等功能。"),
|
||||
TIM_t("联系人"),
|
||||
TIM_t("位置"),
|
||||
'locationAlways',
|
||||
'locationWhenInUse',
|
||||
'mediaLibrary',
|
||||
TIM_t(" 麦克风权限,以正常使用发送语音消息、拍摄视频、音视频通话等功能。"),
|
||||
'phone',
|
||||
TIM_t(" 访问照片权限,以正常使用发送图片、视频等功能。"),
|
||||
TIM_t(" 访问相册写入权限,以正常使用存储图片、视频等功能。"),
|
||||
'reminders',
|
||||
'sensors',
|
||||
'sms',
|
||||
'speech',
|
||||
TIM_t(" 文件读写权限,以正常使用在聊天功能中的图片查看、选择能力和发送文件的能力。"),
|
||||
'ignoreBatteryOptimizations',
|
||||
'notification',
|
||||
'access_media_location',
|
||||
'activity_recognition',
|
||||
'unknown',
|
||||
'bluetooth',
|
||||
'manageExternalStorage',
|
||||
'systemAlertWindow',
|
||||
'requestInstallPackages',
|
||||
'appTrackingTransparency',
|
||||
'criticalAlerts',
|
||||
'accessNotificationPolicy',
|
||||
'bluetoothScan',
|
||||
'bluetoothAdvertise',
|
||||
'bluetoothConnect',
|
||||
'nearbyWifiDevices',
|
||||
TIM_t(" 访问相册中视频权限,以正常使用发送视频等功能。"),
|
||||
'audio',
|
||||
'scheduleExactAlarm'
|
||||
];
|
||||
return _prefix + appName + _postfixList[value];
|
||||
}
|
||||
|
||||
static Future<bool> checkPermission(
|
||||
BuildContext context,
|
||||
int value, [
|
||||
TUITheme? theme,
|
||||
bool isShowPermissionPage = true,
|
||||
]) async {
|
||||
final status = await Permission.byValue(value).status;
|
||||
if (status.isGranted || status.isLimited) {
|
||||
return true;
|
||||
}
|
||||
final bool? shouldRequestPermission = await showPermissionConfirmDialog(
|
||||
context,
|
||||
value,
|
||||
theme,
|
||||
isShowPermissionPage,
|
||||
);
|
||||
if (shouldRequestPermission != null && shouldRequestPermission) {
|
||||
final isGranted = await Permission.byValue(value).request().isGranted;
|
||||
_entry?.remove();
|
||||
_entry = null;
|
||||
return isGranted;
|
||||
}
|
||||
return shouldRequestPermission ?? false;
|
||||
}
|
||||
|
||||
static Future<bool> checkPermissionSetBefore(int value) async {
|
||||
Future<SharedPreferences> _prefs = SharedPreferences.getInstance();
|
||||
SharedPreferences prefs = await _prefs;
|
||||
final _hasPermissionSetBefore = prefs.getBool("permission$value");
|
||||
return _hasPermissionSetBefore ?? false;
|
||||
}
|
||||
|
||||
static Future<bool> setLocalPermission(int value) async {
|
||||
Future<SharedPreferences> _prefs = SharedPreferences.getInstance();
|
||||
SharedPreferences prefs = await _prefs;
|
||||
return await prefs.setBool("permission$value", true);
|
||||
}
|
||||
|
||||
static showPermissionRequestInfoDialog(BuildContext context, value) async {
|
||||
PackageInfo packageInfo = await PackageInfo.fromPlatform();
|
||||
String appName = packageInfo.appName;
|
||||
final entry = OverlayEntry(builder: (context) {
|
||||
return PermissionRequestInfo(
|
||||
appName: appName,
|
||||
removeOverLay: () {
|
||||
_entry?.remove();
|
||||
_entry = null;
|
||||
},
|
||||
permissionType: value,
|
||||
);
|
||||
});
|
||||
_entry = entry;
|
||||
Overlay.of(context).insert(entry);
|
||||
}
|
||||
|
||||
static Future<bool?> showPermissionConfirmDialog(BuildContext context, value,
|
||||
[TUITheme? theme, bool isShowPermissionPage = true]) async {
|
||||
final platformUtils = PlatformUtils();
|
||||
// 第一次直接走系统文案
|
||||
if (!await checkPermissionSetBefore(value)) {
|
||||
await setLocalPermission(value);
|
||||
if (platformUtils.isAndroid && isShowPermissionPage) {
|
||||
showPermissionRequestInfoDialog(context, value);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
PackageInfo packageInfo = await PackageInfo.fromPlatform();
|
||||
String appName = packageInfo.appName;
|
||||
final option2 = _names(context)[value];
|
||||
final permissionText = _permissionText(context, appName, value);
|
||||
|
||||
void closeDialog() {
|
||||
Navigator.of(context).pop(false);
|
||||
}
|
||||
|
||||
void getPermission() async {
|
||||
Navigator.of(context).pop(false);
|
||||
openAppSettings();
|
||||
}
|
||||
|
||||
return showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return platformUtils.isIOS
|
||||
? CupertinoAlertDialog(
|
||||
title: Text("“$appName”" +
|
||||
TIM_t_para(" 想访问您的{{option2}}", " 想访问您的$option2")(
|
||||
option2: option2)),
|
||||
content: Text(permissionText),
|
||||
actions: <Widget>[
|
||||
CupertinoDialogAction(
|
||||
child: Text(TIM_t("以后再说")),
|
||||
onPressed: closeDialog, // 关闭对话框
|
||||
),
|
||||
CupertinoDialogAction(
|
||||
child: Text(TIM_t("去开启")),
|
||||
onPressed: getPermission,
|
||||
),
|
||||
],
|
||||
)
|
||||
: AlertDialog(
|
||||
content: Text(permissionText),
|
||||
actions: <Widget>[
|
||||
const Divider(),
|
||||
SizedBox(
|
||||
height: 48,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextButton(
|
||||
child: Text(TIM_t("以后再说"),
|
||||
style: TextStyle(
|
||||
color: theme?.black ?? Colors.black,
|
||||
)),
|
||||
onPressed: closeDialog, // 关闭对话框
|
||||
),
|
||||
),
|
||||
const VerticalDivider(),
|
||||
Expanded(
|
||||
child: TextButton(
|
||||
child: Text(TIM_t("去开启"),
|
||||
style: TextStyle(
|
||||
color: theme?.black ?? Colors.black,
|
||||
)),
|
||||
onPressed: getPermission,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import 'dart:io';
|
||||
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||
|
||||
class PlatformUtils {
|
||||
PlatformUtils._internal();
|
||||
static late bool _isAndroid;
|
||||
static late bool _isIos;
|
||||
static late bool _isMobile;
|
||||
static late bool _isWeb;
|
||||
static late bool _isWindows;
|
||||
static late bool _isMacOS;
|
||||
static late bool _isLinux;
|
||||
static late bool _isDesktop;
|
||||
static bool _isInstantiation = false;
|
||||
|
||||
factory PlatformUtils() {
|
||||
if (!_isInstantiation) {
|
||||
_isAndroid = !kIsWeb && Platform.isAndroid;
|
||||
_isIos = !kIsWeb && Platform.isIOS;
|
||||
_isMobile = _isAndroid || _isIos;
|
||||
_isWindows = !kIsWeb && Platform.isWindows;
|
||||
_isMacOS = !kIsWeb && Platform.isMacOS;
|
||||
_isLinux = !kIsWeb && Platform.isLinux;
|
||||
_isDesktop = _isMacOS || _isWindows || _isLinux;
|
||||
_isWeb = kIsWeb;
|
||||
_isInstantiation = true;
|
||||
}
|
||||
|
||||
return _instance;
|
||||
}
|
||||
|
||||
static late final PlatformUtils _instance = PlatformUtils._internal();
|
||||
|
||||
get isAndroid {
|
||||
return _isAndroid;
|
||||
}
|
||||
|
||||
get isWeb {
|
||||
return _isWeb;
|
||||
}
|
||||
|
||||
get isIOS {
|
||||
return _isIos;
|
||||
}
|
||||
|
||||
get isWindows {
|
||||
return _isWindows;
|
||||
}
|
||||
|
||||
get isMacOS {
|
||||
return _isMacOS;
|
||||
}
|
||||
|
||||
get isMobile {
|
||||
return _isMobile;
|
||||
}
|
||||
|
||||
bool get isDesktop => _isDesktop;
|
||||
|
||||
bool get isLinux => _isLinux;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
|
||||
class FadeRoute extends PageRouteBuilder {
|
||||
final Widget page;
|
||||
FadeRoute({required this.page})
|
||||
: super(
|
||||
pageBuilder: (
|
||||
BuildContext context,
|
||||
Animation<double> animation,
|
||||
Animation<double> secondaryAnimation,
|
||||
) =>
|
||||
page,
|
||||
transitionsBuilder: (
|
||||
BuildContext context,
|
||||
Animation<double> animation,
|
||||
Animation<double> secondaryAnimation,
|
||||
Widget child,
|
||||
) =>
|
||||
FadeTransition(
|
||||
opacity: animation,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/platform.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class ScreenshotHelper {
|
||||
static Future<String?> captureScreen() async {
|
||||
await requestScreenRecordingPermission();
|
||||
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 = 'screenshot_${uuid.v4()}.png';
|
||||
final filePath = '$directory/$fileName';
|
||||
if (Platform.isMacOS) {
|
||||
// 在macOS平台上使用screencapture工具
|
||||
final result = await Process.run(
|
||||
'screencapture',
|
||||
['-i', '-s', '-o', filePath],
|
||||
);
|
||||
if (result.exitCode == 0) {
|
||||
return filePath;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} else if (Platform.isWindows) {
|
||||
// 在Windows平台上使用snippingtool工具
|
||||
final result = await Process.run(
|
||||
'snippingtool',
|
||||
['/clip', filePath],
|
||||
);
|
||||
if (result.exitCode == 0) {
|
||||
return filePath;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
// 不支持的平台
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<bool> requestScreenRecordingPermission() async {
|
||||
if (Platform.isMacOS) {
|
||||
final result = await Process.run(
|
||||
'sh', ['-c', 'echo ${Platform.environment['USER']}']);
|
||||
final username = result.stdout.toString().trim();
|
||||
const script =
|
||||
'tell application "System Events" to return exists (processes where name is "ControlCenter")';
|
||||
final process = await Process.run('osascript', ['-e', script]);
|
||||
final isControlCenterRunning = process.stdout.toString().trim() == 'true';
|
||||
|
||||
if (!isControlCenterRunning) {
|
||||
await Process.run('open', ['-a', 'ControlCenter']);
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
}
|
||||
|
||||
final script2 = 'tell application "ControlCenter" to activate\n'
|
||||
'tell application "System Events"\n'
|
||||
' tell process "ControlCenter"\n'
|
||||
' click menu item "Screen Recording" of menu "File" of menu bar 1\n'
|
||||
' delay 0.5\n'
|
||||
' keystroke "$username" & return\n'
|
||||
' end tell\n'
|
||||
'end tell\n';
|
||||
await Process.run('osascript', ['-e', script2]);
|
||||
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
final isScreenRecordingEnabled = await SystemChannels.platform
|
||||
.invokeMethod<bool>('isScreenRecordingEnabled');
|
||||
return isScreenRecordingEnabled ?? false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<Size> getImageSize(String imagePath) async {
|
||||
final bytes = await File(imagePath).readAsBytes();
|
||||
final completer = Completer<Size>();
|
||||
final imageStream =
|
||||
Image.memory(bytes).image.resolve(ImageConfiguration.empty);
|
||||
imageStream.addListener(ImageStreamListener((imageInfo, _) {
|
||||
completer.complete(Size(
|
||||
imageInfo.image.width.toDouble(), imageInfo.image.height.toDouble()));
|
||||
}));
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
// ignore_for_file: constant_identifier_names
|
||||
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/platform.dart';
|
||||
|
||||
enum DeviceType { Desktop, Mobile }
|
||||
|
||||
class FormFactor {
|
||||
static double desktop = 900;
|
||||
static double handset = 300;
|
||||
}
|
||||
|
||||
class TUIKitScreenUtils {
|
||||
static DeviceType? deviceType;
|
||||
|
||||
/// Although specifying the `BuildContext` is optional, providing it can prevent layout issues when this widget renders immediately after the app launch.
|
||||
/// If this widget needs to be used at the moment the app launches, it's recommended to provide the `BuildContext` here.
|
||||
static DeviceType getFormFactor([BuildContext? context]) {
|
||||
if (deviceType != null) return deviceType!;
|
||||
|
||||
if (PlatformUtils().isWeb) {
|
||||
final win = WidgetsBinding.instance.platformDispatcher.views.first;
|
||||
final size = win.physicalSize;
|
||||
final screenWidth = size.width / win.devicePixelRatio;
|
||||
final screenHeight = size.height / win.devicePixelRatio;
|
||||
|
||||
final diagonalInInches =
|
||||
sqrt(pow(screenWidth, 2) + pow(screenHeight, 2)) / 96.0;
|
||||
|
||||
deviceType = diagonalInInches < 11.0 ? DeviceType.Mobile : DeviceType.Desktop;
|
||||
return deviceType ?? DeviceType.Mobile;
|
||||
}else{
|
||||
if(context != null){
|
||||
double deviceWidth = MediaQuery.of(context).size.width;
|
||||
double deviceHeight = MediaQuery.of(context).size.height;
|
||||
|
||||
if (deviceWidth > FormFactor.desktop || deviceWidth > deviceHeight * 1.1) {
|
||||
deviceType = DeviceType.Desktop;
|
||||
} else if (deviceWidth > FormFactor.handset) {
|
||||
deviceType = DeviceType.Mobile;
|
||||
}
|
||||
return deviceType ?? DeviceType.Mobile;
|
||||
}else{
|
||||
return DeviceType.Mobile;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static Widget getDeviceWidget({
|
||||
/// Although specifying the `BuildContext` is optional, providing it can prevent layout issues when this widget renders immediately after the app launch.
|
||||
/// If this widget needs to be used at the moment the app launches, it's recommended to provide the `BuildContext` here.
|
||||
BuildContext? context,
|
||||
required Widget defaultWidget,
|
||||
Widget? desktopWidget,
|
||||
Widget? mobileWidget,
|
||||
}) {
|
||||
deviceType ??= getFormFactor(context);
|
||||
if (deviceType == DeviceType.Desktop) return desktopWidget ?? defaultWidget;
|
||||
return mobileWidget ?? defaultWidget;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter_plugin_record_plus/const/play_state.dart';
|
||||
import 'package:flutter_plugin_record_plus/const/response.dart';
|
||||
import 'package:flutter_plugin_record_plus/index.dart';
|
||||
import 'package:just_audio/just_audio.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/import_proxy/import_proxy.dart';
|
||||
|
||||
typedef PlayStateListener = void Function(PlayState playState);
|
||||
typedef SoundInterruptListener = void Function();
|
||||
typedef ResponseListener = void Function(RecordResponse recordResponse);
|
||||
|
||||
class SoundPlayer {
|
||||
final ImportProxy importProxy = ImportProxy();
|
||||
static final FlutterPluginRecord _recorder = FlutterPluginRecord();
|
||||
static SoundInterruptListener? _soundInterruptListener;
|
||||
static bool isInit = false;
|
||||
static final AudioPlayer _audioPlayer = AudioPlayer();
|
||||
|
||||
static initSoundPlayer() {
|
||||
if (!isInit) {
|
||||
_recorder.init();
|
||||
// AudioPlayer.global.setGlobalAudioContext(const AudioContext());
|
||||
isInit = true;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> play({required String url}) async {
|
||||
_audioPlayer.stop();
|
||||
if (_soundInterruptListener != null) {
|
||||
_soundInterruptListener!();
|
||||
}
|
||||
await _audioPlayer.setUrl(url);
|
||||
|
||||
await _audioPlayer.play();
|
||||
}
|
||||
|
||||
static stop() {
|
||||
_audioPlayer.stop();
|
||||
|
||||
}
|
||||
|
||||
static dispose() {
|
||||
_audioPlayer.dispose();
|
||||
_recorder.dispose();
|
||||
}
|
||||
|
||||
static StreamSubscription<PlayerState> playStateListener({required void Function(PlayerState)? listener}) => _audioPlayer.playerStateStream.listen(listener);
|
||||
|
||||
static setSoundInterruptListener(SoundInterruptListener listener) {
|
||||
_soundInterruptListener = listener;
|
||||
}
|
||||
|
||||
static removeSoundInterruptListener() {
|
||||
_soundInterruptListener = null;
|
||||
}
|
||||
|
||||
static StreamSubscription<RecordResponse> responseListener(ResponseListener listener) => _recorder.response.listen(listener);
|
||||
|
||||
static StreamSubscription<RecordResponse> responseFromAmplitudeListener(ResponseListener listener) => _recorder.responseFromAmplitude.listen(listener);
|
||||
|
||||
static startRecord() {
|
||||
_recorder.start();
|
||||
}
|
||||
|
||||
static stopRecord() {
|
||||
_recorder.stop();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
// ignore_for_file: prefer_typing_uninitialized_variables
|
||||
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:tencent_chat_i18n_tool/tencent_chat_i18n_tool.dart';
|
||||
|
||||
class TimeAgo {
|
||||
List<String> dayMap() {
|
||||
return [
|
||||
TIM_t("昨天"),
|
||||
TIM_t("前天"),
|
||||
];
|
||||
}
|
||||
|
||||
List<String> weekdayMap() {
|
||||
return ['', TIM_t("星期一"), TIM_t("星期二"), TIM_t("星期三"), TIM_t("星期四"), TIM_t("星期五"), TIM_t("星期六"), TIM_t("星期天")];
|
||||
}
|
||||
|
||||
String getYearMonthDate(DateTime dateTime) {
|
||||
String month = dateTime.month.toString();
|
||||
String date = dateTime.day.toString();
|
||||
return dateTime.year.toString() + '/' + (month.length == 1 ? '0' : '') + month + '/' + (date.length == 1 ? '0' : '') + date;
|
||||
}
|
||||
|
||||
String getMonthDate(DateTime dateTime) {
|
||||
String month = dateTime.month.toString();
|
||||
String date = dateTime.day.toString();
|
||||
return (month.length == 1 ? '0' : '') + month + '/' + (date.length == 1 ? '0' : '') + date;
|
||||
}
|
||||
|
||||
static String getMonth(DateTime dateTime) {
|
||||
String month = dateTime.month.toString();
|
||||
return (month.length == 1 ? '0' : '') + month;
|
||||
}
|
||||
|
||||
static String getDay(DateTime dateTime) {
|
||||
String day = dateTime.day.toString();
|
||||
return (day.length == 1 ? '0' : '') + day;
|
||||
}
|
||||
|
||||
String? getTimeStringForChat(int timeStamp) {
|
||||
final DateTime date = DateTime.fromMillisecondsSinceEpoch(timeStamp * 1000);
|
||||
final DateTime epochLimit = DateTime.utc(1971);
|
||||
|
||||
if (date.isBefore(epochLimit)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final Duration duration = DateTime.now().difference(date);
|
||||
final int diffDays = duration.inDays + (duration.inMinutes > DateTime.now().difference(DateTime(DateTime.now().year, DateTime.now().month, DateTime.now().day)).inMinutes ? 1 : 0);
|
||||
final int diffMinutes = duration.inMinutes;
|
||||
|
||||
var res;
|
||||
|
||||
// 一个礼拜之内
|
||||
if (diffDays > 0 && diffDays < 7) {
|
||||
if (diffDays <= 2) {
|
||||
res = dayMap()[diffDays - 1];
|
||||
} else {
|
||||
res = weekdayMap()[date.weekday];
|
||||
}
|
||||
} else if (diffDays >= 7) {
|
||||
//当年内
|
||||
if (date.year == DateTime.now().year) {
|
||||
res = getMonthDate(date);
|
||||
} else {
|
||||
res = getYearMonthDate(date);
|
||||
}
|
||||
} else {
|
||||
if (diffMinutes > 1) {
|
||||
if (diffMinutes < 60) {
|
||||
final String option2 = diffMinutes.toString();
|
||||
res = TIM_t_para("{{option2}} 分钟前", "$option2 分钟前")(option2: option2);
|
||||
} else {
|
||||
res = "${date.hour}:${date.minute < 10 ? "0" + date.minute.toString() : date.minute}";
|
||||
// res = "$prefix $timeStr";
|
||||
}
|
||||
} else {
|
||||
res = TIM_t("现在");
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
String getTimeForMessage(int timeStamp) {
|
||||
var nowTime = DateTime.now();
|
||||
nowTime = DateTime(nowTime.year, nowTime.month, nowTime.day);
|
||||
var ftime = DateTime.fromMillisecondsSinceEpoch(timeStamp * 1000);
|
||||
// var preFix = ftime.hour >= 12 ? TIM_t("下午") : TIM_t("上午");
|
||||
final timeStr = DateFormat('HH:mm').format(ftime); // Use 'HH:mm' for 24-hour format
|
||||
// 一年外 年月日 + 时间 (24小时制)
|
||||
if (nowTime.year != ftime.year) {
|
||||
return '${DateFormat('yyyy-MM-dd').format(ftime)} $timeStr';
|
||||
}
|
||||
// 一年内一周外 月日 + 时间 (24小时制)
|
||||
if (ftime.isBefore(nowTime.subtract(const Duration(days: 6)))) {
|
||||
return '${DateFormat('MM-dd').format(ftime)} $timeStr';
|
||||
}
|
||||
// 一周内一天外 星期 + 时间 (24小时制)
|
||||
if (ftime.isBefore(nowTime.subtract(const Duration(days: 1)))) {
|
||||
return '${weekdayMap()[ftime.weekday]} $timeStr';
|
||||
}
|
||||
// 昨日 昨天 + 时间 (24小时制)
|
||||
if (nowTime.day != ftime.day) {
|
||||
String option2 = timeStr;
|
||||
return TIM_t_para("昨天 {{option2}}", "昨天 $option2")(option2: option2);
|
||||
}
|
||||
// 同年月日 时间 (24小时制)
|
||||
return timeStr;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:tencent_chat_i18n_tool/tencent_chat_i18n_tool.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/enum/friend_type_enum.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_uikit/data_services/core/tim_uikit_wide_modal_operation_key.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/wide_popup.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/life_cycle/add_friend_life_cycle.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/core/core_services_implements.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/friendShip/friendship_services.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/services_locatar.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitAddFriend/tim_uikit_send_application.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/avatar.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/tui_theme.dart';
|
||||
|
||||
class TIMUIKitAddFriend extends StatefulWidget {
|
||||
final bool? isShowDefaultGroup;
|
||||
|
||||
/// You may navigate to user profile page, if friendship relationship exists.
|
||||
final Function(String userID) onTapAlreadyFriendsItem;
|
||||
|
||||
/// The life cycle hooks for adding friends and contact business logic
|
||||
final AddFriendLifeCycle? lifeCycle;
|
||||
|
||||
/// The callback function to close the widget upon completion by the parent component.
|
||||
final VoidCallback? closeFunc;
|
||||
|
||||
const TIMUIKitAddFriend(
|
||||
{Key? key,
|
||||
this.isShowDefaultGroup = false,
|
||||
this.lifeCycle,
|
||||
required this.onTapAlreadyFriendsItem,
|
||||
this.closeFunc})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _TIMUIKitAddFriendState();
|
||||
}
|
||||
|
||||
class _TIMUIKitAddFriendState extends TIMUIKitState<TIMUIKitAddFriend> {
|
||||
final TextEditingController _controller = TextEditingController();
|
||||
final CoreServicesImpl _coreServicesImpl = serviceLocator<CoreServicesImpl>();
|
||||
final FriendshipServices _friendshipServices = serviceLocator<FriendshipServices>();
|
||||
final TUISelfInfoViewModel _selfInfoViewModel = serviceLocator<TUISelfInfoViewModel>();
|
||||
final FocusNode _focusNode = FocusNode();
|
||||
bool isFocused = false;
|
||||
bool showResult = false;
|
||||
List<V2TimUserFullInfo>? searchResult;
|
||||
|
||||
Widget _searchResultItemBuilder(V2TimUserFullInfo friendInfo, TUITheme theme) {
|
||||
final isDesktopScreen = TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop;
|
||||
|
||||
final faceUrl = friendInfo.faceUrl ?? "";
|
||||
final userID = friendInfo.userID ?? "";
|
||||
final String showName =
|
||||
((friendInfo.nickName != null && friendInfo.nickName!.isNotEmpty) ? friendInfo.nickName : userID) ?? "";
|
||||
return InkWell(
|
||||
onTap: () async {
|
||||
final checkFriend = await _friendshipServices
|
||||
.checkFriend(userIDList: [userID], checkType: FriendTypeEnum.V2TIM_FRIEND_TYPE_SINGLE);
|
||||
if (checkFriend != null) {
|
||||
final res = checkFriend.first;
|
||||
if (res.resultCode == 0 && res.resultType != 0) {
|
||||
widget.onTapAlreadyFriendsItem(userID);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (userID == _selfInfoViewModel.loginInfo?.userID) {
|
||||
widget.onTapAlreadyFriendsItem(userID);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isDesktopScreen) {
|
||||
if (widget.closeFunc != null) {
|
||||
widget.closeFunc!();
|
||||
}
|
||||
TUIKitWidePopup.showPopupWindow(
|
||||
operationKey: TUIKitWideModalOperationKey.addFriend,
|
||||
context: context,
|
||||
width: MediaQuery.of(context).size.width * 0.3,
|
||||
height: MediaQuery.of(context).size.width * 0.4,
|
||||
title: TIM_t("添加好友"),
|
||||
child: (closeFuncSendApplication) => SendApplication(
|
||||
lifeCycle: widget.lifeCycle,
|
||||
isShowDefaultGroup: widget.isShowDefaultGroup ?? false,
|
||||
friendInfo: friendInfo,
|
||||
model: _selfInfoViewModel),
|
||||
);
|
||||
} else {
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => SendApplication(
|
||||
lifeCycle: widget.lifeCycle,
|
||||
isShowDefaultGroup: widget.isShowDefaultGroup ?? false,
|
||||
friendInfo: friendInfo,
|
||||
model: _selfInfoViewModel)));
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
child: Row(
|
||||
// crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
width: isDesktopScreen ? 38 : 48,
|
||||
height: isDesktopScreen ? 38 : 48,
|
||||
margin: const EdgeInsets.only(right: 16),
|
||||
child: Avatar(faceUrl: faceUrl, showName: showName),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
showName,
|
||||
style: TextStyle(color: theme.darkTextColor, fontSize: isDesktopScreen ? 16 : 18),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 4,
|
||||
),
|
||||
Text(
|
||||
"ID: $userID",
|
||||
style: TextStyle(fontSize: 12, color: theme.weakTextColor),
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _searchResultBuilder(List<V2TimUserFullInfo>? searchResult, TUITheme theme) {
|
||||
final noResult = searchResult == null || searchResult.isEmpty;
|
||||
if (noResult) {
|
||||
return [
|
||||
Container(
|
||||
margin: const EdgeInsets.only(top: 20),
|
||||
child: Center(
|
||||
child: Text(TIM_t("该用户不存在"), style: TextStyle(color: theme.weakTextColor, fontSize: 14)),
|
||||
),
|
||||
)
|
||||
];
|
||||
}
|
||||
return searchResult.map((e) => _searchResultItemBuilder(e, theme)).toList();
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_focusNode.addListener(() {
|
||||
final _isFocused = _focusNode.hasFocus;
|
||||
isFocused = _isFocused;
|
||||
setState(() {});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
searchFriend(String params) async {
|
||||
final response = await _coreServicesImpl.getUsersInfo(userIDList: [params]);
|
||||
if (response.code == 0) {
|
||||
setState(() {
|
||||
searchResult = response.data;
|
||||
});
|
||||
} else {
|
||||
setState(() {
|
||||
searchResult = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final TUITheme theme = value.theme;
|
||||
return MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider.value(value: _selfInfoViewModel),
|
||||
],
|
||||
builder: (BuildContext context, Widget? w) {
|
||||
return Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 20),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
autofocus: true,
|
||||
focusNode: _focusNode,
|
||||
controller: _controller,
|
||||
onChanged: (value) {
|
||||
if (value.trim().isEmpty) {
|
||||
setState(() {
|
||||
showResult = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
textInputAction: TextInputAction.search,
|
||||
onSubmitted: (_) {
|
||||
final searchParams = _controller.text;
|
||||
if (searchParams.trim().isNotEmpty) {
|
||||
searchFriend(searchParams);
|
||||
showResult = true;
|
||||
_focusNode.requestFocus();
|
||||
setState(() {});
|
||||
}
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: Icon(
|
||||
Icons.search_outlined,
|
||||
color: theme.weakTextColor,
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: const BorderSide(
|
||||
width: 0,
|
||||
style: BorderStyle.none,
|
||||
),
|
||||
),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
hintStyle: TextStyle(
|
||||
color: theme.weakTextColor,
|
||||
),
|
||||
fillColor: theme.inputFillColor,
|
||||
filled: true,
|
||||
hintText: TIM_t("搜索用户 ID")),
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (showResult)
|
||||
Expanded(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: _searchResultBuilder(searchResult, theme),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:tencent_chat_i18n_tool/tencent_chat_i18n_tool.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/enum/friend_type_enum.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_uikit/tencent_cloud_chat_uikit.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/life_cycle/add_friend_life_cycle.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/friendShip/friendship_services.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/services_locatar.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/avatar.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/tui_theme.dart';
|
||||
|
||||
class SendApplication extends StatefulWidget {
|
||||
final V2TimUserFullInfo friendInfo;
|
||||
final TUISelfInfoViewModel model;
|
||||
final bool? isShowDefaultGroup;
|
||||
final AddFriendLifeCycle? lifeCycle;
|
||||
|
||||
const SendApplication(
|
||||
{Key? key, this.lifeCycle, required this.friendInfo, required this.model, this.isShowDefaultGroup = false})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _SendApplicationState();
|
||||
}
|
||||
|
||||
class _SendApplicationState extends TIMUIKitState<SendApplication> {
|
||||
final TextEditingController _verficationController = TextEditingController();
|
||||
final TextEditingController _nickNameController = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final showName = widget.model.loginInfo?.nickName ?? widget.model.loginInfo?.userID;
|
||||
_verficationController.text = "我是: $showName";
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final TUITheme theme = value.theme;
|
||||
final FriendshipServices _friendshipServices = serviceLocator<FriendshipServices>();
|
||||
|
||||
final faceUrl = widget.friendInfo.faceUrl ?? "";
|
||||
final userID = widget.friendInfo.userID ?? "";
|
||||
final String showName = ((widget.friendInfo.nickName != null && widget.friendInfo.nickName!.isNotEmpty)
|
||||
? widget.friendInfo.nickName
|
||||
: userID) ??
|
||||
"";
|
||||
final option2 = widget.friendInfo.selfSignature ?? "";
|
||||
|
||||
Widget sendApplicationBody() {
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
color: theme.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
margin: const EdgeInsets.only(right: 12),
|
||||
child: Avatar(faceUrl: faceUrl, showName: showName),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
showName,
|
||||
style: TextStyle(color: theme.darkTextColor, fontSize: 18),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 4,
|
||||
),
|
||||
Text(
|
||||
"ID: $userID",
|
||||
style: TextStyle(fontSize: 13, color: theme.weakTextColor),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 4,
|
||||
),
|
||||
if (TencentUtils.checkString(option2) != null)
|
||||
Text(
|
||||
TIM_t_para("个性签名: {{option2}}", "个性签名: $option2")(option2: option2),
|
||||
style: TextStyle(fontSize: 13, color: theme.weakTextColor),
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 16.0),
|
||||
child: Text(
|
||||
TIM_t("填写验证信息"),
|
||||
style: TextStyle(fontSize: 16, color: theme.weakTextColor),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
margin: const EdgeInsets.only(top: 6, bottom: 12),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
color: theme.white,
|
||||
child: TextField(
|
||||
// minLines: 1,
|
||||
maxLines: 4,
|
||||
controller: _verficationController,
|
||||
keyboardType: TextInputType.multiline,
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
hintStyle: TextStyle(color: theme.textgrey),
|
||||
hintText: '',
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 16.0),
|
||||
child: Text(
|
||||
TIM_t("请填写备注"),
|
||||
style: TextStyle(fontSize: 16, color: theme.weakTextColor),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
color: theme.white,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
margin: const EdgeInsets.only(top: 6),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
TIM_t("备注"),
|
||||
style: TextStyle(color: theme.darkTextColor, fontSize: 16),
|
||||
),
|
||||
SizedBox(
|
||||
width: 50,
|
||||
child: TextField(
|
||||
controller: _nickNameController,
|
||||
decoration: InputDecoration(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
border: InputBorder.none,
|
||||
hintStyle: TextStyle(
|
||||
color: theme.textgrey,
|
||||
),
|
||||
hintText: '',
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(
|
||||
height: 1,
|
||||
),
|
||||
if (widget.isShowDefaultGroup == true)
|
||||
Container(
|
||||
color: theme.white,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
TIM_t("分组"),
|
||||
style: TextStyle(color: theme.darkTextColor, fontSize: 16),
|
||||
),
|
||||
Text(
|
||||
TIM_t("我的好友"),
|
||||
style: TextStyle(color: theme.darkTextColor, fontSize: 16),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
color: theme.white,
|
||||
width: double.infinity,
|
||||
margin: const EdgeInsets.only(top: 10),
|
||||
child: TextButton(
|
||||
onPressed: () async {
|
||||
final remark = _nickNameController.text;
|
||||
final addWording = _verficationController.text;
|
||||
final friendGroup = TIM_t("我的好友");
|
||||
|
||||
if (widget.lifeCycle?.shouldAddFriend != null &&
|
||||
await widget.lifeCycle!.shouldAddFriend(userID, remark, friendGroup, addWording, context) ==
|
||||
false) {
|
||||
return;
|
||||
}
|
||||
|
||||
_friendshipServices.addFriend(
|
||||
userID: userID,
|
||||
addType: FriendTypeEnum.V2TIM_FRIEND_TYPE_BOTH,
|
||||
remark: remark,
|
||||
addWording: addWording,
|
||||
friendGroup: friendGroup);
|
||||
},
|
||||
child: Text(TIM_t("发送"))),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return TUIKitScreenUtils.getDeviceWidget(
|
||||
context: context,
|
||||
desktopWidget: Container(
|
||||
padding: const EdgeInsets.only(top: 10),
|
||||
color: theme.weakBackgroundColor,
|
||||
child: sendApplicationBody(),
|
||||
),
|
||||
defaultWidget: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
TIM_t("添加好友"),
|
||||
style: TextStyle(color: theme.appbarTextColor, fontSize: 17),
|
||||
),
|
||||
shadowColor: theme.white,
|
||||
backgroundColor: theme.appbarBgColor ?? theme.primaryColor,
|
||||
iconTheme: IconThemeData(
|
||||
color: theme.appbarTextColor,
|
||||
),
|
||||
),
|
||||
body: sendApplicationBody(),
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,301 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:tencent_chat_i18n_tool/tencent_chat_i18n_tool.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/enum/group_type.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_conversation.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_info.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_group_info.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/core/tim_uikit_wide_modal_operation_key.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/wide_popup.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_callback.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/life_cycle/add_group_life_cycle.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_friendship_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/conversation/conversation_services.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/group/group_services.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/services_locatar.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/tui_theme.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitAddGroup/tim_uikit_send_application.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/avatar.dart';
|
||||
|
||||
class TIMUIKitAddGroup extends StatefulWidget {
|
||||
/// The life cycle hooks for adding group business logic
|
||||
final AddGroupLifeCycle? lifeCycle;
|
||||
|
||||
/// Navigate to group chat, if user is already a member of the current group.
|
||||
final Function(String groupID, V2TimConversation conversation) onTapExistGroup;
|
||||
|
||||
final VoidCallback? closeFunc;
|
||||
|
||||
const TIMUIKitAddGroup({Key? key, this.lifeCycle, required this.onTapExistGroup, this.closeFunc}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _TIMUIKitAddGroupState();
|
||||
}
|
||||
|
||||
class _TIMUIKitAddGroupState extends TIMUIKitState<TIMUIKitAddGroup> {
|
||||
final TextEditingController _controller = TextEditingController();
|
||||
final GroupServices _groupServices = serviceLocator<GroupServices>();
|
||||
final ConversationService _conversationService = serviceLocator<ConversationService>();
|
||||
final TUIFriendShipViewModel friendShipViewModel = serviceLocator<TUIFriendShipViewModel>();
|
||||
List<V2TimGroupInfo>? _addedGroupList;
|
||||
List<V2TimGroupInfo>? groupResult = [];
|
||||
final FocusNode _focusNode = FocusNode();
|
||||
bool isFocused = false;
|
||||
bool showResult = false;
|
||||
|
||||
String _getGroupType(String type) {
|
||||
String groupType;
|
||||
switch (type) {
|
||||
case GroupType.AVChatRoom:
|
||||
groupType = TIM_t("聊天室");
|
||||
break;
|
||||
case GroupType.Meeting:
|
||||
groupType = TIM_t("会议群");
|
||||
break;
|
||||
case GroupType.Public:
|
||||
groupType = TIM_t("公开群");
|
||||
break;
|
||||
case GroupType.Work:
|
||||
groupType = TIM_t("工作群");
|
||||
break;
|
||||
case GroupType.Community:
|
||||
groupType = TIM_t("社群");
|
||||
break;
|
||||
default:
|
||||
groupType = TIM_t("未知群");
|
||||
break;
|
||||
}
|
||||
return groupType;
|
||||
}
|
||||
|
||||
Widget _searchResultItemBuilder(V2TimGroupInfo groupInfo, TUITheme theme) {
|
||||
final faceUrl = groupInfo.faceUrl ?? "";
|
||||
final groupID = groupInfo.groupID;
|
||||
final showName = groupInfo.groupName ?? groupID;
|
||||
final groupType = _getGroupType(groupInfo.groupType);
|
||||
final isDesktopScreen = TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop;
|
||||
return InkWell(
|
||||
onTap: () async {
|
||||
final V2TimConversation? groupConversation = await getGroupConversation(groupID);
|
||||
if (groupConversation != null) {
|
||||
onTIMCallback(TIMCallback(type: TIMCallbackType.INFO, infoRecommendText: TIM_t("您已是群成员"), infoCode: 6660202));
|
||||
if (widget.closeFunc != null) {
|
||||
widget.closeFunc!();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (isDesktopScreen) {
|
||||
if (widget.closeFunc != null) {
|
||||
widget.closeFunc!();
|
||||
}
|
||||
TUIKitWidePopup.showPopupWindow(
|
||||
operationKey: TUIKitWideModalOperationKey.addGroup,
|
||||
context: context,
|
||||
width: MediaQuery.of(context).size.width * 0.3,
|
||||
height: MediaQuery.of(context).size.width * 0.4,
|
||||
title: TIM_t("添加群聊"),
|
||||
child: (closeFuncSendApplication) => SendJoinGroupApplication(
|
||||
lifeCycle: widget.lifeCycle,
|
||||
groupInfo: groupInfo,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => SendJoinGroupApplication(
|
||||
lifeCycle: widget.lifeCycle,
|
||||
groupInfo: groupInfo,
|
||||
)));
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
child: Row(
|
||||
// crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
width: isDesktopScreen ? 38 : 48,
|
||||
height: isDesktopScreen ? 38 : 48,
|
||||
margin: const EdgeInsets.only(right: 16),
|
||||
child: Avatar(faceUrl: faceUrl, showName: showName),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
showName,
|
||||
style: TextStyle(fontSize: isDesktopScreen ? 16 : 18),
|
||||
),
|
||||
Text(
|
||||
"ID: $groupID",
|
||||
style: TextStyle(fontSize: 12, color: theme.weakTextColor),
|
||||
),
|
||||
Text(
|
||||
"群类型: $groupType",
|
||||
style: TextStyle(fontSize: 12, color: theme.weakTextColor),
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _searchResultBuilder(List<V2TimGroupInfo>? searchResult, TUITheme theme) {
|
||||
final noResult = searchResult != null && searchResult.isEmpty;
|
||||
if (noResult) {
|
||||
return [
|
||||
Container(
|
||||
margin: const EdgeInsets.only(top: 20),
|
||||
child: Center(
|
||||
child: Text(TIM_t("该群聊不存在"), style: TextStyle(color: theme.weakTextColor, fontSize: 14)),
|
||||
),
|
||||
)
|
||||
];
|
||||
}
|
||||
return searchResult?.map((e) => _searchResultItemBuilder(e, theme)).toList() ?? [];
|
||||
}
|
||||
|
||||
Future<V2TimConversation?> getGroupConversation(String groupID) async {
|
||||
if (_addedGroupList == null || _addedGroupList!.isEmpty) {
|
||||
_addedGroupList = await _groupServices.getJoinedGroupList();
|
||||
}
|
||||
try {
|
||||
if ((_addedGroupList?.firstWhere((groupItem) {
|
||||
return groupItem.groupID == groupID;
|
||||
})) !=
|
||||
null) {
|
||||
V2TimConversation? conversation;
|
||||
conversation = await _conversationService.getConversationListByConversationId(convID: "group_$groupID");
|
||||
if (conversation == null) {
|
||||
await friendShipViewModel.loadGroupListData();
|
||||
if (friendShipViewModel.groupList.indexWhere((element) => element.groupID == groupID) > -1) {
|
||||
final V2TimGroupInfo groupInfo =
|
||||
friendShipViewModel.groupList.firstWhere((element) => element.groupID == groupID);
|
||||
conversation = V2TimConversation(
|
||||
conversationID: "group_$groupID",
|
||||
type: 2,
|
||||
groupID: groupID,
|
||||
showName: groupInfo.groupName,
|
||||
groupType: groupInfo.groupType,
|
||||
);
|
||||
}
|
||||
}
|
||||
return conversation;
|
||||
}
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_focusNode.addListener(() {
|
||||
final _isFocused = _focusNode.hasFocus;
|
||||
isFocused = _isFocused;
|
||||
setState(() {});
|
||||
});
|
||||
initGroupList();
|
||||
}
|
||||
|
||||
void initGroupList() async {
|
||||
// Get the joined group list in previous
|
||||
_addedGroupList = await _groupServices.getJoinedGroupList();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
searchGroup(String params) async {
|
||||
final res = await _groupServices.getGroupsInfo(groupIDList: [params]);
|
||||
if (res != null) {
|
||||
setState(() {
|
||||
groupResult = res.where((e) => e.resultCode == 0).map((e) => e.groupInfo!).toList();
|
||||
});
|
||||
} else {
|
||||
setState(() {
|
||||
groupResult = [];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final TUITheme theme = value.theme;
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 20),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
autofocus: true,
|
||||
focusNode: _focusNode,
|
||||
controller: _controller,
|
||||
onChanged: (value) {
|
||||
if (value.trim().isEmpty) {
|
||||
setState(() {
|
||||
showResult = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
textInputAction: TextInputAction.search,
|
||||
onSubmitted: (_) {
|
||||
final searchParams = _controller.text;
|
||||
if (searchParams.trim().isNotEmpty) {
|
||||
searchGroup(searchParams);
|
||||
showResult = true;
|
||||
_focusNode.requestFocus();
|
||||
setState(() {});
|
||||
}
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: Icon(
|
||||
Icons.search_outlined,
|
||||
color: theme.weakTextColor,
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: const BorderSide(
|
||||
width: 0,
|
||||
style: BorderStyle.none,
|
||||
),
|
||||
),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
hintStyle: TextStyle(
|
||||
color: theme.weakTextColor,
|
||||
),
|
||||
fillColor: theme.inputFillColor,
|
||||
filled: true,
|
||||
hintText: TIM_t("搜索群ID")),
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (showResult)
|
||||
Expanded(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: _searchResultBuilder(groupResult, theme),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:tencent_chat_i18n_tool/tencent_chat_i18n_tool.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/enum/group_type.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_callback.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_callback.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_uikit/ui/utils/screen_utils.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/life_cycle/add_group_life_cycle.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/core/core_services_implements.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/widgets/avatar.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_callback.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/tui_theme.dart';
|
||||
|
||||
class SendJoinGroupApplication extends StatefulWidget {
|
||||
final V2TimGroupInfo groupInfo;
|
||||
final AddGroupLifeCycle? lifeCycle;
|
||||
|
||||
const SendJoinGroupApplication({Key? key, required this.groupInfo, this.lifeCycle}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _SendJoinGroupApplicationState();
|
||||
}
|
||||
|
||||
class _SendJoinGroupApplicationState extends TIMUIKitState<SendJoinGroupApplication> {
|
||||
final TextEditingController _verficationController = TextEditingController();
|
||||
final GroupServices _groupServices = serviceLocator<GroupServices>();
|
||||
final CoreServicesImpl _coreServicesImpl = serviceLocator<CoreServicesImpl>();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final loginUserInfo = _coreServicesImpl.loginUserInfo;
|
||||
final option1 = loginUserInfo?.nickName ?? loginUserInfo?.userID;
|
||||
_verficationController.text = TIM_t_para("我是: {{option1}}", "我是: $option1")(option1: option1);
|
||||
}
|
||||
|
||||
Future<V2TimCallback?> addGroup(String groupID, String message) async {
|
||||
if (widget.lifeCycle?.shouldAddGroup != null &&
|
||||
await widget.lifeCycle!.shouldAddGroup(groupID, message, context) == false) {
|
||||
return null;
|
||||
}
|
||||
return _groupServices.joinGroup(groupID: groupID, message: message);
|
||||
}
|
||||
|
||||
String _getGroupType(String type) {
|
||||
String groupType;
|
||||
switch (type) {
|
||||
case GroupType.AVChatRoom:
|
||||
groupType = TIM_t("聊天室");
|
||||
break;
|
||||
case GroupType.Meeting:
|
||||
groupType = TIM_t("会议群");
|
||||
break;
|
||||
case GroupType.Public:
|
||||
groupType = TIM_t("公开群");
|
||||
break;
|
||||
case GroupType.Work:
|
||||
groupType = TIM_t("工作群");
|
||||
break;
|
||||
case GroupType.Community:
|
||||
groupType = TIM_t("社群");
|
||||
break;
|
||||
default:
|
||||
groupType = TIM_t("未知群");
|
||||
break;
|
||||
}
|
||||
return groupType;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final TUITheme theme = value.theme;
|
||||
|
||||
final faceUrl = widget.groupInfo.faceUrl ?? "";
|
||||
final groupID = widget.groupInfo.groupID;
|
||||
final showName = widget.groupInfo.groupName ?? groupID;
|
||||
final option1 = _getGroupType(widget.groupInfo.groupType);
|
||||
|
||||
Widget sendGroupApplicationBody() {
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
color: theme.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
margin: const EdgeInsets.only(right: 12),
|
||||
child: Avatar(faceUrl: faceUrl, showName: showName),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
showName,
|
||||
style: TextStyle(color: theme.darkTextColor, fontSize: 18),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 4,
|
||||
),
|
||||
Text(
|
||||
"ID: $groupID",
|
||||
style: TextStyle(fontSize: 13, color: theme.weakTextColor),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 4,
|
||||
),
|
||||
Text(
|
||||
TIM_t_para("群类型: {{option1}}", "群类型: $option1")(option1: option1),
|
||||
style: TextStyle(fontSize: 12, color: theme.weakTextColor),
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 16.0),
|
||||
child: Text(
|
||||
TIM_t("填写验证信息"),
|
||||
style: TextStyle(fontSize: 16, color: theme.weakTextColor),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
margin: const EdgeInsets.only(top: 6, bottom: 12),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
color: theme.white,
|
||||
child: TextField(
|
||||
maxLines: 4,
|
||||
controller: _verficationController,
|
||||
keyboardType: TextInputType.multiline,
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
hintStyle: TextStyle(
|
||||
color: theme.textgrey,
|
||||
),
|
||||
hintText: '')),
|
||||
),
|
||||
Container(
|
||||
color: theme.white,
|
||||
width: double.infinity,
|
||||
margin: const EdgeInsets.only(top: 10),
|
||||
child: TextButton(
|
||||
onPressed: () async {
|
||||
final addWording = _verficationController.text;
|
||||
final res = await addGroup(groupID, addWording);
|
||||
if (res?.code == 0) {
|
||||
onTIMCallback(TIMCallback(
|
||||
type: TIMCallbackType.INFO, infoRecommendText: TIM_t("群申请已发送"), infoCode: 6660201));
|
||||
}
|
||||
},
|
||||
child: Text(TIM_t("发送"))),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return TUIKitScreenUtils.getDeviceWidget(
|
||||
context: context,
|
||||
desktopWidget: sendGroupApplicationBody(),
|
||||
defaultWidget: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
TIM_t("进群申请"),
|
||||
style: TextStyle(color: theme.appbarTextColor, fontSize: 17),
|
||||
),
|
||||
shadowColor: theme.white,
|
||||
backgroundColor: theme.appbarBgColor ?? theme.primaryColor,
|
||||
iconTheme: IconThemeData(
|
||||
color: theme.appbarTextColor,
|
||||
),
|
||||
),
|
||||
body: sendGroupApplicationBody(),
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_slidable_plus_plus/flutter_slidable_plus_plus.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_friend_info.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_friend_info.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/life_cycle/block_list_life_cycle.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_friendship_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/services_locatar.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/avatar.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/color.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/tui_theme_view_model.dart';
|
||||
|
||||
typedef BlackListItemBuilder = Widget Function(BuildContext context, V2TimFriendInfo friendInfo);
|
||||
|
||||
class TIMUIKitBlackList extends StatefulWidget {
|
||||
final void Function(V2TimFriendInfo friendInfo)? onTapItem;
|
||||
final Widget Function(BuildContext context)? emptyBuilder;
|
||||
final BlackListItemBuilder? itemBuilder;
|
||||
|
||||
/// The life cycle hooks for block list business logic
|
||||
final BlockListLifeCycle? lifeCycle;
|
||||
|
||||
const TIMUIKitBlackList({Key? key, this.onTapItem, this.emptyBuilder, this.itemBuilder, this.lifeCycle})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _TIMUIKitBlackListState();
|
||||
}
|
||||
|
||||
class _TIMUIKitBlackListState extends TIMUIKitState<TIMUIKitBlackList> {
|
||||
final TUIFriendShipViewModel _friendshipViewModel = serviceLocator<TUIFriendShipViewModel>();
|
||||
|
||||
_getShowName(V2TimFriendInfo item) {
|
||||
final friendRemark = item.friendRemark ?? "";
|
||||
final nickName = item.userProfile?.nickName ?? "";
|
||||
final userID = item.userID;
|
||||
final showName = nickName != "" ? nickName : userID;
|
||||
return friendRemark != "" ? friendRemark : showName;
|
||||
}
|
||||
|
||||
Widget _itemBuilder(BuildContext context, V2TimFriendInfo friendInfo) {
|
||||
final theme = Provider.of<TUIThemeViewModel>(context).theme;
|
||||
final showName = _getShowName(friendInfo);
|
||||
final faceUrl = friendInfo.userProfile?.faceUrl ?? "";
|
||||
final isDesktopScreen = TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop;
|
||||
|
||||
Widget itemWidget() {
|
||||
return Material(
|
||||
color: theme.wideBackgroundColor,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
if (widget.onTapItem != null) {
|
||||
widget.onTapItem!(friendInfo);
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(top: 10, left: 16, right: 16),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(bottom: BorderSide(color: theme.weakDividerColor ?? CommonColor.weakDividerColor))),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
margin: const EdgeInsets.only(right: 12),
|
||||
child: SizedBox(
|
||||
height: isDesktopScreen ? 30 : 40,
|
||||
width: isDesktopScreen ? 30 : 40,
|
||||
child: Avatar(faceUrl: faceUrl, showName: showName),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
padding: const EdgeInsets.only(top: 10, bottom: 20),
|
||||
child: Text(
|
||||
showName,
|
||||
style: TextStyle(color: theme.black, fontSize: isDesktopScreen ? 14 : 18),
|
||||
),
|
||||
)),
|
||||
if (isDesktopScreen)
|
||||
OutlinedButton(
|
||||
onPressed: () {
|
||||
_friendshipViewModel.deleteFromBlockList([friendInfo.userID]);
|
||||
},
|
||||
child: Text(
|
||||
TIM_t("移出黑名单"),
|
||||
style: TextStyle(color: theme.primaryColor),
|
||||
))
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return TUIKitScreenUtils.getDeviceWidget(
|
||||
context: context,
|
||||
desktopWidget: itemWidget(),
|
||||
defaultWidget: Slidable(
|
||||
endActionPane: ActionPane(motion: const DrawerMotion(), children: [
|
||||
SlidableAction(
|
||||
onPressed: (context) async {
|
||||
await _friendshipViewModel.deleteFromBlockList([friendInfo.userID]);
|
||||
},
|
||||
backgroundColor: theme.cautionColor ?? CommonColor.cautionColor,
|
||||
foregroundColor: theme.white,
|
||||
label: TIM_t("删除"),
|
||||
autoClose: true,
|
||||
)
|
||||
]),
|
||||
child: itemWidget(),
|
||||
));
|
||||
}
|
||||
|
||||
BlackListItemBuilder _getItemBuilder() {
|
||||
return widget.itemBuilder ?? _itemBuilder;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
return MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider.value(value: _friendshipViewModel),
|
||||
],
|
||||
builder: (BuildContext context, Widget? w) {
|
||||
final model = Provider.of<TUIFriendShipViewModel>(context);
|
||||
model.blockListLifeCycle = widget.lifeCycle;
|
||||
final blockList = model.blockList;
|
||||
if (blockList.isNotEmpty) {
|
||||
return ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: blockList.length,
|
||||
itemBuilder: (context, index) {
|
||||
final friendInfo = blockList[index];
|
||||
final itemBuilder = _getItemBuilder();
|
||||
return itemBuilder(context, friendInfo);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (widget.emptyBuilder != null) {
|
||||
return widget.emptyBuilder!(context);
|
||||
}
|
||||
|
||||
return Container();
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_statelesswidget.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKItMessageList/TIMUIKitTongue/tim_uikit_tongue_item.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart';
|
||||
|
||||
enum MessageListTongueType {
|
||||
none,
|
||||
toLatest,
|
||||
showUnread,
|
||||
atMe,
|
||||
showPrevious,
|
||||
atAll,
|
||||
}
|
||||
|
||||
typedef TongueItemBuilder = Widget Function(
|
||||
VoidCallback onClick, MessageListTongueType valueType, int unreadCount);
|
||||
|
||||
class TIMUIKitHistoryMessageListTongue extends TIMUIKitStatelessWidget {
|
||||
/// the value type currently
|
||||
final MessageListTongueType valueType;
|
||||
|
||||
/// the callback after clicking
|
||||
final VoidCallback onClick;
|
||||
|
||||
/// unread amount currently
|
||||
final int unreadCount;
|
||||
|
||||
/// the builder for tongue item
|
||||
final TongueItemBuilder? tongueItemBuilder;
|
||||
|
||||
/// total amount of messages at me
|
||||
final String atNum;
|
||||
|
||||
final int previousCount;
|
||||
|
||||
TIMUIKitHistoryMessageListTongue({
|
||||
Key? key,
|
||||
required this.valueType,
|
||||
required this.onClick,
|
||||
required this.previousCount,
|
||||
required this.unreadCount,
|
||||
this.tongueItemBuilder,
|
||||
this.atNum = "",
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
late Widget tongueItem;
|
||||
if (tongueItemBuilder != null) {
|
||||
tongueItem = tongueItemBuilder!(onClick, valueType, unreadCount);
|
||||
} else {
|
||||
tongueItem = TIMUIKitTongueItem(
|
||||
onClick: onClick,
|
||||
unreadCount: unreadCount,
|
||||
valueType: valueType,
|
||||
atNum: atNum,
|
||||
previousCount: previousCount,
|
||||
);
|
||||
}
|
||||
return valueType != MessageListTongueType.none ? tongueItem : Container();
|
||||
}
|
||||
}
|
||||
+182
@@ -0,0 +1,182 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:scroll_to_index/scroll_to_index.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_conversation.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_at_info.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_group_at_info.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_chat_separate_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_chat_global_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/services_locatar.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/common_utils.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKItMessageList/TIMUIKitTongue/tim_uikit_chat_history_message_list_tongue.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
class TIMUIKitHistoryMessageListTongueContainer extends StatefulWidget {
|
||||
final Widget Function(void Function(), MessageListTongueType, int)? tongueItemBuilder;
|
||||
final List<V2TimGroupAtInfo?>? groupAtInfoList;
|
||||
final Function(String targetSeq) scrollToIndexBySeq;
|
||||
final AutoScrollController scrollController;
|
||||
final TUIChatSeparateViewModel model;
|
||||
final V2TimConversation conversation;
|
||||
|
||||
const TIMUIKitHistoryMessageListTongueContainer({
|
||||
Key? key,
|
||||
this.tongueItemBuilder,
|
||||
this.groupAtInfoList,
|
||||
required this.conversation,
|
||||
required this.scrollToIndexBySeq,
|
||||
required this.scrollController,
|
||||
required this.model,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _TIMUIKitHistoryMessageListTongueContainerState();
|
||||
}
|
||||
|
||||
class _TIMUIKitHistoryMessageListTongueContainerState extends TIMUIKitState<TIMUIKitHistoryMessageListTongueContainer> {
|
||||
bool isFinishJumpToAt = false;
|
||||
List<V2TimGroupAtInfo?>? groupAtInfoList = [];
|
||||
final TUIChatGlobalModel globalModel = serviceLocator<TUIChatGlobalModel>();
|
||||
bool isClickShowPrevious = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
initScrollListener();
|
||||
groupAtInfoList = widget.groupAtInfoList?.reversed.toList();
|
||||
}
|
||||
|
||||
void changePositionState(HistoryMessagePosition newPosition) {
|
||||
if (globalModel.getMessageListPosition(widget.model.conversationID) != newPosition) {
|
||||
globalModel.setMessageListPosition(widget.model.conversationID, newPosition);
|
||||
}
|
||||
}
|
||||
|
||||
scrollHandler() {
|
||||
final screenHeight = MediaQuery.of(context).size.height;
|
||||
final offset = widget.scrollController.offset;
|
||||
final conversationUnreadCount = widget.model.getConversationUnreadCount();
|
||||
if (offset <= 0.0 && conversationUnreadCount != 0) {
|
||||
widget.model.showLatestUnread();
|
||||
}
|
||||
if (widget.scrollController.offset <= widget.scrollController.position.minScrollExtent &&
|
||||
!widget.scrollController.position.outOfRange &&
|
||||
!widget.model.haveMoreLatestData) {
|
||||
changePositionState(HistoryMessagePosition.bottom);
|
||||
} else if (widget.scrollController.offset <= screenHeight * 1.6 &&
|
||||
widget.scrollController.offset > 0 &&
|
||||
!widget.scrollController.position.outOfRange &&
|
||||
!widget.model.haveMoreLatestData) {
|
||||
changePositionState(HistoryMessagePosition.inTwoScreen);
|
||||
} else if (widget.scrollController.offset > screenHeight * 1.6 &&
|
||||
!widget.scrollController.position.outOfRange &&
|
||||
!widget.model.haveMoreLatestData) {
|
||||
changePositionState(HistoryMessagePosition.awayTwoScreen);
|
||||
}
|
||||
}
|
||||
|
||||
void initScrollListener() {
|
||||
widget.scrollController.addListener(scrollHandler);
|
||||
}
|
||||
|
||||
MessageListTongueType _getTongueValueType(List<V2TimGroupAtInfo?>? groupAtInfoList) {
|
||||
if (globalModel.getMessageListPosition(widget.model.conversationID) == HistoryMessagePosition.notShowLatest) {
|
||||
return MessageListTongueType.none;
|
||||
}
|
||||
if (groupAtInfoList != null && groupAtInfoList.isNotEmpty && !isFinishJumpToAt) {
|
||||
if (groupAtInfoList[0]!.atType == 1) {
|
||||
return MessageListTongueType.atMe;
|
||||
} else {
|
||||
return MessageListTongueType.atAll;
|
||||
}
|
||||
}
|
||||
|
||||
// if ((widget.conversation.unreadCount ?? 0) > 20 && !isClickShowPrevious) {
|
||||
// return MessageListTongueType.showPrevious;
|
||||
// }
|
||||
|
||||
if (globalModel.unreadCountForTongue > 0) {
|
||||
return MessageListTongueType.showUnread;
|
||||
}
|
||||
|
||||
if (globalModel.getMessageListPosition(widget.model.conversationID) == HistoryMessagePosition.awayTwoScreen) {
|
||||
return MessageListTongueType.toLatest;
|
||||
}
|
||||
|
||||
return MessageListTongueType.none;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
widget.scrollController.removeListener(scrollHandler);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
return Selector<TUIChatGlobalModel, Tuple2<HistoryMessagePosition, int>>(
|
||||
builder: (context, value, child) {
|
||||
return Positioned(
|
||||
bottom: _getTongueValueType(groupAtInfoList) != MessageListTongueType.showPrevious ? 16 : null,
|
||||
top: _getTongueValueType(groupAtInfoList) == MessageListTongueType.showPrevious ? 16 : null,
|
||||
right: 16,
|
||||
child: TIMUIKitHistoryMessageListTongue(
|
||||
previousCount: widget.conversation.unreadCount ?? 0,
|
||||
tongueItemBuilder: widget.tongueItemBuilder,
|
||||
unreadCount: value.item2,
|
||||
onClick: () async {
|
||||
if (groupAtInfoList != null && groupAtInfoList!.isNotEmpty) {
|
||||
if (groupAtInfoList?.length == 1) {
|
||||
widget.scrollToIndexBySeq(groupAtInfoList![0]!.seq);
|
||||
|
||||
setState(() {
|
||||
groupAtInfoList = [];
|
||||
isFinishJumpToAt = true;
|
||||
});
|
||||
} else {
|
||||
widget.scrollToIndexBySeq(groupAtInfoList!.removeAt(0)!.seq);
|
||||
}
|
||||
} else if ((widget.conversation.unreadCount ?? 0) > 20 && !isClickShowPrevious) {
|
||||
try {
|
||||
isClickShowPrevious = true;
|
||||
final String? lastSeqString = widget.conversation.lastMessage?.seq;
|
||||
final int? lastSeq =
|
||||
TencentUtils.checkString(lastSeqString) != null ? int.parse(lastSeqString!) : null;
|
||||
final int? previousCount = widget.conversation.unreadCount;
|
||||
if (lastSeq != null && previousCount != null) {
|
||||
final targetSeq = lastSeq - previousCount;
|
||||
await widget.model.loadListForSpecificMessage(seq: targetSeq);
|
||||
// Future.delayed(const Duration(milliseconds: 100), () {
|
||||
// widget.scrollToIndexBySeq((targetSeq).toString());
|
||||
// });
|
||||
}
|
||||
} catch (e) {
|
||||
// TODO: 这里后续加个弹窗提示客户,找消息失败了
|
||||
}
|
||||
// widget.model.loadListForSpecificMessage(seq: count);
|
||||
} else if (value.item1 == HistoryMessagePosition.awayTwoScreen || value.item2 > 0) {
|
||||
widget.model.showLatestUnread();
|
||||
widget.scrollController.animateTo(
|
||||
widget.scrollController.position.minScrollExtent,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.ease,
|
||||
);
|
||||
return;
|
||||
}
|
||||
},
|
||||
atNum: groupAtInfoList?.length.toString() ?? "",
|
||||
valueType: _getTongueValueType(groupAtInfoList),
|
||||
),
|
||||
);
|
||||
},
|
||||
selector: (c, model) {
|
||||
final messageListPosition = model.getMessageListPosition(widget.model.conversationID);
|
||||
final unreadCountForConversation = model.unreadCountForTongue;
|
||||
return Tuple2(messageListPosition, unreadCountForConversation);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:tencent_chat_i18n_tool/tencent_chat_i18n_tool.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_statelesswidget.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKItMessageList/TIMUIKitTongue/tim_uikit_chat_history_message_list_tongue.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/color.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/tui_theme.dart';
|
||||
|
||||
class TIMUIKitTongueItem extends TIMUIKitStatelessWidget {
|
||||
/// the callback after clicking
|
||||
final VoidCallback onClick;
|
||||
|
||||
/// the value type currently
|
||||
final MessageListTongueType valueType;
|
||||
|
||||
/// unread amount currently
|
||||
final int unreadCount;
|
||||
|
||||
/// total amount of messages at me
|
||||
final String atNum;
|
||||
|
||||
final int previousCount;
|
||||
|
||||
TIMUIKitTongueItem({
|
||||
Key? key,
|
||||
required this.onClick,
|
||||
required this.valueType,
|
||||
required this.previousCount,
|
||||
required this.unreadCount,
|
||||
required this.atNum,
|
||||
}) : super(key: key);
|
||||
|
||||
Map<MessageListTongueType, String> textType(BuildContext context) {
|
||||
final option1 = unreadCount.toString();
|
||||
final option2 = atNum.toString();
|
||||
// final option3 = previousCount.toString();
|
||||
final String atMeString = option2 != ""
|
||||
? TIM_t_para("有{{option2}}条@我消息", "有$option2条@我消息")(option2: option2)
|
||||
: TIM_t("有人@我");
|
||||
|
||||
return {
|
||||
// MessageListTongueType.showPrevious:
|
||||
// TIM_t_para("{{option3}}条未读消息", "$option3条未读消息")(option3: option3),
|
||||
MessageListTongueType.toLatest: TIM_t("回到最新位置"),
|
||||
MessageListTongueType.showUnread:
|
||||
TIM_t_para("{{option1}}条新消息", "$option1条新消息")(option1: option1),
|
||||
MessageListTongueType.atMe: atMeString,
|
||||
MessageListTongueType.atAll: TIM_t("@所有人"),
|
||||
};
|
||||
}
|
||||
|
||||
final Map<MessageListTongueType, IconData> iconType = {
|
||||
MessageListTongueType.toLatest: Icons.arrow_downward_outlined,
|
||||
MessageListTongueType.showUnread: Icons.arrow_downward_outlined,
|
||||
MessageListTongueType.atMe: Icons.arrow_upward_outlined,
|
||||
MessageListTongueType.atAll: Icons.arrow_upward_outlined,
|
||||
MessageListTongueType.showPrevious: Icons.arrow_upward_outlined,
|
||||
};
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final TUITheme theme = value.theme;
|
||||
return GestureDetector(
|
||||
onTap: onClick,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: Border.all(color: hexToColor("E5E5E5"), width: 1),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: theme.weakDividerColor ?? hexToColor("E6E9EB"),
|
||||
offset: const Offset(0.0, 0.0),
|
||||
blurRadius: 10,
|
||||
spreadRadius: 2),
|
||||
],
|
||||
),
|
||||
padding: const EdgeInsets.all(10),
|
||||
// width: 112,
|
||||
// height: 37,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
margin: const EdgeInsets.only(right: 6),
|
||||
child: Icon(
|
||||
iconType[valueType],
|
||||
color: theme.primaryColor,
|
||||
size: 12,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
textType(context)[valueType] ?? "",
|
||||
style: TextStyle(
|
||||
color: theme.primaryColor,
|
||||
fontSize: 12.0,
|
||||
fontWeight: FontWeight.w400),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+531
@@ -0,0 +1,531 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:loading_animation_widget/loading_animation_widget.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:scroll_to_index/scroll_to_index.dart';
|
||||
import 'package:tencent_chat_i18n_tool/tencent_chat_i18n_tool.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/enum/message_elem_type.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_conversation.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_at_info.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_group_at_info.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_message.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_statelesswidget.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_chat_separate_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_chat_global_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/logger.dart';
|
||||
// ignore: unused_import
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/optimize_utils.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKItMessageList/tim_uikit_chat_history_message_list_config.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKItMessageList/utils.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/keepalive_wrapper.dart';
|
||||
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_callback.dart';
|
||||
import 'TIMUIKitTongue/tim_uikit_chat_history_message_list_tongue.dart';
|
||||
import 'TIMUIKitTongue/tim_uikit_chat_history_message_list_tongue_container.dart';
|
||||
|
||||
enum LoadingPlace {
|
||||
none,
|
||||
top,
|
||||
bottom,
|
||||
}
|
||||
|
||||
enum ScrollType { toIndex, toIndexBegin }
|
||||
|
||||
class TIMUIKitHistoryMessageListController extends ChangeNotifier {
|
||||
AutoScrollController? scrollController = AutoScrollController();
|
||||
late ScrollType scrollType;
|
||||
late V2TimMessage targetMessage;
|
||||
|
||||
TIMUIKitHistoryMessageListController({
|
||||
AutoScrollController? scrollController,
|
||||
}) {
|
||||
if (scrollController != null) {
|
||||
this.scrollController = scrollController;
|
||||
}
|
||||
}
|
||||
|
||||
scrollToIndex(V2TimMessage message) {
|
||||
scrollType = ScrollType.toIndex;
|
||||
targetMessage = message;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
scrollToIndexBegin(V2TimMessage message) {
|
||||
scrollType = ScrollType.toIndexBegin;
|
||||
targetMessage = message;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
class TIMUIKitHistoryMessageList extends StatefulWidget {
|
||||
/// message list
|
||||
final List<V2TimMessage?> messageList;
|
||||
|
||||
/// tongue item builder
|
||||
final TongueItemBuilder? tongueItemBuilder;
|
||||
|
||||
/// group at info, it can get from conversation info
|
||||
final List<V2TimGroupAtInfo?>? groupAtInfoList;
|
||||
|
||||
/// use for build message item
|
||||
final Widget Function(BuildContext, V2TimMessage?)? itemBuilder;
|
||||
|
||||
/// can controll message list scroll
|
||||
final TIMUIKitHistoryMessageListController? controller;
|
||||
|
||||
/// use for message jump, if passed will jump to target message.
|
||||
final V2TimMessage? initFindingMsg;
|
||||
|
||||
/// use for load more message
|
||||
final Future<bool> Function(String?, LoadDirection direction, [int?, int?]) onLoadMore;
|
||||
|
||||
/// configuration for list view
|
||||
final TIMUIKitHistoryMessageListConfig? mainHistoryListConfig;
|
||||
|
||||
final TUIChatSeparateViewModel model;
|
||||
|
||||
final bool isAllowScroll;
|
||||
|
||||
final V2TimConversation conversation;
|
||||
|
||||
const TIMUIKitHistoryMessageList(
|
||||
{Key? key,
|
||||
required this.model,
|
||||
required this.messageList,
|
||||
this.itemBuilder,
|
||||
this.controller,
|
||||
required this.onLoadMore,
|
||||
this.tongueItemBuilder,
|
||||
this.groupAtInfoList,
|
||||
this.initFindingMsg,
|
||||
this.isAllowScroll = true,
|
||||
this.mainHistoryListConfig,
|
||||
required this.conversation})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _TIMUIKitHistoryMessageListState();
|
||||
}
|
||||
|
||||
class _TIMUIKitHistoryMessageListState extends TIMUIKitState<TIMUIKitHistoryMessageList> {
|
||||
V2TimMessage? findingMsg;
|
||||
String findingSeq = "";
|
||||
late TIMUIKitHistoryMessageListController _controller;
|
||||
late AutoScrollController _autoScrollController;
|
||||
LoadingPlace loadingPlace = LoadingPlace.none;
|
||||
bool maybeHaveMoreMessageForFind = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = widget.controller ?? TIMUIKitHistoryMessageListController();
|
||||
_autoScrollController = _controller.scrollController ?? AutoScrollController();
|
||||
_controller.addListener(_controllerListener);
|
||||
initFinding();
|
||||
}
|
||||
|
||||
initFinding() async {
|
||||
if (widget.initFindingMsg != null) {
|
||||
await widget.onLoadMore(null, LoadDirection.previous);
|
||||
setState(() {
|
||||
findingMsg = widget.initFindingMsg!;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_controllerListener() {
|
||||
final scrollType = _controller.scrollType;
|
||||
final targetMessage = _controller.targetMessage;
|
||||
switch (scrollType) {
|
||||
case ScrollType.toIndex:
|
||||
_onScrollToIndex(targetMessage);
|
||||
break;
|
||||
case ScrollType.toIndexBegin:
|
||||
_onScrollToIndexBegin(targetMessage);
|
||||
break;
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
Widget _getMessageItemBuilder(V2TimMessage? messageItem) {
|
||||
if (widget.itemBuilder != null) {
|
||||
return widget.itemBuilder!(context, messageItem);
|
||||
}
|
||||
return Container();
|
||||
}
|
||||
|
||||
_getMessageId(int index) {
|
||||
if (widget.messageList[index]!.elemType == 11) {
|
||||
return _getMessageId(index - 1);
|
||||
}
|
||||
return widget.messageList[index]!.msgID;
|
||||
}
|
||||
|
||||
void showCantFindMsg() {
|
||||
findingMsg = null;
|
||||
findingSeq = "";
|
||||
loadingPlace = LoadingPlace.none;
|
||||
onTIMCallback(TIMCallback(type: TIMCallbackType.INFO, infoRecommendText: TIM_t("无法定位到原消息"), infoCode: 6660401));
|
||||
}
|
||||
|
||||
_onScrollToIndex(V2TimMessage targetMsg) async {
|
||||
// This method called by @ messages or messages been searched, aims to jump to target message
|
||||
loadingPlace = LoadingPlace.top;
|
||||
const int singleLoadAmount = kIsWeb ? 15 : 40;
|
||||
final lastTimestamp = widget.messageList[widget.messageList.length - 1]?.timestamp;
|
||||
final msgList = widget.messageList;
|
||||
final targetTimeStamp = targetMsg.timestamp!;
|
||||
|
||||
if (targetTimeStamp >= lastTimestamp!) {
|
||||
// 当前列表里应该有这个消息,试试能不能直接定位到那去
|
||||
bool isFound = false;
|
||||
int targetIndex = 1;
|
||||
for (int i = msgList.length - 1; i >= 0; i--) {
|
||||
final currentMsg = msgList[i];
|
||||
if (currentMsg?.timestamp == targetTimeStamp &&
|
||||
currentMsg?.elemType != 11 &&
|
||||
currentMsg!.msgID == targetMsg.msgID) {
|
||||
// find the target index by timestamp and msgID
|
||||
isFound = true;
|
||||
targetIndex = -i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (isFound && targetIndex != 1) {
|
||||
findingMsg = null;
|
||||
_autoScrollController.scrollToIndex(
|
||||
targetIndex,
|
||||
preferPosition: AutoScrollPosition.middle,
|
||||
);
|
||||
|
||||
// execute twice for accurate position, as the position located firstly can be wrong
|
||||
_autoScrollController.scrollToIndex(targetIndex, preferPosition: AutoScrollPosition.middle);
|
||||
_autoScrollController.scrollToIndex(targetIndex, preferPosition: AutoScrollPosition.middle);
|
||||
|
||||
widget.model.jumpMsgID = targetMsg.msgID!;
|
||||
loadingPlace = LoadingPlace.none;
|
||||
} else {
|
||||
showCantFindMsg();
|
||||
}
|
||||
} else {
|
||||
if (maybeHaveMoreMessageForFind) {
|
||||
// if the target message not in current message list, load more
|
||||
findingMsg = targetMsg;
|
||||
final lastMsgId = _getMessageId(widget.messageList.length - 1);
|
||||
maybeHaveMoreMessageForFind = await widget.onLoadMore(lastMsgId, LoadDirection.previous, singleLoadAmount);
|
||||
} else {
|
||||
showCantFindMsg();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_onScrollToIndexBySeq(String targetSeq) async {
|
||||
// This method called by tongue request jumping to target @ message
|
||||
loadingPlace = LoadingPlace.top;
|
||||
// const int singleLoadAmount = 40;
|
||||
final msgList = widget.messageList;
|
||||
String lastSeq = "";
|
||||
for (int i = msgList.length - 1; i >= 0; i--) {
|
||||
final currentMsg = msgList[i];
|
||||
if (currentMsg!.seq != null && currentMsg.seq != "") {
|
||||
lastSeq = currentMsg.seq!;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (int.parse(lastSeq) <= int.parse(targetSeq)) {
|
||||
bool isFound = false;
|
||||
int targetIndex = 1;
|
||||
String? targetMsgID = "";
|
||||
for (int i = msgList.length - 1; i >= 0; i--) {
|
||||
final currentMsg = msgList[i];
|
||||
if (currentMsg?.seq == targetSeq) {
|
||||
isFound = true;
|
||||
targetMsgID = currentMsg?.msgID;
|
||||
targetIndex = -i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (isFound && targetIndex != 1) {
|
||||
findingSeq = "";
|
||||
_autoScrollController.scrollToIndex(
|
||||
targetIndex,
|
||||
preferPosition: AutoScrollPosition.middle,
|
||||
);
|
||||
_autoScrollController.scrollToIndex(targetIndex, preferPosition: AutoScrollPosition.middle);
|
||||
if (targetMsgID != null && targetMsgID != "") {
|
||||
widget.model.jumpMsgID = targetMsgID;
|
||||
}
|
||||
loadingPlace = LoadingPlace.none;
|
||||
} else {
|
||||
showCantFindMsg();
|
||||
}
|
||||
} else {
|
||||
if (maybeHaveMoreMessageForFind) {
|
||||
findingSeq = targetSeq;
|
||||
int requestCount = int.parse(lastSeq) - int.parse(targetSeq);
|
||||
maybeHaveMoreMessageForFind = await widget.onLoadMore(
|
||||
_getMessageId(widget.messageList.length - 1), LoadDirection.previous, requestCount, int.parse(lastSeq));
|
||||
} else {
|
||||
showCantFindMsg();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_onScrollToIndexBegin(V2TimMessage targetMsg) {
|
||||
final lastTimestamp = widget.messageList[widget.messageList.length - 1]?.timestamp;
|
||||
final msgList = widget.messageList;
|
||||
final int targetTimeStamp = targetMsg.timestamp!;
|
||||
|
||||
if (targetTimeStamp >= lastTimestamp!) {
|
||||
bool isFound = false;
|
||||
int targetIndex = 1;
|
||||
for (int i = msgList.length - 1; i >= 0; i--) {
|
||||
final currentMsg = msgList[i];
|
||||
if (currentMsg?.timestamp == targetTimeStamp &&
|
||||
currentMsg?.elemType != 11 &&
|
||||
currentMsg!.msgID == targetMsg.msgID) {
|
||||
isFound = true;
|
||||
targetIndex = -i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (isFound && targetIndex != 1) {
|
||||
_autoScrollController.scrollToIndex(
|
||||
targetIndex,
|
||||
preferPosition: AutoScrollPosition.end,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<V2TimMessage?> _getReceivedMessageList(int receivedMessageListCount) {
|
||||
if (receivedMessageListCount == 0) {
|
||||
return [];
|
||||
}
|
||||
final haveTimeStampMessage = widget.messageList[receivedMessageListCount]?.elemType == 11;
|
||||
final endPoint = haveTimeStampMessage ? receivedMessageListCount + 1 : receivedMessageListCount;
|
||||
return widget.messageList.sublist(0, endPoint).reversed.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
// center key should generate everytime when build method called.
|
||||
final GlobalKey centerKey = GlobalKey();
|
||||
|
||||
final theme = value.theme;
|
||||
if (widget.messageList.isEmpty) {
|
||||
return Container();
|
||||
}
|
||||
|
||||
final messageList = widget.messageList;
|
||||
final globalModel = context.read<TUIChatGlobalModel>();
|
||||
final receivedNewMessageCount = globalModel.receivedNewMessageCount;
|
||||
final shouldShowUnreadMessage = receivedNewMessageCount > 0;
|
||||
final unreadMessageList = _getReceivedMessageList(receivedNewMessageCount);
|
||||
final readMessageList = messageList.sublist(unreadMessageList.length, messageList.length).toList();
|
||||
|
||||
final throttleFunction = OptimizeUtils.multiThrottle((index, LoadDirection direction) async {
|
||||
final msgID = TIMUIKitChatUtils.getMessageIDWithinIndex(readMessageList, index);
|
||||
await widget.onLoadMore(msgID, direction);
|
||||
}, 20);
|
||||
|
||||
final throttleFunctionWithMsgID = OptimizeUtils.multiThrottle((msgID, LoadDirection direction) async {
|
||||
await widget.onLoadMore(msgID, direction);
|
||||
}, 200);
|
||||
|
||||
if (findingMsg != null) {
|
||||
_onScrollToIndex(findingMsg!);
|
||||
} else if (findingSeq != "") {
|
||||
_onScrollToIndexBySeq(findingSeq);
|
||||
}
|
||||
|
||||
String getMessageIdentifier(V2TimMessage? message, int index) {
|
||||
return "${message?.msgID} - ${message?.timestamp} - ${message?.seq} -${message?.id ?? ""}";
|
||||
}
|
||||
|
||||
return Stack(
|
||||
alignment: Alignment.topCenter,
|
||||
children: [
|
||||
Scrollbar(
|
||||
controller: _autoScrollController,
|
||||
child: CustomScrollView(
|
||||
center: shouldShowUnreadMessage ? centerKey : null,
|
||||
key: widget.mainHistoryListConfig?.key,
|
||||
primary: widget.mainHistoryListConfig?.primary,
|
||||
physics: (widget.isAllowScroll == false)
|
||||
? const NeverScrollableScrollPhysics()
|
||||
: widget.mainHistoryListConfig?.physics,
|
||||
// padding: widget.mainHistoryListConfig?.padding ?? EdgeInsets.zero,
|
||||
// itemExtent: widget.mainHistoryListConfig?.itemExtent,
|
||||
// prototypeItem: widget.mainHistoryListConfig?.prototypeItem,
|
||||
cacheExtent: widget.mainHistoryListConfig?.cacheExtent ?? 1500,
|
||||
semanticChildCount: widget.mainHistoryListConfig?.semanticChildCount,
|
||||
dragStartBehavior: widget.mainHistoryListConfig?.dragStartBehavior ?? DragStartBehavior.start,
|
||||
keyboardDismissBehavior:
|
||||
widget.mainHistoryListConfig?.keyboardDismissBehavior ?? ScrollViewKeyboardDismissBehavior.manual,
|
||||
restorationId: widget.mainHistoryListConfig?.restorationId,
|
||||
clipBehavior: widget.mainHistoryListConfig?.clipBehavior ?? Clip.hardEdge,
|
||||
reverse: true,
|
||||
shrinkWrap: !shouldShowUnreadMessage,
|
||||
controller: _autoScrollController,
|
||||
slivers: [
|
||||
SliverPadding(
|
||||
padding: widget.mainHistoryListConfig?.padding ?? EdgeInsets.zero,
|
||||
sliver: SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(BuildContext context, int index) {
|
||||
final messageItem = unreadMessageList[index];
|
||||
if (index == unreadMessageList.length - 1 && widget.model.haveMoreLatestData == true) {
|
||||
throttleFunctionWithMsgID(messageItem?.msgID ?? "", LoadDirection.latest);
|
||||
}
|
||||
outputLogger.i(
|
||||
"Rendering a unread message: ${getMessageIdentifier(messageItem, 0)}, message Type: ${messageItem?.elemType}");
|
||||
return AutoScrollTag(
|
||||
controller: _autoScrollController,
|
||||
index: -index,
|
||||
key: ValueKey(getMessageIdentifier(messageItem, index)),
|
||||
highlightColor: Colors.black.withOpacity(0.1),
|
||||
child: KeepAliveWrapper(
|
||||
keepAlive: messageItem?.elemType == MessageElemType.V2TIM_ELEM_TYPE_SOUND,
|
||||
child: Container(child: _getMessageItemBuilder(messageItem))),
|
||||
);
|
||||
},
|
||||
childCount: unreadMessageList.length,
|
||||
findChildIndexCallback: (Key key) {
|
||||
final ValueKey<String> valueKey = key as ValueKey<String>;
|
||||
final String data = valueKey.value;
|
||||
final int index =
|
||||
unreadMessageList.indexWhere((element) => getMessageIdentifier(element, 0) == data);
|
||||
return index != -1 ? index : null;
|
||||
})),
|
||||
),
|
||||
SliverPadding(
|
||||
padding: EdgeInsets.zero,
|
||||
key: centerKey,
|
||||
),
|
||||
SliverPadding(
|
||||
padding: widget.mainHistoryListConfig?.padding ?? EdgeInsets.zero,
|
||||
sliver: Selector<TUIChatSeparateViewModel, bool>(
|
||||
selector: (context, model) {
|
||||
return model.haveMoreData;
|
||||
},
|
||||
shouldRebuild: (previous, next) {
|
||||
return previous != next;
|
||||
},
|
||||
builder: (context, haveMoreData, child) {
|
||||
return SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(BuildContext context, int index) {
|
||||
final messageItem = readMessageList[index];
|
||||
if (index == readMessageList.length - 1) {
|
||||
if (haveMoreData) {
|
||||
final lastMessage = globalModel
|
||||
.messageListMap[TencentUtils.checkString(widget.conversation.groupID) ??
|
||||
widget.conversation.userID ??
|
||||
widget.conversation.conversationID]
|
||||
?.last;
|
||||
if (lastMessage != null) {
|
||||
throttleFunctionWithMsgID(lastMessage.msgID ?? "", LoadDirection.previous);
|
||||
} else {
|
||||
throttleFunction(index, messageList);
|
||||
}
|
||||
return Column(
|
||||
children: [
|
||||
LoadingAnimationWidget.staggeredDotsWave(
|
||||
color: theme.weakTextColor ?? Colors.grey,
|
||||
size: 28,
|
||||
),
|
||||
AutoScrollTag(
|
||||
controller: _autoScrollController,
|
||||
index: -index,
|
||||
key: ValueKey(getMessageIdentifier(messageItem, index)),
|
||||
highlightColor: Colors.black.withOpacity(0.1),
|
||||
child: KeepAliveWrapper(
|
||||
keepAlive: messageItem?.elemType == MessageElemType.V2TIM_ELEM_TYPE_SOUND,
|
||||
child: Container(child: _getMessageItemBuilder(messageItem))),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
if (index == 0 &&
|
||||
widget.model.haveMoreLatestData == true &&
|
||||
globalModel.receivedNewMessageCount < 10) {
|
||||
throttleFunction(index, LoadDirection.latest);
|
||||
}
|
||||
outputLogger.i(
|
||||
"Rendering a read message: ${getMessageIdentifier(messageItem, 0)}, message Type: ${messageItem?.elemType}");
|
||||
return AutoScrollTag(
|
||||
controller: _autoScrollController,
|
||||
index: -index,
|
||||
key: ValueKey(getMessageIdentifier(messageItem, index)),
|
||||
highlightColor: Colors.black.withOpacity(0.1),
|
||||
child: KeepAliveWrapper(
|
||||
keepAlive: messageItem?.elemType == MessageElemType.V2TIM_ELEM_TYPE_SOUND,
|
||||
child: Container(child: _getMessageItemBuilder(messageItem))),
|
||||
);
|
||||
},
|
||||
childCount: readMessageList.length,
|
||||
findChildIndexCallback: (Key key) {
|
||||
final ValueKey<String> valueKey = key as ValueKey<String>;
|
||||
final String data = valueKey.value;
|
||||
final int index =
|
||||
readMessageList.indexWhere((element) => getMessageIdentifier(element, 0) == data);
|
||||
return index > -1 ? index : null;
|
||||
}));
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
)),
|
||||
TIMUIKitHistoryMessageListTongueContainer(
|
||||
conversation: widget.conversation,
|
||||
model: widget.model,
|
||||
scrollController: _autoScrollController,
|
||||
scrollToIndexBySeq: _onScrollToIndexBySeq,
|
||||
groupAtInfoList: widget.groupAtInfoList,
|
||||
tongueItemBuilder: widget.tongueItemBuilder,
|
||||
),
|
||||
if (loadingPlace == LoadingPlace.top)
|
||||
Positioned(
|
||||
top: 8,
|
||||
child: LoadingAnimationWidget.staggeredDotsWave(
|
||||
color: theme.weakTextColor ?? Colors.grey,
|
||||
size: 28,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TIMUIKitHistoryMessageListSelector extends TIMUIKitStatelessWidget {
|
||||
final Widget Function(BuildContext, List<V2TimMessage?>, Widget?) builder;
|
||||
final String conversationID;
|
||||
|
||||
TIMUIKitHistoryMessageListSelector({Key? key, required this.builder, required this.conversationID}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
return Selector<TUIChatGlobalModel, List<V2TimMessage?>>(
|
||||
builder: builder,
|
||||
shouldRebuild: (previous, next) {
|
||||
final isEquals = const DeepCollectionEquality.unordered().equals(previous, next);
|
||||
return !isEquals;
|
||||
},
|
||||
selector: (context, model) {
|
||||
final messageList = model.getMessageList(conversationID) ?? [];
|
||||
return messageList;
|
||||
});
|
||||
}
|
||||
}
|
||||
+171
@@ -0,0 +1,171 @@
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
// The definition of the following parameters is the same with [ListView.builder]
|
||||
class TIMUIKitHistoryMessageListConfig {
|
||||
Key? key;
|
||||
|
||||
/// {@template flutter.widgets.scroll_view.scrollDirection}
|
||||
/// The axis along which the scroll view scrolls.
|
||||
///
|
||||
/// Defaults to [Axis.vertical].
|
||||
/// {@endtemplate}
|
||||
Axis? scrollDirection;
|
||||
|
||||
/// {@template flutter.widgets.scroll_view.primary}
|
||||
/// Whether this is the primary scroll view associated with the parent
|
||||
/// [PrimaryScrollController].
|
||||
///
|
||||
/// When this is true, the scroll view is scrollable even if it does not have
|
||||
/// sufficient content to actually scroll. Otherwise, by default the user can
|
||||
/// only scroll the view if it has sufficient content. See [physics].
|
||||
///
|
||||
/// Also when true, the scroll view is used for default [ScrollAction]s. If a
|
||||
/// ScrollAction is not handled by an otherwise focused part of the application,
|
||||
/// the ScrollAction will be evaluated using this scroll view, for example,
|
||||
/// when executing [Shortcuts] key events like page up and down.
|
||||
///
|
||||
/// On iOS, this also identifies the scroll view that will scroll to top in
|
||||
/// response to a tap in the status bar.
|
||||
/// {@endtemplate}
|
||||
///
|
||||
/// Defaults to true when [scrollDirection] is [Axis.vertical] and
|
||||
/// [controller] is null.
|
||||
bool? primary;
|
||||
|
||||
/// {@template flutter.widgets.scroll_view.physics}
|
||||
/// How the scroll view should respond to user input.
|
||||
///
|
||||
/// For example, determines how the scroll view continues to animate after the
|
||||
/// user stops dragging the scroll view.
|
||||
///
|
||||
/// Defaults to matching platform conventions. Furthermore, if [primary] is
|
||||
/// false, then the user cannot scroll if there is insufficient content to
|
||||
/// scroll, while if [primary] is true, they can always attempt to scroll.
|
||||
///
|
||||
/// To force the scroll view to always be scrollable even if there is
|
||||
/// insufficient content, as if [primary] was true but without necessarily
|
||||
/// setting it to true, provide an [AlwaysScrollableScrollPhysics] physics
|
||||
/// object, as in:
|
||||
///
|
||||
/// ```dart
|
||||
/// physics: const AlwaysScrollableScrollPhysics(),
|
||||
/// ```
|
||||
///
|
||||
/// To force the scroll view to use the default platform conventions and not
|
||||
/// be scrollable if there is insufficient content, regardless of the value of
|
||||
/// [primary], provide an explicit [ScrollPhysics] object, as in:
|
||||
///
|
||||
/// ```dart
|
||||
/// physics: const ScrollPhysics(),
|
||||
/// ```
|
||||
///
|
||||
/// The physics can be changed dynamically (by providing a new object in a
|
||||
/// subsequent build), but new physics will only take effect if the _class_ of
|
||||
/// the provided object changes. Merely constructing a new instance with a
|
||||
/// different configuration is insufficient to cause the physics to be
|
||||
/// reapplied. (This is because the final object used is generated
|
||||
/// dynamically, which can be relatively expensive, and it would be
|
||||
/// inefficient to speculatively create this object each frame to see if the
|
||||
/// physics should be updated.)
|
||||
/// {@endtemplate}
|
||||
///
|
||||
/// If an explicit [ScrollBehavior] is provided to [scrollBehavior], the
|
||||
/// [ScrollPhysics] provided by that behavior will take precedence after
|
||||
/// [physics].
|
||||
ScrollPhysics? physics;
|
||||
|
||||
/// The amount of space by which to inset the children.
|
||||
EdgeInsetsGeometry? padding;
|
||||
|
||||
/// {@template flutter.widgets.list_view.itemExtent}
|
||||
/// If non-null, forces the children to have the given extent in the scroll
|
||||
/// direction.
|
||||
///
|
||||
/// Specifying an [itemExtent] is more efficient than letting the children
|
||||
/// determine their own extent because the scrolling machinery can make use of
|
||||
/// the foreknowledge of the children's extent to save work, for example when
|
||||
/// the scroll position changes drastically.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [SliverFixedExtentList], the sliver used internally when this property
|
||||
/// is provided. It constrains its box children to have a specific given
|
||||
/// extent along the main axis.
|
||||
/// * The [prototypeItem] property, which allows forcing the children's
|
||||
/// extent to be the same as the given widget.
|
||||
/// {@endtemplate}
|
||||
double? itemExtent;
|
||||
|
||||
/// {@template flutter.widgets.list_view.prototypeItem}
|
||||
/// If non-null, forces the children to have the same extent as the given
|
||||
/// widget in the scroll direction.
|
||||
///
|
||||
/// Specifying an [prototypeItem] is more efficient than letting the children
|
||||
/// determine their own extent because the scrolling machinery can make use of
|
||||
/// the foreknowledge of the children's extent to save work, for example when
|
||||
/// the scroll position changes drastically.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [SliverPrototypeExtentList], the sliver used internally when this
|
||||
/// property is provided. It constrains its box children to have the same
|
||||
/// extent as a prototype item along the main axis.
|
||||
/// * The [itemExtent] property, which allows forcing the children's extent
|
||||
/// to a given value.
|
||||
/// {@endtemplate}
|
||||
Widget? prototypeItem;
|
||||
|
||||
/// {@macro flutter.rendering.RenderViewportBase.cacheExtent}
|
||||
double? cacheExtent;
|
||||
|
||||
/// The number of children that will contribute semantic information.
|
||||
///
|
||||
/// Some subtypes of [ScrollView] can infer this value automatically. For
|
||||
/// example [ListView] will use the number of widgets in the child list,
|
||||
/// while the [ListView.separated] constructor will use half that amount.
|
||||
///
|
||||
/// For [CustomScrollView] and other types which do not receive a builder
|
||||
/// or list of widgets, the child count must be explicitly provided. If the
|
||||
/// number is unknown or unbounded this should be left unset or set to null.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [SemanticsConfiguration.scrollChildCount], the corresponding semantics property.
|
||||
int? semanticChildCount;
|
||||
|
||||
/// {@macro flutter.widgets.scrollable.dragStartBehavior}
|
||||
DragStartBehavior? dragStartBehavior;
|
||||
|
||||
/// {@template flutter.widgets.scroll_view.keyboardDismissBehavior}
|
||||
/// [ScrollViewKeyboardDismissBehavior] the defines how this [ScrollView] will
|
||||
/// dismiss the keyboard automatically.
|
||||
/// {@endtemplate}
|
||||
ScrollViewKeyboardDismissBehavior? keyboardDismissBehavior;
|
||||
|
||||
/// {@macro flutter.widgets.scrollable.restorationId}
|
||||
String? restorationId;
|
||||
|
||||
/// {@macro flutter.material.Material.clipBehavior}
|
||||
///
|
||||
/// Defaults to [Clip.hardEdge].
|
||||
Clip? clipBehavior;
|
||||
|
||||
bool? shrinkWrap;
|
||||
|
||||
TIMUIKitHistoryMessageListConfig(
|
||||
{this.key,
|
||||
this.scrollDirection,
|
||||
this.primary,
|
||||
this.physics,
|
||||
this.padding,
|
||||
this.itemExtent,
|
||||
this.prototypeItem,
|
||||
this.cacheExtent,
|
||||
this.semanticChildCount,
|
||||
this.dragStartBehavior,
|
||||
this.keyboardDismissBehavior,
|
||||
this.restorationId,
|
||||
this.shrinkWrap,
|
||||
this.clipBehavior});
|
||||
}
|
||||
+1460
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,620 @@
|
||||
// ignore_for_file: non_constant_identifier_names, avoid_print
|
||||
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:open_file/open_file.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:tencent_chat_i18n_tool/tencent_chat_i18n_tool.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/enum/group_member_role.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/enum/message_elem_type.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/enum/message_status.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_member_full_info.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_group_member_full_info.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_message.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_self_info_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/services_locatar.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/common_utils.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/message.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitMessageItem/TIMUIKitMessageReaction/tim_uikit_message_reaction_select_emoji.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_chat_separate_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_chat_global_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/platform.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKItMessageList/tim_uikit_chat_history_message_list_item.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/forward_message_screen.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_callback.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/color.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/tui_theme.dart';
|
||||
|
||||
class TIMUIKitMessageTooltip extends StatefulWidget {
|
||||
/// tool tips panel configuration, long press message will show tool tips panel
|
||||
final ToolTipsConfig? toolTipsConfig;
|
||||
|
||||
/// current message
|
||||
final V2TimMessage message;
|
||||
|
||||
/// allow notifi user when send reply message
|
||||
final bool allowAtUserWhenReply;
|
||||
|
||||
/// the callback for long press event, except myself avatar
|
||||
final Function(String? userId, String? nickName)? onLongPressForOthersHeadPortrait;
|
||||
|
||||
final bool isUseMessageReaction;
|
||||
|
||||
/// direction
|
||||
final SelectEmojiPanelPosition selectEmojiPanelPosition;
|
||||
|
||||
/// on add sticker reaction to a message
|
||||
final ValueChanged<int> onSelectSticker;
|
||||
|
||||
/// on close tooltip area
|
||||
final VoidCallback onCloseTooltip;
|
||||
|
||||
final TUIChatSeparateViewModel model;
|
||||
|
||||
final bool isShowMoreSticker;
|
||||
|
||||
final V2TimGroupMemberFullInfo? groupMemberInfo;
|
||||
|
||||
final bool iSUseDefaultHoverBar;
|
||||
|
||||
const TIMUIKitMessageTooltip(
|
||||
{Key? key,
|
||||
this.toolTipsConfig,
|
||||
this.isUseMessageReaction = true,
|
||||
required this.model,
|
||||
required this.message,
|
||||
required this.allowAtUserWhenReply,
|
||||
this.onLongPressForOthersHeadPortrait,
|
||||
required this.selectEmojiPanelPosition,
|
||||
required this.onCloseTooltip,
|
||||
required this.onSelectSticker,
|
||||
this.isShowMoreSticker = false,
|
||||
this.groupMemberInfo,
|
||||
required this.iSUseDefaultHoverBar})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => TIMUIKitMessageTooltipState();
|
||||
}
|
||||
|
||||
class TIMUIKitMessageTooltipState extends TIMUIKitState<TIMUIKitMessageTooltip> {
|
||||
final TUIChatGlobalModel globalModal = serviceLocator<TUIChatGlobalModel>();
|
||||
final TUISelfInfoViewModel selfInfoViewModel = serviceLocator<TUISelfInfoViewModel>();
|
||||
bool isShowMoreSticker = false;
|
||||
bool fileBeenDownloaded = false;
|
||||
String filePath = "";
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
hasFile();
|
||||
isShowMoreSticker = widget.isShowMoreSticker;
|
||||
}
|
||||
|
||||
hasFile() {
|
||||
if (PlatformUtils().isMobile ||
|
||||
(widget.message.fileElem == null && widget.message.imageElem == null && widget.message.videoElem == null)) {
|
||||
fileBeenDownloaded = false;
|
||||
return;
|
||||
}
|
||||
if (PlatformUtils().isWeb) {
|
||||
fileBeenDownloaded = true;
|
||||
return;
|
||||
}
|
||||
if (PlatformUtils().isDesktop) {
|
||||
if (widget.message.fileElem != null) {
|
||||
String savePath = TencentUtils.checkString(globalModal.getFileMessageLocation(widget.message.msgID)) ??
|
||||
TencentUtils.checkString(widget.message.fileElem!.localUrl) ??
|
||||
widget.message.fileElem?.path ??
|
||||
"";
|
||||
File f = File(savePath);
|
||||
if (f.existsSync() && widget.message.msgID != null) {
|
||||
filePath = savePath;
|
||||
fileBeenDownloaded = true;
|
||||
return;
|
||||
}
|
||||
} else if (widget.message.imageElem != null) {
|
||||
if (TencentUtils.checkString(widget.message.imageElem!.imageList![0]!.localUrl) != null &&
|
||||
File(widget.message.imageElem!.imageList![0]!.localUrl!).existsSync()) {
|
||||
fileBeenDownloaded = true;
|
||||
return;
|
||||
}
|
||||
} else if (widget.message.videoElem != null) {
|
||||
if (TencentUtils.checkString(widget.message.videoElem!.localVideoUrl) != null &&
|
||||
File(widget.message.videoElem!.localVideoUrl!).existsSync()) {
|
||||
fileBeenDownloaded = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
fileBeenDownloaded = false;
|
||||
}
|
||||
|
||||
bool isRevocable(int timestamp, int upperTimeLimit) =>
|
||||
((DateTime.now().millisecondsSinceEpoch / 1000).ceil() - timestamp < upperTimeLimit) &&
|
||||
(widget.message.isSelf ?? true);
|
||||
|
||||
Widget ItemInkWell({
|
||||
Widget? child,
|
||||
GestureTapCallback? onTap,
|
||||
}) {
|
||||
return SizedBox(
|
||||
width: 44,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
splashColor: Colors.white,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(bottom: 6, top: 6),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
bool isAdminCanRecall() {
|
||||
if (widget.model.chatConfig.isGroupAdminRecallEnabled) {
|
||||
final selfMemberInfo = widget.groupMemberInfo ?? widget.model.selfMemberInfo;
|
||||
final selfRole = selfMemberInfo?.role;
|
||||
return selfRole == GroupMemberRoleType.V2TIM_GROUP_MEMBER_ROLE_ADMIN ||
|
||||
selfRole == GroupMemberRoleType.V2TIM_GROUP_MEMBER_ROLE_OWNER;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
_buildLongPressTipItem(TUITheme theme, TUIChatSeparateViewModel model, V2TimMessage message) {
|
||||
final isDesktopScreen = TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop;
|
||||
final isCanRevokeSelf = isRevocable(widget.message.timestamp!, model.chatConfig.upperRecallTime);
|
||||
final shouldShowRevokeAction =
|
||||
(isCanRevokeSelf || isAdminCanRecall()) && widget.message.status != MessageStatus.V2TIM_MSG_STATUS_SEND_FAIL;
|
||||
final shouldShowReplyAction =
|
||||
!(widget.message.customElem?.data != null && MessageUtils.isCallingData(widget.message.customElem!.data!));
|
||||
final shouldShowForwardAction =
|
||||
!(widget.message.customElem?.data != null && MessageUtils.isCallingData(widget.message.customElem!.data!));
|
||||
final tooltipsConfig = widget.toolTipsConfig;
|
||||
final messageCanCopy = widget.message.elemType == MessageElemType.V2TIM_ELEM_TYPE_TEXT ||
|
||||
(isDesktopScreen && widget.message.elemType == MessageElemType.V2TIM_ELEM_TYPE_IMAGE && fileBeenDownloaded);
|
||||
bool showTranslation = true;
|
||||
if (widget.message.localCustomData != null) {
|
||||
final LocalCustomDataModel localCustomData =
|
||||
LocalCustomDataModel.fromMap(json.decode(TencentUtils.checkString(widget.message.localCustomData) ?? "{}"));
|
||||
if (localCustomData.translatedText != null && localCustomData.translatedText != "") {
|
||||
showTranslation = false;
|
||||
}
|
||||
}
|
||||
|
||||
final dynamicQuote = model.chatConfig.isAtWhenReplyDynamic?.call(widget.message);
|
||||
|
||||
final List<MessageToolTipItem> defaultTipsList = [
|
||||
if (fileBeenDownloaded)
|
||||
MessageToolTipItem(
|
||||
label: TIM_t("打开"),
|
||||
id: "open",
|
||||
iconImageAsset: "images/open_in_new.png",
|
||||
onClick: () => _onTap("open", model)),
|
||||
if (fileBeenDownloaded && PlatformUtils().isDesktop)
|
||||
MessageToolTipItem(
|
||||
label: PlatformUtils().isMacOS ? TIM_t("在访达中打开") : TIM_t("查看文件夹"),
|
||||
id: "finder",
|
||||
iconImageAsset: "images/folder_open.png",
|
||||
onClick: () => _onTap("finder", model)),
|
||||
if (messageCanCopy)
|
||||
MessageToolTipItem(
|
||||
label: TIM_t("复制"),
|
||||
id: "copyMessage",
|
||||
iconImageAsset: "images/copy_message.png",
|
||||
onClick: () => _onTap("copyMessage", model)),
|
||||
if (shouldShowForwardAction && !model.isVoteMessage(widget.message))
|
||||
MessageToolTipItem(
|
||||
label: TIM_t("转发"),
|
||||
id: "forwardMessage",
|
||||
iconImageAsset: "images/forward_message.png",
|
||||
onClick: () => _onTap("forwardMessage", model)),
|
||||
if (shouldShowReplyAction)
|
||||
MessageToolTipItem(
|
||||
label: TIM_t((dynamicQuote ?? model.chatConfig.isAtWhenReply) ? "回复" : "引用"),
|
||||
id: "replyMessage",
|
||||
iconImageAsset: "images/reply_message.png",
|
||||
onClick: () => _onTap("replyMessage", model)),
|
||||
MessageToolTipItem(
|
||||
label: TIM_t("多选"),
|
||||
id: "multiSelect",
|
||||
iconImageAsset: "images/multi_message.png",
|
||||
onClick: () => _onTap("multiSelect", model)),
|
||||
MessageToolTipItem(
|
||||
label: TIM_t("删除"),
|
||||
id: "delete",
|
||||
iconImageAsset: "images/delete_message.png",
|
||||
onClick: () => _onTap("delete", model)),
|
||||
if (showTranslation)
|
||||
MessageToolTipItem(
|
||||
label: TIM_t("翻译"),
|
||||
id: "translate",
|
||||
iconImageAsset: "images/translate.png",
|
||||
onClick: () => _onTap("translate", model)),
|
||||
if (shouldShowRevokeAction)
|
||||
MessageToolTipItem(
|
||||
label: TIM_t("撤回"),
|
||||
id: "revoke",
|
||||
iconImageAsset: "images/revoke_message.png",
|
||||
onClick: () => _onTap("revoke", model)),
|
||||
];
|
||||
final defaultTipsIds = defaultTipsList.map((e) => e.id);
|
||||
List<MessageToolTipItem> defaultFormattedTipsList = defaultTipsList;
|
||||
if (tooltipsConfig != null) {
|
||||
defaultFormattedTipsList = defaultTipsList.where((element) {
|
||||
final type = element.id;
|
||||
if (type == "copyMessage") {
|
||||
return tooltipsConfig.showCopyMessage;
|
||||
}
|
||||
if (type == "forwardMessage") {
|
||||
return tooltipsConfig.showForwardMessage && !(isDesktopScreen && widget.iSUseDefaultHoverBar);
|
||||
}
|
||||
if (type == "replyMessage") {
|
||||
return tooltipsConfig.showReplyMessage && !(isDesktopScreen && widget.iSUseDefaultHoverBar);
|
||||
}
|
||||
if (type == "delete") {
|
||||
return (!PlatformUtils().isWeb) && tooltipsConfig.showDeleteMessage;
|
||||
}
|
||||
if (type == "multiSelect") {
|
||||
return tooltipsConfig.showMultipleChoiceMessage;
|
||||
}
|
||||
|
||||
if (type == "revoke") {
|
||||
return tooltipsConfig.showRecallMessage;
|
||||
}
|
||||
if (type == "translate") {
|
||||
return tooltipsConfig.showTranslation && widget.message.elemType == MessageElemType.V2TIM_ELEM_TYPE_TEXT;
|
||||
}
|
||||
return true;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
final List<MessageToolTipItem>? customList = widget.toolTipsConfig?.additionalMessageToolTips != null
|
||||
? (widget.toolTipsConfig?.additionalMessageToolTips!(message, widget.onCloseTooltip))
|
||||
: [];
|
||||
|
||||
List<MessageToolTipItem> formattedTipsList = [
|
||||
...defaultFormattedTipsList,
|
||||
...?customList,
|
||||
];
|
||||
|
||||
List<dynamic> widgetList = [];
|
||||
if (isDesktopScreen) {
|
||||
widgetList = formattedTipsList
|
||||
.map(
|
||||
(item) => Material(
|
||||
color: Colors.white,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
item.onClick();
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(6),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Image.asset(
|
||||
item.iconImageAsset,
|
||||
package: defaultTipsIds.contains(item.id) ? 'tencent_cloud_chat_uikit' : null,
|
||||
width: 20,
|
||||
height: 20,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 4,
|
||||
width: 8,
|
||||
),
|
||||
Text(
|
||||
item.label,
|
||||
style: TextStyle(
|
||||
decoration: TextDecoration.none,
|
||||
color: theme.darkTextColor,
|
||||
fontSize: 12,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
} else {
|
||||
widgetList = formattedTipsList
|
||||
.map(
|
||||
(item) => Material(
|
||||
color: Colors.white,
|
||||
child: ItemInkWell(
|
||||
onTap: () {
|
||||
item.onClick();
|
||||
},
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Image.asset(
|
||||
item.iconImageAsset,
|
||||
package: defaultTipsIds.contains(item.id) ? 'tencent_cloud_chat_uikit' : null,
|
||||
width: 20,
|
||||
height: 20,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 4,
|
||||
width: 60,
|
||||
),
|
||||
Text(
|
||||
item.label,
|
||||
style: TextStyle(
|
||||
decoration: TextDecoration.none,
|
||||
color: theme.darkTextColor,
|
||||
fontSize: 10,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
if (widgetList.isEmpty && widget.isUseMessageReaction == false) {
|
||||
widget.onCloseTooltip();
|
||||
}
|
||||
|
||||
return widgetList;
|
||||
}
|
||||
|
||||
_onOpenDesktop(String path) {
|
||||
try {
|
||||
if (PlatformUtils().isDesktop && !PlatformUtils().isWindows) {
|
||||
launchUrl(Uri.file(path));
|
||||
} else {
|
||||
OpenFile.open(path);
|
||||
}
|
||||
// ignore: empty_catches
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
_onTap(String operation, TUIChatSeparateViewModel model) async {
|
||||
final messageItem = widget.message;
|
||||
final msgID = messageItem.msgID as String;
|
||||
switch (operation) {
|
||||
case "open":
|
||||
if (widget.message.fileElem != null) {
|
||||
_onOpenDesktop(TencentUtils.checkString(globalModal.getFileMessageLocation(widget.message.msgID)) ??
|
||||
TencentUtils.checkString(widget.message.fileElem!.localUrl) ??
|
||||
widget.message.fileElem?.path ??
|
||||
"");
|
||||
} else if (widget.message.imageElem != null) {
|
||||
_onOpenDesktop(TencentUtils.checkString(widget.message.imageElem!.imageList?[0]?.localUrl) ??
|
||||
TencentUtils.checkString(widget.message.imageElem?.path) ??
|
||||
"");
|
||||
} else if (widget.message.videoElem != null) {
|
||||
_onOpenDesktop(TencentUtils.checkString(widget.message.videoElem!.localVideoUrl) ??
|
||||
TencentUtils.checkString(widget.message.videoElem?.videoPath) ??
|
||||
"");
|
||||
}
|
||||
break;
|
||||
case "finder":
|
||||
String savePath = "";
|
||||
if (widget.message.fileElem != null) {
|
||||
savePath = (TencentUtils.checkString(globalModal.getFileMessageLocation(widget.message.msgID)) ??
|
||||
TencentUtils.checkString(widget.message.fileElem!.localUrl) ??
|
||||
widget.message.fileElem?.path ??
|
||||
"");
|
||||
} else if (widget.message.imageElem != null) {
|
||||
savePath = (TencentUtils.checkString(widget.message.imageElem!.imageList?[0]?.localUrl) ??
|
||||
TencentUtils.checkString(widget.message.imageElem?.path) ??
|
||||
"");
|
||||
} else if (widget.message.videoElem != null) {
|
||||
savePath = (TencentUtils.checkString(widget.message.videoElem!.localVideoUrl) ??
|
||||
TencentUtils.checkString(widget.message.videoElem?.videoPath) ??
|
||||
"");
|
||||
}
|
||||
final String fileDir = path.dirname(savePath);
|
||||
_onOpenDesktop(fileDir);
|
||||
break;
|
||||
case "delete":
|
||||
model.deleteMsg(msgID, webMessageInstance: messageItem.messageFromWeb);
|
||||
break;
|
||||
case "revoke":
|
||||
model.revokeMsg(msgID, !isRevocable(widget.message.timestamp!, model.chatConfig.upperRecallTime),
|
||||
messageItem.messageFromWeb);
|
||||
break;
|
||||
case 'translate':
|
||||
model.translateText(widget.message);
|
||||
break;
|
||||
case "multiSelect":
|
||||
model.updateMultiSelectStatus(true);
|
||||
model.setMessageItemChecked(widget.message, true);
|
||||
break;
|
||||
case "forwardMessage":
|
||||
model.setMessageItemChecked(widget.message, true);
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => ForwardMessageScreen(
|
||||
conversationType: ConvType.c2c,
|
||||
model: model,
|
||||
)));
|
||||
break;
|
||||
case "copyMessage":
|
||||
if (widget.message.elemType == MessageElemType.V2TIM_ELEM_TYPE_TEXT) {
|
||||
try {
|
||||
await Clipboard.setData(ClipboardData(text: widget.message.textElem?.text ?? ""));
|
||||
onTIMCallback(TIMCallback(type: TIMCallbackType.INFO, infoRecommendText: TIM_t("已复制"), infoCode: 6660408));
|
||||
// ignore: empty_catches
|
||||
} catch (e) {}
|
||||
}
|
||||
break;
|
||||
case "replyMessage":
|
||||
model.repliedMessage = widget.message;
|
||||
final dynamicQuote = model.chatConfig.isAtWhenReplyDynamic?.call(widget.message);
|
||||
final isSelf = widget.message.isSelf ?? true;
|
||||
final isGroup = TencentUtils.checkString(widget.message.groupID) != null;
|
||||
final isAtWhenReply = !isSelf &&
|
||||
isGroup &&
|
||||
(dynamicQuote ?? widget.allowAtUserWhenReply) &&
|
||||
widget.onLongPressForOthersHeadPortrait != null;
|
||||
|
||||
/// If replying to a self message, do not add a at tag, only requestFocus.
|
||||
widget.onLongPressForOthersHeadPortrait!(
|
||||
!isAtWhenReply ? null : widget.message.sender, !isAtWhenReply ? null : widget.message.nickName);
|
||||
break;
|
||||
default:
|
||||
onTIMCallback(TIMCallback(type: TIMCallbackType.INFO, infoRecommendText: TIM_t("暂未实现"), infoCode: 6660409));
|
||||
}
|
||||
widget.onCloseTooltip();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final TUITheme theme = value.theme;
|
||||
final isDesktopScreen = TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop;
|
||||
return MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider.value(value: widget.model),
|
||||
],
|
||||
builder: (BuildContext context, Widget? w) {
|
||||
final TUIChatSeparateViewModel model = Provider.of<TUIChatSeparateViewModel>(context);
|
||||
final bool haveExtraTipsConfig =
|
||||
widget.toolTipsConfig != null && widget.toolTipsConfig?.additionalItemBuilder != null;
|
||||
Widget? extraTipsActionItem = haveExtraTipsConfig
|
||||
? widget.toolTipsConfig!.additionalItemBuilder!(widget.message, widget.onCloseTooltip, null, context)
|
||||
: null;
|
||||
final message = widget.message;
|
||||
return Container(
|
||||
decoration: isDesktopScreen
|
||||
? BoxDecoration(
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0xCCbebebe),
|
||||
offset: Offset(2, 2),
|
||||
blurRadius: 10,
|
||||
spreadRadius: 1,
|
||||
),
|
||||
],
|
||||
border: Border.all(
|
||||
width: 1,
|
||||
color: hexToColor("dee0e3"),
|
||||
),
|
||||
color: Colors.white,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(10)),
|
||||
)
|
||||
: null,
|
||||
color: isDesktopScreen ? null : Colors.white,
|
||||
padding: EdgeInsets.symmetric(horizontal: 8, vertical: isDesktopScreen ? 8 : 4),
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: min(MediaQuery.of(context).size.width * 0.75, 350),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if ((!isDesktopScreen || widget.isShowMoreSticker) &&
|
||||
widget.isUseMessageReaction &&
|
||||
widget.selectEmojiPanelPosition == SelectEmojiPanelPosition.up)
|
||||
TIMUIKitMessageReactionEmojiSelectPanel(
|
||||
isShowMoreSticker: isShowMoreSticker,
|
||||
onSelect: (int value) => widget.onSelectSticker(value),
|
||||
onClickShowMore: (bool value) {
|
||||
setState(() {
|
||||
isShowMoreSticker = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
if (!isDesktopScreen &&
|
||||
widget.isUseMessageReaction &&
|
||||
widget.selectEmojiPanelPosition == SelectEmojiPanelPosition.up &&
|
||||
isShowMoreSticker == false)
|
||||
Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 6),
|
||||
child: const Divider(
|
||||
thickness: 1,
|
||||
indent: 0,
|
||||
// endIndent: 10,
|
||||
color: Colors.black12)),
|
||||
if (isShowMoreSticker == false)
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (!isDesktopScreen && widget.isUseMessageReaction)
|
||||
Expanded(
|
||||
child: Wrap(
|
||||
direction: Axis.horizontal,
|
||||
alignment: TUIKitScreenUtils.getFormFactor(context) == DeviceType.Mobile
|
||||
? WrapAlignment.start
|
||||
: WrapAlignment.start,
|
||||
spacing: 12,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
..._buildLongPressTipItem(theme, model, message),
|
||||
if (extraTipsActionItem != null) extraTipsActionItem
|
||||
],
|
||||
)),
|
||||
if (!isDesktopScreen && !widget.isUseMessageReaction)
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: min(MediaQuery.of(context).size.width * 0.75, 350),
|
||||
),
|
||||
child: Wrap(
|
||||
direction: Axis.horizontal,
|
||||
alignment: TUIKitScreenUtils.getFormFactor(context) == DeviceType.Mobile
|
||||
? WrapAlignment.spaceBetween
|
||||
: WrapAlignment.start,
|
||||
spacing: 4,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
..._buildLongPressTipItem(theme, model, message),
|
||||
if (extraTipsActionItem != null) extraTipsActionItem
|
||||
],
|
||||
),
|
||||
),
|
||||
if (isDesktopScreen)
|
||||
Table(columnWidths: const <int, TableColumnWidth>{
|
||||
0: IntrinsicColumnWidth(),
|
||||
}, children: <TableRow>[
|
||||
..._buildLongPressTipItem(theme, model, message).map((e) => TableRow(children: <Widget>[e]))
|
||||
])
|
||||
],
|
||||
),
|
||||
if (!isDesktopScreen &&
|
||||
widget.isUseMessageReaction &&
|
||||
widget.selectEmojiPanelPosition == SelectEmojiPanelPosition.down &&
|
||||
isShowMoreSticker == false)
|
||||
Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 6),
|
||||
child: const Divider(
|
||||
thickness: 1,
|
||||
indent: 0,
|
||||
// endIndent: 10,
|
||||
color: Colors.black12)),
|
||||
if ((!isDesktopScreen || widget.isShowMoreSticker) &&
|
||||
widget.isUseMessageReaction &&
|
||||
widget.selectEmojiPanelPosition == SelectEmojiPanelPosition.down)
|
||||
TIMUIKitMessageReactionEmojiSelectPanel(
|
||||
isShowMoreSticker: isShowMoreSticker,
|
||||
onSelect: (int value) => widget.onSelectSticker(value),
|
||||
onClickShowMore: (bool value) {
|
||||
setState(() {
|
||||
isShowMoreSticker = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
));
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
+203
@@ -0,0 +1,203 @@
|
||||
// ignore_for_file: deprecated_member_use_from_same_package
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:scroll_to_index/scroll_to_index.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_conversation.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_at_info.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_group_at_info.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_member_full_info.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_group_member_full_info.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_message.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitTextField/tim_uikit_text_field_controller.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_chat_separate_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_chat_global_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/constants/history_message_constant.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKItMessageList/TIMUIKitTongue/tim_uikit_chat_history_message_list_tongue.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKItMessageList/tim_uikit_chat_history_message_list_config.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKItMessageList/tim_uikit_chat_history_message_list_item.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKItMessageList/tim_uikit_chat_history_message_list.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/tim_uikit_chat_config.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart';
|
||||
import 'package:tim_ui_kit_sticker_plugin/utils/tim_custom_face_data.dart';
|
||||
|
||||
enum LoadingPlace {
|
||||
none,
|
||||
top,
|
||||
bottom,
|
||||
}
|
||||
|
||||
class TIMUIKitHistoryMessageListContainer extends StatefulWidget {
|
||||
final Widget Function(BuildContext, V2TimMessage?)? itemBuilder;
|
||||
final AutoScrollController? scrollController;
|
||||
final String conversationID;
|
||||
final Function(String? userId, String? nickName)? onLongPressForOthersHeadPortrait;
|
||||
final List<V2TimGroupAtInfo?>? groupAtInfoList;
|
||||
final V2TimMessage? initFindingMsg;
|
||||
|
||||
/// message item builder, works for customize all message types and row layout.
|
||||
final MessageItemBuilder? messageItemBuilder;
|
||||
|
||||
/// The controller for text field.
|
||||
final TIMUIKitInputTextFieldController? textFieldController;
|
||||
|
||||
/// the builder for avatar
|
||||
final Widget Function(BuildContext context, V2TimMessage message)? userAvatarBuilder;
|
||||
|
||||
/// the builder for tongue
|
||||
final TongueItemBuilder? tongueItemBuilder;
|
||||
|
||||
final Widget? Function(V2TimMessage message, Function() closeTooltip, [Key? key, BuildContext? context])?
|
||||
extraTipsActionItemBuilder;
|
||||
|
||||
/// conversation type
|
||||
final ConvType conversationType;
|
||||
|
||||
/// Avatar and name in message reaction tap callback.
|
||||
final void Function(String userID, TapDownDetails tapDetails)? onTapAvatar;
|
||||
|
||||
/// Avatar and name in message reaction secondary tap callback.
|
||||
final void Function(String userID, TapDownDetails tapDetails)? onSecondaryTapAvatar;
|
||||
|
||||
@Deprecated(
|
||||
"Nickname will not show in one-to-one chat, if you tend to control it in group chat, please use `isShowSelfNameInGroup` and `isShowOthersNameInGroup` from `config: TIMUIKitChatConfig` instead")
|
||||
final bool showNickName;
|
||||
|
||||
final TIMUIKitHistoryMessageListConfig? mainHistoryListConfig;
|
||||
|
||||
/// tool tips panel configuration, long press message will show tool tips panel
|
||||
final ToolTipsConfig? toolTipsConfig;
|
||||
|
||||
final List<CustomEmojiFaceData> customEmojiStickerList;
|
||||
|
||||
final bool isAllowScroll;
|
||||
|
||||
final V2TimConversation conversation;
|
||||
|
||||
final V2TimGroupMemberFullInfo? groupMemberInfo;
|
||||
|
||||
/// This parameter accepts a custom widget to be displayed when the mouse hovers over a message,
|
||||
/// replacing the default message hover action bar.
|
||||
/// Applicable only on desktop platforms.
|
||||
/// If provided, the default message action functionality will appear in the right-click context menu instead.
|
||||
final Widget? Function(V2TimMessage message)? customMessageHoverBarOnDesktop;
|
||||
|
||||
const TIMUIKitHistoryMessageListContainer({
|
||||
Key? key,
|
||||
this.itemBuilder,
|
||||
this.scrollController,
|
||||
required this.conversationID,
|
||||
required this.conversationType,
|
||||
this.userAvatarBuilder,
|
||||
this.onLongPressForOthersHeadPortrait,
|
||||
this.groupAtInfoList,
|
||||
this.messageItemBuilder,
|
||||
this.tongueItemBuilder,
|
||||
this.extraTipsActionItemBuilder,
|
||||
this.isAllowScroll = true,
|
||||
this.onTapAvatar,
|
||||
@Deprecated(
|
||||
"Nickname will not show in one-to-one chat, if you tend to control it in group chat, please use `isShowSelfNameInGroup` and `isShowOthersNameInGroup` from `config: TIMUIKitChatConfig` instead")
|
||||
this.showNickName = true,
|
||||
this.initFindingMsg,
|
||||
this.mainHistoryListConfig,
|
||||
this.toolTipsConfig,
|
||||
this.customEmojiStickerList = const [],
|
||||
this.textFieldController,
|
||||
required this.conversation,
|
||||
this.onSecondaryTapAvatar,
|
||||
this.groupMemberInfo,
|
||||
this.customMessageHoverBarOnDesktop,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _TIMUIKitHistoryMessageListContainerState();
|
||||
}
|
||||
|
||||
class _TIMUIKitHistoryMessageListContainerState extends TIMUIKitState<TIMUIKitHistoryMessageListContainer> {
|
||||
late TIMUIKitHistoryMessageListController _historyMessageListController;
|
||||
|
||||
List<V2TimMessage?> historyMessageList = [];
|
||||
|
||||
Future<bool> requestForData(String? lastMsgID, LoadDirection direction, TUIChatSeparateViewModel model,
|
||||
[int? count, int? lastSeq]) async {
|
||||
if ((direction == LoadDirection.previous) || (direction == LoadDirection.latest && model.haveMoreLatestData)) {
|
||||
return await model.loadChatRecord(
|
||||
direction: direction,
|
||||
count: count ?? (kIsWeb ? 15 : HistoryMessageDartConstant.getCount),
|
||||
lastMsgID: lastMsgID,
|
||||
lastMsgSeq: lastSeq ?? -1,
|
||||
);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Widget Function(BuildContext, V2TimMessage)? _getTopRowBuilder(TUIChatSeparateViewModel model) {
|
||||
if (widget.messageItemBuilder?.messageNickNameBuilder != null) {
|
||||
return (BuildContext context, V2TimMessage message) {
|
||||
return widget.messageItemBuilder!.messageNickNameBuilder!(context, message, model);
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_historyMessageListController = TIMUIKitHistoryMessageListController(scrollController: widget.scrollController);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final chatConfig = Provider.of<TIMUIKitChatConfig>(context);
|
||||
final TUIChatSeparateViewModel model = Provider.of<TUIChatSeparateViewModel>(context, listen: false);
|
||||
|
||||
return TIMUIKitHistoryMessageListSelector(
|
||||
conversationID: model.conversationID,
|
||||
builder: (context, messageList, child) {
|
||||
return TIMUIKitHistoryMessageList(
|
||||
conversation: widget.conversation,
|
||||
model: model,
|
||||
isAllowScroll: widget.isAllowScroll,
|
||||
controller: _historyMessageListController,
|
||||
groupAtInfoList: widget.groupAtInfoList,
|
||||
mainHistoryListConfig: widget.mainHistoryListConfig,
|
||||
itemBuilder: (context, message) {
|
||||
return TIMUIKitHistoryMessageListItem(
|
||||
customMessageHoverBarOnDesktop: widget.customMessageHoverBarOnDesktop,
|
||||
groupMemberInfo: widget.groupMemberInfo,
|
||||
textFieldController: widget.textFieldController,
|
||||
userAvatarBuilder: widget.userAvatarBuilder,
|
||||
customEmojiStickerList: widget.customEmojiStickerList,
|
||||
topRowBuilder: _getTopRowBuilder(model),
|
||||
onScrollToIndex: _historyMessageListController.scrollToIndex,
|
||||
onScrollToIndexBegin: _historyMessageListController.scrollToIndexBegin,
|
||||
toolTipsConfig:
|
||||
widget.toolTipsConfig ?? ToolTipsConfig(additionalItemBuilder: widget.extraTipsActionItemBuilder),
|
||||
message: message!,
|
||||
showAvatar: chatConfig.isShowAvatar,
|
||||
onSecondaryTapForOthersPortrait: widget.onSecondaryTapAvatar,
|
||||
onTapForOthersPortrait: widget.onTapAvatar,
|
||||
messageItemBuilder: widget.messageItemBuilder,
|
||||
onLongPressForOthersHeadPortrait: widget.onLongPressForOthersHeadPortrait,
|
||||
allowAtUserWhenReply: chatConfig.isAtWhenReply,
|
||||
allowAvatarTap: chatConfig.isAllowClickAvatar,
|
||||
allowLongPress: chatConfig.isAllowLongPressMessage,
|
||||
isUseMessageReaction: chatConfig.isUseMessageReaction);
|
||||
},
|
||||
tongueItemBuilder: widget.tongueItemBuilder,
|
||||
initFindingMsg: widget.initFindingMsg,
|
||||
messageList: messageList,
|
||||
onLoadMore: (String? a, LoadDirection direction, [int? b, int? lastSeq]) async {
|
||||
return await requestForData(a, direction, model, b, lastSeq);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:tencent_chat_i18n_tool/tencent_chat_i18n_tool.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_message.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_message_receipt.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_message_receipt.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_statelesswidget.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_chat_separate_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_chat_global_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/core/tim_uikit_wide_modal_operation_key.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/message_read_receipt.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/tui_theme.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/wide_popup.dart';
|
||||
|
||||
class TIMUIKitMessageReadReceipt extends TIMUIKitStatelessWidget {
|
||||
final V2TimMessage messageItem;
|
||||
final void Function(String, TapDownDetails tapDetails)? onTapAvatar;
|
||||
|
||||
TIMUIKitMessageReadReceipt({Key? key, this.onTapAvatar, required this.messageItem}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final TUITheme theme = value.theme;
|
||||
final TUIChatSeparateViewModel model = Provider.of<TUIChatSeparateViewModel>(context, listen: false);
|
||||
final isDesktopScreen = TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop;
|
||||
|
||||
return Selector<TUIChatGlobalModel, V2TimMessageReceipt?>(
|
||||
builder: (context, value, child) {
|
||||
if (model.conversationType == ConvType.c2c) {
|
||||
bool isPeerRead = false;
|
||||
if (value != null) {
|
||||
isPeerRead = value.isPeerRead ?? false;
|
||||
}
|
||||
return Container(
|
||||
padding: const EdgeInsets.only(bottom: 3),
|
||||
margin: const EdgeInsets.only(right: 6),
|
||||
child: Text(
|
||||
isPeerRead ? TIM_t("已读") : TIM_t("未读"),
|
||||
style: TextStyle(color: theme.chatMessageItemUnreadStatusTextColor, fontSize: 12),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () {
|
||||
if ((value?.readCount ?? 0) > 0) {
|
||||
if (isDesktopScreen) {
|
||||
TUIKitWidePopup.showPopupWindow(
|
||||
operationKey: TUIKitWideModalOperationKey.messageReadDetails,
|
||||
context: context,
|
||||
width: MediaQuery.of(context).size.width * 0.5,
|
||||
height: MediaQuery.of(context).size.height * 0.8,
|
||||
title: TIM_t("消息详情"),
|
||||
child: (onClose) => MessageReadReceipt(
|
||||
model: model,
|
||||
onTapAvatar: onTapAvatar,
|
||||
messageItem: messageItem,
|
||||
unreadCount: value?.unreadCount ?? 0,
|
||||
readCount: value?.readCount ?? 0));
|
||||
} else {
|
||||
if (value?.unreadCount == 0) {
|
||||
return;
|
||||
}
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => MessageReadReceipt(
|
||||
model: model,
|
||||
onTapAvatar: onTapAvatar,
|
||||
messageItem: messageItem,
|
||||
unreadCount: value?.unreadCount ?? 0,
|
||||
readCount: value?.readCount ?? 0)));
|
||||
}
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
padding: EdgeInsets.only(bottom: 3, right: 6, left: 6, top: isDesktopScreen ? 2 : 6),
|
||||
child: ((value?.unreadCount ?? 0) == 0 && (value?.readCount ?? 0) > 0)
|
||||
? Icon(
|
||||
Icons.check_circle_outline,
|
||||
size: 18,
|
||||
color: theme.weakTextColor,
|
||||
)
|
||||
: Container(
|
||||
width: 14,
|
||||
height: 14,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
width: 1.3,
|
||||
color: (value?.readCount ?? 0) > 0 ? theme.primaryColor! : theme.weakTextColor!)),
|
||||
child: (value?.readCount ?? 0) > 0
|
||||
? Text(
|
||||
'${value?.readCount ?? 0}',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 8, color: theme.primaryColor),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
selector: (c, model) {
|
||||
return model.getMessageReadReceipt(messageItem.msgID ?? "");
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_message.dart';
|
||||
|
||||
class TIMUIKitChatUtils {
|
||||
static String? getMessageIDWithinIndex(List<V2TimMessage?> messageList, int index) {
|
||||
if (messageList[index]!.elemType == 11) {
|
||||
if (index > 0) {
|
||||
return getMessageIDWithinIndex(messageList, index - 1);
|
||||
}
|
||||
}
|
||||
return messageList[index]!.msgID;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
// ignore_for_file: deprecated_member_use
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:tencent_chat_i18n_tool/tencent_chat_i18n_tool.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/enum/V2TimFriendshipListener.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/enum/V2TimGroupListener.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/enum/group_change_info_type.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_chat_separate_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_chat_global_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/friendShip/friendship_services.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/group/group_services.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/services_locatar.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitAppBar/tim_uikit_appbar_title.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/color.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/tui_theme.dart';
|
||||
|
||||
class TIMUIKitAppBar extends StatefulWidget implements PreferredSizeWidget {
|
||||
/// Appbar config
|
||||
final AppBar? config;
|
||||
|
||||
/// Allow show conversation total unread count
|
||||
final bool showTotalUnReadCount;
|
||||
|
||||
/// Conversation id
|
||||
final String conversationID;
|
||||
|
||||
/// conversation name
|
||||
final String conversationShowName;
|
||||
|
||||
/// If allow update the conversation show name automatically.
|
||||
final bool isConversationShowNameFixed;
|
||||
|
||||
final bool showC2cMessageEditStatus;
|
||||
|
||||
final GestureTapDownCallback? onClickTitle;
|
||||
|
||||
const TIMUIKitAppBar({
|
||||
Key? key,
|
||||
this.config,
|
||||
this.isConversationShowNameFixed = false,
|
||||
this.showTotalUnReadCount = true,
|
||||
this.conversationID = "",
|
||||
this.conversationShowName = "",
|
||||
this.showC2cMessageEditStatus = true,
|
||||
this.onClickTitle,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Size get preferredSize =>
|
||||
config?.preferredSize ?? const Size.fromHeight(56.0);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _TIMUIKitAppBarState();
|
||||
}
|
||||
|
||||
class _TIMUIKitAppBarState extends TIMUIKitState<TIMUIKitAppBar> {
|
||||
final FriendshipServices _friendshipServices =
|
||||
serviceLocator<FriendshipServices>();
|
||||
final GroupServices _groupServices = serviceLocator<GroupServices>();
|
||||
|
||||
V2TimFriendshipListener? _friendshipListener;
|
||||
V2TimGroupListener? _groupListener;
|
||||
|
||||
String _conversationShowName = "";
|
||||
|
||||
_addConversationShowNameListener() {
|
||||
_friendshipListener = V2TimFriendshipListener(
|
||||
onFriendInfoChanged: (infoList) {
|
||||
try {
|
||||
final changedInfo = infoList.firstWhere(
|
||||
(element) => element.userID == widget.conversationID,
|
||||
);
|
||||
if (changedInfo.friendRemark != null &&
|
||||
changedInfo.friendRemark!.isNotEmpty) {
|
||||
_conversationShowName = changedInfo.friendRemark!;
|
||||
setState(() {});
|
||||
} else {
|
||||
_conversationShowName = (changedInfo.userProfile?.nickName ??
|
||||
changedInfo.userProfile?.userID) ??
|
||||
"";
|
||||
}
|
||||
// ignore: empty_catches
|
||||
} catch (e) {}
|
||||
},
|
||||
);
|
||||
if (_friendshipListener != null) {
|
||||
_friendshipServices.addFriendListener(listener: _friendshipListener!);
|
||||
}
|
||||
}
|
||||
|
||||
_addGroupListener() {
|
||||
_groupListener = V2TimGroupListener(
|
||||
onGroupInfoChanged: (groupID, changeInfos) {
|
||||
try {
|
||||
if (groupID == widget.conversationID) {
|
||||
final groupNameChangeInfo = changeInfos.firstWhere((element) =>
|
||||
element.type ==
|
||||
GroupChangeInfoType.V2TIM_GROUP_INFO_CHANGE_TYPE_NAME);
|
||||
if (groupNameChangeInfo.value != null) {
|
||||
_conversationShowName = groupNameChangeInfo.value!;
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
// ignore: empty_catches
|
||||
} catch (e) {}
|
||||
},
|
||||
);
|
||||
if (_groupListener != null) {
|
||||
_groupServices.addGroupListener(listener: _groupListener!);
|
||||
}
|
||||
}
|
||||
|
||||
String _getTotalUnReadCount(int unreadCount) {
|
||||
return unreadCount < 99 ? unreadCount.toString() : "99";
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_conversationShowName = widget.conversationShowName;
|
||||
if (!widget.isConversationShowNameFixed) {
|
||||
_addConversationShowNameListener();
|
||||
_addGroupListener();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
if (!widget.isConversationShowNameFixed) {
|
||||
_groupServices.removeGroupListener(listener: _groupListener);
|
||||
_friendshipServices.removeFriendListener(listener: _friendshipListener);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(TIMUIKitAppBar oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.conversationShowName != widget.conversationShowName) {
|
||||
if (widget.conversationShowName.isNotEmpty) {
|
||||
setState(() {
|
||||
_conversationShowName = widget.conversationShowName;
|
||||
});
|
||||
} else {
|
||||
updateTitleFuture();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void updateTitleFuture() async {
|
||||
try {
|
||||
final res = await _friendshipServices
|
||||
.getFriendsInfo(userIDList: [widget.conversationID]);
|
||||
if (res != null && res.isNotEmpty && res.first.resultCode == 0) {
|
||||
setState(() {
|
||||
_conversationShowName = res.first.friendInfo?.userProfile?.nickName ??
|
||||
res.first.friendInfo?.userProfile?.userID ??
|
||||
"";
|
||||
});
|
||||
}
|
||||
// ignore: empty_catches
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final TUITheme theme = value.theme;
|
||||
|
||||
final setAppbar = widget.config;
|
||||
final TUIChatSeparateViewModel chatVM =
|
||||
Provider.of<TUIChatSeparateViewModel>(context);
|
||||
final isDesktopScreen =
|
||||
TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop;
|
||||
return AppBar(
|
||||
backgroundColor: setAppbar?.backgroundColor ??
|
||||
theme.chatHeaderBgColor ??
|
||||
theme.appbarBgColor ??
|
||||
theme.primaryColor,
|
||||
actionsIconTheme: setAppbar?.actionsIconTheme,
|
||||
foregroundColor: setAppbar?.foregroundColor,
|
||||
elevation: setAppbar?.elevation ?? (isDesktopScreen ? 0 : 1),
|
||||
bottom: setAppbar?.bottom,
|
||||
bottomOpacity: setAppbar?.bottomOpacity ?? 1.0,
|
||||
titleSpacing: setAppbar?.titleSpacing,
|
||||
automaticallyImplyLeading: setAppbar?.automaticallyImplyLeading ?? false,
|
||||
shadowColor: setAppbar?.shadowColor ?? theme.weakDividerColor,
|
||||
excludeHeaderSemantics: setAppbar?.excludeHeaderSemantics ?? false,
|
||||
toolbarHeight: setAppbar?.toolbarHeight,
|
||||
titleTextStyle: setAppbar?.titleTextStyle,
|
||||
toolbarOpacity: setAppbar?.toolbarOpacity ?? 1.0,
|
||||
toolbarTextStyle: setAppbar?.toolbarTextStyle,
|
||||
|
||||
// textTheme: setAppbar?.textTheme,
|
||||
iconTheme: setAppbar?.iconTheme ??
|
||||
const IconThemeData(
|
||||
color: Colors.white,
|
||||
),
|
||||
title: TIMUIKitAppBarTitle(
|
||||
title: setAppbar?.title,
|
||||
onClick: widget.onClickTitle,
|
||||
textStyle: TextStyle(
|
||||
color: theme.appbarTextColor ?? hexToColor("010000"), fontSize: 16),
|
||||
conversationShowName: _conversationShowName,
|
||||
showC2cMessageEditStatus: widget.showC2cMessageEditStatus,
|
||||
fromUser: widget.conversationID,
|
||||
),
|
||||
centerTitle: setAppbar?.centerTitle ?? (isDesktopScreen ? false : true),
|
||||
leadingWidth: setAppbar?.leadingWidth ?? (isDesktopScreen ? 8 : 70),
|
||||
leading: Selector<TUIChatGlobalModel, Tuple2<bool, int>>(
|
||||
builder: (context, data, _) {
|
||||
final isMultiSelect = data.item1;
|
||||
final unReadCount = data.item2;
|
||||
return (!isDesktopScreen && isMultiSelect)
|
||||
? TextButton(
|
||||
onPressed: () {
|
||||
chatVM.updateMultiSelectStatus(false);
|
||||
},
|
||||
child: Text(
|
||||
TIM_t('取消'),
|
||||
style: TextStyle(
|
||||
color: theme.appbarTextColor ?? hexToColor("010000"),
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
)
|
||||
: setAppbar?.leading ??
|
||||
(isDesktopScreen
|
||||
? Container()
|
||||
: Row(
|
||||
children: [
|
||||
IconButton(
|
||||
padding: const EdgeInsets.only(left: 16),
|
||||
constraints: const BoxConstraints(),
|
||||
icon: Icon(
|
||||
Icons.arrow_back_ios,
|
||||
color: hexToColor("010000"),
|
||||
size: 17,
|
||||
),
|
||||
onPressed: () async {
|
||||
chatVM.repliedMessage = null;
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
if (widget.showTotalUnReadCount &&
|
||||
unReadCount > 0)
|
||||
Container(
|
||||
width: 22,
|
||||
height: 22,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: theme.cautionColor,
|
||||
),
|
||||
child:
|
||||
Text(_getTotalUnReadCount(unReadCount)),
|
||||
),
|
||||
],
|
||||
));
|
||||
},
|
||||
shouldRebuild: (prev, next) =>
|
||||
prev.item1 != next.item1 || prev.item2 != next.item2,
|
||||
selector: (_, model) =>
|
||||
Tuple2(chatVM.isMultiSelect, model.totalUnReadCount)),
|
||||
actions: setAppbar?.actions,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:tencent_chat_i18n_tool/tencent_chat_i18n_tool.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_chat_global_model.dart';
|
||||
|
||||
class TIMUIKitAppBarTitle extends StatelessWidget {
|
||||
final Widget? title;
|
||||
final String conversationShowName;
|
||||
final bool showC2cMessageEditStatus;
|
||||
final String fromUser;
|
||||
final GestureTapDownCallback? onClick;
|
||||
final TextStyle? textStyle;
|
||||
|
||||
const TIMUIKitAppBarTitle(
|
||||
{Key? key,
|
||||
this.title,
|
||||
this.textStyle,
|
||||
required this.conversationShowName,
|
||||
required this.showC2cMessageEditStatus,
|
||||
required this.fromUser, this.onClick})
|
||||
: super(key: key);
|
||||
|
||||
Widget titleText(String text){
|
||||
return InkWell(
|
||||
onTapDown: onClick,
|
||||
child: Text(
|
||||
text,
|
||||
style: textStyle ??
|
||||
const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 17,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// String conversationShowName;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
int status = Provider.of<TUIChatGlobalModel>(context, listen: true)
|
||||
.getC2cMessageEditStatus(fromUser);
|
||||
if (status == 0) {
|
||||
if (title != null) {
|
||||
return title!;
|
||||
}
|
||||
return titleText(conversationShowName,);
|
||||
} else {
|
||||
if (showC2cMessageEditStatus) {
|
||||
return titleText(
|
||||
TIM_t("对方正在输入中..."),);
|
||||
|
||||
} else {
|
||||
if (title != null) {
|
||||
return title!;
|
||||
}
|
||||
return titleText(
|
||||
conversationShowName,);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
List<Map<String, Object>> messageReactionEmojiData = [
|
||||
{"name": "red heart", "unicode": 10084},
|
||||
{"name": "OK", "unicode": 128076},
|
||||
{"name": "thumbs up", "unicode": 128077},
|
||||
{"name": "flexed biceps", "unicode": 128170},
|
||||
{"name": "waving hand", "unicode": 128079},
|
||||
{"name": "handshake", "unicode": 129309},
|
||||
{"name": "folded hands", "unicode": 128591},
|
||||
{"name": "raised fist", "unicode": 9994},
|
||||
{"name": "thumbs down", "unicode": 128078},
|
||||
{"name": "smiling face with sunglasses", "unicode": 128526},
|
||||
{"name": "GRINNING FACE WITH SMILING EYES", "unicode": 128513},
|
||||
{"name": "SMILING FACE WITH SMILING EYES", "unicode": 128522},
|
||||
{"name": "smiling face with hearts", "unicode": 129392},
|
||||
{"name": "SMILING FACE WITH HEART-SHAPED EYES", "unicode": 128525},
|
||||
{"name": "FACE WITH TEARS OF JOY", "unicode": 128514},
|
||||
{"name": "flushed face", "unicode": 128563},
|
||||
{"name": "SMILING FACE WITH OPEN MOUTH AND COLD SWEAT", "unicode": 128517},
|
||||
{"name": "FACE WITH COLD SWEAT", "unicode": 128531},
|
||||
{"name": "loudly crying face", "unicode": 128557},
|
||||
{"name": "CONFOUNDED FACE", "unicode": 128534},
|
||||
{"name": "KISSING FACE WITH CLOSED EYES", "unicode": 128538},
|
||||
{
|
||||
"name": "FACE WITH STUCK-OUT TONGUE AND TIGHTLY-CLOSED EYES",
|
||||
"unicode": 128541
|
||||
},
|
||||
{"name": "DISAPPOINTED FACE", "unicode": 128542},
|
||||
{"name": "ANGRY FACE", "unicode": 128544},
|
||||
{"name": "POUTING FACE", "unicode": 128545},
|
||||
{"name": "CRYING FACE", "unicode": 128546},
|
||||
{"name": "FACE WITH LOOK OF TRIUMPH", "unicode": 128548},
|
||||
{"name": "FEARFUL FACE", "unicode": 128552},
|
||||
{"name": "SLEEPY FACE", "unicode": 128554},
|
||||
{"name": "FACE WITH NO GOOD GESTURE", "unicode": 128581},
|
||||
{"name": "FACE WITH OK GESTURE", "unicode": 128582},
|
||||
];
|
||||
+181
@@ -0,0 +1,181 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_member_full_info.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_group_member_full_info.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_self_info_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/services_locatar.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/avatar.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/color.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/tui_theme.dart';
|
||||
|
||||
class TIMUIKitMessageReactionDetail extends StatefulWidget {
|
||||
/// the index of the current emoji sticker
|
||||
final int currentStickerIndex;
|
||||
|
||||
/// the list of member
|
||||
final List<V2TimGroupMemberFullInfo?>? memberList;
|
||||
|
||||
/// message reaction map
|
||||
final Map<String, dynamic> messageReaction;
|
||||
|
||||
/// the sticker list from message reaction
|
||||
final List<int> stickerList;
|
||||
|
||||
final Function(String userID, TapDownDetails tapDetails)? onTapAvatar;
|
||||
|
||||
const TIMUIKitMessageReactionDetail(
|
||||
{required this.currentStickerIndex,
|
||||
this.memberList,
|
||||
required this.messageReaction,
|
||||
Key? key,
|
||||
required this.stickerList,
|
||||
this.onTapAvatar})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => TIMUIKitMessageReactionDetailState();
|
||||
}
|
||||
|
||||
class TIMUIKitMessageReactionDetailState extends TIMUIKitState<TIMUIKitMessageReactionDetail>
|
||||
with TickerProviderStateMixin {
|
||||
final TUISelfInfoViewModel selfInfoModel = serviceLocator<TUISelfInfoViewModel>();
|
||||
|
||||
Widget getUserItem(String userID, TUITheme theme, Function(String userID, TapDownDetails tapDetails)? onTapAvatar) {
|
||||
V2TimGroupMemberFullInfo? memberInfo;
|
||||
String showName = userID;
|
||||
try {
|
||||
memberInfo = widget.memberList?.firstWhere((element) => element?.userID == userID);
|
||||
if (memberInfo != null) {
|
||||
if (memberInfo.friendRemark != null && memberInfo.friendRemark!.isNotEmpty) {
|
||||
showName = memberInfo.friendRemark!;
|
||||
} else if (memberInfo.nameCard != null && memberInfo.nameCard!.isNotEmpty) {
|
||||
showName = memberInfo.nameCard!;
|
||||
} else if (memberInfo.nickName != null && memberInfo.nickName!.isNotEmpty) {
|
||||
showName = memberInfo.nickName!;
|
||||
} else {
|
||||
showName = memberInfo.userID;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// e
|
||||
}
|
||||
|
||||
return InkWell(
|
||||
onTapDown: (tapDetails) {
|
||||
if (onTapAvatar != null) {
|
||||
if (userID != selfInfoModel.loginInfo?.userID) {
|
||||
onTapAvatar(userID, tapDetails);
|
||||
}
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border(bottom: BorderSide(color: theme.weakDividerColor ?? CommonColor.weakDividerColor))),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
margin: const EdgeInsets.only(right: 12),
|
||||
child: SizedBox(
|
||||
height: 40,
|
||||
width: 40,
|
||||
child: (memberInfo?.faceUrl != null)
|
||||
? Avatar(faceUrl: memberInfo!.faceUrl!, showName: userID)
|
||||
: Container(),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
padding: const EdgeInsets.only(top: 20, bottom: 20, right: 28),
|
||||
child: Text(
|
||||
showName,
|
||||
style: TextStyle(color: theme.black, fontSize: 18),
|
||||
),
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget stickerItem(int sticker, int length) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(top: 12, bottom: 12),
|
||||
padding: const EdgeInsets.only(left: 10, right: 10, top: 2, bottom: 1),
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0x198a8a8a),
|
||||
borderRadius: BorderRadius.all(Radius.circular(20)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.only(bottom: 3),
|
||||
child: Text(
|
||||
String.fromCharCode(sticker),
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
margin: const EdgeInsets.only(left: 6),
|
||||
child: Text(length.toString()),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget getStickerNameList(
|
||||
int sticker, TUITheme theme, Function(String userID, TapDownDetails tapDetails)? onTapAvatar) {
|
||||
final nameList = widget.messageReaction[sticker.toString()];
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [...nameList.map((e) => getUserItem(e, theme, onTapAvatar))],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final theme = value.theme;
|
||||
return DefaultTabController(
|
||||
initialIndex: widget.currentStickerIndex,
|
||||
length: widget.stickerList.length,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
margin: const EdgeInsets.only(bottom: 4),
|
||||
child: TabBar(
|
||||
isScrollable: true,
|
||||
labelColor: theme.primaryColor,
|
||||
labelStyle: const TextStyle(fontWeight: FontWeight.bold),
|
||||
unselectedLabelColor: hexToColor("62626b"),
|
||||
unselectedLabelStyle: const TextStyle(fontWeight: FontWeight.normal),
|
||||
indicatorSize: TabBarIndicatorSize.label,
|
||||
indicatorColor: theme.primaryColor ?? hexToColor("62626b"),
|
||||
tabs: [
|
||||
...widget.stickerList.map((element) {
|
||||
return stickerItem(element, widget.messageReaction[element.toString()].length);
|
||||
})
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
children: widget.stickerList
|
||||
.map((int sticker) => getStickerNameList(sticker, theme, widget.onTapAvatar))
|
||||
.toList()))
|
||||
],
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitMessageItem/TIMUIKitMessageReaction/message_reaction_emoji.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitTextField/tim_uikit_emoji_panel.dart'
|
||||
as emoji;
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/emoji.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/extended_wrap/extended_wrap.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/color.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/tui_theme.dart';
|
||||
|
||||
enum SelectEmojiPanelPosition { up, down }
|
||||
|
||||
class TIMUIKitMessageReactionEmojiSelectPanel extends StatefulWidget {
|
||||
final ValueChanged<int> onSelect;
|
||||
final bool isShowMoreSticker;
|
||||
final ValueChanged<bool> onClickShowMore;
|
||||
|
||||
const TIMUIKitMessageReactionEmojiSelectPanel(
|
||||
{Key? key,
|
||||
required this.onSelect,
|
||||
required this.isShowMoreSticker,
|
||||
required this.onClickShowMore})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() =>
|
||||
TIMUIKitMessageReactionEmojiSelectPanelState();
|
||||
}
|
||||
|
||||
class TIMUIKitMessageReactionEmojiSelectPanelState
|
||||
extends TIMUIKitState<TIMUIKitMessageReactionEmojiSelectPanel> {
|
||||
bool isShowMore = false;
|
||||
|
||||
_buildSimplePanel(TUITheme theme) {
|
||||
final List<Map<String, Object>> emojiData = messageReactionEmojiData;
|
||||
final isDesktopScreen = TUIKitScreenUtils.getFormFactor() == DeviceType.Desktop;
|
||||
return Material(
|
||||
color: Colors.white,
|
||||
child: ExtendedWrap(
|
||||
maxLines: widget.isShowMoreSticker ? 5 : 1,
|
||||
spacing: 18,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
runSpacing: 24,
|
||||
children: [
|
||||
if(!isDesktopScreen)
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
widget.onClickShowMore(!widget.isShowMoreSticker);
|
||||
},
|
||||
child: SizedBox(
|
||||
height: 34,
|
||||
child: Icon(
|
||||
widget.isShowMoreSticker
|
||||
? Icons.cancel_outlined
|
||||
: Icons.add_circle_outline_outlined,
|
||||
color: hexToColor("444444"),
|
||||
size: 26),
|
||||
),
|
||||
),
|
||||
...emojiData.map(
|
||||
(e) {
|
||||
var item = Emoji.fromJson(e);
|
||||
return SizedBox(
|
||||
// width: 50,
|
||||
child: InkWell(
|
||||
splashColor: Colors.white,
|
||||
onTap: () {
|
||||
widget.onSelect(item.unicode);
|
||||
},
|
||||
child: emoji.EmojiItem(
|
||||
name: item.name,
|
||||
unicode: item.unicode,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
).toList()
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final TUITheme theme = value.theme;
|
||||
|
||||
return Container(
|
||||
child: _buildSimplePanel(theme),
|
||||
);
|
||||
}
|
||||
}
|
||||
+198
@@ -0,0 +1,198 @@
|
||||
// ignore_for_file: unused_field
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:tencent_chat_i18n_tool/tencent_chat_i18n_tool.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_member_full_info.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_group_member_full_info.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_message.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_message_change_info.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_message_change_info.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_value_callback.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_value_callback.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_statelesswidget.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_chat_separate_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_self_info_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/message/message_services.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/services_locatar.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/platform.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitMessageItem/TIMUIKitMessageReaction/tim_uikit_message_reaction_utils.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/extended_wrap/extended_wrap.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/color.dart';
|
||||
|
||||
class TIMUIKitMessageReactionShowItem extends TIMUIKitStatelessWidget {
|
||||
/// the unicode of the emoji
|
||||
final int sticker;
|
||||
|
||||
/// the list contains the name who choose the current emoji
|
||||
final List nameList;
|
||||
|
||||
/// current message
|
||||
final V2TimMessage message;
|
||||
|
||||
/// show the details of message reaction
|
||||
final Function(int sticker) onShowDetail;
|
||||
|
||||
/// the member in current chat
|
||||
final List<V2TimGroupMemberFullInfo?> memberList;
|
||||
|
||||
TIMUIKitMessageReactionShowItem(
|
||||
{required this.message,
|
||||
required this.sticker,
|
||||
required this.memberList,
|
||||
required this.onShowDetail,
|
||||
required this.nameList,
|
||||
Key? key})
|
||||
: super(key: key);
|
||||
|
||||
final TUISelfInfoViewModel selfInfoModel = serviceLocator<TUISelfInfoViewModel>();
|
||||
final MessageService _messageService = serviceLocator<MessageService>();
|
||||
|
||||
clickOnCurrentSticker() async {
|
||||
for (int i = 0; i < 5; i++) {
|
||||
final res = await modifySticker();
|
||||
if (res.code == 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<V2TimValueCallback<V2TimMessageChangeInfo>> modifySticker() async {
|
||||
return await Future.delayed(const Duration(milliseconds: 50), () async {
|
||||
return await MessageReactionUtils.clickOnSticker(message, sticker);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final theme = value.theme;
|
||||
final option1 = nameList.length;
|
||||
final TUIChatSeparateViewModel model = Provider.of<TUIChatSeparateViewModel>(context);
|
||||
|
||||
final List<String> userIDs = [];
|
||||
for (final user in nameList) {
|
||||
final V2TimGroupMemberFullInfo? memberInfo =
|
||||
memberList.firstWhereOrNull((element) => element?.userID == user && TencentUtils.checkString(user) != null);
|
||||
if ((memberInfo == null || TencentUtils.checkString(memberInfo.userID) == null) &&
|
||||
TencentUtils.checkString(user.toString()) != null) {
|
||||
userIDs.add(user.toString());
|
||||
}
|
||||
}
|
||||
if (userIDs.isNotEmpty) {
|
||||
model.getUserShowName(userIDs);
|
||||
}
|
||||
|
||||
return LayoutBuilder(builder: (context, constraints) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 10,
|
||||
right: 10,
|
||||
),
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0x19727271),
|
||||
borderRadius: BorderRadius.all(Radius.circular(20)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
InkWell(
|
||||
onTap: clickOnCurrentSticker,
|
||||
child: Container(
|
||||
margin:
|
||||
EdgeInsets.only(bottom: (!PlatformUtils().isIOS) ? 4 : 2, top: (!PlatformUtils().isIOS) ? 4 : 0),
|
||||
child: Text(
|
||||
String.fromCharCode(sticker),
|
||||
style: TextStyle(fontSize: (!PlatformUtils().isIOS) ? 12 : 16, color: hexToColor("f9453d")),
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
margin: const EdgeInsets.only(left: 6, right: 6),
|
||||
child: SizedBox(
|
||||
width: 1,
|
||||
height: 14,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(color: theme.weakTextColor),
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: constraints.maxWidth * 0.8,
|
||||
),
|
||||
child: ExtendedWrap(
|
||||
maxLines: 1,
|
||||
spacing: 8,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
overflowWidget: GestureDetector(
|
||||
onTap: () {
|
||||
onShowDetail(sticker);
|
||||
},
|
||||
child: Text(
|
||||
TIM_t_para("...共{{option1}}人", "...共$option1人")(option1: option1),
|
||||
style: TextStyle(fontSize: 12, color: hexToColor("616669")),
|
||||
),
|
||||
),
|
||||
children: [
|
||||
...nameList.map((e) {
|
||||
String showName = e;
|
||||
if (memberList.isNotEmpty) {
|
||||
try {
|
||||
final V2TimGroupMemberFullInfo? memberInfo =
|
||||
memberList.firstWhere((element) => element?.userID == e);
|
||||
if (memberInfo != null) {
|
||||
if (memberInfo.friendRemark != null && memberInfo.friendRemark!.isNotEmpty) {
|
||||
showName = memberInfo.friendRemark!;
|
||||
} else if (memberInfo.nameCard != null && memberInfo.nameCard!.isNotEmpty) {
|
||||
showName = memberInfo.nameCard!;
|
||||
} else if (memberInfo.nickName != null && memberInfo.nickName!.isNotEmpty) {
|
||||
showName = memberInfo.nickName!;
|
||||
} else {
|
||||
showName = memberInfo.userID;
|
||||
}
|
||||
} else {
|
||||
final String? data = model.groupUserShowName[e];
|
||||
if (TencentUtils.checkString(data) != null) {
|
||||
showName = data ?? e;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
final String? data = model.groupUserShowName[e];
|
||||
if (TencentUtils.checkString(data) != null) {
|
||||
showName = data ?? e;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
final String? data = model.groupUserShowName[e];
|
||||
if (TencentUtils.checkString(data) != null) {
|
||||
showName = data ?? e;
|
||||
}
|
||||
}
|
||||
return InkWell(
|
||||
onTapDown: (tapDetails) {
|
||||
if (model.onTapAvatar != null) {
|
||||
if (e != selfInfoModel.loginInfo?.userID) {
|
||||
model.onTapAvatar!(e, tapDetails);
|
||||
}
|
||||
}
|
||||
},
|
||||
child: Text(
|
||||
showName,
|
||||
style: TextStyle(fontSize: 12, color: hexToColor("616669")),
|
||||
),
|
||||
);
|
||||
})
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:tencent_chat_i18n_tool/tencent_chat_i18n_tool.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_member_full_info.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_group_member_full_info.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_message.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_statelesswidget.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_chat_separate_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_chat_global_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_self_info_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/services_locatar.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/platform.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitMessageItem/TIMUIKitMessageReaction/tim_uikit_message_reaction_detail.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitMessageItem/TIMUIKitMessageReaction/tim_uikit_message_reaction_show_item.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitMessageItem/TIMUIKitMessageReaction/tim_uikit_message_reaction_utils.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/tim_uikit_cloud_custom_data.dart';
|
||||
|
||||
class TIMUIKitMessageReactionShowPanel extends TIMUIKitStatelessWidget {
|
||||
/// current message
|
||||
final V2TimMessage message;
|
||||
|
||||
TIMUIKitMessageReactionShowPanel({required this.message, Key? key}) : super(key: key);
|
||||
|
||||
final TUISelfInfoViewModel selfInfoModel = serviceLocator<TUISelfInfoViewModel>();
|
||||
|
||||
void showMore(BuildContext context, List<V2TimGroupMemberFullInfo?>? memberList, Map<String, dynamic> messageReaction,
|
||||
int currentSticker, List<int> stickerList, TUIChatSeparateViewModel model) async {
|
||||
_showCustomModalBottomSheet(context, memberList, messageReaction, currentSticker, stickerList, model);
|
||||
}
|
||||
|
||||
Future<Future<int?>> _showCustomModalBottomSheet(
|
||||
context,
|
||||
List<V2TimGroupMemberFullInfo?>? memberList,
|
||||
Map<String, dynamic> messageReaction,
|
||||
int currentSticker,
|
||||
List<int> stickerList,
|
||||
TUIChatSeparateViewModel model) async {
|
||||
return showModalBottomSheet<int>(
|
||||
backgroundColor: Colors.transparent,
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return Container(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(20.0),
|
||||
topRight: Radius.circular(20.0),
|
||||
),
|
||||
),
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: MediaQuery.of(context).size.height * 0.66,
|
||||
minHeight: MediaQuery.of(context).size.height * 0.2,
|
||||
),
|
||||
child: Column(children: [
|
||||
SizedBox(
|
||||
height: 50,
|
||||
child: Stack(
|
||||
textDirection: TextDirection.rtl,
|
||||
children: [
|
||||
Center(
|
||||
child: Text(
|
||||
TIM_t("回应详情"),
|
||||
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16.0),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(height: 1.0),
|
||||
Expanded(
|
||||
child: TIMUIKitMessageReactionDetail(
|
||||
onTapAvatar: model.onTapAvatar,
|
||||
stickerList: stickerList,
|
||||
currentStickerIndex: stickerList.indexWhere((element) => element == currentSticker),
|
||||
memberList: memberList,
|
||||
messageReaction: messageReaction,
|
||||
)),
|
||||
]),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
Map<String, dynamic> messageReaction = {};
|
||||
CloudCustomData messageCloudCustomData = MessageReactionUtils.getCloudCustomData(message);
|
||||
final TUIChatSeparateViewModel model = Provider.of<TUIChatSeparateViewModel>(context);
|
||||
if (messageCloudCustomData.messageReaction != null && messageCloudCustomData.messageReaction!.isNotEmpty) {
|
||||
messageReaction = messageCloudCustomData.messageReaction!;
|
||||
} else {
|
||||
return const SizedBox(width: 0, height: 0);
|
||||
}
|
||||
|
||||
final List<int> messageReactionStickerList = [];
|
||||
|
||||
messageReaction.forEach((key, value) {
|
||||
messageReactionStickerList.add(int.parse(key));
|
||||
});
|
||||
|
||||
final filteredMessageReactionStickerList = messageReactionStickerList.where((sticker) {
|
||||
if (messageReaction[sticker.toString()] == null ||
|
||||
messageReaction[sticker.toString()] is! List ||
|
||||
messageReaction[sticker.toString()].length == 0) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}).toList();
|
||||
|
||||
final ConvType convType = model.conversationType ?? ConvType.c2c;
|
||||
List<V2TimGroupMemberFullInfo?> memberList = [];
|
||||
if (convType == ConvType.group) {
|
||||
memberList = model.groupMemberList ?? [];
|
||||
} else {
|
||||
final V2TimGroupMemberFullInfo selfInfo = V2TimGroupMemberFullInfo(
|
||||
userID: selfInfoModel.loginInfo?.userID ?? "",
|
||||
nickName: selfInfoModel.loginInfo?.nickName,
|
||||
faceUrl: selfInfoModel.loginInfo?.faceUrl,
|
||||
);
|
||||
|
||||
final V2TimGroupMemberFullInfo targetInfo = V2TimGroupMemberFullInfo(
|
||||
userID: model.conversationID,
|
||||
);
|
||||
memberList = [selfInfo, model.currentChatUserInfo ?? targetInfo];
|
||||
}
|
||||
|
||||
return filteredMessageReactionStickerList.isNotEmpty
|
||||
? Container(
|
||||
margin: const EdgeInsets.only(top: 12),
|
||||
child: Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: (!PlatformUtils().isIOS) ? 12 : 8,
|
||||
children: [
|
||||
...filteredMessageReactionStickerList.map((sticker) {
|
||||
return TIMUIKitMessageReactionShowItem(
|
||||
memberList: memberList,
|
||||
message: message,
|
||||
nameList: messageReaction[sticker.toString()],
|
||||
sticker: sticker,
|
||||
onShowDetail: (int sticker) {
|
||||
showMore(
|
||||
context, memberList, messageReaction, sticker, filteredMessageReactionStickerList, model);
|
||||
});
|
||||
}).toList(),
|
||||
],
|
||||
),
|
||||
)
|
||||
: const SizedBox(width: 0, height: 0);
|
||||
}
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_message.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_message_change_info.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_message_change_info.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_value_callback.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_value_callback.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_self_info_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/message/message_services.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/services_locatar.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/platform.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/tim_uikit_cloud_custom_data.dart';
|
||||
|
||||
class MessageReactionUtils {
|
||||
static final TUISelfInfoViewModel selfInfoModel = serviceLocator<TUISelfInfoViewModel>();
|
||||
static final MessageService _messageService = serviceLocator<MessageService>();
|
||||
|
||||
static CloudCustomData getCloudCustomData(V2TimMessage message) {
|
||||
CloudCustomData messageCloudCustomData;
|
||||
try {
|
||||
messageCloudCustomData = CloudCustomData.fromJson(
|
||||
json.decode(TencentUtils.checkString(message.cloudCustomData) != null ? message.cloudCustomData! : "{}"));
|
||||
} catch (e) {
|
||||
messageCloudCustomData = CloudCustomData();
|
||||
}
|
||||
|
||||
return messageCloudCustomData;
|
||||
}
|
||||
|
||||
static Map<String, dynamic> getMessageReaction(V2TimMessage message) {
|
||||
return getCloudCustomData(message).messageReaction ?? {};
|
||||
}
|
||||
|
||||
static Future<V2TimValueCallback<V2TimMessageChangeInfo>> clickOnSticker(V2TimMessage message, int sticker) async {
|
||||
final CloudCustomData messageCloudCustomData = getCloudCustomData(message);
|
||||
final Map<String, dynamic> messageReaction = messageCloudCustomData.messageReaction ?? {};
|
||||
List targetList = messageReaction["$sticker"] ?? [];
|
||||
if (targetList.contains(selfInfoModel.loginInfo!.userID!)) {
|
||||
targetList.remove(selfInfoModel.loginInfo!.userID!);
|
||||
} else {
|
||||
targetList = [selfInfoModel.loginInfo!.userID!, ...targetList];
|
||||
}
|
||||
messageReaction["$sticker"] = targetList;
|
||||
|
||||
if (PlatformUtils().isWeb) {
|
||||
final decodedMessage = jsonDecode(message.messageFromWeb!);
|
||||
decodedMessage["cloudCustomData"] = jsonEncode(messageCloudCustomData.toMap());
|
||||
message.messageFromWeb = jsonEncode(decodedMessage);
|
||||
} else {
|
||||
message.cloudCustomData = json.encode(messageCloudCustomData.toMap());
|
||||
}
|
||||
return await _messageService.modifyMessage(message: message);
|
||||
}
|
||||
}
|
||||
+175
@@ -0,0 +1,175 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_message.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/common_utils.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_chat_separate_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitMessageItem/TIMUIKitMessageReaction/tim_uikit_message_reaction_show_panel.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/tim_uikit_cloud_custom_data.dart';
|
||||
|
||||
class TIMUIKitMessageReactionWrapper extends StatefulWidget {
|
||||
final Widget child;
|
||||
final V2TimMessage message;
|
||||
final Color? backgroundColor;
|
||||
final bool isFromSelf;
|
||||
final BorderRadius? borderRadius;
|
||||
final bool isShowJump;
|
||||
final VoidCallback? clearJump;
|
||||
final bool isShowMessageReaction;
|
||||
final TUIChatSeparateViewModel chatModel;
|
||||
|
||||
const TIMUIKitMessageReactionWrapper(
|
||||
{Key? key,
|
||||
required this.isShowJump,
|
||||
this.clearJump,
|
||||
required this.isFromSelf,
|
||||
this.backgroundColor,
|
||||
required this.chatModel,
|
||||
required this.message,
|
||||
this.borderRadius,
|
||||
required this.child,
|
||||
this.isShowMessageReaction = true})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _TIMUIKitMessageReactionWrapperState();
|
||||
}
|
||||
|
||||
class _TIMUIKitMessageReactionWrapperState extends TIMUIKitState<TIMUIKitMessageReactionWrapper> {
|
||||
bool isShowJumpState = false;
|
||||
bool isShining = false;
|
||||
bool isShowBorder = false;
|
||||
|
||||
_showJumpColor() {
|
||||
if ((widget.chatModel.jumpMsgID != widget.message.msgID) && (widget.message.msgID?.isNotEmpty ?? true)) {
|
||||
return;
|
||||
}
|
||||
isShining = true;
|
||||
int shineAmount = 6;
|
||||
setState(() {
|
||||
isShowJumpState = true;
|
||||
isShowBorder = true;
|
||||
});
|
||||
Timer.periodic(const Duration(milliseconds: 300), (timer) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
isShowJumpState = shineAmount.isOdd ? true : false;
|
||||
isShowBorder = shineAmount.isOdd ? true : false;
|
||||
});
|
||||
}
|
||||
if (shineAmount == 0 || !mounted) {
|
||||
isShining = false;
|
||||
timer.cancel();
|
||||
}
|
||||
shineAmount--;
|
||||
});
|
||||
if (widget.clearJump != null) {
|
||||
widget.clearJump!();
|
||||
}
|
||||
}
|
||||
|
||||
bool isHaveMessageReaction() {
|
||||
Map<String, dynamic> messageReaction = {};
|
||||
CloudCustomData messageCloudCustomData;
|
||||
try {
|
||||
messageCloudCustomData = CloudCustomData.fromJson(json.decode(
|
||||
TencentUtils.checkString(widget.message.cloudCustomData) != null ? widget.message.cloudCustomData! : "{}"));
|
||||
} catch (e) {
|
||||
messageCloudCustomData = CloudCustomData();
|
||||
}
|
||||
|
||||
if (messageCloudCustomData.messageReaction != null && messageCloudCustomData.messageReaction!.isNotEmpty) {
|
||||
messageReaction = messageCloudCustomData.messageReaction!;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
final List<int> messageReactionStickerList = [];
|
||||
|
||||
messageReaction.forEach((key, value) {
|
||||
messageReactionStickerList.add(int.parse(key));
|
||||
});
|
||||
|
||||
final filteredMessageReactionStickerList = messageReactionStickerList.where((sticker) {
|
||||
if (messageReaction[sticker.toString()] == null ||
|
||||
messageReaction[sticker.toString()] is! List ||
|
||||
messageReaction[sticker.toString()].length == 0) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
if (filteredMessageReactionStickerList.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final theme = value.theme;
|
||||
final borderRadius = widget.isFromSelf
|
||||
? const BorderRadius.only(
|
||||
topLeft: Radius.circular(10),
|
||||
topRight: Radius.circular(2),
|
||||
bottomLeft: Radius.circular(10),
|
||||
bottomRight: Radius.circular(10))
|
||||
: const BorderRadius.only(
|
||||
topLeft: Radius.circular(2),
|
||||
topRight: Radius.circular(10),
|
||||
bottomLeft: Radius.circular(10),
|
||||
bottomRight: Radius.circular(10));
|
||||
|
||||
if (widget.isShowJump) {
|
||||
if (!isShining) {
|
||||
Future.delayed(Duration.zero, () {
|
||||
_showJumpColor();
|
||||
});
|
||||
} else {
|
||||
if ((widget.chatModel.jumpMsgID == widget.message.msgID) && (widget.message.msgID?.isNotEmpty ?? false)) {
|
||||
if (widget.clearJump != null) {
|
||||
widget.clearJump!();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final defaultStyle = widget.isFromSelf ? theme.lightPrimaryMaterialColor.shade50 : theme.weakBackgroundColor;
|
||||
final backgroundColor =
|
||||
isShowJumpState ? const Color.fromRGBO(245, 166, 35, 1) : (widget.backgroundColor ?? defaultStyle);
|
||||
|
||||
if (!widget.isShowMessageReaction || !isHaveMessageReaction()) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(5)),
|
||||
border: Border.all(color: Color.fromRGBO(245, 166, 35, (isShowBorder ? 1 : 0)), width: 2)),
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
borderRadius: widget.borderRadius ?? borderRadius,
|
||||
),
|
||||
constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.7),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(5)),
|
||||
border: Border.all(color: Color.fromRGBO(245, 166, 35, (isShowBorder ? 1 : 0)), width: 2)),
|
||||
child: widget.child,
|
||||
),
|
||||
if (widget.isShowMessageReaction) TIMUIKitMessageReactionShowPanel(message: widget.message)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitMessageItem/tim_uikit_chat_file_elem.dart';
|
||||
export 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitMessageItem/tim_uikit_chat_group_tips_elem.dart';
|
||||
export 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitMessageItem/tim_uikit_chat_image_elem.dart';
|
||||
export 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitMessageItem/tim_uikit_chat_sound_elem.dart';
|
||||
export 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitMessageItem/tim_uikit_chat_text_elem.dart';
|
||||
export 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitMessageItem/tim_uikit_chat_video_elem.dart';
|
||||
export 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitMessageItem/tim_uikit_merger_message_elem.dart';
|
||||
export 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitMessageItem/tim_uikit_chat_reply_elem.dart';
|
||||
@@ -0,0 +1,60 @@
|
||||
// ignore_for_file: unrelated_type_equality_checks
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:tencent_chat_i18n_tool/tencent_chat_i18n_tool.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_custom_elem.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_custom_elem.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_message.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_statelesswidget.dart';
|
||||
|
||||
class TIMUIKitCustomElem extends TIMUIKitStatelessWidget {
|
||||
final V2TimCustomElem? customElem;
|
||||
final bool isFromSelf;
|
||||
final TextStyle? messageFontStyle;
|
||||
final BorderRadius? messageBorderRadius;
|
||||
final Color? messageBackgroundColor;
|
||||
final EdgeInsetsGeometry? textPadding;
|
||||
final V2TimMessage message;
|
||||
final bool? isShowMessageReaction;
|
||||
|
||||
TIMUIKitCustomElem({
|
||||
Key? key,
|
||||
required this.message,
|
||||
this.isShowMessageReaction,
|
||||
this.customElem,
|
||||
this.isFromSelf = false,
|
||||
this.messageFontStyle,
|
||||
this.messageBorderRadius,
|
||||
this.messageBackgroundColor,
|
||||
this.textPadding,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final theme = value.theme;
|
||||
final borderRadius = isFromSelf
|
||||
? const BorderRadius.only(
|
||||
topLeft: Radius.circular(10),
|
||||
topRight: Radius.circular(2),
|
||||
bottomLeft: Radius.circular(10),
|
||||
bottomRight: Radius.circular(10))
|
||||
: const BorderRadius.only(
|
||||
topLeft: Radius.circular(2),
|
||||
topRight: Radius.circular(10),
|
||||
bottomLeft: Radius.circular(10),
|
||||
bottomRight: Radius.circular(10));
|
||||
final backgroundColor = isFromSelf ? theme.lightPrimaryMaterialColor.shade50 : theme.weakBackgroundColor;
|
||||
return Container(
|
||||
padding: textPadding ?? const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: messageBackgroundColor ?? backgroundColor,
|
||||
borderRadius: messageBorderRadius ?? borderRadius,
|
||||
),
|
||||
constraints: const BoxConstraints(maxWidth: 240),
|
||||
child: Column(
|
||||
children: [Text(TIM_t("自定义消息"))],
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_message.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_chat_separate_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitMessageItem/TIMUIKitMessageReaction/tim_uikit_message_reaction_wrapper.dart';
|
||||
|
||||
class TIMUIKitFaceElem extends StatefulWidget {
|
||||
final String path;
|
||||
final bool isShowJump;
|
||||
final VoidCallback? clearJump;
|
||||
final V2TimMessage message;
|
||||
final bool? isShowMessageReaction;
|
||||
final TUIChatSeparateViewModel model;
|
||||
|
||||
const TIMUIKitFaceElem(
|
||||
{Key? key,
|
||||
required this.path,
|
||||
required this.isShowJump,
|
||||
this.clearJump,
|
||||
required this.message,
|
||||
this.isShowMessageReaction,
|
||||
required this.model})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _TIMUIKitTextElemState();
|
||||
}
|
||||
|
||||
class _TIMUIKitTextElemState extends TIMUIKitState<TIMUIKitFaceElem> {
|
||||
bool isFromNetwork() {
|
||||
return widget.path.startsWith('http');
|
||||
}
|
||||
|
||||
createPathFromNative(String path) {
|
||||
String prefix = "";
|
||||
String suffix = "";
|
||||
if (widget.model.chatConfig.faceURIPrefix != null) {
|
||||
prefix = widget.model.chatConfig.faceURIPrefix!(path);
|
||||
}
|
||||
if (widget.model.chatConfig.faceURISuffix != null) {
|
||||
suffix = widget.model.chatConfig.faceURISuffix!(path);
|
||||
}
|
||||
return "$prefix$path$suffix";
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final isDesktopScreen = TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop;
|
||||
return TIMUIKitMessageReactionWrapper(
|
||||
chatModel: widget.model,
|
||||
isShowJump: widget.isShowJump,
|
||||
isFromSelf: widget.message.isSelf ?? true,
|
||||
clearJump: widget.clearJump,
|
||||
message: widget.message,
|
||||
isShowMessageReaction: widget.isShowMessageReaction ?? true,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * (isDesktopScreen ? 0.1 : 0.3)),
|
||||
child: isFromNetwork() ? Image.network(widget.path) : Image.asset(createPathFromNative(widget.path)),
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,472 @@
|
||||
// ignore_for_file: avoid_print
|
||||
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:loading_animation_widget/loading_animation_widget.dart';
|
||||
import 'package:open_file/open_file.dart';
|
||||
import 'package:tencent_chat_i18n_tool/tencent_chat_i18n_tool.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/enum/V2TimAdvancedMsgListener.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_file_elem.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_file_elem.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_message.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_message_download_progress.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_message_download_progress.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/tencent_im_sdk_plugin.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_callback.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_chat_separate_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_chat_global_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/services_locatar.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/color.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/tui_theme.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/permission.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/platform.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitMessageItem/TIMUIKitMessageReaction/tim_uikit_message_reaction_wrapper.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitMessageItem/tim_uikit_chat_file_icon.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/textSize.dart';
|
||||
import 'package:universal_html/html.dart' as html;
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class TIMUIKitFileElem extends StatefulWidget {
|
||||
final String? messageID;
|
||||
final V2TimFileElem? fileElem;
|
||||
final bool isSelf;
|
||||
final bool isShowJump;
|
||||
final VoidCallback? clearJump;
|
||||
final V2TimMessage message;
|
||||
final bool? isShowMessageReaction;
|
||||
final TUIChatSeparateViewModel chatModel;
|
||||
|
||||
const TIMUIKitFileElem(
|
||||
{Key? key,
|
||||
required this.chatModel,
|
||||
required this.messageID,
|
||||
required this.fileElem,
|
||||
required this.isSelf,
|
||||
required this.isShowJump,
|
||||
this.clearJump,
|
||||
required this.message,
|
||||
this.isShowMessageReaction})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _TIMUIKitFileElemState();
|
||||
}
|
||||
|
||||
class _TIMUIKitFileElemState extends TIMUIKitState<TIMUIKitFileElem> {
|
||||
String filePath = "";
|
||||
bool isWebDownloading = false;
|
||||
final TUIChatGlobalModel model = serviceLocator<TUIChatGlobalModel>();
|
||||
int downloadProgress = 0;
|
||||
V2TimAdvancedMsgListener? advancedMsgListener;
|
||||
final GlobalKey containerKey = GlobalKey();
|
||||
double? containerHeight;
|
||||
bool? _downloadFailed = false;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (advancedMsgListener != null) {
|
||||
TencentImSDKPlugin.v2TIMManager.getMessageManager().removeAdvancedMsgListener(listener: advancedMsgListener);
|
||||
advancedMsgListener = null;
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (!PlatformUtils().isWeb) {
|
||||
Future.delayed(const Duration(microseconds: 10), () {
|
||||
hasFile();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> addAdvancedMsgListenerForDownload() async {
|
||||
if (advancedMsgListener != null) {
|
||||
return false;
|
||||
}
|
||||
advancedMsgListener = V2TimAdvancedMsgListener(
|
||||
onMessageDownloadProgressCallback: (V2TimMessageDownloadProgress messageProgress) async {
|
||||
if (messageProgress.msgID == widget.message.msgID) {
|
||||
if (messageProgress.isError || messageProgress.errorCode != 0) {
|
||||
setState(() {
|
||||
_downloadFailed = true;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (messageProgress.isFinish) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
downloadProgress = 100;
|
||||
});
|
||||
|
||||
if (advancedMsgListener != null) {
|
||||
TencentImSDKPlugin.v2TIMManager
|
||||
.getMessageManager()
|
||||
.removeAdvancedMsgListener(listener: advancedMsgListener);
|
||||
advancedMsgListener = null;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
final currentProgress = (messageProgress.currentSize / messageProgress.totalSize * 100).floor();
|
||||
if (mounted && currentProgress > downloadProgress) {
|
||||
setState(() {
|
||||
downloadProgress = currentProgress;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
await TencentImSDKPlugin.v2TIMManager.getMessageManager().addAdvancedMsgListener(listener: advancedMsgListener!);
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<String> getSavePath() async {
|
||||
String savePathWithAppPath = '/storage/emulated/0/Android/data/com.tencent.flutter.tuikit/cache/' +
|
||||
(widget.message.msgID ?? "") +
|
||||
widget.fileElem!.fileName!;
|
||||
return savePathWithAppPath;
|
||||
}
|
||||
|
||||
Future<bool> hasFile() async {
|
||||
if (PlatformUtils().isWeb) {
|
||||
return true;
|
||||
}
|
||||
String savePath = TencentUtils.checkString(model.getFileMessageLocation(widget.messageID)) ??
|
||||
TencentUtils.checkString(widget.message.fileElem!.localUrl) ??
|
||||
widget.message.fileElem?.path ??
|
||||
'';
|
||||
|
||||
File f = File(savePath);
|
||||
if (widget.messageID != null) {
|
||||
if (f.existsSync()) {
|
||||
filePath = savePath;
|
||||
if (downloadProgress != 100) {
|
||||
setState(() {
|
||||
downloadProgress = 100;
|
||||
});
|
||||
}
|
||||
if (model.getMessageProgress(widget.messageID) != 100) {
|
||||
model.setMessageProgress(widget.messageID!, 100);
|
||||
}
|
||||
if (advancedMsgListener != null) {
|
||||
TencentImSDKPlugin.v2TIMManager.getMessageManager().removeAdvancedMsgListener(listener: advancedMsgListener);
|
||||
advancedMsgListener = null;
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
model.setMessageProgress(widget.messageID!, 0);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
String showFileSize(int fileSize) {
|
||||
if (fileSize < 1024) {
|
||||
return fileSize.toString() + "B";
|
||||
} else if (fileSize < 1024 * 1024) {
|
||||
return (fileSize / 1024).toStringAsFixed(2) + "KB";
|
||||
} else if (fileSize < 1024 * 1024 * 1024) {
|
||||
return (fileSize / 1024 / 1024).toStringAsFixed(2) + "MB";
|
||||
} else {
|
||||
return (fileSize / 1024 / 1024 / 1024).toStringAsFixed(2) + "GB";
|
||||
}
|
||||
}
|
||||
|
||||
addUrlToWaitingPath(TUITheme theme) async {
|
||||
if (widget.messageID != null) {
|
||||
model.addWaitingList(widget.messageID!);
|
||||
}
|
||||
if (model.getWaitingListLength() == 1) {
|
||||
await downloadFile(theme);
|
||||
}
|
||||
}
|
||||
|
||||
checkIsWaiting() {
|
||||
bool res = false;
|
||||
try {
|
||||
if (widget.messageID!.isNotEmpty) {
|
||||
res = model.isWaiting(widget.messageID!);
|
||||
}
|
||||
} catch (err) {
|
||||
// err
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
downloadFile(TUITheme theme) async {
|
||||
if (PlatformUtils().isMobile) {
|
||||
if (PlatformUtils().isIOS) {
|
||||
if (!await Permissions.checkPermission(context, Permission.photosAddOnly.value, theme, false)) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
final DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
|
||||
AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo;
|
||||
if ((androidInfo.version.sdkInt) >= 33) {
|
||||
} else {
|
||||
var storage = await Permissions.checkPermission(
|
||||
context,
|
||||
Permission.storage.value,
|
||||
);
|
||||
if (!storage) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
await model.downloadFile();
|
||||
}
|
||||
|
||||
Future<bool> hasZeroSize(String filePath) async {
|
||||
try {
|
||||
final file = File(filePath);
|
||||
final fileSize = await file.length();
|
||||
return fileSize == 0;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
tryOpenFile(context, theme) async {
|
||||
if (!PlatformUtils().isWeb && (await hasZeroSize(filePath) || widget.message.status == 3)) {
|
||||
onTIMCallback(TIMCallback(type: TIMCallbackType.INFO, infoRecommendText: "不支持 0KB 文件的传输", infoCode: 6660417));
|
||||
return;
|
||||
}
|
||||
if (PlatformUtils().isMobile) {
|
||||
if (PlatformUtils().isIOS) {
|
||||
if (!await Permissions.checkPermission(context, Permission.photosAddOnly.value, theme!, false)) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
final DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
|
||||
AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo;
|
||||
if ((androidInfo.version.sdkInt) >= 33) {
|
||||
} else {
|
||||
var storage = await Permissions.checkPermission(
|
||||
context,
|
||||
Permission.storage.value,
|
||||
);
|
||||
if (!storage) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (PlatformUtils().isDesktop && !PlatformUtils().isWindows) {
|
||||
launchUrl(Uri.file(filePath));
|
||||
} else {
|
||||
OpenFile.open(filePath);
|
||||
}
|
||||
// ignore: empty_catches
|
||||
} catch (e) {
|
||||
OpenFile.open(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
void downloadWebFile(String fileUrl) async {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
isWebDownloading = true;
|
||||
});
|
||||
}
|
||||
String fileName = Uri.parse(fileUrl).pathSegments.last;
|
||||
try {
|
||||
http.Response response = await http.get(
|
||||
Uri.parse(fileUrl),
|
||||
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||||
);
|
||||
|
||||
final html.AnchorElement downloadAnchor = html.document.createElement('a') as html.AnchorElement;
|
||||
|
||||
final html.Blob blob = html.Blob([response.bodyBytes]);
|
||||
|
||||
downloadAnchor.href = html.Url.createObjectUrlFromBlob(blob);
|
||||
downloadAnchor.download = widget.message.fileElem?.fileName ?? fileName;
|
||||
|
||||
downloadAnchor.click();
|
||||
} catch (e) {
|
||||
html.AnchorElement(
|
||||
href: widget.fileElem?.path ?? "",
|
||||
)
|
||||
..setAttribute("download", widget.message.fileElem?.fileName ?? fileName)
|
||||
..setAttribute("target", '_blank')
|
||||
..style.display = "none"
|
||||
..click();
|
||||
}
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
isWebDownloading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final theme = value.theme;
|
||||
final received = downloadProgress;
|
||||
final fileName = widget.fileElem!.fileName ?? "";
|
||||
final fileSize = widget.fileElem!.fileSize;
|
||||
final borderRadius = widget.isSelf
|
||||
? const BorderRadius.only(
|
||||
topLeft: Radius.circular(10),
|
||||
topRight: Radius.circular(2),
|
||||
bottomLeft: Radius.circular(10),
|
||||
bottomRight: Radius.circular(10))
|
||||
: const BorderRadius.only(
|
||||
topLeft: Radius.circular(2),
|
||||
topRight: Radius.circular(10),
|
||||
bottomLeft: Radius.circular(10),
|
||||
bottomRight: Radius.circular(10));
|
||||
String? fileFormat;
|
||||
if (widget.fileElem?.fileName != null && widget.fileElem!.fileName!.isNotEmpty) {
|
||||
final String fileName = widget.fileElem!.fileName!;
|
||||
fileFormat = fileName.split(".")[max(fileName.split(".").length - 1, 0)];
|
||||
}
|
||||
final RenderBox? containerRenderBox = containerKey.currentContext?.findRenderObject() as RenderBox?;
|
||||
if (containerRenderBox != null) {
|
||||
containerHeight = containerRenderBox.size.height;
|
||||
}
|
||||
|
||||
return Row(
|
||||
key: containerKey,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (widget.isSelf && isWebDownloading)
|
||||
Container(
|
||||
margin: const EdgeInsets.only(top: 2),
|
||||
child: LoadingAnimationWidget.threeArchedCircle(
|
||||
color: theme.weakTextColor ?? Colors.grey,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
TIMUIKitMessageReactionWrapper(
|
||||
chatModel: widget.chatModel,
|
||||
isShowJump: widget.isShowJump,
|
||||
clearJump: widget.clearJump,
|
||||
isFromSelf: widget.message.isSelf ?? true,
|
||||
isShowMessageReaction: widget.isShowMessageReaction ?? true,
|
||||
message: widget.message,
|
||||
child: GestureDetector(
|
||||
onTap: () async {
|
||||
try {
|
||||
if (PlatformUtils().isWeb) {
|
||||
if (!isWebDownloading) {
|
||||
downloadWebFile(widget.fileElem?.path ?? "");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
await addAdvancedMsgListenerForDownload();
|
||||
if (await hasFile()) {
|
||||
if (received == 100) {
|
||||
tryOpenFile(context, theme);
|
||||
} else {
|
||||
onTIMCallback(
|
||||
TIMCallback(
|
||||
type: TIMCallbackType.INFO,
|
||||
infoRecommendText: TIM_t("正在下载中"),
|
||||
infoCode: 6660411,
|
||||
),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (checkIsWaiting()) {
|
||||
onTIMCallback(
|
||||
TIMCallback(
|
||||
type: TIMCallbackType.INFO, infoRecommendText: TIM_t("已加入待下载队列,其他文件下载中"), infoCode: 6660413),
|
||||
);
|
||||
return;
|
||||
} else {
|
||||
await addUrlToWaitingPath(theme);
|
||||
}
|
||||
} catch (e) {
|
||||
onTIMCallback(
|
||||
TIMCallback(type: TIMCallbackType.INFO, infoRecommendText: "文件处理异常", infoCode: 6660416));
|
||||
}
|
||||
},
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxHeight: 72),
|
||||
child: Container(
|
||||
width: 170,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: theme.weakDividerColor ?? CommonColor.weakDividerColor,
|
||||
),
|
||||
borderRadius: borderRadius),
|
||||
child: Stack(children: [
|
||||
ClipRRect(
|
||||
borderRadius: borderRadius,
|
||||
child: LinearProgressIndicator(
|
||||
minHeight: ((containerHeight) ?? 72) - 6,
|
||||
value: (received == 100 ? 0 : received) / 100,
|
||||
backgroundColor: received == 100 ? theme.weakBackgroundColor : Colors.white,
|
||||
valueColor: AlwaysStoppedAnimation(theme.lightPrimaryMaterialColor.shade50),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12),
|
||||
child: Row(
|
||||
mainAxisAlignment: widget.isSelf ? MainAxisAlignment.end : MainAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
constraints: const BoxConstraints(maxWidth: 160),
|
||||
child: LayoutBuilder(
|
||||
builder: (buildContext, boxConstraints) {
|
||||
return CustomText(
|
||||
fileName,
|
||||
width: boxConstraints.maxWidth,
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
color: theme.darkTextColor,
|
||||
fontSize: 16,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
if (fileSize != null)
|
||||
Text(
|
||||
showFileSize(fileSize),
|
||||
style: TextStyle(fontSize: 14, color: theme.weakTextColor),
|
||||
)
|
||||
],
|
||||
)),
|
||||
TIMUIKitFileIcon(
|
||||
fileFormat: fileFormat,
|
||||
),
|
||||
])),
|
||||
]),
|
||||
),
|
||||
),
|
||||
)),
|
||||
if (!widget.isSelf && isWebDownloading)
|
||||
Container(
|
||||
margin: const EdgeInsets.only(top: 2),
|
||||
child: LoadingAnimationWidget.threeArchedCircle(
|
||||
color: theme.weakTextColor ?? Colors.grey,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
// ignore_for_file: must_be_immutable
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_statelesswidget.dart';
|
||||
|
||||
class TIMUIKitFileIcon extends TIMUIKitStatelessWidget {
|
||||
final String? fileFormat;
|
||||
final double? size;
|
||||
|
||||
TIMUIKitFileIcon( {this.size, this.fileFormat, Key? key}) : super(key: key);
|
||||
|
||||
Map fileMap = {
|
||||
"doc": "images/word.png",
|
||||
"docx": "images/word.png",
|
||||
"ppt": "images/ppt.png",
|
||||
"pptx": "images/ppt.png",
|
||||
"xls": "images/excel.png",
|
||||
"xlsx": "images/excel.png",
|
||||
"pdf": "images/pdf.png",
|
||||
"zip": "images/zip.png",
|
||||
"rar": "images/zip.png",
|
||||
"7z": "images/zip.png",
|
||||
"tar": "images/zip.png",
|
||||
"gz": "images/zip.png",
|
||||
"xz": "images/zip.png",
|
||||
"bz2": "images/zip.png",
|
||||
"txt": "images/txt.png",
|
||||
"jpg": "images/image_icon.png",
|
||||
"bmp": "images/image_icon.png",
|
||||
"gif": "images/image_icon.png",
|
||||
"png": "images/image_icon.png",
|
||||
"jpeg": "images/image_icon.png",
|
||||
"tif": "images/image_icon.png",
|
||||
"wmf": "images/image_icon.png",
|
||||
"dib": "images/image_icon.png",
|
||||
"mp4": "images/video_icon.png",
|
||||
"avi": "images/video_icon.png",
|
||||
"mov": "images/video_icon.png",
|
||||
"wmv": "images/video_icon.png",
|
||||
"flv": "images/video_icon.png",
|
||||
};
|
||||
|
||||
Widget _getFileIcon() {
|
||||
return Image.asset(
|
||||
fileMap[fileFormat?.toLowerCase()] ?? "images/unknown.png",
|
||||
package: 'tencent_cloud_chat_uikit',
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
return SizedBox(
|
||||
height: size ?? 50,
|
||||
width: size ?? 50,
|
||||
child: Container(padding: const EdgeInsets.all(4), child: _getFileIcon()),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_member_full_info.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_group_member_full_info.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_tips_elem.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_group_tips_elem.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/message.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/color.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/tui_theme.dart';
|
||||
|
||||
class TIMUIKitGroupTipsElem extends StatefulWidget {
|
||||
final V2TimGroupTipsElem groupTipsElem;
|
||||
final List<V2TimGroupMemberFullInfo?> groupMemberList;
|
||||
|
||||
const TIMUIKitGroupTipsElem({Key? key, required this.groupMemberList, required this.groupTipsElem}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<TIMUIKitGroupTipsElem> createState() => _TIMUIKitGroupTipsElemState();
|
||||
}
|
||||
|
||||
class _TIMUIKitGroupTipsElemState extends TIMUIKitState<TIMUIKitGroupTipsElem> {
|
||||
String groupTipsAbstractText = "";
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
getText();
|
||||
}
|
||||
|
||||
void getText() async {
|
||||
final newText = await MessageUtils.groupTipsMessageAbstract(widget.groupTipsElem, widget.groupMemberList);
|
||||
setState(() {
|
||||
groupTipsAbstractText = newText;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final TUITheme theme = value.theme;
|
||||
|
||||
return MessageUtils.wrapMessageTips(
|
||||
Text(
|
||||
groupTipsAbstractText,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 12, fontWeight: FontWeight.w400, color: hexToColor("888888")),
|
||||
),
|
||||
theme);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,638 @@
|
||||
// ignore_for_file: prefer_typing_uninitialized_variables, unused_import
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:image_gallery_saver_plus/image_gallery_saver_plus.dart';
|
||||
import 'package:loading_animation_widget/loading_animation_widget.dart';
|
||||
import 'package:open_file/open_file.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:tencent_chat_i18n_tool/tencent_chat_i18n_tool.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_image.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_image.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_message.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_callback.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_chat_separate_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_chat_global_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/message/message_services.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/services_locatar.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/tui_theme.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/constants/history_message_constant.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/logger.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/message.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/permission.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/platform.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitMessageItem/TIMUIKitMessageReaction/tim_uikit_message_reaction_wrapper.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/image_screen.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/wide_popup.dart';
|
||||
import 'package:transparent_image/transparent_image.dart';
|
||||
import 'package:universal_html/html.dart' as html;
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class TIMUIKitImageElem extends StatefulWidget {
|
||||
final V2TimMessage message;
|
||||
final bool isShowJump;
|
||||
final VoidCallback? clearJump;
|
||||
final String? isFrom;
|
||||
final bool? isShowMessageReaction;
|
||||
final TUIChatSeparateViewModel chatModel;
|
||||
|
||||
const TIMUIKitImageElem(
|
||||
{required this.message,
|
||||
this.isShowJump = false,
|
||||
required this.chatModel,
|
||||
this.clearJump,
|
||||
this.isFrom,
|
||||
Key? key,
|
||||
this.isShowMessageReaction})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _TIMUIKitImageElem();
|
||||
}
|
||||
|
||||
class _TIMUIKitImageElem extends TIMUIKitState<TIMUIKitImageElem> {
|
||||
final TUIChatGlobalModel globalModel = serviceLocator<TUIChatGlobalModel>();
|
||||
final TUIChatGlobalModel model = serviceLocator<TUIChatGlobalModel>();
|
||||
final MessageService _messageService = serviceLocator<MessageService>();
|
||||
Widget? imageItem;
|
||||
|
||||
@override
|
||||
didUpdateWidget(oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
}
|
||||
|
||||
String getOriginImgURL() {
|
||||
// 实际拿的是原图
|
||||
V2TimImage? img =
|
||||
MessageUtils.getImageFromImgList(widget.message.imageElem!.imageList, HistoryMessageDartConstant.oriImgPrior);
|
||||
return img == null ? widget.message.imageElem!.path! : img.url!;
|
||||
}
|
||||
|
||||
Widget errorDisplay(BuildContext context, TUITheme? theme) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(5)),
|
||||
border: Border.all(
|
||||
width: 2,
|
||||
color: theme?.weakDividerColor ?? Colors.grey,
|
||||
)),
|
||||
height: 170,
|
||||
width: 170,
|
||||
child: Center(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
LoadingAnimationWidget.staggeredDotsWave(
|
||||
color: theme?.weakTextColor ?? Colors.grey,
|
||||
size: 28,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget getImage(image, {imageElem}) {
|
||||
Widget res = ClipRRect(
|
||||
clipper: ImageClipper(),
|
||||
child: image,
|
||||
);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
//保存网络图片到本地
|
||||
Future<void> _saveImageToLocal(
|
||||
context,
|
||||
String imageUrl, {
|
||||
bool isLocalResource = true,
|
||||
TUITheme? theme,
|
||||
}) async {
|
||||
if (PlatformUtils().isWeb) {
|
||||
download(imageUrl) async {
|
||||
final http.Response r = await http.get(Uri.parse(imageUrl));
|
||||
final data = r.bodyBytes;
|
||||
final base64data = base64Encode(data);
|
||||
final a = html.AnchorElement(href: 'data:image/jpeg;base64,$base64data');
|
||||
a.download = md5.convert(utf8.encode(imageUrl)).toString();
|
||||
a.click();
|
||||
a.remove();
|
||||
}
|
||||
|
||||
download(imageUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
if (PlatformUtils().isIOS) {
|
||||
if (!await Permissions.checkPermission(context, Permission.photosAddOnly.value, theme!, false)) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
final DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
|
||||
AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo;
|
||||
if (PlatformUtils().isMobile) {
|
||||
if ((androidInfo.version.sdkInt) >= 33) {
|
||||
final photos = await Permissions.checkPermission(
|
||||
context,
|
||||
Permission.photos.value,
|
||||
theme,
|
||||
);
|
||||
if (!photos) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
final storage = await Permissions.checkPermission(
|
||||
context,
|
||||
Permission.storage.value,
|
||||
);
|
||||
if (!storage) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isLocalResource) {
|
||||
if (widget.message.msgID == null || widget.message.msgID!.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (model.getMessageProgress(widget.message.msgID) == 100) {
|
||||
String savePath;
|
||||
if (widget.message.imageElem!.path != null &&
|
||||
widget.message.imageElem!.path != '' &&
|
||||
File(widget.message.imageElem!.path!).existsSync()) {
|
||||
savePath = widget.message.imageElem!.path!;
|
||||
} else {
|
||||
savePath = model.getFileMessageLocation(widget.message.msgID);
|
||||
}
|
||||
File f = File(savePath);
|
||||
if (f.existsSync()) {
|
||||
var result = await ImageGallerySaverPlus.saveFile(savePath);
|
||||
|
||||
if (PlatformUtils().isIOS) {
|
||||
if (result['isSuccess']) {
|
||||
onTIMCallback(
|
||||
TIMCallback(type: TIMCallbackType.INFO, infoRecommendText: TIM_t("图片保存成功"), infoCode: 6660406));
|
||||
} else {
|
||||
onTIMCallback(
|
||||
TIMCallback(type: TIMCallbackType.INFO, infoRecommendText: TIM_t("图片保存失败"), infoCode: 6660407));
|
||||
}
|
||||
} else {
|
||||
if (result != null) {
|
||||
onTIMCallback(
|
||||
TIMCallback(type: TIMCallbackType.INFO, infoRecommendText: TIM_t("图片保存成功"), infoCode: 6660406));
|
||||
} else {
|
||||
onTIMCallback(
|
||||
TIMCallback(type: TIMCallbackType.INFO, infoRecommendText: TIM_t("图片保存失败"), infoCode: 6660407));
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
onTIMCallback(TIMCallback(
|
||||
type: TIMCallbackType.INFO, infoRecommendText: TIM_t("the message is downloading"), infoCode: -1));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var result = await ImageGallerySaverPlus.saveFile(imageUrl);
|
||||
|
||||
if (PlatformUtils().isIOS) {
|
||||
if (result['isSuccess']) {
|
||||
onTIMCallback(TIMCallback(type: TIMCallbackType.INFO, infoRecommendText: TIM_t("图片保存成功"), infoCode: 6660406));
|
||||
} else {
|
||||
onTIMCallback(TIMCallback(type: TIMCallbackType.INFO, infoRecommendText: TIM_t("图片保存失败"), infoCode: 6660407));
|
||||
}
|
||||
} else {
|
||||
if (result != null) {
|
||||
onTIMCallback(TIMCallback(type: TIMCallbackType.INFO, infoRecommendText: TIM_t("图片保存成功"), infoCode: 6660406));
|
||||
} else {
|
||||
onTIMCallback(TIMCallback(type: TIMCallbackType.INFO, infoRecommendText: TIM_t("图片保存失败"), infoCode: 6660407));
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
Future<void> _saveImg(TUITheme theme) async {
|
||||
try {
|
||||
String? imageUrl;
|
||||
bool isAssetBool = false;
|
||||
final imageElem = widget.message.imageElem;
|
||||
|
||||
if (imageElem != null) {
|
||||
final originUrl = getOriginImgURL();
|
||||
final localUrl = imageElem.imageList?.firstOrNull?.localUrl;
|
||||
final filePath = imageElem.path;
|
||||
final isWeb = PlatformUtils().isWeb;
|
||||
|
||||
if (!isWeb && filePath != null && File(filePath).existsSync()) {
|
||||
imageUrl = filePath;
|
||||
isAssetBool = true;
|
||||
} else if (localUrl != null && (!isWeb && File(localUrl).existsSync())) {
|
||||
imageUrl = localUrl;
|
||||
isAssetBool = true;
|
||||
} else {
|
||||
imageUrl = originUrl;
|
||||
isAssetBool = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (imageUrl != null) {
|
||||
return await _saveImageToLocal(
|
||||
context,
|
||||
imageUrl,
|
||||
isLocalResource: isAssetBool,
|
||||
theme: theme,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
onTIMCallback(TIMCallback(infoCode: 6660414, infoRecommendText: TIM_t("正在下载中"), type: TIMCallbackType.INFO));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
V2TimImage? getImageFromList(V2TimImageTypesEnum imgType) {
|
||||
V2TimImage? img = MessageUtils.getImageFromImgList(widget.message.imageElem!.imageList,
|
||||
HistoryMessageDartConstant.imgPriorMap[imgType] ?? HistoryMessageDartConstant.oriImgPrior);
|
||||
|
||||
return img;
|
||||
}
|
||||
|
||||
void launchDesktopFile(String path) {
|
||||
if (PlatformUtils().isWindows) {
|
||||
OpenFile.open(path);
|
||||
} else {
|
||||
launchUrl(Uri.file(path));
|
||||
}
|
||||
}
|
||||
|
||||
Widget errorPage(theme) => Container(
|
||||
height: MediaQuery.of(context).size.height,
|
||||
color: theme.black,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: errorDisplay(context, theme),
|
||||
));
|
||||
|
||||
bool checkIfDownloadSuccess() {
|
||||
final localUrl = TencentUtils.checkString(model.getFileMessageLocation(widget.message.msgID)) ??
|
||||
widget.message.imageElem!.imageList![0]!.localUrl;
|
||||
return TencentUtils.checkString(localUrl) != null && File(localUrl!).existsSync();
|
||||
}
|
||||
|
||||
_onClickOpenImageInNewWindow() {
|
||||
final localUrl = TencentUtils.checkString(model.getFileMessageLocation(widget.message.msgID)) ??
|
||||
widget.message.imageElem!.imageList![0]!.localUrl;
|
||||
Future.delayed(const Duration(milliseconds: 0), () async {
|
||||
final isDownloaded = checkIfDownloadSuccess();
|
||||
if (isDownloaded) {
|
||||
launchDesktopFile(localUrl ?? "");
|
||||
} else {
|
||||
onTIMCallback(
|
||||
TIMCallback(infoCode: 6660414, infoRecommendText: TIM_t("正在下载原始资源,请稍候..."), type: TIMCallbackType.INFO));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_handleOnTapPreviewImageOnDesktop({
|
||||
double? positionRadio,
|
||||
String? originImgUrl,
|
||||
}) {
|
||||
final localUrl = TencentUtils.checkString(model.getFileMessageLocation(widget.message.msgID)) ??
|
||||
widget.message.imageElem!.imageList![0]!.localUrl;
|
||||
if (checkIfDownloadSuccess()) {
|
||||
TUIKitWidePopup.showMedia(
|
||||
aspectRatio: positionRadio,
|
||||
context: context,
|
||||
mediaLocalPath: localUrl ?? "",
|
||||
onClickOrigin: () => _onClickOpenImageInNewWindow());
|
||||
} else {
|
||||
if (TencentUtils.checkString(originImgUrl) != null) {
|
||||
TUIKitWidePopup.showMedia(
|
||||
aspectRatio: positionRadio,
|
||||
context: context,
|
||||
mediaURL: originImgUrl,
|
||||
onClickOrigin: () => _onClickOpenImageInNewWindow());
|
||||
} else {
|
||||
onTIMCallback(TIMCallback(infoCode: 6660414, infoRecommendText: TIM_t("正在下载中"), type: TIMCallbackType.INFO));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<double> calculateAspectRatio(ImageProvider imageProvider) async {
|
||||
Completer<double> completer = Completer<double>();
|
||||
|
||||
final imageStream = imageProvider.resolve(const ImageConfiguration());
|
||||
imageStream.addListener(
|
||||
ImageStreamListener((imageInfo, synchronousCall) {
|
||||
if (imageInfo.image.width != 0 && imageInfo.image.height != 0) {
|
||||
double aspectRatio = imageInfo.image.width / imageInfo.image.height;
|
||||
completer.complete(aspectRatio);
|
||||
} else {
|
||||
// If unable to calculate aspect ratio, return default value of 0.5
|
||||
completer.complete(0.5);
|
||||
}
|
||||
}, onError: (Object exception, StackTrace? stackTrace) {
|
||||
// If there's an error, return default value of 0.5
|
||||
completer.complete(1);
|
||||
}),
|
||||
);
|
||||
|
||||
return await completer.future;
|
||||
}
|
||||
|
||||
void onClickImage({
|
||||
required bool isNetworkImage,
|
||||
dynamic heroTag,
|
||||
required TUITheme theme,
|
||||
String? imgUrl,
|
||||
String? imgPath,
|
||||
}) {
|
||||
if (isNetworkImage) {
|
||||
if (PlatformUtils().isWeb) {
|
||||
TUIKitWidePopup.showMedia(
|
||||
context: context,
|
||||
mediaURL: widget.message.imageElem?.path ?? "",
|
||||
onClickOrigin: () => launchUrl(
|
||||
Uri.parse(widget.message.imageElem?.path ?? ""),
|
||||
mode: LaunchMode.externalApplication,
|
||||
));
|
||||
return;
|
||||
}
|
||||
if (PlatformUtils().isDesktop) {
|
||||
_handleOnTapPreviewImageOnDesktop(
|
||||
originImgUrl: imgUrl,
|
||||
);
|
||||
} else {
|
||||
Navigator.of(context).push(
|
||||
PageRouteBuilder(
|
||||
opaque: false,
|
||||
pageBuilder: (_, __, ___) => ImageScreen(
|
||||
imageProvider: CachedNetworkImageProvider(
|
||||
imgUrl ?? "",
|
||||
cacheKey: widget.message.msgID,
|
||||
),
|
||||
heroTag: heroTag,
|
||||
messageID: widget.message.msgID,
|
||||
downloadFn: () async {
|
||||
return await _saveImg(theme);
|
||||
})),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (PlatformUtils().isDesktop) {
|
||||
TUIKitWidePopup.showMedia(
|
||||
mediaLocalPath: imgPath, context: context, onClickOrigin: () => launchDesktopFile(imgPath ?? ""));
|
||||
} else {
|
||||
Navigator.of(context).push(
|
||||
PageRouteBuilder(
|
||||
opaque: false, // set to false
|
||||
pageBuilder: (_, __, ___) => ImageScreen(
|
||||
imageProvider: FileImage(File(imgPath ?? "")),
|
||||
heroTag: heroTag,
|
||||
messageID: widget.message.msgID,
|
||||
downloadFn: () async {
|
||||
return await _saveImg(theme);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Widget _renderAllImage(
|
||||
{dynamic heroTag,
|
||||
double? positionRadio,
|
||||
required TUITheme theme,
|
||||
bool isNetworkImage = false,
|
||||
String? webPath,
|
||||
V2TimImage? originalImg,
|
||||
V2TimImage? smallImg,
|
||||
String? smallLocalPath,
|
||||
String? originLocalPath}) {
|
||||
Widget getImageWidget() {
|
||||
if (isNetworkImage) {
|
||||
return Hero(
|
||||
tag: heroTag,
|
||||
child: PlatformUtils().isWeb
|
||||
? Image.network(webPath ?? smallImg?.url ?? originalImg!.url!, fit: BoxFit.contain)
|
||||
: CachedNetworkImage(
|
||||
alignment: Alignment.topCenter,
|
||||
imageUrl: webPath ?? smallImg?.url ?? originalImg!.url!,
|
||||
errorWidget: (context, error, stackTrace) => errorPage(theme),
|
||||
fit: BoxFit.contain,
|
||||
cacheKey: smallImg?.uuid ?? originalImg!.uuid,
|
||||
placeholder: (context, url) => Image(image: MemoryImage(kTransparentImage)),
|
||||
fadeInDuration: const Duration(milliseconds: 0),
|
||||
));
|
||||
} else {
|
||||
final imgPath = (TencentUtils.checkString(smallLocalPath) != null ? smallLocalPath : originLocalPath)!;
|
||||
return Hero(tag: heroTag, child: Image.file(File(imgPath), fit: BoxFit.contain));
|
||||
}
|
||||
}
|
||||
|
||||
double? currentPositionRadio;
|
||||
// File imgF = File((TencentUtils.checkString(originLocalPath) != null
|
||||
// ? originLocalPath
|
||||
// : smallLocalPath) ??
|
||||
// "");
|
||||
// bool isExist = imgF.existsSync();
|
||||
//
|
||||
// if (!isExist) {
|
||||
// return errorDisplay(context, theme);
|
||||
// }
|
||||
// Image image = Image.file(imgF);
|
||||
//
|
||||
// image.image
|
||||
// .resolve(const ImageConfiguration())
|
||||
// .addListener(ImageStreamListener((image, synchronousCall) {
|
||||
// if (image.image.width != 0 && image.image.height != 0) {
|
||||
// currentPositionRadio = image.image.width / image.image.height;
|
||||
// }
|
||||
// }));
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () => onClickImage(
|
||||
theme: theme,
|
||||
heroTag: heroTag,
|
||||
isNetworkImage: isNetworkImage,
|
||||
imgUrl: webPath ?? smallImg?.url ?? originalImg?.url ?? "",
|
||||
imgPath: (TencentUtils.checkString(originLocalPath) != null ? originLocalPath : smallLocalPath) ?? ""),
|
||||
child: Stack(
|
||||
children: [
|
||||
if (positionRadio != null)
|
||||
AspectRatio(
|
||||
aspectRatio: (currentPositionRadio ?? positionRadio)!,
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(color: Colors.transparent),
|
||||
),
|
||||
),
|
||||
getImageWidget(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void initImages() async {
|
||||
final zeroImageLocal = TencentUtils.checkString(
|
||||
widget.message.imageElem?.imageList?.firstWhereOrNull((element) => element?.type == 0)?.localUrl);
|
||||
final oneImageLocal = TencentUtils.checkString(
|
||||
widget.message.imageElem?.imageList?.firstWhereOrNull((element) => element?.type == 1)?.localUrl);
|
||||
final twoImageLocal = TencentUtils.checkString(
|
||||
widget.message.imageElem?.imageList?.firstWhereOrNull((element) => element?.type == 2)?.localUrl);
|
||||
|
||||
if (!PlatformUtils().isWeb && TencentUtils.checkString(widget.message.msgID) != null) {
|
||||
if ((widget.message.imageElem?.imageList) == null || widget.message.imageElem!.imageList!.isEmpty) {
|
||||
final response = await _messageService.getMessageOnlineUrl(msgID: widget.message.msgID!);
|
||||
final elem = response.data;
|
||||
if (elem != null && elem.imageElem != null) {
|
||||
widget.message.imageElem = elem.imageElem;
|
||||
}
|
||||
}
|
||||
if (oneImageLocal == null || !File(oneImageLocal).existsSync()) {
|
||||
_messageService.downloadMessage(msgID: widget.message.msgID!, messageType: 3, imageType: 1, isSnapshot: false);
|
||||
}
|
||||
if (twoImageLocal == null || !File(twoImageLocal).existsSync()) {
|
||||
_messageService.downloadMessage(msgID: widget.message.msgID!, messageType: 3, imageType: 2, isSnapshot: false);
|
||||
}
|
||||
if (zeroImageLocal == null || !File(zeroImageLocal).existsSync()) {
|
||||
_messageService.downloadMessage(msgID: widget.message.msgID!, messageType: 3, imageType: 0, isSnapshot: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
initImages();
|
||||
}
|
||||
|
||||
Widget? _renderImage(dynamic heroTag, TUITheme theme, {V2TimImage? originalImg, V2TimImage? smallImg}) {
|
||||
double positionRadio = 1.0;
|
||||
if (smallImg?.width != null && smallImg?.height != null && smallImg?.width != 0 && smallImg?.height != 0) {
|
||||
positionRadio = (smallImg!.width! / smallImg.height!);
|
||||
}
|
||||
|
||||
if (PlatformUtils().isWeb && widget.message.imageElem!.path != null) {
|
||||
// Displaying on Web only
|
||||
return _renderAllImage(
|
||||
heroTag: heroTag,
|
||||
theme: theme,
|
||||
isNetworkImage: true,
|
||||
smallImg: smallImg,
|
||||
originalImg: originalImg,
|
||||
positionRadio: positionRadio,
|
||||
webPath: widget.message.imageElem!.path);
|
||||
}
|
||||
|
||||
try {
|
||||
if ((widget.message.isSelf! &&
|
||||
widget.message.imageElem!.path != null &&
|
||||
widget.message.imageElem!.path!.isNotEmpty &&
|
||||
File(widget.message.imageElem!.path!).existsSync())) {
|
||||
return _renderAllImage(
|
||||
smallLocalPath: widget.message.imageElem!.path!,
|
||||
heroTag: heroTag,
|
||||
theme: theme,
|
||||
positionRadio: positionRadio,
|
||||
originLocalPath: widget.message.imageElem!.path!);
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore: avoid_print
|
||||
outputLogger.i(e.toString());
|
||||
}
|
||||
|
||||
try {
|
||||
if ((TencentUtils.checkString(smallImg?.localUrl) != null && File((smallImg?.localUrl!)!).existsSync()) ||
|
||||
(TencentUtils.checkString(originalImg?.localUrl) != null && File((originalImg?.localUrl!)!).existsSync())) {
|
||||
return _renderAllImage(
|
||||
smallLocalPath: smallImg?.localUrl ?? "",
|
||||
heroTag: heroTag,
|
||||
theme: theme,
|
||||
positionRadio: positionRadio,
|
||||
originLocalPath: originalImg?.localUrl);
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore: avoid_print
|
||||
outputLogger.i(e.toString());
|
||||
return _renderAllImage(
|
||||
heroTag: heroTag,
|
||||
theme: theme,
|
||||
isNetworkImage: true,
|
||||
smallImg: smallImg,
|
||||
positionRadio: positionRadio,
|
||||
originalImg: originalImg);
|
||||
}
|
||||
|
||||
if ((smallImg?.url ?? originalImg?.url) != null && (smallImg?.url ?? originalImg?.url)!.isNotEmpty) {
|
||||
return _renderAllImage(
|
||||
heroTag: heroTag,
|
||||
theme: theme,
|
||||
isNetworkImage: true,
|
||||
positionRadio: positionRadio,
|
||||
smallImg: smallImg,
|
||||
originalImg: originalImg);
|
||||
}
|
||||
|
||||
return errorDisplay(context, theme);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final theme = value.theme;
|
||||
final isDesktopScreen = TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop;
|
||||
final heroTag =
|
||||
"${widget.message.msgID ?? widget.message.id ?? widget.message.timestamp ?? DateTime.now().millisecondsSinceEpoch}${widget.isFrom}";
|
||||
|
||||
V2TimImage? originalImg = getImageFromList(V2TimImageTypesEnum.original);
|
||||
V2TimImage? smallImg = getImageFromList(V2TimImageTypesEnum.small);
|
||||
return TIMUIKitMessageReactionWrapper(
|
||||
chatModel: widget.chatModel,
|
||||
isShowJump: widget.isShowJump,
|
||||
clearJump: widget.clearJump,
|
||||
isFromSelf: widget.message.isSelf ?? true,
|
||||
isShowMessageReaction: widget.isShowMessageReaction ?? true,
|
||||
message: widget.message,
|
||||
child: LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) {
|
||||
return ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: constraints.maxWidth * (isDesktopScreen ? 0.4 : 0.5),
|
||||
minWidth: 64,
|
||||
maxHeight: 256,
|
||||
),
|
||||
child: _renderImage(heroTag, theme, originalImg: originalImg, smallImg: smallImg),
|
||||
);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
class ImageClipper extends CustomClipper<RRect> {
|
||||
@override
|
||||
RRect getClip(Size size) {
|
||||
return RRect.fromRectAndRadius(Rect.fromLTWH(0, 0, size.width, min(size.height, 256)), const Radius.circular(5));
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldReclip(CustomClipper<RRect> oldClipper) {
|
||||
return oldClipper != this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,422 @@
|
||||
// ignore_for_file: unused_import
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:tencent_chat_i18n_tool/tencent_chat_i18n_tool.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/enum/message_elem_type.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/enum/message_status.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_message.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_chat_model_tools.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/common_utils.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/platform.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitTextField/special_text/DefaultSpecialTextSpanBuilder.dart';
|
||||
import 'package:extended_text/extended_text.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_callback.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_chat_separate_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_chat_global_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/services_locatar.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/tui_theme.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitMessageItem/TIMUIKitMessageReaction/tim_uikit_message_reaction_show_panel.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitMessageItem/main.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitMessageItem/tim_uikit_chat_face_elem.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/tim_uikit_chat_config.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/tim_uikit_cloud_custom_data.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/link_preview/link_preview_entry.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/link_preview/models/link_preview_content.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/link_preview/widgets/link_preview.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/logger.dart';
|
||||
import 'package:tim_ui_kit_sticker_plugin/utils/tim_custom_face_data.dart';
|
||||
|
||||
class TIMUIKitReplyElem extends StatefulWidget {
|
||||
final V2TimMessage message;
|
||||
final Function scrollToIndex;
|
||||
final bool isShowJump;
|
||||
final VoidCallback clearJump;
|
||||
final TextStyle? fontStyle;
|
||||
final BorderRadius? borderRadius;
|
||||
final Color? backgroundColor;
|
||||
final EdgeInsetsGeometry? textPadding;
|
||||
final TUIChatSeparateViewModel chatModel;
|
||||
final bool? isShowMessageReaction;
|
||||
final List<CustomEmojiFaceData> customEmojiStickerList;
|
||||
|
||||
const TIMUIKitReplyElem({
|
||||
Key? key,
|
||||
required this.message,
|
||||
required this.scrollToIndex,
|
||||
this.isShowJump = false,
|
||||
required this.clearJump,
|
||||
this.fontStyle,
|
||||
this.borderRadius,
|
||||
this.isShowMessageReaction,
|
||||
this.backgroundColor,
|
||||
this.textPadding,
|
||||
this.customEmojiStickerList = const [],
|
||||
required this.chatModel,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _TIMUIKitReplyElemState();
|
||||
}
|
||||
|
||||
class _TIMUIKitReplyElemState extends TIMUIKitState<TIMUIKitReplyElem> {
|
||||
MessageRepliedData? repliedMessage;
|
||||
V2TimMessage? rawMessage;
|
||||
bool isShowJumpState = false;
|
||||
bool isShining = false;
|
||||
|
||||
MessageRepliedData? _getRepliedMessage() {
|
||||
try {
|
||||
final CloudCustomData messageCloudCustomData = CloudCustomData.fromJson(json.decode(
|
||||
TencentUtils.checkString(widget.message.cloudCustomData) != null ? widget.message.cloudCustomData! : "{}"));
|
||||
if (messageCloudCustomData.messageReply != null) {
|
||||
final MessageRepliedData repliedMessage = MessageRepliedData.fromJson(messageCloudCustomData.messageReply!);
|
||||
return repliedMessage;
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
_getMessageByMessageID() async {
|
||||
final MessageRepliedData? cloudCustomData = _getRepliedMessage();
|
||||
if (cloudCustomData != null) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
repliedMessage = cloudCustomData;
|
||||
});
|
||||
}
|
||||
|
||||
final messageID = cloudCustomData.messageID;
|
||||
if (PlatformUtils().isWeb) {
|
||||
return;
|
||||
}
|
||||
V2TimMessage? message = await widget.chatModel.findMessage(messageID);
|
||||
if (message == null) {
|
||||
try {
|
||||
final RepliedMessageAbstract repliedMessageAbstract =
|
||||
RepliedMessageAbstract.fromJson(jsonDecode(cloudCustomData.messageAbstract));
|
||||
if (repliedMessageAbstract.isNotEmpty) {
|
||||
message = V2TimMessage(
|
||||
elemType: 0,
|
||||
seq: repliedMessageAbstract.seq,
|
||||
timestamp: repliedMessageAbstract.timestamp,
|
||||
msgID: repliedMessageAbstract.msgID);
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore: avoid_print
|
||||
outputLogger.i(e.toString());
|
||||
}
|
||||
}
|
||||
if (message != null) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
rawMessage = message;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Widget _defaultRawMessageText(String text, TUITheme? theme) {
|
||||
return Text(text,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(fontSize: 12, color: theme?.weakTextColor, fontWeight: FontWeight.w400));
|
||||
}
|
||||
|
||||
_renderMessageSummary(TUITheme? theme) {
|
||||
try {
|
||||
final RepliedMessageAbstract repliedMessageAbstract =
|
||||
RepliedMessageAbstract.fromJson(jsonDecode(repliedMessage?.messageAbstract ?? ""));
|
||||
if (TencentUtils.checkString(repliedMessageAbstract.summary) != null) {
|
||||
return _defaultRawMessageText(repliedMessageAbstract.summary!, theme);
|
||||
}
|
||||
return _defaultRawMessageText(repliedMessage?.messageAbstract ?? TIM_t("[未知消息]"), theme);
|
||||
} catch (e) {
|
||||
return _defaultRawMessageText(repliedMessage?.messageAbstract ?? TIM_t("[未知消息]"), theme);
|
||||
}
|
||||
}
|
||||
|
||||
(bool isRevoke, bool isRevokeByAdmin) isRevokeMessage(V2TimMessage message) {
|
||||
if (message.status == 6) {
|
||||
return (true, false);
|
||||
} else {
|
||||
try {
|
||||
final customData = jsonDecode(message.cloudCustomData ?? "{}");
|
||||
final isRevoke = customData["isRevoke"] ?? false;
|
||||
final revokeByAdmin = customData["revokeByAdmin"] ?? false;
|
||||
return (isRevoke, revokeByAdmin);
|
||||
} catch (e) {
|
||||
return (false, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_rawMessageBuilder(V2TimMessage? message, TUITheme? theme) {
|
||||
if (repliedMessage == null) {
|
||||
return const SizedBox(width: 0, height: 12);
|
||||
}
|
||||
if (message == null) {
|
||||
if (repliedMessage?.messageAbstract != null) {
|
||||
return _renderMessageSummary(theme);
|
||||
}
|
||||
return const SizedBox(width: 0, height: 12);
|
||||
}
|
||||
|
||||
final revokeStatus = isRevokeMessage(message);
|
||||
final isRevokedMsg = revokeStatus.$1;
|
||||
final isAdminRevoke = revokeStatus.$2;
|
||||
|
||||
if (isRevokedMsg) {
|
||||
return _defaultRawMessageText(isAdminRevoke ? TIM_t("[消息被管理员撤回]") : TIM_t("[消息被撤回]"), theme);
|
||||
}
|
||||
|
||||
final messageType = message.elemType;
|
||||
final isSelf = message.isSelf ?? true;
|
||||
final customAbstractMessage =
|
||||
widget.chatModel.abstractMessageBuilder != null ? widget.chatModel.abstractMessageBuilder!(message) : null;
|
||||
if (customAbstractMessage != null) {
|
||||
return _defaultRawMessageText(
|
||||
customAbstractMessage,
|
||||
theme,
|
||||
);
|
||||
}
|
||||
switch (messageType) {
|
||||
case MessageElemType.V2TIM_ELEM_TYPE_CUSTOM:
|
||||
return _defaultRawMessageText(TIM_t("[自定义]"), theme);
|
||||
case MessageElemType.V2TIM_ELEM_TYPE_SOUND:
|
||||
return _defaultRawMessageText(TIM_t("[语音消息]"), theme);
|
||||
case MessageElemType.V2TIM_ELEM_TYPE_TEXT:
|
||||
return _defaultRawMessageText(message.textElem?.text ?? "", theme);
|
||||
case MessageElemType.V2TIM_ELEM_TYPE_FACE:
|
||||
return TIMUIKitFaceElem(
|
||||
model: widget.chatModel,
|
||||
isShowJump: false,
|
||||
isShowMessageReaction: false,
|
||||
path: message.faceElem!.data ?? "",
|
||||
message: message,
|
||||
);
|
||||
case MessageElemType.V2TIM_ELEM_TYPE_FILE:
|
||||
return TIMUIKitFileElem(
|
||||
chatModel: widget.chatModel,
|
||||
isShowMessageReaction: false,
|
||||
message: message,
|
||||
messageID: message.msgID,
|
||||
fileElem: message.fileElem,
|
||||
isSelf: isSelf,
|
||||
isShowJump: false);
|
||||
case MessageElemType.V2TIM_ELEM_TYPE_IMAGE:
|
||||
return TIMUIKitImageElem(
|
||||
chatModel: widget.chatModel, message: message, isFrom: "reply", isShowMessageReaction: false);
|
||||
case MessageElemType.V2TIM_ELEM_TYPE_VIDEO:
|
||||
return TIMUIKitVideoElem(message, chatModel: widget.chatModel, isFrom: "reply", isShowMessageReaction: false);
|
||||
case MessageElemType.V2TIM_ELEM_TYPE_LOCATION:
|
||||
return _defaultRawMessageText(TIM_t("[位置]"), theme);
|
||||
case MessageElemType.V2TIM_ELEM_TYPE_MERGER:
|
||||
return TIMUIKitMergerElem(
|
||||
model: widget.chatModel,
|
||||
isShowJump: false,
|
||||
isShowMessageReaction: false,
|
||||
message: message,
|
||||
mergerElem: message.mergerElem!,
|
||||
messageID: message.msgID ?? "",
|
||||
isSelf: isSelf);
|
||||
default:
|
||||
return _renderMessageSummary(theme);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_getMessageByMessageID();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant TIMUIKitReplyElem oldWidget) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((mag) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
_getMessageByMessageID();
|
||||
});
|
||||
}
|
||||
|
||||
_showJumpColor() {
|
||||
if ((widget.chatModel.jumpMsgID != widget.message.msgID) && (widget.message.msgID?.isNotEmpty ?? true)) {
|
||||
return;
|
||||
}
|
||||
isShining = true;
|
||||
int shineAmount = 6;
|
||||
setState(() {
|
||||
isShowJumpState = true;
|
||||
});
|
||||
Timer.periodic(const Duration(milliseconds: 300), (timer) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
isShowJumpState = shineAmount.isOdd ? true : false;
|
||||
});
|
||||
}
|
||||
if (shineAmount == 0 || !mounted) {
|
||||
isShining = false;
|
||||
timer.cancel();
|
||||
}
|
||||
shineAmount--;
|
||||
});
|
||||
widget.clearJump();
|
||||
}
|
||||
|
||||
void _jumpToRawMsg() {
|
||||
if (rawMessage?.status != MessageStatus.V2TIM_MSG_STATUS_LOCAL_REVOKED && rawMessage?.timestamp != null) {
|
||||
widget.scrollToIndex(rawMessage);
|
||||
} else {
|
||||
onTIMCallback(TIMCallback(type: TIMCallbackType.INFO, infoRecommendText: TIM_t("无法定位到原消息")));
|
||||
}
|
||||
}
|
||||
|
||||
Widget? _renderPreviewWidget() {
|
||||
// If the link preview info from [localCustomData] is available, use it to render the preview card.
|
||||
// Otherwise, it will returns null.
|
||||
if (widget.message.localCustomData != null && widget.message.localCustomData!.isNotEmpty) {
|
||||
try {
|
||||
final String localJSON = widget.message.localCustomData!;
|
||||
final LocalCustomDataModel? localPreviewInfo = LocalCustomDataModel.fromMap(json.decode(localJSON));
|
||||
if (localPreviewInfo != null && !localPreviewInfo.isLinkPreviewEmpty()) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(top: 8),
|
||||
child:
|
||||
// You can use this default widget [LinkPreviewWidget] to render preview card, or you can use custom widget.
|
||||
LinkPreviewWidget(linkPreview: localPreviewInfo),
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final theme = value.theme;
|
||||
final isDesktopScreen = TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop;
|
||||
if (widget.isShowJump) {
|
||||
if (!isShining) {
|
||||
Future.delayed(Duration.zero, () {
|
||||
_showJumpColor();
|
||||
});
|
||||
} else {
|
||||
if ((widget.chatModel.jumpMsgID == widget.message.msgID) && (widget.message.msgID?.isNotEmpty ?? false)) {
|
||||
widget.clearJump();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final isFromSelf = widget.message.isSelf ?? true;
|
||||
|
||||
final defaultStyle = isFromSelf
|
||||
? (theme.chatMessageItemFromSelfBgColor ?? theme.lightPrimaryMaterialColor.shade50)
|
||||
: (theme.chatMessageItemFromOthersBgColor);
|
||||
|
||||
final backgroundColor =
|
||||
isShowJumpState ? const Color.fromRGBO(245, 166, 35, 1) : (defaultStyle ?? widget.backgroundColor);
|
||||
|
||||
final borderRadius = isFromSelf
|
||||
? const BorderRadius.only(
|
||||
topLeft: Radius.circular(10),
|
||||
topRight: Radius.circular(2),
|
||||
bottomLeft: Radius.circular(10),
|
||||
bottomRight: Radius.circular(10))
|
||||
: const BorderRadius.only(
|
||||
topLeft: Radius.circular(2),
|
||||
topRight: Radius.circular(10),
|
||||
bottomLeft: Radius.circular(10),
|
||||
bottomRight: Radius.circular(10));
|
||||
final textWithLink = LinkPreviewEntry.getHyperlinksText(
|
||||
widget.message.textElem?.text ?? "", widget.chatModel.chatConfig.isSupportMarkdownForTextMessage,
|
||||
onLinkTap: widget.chatModel.chatConfig.onTapLink,
|
||||
isUseQQPackage: widget.chatModel.chatConfig.stickerPanelConfig?.useQQStickerPackage ?? true,
|
||||
isUseTencentCloudChatPackage:
|
||||
widget.chatModel.chatConfig.stickerPanelConfig?.useTencentCloudChatStickerPackage ?? true,
|
||||
isUseTencentCloudChatPackageOldKeys:
|
||||
widget.chatModel.chatConfig.stickerPanelConfig?.useTencentCloudChatStickerPackageOldKeys ?? false,
|
||||
customEmojiStickerList: widget.customEmojiStickerList,
|
||||
isEnableTextSelection: widget.chatModel.chatConfig.isEnableTextSelection ?? false);
|
||||
return Container(
|
||||
padding: widget.textPadding ?? EdgeInsets.all(isDesktopScreen ? 12 : 10),
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
borderRadius: widget.borderRadius ?? borderRadius,
|
||||
),
|
||||
constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.6),
|
||||
child: GestureDetector(
|
||||
onTap: _jumpToRawMsg,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
// 这里是引用的部分
|
||||
padding: const EdgeInsets.symmetric(vertical: 3, horizontal: 6),
|
||||
constraints: const BoxConstraints(minWidth: 120),
|
||||
decoration: const BoxDecoration(
|
||||
color: Color.fromRGBO(68, 68, 68, 0.05),
|
||||
border: Border(left: BorderSide(color: Color.fromRGBO(68, 68, 68, 0.1), width: 2))),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
repliedMessage != null ? "${repliedMessage!.messageSender}:" : "",
|
||||
style: TextStyle(fontSize: 12, color: theme.weakTextColor, fontWeight: FontWeight.w500),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 4,
|
||||
),
|
||||
_rawMessageBuilder(rawMessage, theme)
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
// If the [elemType] is text message, it will not be null here.
|
||||
// You can render the widget from extension directly, with a [TextStyle] optionally.
|
||||
widget.chatModel.chatConfig.urlPreviewType != UrlPreviewType.none
|
||||
? textWithLink!(
|
||||
style: widget.fontStyle ??
|
||||
TextStyle(
|
||||
fontSize: isDesktopScreen ? 14 : 16,
|
||||
textBaseline: TextBaseline.ideographic,
|
||||
height: widget.chatModel.chatConfig.textHeight))
|
||||
: ExtendedText(widget.message.textElem?.text ?? "",
|
||||
softWrap: true,
|
||||
style: widget.fontStyle ??
|
||||
TextStyle(fontSize: isDesktopScreen ? 14 : 16, height: widget.chatModel.chatConfig.textHeight),
|
||||
specialTextSpanBuilder: DefaultSpecialTextSpanBuilder(
|
||||
isUseQQPackage: widget.chatModel.chatConfig.stickerPanelConfig?.useQQStickerPackage ?? true,
|
||||
isUseTencentCloudChatPackage:
|
||||
widget.chatModel.chatConfig.stickerPanelConfig?.useTencentCloudChatStickerPackage ?? true,
|
||||
isUseTencentCloudChatPackageOldKeys:
|
||||
widget.chatModel.chatConfig.stickerPanelConfig?.useTencentCloudChatStickerPackageOldKeys ??
|
||||
false,
|
||||
customEmojiStickerList: widget.customEmojiStickerList,
|
||||
showAtBackground: true,
|
||||
)),
|
||||
// If the link preview info is available, render the preview card.
|
||||
if (_renderPreviewWidget() != null &&
|
||||
widget.chatModel.chatConfig.urlPreviewType == UrlPreviewType.previewCardAndHyperlink)
|
||||
_renderPreviewWidget()!,
|
||||
if (widget.isShowMessageReaction ?? true) TIMUIKitMessageReactionShowPanel(message: widget.message)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:just_audio/just_audio.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_message.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_sound_elem.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_sound_elem.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_chat_separate_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_chat_global_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/message/message_services.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/services_locatar.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/constants/history_message_constant.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/platform.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/sound_record.dart';
|
||||
import 'TIMUIKitMessageReaction/tim_uikit_message_reaction_show_panel.dart';
|
||||
|
||||
class TIMUIKitSoundElem extends StatefulWidget {
|
||||
final V2TimMessage message;
|
||||
final V2TimSoundElem soundElem;
|
||||
final String msgID;
|
||||
final bool isFromSelf;
|
||||
final int? localCustomInt;
|
||||
final bool isShowJump;
|
||||
final VoidCallback? clearJump;
|
||||
final TextStyle? fontStyle;
|
||||
final BorderRadius? borderRadius;
|
||||
final Color? backgroundColor;
|
||||
final EdgeInsetsGeometry? textPadding;
|
||||
final bool? isShowMessageReaction;
|
||||
final TUIChatSeparateViewModel chatModel;
|
||||
|
||||
const TIMUIKitSoundElem(
|
||||
{Key? key,
|
||||
required this.soundElem,
|
||||
required this.msgID,
|
||||
required this.isFromSelf,
|
||||
this.isShowJump = false,
|
||||
this.clearJump,
|
||||
this.localCustomInt,
|
||||
this.fontStyle,
|
||||
this.borderRadius,
|
||||
this.backgroundColor,
|
||||
this.textPadding,
|
||||
required this.message,
|
||||
this.isShowMessageReaction,
|
||||
required this.chatModel})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _TIMUIKitSoundElemState();
|
||||
}
|
||||
|
||||
class _TIMUIKitSoundElemState extends TIMUIKitState<TIMUIKitSoundElem> {
|
||||
final int charLen = 8;
|
||||
bool isPlaying = false;
|
||||
StreamSubscription<Object>? subscription;
|
||||
bool isShowJumpState = false;
|
||||
bool isShining = false;
|
||||
final TUIChatGlobalModel globalModel = serviceLocator<TUIChatGlobalModel>();
|
||||
final MessageService _messageService = serviceLocator<MessageService>();
|
||||
late V2TimSoundElem stateElement = widget.message.soundElem!;
|
||||
|
||||
_playSound() async {
|
||||
if (!SoundPlayer.isInit) {
|
||||
SoundPlayer.initSoundPlayer();
|
||||
}
|
||||
if (widget.localCustomInt == null || widget.localCustomInt != HistoryMessageDartConstant.read) {
|
||||
globalModel.setLocalCustomInt(widget.msgID, HistoryMessageDartConstant.read, widget.chatModel.conversationID);
|
||||
}
|
||||
if (isPlaying) {
|
||||
SoundPlayer.stop();
|
||||
widget.chatModel.currentPlayedMsgId = "";
|
||||
} else {
|
||||
SoundPlayer.play(url: stateElement.url!);
|
||||
widget.chatModel.currentPlayedMsgId = widget.msgID;
|
||||
|
||||
setState(() {
|
||||
isPlaying = widget.chatModel.currentPlayedMsgId != '' && widget.chatModel.currentPlayedMsgId == widget.msgID;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
downloadMessageDetailAndSave() async {
|
||||
if (widget.message.msgID != null && widget.message.msgID != '') {
|
||||
if (widget.message.soundElem!.url == null || widget.message.soundElem!.url == '') {
|
||||
final response = await _messageService.getMessageOnlineUrl(msgID: widget.message.msgID!);
|
||||
if (response.data != null) {
|
||||
widget.message.soundElem = response.data!.soundElem;
|
||||
Future.delayed(const Duration(microseconds: 10), () {
|
||||
setState(() => stateElement = response.data!.soundElem!);
|
||||
});
|
||||
}
|
||||
}
|
||||
if (!PlatformUtils().isWeb) {
|
||||
if (widget.message.soundElem!.localUrl == null || widget.message.soundElem!.localUrl == '') {
|
||||
_messageService.downloadMessage(
|
||||
msgID: widget.message.msgID!, messageType: 4, imageType: 0, isSnapshot: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
subscription = SoundPlayer.playStateListener(listener: (PlayerState state) {
|
||||
if (state.processingState == ProcessingState.completed) {
|
||||
widget.chatModel.currentPlayedMsgId = "";
|
||||
setState(() {
|
||||
isPlaying = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
downloadMessageDetailAndSave();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (isPlaying) {
|
||||
SoundPlayer.stop();
|
||||
widget.chatModel.currentPlayedMsgId = "";
|
||||
}
|
||||
subscription?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
double _getSoundLen() {
|
||||
double soundLen = 32;
|
||||
if (stateElement.duration != null) {
|
||||
final realSoundLen = stateElement.duration!;
|
||||
int sdLen = 32;
|
||||
if (realSoundLen > 10) {
|
||||
sdLen = 12 * charLen + ((realSoundLen - 10) * charLen / 0.5).floor();
|
||||
} else if (realSoundLen > 2) {
|
||||
sdLen = 2 * charLen + realSoundLen * charLen;
|
||||
}
|
||||
sdLen = min(sdLen, 20 * charLen);
|
||||
soundLen = sdLen.toDouble();
|
||||
}
|
||||
|
||||
return soundLen;
|
||||
}
|
||||
|
||||
_showJumpColor() {
|
||||
if ((widget.chatModel.jumpMsgID != widget.message.msgID) && (widget.message.msgID?.isNotEmpty ?? true)) {
|
||||
return;
|
||||
}
|
||||
isShining = true;
|
||||
int shineAmount = 6;
|
||||
setState(() {
|
||||
isShowJumpState = true;
|
||||
});
|
||||
Timer.periodic(const Duration(milliseconds: 300), (timer) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
isShowJumpState = shineAmount.isOdd ? true : false;
|
||||
});
|
||||
}
|
||||
if (shineAmount == 0 || !mounted) {
|
||||
isShining = false;
|
||||
timer.cancel();
|
||||
}
|
||||
shineAmount--;
|
||||
});
|
||||
widget.clearJump!();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final theme = value.theme;
|
||||
|
||||
final backgroundColor = widget.isFromSelf
|
||||
? (theme.chatMessageItemFromSelfBgColor ?? theme.lightPrimaryMaterialColor.shade50)
|
||||
: (theme.chatMessageItemFromOthersBgColor);
|
||||
|
||||
final borderRadius = widget.isFromSelf
|
||||
? const BorderRadius.only(
|
||||
topLeft: Radius.circular(10),
|
||||
topRight: Radius.circular(2),
|
||||
bottomLeft: Radius.circular(10),
|
||||
bottomRight: Radius.circular(10))
|
||||
: const BorderRadius.only(
|
||||
topLeft: Radius.circular(2),
|
||||
topRight: Radius.circular(10),
|
||||
bottomLeft: Radius.circular(10),
|
||||
bottomRight: Radius.circular(10));
|
||||
if (widget.isShowJump) {
|
||||
if (!isShining) {
|
||||
Future.delayed(Duration.zero, () {
|
||||
_showJumpColor();
|
||||
});
|
||||
} else {
|
||||
if ((widget.chatModel.jumpMsgID == widget.message.msgID) && (widget.message.msgID?.isNotEmpty ?? false)) {
|
||||
widget.clearJump!();
|
||||
}
|
||||
}
|
||||
}
|
||||
return GestureDetector(
|
||||
onTap: () => _playSound(),
|
||||
child: Container(
|
||||
padding: widget.textPadding ?? const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: isShowJumpState ? const Color.fromRGBO(245, 166, 35, 1) : (widget.backgroundColor ?? backgroundColor),
|
||||
borderRadius: widget.borderRadius ?? borderRadius,
|
||||
),
|
||||
constraints: const BoxConstraints(maxWidth: 240),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: widget.isFromSelf
|
||||
? [
|
||||
Container(width: _getSoundLen()),
|
||||
Text(
|
||||
"''${stateElement.duration} ",
|
||||
style: widget.fontStyle,
|
||||
),
|
||||
isPlaying
|
||||
? Image.asset(
|
||||
'images/play_voice_send.gif',
|
||||
package: 'tencent_cloud_chat_uikit',
|
||||
width: 16,
|
||||
height: 16,
|
||||
)
|
||||
: Image.asset(
|
||||
'images/voice_send.png',
|
||||
package: 'tencent_cloud_chat_uikit',
|
||||
width: 16,
|
||||
height: 16,
|
||||
),
|
||||
]
|
||||
: [
|
||||
isPlaying
|
||||
? Image.asset(
|
||||
'images/play_voice_receive.gif',
|
||||
package: 'tencent_cloud_chat_uikit',
|
||||
width: 16,
|
||||
height: 16,
|
||||
)
|
||||
: Image.asset(
|
||||
'images/voice_receive.png',
|
||||
width: 16,
|
||||
height: 16,
|
||||
package: 'tencent_cloud_chat_uikit',
|
||||
),
|
||||
Text(
|
||||
" ${stateElement.duration}''",
|
||||
style: widget.fontStyle,
|
||||
),
|
||||
Container(width: _getSoundLen()),
|
||||
],
|
||||
),
|
||||
if (widget.isShowMessageReaction ?? true)
|
||||
TIMUIKitMessageReactionShowPanel(
|
||||
message: widget.message,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_message.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart';
|
||||
import 'package:extended_text/extended_text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_chat_separate_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitTextField/special_text/DefaultSpecialTextSpanBuilder.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/link_preview/link_preview_entry.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/link_preview/widgets/link_preview.dart';
|
||||
import 'TIMUIKitMessageReaction/tim_uikit_message_reaction_show_panel.dart';
|
||||
|
||||
class TIMUIKitTextElem extends StatefulWidget {
|
||||
final V2TimMessage message;
|
||||
final bool isFromSelf;
|
||||
final bool isShowJump;
|
||||
final VoidCallback clearJump;
|
||||
final TextStyle? fontStyle;
|
||||
final BorderRadius? borderRadius;
|
||||
final Color? backgroundColor;
|
||||
final EdgeInsetsGeometry? textPadding;
|
||||
final TUIChatSeparateViewModel chatModel;
|
||||
final bool? isShowMessageReaction;
|
||||
final List<CustomEmojiFaceData> customEmojiStickerList;
|
||||
|
||||
const TIMUIKitTextElem(
|
||||
{Key? key,
|
||||
required this.message,
|
||||
required this.isFromSelf,
|
||||
required this.isShowJump,
|
||||
required this.clearJump,
|
||||
this.fontStyle,
|
||||
this.borderRadius,
|
||||
this.isShowMessageReaction,
|
||||
this.backgroundColor,
|
||||
this.textPadding,
|
||||
required this.chatModel,
|
||||
this.customEmojiStickerList = const []})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _TIMUIKitTextElemState();
|
||||
}
|
||||
|
||||
class _TIMUIKitTextElemState extends TIMUIKitState<TIMUIKitTextElem> {
|
||||
bool isShowJumpState = false;
|
||||
bool isShining = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// get the link preview info
|
||||
_getLinkPreview();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(TIMUIKitTextElem oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.message.msgID == null && widget.message.msgID != null) {
|
||||
_getLinkPreview();
|
||||
}
|
||||
}
|
||||
|
||||
_showJumpColor() {
|
||||
if ((widget.chatModel.jumpMsgID != widget.message.msgID) && (widget.message.msgID?.isNotEmpty ?? true)) {
|
||||
return;
|
||||
}
|
||||
isShining = true;
|
||||
int shineAmount = 6;
|
||||
setState(() {
|
||||
isShowJumpState = true;
|
||||
});
|
||||
Timer.periodic(const Duration(milliseconds: 300), (timer) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
isShowJumpState = shineAmount.isOdd ? true : false;
|
||||
});
|
||||
}
|
||||
if (shineAmount == 0 || !mounted) {
|
||||
isShining = false;
|
||||
timer.cancel();
|
||||
}
|
||||
shineAmount--;
|
||||
});
|
||||
Future.delayed(const Duration(milliseconds: 100), () {
|
||||
widget.clearJump();
|
||||
});
|
||||
}
|
||||
|
||||
// get the link preview info
|
||||
_getLinkPreview() {
|
||||
if (widget.chatModel.chatConfig.urlPreviewType != UrlPreviewType.previewCardAndHyperlink) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (widget.message.localCustomData != null && widget.message.localCustomData!.isNotEmpty) {
|
||||
final String localJSON = widget.message.localCustomData!;
|
||||
final LocalCustomDataModel? localPreviewInfo = LocalCustomDataModel.fromMap(json.decode(localJSON));
|
||||
// If [localCustomData] is not empty, check if the link preview info exists
|
||||
if (localPreviewInfo == null || localPreviewInfo.isLinkPreviewEmpty()) {
|
||||
// If not exists, get it
|
||||
_initLinkPreview();
|
||||
}
|
||||
} else {
|
||||
// It [localCustomData] is empty, get the link info
|
||||
_initLinkPreview();
|
||||
}
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
_initLinkPreview() async {
|
||||
// Get the link preview info from extension, let it update the message UI automatically by providing a [onUpdateMessage].
|
||||
// The `onUpdateMessage` can use the `updateMessage()` from the [TIMUIKitChatController] directly.
|
||||
LinkPreviewEntry.getFirstLinkPreviewContent(
|
||||
message: widget.message,
|
||||
onUpdateMessage: (message) {
|
||||
widget.chatModel.updateMessageFromController(msgID: widget.message.msgID!, message: message);
|
||||
});
|
||||
}
|
||||
|
||||
Widget? _renderPreviewWidget() {
|
||||
// If the link preview info from [localCustomData] is available, use it to render the preview card.
|
||||
// Otherwise, it will returns null.
|
||||
if (widget.message.localCustomData != null && widget.message.localCustomData!.isNotEmpty) {
|
||||
try {
|
||||
final String localJSON = widget.message.localCustomData!;
|
||||
final LocalCustomDataModel? localPreviewInfo = LocalCustomDataModel.fromMap(json.decode(localJSON));
|
||||
if (localPreviewInfo != null && !localPreviewInfo.isLinkPreviewEmpty()) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(top: 8),
|
||||
child:
|
||||
// You can use this default widget [LinkPreviewWidget] to render preview card, or you can use custom widget.
|
||||
LinkPreviewWidget(linkPreview: localPreviewInfo),
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final theme = value.theme;
|
||||
final isDesktopScreen = TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop;
|
||||
final textWithLink = LinkPreviewEntry.getHyperlinksText(
|
||||
widget.message.textElem?.text ?? "", widget.chatModel.chatConfig.isSupportMarkdownForTextMessage,
|
||||
onLinkTap: widget.chatModel.chatConfig.onTapLink,
|
||||
isUseQQPackage: widget.chatModel.chatConfig.stickerPanelConfig?.useQQStickerPackage ?? true,
|
||||
isUseTencentCloudChatPackage:
|
||||
widget.chatModel.chatConfig.stickerPanelConfig?.useTencentCloudChatStickerPackage ?? true,
|
||||
isUseTencentCloudChatPackageOldKeys:
|
||||
widget.chatModel.chatConfig.stickerPanelConfig?.useTencentCloudChatStickerPackageOldKeys ?? false,
|
||||
customEmojiStickerList: widget.customEmojiStickerList,
|
||||
isEnableTextSelection: widget.chatModel.chatConfig.isEnableTextSelection ?? false);
|
||||
final borderRadius = widget.isFromSelf
|
||||
? const BorderRadius.only(
|
||||
topLeft: Radius.circular(10),
|
||||
topRight: Radius.circular(2),
|
||||
bottomLeft: Radius.circular(10),
|
||||
bottomRight: Radius.circular(10))
|
||||
: const BorderRadius.only(
|
||||
topLeft: Radius.circular(2),
|
||||
topRight: Radius.circular(10),
|
||||
bottomLeft: Radius.circular(10),
|
||||
bottomRight: Radius.circular(10));
|
||||
if ((widget.chatModel.jumpMsgID == widget.message.msgID)) {}
|
||||
if (widget.isShowJump) {
|
||||
if (!isShining) {
|
||||
Future.delayed(Duration.zero, () {
|
||||
_showJumpColor();
|
||||
});
|
||||
} else {
|
||||
if ((widget.chatModel.jumpMsgID == widget.message.msgID) && (widget.message.msgID?.isNotEmpty ?? false)) {
|
||||
widget.clearJump();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final defaultStyle = widget.isFromSelf
|
||||
? (theme.chatMessageItemFromSelfBgColor ?? theme.lightPrimaryMaterialColor.shade50)
|
||||
: (theme.chatMessageItemFromOthersBgColor);
|
||||
|
||||
final backgroundColor =
|
||||
isShowJumpState ? const Color.fromRGBO(245, 166, 35, 1) : (defaultStyle ?? widget.backgroundColor);
|
||||
|
||||
return Container(
|
||||
padding: widget.textPadding ?? EdgeInsets.all(isDesktopScreen ? 12 : 10),
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
borderRadius: widget.borderRadius ?? borderRadius,
|
||||
),
|
||||
constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.6),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// If the [elemType] is text message, it will not be null here.
|
||||
// You can render the widget from extension directly, with a [TextStyle] optionally.
|
||||
widget.chatModel.chatConfig.urlPreviewType != UrlPreviewType.none
|
||||
? textWithLink!(
|
||||
style: widget.fontStyle ??
|
||||
TextStyle(
|
||||
fontSize: isDesktopScreen ? 14 : 16,
|
||||
textBaseline: TextBaseline.ideographic,
|
||||
height: widget.chatModel.chatConfig.textHeight))
|
||||
: ExtendedText(widget.message.textElem?.text ?? "",
|
||||
softWrap: true,
|
||||
style: widget.fontStyle ??
|
||||
TextStyle(fontSize: isDesktopScreen ? 14 : 16, height: widget.chatModel.chatConfig.textHeight),
|
||||
specialTextSpanBuilder: DefaultSpecialTextSpanBuilder(
|
||||
isUseQQPackage: widget.chatModel.chatConfig.stickerPanelConfig?.useQQStickerPackage ?? true,
|
||||
isUseTencentCloudChatPackage:
|
||||
widget.chatModel.chatConfig.stickerPanelConfig?.useTencentCloudChatStickerPackage ?? true,
|
||||
isUseTencentCloudChatPackageOldKeys:
|
||||
widget.chatModel.chatConfig.stickerPanelConfig?.useTencentCloudChatStickerPackageOldKeys ??
|
||||
false,
|
||||
customEmojiStickerList: widget.customEmojiStickerList,
|
||||
showAtBackground: true,
|
||||
checkHttpLink: true,
|
||||
)),
|
||||
// If the link preview info is available, render the preview card.
|
||||
if (_renderPreviewWidget() != null &&
|
||||
widget.chatModel.chatConfig.urlPreviewType == UrlPreviewType.previewCardAndHyperlink)
|
||||
_renderPreviewWidget()!,
|
||||
if (widget.isShowMessageReaction ?? true) TIMUIKitMessageReactionShowPanel(message: widget.message)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+190
@@ -0,0 +1,190 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:tencent_chat_i18n_tool/tencent_chat_i18n_tool.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_message.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart';
|
||||
import 'package:extended_text/extended_text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_chat_separate_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitTextField/special_text/DefaultSpecialTextSpanBuilder.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/link_preview/link_preview_entry.dart';
|
||||
|
||||
class TIMUIKitTextTranslationElem extends StatefulWidget {
|
||||
final V2TimMessage message;
|
||||
final bool isFromSelf;
|
||||
final bool isShowJump;
|
||||
final VoidCallback clearJump;
|
||||
final TextStyle? fontStyle;
|
||||
final BorderRadius? borderRadius;
|
||||
final Color? backgroundColor;
|
||||
final EdgeInsetsGeometry? textPadding;
|
||||
final TUIChatSeparateViewModel chatModel;
|
||||
final bool? isShowMessageReaction;
|
||||
final List<CustomEmojiFaceData> customEmojiStickerList;
|
||||
|
||||
const TIMUIKitTextTranslationElem(
|
||||
{Key? key,
|
||||
required this.message,
|
||||
required this.isFromSelf,
|
||||
required this.isShowJump,
|
||||
required this.clearJump,
|
||||
this.fontStyle,
|
||||
this.borderRadius,
|
||||
this.isShowMessageReaction,
|
||||
this.backgroundColor,
|
||||
this.textPadding,
|
||||
required this.chatModel,
|
||||
this.customEmojiStickerList = const []})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _TIMUIKitTextTranslationElemState();
|
||||
}
|
||||
|
||||
class _TIMUIKitTextTranslationElemState extends TIMUIKitState<TIMUIKitTextTranslationElem> {
|
||||
bool isShowJumpState = false;
|
||||
bool isShining = false;
|
||||
|
||||
_showJumpColor() {
|
||||
if ((widget.chatModel.jumpMsgID != widget.message.msgID) && (widget.message.msgID?.isNotEmpty ?? true)) {
|
||||
return;
|
||||
}
|
||||
isShining = true;
|
||||
int shineAmount = 6;
|
||||
setState(() {
|
||||
isShowJumpState = true;
|
||||
});
|
||||
Timer.periodic(const Duration(milliseconds: 300), (timer) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
isShowJumpState = shineAmount.isOdd ? true : false;
|
||||
});
|
||||
}
|
||||
if (shineAmount == 0 || !mounted) {
|
||||
isShining = false;
|
||||
timer.cancel();
|
||||
}
|
||||
shineAmount--;
|
||||
});
|
||||
Future.delayed(const Duration(milliseconds: 100), () {
|
||||
widget.clearJump();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final theme = value.theme;
|
||||
final isDesktopScreen = TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop;
|
||||
final borderRadius = widget.isFromSelf
|
||||
? const BorderRadius.only(
|
||||
topLeft: Radius.circular(10),
|
||||
topRight: Radius.circular(2),
|
||||
bottomLeft: Radius.circular(10),
|
||||
bottomRight: Radius.circular(10))
|
||||
: const BorderRadius.only(
|
||||
topLeft: Radius.circular(2),
|
||||
topRight: Radius.circular(10),
|
||||
bottomLeft: Radius.circular(10),
|
||||
bottomRight: Radius.circular(10));
|
||||
if ((widget.chatModel.jumpMsgID == widget.message.msgID)) {}
|
||||
if (widget.isShowJump) {
|
||||
if (!isShining) {
|
||||
Future.delayed(Duration.zero, () {
|
||||
_showJumpColor();
|
||||
});
|
||||
} else {
|
||||
if ((widget.chatModel.jumpMsgID == widget.message.msgID) && (widget.message.msgID?.isNotEmpty ?? false)) {
|
||||
widget.clearJump();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final defaultStyle = widget.isFromSelf
|
||||
? (theme.chatMessageItemFromSelfBgColor ?? theme.lightPrimaryMaterialColor.shade50)
|
||||
: (theme.chatMessageItemFromOthersBgColor);
|
||||
|
||||
final backgroundColor =
|
||||
isShowJumpState ? const Color.fromRGBO(245, 166, 35, 1) : (defaultStyle ?? widget.backgroundColor);
|
||||
|
||||
final LocalCustomDataModel localCustomData =
|
||||
LocalCustomDataModel.fromMap(json.decode(TencentUtils.checkString(widget.message.localCustomData) ?? "{}"));
|
||||
final String? translateText = localCustomData.translatedText;
|
||||
|
||||
final textWithLink = LinkPreviewEntry.getHyperlinksText(
|
||||
translateText ?? "", widget.chatModel.chatConfig.isSupportMarkdownForTextMessage,
|
||||
onLinkTap: widget.chatModel.chatConfig.onTapLink,
|
||||
isUseQQPackage: widget.chatModel.chatConfig.stickerPanelConfig?.useQQStickerPackage ?? true,
|
||||
isUseTencentCloudChatPackage:
|
||||
widget.chatModel.chatConfig.stickerPanelConfig?.useTencentCloudChatStickerPackage ?? true,
|
||||
isUseTencentCloudChatPackageOldKeys:
|
||||
widget.chatModel.chatConfig.stickerPanelConfig?.useTencentCloudChatStickerPackageOldKeys ?? false,
|
||||
customEmojiStickerList: widget.customEmojiStickerList,
|
||||
isEnableTextSelection: widget.chatModel.chatConfig.isEnableTextSelection ?? false);
|
||||
|
||||
return TencentUtils.checkString(translateText) != null
|
||||
? Container(
|
||||
margin: const EdgeInsets.only(top: 6),
|
||||
padding: widget.textPadding ?? EdgeInsets.all(isDesktopScreen ? 12 : 10),
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
borderRadius: widget.borderRadius ?? borderRadius,
|
||||
),
|
||||
constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.6),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// If the [elemType] is text message, it will not be null here.
|
||||
// You can render the widget from extension directly, with a [TextStyle] optionally.
|
||||
widget.chatModel.chatConfig.urlPreviewType != UrlPreviewType.none
|
||||
? textWithLink!(
|
||||
style: widget.fontStyle ??
|
||||
TextStyle(
|
||||
fontSize: isDesktopScreen ? 14 : 16,
|
||||
textBaseline: TextBaseline.ideographic,
|
||||
height: widget.chatModel.chatConfig.textHeight))
|
||||
: ExtendedText(translateText!,
|
||||
softWrap: true,
|
||||
style: widget.fontStyle ??
|
||||
TextStyle(
|
||||
fontSize: isDesktopScreen ? 14 : 16, height: widget.chatModel.chatConfig.textHeight),
|
||||
specialTextSpanBuilder: DefaultSpecialTextSpanBuilder(
|
||||
isUseQQPackage: widget.chatModel.chatConfig.stickerPanelConfig?.useQQStickerPackage ?? true,
|
||||
isUseTencentCloudChatPackage:
|
||||
widget.chatModel.chatConfig.stickerPanelConfig?.useTencentCloudChatStickerPackage ?? true,
|
||||
isUseTencentCloudChatPackageOldKeys: widget
|
||||
.chatModel.chatConfig.stickerPanelConfig?.useTencentCloudChatStickerPackageOldKeys ??
|
||||
false,
|
||||
customEmojiStickerList: widget.customEmojiStickerList,
|
||||
showAtBackground: true,
|
||||
)),
|
||||
const SizedBox(height: 6),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.check_circle,
|
||||
color: Color(0x72282c34),
|
||||
size: 12,
|
||||
),
|
||||
const SizedBox(
|
||||
width: 4,
|
||||
),
|
||||
Text(
|
||||
TIM_t("翻译完成"),
|
||||
style: const TextStyle(color: Color(0x72282c34), fontSize: 10),
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
: const SizedBox(width: 0, height: 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:loading_animation_widget/loading_animation_widget.dart';
|
||||
import 'package:open_file/open_file.dart';
|
||||
import 'package:tencent_chat_i18n_tool/tencent_chat_i18n_tool.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/enum/message_status.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_message.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_video_elem.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_video_elem.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_callback.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_chat_separate_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/message/message_services.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/services_locatar.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/tui_theme.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/message.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/platform.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitMessageItem/TIMUIKitMessageReaction/tim_uikit_message_reaction_wrapper.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/video_screen.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/wide_popup.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class TIMUIKitVideoElem extends StatefulWidget {
|
||||
final V2TimMessage message;
|
||||
final bool isShowJump;
|
||||
final VoidCallback? clearJump;
|
||||
final String? isFrom;
|
||||
final TUIChatSeparateViewModel chatModel;
|
||||
final bool? isShowMessageReaction;
|
||||
|
||||
const TIMUIKitVideoElem(this.message,
|
||||
{Key? key,
|
||||
this.isShowJump = false,
|
||||
this.clearJump,
|
||||
this.isFrom,
|
||||
this.isShowMessageReaction,
|
||||
required this.chatModel})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _TIMUIKitVideoElemState();
|
||||
}
|
||||
|
||||
class _TIMUIKitVideoElemState extends TIMUIKitState<TIMUIKitVideoElem> {
|
||||
final MessageService _messageService = serviceLocator<MessageService>();
|
||||
late V2TimVideoElem stateElement = widget.message.videoElem!;
|
||||
|
||||
Widget errorDisplay(TUITheme? theme) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
width: 1,
|
||||
color: Colors.black12,
|
||||
)),
|
||||
height: 100,
|
||||
child: Center(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.warning_amber_outlined,
|
||||
color: theme?.cautionColor,
|
||||
size: 16,
|
||||
),
|
||||
Text(
|
||||
TIM_t("视频加载失败"),
|
||||
style: TextStyle(color: theme?.cautionColor),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget generateSnapshot(TUITheme theme, int height) {
|
||||
if (!PlatformUtils().isWeb) {
|
||||
final current = (DateTime.now().millisecondsSinceEpoch / 1000).ceil();
|
||||
final timeStamp = widget.message.timestamp ?? current;
|
||||
if (current - timeStamp < 300) {
|
||||
if (stateElement.snapshotPath != null && stateElement.snapshotPath != '') {
|
||||
File imgF = File(stateElement.snapshotPath!);
|
||||
bool isExist = imgF.existsSync();
|
||||
if (isExist) {
|
||||
return Image.file(File(stateElement.snapshotPath!), fit: BoxFit.fitWidth);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ((stateElement.snapshotUrl == null || stateElement.snapshotUrl == '') &&
|
||||
(stateElement.snapshotPath == null || stateElement.snapshotPath == '')) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(5)),
|
||||
border: Border.all(
|
||||
width: 1,
|
||||
color: Colors.black12,
|
||||
)),
|
||||
height: double.parse(height.toString()),
|
||||
child: Center(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
LoadingAnimationWidget.staggeredDotsWave(
|
||||
color: theme.weakTextColor ?? Colors.grey,
|
||||
size: 28,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return (!PlatformUtils().isWeb && stateElement.snapshotUrl == null ||
|
||||
widget.message.status == MessageStatus.V2TIM_MSG_STATUS_SENDING)
|
||||
? (stateElement.snapshotPath!.isNotEmpty
|
||||
? Image.file(File(stateElement.snapshotPath!), fit: BoxFit.fitWidth)
|
||||
: Image.file(File(stateElement.localSnapshotUrl!), fit: BoxFit.fitWidth))
|
||||
: (PlatformUtils().isWeb || stateElement.localSnapshotUrl == null || stateElement.localSnapshotUrl == "")
|
||||
? Image.network(stateElement.snapshotUrl!, fit: BoxFit.fitWidth)
|
||||
: Image.file(File(stateElement.localSnapshotUrl!), fit: BoxFit.fitWidth);
|
||||
}
|
||||
|
||||
downloadMessageDetailAndSave() async {
|
||||
if (TencentUtils.checkString(widget.message.msgID) != null) {
|
||||
if (TencentUtils.checkString(widget.message.videoElem!.videoUrl) == null) {
|
||||
final response = await _messageService.getMessageOnlineUrl(msgID: widget.message.msgID!);
|
||||
if (response.data != null) {
|
||||
widget.message.videoElem = response.data!.videoElem;
|
||||
Future.delayed(const Duration(microseconds: 10), () {
|
||||
setState(() => stateElement = response.data!.videoElem!);
|
||||
});
|
||||
}
|
||||
}
|
||||
if (!PlatformUtils().isWeb) {
|
||||
if (TencentUtils.checkString(widget.message.videoElem!.localVideoUrl) == null ||
|
||||
!File(widget.message.videoElem!.localVideoUrl!).existsSync()) {
|
||||
_messageService.downloadMessage(
|
||||
msgID: widget.message.msgID!, messageType: 5, imageType: 0, isSnapshot: false);
|
||||
}
|
||||
if (TencentUtils.checkString(widget.message.videoElem!.localSnapshotUrl) == null ||
|
||||
!File(widget.message.videoElem!.localSnapshotUrl!).existsSync()) {
|
||||
_messageService.downloadMessage(msgID: widget.message.msgID!, messageType: 5, imageType: 0, isSnapshot: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
downloadMessageDetailAndSave();
|
||||
}
|
||||
|
||||
void launchDesktopFile(String path) {
|
||||
if (PlatformUtils().isWindows) {
|
||||
OpenFile.open(path);
|
||||
} else {
|
||||
launchUrl(Uri.file(path));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final theme = value.theme;
|
||||
final heroTag =
|
||||
"${widget.message.msgID ?? widget.message.id ?? widget.message.timestamp ?? DateTime.now().millisecondsSinceEpoch}${widget.isFrom}";
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
if (PlatformUtils().isWeb) {
|
||||
final url = widget.message.videoElem?.videoUrl ?? widget.message.videoElem?.videoPath ?? "";
|
||||
TUIKitWidePopup.showMedia(
|
||||
context: context,
|
||||
mediaURL: url,
|
||||
onClickOrigin: () => launchUrl(
|
||||
Uri.parse(url),
|
||||
mode: LaunchMode.externalApplication,
|
||||
));
|
||||
return;
|
||||
}
|
||||
if (PlatformUtils().isDesktop) {
|
||||
final videoElem = widget.message.videoElem;
|
||||
if (videoElem != null) {
|
||||
final localVideoUrl = TencentUtils.checkString(videoElem.localVideoUrl);
|
||||
final videoPath = TencentUtils.checkString(videoElem.videoPath);
|
||||
final videoUrl = videoElem.videoUrl;
|
||||
if (localVideoUrl != null) {
|
||||
launchDesktopFile(localVideoUrl);
|
||||
// todo
|
||||
// TUIKitWidePopup.showMedia(
|
||||
// context: context,
|
||||
// mediaPath: localVideoUrl,
|
||||
// onClickOrigin: () => launchDesktopFile(localVideoUrl));
|
||||
} else if (videoPath != null && File(videoPath).existsSync()) {
|
||||
launchDesktopFile(videoPath);
|
||||
// todo
|
||||
// TUIKitWidePopup.showMedia(
|
||||
// context: context,
|
||||
// mediaPath: videoPath,
|
||||
// onClickOrigin: () => launchDesktopFile(videoPath));
|
||||
} else if (TencentUtils.isTextNotEmpty(videoUrl)) {
|
||||
onTIMCallback(
|
||||
TIMCallback(infoCode: 6660414, infoRecommendText: TIM_t("正在下载中"), type: TIMCallbackType.INFO));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Navigator.of(context).push(
|
||||
PageRouteBuilder(
|
||||
opaque: false, // set to false
|
||||
pageBuilder: (_, __, ___) => VideoScreen(
|
||||
message: widget.message,
|
||||
heroTag: heroTag,
|
||||
videoElement: stateElement,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Hero(
|
||||
tag: heroTag,
|
||||
child: TIMUIKitMessageReactionWrapper(
|
||||
chatModel: widget.chatModel,
|
||||
message: widget.message,
|
||||
isShowJump: widget.isShowJump,
|
||||
isShowMessageReaction: widget.isShowMessageReaction ?? true,
|
||||
clearJump: widget.clearJump,
|
||||
isFromSelf: widget.message.isSelf ?? true,
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(5)),
|
||||
child: LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) {
|
||||
double? positionRadio;
|
||||
if ((stateElement.snapshotWidth) != null &&
|
||||
stateElement.snapshotHeight != null &&
|
||||
stateElement.snapshotWidth != 0 &&
|
||||
stateElement.snapshotHeight != 0) {
|
||||
positionRadio = (stateElement.snapshotWidth! / stateElement.snapshotHeight!);
|
||||
}
|
||||
return ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: PlatformUtils().isWeb ? 300 : constraints.maxWidth * 0.5,
|
||||
maxHeight: min(constraints.maxHeight * 0.8, 300),
|
||||
minHeight: 20,
|
||||
minWidth: 20),
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
if (positionRadio != null &&
|
||||
(stateElement.snapshotUrl != null || stateElement.snapshotUrl != null))
|
||||
AspectRatio(
|
||||
aspectRatio: positionRadio,
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(color: Colors.transparent),
|
||||
),
|
||||
),
|
||||
Row(
|
||||
children: [Expanded(child: generateSnapshot(theme, stateElement.snapshotHeight ?? 100))],
|
||||
),
|
||||
if (widget.message.status != MessageStatus.V2TIM_MSG_STATUS_SENDING &&
|
||||
(stateElement.snapshotUrl != null || stateElement.snapshotPath != null) &&
|
||||
stateElement.videoPath != null ||
|
||||
stateElement.videoUrl != null)
|
||||
Positioned.fill(
|
||||
// alignment: Alignment.center,
|
||||
child: Center(
|
||||
child:
|
||||
Image.asset('images/play.png', package: 'tencent_cloud_chat_uikit', height: 64)),
|
||||
),
|
||||
if (widget.message.videoElem?.duration != null && widget.message.videoElem!.duration! > 0)
|
||||
Positioned(
|
||||
right: 10,
|
||||
bottom: 10,
|
||||
child: Text(
|
||||
MessageUtils.formatVideoTime(widget.message.videoElem!.duration!).toString(),
|
||||
style: const TextStyle(color: Colors.white, fontSize: 12))),
|
||||
],
|
||||
));
|
||||
}),
|
||||
))),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:better_player_plus/better_player_plus.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/enum/message_elem_type.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_message.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_message_online_url.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_message_online_url.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_value_callback.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_value_callback.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/tencent_im_sdk_plugin.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart';
|
||||
|
||||
class TIMUIKitVideoPlayer extends StatefulWidget {
|
||||
final V2TimMessage message;
|
||||
final bool controller;
|
||||
final bool isSending;
|
||||
|
||||
const TIMUIKitVideoPlayer({
|
||||
super.key,
|
||||
required this.message,
|
||||
required this.controller,
|
||||
required this.isSending,
|
||||
});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => TIMUIKitVideoPlayerState();
|
||||
}
|
||||
|
||||
enum CurrentVideoType {
|
||||
online,
|
||||
local,
|
||||
}
|
||||
|
||||
class CurrentVideoInfo {
|
||||
final String path;
|
||||
final CurrentVideoType type;
|
||||
final double aspectRatio;
|
||||
|
||||
CurrentVideoInfo({
|
||||
required this.path,
|
||||
required this.type,
|
||||
required this.aspectRatio,
|
||||
});
|
||||
}
|
||||
|
||||
class TIMUIKitVideoPlayerState extends State<TIMUIKitVideoPlayer> {
|
||||
final String _tag = "TencentCloudChatMessageVideoPlayer";
|
||||
|
||||
BetterPlayerController? _betterPlayerController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initializePlayer();
|
||||
}
|
||||
|
||||
Future<void> _initializePlayer() async {
|
||||
try {
|
||||
final info = await getMessageInfo();
|
||||
if (info != null && mounted) {
|
||||
BetterPlayerDataSource dataSource;
|
||||
if (info.type == CurrentVideoType.online) {
|
||||
dataSource = BetterPlayerDataSource(
|
||||
BetterPlayerDataSourceType.network,
|
||||
info.path,
|
||||
);
|
||||
} else {
|
||||
dataSource = BetterPlayerDataSource(
|
||||
BetterPlayerDataSourceType.file,
|
||||
info.path,
|
||||
);
|
||||
}
|
||||
|
||||
final betterPlayerConfiguration = BetterPlayerConfiguration(
|
||||
aspectRatio: info.aspectRatio,
|
||||
fit: BoxFit.contain,
|
||||
autoPlay: true,
|
||||
allowedScreenSleep: false,
|
||||
fullScreenByDefault: false,
|
||||
controlsConfiguration: const BetterPlayerControlsConfiguration(
|
||||
enableFullscreen: false,
|
||||
enablePlayPause: true,
|
||||
enableProgressBar: true,
|
||||
enableProgressText: true,
|
||||
showControlsOnInitialize: false,
|
||||
enableMute: false,
|
||||
enableOverflowMenu: false,
|
||||
enableSkips: false,
|
||||
),
|
||||
);
|
||||
|
||||
_betterPlayerController = BetterPlayerController(
|
||||
betterPlayerConfiguration,
|
||||
betterPlayerDataSource: dataSource,
|
||||
);
|
||||
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint("Video initialization error: $e");
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_betterPlayerController?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<CurrentVideoInfo?> getMessageInfo() async {
|
||||
if (widget.message.elemType == MessageElemType.V2TIM_ELEM_TYPE_VIDEO) {
|
||||
double aspectRatio = (9 / 16);
|
||||
|
||||
if (widget.isSending) {
|
||||
var lp = widget.message.videoElem!.videoPath ?? "";
|
||||
if (lp.isNotEmpty) {
|
||||
console("view sending message video path");
|
||||
if (File(lp).existsSync() && !kIsWeb) {
|
||||
return CurrentVideoInfo(path: lp, type: CurrentVideoType.local, aspectRatio: aspectRatio);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (widget.message.videoElem!.snapshotWidth != null && widget.message.videoElem!.snapshotHeight != null) {
|
||||
if (widget.message.videoElem!.snapshotHeight != 0) {
|
||||
aspectRatio = (widget.message.videoElem!.snapshotWidth!) / (widget.message.videoElem!.snapshotHeight!);
|
||||
}
|
||||
}
|
||||
|
||||
if (TencentUtils.checkString(widget.message.videoElem!.videoPath) != null) {
|
||||
// 先查本地发送的视频地址
|
||||
if (File(widget.message.videoElem!.videoPath!).existsSync()) {
|
||||
console("video: local video path exists");
|
||||
return CurrentVideoInfo(
|
||||
path: widget.message.videoElem!.videoPath!, type: CurrentVideoType.local, aspectRatio: aspectRatio);
|
||||
}
|
||||
} else if (TencentUtils.checkString(widget.message.videoElem!.localVideoUrl) != null) {
|
||||
// 再查本地下载的视频地址
|
||||
if (File(widget.message.videoElem!.localVideoUrl!).existsSync()) {
|
||||
console("video: local url exists");
|
||||
return CurrentVideoInfo(
|
||||
path: widget.message.videoElem!.localVideoUrl!, type: CurrentVideoType.local, aspectRatio: aspectRatio);
|
||||
}
|
||||
} else {
|
||||
// 最后再查在线地址(todo 使用 getMessageOnlineUrl 查询)
|
||||
if (widget.message.videoElem != null) {
|
||||
if (widget.message.videoElem!.snapshotUrl != null) {
|
||||
console("video: online url ${widget.message.videoElem!.videoUrl}");
|
||||
return CurrentVideoInfo(
|
||||
path: widget.message.videoElem!.videoUrl!,
|
||||
type: CurrentVideoType.online,
|
||||
aspectRatio: aspectRatio,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (!kIsWeb) {
|
||||
V2TimValueCallback<V2TimMessageOnlineUrl> urlres = await TencentImSDKPlugin.v2TIMManager
|
||||
.getMessageManager()
|
||||
.getMessageOnlineUrl(msgID: widget.message.msgID ?? "");
|
||||
if (urlres.data != null) {
|
||||
if (urlres.data?.videoElem != null) {
|
||||
if (TencentUtils.checkString(urlres.data?.videoElem?.videoUrl) != null) {
|
||||
console("view video online url ${urlres.data?.videoElem?.videoUrl}");
|
||||
return CurrentVideoInfo(
|
||||
path: urlres.data!.videoElem!.videoUrl!, type: CurrentVideoType.online, aspectRatio: aspectRatio);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console("The component received a non-video message parameter. please check");
|
||||
}
|
||||
console("has no view video source. please check");
|
||||
return null;
|
||||
}
|
||||
|
||||
console(String log) {
|
||||
print("$_tag, $log");
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.message.hasRiskContent == true) {
|
||||
return const Center(
|
||||
child: Text(
|
||||
"Risk Video",
|
||||
style: TextStyle(color: Colors.white),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (_betterPlayerController == null) {
|
||||
return Container();
|
||||
}
|
||||
|
||||
return AspectRatio(
|
||||
aspectRatio: _betterPlayerController!.videoPlayerController?.value.aspectRatio ?? 9 / 16,
|
||||
child: BetterPlayer(
|
||||
controller: _betterPlayerController!,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:tencent_chat_i18n_tool/tencent_chat_i18n_tool.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_merger_elem.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_merger_elem.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_message.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_chat_separate_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/core/tim_uikit_wide_modal_operation_key.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKItMessageList/tim_uikit_chat_history_message_list_item.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/merger_message_screen.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_callback.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/wide_popup.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/color.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/tui_theme.dart';
|
||||
import 'TIMUIKitMessageReaction/tim_uikit_message_reaction_show_panel.dart';
|
||||
|
||||
class TIMUIKitMergerElem extends StatefulWidget {
|
||||
final V2TimMergerElem mergerElem;
|
||||
final String messageID;
|
||||
final bool isSelf;
|
||||
final bool isShowJump;
|
||||
final VoidCallback? clearJump;
|
||||
final V2TimMessage message;
|
||||
final bool? isShowMessageReaction;
|
||||
final TUIChatSeparateViewModel model;
|
||||
final MessageItemBuilder? messageItemBuilder;
|
||||
|
||||
const TIMUIKitMergerElem(
|
||||
{Key? key,
|
||||
required this.message,
|
||||
required this.model,
|
||||
required this.mergerElem,
|
||||
required this.isSelf,
|
||||
this.isShowMessageReaction,
|
||||
required this.messageID,
|
||||
required this.isShowJump,
|
||||
this.clearJump,
|
||||
this.messageItemBuilder})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => TIMUIKitMergerElemState();
|
||||
}
|
||||
|
||||
class TIMUIKitMergerElemState extends TIMUIKitState<TIMUIKitMergerElem> {
|
||||
bool isShowJumpState = false;
|
||||
late ScrollController _scrollController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_scrollController = ScrollController();
|
||||
}
|
||||
|
||||
_showJumpColor() {
|
||||
int shineAmount = 6;
|
||||
setState(() {
|
||||
isShowJumpState = true;
|
||||
});
|
||||
Future.delayed(const Duration(milliseconds: 100), () {
|
||||
if (widget.clearJump != null) {
|
||||
widget.clearJump!();
|
||||
}
|
||||
});
|
||||
Timer.periodic(const Duration(milliseconds: 300), (timer) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
isShowJumpState = shineAmount.isOdd ? true : false;
|
||||
});
|
||||
}
|
||||
if (shineAmount == 0 || !mounted) {
|
||||
timer.cancel();
|
||||
}
|
||||
shineAmount--;
|
||||
});
|
||||
}
|
||||
|
||||
_handleTap(BuildContext context, TUIChatSeparateViewModel model) async {
|
||||
try {
|
||||
if (widget.messageID != "") {
|
||||
final isDesktopScreen = TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop;
|
||||
|
||||
if (isDesktopScreen) {
|
||||
TUIKitWidePopup.showPopupWindow(
|
||||
operationKey: TUIKitWideModalOperationKey.mergerMessageList,
|
||||
context: context,
|
||||
width: MediaQuery.of(context).size.width * 0.7,
|
||||
title: TIM_t("聊天记录"),
|
||||
height: MediaQuery.of(context).size.height * 0.7,
|
||||
child: (onClose) => Scrollbar(
|
||||
controller: _scrollController,
|
||||
child: MergerMessageScreen(
|
||||
messageItemBuilder: widget.messageItemBuilder, model: model, msgID: widget.messageID),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => MergerMessageScreen(
|
||||
messageItemBuilder: widget.messageItemBuilder, model: model, msgID: widget.messageID),
|
||||
));
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
onTIMCallback(TIMCallback(type: TIMCallbackType.INFO, infoRecommendText: TIM_t("无法定位到原消息"), infoCode: 6660401));
|
||||
}
|
||||
}
|
||||
|
||||
List<String>? _getAbstractList() {
|
||||
final length = widget.mergerElem.abstractList!.length;
|
||||
if (length <= 4) {
|
||||
return widget.mergerElem.abstractList;
|
||||
}
|
||||
return widget.mergerElem.abstractList!.getRange(0, 4).toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final TUITheme theme = value.theme;
|
||||
if (widget.isShowJump) {
|
||||
Future.delayed(Duration.zero, () {
|
||||
_showJumpColor();
|
||||
});
|
||||
}
|
||||
final isDesktopScreen = TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop;
|
||||
return Container(
|
||||
constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * (isDesktopScreen ? 0.3 : 0.6)),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: widget.isSelf ? const Radius.circular(10) : Radius.zero,
|
||||
bottomLeft: const Radius.circular(10),
|
||||
topRight: widget.isSelf ? Radius.zero : const Radius.circular(10),
|
||||
bottomRight: const Radius.circular(10),
|
||||
),
|
||||
border: Border.all(
|
||||
color: isShowJumpState
|
||||
? const Color.fromRGBO(245, 166, 35, 1)
|
||||
: (theme.weakDividerColor ?? CommonColor.weakDividerColor),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
_handleTap(context, widget.model);
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
widget.mergerElem.title!,
|
||||
softWrap: true,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.left,
|
||||
maxLines: 1,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: 4,
|
||||
),
|
||||
// const Divider(),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: _getAbstractList()!
|
||||
.map(
|
||||
(e) => Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
e,
|
||||
textAlign: TextAlign.left,
|
||||
softWrap: true,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 2,
|
||||
style: TextStyle(
|
||||
color: theme.weakTextColor,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 4,
|
||||
),
|
||||
const Divider(),
|
||||
Text(
|
||||
TIM_t("聊天记录"),
|
||||
style: TextStyle(
|
||||
color: theme.weakTextColor,
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
if (widget.isShowMessageReaction ?? true) TIMUIKitMessageReactionShowPanel(message: widget.message)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:scroll_to_index/scroll_to_index.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_member_full_info.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_group_member_full_info.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_chat_separate_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/avatar.dart';
|
||||
|
||||
class AtMemberPanel extends StatefulWidget {
|
||||
/// messageList widget scroll controller
|
||||
final AutoScrollController atMemberPanelScroll;
|
||||
|
||||
final ValueChanged<V2TimGroupMemberFullInfo> onSelectMember;
|
||||
|
||||
// final TextFieldWebController textFieldWebController;
|
||||
const AtMemberPanel(
|
||||
// this.textFieldWebController,
|
||||
{Key? key,
|
||||
required this.atMemberPanelScroll,
|
||||
required this.onSelectMember})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() {
|
||||
return _AtMemberPanelState();
|
||||
}
|
||||
}
|
||||
|
||||
_getShowName(V2TimGroupMemberFullInfo? item) {
|
||||
return TencentUtils.checkStringWithoutSpace(item?.nameCard) ??
|
||||
TencentUtils.checkStringWithoutSpace(item?.nickName) ??
|
||||
TencentUtils.checkStringWithoutSpace(item?.userID);
|
||||
}
|
||||
|
||||
class _AtMemberPanelState extends TIMUIKitState<AtMemberPanel> {
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final theme = value.theme;
|
||||
final chatModal = Provider.of<TUIChatSeparateViewModel>(context);
|
||||
final List<V2TimGroupMemberFullInfo?> groupMemberList = chatModal.showAtMemberList;
|
||||
final double positionX = chatModal.atPositionX;
|
||||
final double positionY = chatModal.atPositionY;
|
||||
final int activeIndex = chatModal.activeAtIndex;
|
||||
|
||||
if (groupMemberList.isEmpty) {
|
||||
return Container();
|
||||
}
|
||||
return Positioned(
|
||||
left: positionX,
|
||||
bottom: positionY,
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxHeight: 170, maxWidth: 170),
|
||||
padding: const EdgeInsets.symmetric(vertical: 5),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
border: Border.all(color: const Color(0xFFE5E6E9))),
|
||||
child: Scrollbar(
|
||||
controller: widget.atMemberPanelScroll,
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: groupMemberList.length,
|
||||
controller: widget.atMemberPanelScroll,
|
||||
itemBuilder: ((context, index) {
|
||||
final memberItem = groupMemberList[index];
|
||||
if (memberItem == null) {
|
||||
return AutoScrollTag(key: ValueKey(index), controller: widget.atMemberPanelScroll, index: index);
|
||||
}
|
||||
final showName = _getShowName(memberItem);
|
||||
final isAtAll = memberItem.userID == "__kImSDK_MesssageAtALL__";
|
||||
return AutoScrollTag(
|
||||
key: ValueKey(index),
|
||||
controller: widget.atMemberPanelScroll,
|
||||
index: index,
|
||||
child: Material(
|
||||
color: theme.wideBackgroundColor,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
chatModal.activeAtIndex = index;
|
||||
widget.onSelectMember(memberItem);
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
color: activeIndex == index ? theme.weakBackgroundColor : theme.wideBackgroundColor,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 24,
|
||||
width: 24,
|
||||
child: Avatar(faceUrl: memberItem.faceUrl ?? "", type: 1, showName: showName),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 8,
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
isAtAll ? "$showName(${groupMemberList.length - 1})" : showName,
|
||||
softWrap: false,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
fontWeight: memberItem.role == 400 || memberItem.role == 300
|
||||
? FontWeight.w500
|
||||
: FontWeight.normal,
|
||||
color: memberItem.role == 400 || memberItem.role == 300
|
||||
? theme.primaryColor
|
||||
: theme.darkTextColor),
|
||||
))
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
})),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
// ignore_for_file: file_names
|
||||
|
||||
import 'package:extended_text_field/extended_text_field.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitTextField/special_text/http_text.dart';
|
||||
import 'package:tim_ui_kit_sticker_plugin/utils/tim_custom_face_data.dart';
|
||||
|
||||
import 'emoji_text.dart';
|
||||
|
||||
class DefaultSpecialTextSpanBuilder extends SpecialTextSpanBuilder {
|
||||
DefaultSpecialTextSpanBuilder({
|
||||
this.isUseQQPackage = false,
|
||||
this.isUseTencentCloudChatPackage = false,
|
||||
this.isUseTencentCloudChatPackageOldKeys = false,
|
||||
this.customEmojiStickerList = const [],
|
||||
this.showAtBackground = false,
|
||||
this.checkHttpLink = true,
|
||||
});
|
||||
|
||||
/// whether show background for @somebody
|
||||
final bool showAtBackground;
|
||||
|
||||
final bool isUseQQPackage;
|
||||
|
||||
final bool isUseTencentCloudChatPackage;
|
||||
|
||||
final bool isUseTencentCloudChatPackageOldKeys;
|
||||
|
||||
final bool checkHttpLink;
|
||||
|
||||
final List<CustomEmojiFaceData> customEmojiStickerList;
|
||||
|
||||
@override
|
||||
SpecialText? createSpecialText(String flag,
|
||||
{TextStyle? textStyle, SpecialTextGestureTapCallback? onTap, int? index}) {
|
||||
if (flag == '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
///index is end index of start flag, so text start index should be index-(flag.length-1)
|
||||
if (isStart(flag, EmojiText.flag)) {
|
||||
return EmojiText(textStyle,
|
||||
isUseTencentCloudChatPackage: isUseTencentCloudChatPackage,
|
||||
isUseTencentCloudChatPackageOldKeys: isUseTencentCloudChatPackageOldKeys,
|
||||
isUseQQPackage: isUseQQPackage,
|
||||
start: index! - (EmojiText.flag.length - 1),
|
||||
customEmojiStickerList: customEmojiStickerList);
|
||||
} else if (isStart(flag, HttpText.flag) && checkHttpLink) {
|
||||
return HttpText(textStyle, onTap, start: index! - (HttpText.flag.length - 1));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:extended_text/extended_text.dart';
|
||||
import 'package:tim_ui_kit_sticker_plugin/constant/emoji.dart';
|
||||
import 'package:tim_ui_kit_sticker_plugin/utils/tim_custom_face_data.dart';
|
||||
|
||||
///emoji/image text
|
||||
class EmojiText extends SpecialText {
|
||||
EmojiText(TextStyle? textStyle,
|
||||
{this.start,
|
||||
this.isUseQQPackage = false,
|
||||
this.isUseTencentCloudChatPackage = false,
|
||||
this.isUseTencentCloudChatPackageOldKeys = false,
|
||||
this.customEmojiStickerList = const []})
|
||||
: super(EmojiText.flag, ']', textStyle);
|
||||
static const String flag = '[';
|
||||
final int? start;
|
||||
final bool isUseQQPackage;
|
||||
final bool isUseTencentCloudChatPackage;
|
||||
final bool isUseTencentCloudChatPackageOldKeys;
|
||||
final List<CustomEmojiFaceData> customEmojiStickerList;
|
||||
|
||||
@override
|
||||
InlineSpan finishText() {
|
||||
final String key = toString();
|
||||
final EmojiUtil emojiUtil = EmojiUtil(
|
||||
isUseQQPackage: isUseQQPackage,
|
||||
isUseTencentCloudChatPackage: isUseTencentCloudChatPackage,
|
||||
isUseTencentCloudChatPackageOldKeys: isUseTencentCloudChatPackageOldKeys,
|
||||
customEmojiStickerList: customEmojiStickerList);
|
||||
|
||||
if (emojiUtil.emojiMap.containsKey(key)) {
|
||||
double size = 16;
|
||||
|
||||
final TextStyle ts = textStyle!;
|
||||
if (ts.fontSize != null) {
|
||||
size = ts.fontSize! * 1.44;
|
||||
}
|
||||
|
||||
if (isUseQQPackage == true && (emojiUtil.emojiKeyCategoryMap["4349"]?.contains(key) ?? false)) {
|
||||
return ImageSpan(AssetImage(emojiUtil.emojiMap[key]!, package: "tim_ui_kit_sticker_plugin"),
|
||||
actualText: key,
|
||||
imageWidth: size,
|
||||
imageHeight: size,
|
||||
start: start!,
|
||||
// fit: BoxFit.cover,
|
||||
margin: const EdgeInsets.all(0));
|
||||
} else if (isUseTencentCloudChatPackage == true &&
|
||||
(emojiUtil.emojiKeyCategoryMap["tcc1"]?.contains(key) ?? false)) {
|
||||
return ImageSpan(AssetImage(emojiUtil.emojiMap[key]!, package: "tim_ui_kit_sticker_plugin"),
|
||||
actualText: key,
|
||||
imageWidth: size,
|
||||
imageHeight: size,
|
||||
start: start!,
|
||||
// fit: BoxFit.cover,
|
||||
margin: const EdgeInsets.all(0));
|
||||
} else {
|
||||
return ImageSpan(AssetImage(emojiUtil.emojiMap[key]!),
|
||||
actualText: key,
|
||||
imageWidth: size,
|
||||
imageHeight: size,
|
||||
start: start!,
|
||||
// fit: BoxFit.cover,
|
||||
margin: const EdgeInsets.all(0));
|
||||
}
|
||||
}
|
||||
|
||||
return TextSpan(text: toString(), style: textStyle);
|
||||
}
|
||||
}
|
||||
|
||||
class EmojiUtil {
|
||||
// Private constructor initializing the emoji data
|
||||
EmojiUtil._internal(
|
||||
{required this.isUseQQPackage,
|
||||
required this.isUseTencentCloudChatPackage,
|
||||
required this.isUseTencentCloudChatPackageOldKeys,
|
||||
required this.customEmojiStickerList}) {
|
||||
_emojiMap.addAll(loadDefaultEmojis());
|
||||
|
||||
final customEmojis = loadCustomEmojis();
|
||||
_emojiMap.addAll(customEmojis.$1);
|
||||
_emojiKeyCategoryMap["custom"] = customEmojis.$2;
|
||||
}
|
||||
|
||||
final bool isUseQQPackage;
|
||||
final bool isUseTencentCloudChatPackage;
|
||||
final bool isUseTencentCloudChatPackageOldKeys;
|
||||
final List<CustomEmojiFaceData> customEmojiStickerList;
|
||||
|
||||
// Load the default emojis into a Map
|
||||
Map<String, String> loadDefaultEmojis() {
|
||||
Map<String, String> defaultEmojiMap = {};
|
||||
for (final emojiGroup in TUIKitStickerConstData.emojiList) {
|
||||
final groupName = emojiGroup.name;
|
||||
final keyList = [];
|
||||
if (isUseQQPackage && groupName == "4349") {
|
||||
for (final emoji in emojiGroup.list) {
|
||||
String emojiName = emoji.split('.png')[0];
|
||||
defaultEmojiMap['[$emojiName]'] = '$_emojiFilePath/$groupName/$emojiName.png';
|
||||
keyList.add('[$emojiName]');
|
||||
|
||||
final zhKey = TUIKitStickerConstData.emoji4349ZhMapList[emojiName];
|
||||
defaultEmojiMap['[$zhKey]'] = '$_emojiFilePath/$groupName/$emojiName.png';
|
||||
keyList.add('[$zhKey]');
|
||||
}
|
||||
_emojiKeyCategoryMap[groupName] = keyList;
|
||||
}
|
||||
|
||||
if (isUseTencentCloudChatPackage && groupName == "tcc1") {
|
||||
for (final emoji in emojiGroup.list) {
|
||||
String emojiName = emoji.split('.png')[0];
|
||||
String compatibleEmojiName = emojiName;
|
||||
if (isUseTencentCloudChatPackageOldKeys) {
|
||||
// use old emoji keys in 3.x version
|
||||
compatibleEmojiName = getCompatibleEmojiName(emojiName);
|
||||
}
|
||||
|
||||
defaultEmojiMap['[$compatibleEmojiName]'] = '$_emojiFilePath/$groupName/$emojiName.png';
|
||||
keyList.add('[$compatibleEmojiName]');
|
||||
}
|
||||
_emojiKeyCategoryMap[groupName] = keyList;
|
||||
}
|
||||
}
|
||||
return defaultEmojiMap;
|
||||
}
|
||||
|
||||
// Load the custom emojis into a Map
|
||||
(Map<String, String>, List<String>) loadCustomEmojis() {
|
||||
Map<String, String> customEmojiMap = {};
|
||||
List<String> keyList = [];
|
||||
for (final customEmojiGroup in customEmojiStickerList) {
|
||||
for (final customEmoji in customEmojiGroup.list) {
|
||||
String customEmojiName = customEmoji.split('.png')[0];
|
||||
customEmojiMap['[$customEmojiName]'] = '$_emojiFilePath/${customEmojiGroup.name}/$customEmojiName.png';
|
||||
keyList.add('[$customEmojiName]');
|
||||
}
|
||||
}
|
||||
return (customEmojiMap, keyList);
|
||||
}
|
||||
|
||||
// A Map instance variable to store the emojis and their paths
|
||||
final Map<String, String> _emojiMap = <String, String>{};
|
||||
|
||||
// A getter method for _emojiMap
|
||||
Map<String, String> get emojiMap => _emojiMap;
|
||||
|
||||
// A Map instance variable to store the emojis and their paths
|
||||
final Map<String, List> _emojiKeyCategoryMap = <String, List>{};
|
||||
|
||||
// A getter method for _emojiMap
|
||||
Map<String, List> get emojiKeyCategoryMap => _emojiKeyCategoryMap;
|
||||
|
||||
// An instance variable to store the emoji file path
|
||||
final String _emojiFilePath = 'assets/custom_face_resource';
|
||||
|
||||
// Singleton pattern to avoid creating multiple instances of EmojiUtil
|
||||
static EmojiUtil? _instance;
|
||||
|
||||
// Factory constructor to return the singleton instance of EmojiUtil with custom parameters
|
||||
factory EmojiUtil(
|
||||
{bool isUseQQPackage = false,
|
||||
bool isUseTencentCloudChatPackage = false,
|
||||
bool isUseTencentCloudChatPackageOldKeys = false,
|
||||
List<CustomEmojiFaceData> customEmojiStickerList = const []}) {
|
||||
return _instance ??= EmojiUtil._internal(
|
||||
isUseQQPackage: isUseQQPackage,
|
||||
customEmojiStickerList: customEmojiStickerList,
|
||||
isUseTencentCloudChatPackage: isUseTencentCloudChatPackage,
|
||||
isUseTencentCloudChatPackageOldKeys: isUseTencentCloudChatPackageOldKeys);
|
||||
}
|
||||
|
||||
static String getCompatibleEmojiName(String emojiName) {
|
||||
String compatibleEmojiName = emojiName;
|
||||
try {
|
||||
compatibleEmojiName = emojiName.split('_')[1];
|
||||
// 对特殊字符串 Ok 进行处理
|
||||
if (compatibleEmojiName == 'Ok') {
|
||||
compatibleEmojiName = 'OK';
|
||||
}
|
||||
} catch (e) {
|
||||
print(e);
|
||||
}
|
||||
|
||||
return compatibleEmojiName;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/link_preview/common/utils.dart';
|
||||
import 'package:extended_text/extended_text.dart';
|
||||
|
||||
class HttpText extends SpecialText {
|
||||
HttpText(TextStyle? textStyle, SpecialTextGestureTapCallback? onTap,
|
||||
{this.start})
|
||||
: super(flag, flag, textStyle, onTap: onTap);
|
||||
static const String flag = '!@TURL#*&\$';
|
||||
final int? start;
|
||||
@override
|
||||
InlineSpan finishText() {
|
||||
final String text = getContent();
|
||||
|
||||
return SpecialTextSpan(
|
||||
text: text,
|
||||
actualText: toString(),
|
||||
start: start!,
|
||||
|
||||
///caret can move into special text
|
||||
deleteAll: true,
|
||||
style: TextStyle(color: LinkUtils.hexToColor("015fff")),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () {
|
||||
if (onTap != null) {
|
||||
onTap!(toString());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:tencent_chat_i18n_tool/tencent_chat_i18n_tool.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_info.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_group_info.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_member_full_info.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_group_member_full_info.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_member_search_param.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_group_member_search_param.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_member_search_result.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_group_member_search_result.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_user_full_info.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_user_full_info.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_value_callback.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_value_callback.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_self_info_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/group/group_services.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/services_locatar.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/platform.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitGroupProfile/widgets/tim_ui_group_member_search.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/group_member_list.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/tui_theme.dart';
|
||||
|
||||
class AtText extends StatefulWidget {
|
||||
final String? groupID;
|
||||
final V2TimGroupInfo? groupInfo;
|
||||
final List<V2TimGroupMemberFullInfo?>? groupMemberList;
|
||||
final VoidCallback? closeFunc;
|
||||
final Function(List<V2TimGroupMemberFullInfo> memberInfo)? onChooseMember;
|
||||
final bool canAtAll;
|
||||
|
||||
// some Group type cant @all
|
||||
final String? groupType;
|
||||
|
||||
const AtText({
|
||||
this.groupID,
|
||||
this.groupType,
|
||||
Key? key,
|
||||
this.groupInfo,
|
||||
this.groupMemberList,
|
||||
this.closeFunc,
|
||||
this.onChooseMember,
|
||||
this.canAtAll = false,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _AtTextState();
|
||||
}
|
||||
|
||||
class _AtTextState extends TIMUIKitState<AtText> {
|
||||
final GroupServices _groupServices = serviceLocator<GroupServices>();
|
||||
final TUISelfInfoViewModel _selfInfoViewModel = serviceLocator<TUISelfInfoViewModel>();
|
||||
|
||||
List<V2TimGroupMemberFullInfo?>? groupMemberList;
|
||||
List<V2TimGroupMemberFullInfo?>? searchMemberList;
|
||||
|
||||
List<V2TimGroupMemberFullInfo> selectedGroupMemberList = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
groupMemberList = widget.groupMemberList;
|
||||
searchMemberList = groupMemberList;
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _submitAtMemberList() {
|
||||
if (widget.closeFunc != null) {
|
||||
widget.closeFunc!();
|
||||
}
|
||||
|
||||
if (widget.onChooseMember != null) {
|
||||
widget.onChooseMember!(selectedGroupMemberList);
|
||||
} else {
|
||||
Navigator.pop(context, selectedGroupMemberList);
|
||||
}
|
||||
}
|
||||
|
||||
Future<V2TimValueCallback<V2GroupMemberInfoSearchResult>> searchGroupMember(
|
||||
V2TimGroupMemberSearchParam searchParam) async {
|
||||
final res = await _groupServices.searchGroupMembers(searchParam: searchParam);
|
||||
|
||||
if (res.code == 0) {}
|
||||
return res;
|
||||
}
|
||||
|
||||
handleSearchGroupMembers(String searchText, context) async {
|
||||
final res = await searchGroupMember(V2TimGroupMemberSearchParam(
|
||||
keywordList: [searchText],
|
||||
groupIDList: [widget.groupID!],
|
||||
));
|
||||
|
||||
if (res.code == 0) {
|
||||
List<V2TimGroupMemberFullInfo?> list = [];
|
||||
final searchResult = res.data!.groupMemberSearchResultItems!;
|
||||
searchResult.forEach((key, value) {
|
||||
if (value is List) {
|
||||
for (V2TimGroupMemberFullInfo item in value) {
|
||||
list.add(item);
|
||||
}
|
||||
}
|
||||
});
|
||||
searchMemberList = list;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
searchMemberList = isSearchTextExist(searchText) ? searchMemberList : groupMemberList;
|
||||
});
|
||||
}
|
||||
|
||||
bool isSearchTextExist(String? searchText) {
|
||||
return searchText != null && searchText != "";
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final TUITheme theme = value.theme;
|
||||
|
||||
V2TimUserFullInfo? loginUserInfo = _selfInfoViewModel.loginInfo;
|
||||
if (loginUserInfo != null) {
|
||||
searchMemberList?.removeWhere((memberInfo) {
|
||||
return memberInfo?.userID == loginUserInfo.userID;
|
||||
});
|
||||
}
|
||||
|
||||
Widget mentionedMembersBody() {
|
||||
return GroupProfileMemberList(
|
||||
groupType: widget.groupType ?? "",
|
||||
memberList: searchMemberList ?? [],
|
||||
canAtAll: widget.canAtAll,
|
||||
canSelectMember: true,
|
||||
canSlideDelete: false,
|
||||
onSelectedMemberChange: (selectedMemberList) {
|
||||
selectedGroupMemberList = selectedMemberList;
|
||||
bool isAtAllSelected = selectedGroupMemberList.where((element) {
|
||||
return element.userID == GroupProfileMemberList.AT_ALL_USER_ID;
|
||||
}).isNotEmpty;
|
||||
|
||||
if (isAtAllSelected) {
|
||||
_submitAtMemberList();
|
||||
}
|
||||
},
|
||||
touchBottomCallBack: () {
|
||||
// Get all by once, unnecessary to load more
|
||||
},
|
||||
customTopArea: PlatformUtils().isWeb
|
||||
? null
|
||||
: GroupMemberSearchTextField(
|
||||
onTextChange: (text) => handleSearchGroupMembers(text, context),
|
||||
));
|
||||
}
|
||||
|
||||
return TUIKitScreenUtils.getDeviceWidget(
|
||||
context: context,
|
||||
desktopWidget: mentionedMembersBody(),
|
||||
defaultWidget: Scaffold(
|
||||
appBar: AppBar(
|
||||
shadowColor: theme.weakBackgroundColor,
|
||||
iconTheme: IconThemeData(
|
||||
color: theme.appbarTextColor,
|
||||
),
|
||||
backgroundColor: theme.appbarBgColor ?? theme.primaryColor,
|
||||
leading: Row(
|
||||
children: [
|
||||
IconButton(
|
||||
padding: const EdgeInsets.only(left: 16),
|
||||
constraints: const BoxConstraints(),
|
||||
icon: Image.asset(
|
||||
'images/arrow_back.png',
|
||||
package: 'tencent_cloud_chat_uikit',
|
||||
height: 34,
|
||||
width: 34,
|
||||
color: theme.appbarTextColor,
|
||||
),
|
||||
onPressed: () async {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
centerTitle: true,
|
||||
leadingWidth: 100,
|
||||
title: Text(
|
||||
TIM_t("选择提醒人"),
|
||||
style: TextStyle(
|
||||
color: theme.appbarTextColor,
|
||||
fontSize: 17,
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
_submitAtMemberList();
|
||||
},
|
||||
child: Text(
|
||||
TIM_t("确定"),
|
||||
style: TextStyle(
|
||||
color: theme.appbarTextColor,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
body: mentionedMembersBody()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:loading_animation_widget/loading_animation_widget.dart';
|
||||
import 'package:tencent_chat_i18n_tool/tencent_chat_i18n_tool.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/enum/group_member_filter_enum.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_member_full_info.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_group_member_full_info.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_member_search_param.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_group_member_search_param.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_member_search_result.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_group_member_search_result.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_value_callback.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_value_callback.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/group/group_services.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/services_locatar.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart';
|
||||
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/platform.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitGroupProfile/widgets/tim_ui_group_member_search.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/group_member_list.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart';
|
||||
|
||||
import 'package:tencent_cloud_chat_uikit/theme/tui_theme.dart';
|
||||
|
||||
class SelectCallInviter extends StatefulWidget {
|
||||
final String? groupID;
|
||||
const SelectCallInviter({
|
||||
this.groupID,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _SelectCallInviterState();
|
||||
}
|
||||
|
||||
class _SelectCallInviterState extends TIMUIKitState<SelectCallInviter> {
|
||||
final CoreServicesImpl _coreServicesImpl = serviceLocator<CoreServicesImpl>();
|
||||
final GroupServices _groupServices = serviceLocator<GroupServices>();
|
||||
List<V2TimGroupMemberFullInfo> selectedMember = [];
|
||||
List<V2TimGroupMemberFullInfo?>? _groupMemberList = [];
|
||||
String _groupMemberListSeq = "0";
|
||||
List<V2TimGroupMemberFullInfo?>? searchMemberList;
|
||||
String? searchText;
|
||||
bool loading = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.groupID != null) {
|
||||
_loadGroupMemberList(groupID: widget.groupID!);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
bool isSearchTextExist(String? searchText) {
|
||||
return searchText != null && searchText != "";
|
||||
}
|
||||
|
||||
Future<void> _loadGroupMemberList({required String groupID, int count = 100, String? seq}) async {
|
||||
if (seq == null || seq == "" || seq == "0") {
|
||||
_groupMemberList = [];
|
||||
}
|
||||
final String? nextSeq = await _loadGroupMemberListFunction(groupID: groupID, seq: seq, count: count);
|
||||
if (nextSeq != null && nextSeq != "0" && nextSeq != "") {
|
||||
return await _loadGroupMemberList(groupID: groupID, count: count, seq: nextSeq);
|
||||
} else {
|
||||
setState(() {
|
||||
_groupMemberList = _groupMemberList;
|
||||
searchMemberList = _groupMemberList;
|
||||
loading = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<String?> _loadGroupMemberListFunction({required String groupID, int count = 100, String? seq}) async {
|
||||
if (seq == "0") {
|
||||
_groupMemberList?.clear();
|
||||
}
|
||||
final res = await _groupServices.getGroupMemberList(
|
||||
groupID: widget.groupID!,
|
||||
filter: GroupMemberFilterTypeEnum.V2TIM_GROUP_MEMBER_FILTER_ALL,
|
||||
count: count,
|
||||
nextSeq: seq ?? _groupMemberListSeq);
|
||||
final groupMemberListRes = res.data;
|
||||
if (res.code == 0 && groupMemberListRes != null) {
|
||||
final groupMemberListTemp = groupMemberListRes.memberInfoList ?? [];
|
||||
_groupMemberList = [...?_groupMemberList, ...groupMemberListTemp];
|
||||
_groupMemberListSeq = groupMemberListRes.nextSeq ?? "0";
|
||||
}
|
||||
return groupMemberListRes?.nextSeq;
|
||||
}
|
||||
|
||||
Future<V2TimValueCallback<V2GroupMemberInfoSearchResult>> searchGroupMember(
|
||||
V2TimGroupMemberSearchParam searchParam) async {
|
||||
final res = await _groupServices.searchGroupMembers(searchParam: searchParam);
|
||||
|
||||
if (res.code == 0) {}
|
||||
return res;
|
||||
}
|
||||
|
||||
handleSearchGroupMembers(String searchText, context) async {
|
||||
loading = true;
|
||||
if (widget.groupID == null || widget.groupID!.isEmpty) {
|
||||
return;
|
||||
}
|
||||
List<V2TimGroupMemberFullInfo?> currentGroupMember = [];
|
||||
final res = await searchGroupMember(V2TimGroupMemberSearchParam(
|
||||
keywordList: [searchText],
|
||||
groupIDList: [widget.groupID!],
|
||||
));
|
||||
|
||||
if (res.code == 0) {
|
||||
List<V2TimGroupMemberFullInfo?> list = [];
|
||||
final searchResult = res.data!.groupMemberSearchResultItems!;
|
||||
searchResult.forEach((key, value) {
|
||||
if (value is List) {
|
||||
for (V2TimGroupMemberFullInfo item in value) {
|
||||
list.add(item);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
currentGroupMember = list;
|
||||
} else {
|
||||
currentGroupMember = [];
|
||||
}
|
||||
setState(() {
|
||||
loading = false;
|
||||
searchMemberList = isSearchTextExist(searchText) ? currentGroupMember : _groupMemberList;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final TUITheme theme = value.theme;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
shadowColor: theme.weakBackgroundColor,
|
||||
iconTheme: IconThemeData(
|
||||
color: theme.appbarTextColor,
|
||||
),
|
||||
backgroundColor: theme.appbarBgColor ?? theme.primaryColor,
|
||||
leading: TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: Text(
|
||||
TIM_t("取消"),
|
||||
style: TextStyle(
|
||||
color: theme.appbarTextColor,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
if (selectedMember.isNotEmpty) {
|
||||
Navigator.pop(context, selectedMember);
|
||||
}
|
||||
},
|
||||
child: Text(
|
||||
TIM_t("完成"),
|
||||
style: TextStyle(
|
||||
color: theme.appbarTextColor,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
centerTitle: true,
|
||||
leadingWidth: 80,
|
||||
title: Text(
|
||||
TIM_t("发起呼叫"),
|
||||
style: TextStyle(
|
||||
color: theme.appbarTextColor,
|
||||
fontSize: 17,
|
||||
),
|
||||
),
|
||||
),
|
||||
body: ((searchMemberList ?? []).isNotEmpty || loading == false)
|
||||
? GroupProfileMemberList(
|
||||
customTopArea: PlatformUtils().isWeb
|
||||
? null
|
||||
: GroupMemberSearchTextField(
|
||||
onTextChange: (text) => handleSearchGroupMembers(text, context),
|
||||
),
|
||||
memberList: (searchMemberList ?? [])
|
||||
.where((element) => element?.userID != _coreServicesImpl.loginInfo.userID)
|
||||
.toList(),
|
||||
canSlideDelete: false,
|
||||
canSelectMember: true,
|
||||
onSelectedMemberChange: (member) {
|
||||
selectedMember = member;
|
||||
setState(() {});
|
||||
},
|
||||
)
|
||||
: Center(
|
||||
child: LoadingAnimationWidget.staggeredDotsWave(
|
||||
color: theme.primaryColor ?? Colors.grey,
|
||||
size: 40,
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:tencent_chat_i18n_tool/tencent_chat_i18n_tool.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_statelesswidget.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/platform.dart';
|
||||
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/logger.dart';
|
||||
|
||||
import 'package:tencent_cloud_chat_uikit/theme/color.dart';
|
||||
|
||||
class EmojiPanel extends TIMUIKitStatelessWidget {
|
||||
final void Function(int unicode) onTapEmoji;
|
||||
final void Function() onSubmitted;
|
||||
final void Function() delete;
|
||||
final bool showBottomContainer;
|
||||
|
||||
EmojiPanel({
|
||||
Key? key,
|
||||
required this.onTapEmoji,
|
||||
required this.onSubmitted,
|
||||
required this.delete,
|
||||
this.showBottomContainer = true, // 可选参数,是否展示下方的底部导航栏
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
// ignore: avoid_print
|
||||
outputLogger.i(TIM_t(
|
||||
"暂未安装表情包插件,如需使用表情相关功能,请根据本文档安装:https://cloud.tencent.com/document/product/269/70746"));
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
height: showBottomContainer ? 190 : 248,
|
||||
// color: theme.weakBackgroundColor,
|
||||
padding: const EdgeInsets.fromLTRB(24, 16, 24, 16),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(TIM_t("暂无表情包")),
|
||||
],
|
||||
),
|
||||
),
|
||||
showBottomContainer
|
||||
? Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
SingleChildScrollView(
|
||||
child: Container(
|
||||
// color: Colors.white,
|
||||
margin: const EdgeInsets.only(right: 25),
|
||||
// height: MediaQuery.of(context).padding.bottom,
|
||||
child: ElevatedButton(
|
||||
child: Text(TIM_t("发送")),
|
||||
style: ElevatedButton.styleFrom(),
|
||||
onPressed: () {
|
||||
onSubmitted();
|
||||
})),
|
||||
),
|
||||
],
|
||||
)
|
||||
: Container()
|
||||
],
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
class EmojiItem extends TIMUIKitStatelessWidget {
|
||||
EmojiItem({Key? key, required this.name, required this.unicode})
|
||||
: super(key: key);
|
||||
final String name;
|
||||
final int unicode;
|
||||
|
||||
// final String toUser;
|
||||
// final int type;
|
||||
// final Function close;
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
return DefaultTextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: (PlatformUtils().isAndroid) ? 20 : 26,
|
||||
color: hexToColor("f9453d")
|
||||
),
|
||||
child: Text(
|
||||
String.fromCharCode(unicode),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,741 @@
|
||||
// ignore_for_file: unused_field, avoid_print, unused_import
|
||||
|
||||
import 'dart:io';
|
||||
import 'package:better_player_plus/better_player_plus.dart';
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:fc_native_video_thumbnail/fc_native_video_thumbnail.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:tencent_chat_i18n_tool/tencent_chat_i18n_tool.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_member_full_info.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_group_member_full_info.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitTextField/tim_uikit_call_invite_list.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_chat_separate_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_chat_global_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_self_info_view_model.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/services_locatar.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/message.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/permission.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/platform.dart';
|
||||
import 'package:wechat_assets_picker/wechat_assets_picker.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart';
|
||||
|
||||
// ignore: unnecessary_import
|
||||
import 'dart:typed_data';
|
||||
import 'package:universal_html/html.dart' as html;
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/logger.dart';
|
||||
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_callback.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/color.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/tui_theme.dart';
|
||||
|
||||
class MorePanelConfig {
|
||||
static final int FILE_MAX_SIZE = 100 * 1024 * 1024;
|
||||
static final int VIDEO_MAX_SIZE = 100 * 1024 * 1024;
|
||||
static final int IMAGE_MAX_SIZE = 28 * 1024 * 1024;
|
||||
|
||||
final bool showGalleryPickAction;
|
||||
final bool showCameraAction;
|
||||
final bool showFilePickAction;
|
||||
final bool showWebImagePickAction;
|
||||
final bool showWebVideoPickAction;
|
||||
final bool showVoiceCall;
|
||||
final bool showVideoCall;
|
||||
final List<MorePanelItem>? extraAction;
|
||||
final Widget Function(MorePanelItem item)? actionBuilder;
|
||||
|
||||
MorePanelConfig({
|
||||
this.showFilePickAction = true,
|
||||
this.showGalleryPickAction = true,
|
||||
this.showCameraAction = true,
|
||||
this.showWebImagePickAction = true,
|
||||
this.showWebVideoPickAction = true,
|
||||
this.showVoiceCall = true,
|
||||
this.showVideoCall = true,
|
||||
this.extraAction,
|
||||
this.actionBuilder,
|
||||
});
|
||||
}
|
||||
|
||||
class MorePanelItem {
|
||||
final String title;
|
||||
final String id;
|
||||
final Widget icon;
|
||||
final Function(BuildContext context)? onTap;
|
||||
|
||||
MorePanelItem({this.onTap, required this.icon, required this.id, required this.title});
|
||||
}
|
||||
|
||||
class MorePanel extends StatefulWidget {
|
||||
/// 会话ID
|
||||
final String conversationID;
|
||||
|
||||
/// 会话类型
|
||||
final ConvType conversationType;
|
||||
|
||||
final MorePanelConfig? morePanelConfig;
|
||||
|
||||
const MorePanel({required this.conversationID, required this.conversationType, Key? key, this.morePanelConfig})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _MorePanelState();
|
||||
}
|
||||
|
||||
class _MorePanelState extends TIMUIKitState<MorePanel> {
|
||||
final ImagePicker _picker = ImagePicker();
|
||||
final TUISelfInfoViewModel _selfInfoViewModel = serviceLocator<TUISelfInfoViewModel>();
|
||||
Uint8List? fileContent;
|
||||
String? fileName;
|
||||
File? tempFile;
|
||||
final _tUICore = TUICore();
|
||||
final _tUILogin = TUILogin();
|
||||
bool isInstallCallkit = false;
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
final DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
|
||||
|
||||
late BetterPlayerController _betterPlayerController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (PlatformUtils().isMobile) {
|
||||
_tUICore.getService(TUICALLKIT_SERVICE_NAME).then((value) {
|
||||
setState(() {
|
||||
isInstallCallkit = value;
|
||||
});
|
||||
});
|
||||
_betterPlayerController = BetterPlayerController(const BetterPlayerConfiguration());
|
||||
}
|
||||
}
|
||||
|
||||
List<MorePanelItem> itemList(TUIChatSeparateViewModel model, TUITheme theme) {
|
||||
final config = widget.morePanelConfig ?? MorePanelConfig();
|
||||
return [
|
||||
if (!PlatformUtils().isWeb)
|
||||
MorePanelItem(
|
||||
id: "photo",
|
||||
title: TIM_t("照片"),
|
||||
onTap: (c) {
|
||||
_onFeatureTap(
|
||||
"photo",
|
||||
c,
|
||||
model,
|
||||
theme,
|
||||
);
|
||||
},
|
||||
icon: Container(
|
||||
height: 64,
|
||||
width: 64,
|
||||
margin: const EdgeInsets.only(bottom: 4),
|
||||
decoration: const BoxDecoration(color: Colors.white, borderRadius: BorderRadius.all(Radius.circular(5))),
|
||||
child: SvgPicture.asset(
|
||||
"images/photo.svg",
|
||||
package: 'tencent_cloud_chat_uikit',
|
||||
height: 64,
|
||||
width: 64,
|
||||
),
|
||||
)),
|
||||
if (PlatformUtils().isMobile)
|
||||
MorePanelItem(
|
||||
id: "take_photo",
|
||||
title: TIM_t("拍照"),
|
||||
onTap: (c) {
|
||||
_onFeatureTap("take_photo", c, model, theme);
|
||||
},
|
||||
icon: Container(
|
||||
height: 64,
|
||||
width: 64,
|
||||
margin: const EdgeInsets.only(bottom: 4),
|
||||
decoration: const BoxDecoration(color: Colors.white, borderRadius: BorderRadius.all(Radius.circular(5))),
|
||||
child: SvgPicture.asset(
|
||||
"images/screen.svg",
|
||||
package: 'tencent_cloud_chat_uikit',
|
||||
height: 64,
|
||||
width: 64,
|
||||
),
|
||||
)),
|
||||
if (PlatformUtils().isMobile)
|
||||
MorePanelItem(
|
||||
id: "take_video",
|
||||
title: TIM_t("录像"),
|
||||
onTap: (c) {
|
||||
_onFeatureTap("take_video", c, model, theme);
|
||||
},
|
||||
icon: Container(
|
||||
height: 64,
|
||||
width: 64,
|
||||
margin: const EdgeInsets.only(bottom: 4),
|
||||
decoration: const BoxDecoration(color: Colors.white, borderRadius: BorderRadius.all(Radius.circular(5))),
|
||||
child: Image.asset(
|
||||
"images/take_video.png",
|
||||
package: 'tencent_cloud_chat_uikit',
|
||||
height: 64,
|
||||
width: 64,
|
||||
),
|
||||
)),
|
||||
if (PlatformUtils().isWeb)
|
||||
MorePanelItem(
|
||||
id: "image",
|
||||
title: TIM_t("图片"),
|
||||
onTap: (c) {
|
||||
_onFeatureTap(
|
||||
"image",
|
||||
c,
|
||||
model,
|
||||
theme,
|
||||
);
|
||||
},
|
||||
icon: Container(
|
||||
height: 64,
|
||||
width: 64,
|
||||
margin: const EdgeInsets.only(bottom: 4),
|
||||
decoration: const BoxDecoration(color: Colors.white, borderRadius: BorderRadius.all(Radius.circular(5))),
|
||||
child: SvgPicture.asset(
|
||||
"images/photo.svg",
|
||||
package: 'tencent_cloud_chat_uikit',
|
||||
height: 64,
|
||||
width: 64,
|
||||
),
|
||||
)),
|
||||
if (PlatformUtils().isWeb)
|
||||
MorePanelItem(
|
||||
id: "video",
|
||||
title: TIM_t("视频"),
|
||||
onTap: (c) {
|
||||
_onFeatureTap(
|
||||
"video",
|
||||
c,
|
||||
model,
|
||||
theme,
|
||||
);
|
||||
},
|
||||
icon: Container(
|
||||
height: 64,
|
||||
width: 64,
|
||||
margin: const EdgeInsets.only(bottom: 4),
|
||||
decoration: const BoxDecoration(color: Colors.white, borderRadius: BorderRadius.all(Radius.circular(5))),
|
||||
child: Icon(Icons.video_file, color: hexToColor("5c6168"), size: 26),
|
||||
)),
|
||||
MorePanelItem(
|
||||
id: "file",
|
||||
title: TIM_t("文件"),
|
||||
onTap: (c) {
|
||||
_onFeatureTap(
|
||||
"file",
|
||||
c,
|
||||
model,
|
||||
theme,
|
||||
);
|
||||
},
|
||||
icon: Container(
|
||||
height: 64,
|
||||
width: 64,
|
||||
margin: const EdgeInsets.only(bottom: 4),
|
||||
decoration: const BoxDecoration(color: Colors.white, borderRadius: BorderRadius.all(Radius.circular(5))),
|
||||
child: SvgPicture.asset(
|
||||
"images/file.svg",
|
||||
package: 'tencent_cloud_chat_uikit',
|
||||
height: 64,
|
||||
width: 64,
|
||||
),
|
||||
)),
|
||||
if (isInstallCallkit && PlatformUtils().isMobile)
|
||||
MorePanelItem(
|
||||
id: "videoCall",
|
||||
title: TIM_t("视频通话"),
|
||||
onTap: (c) {
|
||||
_onFeatureTap(
|
||||
"videoCall",
|
||||
c,
|
||||
model,
|
||||
theme,
|
||||
);
|
||||
},
|
||||
icon: Container(
|
||||
height: 64,
|
||||
width: 64,
|
||||
margin: const EdgeInsets.only(bottom: 4),
|
||||
decoration: const BoxDecoration(color: Colors.white, borderRadius: BorderRadius.all(Radius.circular(5))),
|
||||
child: SvgPicture.asset(
|
||||
"images/video-call.svg",
|
||||
package: 'tencent_cloud_chat_uikit',
|
||||
height: 64,
|
||||
width: 64,
|
||||
),
|
||||
)),
|
||||
if (isInstallCallkit && PlatformUtils().isMobile)
|
||||
MorePanelItem(
|
||||
id: "voiceCall",
|
||||
title: TIM_t("语音通话"),
|
||||
onTap: (c) {
|
||||
_onFeatureTap(
|
||||
"voiceCall",
|
||||
c,
|
||||
model,
|
||||
theme,
|
||||
);
|
||||
},
|
||||
icon: Container(
|
||||
height: 64,
|
||||
width: 64,
|
||||
margin: const EdgeInsets.only(bottom: 4),
|
||||
decoration: const BoxDecoration(color: Colors.white, borderRadius: BorderRadius.all(Radius.circular(5))),
|
||||
child: SvgPicture.asset(
|
||||
"images/voice-call.svg",
|
||||
package: 'tencent_cloud_chat_uikit',
|
||||
height: 64,
|
||||
width: 64,
|
||||
),
|
||||
)),
|
||||
if (config.extraAction != null) ...?config.extraAction,
|
||||
].where((element) {
|
||||
if (element.id == "screen") {
|
||||
return config.showCameraAction;
|
||||
}
|
||||
|
||||
if (element.id == "file") {
|
||||
return config.showFilePickAction;
|
||||
}
|
||||
|
||||
if (element.id == "photo") {
|
||||
return config.showGalleryPickAction;
|
||||
}
|
||||
|
||||
if (element.id == "image") {
|
||||
return config.showWebImagePickAction;
|
||||
}
|
||||
|
||||
if (element.id == "video") {
|
||||
return config.showWebVideoPickAction;
|
||||
}
|
||||
if (element.id == "voiceCall") {
|
||||
return config.showVoiceCall;
|
||||
}
|
||||
if (element.id == "videoCall") {
|
||||
return config.showVideoCall;
|
||||
}
|
||||
return true;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
_sendVideoMessage(String originFilePath, int duration, int size, TUIChatSeparateViewModel model) async {
|
||||
if (size >= MorePanelConfig.VIDEO_MAX_SIZE) {
|
||||
onTIMCallback(TIMCallback(type: TIMCallbackType.INFO, infoRecommendText: TIM_t("文件大小超出了限制")));
|
||||
return;
|
||||
}
|
||||
|
||||
final plugin = FcNativeVideoThumbnail();
|
||||
|
||||
final convID = widget.conversationID;
|
||||
final convType = widget.conversationType;
|
||||
|
||||
String tempPath = (await getTemporaryDirectory()).path + p.basename(originFilePath) + ".jpeg";
|
||||
|
||||
await plugin.getVideoThumbnail(
|
||||
srcFile: originFilePath,
|
||||
destFile: tempPath,
|
||||
format: 'jpeg',
|
||||
width: 1280,
|
||||
quality: 100,
|
||||
height: 1280,
|
||||
);
|
||||
MessageUtils.handleMessageError(
|
||||
model.sendVideoMessage(
|
||||
videoPath: originFilePath, duration: duration, snapshotPath: tempPath, convID: convID, convType: convType),
|
||||
context);
|
||||
}
|
||||
|
||||
_sendImageMessage(TUIChatSeparateViewModel model, TUITheme theme) async {
|
||||
try {
|
||||
if (PlatformUtils().isMobile) {
|
||||
if (PlatformUtils().isAndroid) {
|
||||
AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo;
|
||||
if ((androidInfo.version.sdkInt) >= 33) {
|
||||
final videos = await Permissions.checkPermission(
|
||||
context,
|
||||
Permission.videos.value,
|
||||
theme,
|
||||
);
|
||||
final photos = await Permissions.checkPermission(
|
||||
context,
|
||||
Permission.photos.value,
|
||||
theme,
|
||||
);
|
||||
if (!videos && !photos) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
final storage = await Permissions.checkPermission(
|
||||
context,
|
||||
Permission.storage.value,
|
||||
theme,
|
||||
);
|
||||
if (!storage) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
final photos = await Permissions.checkPermission(
|
||||
context,
|
||||
Permission.photos.value,
|
||||
theme,
|
||||
);
|
||||
if (!photos) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final convID = widget.conversationID;
|
||||
final convType = widget.conversationType;
|
||||
|
||||
if (PlatformUtils().isMobile) {
|
||||
final pickedAssets = await AssetPicker.pickAssets(context);
|
||||
|
||||
if (pickedAssets != null) {
|
||||
for (var asset in pickedAssets) {
|
||||
final originFile = await asset.originFile;
|
||||
final filePath = originFile?.path;
|
||||
final type = asset.type;
|
||||
final size = await originFile!.length();
|
||||
if (filePath != null) {
|
||||
if (type == AssetType.image) {
|
||||
if (size >= MorePanelConfig.IMAGE_MAX_SIZE) {
|
||||
onTIMCallback(TIMCallback(type: TIMCallbackType.INFO, infoRecommendText: TIM_t("文件大小超出了限制")));
|
||||
return;
|
||||
}
|
||||
|
||||
MessageUtils.handleMessageError(
|
||||
model.sendImageMessage(imagePath: filePath, convID: convID, convType: convType), context);
|
||||
}
|
||||
|
||||
if (type == AssetType.video) {
|
||||
_sendVideoMessage(originFile!.path, asset.videoDuration.inSeconds, size, model);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
FilePickerResult? result = await FilePicker.platform.pickFiles(type: FileType.media);
|
||||
if (result != null && result.files.isNotEmpty) {
|
||||
File file = File(result.files.single.path!);
|
||||
final String savePath = file.path;
|
||||
final String type =
|
||||
TencentUtils.getFileType(savePath.split(".")[savePath.split(".").length - 1]).split("/")[0];
|
||||
|
||||
if (type == "image") {
|
||||
MessageUtils.handleMessageError(
|
||||
model.sendImageMessage(imagePath: savePath, convID: convID, convType: convType), context);
|
||||
} else if (type == "video") {
|
||||
MessageUtils.handleMessageError(
|
||||
model.sendVideoMessage(videoPath: savePath, convID: convID, convType: convType), context);
|
||||
}
|
||||
} else {
|
||||
throw TypeError();
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
outputLogger.i("err: $err");
|
||||
}
|
||||
}
|
||||
|
||||
_sendImageFromCamera(TUIChatSeparateViewModel model, TUITheme theme, {required isVideo}) async {
|
||||
try {
|
||||
if (!await Permissions.checkPermission(
|
||||
context,
|
||||
Permission.camera.value,
|
||||
theme,
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
await Permissions.checkPermission(
|
||||
context,
|
||||
Permission.microphone.value,
|
||||
theme,
|
||||
);
|
||||
|
||||
final convID = widget.conversationID;
|
||||
final convType = widget.conversationType;
|
||||
final ImagePicker picker = ImagePicker();
|
||||
XFile? originFile;
|
||||
if (isVideo) {
|
||||
originFile = await picker.pickVideo(source: ImageSource.camera);
|
||||
} else {
|
||||
originFile = await picker.pickImage(source: ImageSource.camera);
|
||||
}
|
||||
final size = await originFile!.length();
|
||||
if (!isVideo) {
|
||||
if (size >= MorePanelConfig.IMAGE_MAX_SIZE) {
|
||||
onTIMCallback(TIMCallback(type: TIMCallbackType.INFO, infoRecommendText: TIM_t("文件大小超出了限制")));
|
||||
return;
|
||||
}
|
||||
|
||||
MessageUtils.handleMessageError(
|
||||
model.sendImageMessage(imagePath: originFile.path, convID: convID, convType: convType), context);
|
||||
} else {
|
||||
// 监听视频准备完成事件
|
||||
_betterPlayerController.addEventsListener((event) {
|
||||
if (event.betterPlayerEventType == BetterPlayerEventType.initialized) {
|
||||
// 获取视频时长(单位:秒)
|
||||
int durationInSeconds = _betterPlayerController.videoPlayerController?.value.duration?.inSeconds ?? 0;
|
||||
_sendVideoMessage(originFile!.path, durationInSeconds, size, model);
|
||||
}
|
||||
});
|
||||
|
||||
// 加载视频源
|
||||
_betterPlayerController.setupDataSource(
|
||||
BetterPlayerDataSource(
|
||||
BetterPlayerDataSourceType.file,
|
||||
originFile.path, // 替换为你的视频 URL
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
outputLogger.i("err: $error");
|
||||
}
|
||||
}
|
||||
|
||||
_sendImageFileOnWeb(TUIChatSeparateViewModel model) async {
|
||||
try {
|
||||
final pickedFile = await _picker.pickImage(source: ImageSource.gallery);
|
||||
final imageContent = await pickedFile!.readAsBytes();
|
||||
fileName = pickedFile.name;
|
||||
tempFile = File(pickedFile.path);
|
||||
fileContent = imageContent;
|
||||
|
||||
html.Node? inputElem;
|
||||
inputElem = html.document.getElementById("__image_picker_web-file-input")?.querySelector("input");
|
||||
final convID = widget.conversationID;
|
||||
final convType = widget.conversationType;
|
||||
MessageUtils.handleMessageError(
|
||||
model.sendImageMessage(
|
||||
inputElement: inputElem, imagePath: tempFile?.path, convID: convID, convType: convType),
|
||||
context);
|
||||
} catch (e) {
|
||||
outputLogger.i("_sendFileErr: ${e.toString()}");
|
||||
}
|
||||
}
|
||||
|
||||
_sendVideoFileOnWeb(TUIChatSeparateViewModel model) async {
|
||||
try {
|
||||
final pickedFile = await _picker.pickVideo(source: ImageSource.gallery);
|
||||
final videoContent = await pickedFile!.readAsBytes();
|
||||
fileName = pickedFile.name;
|
||||
tempFile = File(pickedFile.path);
|
||||
fileContent = videoContent;
|
||||
|
||||
if (fileName!.split(".")[fileName!.split(".").length - 1] != "mp4") {
|
||||
onTIMCallback(
|
||||
TIMCallback(type: TIMCallbackType.INFO, infoRecommendText: TIM_t("视频消息仅限 mp4 格式"), infoCode: 6660412));
|
||||
return;
|
||||
}
|
||||
|
||||
html.Node? inputElem;
|
||||
inputElem = html.document.getElementById("__image_picker_web-file-input")?.querySelector("input");
|
||||
final convID = widget.conversationID;
|
||||
final convType = widget.conversationType;
|
||||
MessageUtils.handleMessageError(
|
||||
model.sendVideoMessage(
|
||||
inputElement: inputElem, videoPath: tempFile?.path, convID: convID, convType: convType),
|
||||
context);
|
||||
} catch (e) {
|
||||
outputLogger.i("_sendFileErr: ${e.toString()}");
|
||||
}
|
||||
}
|
||||
|
||||
_sendFile(
|
||||
TUIChatSeparateViewModel model,
|
||||
TUITheme theme,
|
||||
) async {
|
||||
try {
|
||||
final convID = widget.conversationID;
|
||||
final convType = widget.conversationType;
|
||||
FilePickerResult? result = await FilePicker.platform.pickFiles();
|
||||
if (result != null && result.files.isNotEmpty) {
|
||||
if (PlatformUtils().isWeb) {
|
||||
html.Node? inputElem;
|
||||
inputElem = html.document.getElementById("__file_picker_web-file-input")?.querySelector("input");
|
||||
fileName = result.files.single.name;
|
||||
|
||||
MessageUtils.handleMessageError(
|
||||
model.sendFileMessage(inputElement: inputElem, fileName: fileName, convID: convID, convType: convType),
|
||||
context);
|
||||
return;
|
||||
}
|
||||
|
||||
String? option2 = result.files.single.path ?? "";
|
||||
outputLogger.i(TIM_t_para("选择成功{{option2}}", "选择成功$option2")(option2: option2));
|
||||
|
||||
File file = File(result.files.single.path!);
|
||||
final int size = file.lengthSync();
|
||||
if (size >= MorePanelConfig.FILE_MAX_SIZE) {
|
||||
onTIMCallback(TIMCallback(type: TIMCallbackType.INFO, infoRecommendText: TIM_t("文件大小超出了限制")));
|
||||
return;
|
||||
}
|
||||
|
||||
final String savePath = file.path;
|
||||
|
||||
MessageUtils.handleMessageError(
|
||||
model.sendFileMessage(filePath: savePath, size: size, convID: convID, convType: convType), context);
|
||||
} else {
|
||||
throw TypeError();
|
||||
}
|
||||
} catch (e) {
|
||||
outputLogger.i("_sendFileErr: ${e.toString()}");
|
||||
}
|
||||
}
|
||||
|
||||
_onFeatureTap(
|
||||
String id,
|
||||
BuildContext context,
|
||||
TUIChatSeparateViewModel model,
|
||||
TUITheme theme,
|
||||
) async {
|
||||
switch (id) {
|
||||
case "photo":
|
||||
_sendImageMessage(model, theme);
|
||||
break;
|
||||
case "take_photo":
|
||||
_sendImageFromCamera(model, theme, isVideo: false);
|
||||
break;
|
||||
case "take_video":
|
||||
_sendImageFromCamera(model, theme, isVideo: true);
|
||||
break;
|
||||
case "file":
|
||||
_sendFile(model, theme);
|
||||
break;
|
||||
case "image":
|
||||
// only for web
|
||||
_sendImageFileOnWeb(model);
|
||||
break;
|
||||
case "video":
|
||||
// only for web
|
||||
_sendVideoFileOnWeb(model);
|
||||
break;
|
||||
case "voiceCall":
|
||||
_goToVideoUI(TYPE_AUDIO);
|
||||
break;
|
||||
case "videoCall":
|
||||
_goToVideoUI(TYPE_VIDEO);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_goToVideoUI(String type) async {
|
||||
if (!PlatformUtils().isWeb) {
|
||||
bool hasCameraPermission = false;
|
||||
bool hasMicrophonePermission = false;
|
||||
if (type == TYPE_VIDEO) {
|
||||
hasCameraPermission = await Permissions.checkPermission(context, Permission.camera.value);
|
||||
hasMicrophonePermission = await Permissions.checkPermission(context, Permission.microphone.value);
|
||||
if (!hasCameraPermission || !hasMicrophonePermission) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
hasMicrophonePermission = await Permissions.checkPermission(context, Permission.microphone.value);
|
||||
if (!hasMicrophonePermission) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final isGroup = widget.conversationType == ConvType.group;
|
||||
if (isGroup) {
|
||||
List<V2TimGroupMemberFullInfo>? selectedMember = await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => SelectCallInviter(
|
||||
groupID: widget.conversationID,
|
||||
),
|
||||
),
|
||||
);
|
||||
if (selectedMember != null) {
|
||||
final inviteMember = selectedMember.map((e) => e.userID).toList();
|
||||
_tUICore.callService(TUICALLKIT_SERVICE_NAME, METHOD_NAME_CALL, {
|
||||
PARAM_NAME_TYPE: type,
|
||||
PARAM_NAME_USERIDS: inviteMember,
|
||||
PARAM_NAME_GROUPID: widget.conversationType == ConvType.group ? widget.conversationID : ""
|
||||
});
|
||||
}
|
||||
} else {
|
||||
_tUICore.callService(TUICALLKIT_SERVICE_NAME, METHOD_NAME_CALL, {
|
||||
PARAM_NAME_TYPE: type,
|
||||
PARAM_NAME_USERIDS: [widget.conversationID],
|
||||
PARAM_NAME_GROUPID: ""
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_betterPlayerController?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final TUITheme theme = value.theme;
|
||||
final TUIChatSeparateViewModel model = Provider.of<TUIChatSeparateViewModel>(context);
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
return Container(
|
||||
height: 248,
|
||||
decoration: BoxDecoration(
|
||||
// color: hexToColor("EBF0F6"),
|
||||
border: Border(
|
||||
top: BorderSide(width: 1, color: Colors.grey.shade300),
|
||||
),
|
||||
),
|
||||
padding: const EdgeInsets.only(top: 20, left: 20, right: 20),
|
||||
width: screenWidth,
|
||||
child: Scrollbar(
|
||||
controller: _scrollController,
|
||||
child: SingleChildScrollView(
|
||||
controller: _scrollController,
|
||||
child: Wrap(
|
||||
spacing: (screenWidth - (23 * 2) - 64 * 4) / 3,
|
||||
runSpacing: 20,
|
||||
children: itemList(model, theme)
|
||||
.map((item) => InkWell(
|
||||
onTap: () {
|
||||
if (item.onTap != null) {
|
||||
item.onTap!(context);
|
||||
}
|
||||
},
|
||||
child: widget.morePanelConfig?.actionBuilder != null
|
||||
? widget.morePanelConfig?.actionBuilder!(item)
|
||||
: SizedBox(
|
||||
height: 94,
|
||||
width: 64,
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
height: 64,
|
||||
width: 64,
|
||||
margin: const EdgeInsets.only(bottom: 4),
|
||||
decoration: const BoxDecoration(borderRadius: BorderRadius.all(Radius.circular(5))),
|
||||
child: item.icon,
|
||||
),
|
||||
Text(
|
||||
item.title,
|
||||
style: TextStyle(fontSize: 12, color: theme.darkTextColor),
|
||||
)
|
||||
],
|
||||
),
|
||||
)))
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,322 @@
|
||||
// ignore_for_file: avoid_print, unused_import
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:tencent_chat_i18n_tool/tencent_chat_i18n_tool.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_chat_separate_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_chat_global_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/services_locatar.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/message.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/permission.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/sound_record.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/logger.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_callback.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/tui_theme.dart';
|
||||
|
||||
class SendSoundMessage extends StatefulWidget {
|
||||
/// conversation ID
|
||||
final String conversationID;
|
||||
|
||||
/// control the list to bottom
|
||||
final VoidCallback onDownBottom;
|
||||
|
||||
/// the conversation type
|
||||
final ConvType conversationType;
|
||||
|
||||
const SendSoundMessage(
|
||||
{required this.conversationID,
|
||||
required this.conversationType,
|
||||
Key? key,
|
||||
required this.onDownBottom})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _SendSoundMessageState();
|
||||
}
|
||||
|
||||
class _SendSoundMessageState extends TIMUIKitState<SendSoundMessage> {
|
||||
final TUIChatGlobalModel model = serviceLocator<TUIChatGlobalModel>();
|
||||
String soundTipsText = "";
|
||||
bool isRecording = false;
|
||||
bool isInit = false;
|
||||
bool isCancelSend = false;
|
||||
DateTime startTime = DateTime.now();
|
||||
List<StreamSubscription<Object>> subscriptions = [];
|
||||
|
||||
OverlayEntry? overlayEntry;
|
||||
String voiceIcon = "images/voice_volume_1.png";
|
||||
double volume = 0.1;
|
||||
|
||||
buildOverLayView(BuildContext context) {
|
||||
if (overlayEntry == null) {
|
||||
overlayEntry = OverlayEntry(builder: (content) {
|
||||
return Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: MediaQuery.of(context).size.width,
|
||||
height: MediaQuery.of(context).size.height,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
type: MaterialType.canvas,
|
||||
child: Center(
|
||||
child: Opacity(
|
||||
opacity: 0.8,
|
||||
child: Container(
|
||||
width: 160,
|
||||
height: 160,
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xff77797A),
|
||||
borderRadius: BorderRadius.all(Radius.circular(20.0)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
const SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
margin: const EdgeInsets.only(top: 10),
|
||||
child: Image.asset(
|
||||
"images/microphone.png",
|
||||
width: 50,
|
||||
height: 60,
|
||||
package: 'flutter_plugin_record_plus',
|
||||
),
|
||||
),
|
||||
ClipRect(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
child: Align(
|
||||
heightFactor: max(min(volume, 1), 0.1),
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: SizedBox(
|
||||
width: 50,
|
||||
height: 60,
|
||||
child: Image.asset(
|
||||
"images/voice_volume_total.png",
|
||||
width: 50,
|
||||
height: 60,
|
||||
package: 'flutter_plugin_record_plus',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
Text(
|
||||
soundTipsText,
|
||||
style: const TextStyle(
|
||||
fontStyle: FontStyle.normal,
|
||||
color: Colors.white,
|
||||
fontSize: 14,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
Overlay.of(context).insert(overlayEntry!);
|
||||
}
|
||||
}
|
||||
|
||||
onLongPressStart(_) {
|
||||
if (isInit) {
|
||||
setState(() {
|
||||
soundTipsText = TIM_t("手指上滑,取消发送");
|
||||
});
|
||||
startTime = DateTime.now();
|
||||
SoundPlayer.startRecord();
|
||||
buildOverLayView(context);
|
||||
}
|
||||
}
|
||||
|
||||
onLongPressUpdate(e) {
|
||||
double height = MediaQuery.of(context).size.height * 0.5 - 240;
|
||||
double dy = e.localPosition.dy;
|
||||
|
||||
if (dy.abs() > height) {
|
||||
if (mounted && soundTipsText != TIM_t("松开取消")) {
|
||||
setState(() {
|
||||
soundTipsText = TIM_t("松开取消");
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (mounted && soundTipsText == TIM_t("松开取消")) {
|
||||
setState(() {
|
||||
soundTipsText = TIM_t("手指上滑,取消发送");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onLongPressEnd(e) {
|
||||
double dy = e.localPosition.dy;
|
||||
// 此高度为 160为录音取消组件距离顶部的预留距离
|
||||
double height = MediaQuery.of(context).size.height * 0.5 - 240;
|
||||
if (dy.abs() > height) {
|
||||
isCancelSend = true;
|
||||
} else {
|
||||
isCancelSend = false;
|
||||
}
|
||||
if (overlayEntry != null) {
|
||||
overlayEntry!.remove();
|
||||
overlayEntry = null;
|
||||
}
|
||||
// Did not receive onStop from FlutterPluginRecord if the duration is too short.
|
||||
if (DateTime.now().difference(startTime).inSeconds < 1) {
|
||||
isCancelSend = true;
|
||||
onTIMCallback(TIMCallback(
|
||||
type: TIMCallbackType.INFO,
|
||||
infoRecommendText: TIM_t("说话时间太短"),
|
||||
infoCode: 6660404));
|
||||
}
|
||||
stop();
|
||||
}
|
||||
|
||||
onLonePressCancel() {
|
||||
if (isRecording) {
|
||||
isCancelSend = true;
|
||||
if (overlayEntry != null) {
|
||||
overlayEntry!.remove();
|
||||
overlayEntry = null;
|
||||
}
|
||||
stop();
|
||||
}
|
||||
}
|
||||
|
||||
void stop() {
|
||||
setState(() {
|
||||
isRecording = false;
|
||||
});
|
||||
SoundPlayer.stopRecord();
|
||||
setState(() {
|
||||
soundTipsText = TIM_t("手指上滑,取消发送");
|
||||
});
|
||||
}
|
||||
|
||||
sendSound(
|
||||
{required String path,
|
||||
required int duration,
|
||||
required TUIChatSeparateViewModel model}) {
|
||||
final convID = widget.conversationID;
|
||||
final convType = widget.conversationType;
|
||||
|
||||
if (duration > 0) {
|
||||
if (!isCancelSend) {
|
||||
MessageUtils.handleMessageError(
|
||||
model.sendSoundMessage(
|
||||
soundPath: path,
|
||||
duration: duration,
|
||||
convID: convID,
|
||||
convType: convType),
|
||||
context);
|
||||
widget.onDownBottom();
|
||||
} else {
|
||||
isCancelSend = false;
|
||||
}
|
||||
} else {
|
||||
onTIMCallback(TIMCallback(
|
||||
type: TIMCallbackType.INFO,
|
||||
infoRecommendText: TIM_t("说话时间太短"),
|
||||
infoCode: 6660404));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
dispose() {
|
||||
for (var subscription in subscriptions) {
|
||||
subscription.cancel();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
initRecordSound(TUIChatSeparateViewModel model) {
|
||||
final responseSubscription = SoundPlayer.responseListener((recordResponse) {
|
||||
final status = recordResponse.msg;
|
||||
if (status == "onStop") {
|
||||
if (!isCancelSend) {
|
||||
final soundPath = recordResponse.path;
|
||||
final recordDuration = recordResponse.audioTimeLength;
|
||||
sendSound(
|
||||
path: soundPath!, duration: recordDuration!.ceil(), model: model);
|
||||
}
|
||||
} else if (status == "onStart") {
|
||||
outputLogger.i("start record");
|
||||
setState(() {
|
||||
isRecording = true;
|
||||
});
|
||||
} else {
|
||||
outputLogger.i(status.toString());
|
||||
}
|
||||
});
|
||||
final amplitudesResponseSubscription =
|
||||
SoundPlayer.responseFromAmplitudeListener((recordResponse) {
|
||||
setState(() {
|
||||
volume = double.parse(recordResponse.msg!) * 1.1;
|
||||
if (overlayEntry != null) {
|
||||
overlayEntry!.markNeedsBuild();
|
||||
}
|
||||
});
|
||||
});
|
||||
subscriptions = [responseSubscription, amplitudesResponseSubscription];
|
||||
SoundPlayer.initSoundPlayer();
|
||||
isInit = true;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final TUITheme theme = value.theme;
|
||||
final TUIChatSeparateViewModel model =
|
||||
Provider.of<TUIChatSeparateViewModel>(context);
|
||||
return GestureDetector(
|
||||
onTapDown: (detail) async {
|
||||
if (!isInit) {
|
||||
bool hasMicrophonePermission = await Permissions.checkPermission(
|
||||
context,
|
||||
Permission.microphone.value,
|
||||
theme,
|
||||
);
|
||||
if (!hasMicrophonePermission) {
|
||||
return;
|
||||
}
|
||||
initRecordSound(model);
|
||||
}
|
||||
},
|
||||
onLongPressStart: onLongPressStart,
|
||||
onLongPressMoveUpdate: onLongPressUpdate,
|
||||
onLongPressEnd: onLongPressEnd,
|
||||
onLongPressCancel: onLonePressCancel,
|
||||
child: Container(
|
||||
height: 35,
|
||||
color: isRecording ? theme.weakBackgroundColor : Colors.white,
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
TIM_t("按住说话"),
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
color: theme.darkTextColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,50 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_member_full_info.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_group_member_full_info.dart';
|
||||
|
||||
enum ActionType { hideAllPanel, longPressToAt, setTextField, requestFocus, handleAtMember }
|
||||
|
||||
class TIMUIKitInputTextFieldController extends ChangeNotifier {
|
||||
TextEditingController? textEditingController = TextEditingController();
|
||||
ActionType? actionType;
|
||||
String? atUserName;
|
||||
String? atUserID;
|
||||
String inputText = "";
|
||||
V2TimGroupMemberFullInfo? groupMemberFullInfo;
|
||||
|
||||
TIMUIKitInputTextFieldController([TextEditingController? controller]) {
|
||||
if (controller != null) {
|
||||
textEditingController = controller;
|
||||
}
|
||||
}
|
||||
|
||||
/// text field unfocused and hide all panel
|
||||
hideAllPanel() {
|
||||
actionType = ActionType.hideAllPanel;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
longPressToAt(String? userName, String? userID) {
|
||||
actionType = ActionType.longPressToAt;
|
||||
atUserName = userName;
|
||||
atUserID = userID;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
setTextField(String text) {
|
||||
inputText = text;
|
||||
actionType = ActionType.setTextField;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
requestFocus() {
|
||||
actionType = ActionType.requestFocus;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
handleAtMember(V2TimGroupMemberFullInfo? memberInfo) {
|
||||
actionType = ActionType.handleAtMember;
|
||||
groupMemberFullInfo = memberInfo;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,632 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:extended_text_field/extended_text_field.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:tencent_chat_i18n_tool/tencent_chat_i18n_tool.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_message.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_chat_separate_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_chat_global_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_setting_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/services_locatar.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/color.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/tui_theme.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/message.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/optimize_utils.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/permission.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/platform.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitTextField/special_text/DefaultSpecialTextSpanBuilder.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitTextField/special_text/emoji_text.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitTextField/tim_uikit_send_sound_message.dart';
|
||||
import 'package:tencent_keyboard_visibility/tencent_keyboard_visibility.dart';
|
||||
|
||||
GlobalKey<_TIMUIKitTextFieldLayoutNarrowState> narrowTextFieldKey = GlobalKey();
|
||||
|
||||
class TIMUIKitTextFieldLayoutNarrow extends StatefulWidget {
|
||||
/// sticker panel customization
|
||||
final CustomStickerPanel? customStickerPanel;
|
||||
|
||||
final VoidCallback onEmojiSubmitted;
|
||||
final Function(int, String) onCustomEmojiFaceSubmitted;
|
||||
final Function(String, bool) handleSendEditStatus;
|
||||
final VoidCallback backSpaceText;
|
||||
final ValueChanged<String> addStickerToText;
|
||||
|
||||
final ValueChanged<String> handleAtText;
|
||||
|
||||
/// Whether to use the default emoji
|
||||
final bool isUseDefaultEmoji;
|
||||
|
||||
final bool isUseTencentCloudChatPackageOldKeys;
|
||||
|
||||
final TUIChatSeparateViewModel model;
|
||||
|
||||
/// background color
|
||||
final Color? backgroundColor;
|
||||
|
||||
/// control input field behavior
|
||||
final TIMUIKitInputTextFieldController? controller;
|
||||
|
||||
/// config for more panel
|
||||
final MorePanelConfig? morePanelConfig;
|
||||
|
||||
final String languageType;
|
||||
|
||||
final TextEditingController textEditingController;
|
||||
|
||||
/// conversation id
|
||||
final String conversationID;
|
||||
|
||||
/// conversation type
|
||||
final ConvType conversationType;
|
||||
|
||||
final FocusNode focusNode;
|
||||
|
||||
/// show more panel
|
||||
final bool showMorePanel;
|
||||
|
||||
/// hint text for textField widget
|
||||
final String? hintText;
|
||||
|
||||
final int? currentCursor;
|
||||
|
||||
final ValueChanged<int?> setCurrentCursor;
|
||||
|
||||
final VoidCallback onCursorChange;
|
||||
|
||||
/// show send audio icon
|
||||
final bool showSendAudio;
|
||||
|
||||
final VoidCallback handleSoftKeyBoardDelete;
|
||||
|
||||
/// on text changed
|
||||
final void Function(String)? onChanged;
|
||||
|
||||
final V2TimMessage? repliedMessage;
|
||||
|
||||
final void Function(String)? onDeleteText;
|
||||
|
||||
/// show send emoji icon
|
||||
final bool showSendEmoji;
|
||||
|
||||
final VoidCallback onSubmitted;
|
||||
|
||||
final VoidCallback goDownBottom;
|
||||
|
||||
final List<CustomEmojiFaceData> customEmojiStickerList;
|
||||
|
||||
final List<CustomStickerPackage> stickerPackageList;
|
||||
|
||||
const TIMUIKitTextFieldLayoutNarrow(
|
||||
{Key? key,
|
||||
this.customStickerPanel,
|
||||
required this.onEmojiSubmitted,
|
||||
required this.onCustomEmojiFaceSubmitted,
|
||||
required this.backSpaceText,
|
||||
required this.addStickerToText,
|
||||
required this.isUseDefaultEmoji,
|
||||
this.isUseTencentCloudChatPackageOldKeys = false,
|
||||
required this.languageType,
|
||||
required this.textEditingController,
|
||||
this.morePanelConfig,
|
||||
required this.conversationID,
|
||||
required this.conversationType,
|
||||
required this.focusNode,
|
||||
this.currentCursor,
|
||||
required this.setCurrentCursor,
|
||||
required this.onCursorChange,
|
||||
required this.model,
|
||||
this.backgroundColor,
|
||||
this.onChanged,
|
||||
this.onDeleteText,
|
||||
required this.handleSendEditStatus,
|
||||
required this.handleAtText,
|
||||
required this.handleSoftKeyBoardDelete,
|
||||
this.repliedMessage,
|
||||
required this.onSubmitted,
|
||||
required this.goDownBottom,
|
||||
required this.showSendAudio,
|
||||
required this.showSendEmoji,
|
||||
required this.showMorePanel,
|
||||
this.hintText,
|
||||
required this.customEmojiStickerList,
|
||||
this.controller,
|
||||
required this.stickerPackageList})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<TIMUIKitTextFieldLayoutNarrow> createState() => _TIMUIKitTextFieldLayoutNarrowState();
|
||||
}
|
||||
|
||||
class _TIMUIKitTextFieldLayoutNarrowState extends TIMUIKitState<TIMUIKitTextFieldLayoutNarrow> {
|
||||
final TUISettingModel settingModel = serviceLocator<TUISettingModel>();
|
||||
|
||||
bool showMore = false;
|
||||
bool showMoreButton = true;
|
||||
bool showSendSoundText = false;
|
||||
bool showEmojiPanel = false;
|
||||
bool showKeyboard = false;
|
||||
Function? setKeyboardHeight;
|
||||
double? bottomPadding;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.controller != null) {
|
||||
widget.controller?.addListener(
|
||||
() {
|
||||
final actionType = widget.controller?.actionType;
|
||||
if (actionType == ActionType.hideAllPanel) {
|
||||
hideAllPanel();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void setSendButton() {
|
||||
final value = widget.textEditingController.text;
|
||||
if (isWebDevice() || isAndroidDevice()) {
|
||||
if (value.isEmpty && showMoreButton != true) {
|
||||
setState(() {
|
||||
showMoreButton = true;
|
||||
});
|
||||
} else if (value.isNotEmpty && showMoreButton == true) {
|
||||
setState(() {
|
||||
showMoreButton = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hideAllPanel() {
|
||||
widget.focusNode.unfocus();
|
||||
widget.currentCursor == null;
|
||||
if (showKeyboard != false || showMore != false || showEmojiPanel != false) {
|
||||
setState(() {
|
||||
showKeyboard = false;
|
||||
showMore = false;
|
||||
showEmojiPanel = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Widget _getBottomContainer(TUITheme theme) {
|
||||
if (showEmojiPanel) {
|
||||
return widget.customStickerPanel != null
|
||||
? widget.customStickerPanel!(
|
||||
sendTextMessage: () {
|
||||
widget.onEmojiSubmitted();
|
||||
setSendButton();
|
||||
},
|
||||
sendFaceMessage: widget.onCustomEmojiFaceSubmitted,
|
||||
deleteText: () {
|
||||
widget.backSpaceText();
|
||||
setSendButton();
|
||||
},
|
||||
addText: (int unicode) {
|
||||
final newText = String.fromCharCode(unicode);
|
||||
widget.addStickerToText(newText);
|
||||
setSendButton();
|
||||
// handleSetDraftText();
|
||||
},
|
||||
addCustomEmojiText: ((String singleEmojiName) {
|
||||
String? emojiName = singleEmojiName.split('.png')[0];
|
||||
String compatibleEmojiName = emojiName;
|
||||
if (widget.isUseTencentCloudChatPackageOldKeys) {
|
||||
compatibleEmojiName = EmojiUtil.getCompatibleEmojiName(emojiName);
|
||||
}
|
||||
|
||||
String newText = '[$compatibleEmojiName]';
|
||||
widget.addStickerToText(newText);
|
||||
setSendButton();
|
||||
}),
|
||||
defaultCustomEmojiStickerList: widget.isUseDefaultEmoji ? TUIKitStickerConstData.emojiList : [])
|
||||
: StickerPanel(
|
||||
isWideScreen: false,
|
||||
sendTextMsg: () {
|
||||
widget.onEmojiSubmitted();
|
||||
setSendButton();
|
||||
},
|
||||
sendFaceMsg: widget.onCustomEmojiFaceSubmitted,
|
||||
deleteText: () {
|
||||
widget.backSpaceText();
|
||||
setSendButton();
|
||||
},
|
||||
addText: (int unicode) {
|
||||
final newText = String.fromCharCode(unicode);
|
||||
widget.addStickerToText(newText);
|
||||
setSendButton();
|
||||
// handleSetDraftText();
|
||||
},
|
||||
addCustomEmojiText: ((String singleEmojiName) {
|
||||
String? emojiName = singleEmojiName.split('.png')[0];
|
||||
String compatibleEmojiName = emojiName;
|
||||
if (widget.isUseTencentCloudChatPackageOldKeys) {
|
||||
compatibleEmojiName = EmojiUtil.getCompatibleEmojiName(emojiName);
|
||||
}
|
||||
|
||||
String newText = '[$compatibleEmojiName]';
|
||||
widget.addStickerToText(newText);
|
||||
setSendButton();
|
||||
}),
|
||||
customStickerPackageList: widget.stickerPackageList,
|
||||
lightPrimaryColor: theme.lightPrimaryColor);
|
||||
}
|
||||
|
||||
if (showMore) {
|
||||
return MorePanel(
|
||||
morePanelConfig: widget.morePanelConfig,
|
||||
conversationID: widget.conversationID,
|
||||
conversationType: widget.conversationType);
|
||||
}
|
||||
|
||||
return const SizedBox(height: 0);
|
||||
}
|
||||
|
||||
double _getBottomHeight() {
|
||||
if (showKeyboard) {
|
||||
final currentKeyboardHeight = MediaQuery.of(context).viewInsets.bottom;
|
||||
double originHeight = settingModel.keyboardHeight;
|
||||
if (currentKeyboardHeight != 0) {
|
||||
if (currentKeyboardHeight >= originHeight) {
|
||||
originHeight = currentKeyboardHeight;
|
||||
}
|
||||
if (setKeyboardHeight != null) {
|
||||
setKeyboardHeight!(currentKeyboardHeight);
|
||||
}
|
||||
}
|
||||
final height = originHeight != 0 ? originHeight : currentKeyboardHeight;
|
||||
return height;
|
||||
} else if (showMore || showEmojiPanel) {
|
||||
return 248.0 + (bottomPadding ?? 0.0);
|
||||
} else if (widget.textEditingController.text.length >= 46 && showKeyboard == false) {
|
||||
return 25 + (bottomPadding ?? 0.0);
|
||||
} else {
|
||||
return bottomPadding ?? 0;
|
||||
}
|
||||
}
|
||||
|
||||
_openMore() {
|
||||
if (!showMore) {
|
||||
widget.focusNode.unfocus();
|
||||
widget.setCurrentCursor(null);
|
||||
}
|
||||
setState(() {
|
||||
showKeyboard = false;
|
||||
showEmojiPanel = false;
|
||||
showSendSoundText = false;
|
||||
showMore = !showMore;
|
||||
});
|
||||
}
|
||||
|
||||
_openEmojiPanel() {
|
||||
widget.onCursorChange();
|
||||
showKeyboard = showEmojiPanel;
|
||||
if (showEmojiPanel) {
|
||||
widget.focusNode.requestFocus();
|
||||
} else {
|
||||
widget.focusNode.unfocus();
|
||||
}
|
||||
|
||||
setState(() {
|
||||
showMore = false;
|
||||
showSendSoundText = false;
|
||||
showEmojiPanel = !showEmojiPanel;
|
||||
});
|
||||
}
|
||||
|
||||
_debounce(
|
||||
Function(String text) fun, [
|
||||
Duration delay = const Duration(milliseconds: 30),
|
||||
]) {
|
||||
Timer? timer;
|
||||
return (String text) {
|
||||
if (timer != null) {
|
||||
timer?.cancel();
|
||||
}
|
||||
|
||||
timer = Timer(delay, () {
|
||||
fun(text);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
String getAbstractMessage(V2TimMessage message) {
|
||||
final String? customAbstractMessage =
|
||||
widget.model.abstractMessageBuilder != null ? widget.model.abstractMessageBuilder!(message) : null;
|
||||
return customAbstractMessage ?? MessageUtils.getAbstractMessageAsync(message, widget.model.groupMemberList ?? []);
|
||||
}
|
||||
|
||||
_buildRepliedMessage(V2TimMessage? repliedMessage) {
|
||||
final haveRepliedMessage = repliedMessage != null;
|
||||
if (haveRepliedMessage) {
|
||||
final String text = "${MessageUtils.getDisplayName(repliedMessage)}:${getAbstractMessage(repliedMessage)}";
|
||||
return Container(
|
||||
color: widget.backgroundColor ?? hexToColor("f5f5f6"),
|
||||
alignment: Alignment.centerLeft,
|
||||
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 16),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
text,
|
||||
softWrap: true,
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(color: hexToColor("8f959e"), fontSize: 14),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 16,
|
||||
),
|
||||
InkWell(
|
||||
onTap: () {
|
||||
widget.model.repliedMessage = null;
|
||||
},
|
||||
child: Icon(Icons.clear, color: hexToColor("8f959e"), size: 18),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
return Container();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final theme = value.theme;
|
||||
|
||||
setKeyboardHeight ??= OptimizeUtils.debounce((height) {
|
||||
settingModel.keyboardHeight = height;
|
||||
}, const Duration(seconds: 1));
|
||||
|
||||
final debounceFunc = _debounce((value) {
|
||||
if (isWebDevice() || isAndroidDevice()) {
|
||||
if (value.isEmpty && showMoreButton != true) {
|
||||
setState(() {
|
||||
showMoreButton = true;
|
||||
});
|
||||
} else if (value.isNotEmpty && showMoreButton == true) {
|
||||
setState(() {
|
||||
showMoreButton = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
if (widget.onChanged != null) {
|
||||
widget.onChanged!(value);
|
||||
}
|
||||
widget.handleAtText(value);
|
||||
widget.handleSendEditStatus(value, true);
|
||||
final isEmpty = value.isEmpty;
|
||||
if (isEmpty) {
|
||||
widget.handleSoftKeyBoardDelete();
|
||||
}
|
||||
}, const Duration(milliseconds: 80));
|
||||
|
||||
final MediaQueryData data = MediaQuery.of(context);
|
||||
EdgeInsets padding = data.padding;
|
||||
if (bottomPadding == null || padding.bottom > bottomPadding!) {
|
||||
bottomPadding = padding.bottom;
|
||||
}
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {},
|
||||
child: Column(
|
||||
children: [
|
||||
_buildRepliedMessage(widget.repliedMessage),
|
||||
Container(
|
||||
color: widget.backgroundColor ?? hexToColor("f5f5f6"),
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
|
||||
constraints: const BoxConstraints(minHeight: 50),
|
||||
child: Row(
|
||||
children: [
|
||||
if (PlatformUtils().isMobile && widget.showSendAudio)
|
||||
InkWell(
|
||||
onTap: () async {
|
||||
showKeyboard = showSendSoundText;
|
||||
if (showSendSoundText) {
|
||||
widget.focusNode.requestFocus();
|
||||
}
|
||||
if (await Permissions.checkPermission(
|
||||
context,
|
||||
Permission.microphone.value,
|
||||
theme,
|
||||
)) {
|
||||
setState(() {
|
||||
showEmojiPanel = false;
|
||||
showMore = false;
|
||||
showSendSoundText = !showSendSoundText;
|
||||
});
|
||||
}
|
||||
},
|
||||
child: SvgPicture.asset(
|
||||
showSendSoundText ? 'images/keyboard.svg' : 'images/voice.svg',
|
||||
package: 'tencent_cloud_chat_uikit',
|
||||
color: const Color.fromRGBO(68, 68, 68, 1),
|
||||
height: 28,
|
||||
width: 28,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 10,
|
||||
),
|
||||
Expanded(
|
||||
child: showSendSoundText
|
||||
? SendSoundMessage(
|
||||
onDownBottom: widget.goDownBottom,
|
||||
conversationID: widget.conversationID,
|
||||
conversationType: widget.conversationType)
|
||||
: Stack(children: [
|
||||
Center(
|
||||
child: KeyboardVisibility(
|
||||
child: ExtendedTextField(
|
||||
maxLines: 4,
|
||||
minLines: 1,
|
||||
focusNode: widget.focusNode,
|
||||
onChanged: debounceFunc,
|
||||
onTap: () {
|
||||
showKeyboard = true;
|
||||
widget.goDownBottom();
|
||||
setState(() {
|
||||
showEmojiPanel = false;
|
||||
showMore = false;
|
||||
});
|
||||
},
|
||||
keyboardType: TextInputType.multiline,
|
||||
textInputAction: PlatformUtils().isAndroid
|
||||
? TextInputAction.newline
|
||||
: TextInputAction.send,
|
||||
onEditingComplete: () {
|
||||
widget.onSubmitted();
|
||||
if (showKeyboard) {
|
||||
widget.focusNode.requestFocus();
|
||||
}
|
||||
setState(() {
|
||||
if (widget.textEditingController.text.isEmpty) {
|
||||
showMoreButton = true;
|
||||
}
|
||||
});
|
||||
},
|
||||
textAlignVertical: TextAlignVertical.top,
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
hintStyle: const TextStyle(
|
||||
// fontSize: 10,
|
||||
color: Color(0xffAEA4A3),
|
||||
),
|
||||
fillColor: Colors.white,
|
||||
filled: true,
|
||||
isDense: true,
|
||||
hintText: widget.hintText ?? ''),
|
||||
controller: widget.textEditingController,
|
||||
specialTextSpanBuilder: PlatformUtils().isWeb
|
||||
? null
|
||||
: DefaultSpecialTextSpanBuilder(
|
||||
isUseQQPackage:
|
||||
widget.model.chatConfig.stickerPanelConfig?.useQQStickerPackage ??
|
||||
true,
|
||||
isUseTencentCloudChatPackage: widget.model.chatConfig
|
||||
.stickerPanelConfig?.useTencentCloudChatStickerPackage ??
|
||||
true,
|
||||
isUseTencentCloudChatPackageOldKeys: widget
|
||||
.model
|
||||
.chatConfig
|
||||
.stickerPanelConfig
|
||||
?.useTencentCloudChatStickerPackageOldKeys ??
|
||||
false,
|
||||
customEmojiStickerList: widget.customEmojiStickerList,
|
||||
showAtBackground: true,
|
||||
checkHttpLink: false,
|
||||
)),
|
||||
onChanged: (bool visibility) {
|
||||
if (showKeyboard != visibility) {
|
||||
setState(() {
|
||||
showKeyboard = visibility;
|
||||
});
|
||||
}
|
||||
}),
|
||||
),
|
||||
RawKeyboardListener(
|
||||
autofocus: true,
|
||||
focusNode: FocusNode(),
|
||||
onKey: (key) {
|
||||
if (key is RawKeyDownEvent && key.logicalKey == LogicalKeyboardKey.backspace) {
|
||||
if (widget.onDeleteText != null) {
|
||||
widget.onDeleteText!(widget.textEditingController.text);
|
||||
}
|
||||
}
|
||||
},
|
||||
child: Container(),
|
||||
),
|
||||
]),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 10,
|
||||
),
|
||||
if (widget.showSendEmoji)
|
||||
InkWell(
|
||||
onTap: () {
|
||||
_openEmojiPanel();
|
||||
widget.goDownBottom();
|
||||
},
|
||||
child: PlatformUtils().isWeb
|
||||
? Icon(showEmojiPanel ? Icons.keyboard_alt_outlined : Icons.mood_outlined,
|
||||
color: hexToColor("5c6168"), size: 32)
|
||||
: SvgPicture.asset(
|
||||
showEmojiPanel ? 'images/keyboard.svg' : 'images/face.svg',
|
||||
package: 'tencent_cloud_chat_uikit',
|
||||
color: const Color.fromRGBO(68, 68, 68, 1),
|
||||
height: 28,
|
||||
width: 28,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 10,
|
||||
),
|
||||
if (widget.showMorePanel && showMoreButton)
|
||||
InkWell(
|
||||
onTap: () {
|
||||
// model.sendCustomMessage(data: "a", convID: model.currentSelectedConv, convType: model.currentSelectedConvType == 1 ? ConvType.c2c : ConvType.group);
|
||||
_openMore();
|
||||
widget.goDownBottom();
|
||||
},
|
||||
child: PlatformUtils().isWeb
|
||||
? Icon(Icons.add_circle_outline_outlined, color: hexToColor("5c6168"), size: 32)
|
||||
: SvgPicture.asset(
|
||||
'images/add.svg',
|
||||
package: 'tencent_cloud_chat_uikit',
|
||||
color: const Color.fromRGBO(68, 68, 68, 1),
|
||||
height: 28,
|
||||
width: 28,
|
||||
),
|
||||
),
|
||||
if ((isAndroidDevice() || isWebDevice()) && !showMoreButton)
|
||||
SizedBox(
|
||||
height: 32.0,
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
widget.onSubmitted();
|
||||
if (showKeyboard) {
|
||||
widget.focusNode.requestFocus();
|
||||
}
|
||||
if (widget.textEditingController.text.isEmpty) {
|
||||
setState(() {
|
||||
showMoreButton = true;
|
||||
});
|
||||
}
|
||||
},
|
||||
child: Text(TIM_t("发送")),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
AnimatedContainer(
|
||||
duration: Duration(milliseconds: (showKeyboard && PlatformUtils().isAndroid) ? 200 : 340),
|
||||
curve: Curves.fastOutSlowIn,
|
||||
height: max(_getBottomHeight(), 0.0),
|
||||
child: ListView(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
children: [_getBottomContainer(theme)],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,994 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
// ignore: unnecessary_import
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:extended_text_field/extended_text_field.dart';
|
||||
import 'package:fc_native_video_thumbnail/fc_native_video_thumbnail.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:pasteboard/pasteboard.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:tencent_chat_i18n_tool/tencent_chat_i18n_tool.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_conversation.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_message.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_chat_separate_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_chat_global_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_setting_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/core/tim_uikit_wide_modal_operation_key.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/services_locatar.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/logger.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/message.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/optimize_utils.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/platform.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/screen_shot.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitTextField/special_text/DefaultSpecialTextSpanBuilder.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitTextField/special_text/emoji_text.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/drag_widget.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/wide_popup.dart';
|
||||
import 'package:universal_html/html.dart' as html;
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
import 'package:wechat_assets_picker/wechat_assets_picker.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_callback.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/color.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/tui_theme.dart';
|
||||
|
||||
class DesktopControlBarItem {
|
||||
final String item;
|
||||
final IconData? icon;
|
||||
final String? imgPath;
|
||||
final String? svgPath;
|
||||
final Color? color;
|
||||
final ValueChanged<Offset?> onClick;
|
||||
final String? showName;
|
||||
final double? size;
|
||||
|
||||
DesktopControlBarItem(
|
||||
{required this.item,
|
||||
this.icon,
|
||||
this.color,
|
||||
this.imgPath,
|
||||
this.svgPath,
|
||||
required this.onClick,
|
||||
this.showName,
|
||||
this.size})
|
||||
: assert(icon != null || TencentUtils.checkString(imgPath) != null || TencentUtils.checkString(svgPath) != null);
|
||||
}
|
||||
|
||||
class DesktopControlBarConfig {
|
||||
final bool showStickerPanel;
|
||||
final bool showScreenshotButton;
|
||||
final bool showSendFileButton;
|
||||
final bool showSendImageButton;
|
||||
final bool showSendVideoButton;
|
||||
final bool showMessageHistoryButton;
|
||||
|
||||
DesktopControlBarConfig({
|
||||
this.showStickerPanel = true,
|
||||
this.showScreenshotButton = true,
|
||||
this.showSendFileButton = true,
|
||||
this.showSendImageButton = true,
|
||||
this.showSendVideoButton = true,
|
||||
this.showMessageHistoryButton = true,
|
||||
});
|
||||
}
|
||||
|
||||
class TIMUIKitTextFieldLayoutWide extends StatefulWidget {
|
||||
/// sticker panel customization
|
||||
final CustomStickerPanel? customStickerPanel;
|
||||
final VoidCallback onEmojiSubmitted;
|
||||
final Function(int, String) onCustomEmojiFaceSubmitted;
|
||||
final Function(String, bool) handleSendEditStatus;
|
||||
final VoidCallback backSpaceText;
|
||||
final ValueChanged<String> addStickerToText;
|
||||
final TUITheme theme;
|
||||
final ValueChanged<String> handleAtText;
|
||||
|
||||
/// Whether to use the default emoji
|
||||
final bool isUseDefaultEmoji;
|
||||
|
||||
final bool isCompatibleWithTencentCloudChatPackageOldKeys;
|
||||
|
||||
final TUIChatSeparateViewModel model;
|
||||
|
||||
/// background color
|
||||
final Color? backgroundColor;
|
||||
|
||||
/// control input field behavior
|
||||
final TIMUIKitInputTextFieldController? controller;
|
||||
|
||||
/// config for more panel
|
||||
final MorePanelConfig? morePanelConfig;
|
||||
|
||||
final String languageType;
|
||||
|
||||
final TextEditingController textEditingController;
|
||||
|
||||
/// conversation id
|
||||
final String conversationID;
|
||||
|
||||
/// conversation type
|
||||
final ConvType conversationType;
|
||||
|
||||
final FocusNode focusNode;
|
||||
|
||||
/// show more panel
|
||||
final bool showMorePanel;
|
||||
|
||||
/// hint text for textField widget
|
||||
final String? hintText;
|
||||
|
||||
final int? currentCursor;
|
||||
|
||||
final ValueChanged<int?> setCurrentCursor;
|
||||
|
||||
final VoidCallback onCursorChange;
|
||||
|
||||
/// show send audio icon
|
||||
final bool showSendAudio;
|
||||
|
||||
final TIMUIKitChatConfig chatConfig;
|
||||
|
||||
/// on text changed
|
||||
final void Function(String)? onChanged;
|
||||
|
||||
final V2TimMessage? repliedMessage;
|
||||
|
||||
/// show send emoji icon
|
||||
final bool showSendEmoji;
|
||||
|
||||
final VoidCallback onSubmitted;
|
||||
|
||||
final VoidCallback goDownBottom;
|
||||
|
||||
final List<CustomEmojiFaceData> customEmojiStickerList;
|
||||
|
||||
/// Conversation need search
|
||||
final V2TimConversation currentConversation;
|
||||
|
||||
final List<CustomStickerPackage> stickerPackageList;
|
||||
|
||||
const TIMUIKitTextFieldLayoutWide(
|
||||
{Key? key,
|
||||
this.customStickerPanel,
|
||||
required this.onEmojiSubmitted,
|
||||
required this.onCustomEmojiFaceSubmitted,
|
||||
required this.backSpaceText,
|
||||
required this.addStickerToText,
|
||||
required this.isUseDefaultEmoji,
|
||||
this.isCompatibleWithTencentCloudChatPackageOldKeys = false,
|
||||
required this.languageType,
|
||||
required this.textEditingController,
|
||||
this.morePanelConfig,
|
||||
required this.conversationID,
|
||||
required this.conversationType,
|
||||
required this.focusNode,
|
||||
this.currentCursor,
|
||||
required this.setCurrentCursor,
|
||||
required this.onCursorChange,
|
||||
required this.model,
|
||||
this.backgroundColor,
|
||||
this.onChanged,
|
||||
required this.handleSendEditStatus,
|
||||
required this.handleAtText,
|
||||
this.repliedMessage,
|
||||
required this.onSubmitted,
|
||||
required this.goDownBottom,
|
||||
required this.showSendAudio,
|
||||
required this.showSendEmoji,
|
||||
required this.showMorePanel,
|
||||
this.hintText,
|
||||
required this.customEmojiStickerList,
|
||||
this.controller,
|
||||
required this.currentConversation,
|
||||
required this.theme,
|
||||
required this.chatConfig,
|
||||
required this.stickerPackageList})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<TIMUIKitTextFieldLayoutWide> createState() => _TIMUIKitTextFieldLayoutWideState();
|
||||
}
|
||||
|
||||
class _TIMUIKitTextFieldLayoutWideState extends TIMUIKitState<TIMUIKitTextFieldLayoutWide> {
|
||||
final TUISettingModel settingModel = serviceLocator<TUISettingModel>();
|
||||
OverlayEntry? entry;
|
||||
final ImagePicker _picker = ImagePicker();
|
||||
Uint8List? fileContent;
|
||||
String? fileName;
|
||||
File? tempFile;
|
||||
Function? setKeyboardHeight;
|
||||
double? bottomPadding;
|
||||
late ScrollController _scrollController;
|
||||
late FocusNode textFocusNode;
|
||||
late List<DesktopControlBarItem> defaultControlBarItems;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.controller != null) {
|
||||
widget.controller?.addListener(() {
|
||||
final actionType = widget.controller?.actionType;
|
||||
if (actionType == ActionType.hideAllPanel) {
|
||||
hideAllPanel();
|
||||
}
|
||||
});
|
||||
}
|
||||
textFocusNode = FocusNode();
|
||||
widget.focusNode.requestFocus();
|
||||
_scrollController = ScrollController();
|
||||
try {
|
||||
if (PlatformUtils().isWeb) {
|
||||
html.window.addEventListener('paste', (event) {
|
||||
_handlePaste(event as html.ClipboardEvent);
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore: avoid_print
|
||||
outputLogger.i(e.toString());
|
||||
}
|
||||
generateDefaultControlBarItems();
|
||||
}
|
||||
|
||||
Future<void> _handlePaste(html.ClipboardEvent event) async {
|
||||
try {
|
||||
if (event.clipboardData!.files!.isNotEmpty) {
|
||||
html.File imageFile = event.clipboardData!.files![0];
|
||||
sendFileUseJs(imageFile);
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore: avoid_print
|
||||
outputLogger.i("Paste image failed: ${e.toString()}");
|
||||
}
|
||||
}
|
||||
|
||||
hideAllPanel() {
|
||||
widget.focusNode.unfocus();
|
||||
widget.currentCursor == null;
|
||||
}
|
||||
|
||||
_debounce(
|
||||
Function(String text) fun, [
|
||||
Duration delay = const Duration(milliseconds: 30),
|
||||
]) {
|
||||
Timer? timer;
|
||||
return (String text) {
|
||||
if (timer != null) {
|
||||
timer?.cancel();
|
||||
}
|
||||
|
||||
timer = Timer(delay, () {
|
||||
fun(text);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
String getAbstractMessage(V2TimMessage message) {
|
||||
final String? customAbstractMessage = widget.model.abstractMessageBuilder != null
|
||||
? widget.model.abstractMessageBuilder!(widget.model.repliedMessage!)
|
||||
: null;
|
||||
return customAbstractMessage ??
|
||||
MessageUtils.getAbstractMessageAsync(widget.model.repliedMessage!, widget.model.groupMemberList ?? []);
|
||||
}
|
||||
|
||||
_buildRepliedMessage(V2TimMessage? repliedMessage) {
|
||||
final haveRepliedMessage = repliedMessage != null;
|
||||
if (haveRepliedMessage) {
|
||||
return Container(
|
||||
color: widget.backgroundColor ?? hexToColor("f5f5f6"),
|
||||
alignment: Alignment.centerLeft,
|
||||
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 16),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
TIM_t("回复 "),
|
||||
style: TextStyle(color: hexToColor("8f959e"), fontSize: 14),
|
||||
),
|
||||
Text(
|
||||
MessageUtils.getDisplayName(widget.model.repliedMessage!),
|
||||
softWrap: true,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(color: hexToColor("8f959e"), fontSize: 14, fontWeight: FontWeight.bold),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
": ${getAbstractMessage(repliedMessage)}",
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: hexToColor("8f959e"),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 8,
|
||||
),
|
||||
InkWell(
|
||||
onTap: () {
|
||||
widget.model.repliedMessage = null;
|
||||
},
|
||||
child: Icon(Icons.cancel, color: hexToColor("8f959e"), size: 18),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
return Container();
|
||||
}
|
||||
|
||||
_sendEmoji(Offset? offset, TUITheme theme) {
|
||||
widget.onCursorChange();
|
||||
if (entry != null) {
|
||||
entry?.remove();
|
||||
entry = null;
|
||||
} else {
|
||||
entry = OverlayEntry(builder: (BuildContext context) {
|
||||
return TUIKitDragArea(
|
||||
closeFun: () {
|
||||
if (entry != null) {
|
||||
entry?.remove();
|
||||
entry = null;
|
||||
}
|
||||
},
|
||||
initOffset: offset != null
|
||||
? Offset(offset.dx, max(offset.dy, 16))
|
||||
: Offset(MediaQuery.of(context).size.height * 0.5 + 20, MediaQuery.of(context).size.height * 0.5 - 100),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
color: theme.wideBackgroundColor,
|
||||
border: Border.all(
|
||||
width: 2,
|
||||
color: theme.weakBackgroundColor ?? const Color(0xFFbebebe),
|
||||
),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0xFFbebebe),
|
||||
offset: Offset(5, 5),
|
||||
blurRadius: 10,
|
||||
spreadRadius: 1,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Container(
|
||||
child: widget.customStickerPanel != null
|
||||
? widget.customStickerPanel!(
|
||||
height: widget.chatConfig.desktopStickerPanelHeight,
|
||||
width: 350,
|
||||
sendTextMessage: () {
|
||||
widget.onEmojiSubmitted();
|
||||
},
|
||||
sendFaceMessage: widget.onCustomEmojiFaceSubmitted,
|
||||
deleteText: () {
|
||||
widget.backSpaceText();
|
||||
},
|
||||
addText: (int unicode) {
|
||||
final newText = String.fromCharCode(unicode);
|
||||
widget.addStickerToText(newText);
|
||||
entry?.remove();
|
||||
entry = null;
|
||||
},
|
||||
addCustomEmojiText: ((String singleEmojiName) {
|
||||
String? emojiName = singleEmojiName.split('.png')[0];
|
||||
String compatibleEmojiName = emojiName;
|
||||
if (widget.isCompatibleWithTencentCloudChatPackageOldKeys) {
|
||||
compatibleEmojiName = EmojiUtil.getCompatibleEmojiName(emojiName);
|
||||
}
|
||||
|
||||
String newText = '[$compatibleEmojiName]';
|
||||
widget.addStickerToText(newText);
|
||||
entry?.remove();
|
||||
entry = null;
|
||||
}),
|
||||
defaultCustomEmojiStickerList: widget.isUseDefaultEmoji ? TUIKitStickerConstData.emojiList : [])
|
||||
: Material(
|
||||
color: Colors.transparent,
|
||||
child: StickerPanel(
|
||||
isWideScreen: true,
|
||||
height: widget.chatConfig.desktopStickerPanelHeight,
|
||||
width: 350,
|
||||
sendTextMsg: null,
|
||||
sendFaceMsg: (_, __) {
|
||||
widget.onCustomEmojiFaceSubmitted(_, __);
|
||||
entry?.remove();
|
||||
entry = null;
|
||||
},
|
||||
deleteText: () {
|
||||
widget.backSpaceText();
|
||||
},
|
||||
addText: (int unicode) {
|
||||
final newText = String.fromCharCode(unicode);
|
||||
widget.addStickerToText(newText);
|
||||
entry?.remove();
|
||||
entry = null;
|
||||
},
|
||||
addCustomEmojiText: ((String singleEmojiName) {
|
||||
String? emojiName = singleEmojiName.split('.png')[0];
|
||||
String compatibleEmojiName = emojiName;
|
||||
if (widget.isCompatibleWithTencentCloudChatPackageOldKeys) {
|
||||
compatibleEmojiName = EmojiUtil.getCompatibleEmojiName(emojiName);
|
||||
}
|
||||
|
||||
String newText = '[$compatibleEmojiName]';
|
||||
widget.addStickerToText(newText);
|
||||
entry?.remove();
|
||||
entry = null;
|
||||
}),
|
||||
customStickerPackageList: widget.stickerPackageList,
|
||||
bottomColor: theme.weakBackgroundColor,
|
||||
backgroundColor: theme.wideBackgroundColor,
|
||||
lightPrimaryColor: theme.lightPrimaryColor),
|
||||
),
|
||||
),
|
||||
));
|
||||
});
|
||||
Overlay.of(context).insert(entry!);
|
||||
}
|
||||
}
|
||||
|
||||
_addGreyOverlay() {
|
||||
if (entry != null) {
|
||||
_removeOverlay();
|
||||
return;
|
||||
} else {
|
||||
entry = OverlayEntry(builder: (BuildContext context) {
|
||||
return Container(
|
||||
color: const Color(0x7F000000),
|
||||
);
|
||||
});
|
||||
Overlay.of(context).insert(entry!);
|
||||
}
|
||||
}
|
||||
|
||||
_removeOverlay() {
|
||||
entry?.remove();
|
||||
entry = null;
|
||||
}
|
||||
|
||||
_sendFile(
|
||||
TUIChatSeparateViewModel model,
|
||||
TUITheme theme,
|
||||
) async {
|
||||
if (!PlatformUtils().isWeb) {
|
||||
_addGreyOverlay();
|
||||
}
|
||||
try {
|
||||
final convID = widget.conversationID;
|
||||
final convType = widget.conversationType;
|
||||
FilePickerResult? result = await FilePicker.platform.pickFiles();
|
||||
_removeOverlay();
|
||||
if (result != null && result.files.isNotEmpty) {
|
||||
if (PlatformUtils().isWeb) {
|
||||
html.Node? inputElem;
|
||||
inputElem = html.document.getElementById("__file_picker_web-file-input")?.querySelector("input");
|
||||
fileName = result.files.single.name;
|
||||
|
||||
MessageUtils.handleMessageError(
|
||||
model.sendFileMessage(inputElement: inputElem, fileName: fileName, convID: convID, convType: convType),
|
||||
context);
|
||||
} else {
|
||||
File file = File(result.files.single.path!);
|
||||
final int size = file.lengthSync();
|
||||
final String savePath = file.path;
|
||||
|
||||
MessageUtils.handleMessageError(
|
||||
model.sendFileMessage(filePath: savePath, size: size, convID: convID, convType: convType), context);
|
||||
}
|
||||
} else {
|
||||
throw TypeError();
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore: avoid_print
|
||||
outputLogger.i("_sendFileErr: ${e.toString()}");
|
||||
}
|
||||
}
|
||||
|
||||
List<Widget> generateBarIcons(List<DesktopControlBarItem> items, TUITheme theme) {
|
||||
final defaultItems = defaultControlBarItems.map((e) => e.item);
|
||||
return items.map((e) {
|
||||
final GlobalKey key = GlobalKey();
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(right: 10),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
final alignBox = key.currentContext?.findRenderObject() as RenderBox?;
|
||||
var offset = alignBox?.localToGlobal(Offset.zero);
|
||||
final double? dx = (offset?.dx != null) ? offset!.dx : null;
|
||||
final double? dy = (offset?.dy != null && alignBox?.size.height != null)
|
||||
? offset!.dy - (widget.chatConfig.desktopStickerPanelHeight + 20)
|
||||
: null;
|
||||
e.onClick((dx != null && dy != null) ? Offset(dx, dy) : null);
|
||||
},
|
||||
child: Tooltip(
|
||||
preferBelow: false,
|
||||
textStyle: TextStyle(fontSize: 12, color: theme.white),
|
||||
message: e.showName,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(borderRadius: BorderRadius.circular(2)),
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: () {
|
||||
if (TencentUtils.checkString(e.svgPath) != null) {
|
||||
return SvgPicture.asset(
|
||||
e.svgPath!,
|
||||
package: defaultItems.contains(e.item) ? 'tencent_cloud_chat_uikit' : null,
|
||||
key: key,
|
||||
width: e.size ?? 16,
|
||||
height: e.size ?? 16,
|
||||
);
|
||||
}
|
||||
if (TencentUtils.checkString(e.imgPath) != null) {
|
||||
return Image.asset(
|
||||
e.imgPath!,
|
||||
package: defaultItems.contains(e.item) ? 'tencent_cloud_chat_uikit' : null,
|
||||
key: key,
|
||||
width: e.size ?? 16,
|
||||
height: e.size ?? 16,
|
||||
);
|
||||
}
|
||||
return Icon(
|
||||
e.icon,
|
||||
key: key,
|
||||
color: e.color ?? hexToColor("646a73"),
|
||||
size: e.size ?? 20,
|
||||
);
|
||||
}(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
_sendImageFileOnWeb(TUIChatSeparateViewModel model) async {
|
||||
try {
|
||||
final pickedFile = await _picker.pickImage(source: ImageSource.gallery);
|
||||
final imageContent = await pickedFile!.readAsBytes();
|
||||
fileName = pickedFile.name;
|
||||
tempFile = File(pickedFile.path);
|
||||
fileContent = imageContent;
|
||||
|
||||
html.Node? inputElem;
|
||||
inputElem = html.document.getElementById("__image_picker_web-file-input")?.querySelector("input");
|
||||
final convID = widget.conversationID;
|
||||
final convType = widget.conversationType;
|
||||
MessageUtils.handleMessageError(
|
||||
model.sendImageMessage(
|
||||
inputElement: inputElem, imagePath: tempFile?.path, convID: convID, convType: convType),
|
||||
context);
|
||||
} catch (e) {
|
||||
// ignore: avoid_print
|
||||
outputLogger.i("_sendFileErr: ${e.toString()}");
|
||||
}
|
||||
}
|
||||
|
||||
_sendVideoFileOnWeb(TUIChatSeparateViewModel model) async {
|
||||
try {
|
||||
final pickedFile = await _picker.pickVideo(source: ImageSource.gallery);
|
||||
final videoContent = await pickedFile!.readAsBytes();
|
||||
fileName = pickedFile.name;
|
||||
tempFile = File(pickedFile.path);
|
||||
fileContent = videoContent;
|
||||
|
||||
if (fileName!.split(".")[fileName!.split(".").length - 1] != "mp4") {
|
||||
onTIMCallback(
|
||||
TIMCallback(type: TIMCallbackType.INFO, infoRecommendText: TIM_t("视频消息仅限 mp4 格式"), infoCode: 6660412));
|
||||
return;
|
||||
}
|
||||
|
||||
html.Node? inputElem;
|
||||
inputElem = html.document.getElementById("__image_picker_web-file-input")?.querySelector("input");
|
||||
final convID = widget.conversationID;
|
||||
final convType = widget.conversationType;
|
||||
MessageUtils.handleMessageError(
|
||||
model.sendVideoMessage(
|
||||
inputElement: inputElem, videoPath: tempFile?.path, convID: convID, convType: convType),
|
||||
context);
|
||||
} catch (e) {
|
||||
// ignore: avoid_print
|
||||
outputLogger.i("_sendFileErr: ${e.toString()}");
|
||||
}
|
||||
}
|
||||
|
||||
_sendVideoMessage(AssetEntity asset, TUIChatSeparateViewModel model) async {
|
||||
try {
|
||||
final plugin = FcNativeVideoThumbnail();
|
||||
final originFile = await asset.originFile;
|
||||
final size = await originFile!.length();
|
||||
if (size >= 104857600) {
|
||||
onTIMCallback(
|
||||
TIMCallback(type: TIMCallbackType.INFO, infoRecommendText: TIM_t("发送失败,视频不能大于100MB"), infoCode: 6660405));
|
||||
return;
|
||||
}
|
||||
|
||||
final duration = asset.videoDuration.inSeconds;
|
||||
final filePath = originFile.path;
|
||||
final convID = widget.conversationID;
|
||||
final convType = widget.conversationType;
|
||||
|
||||
String tempPath = (await getTemporaryDirectory()).path + p.extension(originFile.path, 3) + ".jpeg";
|
||||
|
||||
await plugin.getVideoThumbnail(
|
||||
srcFile: originFile.path,
|
||||
destFile: tempPath,
|
||||
format: 'jpeg',
|
||||
width: 128,
|
||||
quality: 100,
|
||||
height: 128,
|
||||
);
|
||||
MessageUtils.handleMessageError(
|
||||
model.sendVideoMessage(
|
||||
videoPath: filePath, duration: duration, snapshotPath: tempPath, convID: convID, convType: convType),
|
||||
context);
|
||||
} catch (e) {
|
||||
onTIMCallback(TIMCallback(type: TIMCallbackType.INFO, infoRecommendText: TIM_t("视频文件异常"), infoCode: 6660415));
|
||||
}
|
||||
}
|
||||
|
||||
_sendMediaMessage(TUIChatSeparateViewModel model, TUITheme theme, FileType fileType) async {
|
||||
try {
|
||||
final convID = widget.conversationID;
|
||||
final convType = widget.conversationType;
|
||||
|
||||
if (PlatformUtils().isMobile) {
|
||||
final pickedAssets = await AssetPicker.pickAssets(context);
|
||||
|
||||
if (pickedAssets != null) {
|
||||
for (var asset in pickedAssets) {
|
||||
final originFile = await asset.originFile;
|
||||
final filePath = originFile?.path;
|
||||
final type = asset.type;
|
||||
if (filePath != null) {
|
||||
if (type == AssetType.image) {
|
||||
MessageUtils.handleMessageError(
|
||||
model.sendImageMessage(imagePath: filePath, convID: convID, convType: convType), context);
|
||||
}
|
||||
|
||||
if (type == AssetType.video) {
|
||||
_sendVideoMessage(asset, model);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
final plugin = FcNativeVideoThumbnail();
|
||||
_addGreyOverlay();
|
||||
FilePickerResult? result = await FilePicker.platform.pickFiles(type: fileType);
|
||||
_removeOverlay();
|
||||
if (result != null && result.files.isNotEmpty) {
|
||||
File file = File(result.files.single.path!);
|
||||
final String savePath = file.path;
|
||||
final String type =
|
||||
TencentUtils.getFileType((savePath.split(".")[savePath.split(".").length - 1]).toLowerCase())
|
||||
.split("/")[0];
|
||||
|
||||
if (type == "image") {
|
||||
MessageUtils.handleMessageError(
|
||||
model.sendImageMessage(imagePath: savePath, convID: convID, convType: convType), context);
|
||||
} else if (type == "video") {
|
||||
String tempPath = (await getTemporaryDirectory()).path + p.basename(savePath) + ".jpeg";
|
||||
await plugin.getVideoThumbnail(
|
||||
srcFile: savePath,
|
||||
destFile: tempPath,
|
||||
format: 'jpeg',
|
||||
width: 128,
|
||||
quality: 100,
|
||||
height: 128,
|
||||
);
|
||||
MessageUtils.handleMessageError(
|
||||
model.sendVideoMessage(videoPath: savePath, convID: convID, convType: convType, snapshotPath: tempPath),
|
||||
context);
|
||||
}
|
||||
} else {
|
||||
throw TypeError();
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// ignore: avoid_print
|
||||
outputLogger.i("send media err: $err");
|
||||
onTIMCallback(TIMCallback(type: TIMCallbackType.INFO, infoRecommendText: TIM_t("视频文件异常"), infoCode: 6660415));
|
||||
}
|
||||
}
|
||||
|
||||
_sendImageWithConfirmation({String? fileName, Size? fileSize, required String filePath}) async {
|
||||
final option1 =
|
||||
widget.currentConversation.showName ?? (widget.conversationType == ConvType.group ? TIM_t("群聊") : TIM_t("对方"));
|
||||
final size = fileSize ?? await ScreenshotHelper.getImageSize(filePath);
|
||||
|
||||
TUIKitWidePopup.showPopupWindow(
|
||||
operationKey: TUIKitWideModalOperationKey.beforeSendScreenShot,
|
||||
context: context,
|
||||
isDarkBackground: false,
|
||||
width: 500,
|
||||
height: min(500, size.height / 2 + 140),
|
||||
title: TIM_t_para("发送给{{option1}}", "发送给$option1")(option1: option1),
|
||||
child: (closeFunc) => Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: min(360, size.height / 2),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
launchUrl(PlatformUtils().isWeb ? Uri.parse(filePath) : Uri.file(filePath));
|
||||
},
|
||||
child: PlatformUtils().isWeb
|
||||
? Image.network(
|
||||
filePath,
|
||||
height: min(360, size.height / 2),
|
||||
)
|
||||
: Image.file(
|
||||
File(filePath),
|
||||
height: min(360, size.height / 2),
|
||||
),
|
||||
),
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
OutlinedButton(
|
||||
onPressed: () {
|
||||
closeFunc();
|
||||
},
|
||||
child: Text(TIM_t("取消"))),
|
||||
const SizedBox(
|
||||
width: 20,
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
MessageUtils.handleMessageError(
|
||||
widget.model.sendImageMessage(
|
||||
imagePath: filePath,
|
||||
imageName: fileName,
|
||||
convID: widget.conversationID,
|
||||
convType: widget.conversationType),
|
||||
context);
|
||||
closeFunc();
|
||||
},
|
||||
child: Text(TIM_t("发送")))
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_sendScreenShot() async {
|
||||
final file = await ScreenshotHelper.captureScreen();
|
||||
if (file != null) {
|
||||
_sendImageWithConfirmation(filePath: file);
|
||||
} else {}
|
||||
}
|
||||
|
||||
generateDefaultControlBarItems() {
|
||||
final DesktopControlBarConfig config = widget.chatConfig.desktopControlBarConfig ?? DesktopControlBarConfig();
|
||||
final List<DesktopControlBarItem> itemsList = [
|
||||
if (config.showStickerPanel)
|
||||
DesktopControlBarItem(
|
||||
item: "face",
|
||||
showName: TIM_t("表情"),
|
||||
onClick: (offset) {
|
||||
_sendEmoji(offset, widget.theme);
|
||||
},
|
||||
imgPath: "images/svg/send_face.png"),
|
||||
if (config.showScreenshotButton && PlatformUtils().isDesktop)
|
||||
DesktopControlBarItem(
|
||||
item: "screenShot",
|
||||
showName: TIM_t("截图"),
|
||||
onClick: (offset) {
|
||||
_sendScreenShot();
|
||||
},
|
||||
svgPath: "images/svg/send_screenshot.svg"),
|
||||
if (config.showSendFileButton)
|
||||
DesktopControlBarItem(
|
||||
item: "file",
|
||||
showName: TIM_t("文件"),
|
||||
onClick: (offset) {
|
||||
_sendFile(widget.model, widget.theme);
|
||||
},
|
||||
svgPath: "images/svg/send_file.svg"),
|
||||
if (config.showSendImageButton)
|
||||
DesktopControlBarItem(
|
||||
item: "photo",
|
||||
showName: TIM_t("图片"),
|
||||
onClick: (offset) {
|
||||
if (PlatformUtils().isWeb) {
|
||||
_sendImageFileOnWeb(widget.model);
|
||||
} else {
|
||||
_sendMediaMessage(widget.model, widget.theme, FileType.image);
|
||||
}
|
||||
},
|
||||
svgPath: "images/svg/send_image.svg"),
|
||||
if (config.showSendVideoButton)
|
||||
DesktopControlBarItem(
|
||||
item: "video",
|
||||
showName: TIM_t("视频"),
|
||||
onClick: (offset) {
|
||||
if (PlatformUtils().isWeb) {
|
||||
_sendVideoFileOnWeb(widget.model);
|
||||
} else {
|
||||
_sendMediaMessage(widget.model, widget.theme, FileType.video);
|
||||
}
|
||||
},
|
||||
svgPath: "images/svg/send_video.svg"),
|
||||
if (config.showMessageHistoryButton)
|
||||
DesktopControlBarItem(
|
||||
item: "history",
|
||||
showName: TIM_t("消息历史"),
|
||||
onClick: (offset) {
|
||||
TUIKitWidePopup.showPopupWindow(
|
||||
operationKey: TUIKitWideModalOperationKey.chatHistory,
|
||||
context: context,
|
||||
width: MediaQuery.of(context).size.width * 0.5,
|
||||
height: MediaQuery.of(context).size.width * 0.5,
|
||||
child: (onClose) => TIMUIKitSearchMsgDetail(
|
||||
currentConversation: widget.currentConversation,
|
||||
keyword: '',
|
||||
initMessageList: widget.model
|
||||
.getOriginMessageList()
|
||||
.getRange(0, min(widget.model.getOriginMessageList().length, 100))
|
||||
.toList(),
|
||||
onTapConversation: (V2TimConversation conversation, V2TimMessage? message) {},
|
||||
),
|
||||
theme: widget.theme);
|
||||
},
|
||||
svgPath: "images/svg/message_history.svg"),
|
||||
];
|
||||
defaultControlBarItems = itemsList;
|
||||
}
|
||||
|
||||
List<Widget> generateControlBar(TUIChatSeparateViewModel model, TUITheme theme) {
|
||||
final List<DesktopControlBarItem> itemsList = [
|
||||
...defaultControlBarItems,
|
||||
...(widget.chatConfig.additionalDesktopControlBarItems ?? [])
|
||||
];
|
||||
|
||||
return generateBarIcons(itemsList, theme);
|
||||
}
|
||||
|
||||
sendFileUseJs(html.File file) {
|
||||
final mimeType = file.type.split('/');
|
||||
final type = mimeType[0];
|
||||
final blobUrl = html.Url.createObjectUrl(file);
|
||||
if (type == 'image') {
|
||||
_sendImageWithConfirmation(filePath: blobUrl, fileName: file.name, fileSize: const Size(500, 500));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _handleKeyEvent(RawKeyEvent event) async {
|
||||
if (PlatformUtils().isDesktop &&
|
||||
((event.isKeyPressed(LogicalKeyboardKey.controlLeft) && event.logicalKey == LogicalKeyboardKey.keyV) ||
|
||||
(event.isMetaPressed && event.logicalKey == LogicalKeyboardKey.keyV))) {
|
||||
final bytes = await Pasteboard.image;
|
||||
if (bytes != null) {
|
||||
String directory;
|
||||
if (PlatformUtils().isWindows) {
|
||||
final String documentsDirectoryPath = "${Platform.environment['USERPROFILE']}";
|
||||
PackageInfo packageInfo = await PackageInfo.fromPlatform();
|
||||
String pkgName = packageInfo.packageName;
|
||||
directory = p.join(documentsDirectoryPath, "Documents", ".TencentCloudChat", pkgName, "screenshots");
|
||||
} else {
|
||||
final dic = await getApplicationSupportDirectory();
|
||||
directory = dic.path;
|
||||
}
|
||||
const uuid = Uuid();
|
||||
final fileName = 'paste_image_${uuid.v4()}.png';
|
||||
final scDirectory = Directory(directory);
|
||||
final filePath = '${scDirectory.path}${PlatformUtils().isWindows ? "\\" : "/"}$fileName';
|
||||
final file = File(filePath);
|
||||
if (!await scDirectory.exists()) {
|
||||
await scDirectory.create(recursive: true);
|
||||
}
|
||||
await file.writeAsBytes(bytes.toList());
|
||||
_sendImageWithConfirmation(filePath: filePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final theme = value.theme;
|
||||
|
||||
setKeyboardHeight ??= OptimizeUtils.debounce((height) {
|
||||
settingModel.keyboardHeight = height;
|
||||
}, const Duration(seconds: 1));
|
||||
|
||||
final debounceFunc = _debounce((value) {
|
||||
if (widget.onChanged != null) {
|
||||
widget.onChanged!(value);
|
||||
}
|
||||
widget.handleAtText(value);
|
||||
widget.handleSendEditStatus(value, true);
|
||||
}, const Duration(milliseconds: 80));
|
||||
|
||||
final MediaQueryData data = MediaQuery.of(context);
|
||||
EdgeInsets padding = data.padding;
|
||||
if (bottomPadding == null || padding.bottom > bottomPadding!) {
|
||||
bottomPadding = padding.bottom;
|
||||
}
|
||||
|
||||
return RawKeyboardListener(
|
||||
focusNode: textFocusNode,
|
||||
onKey: _handleKeyEvent,
|
||||
child: Container(
|
||||
color: widget.backgroundColor ?? theme.desktopChatMessageInputBgColor,
|
||||
child: Column(
|
||||
children: [
|
||||
_buildRepliedMessage(widget.repliedMessage),
|
||||
SizedBox(height: 1, child: Container(color: theme.weakDividerColor ?? Colors.black12)),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 12),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: generateControlBar(widget.model, theme),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 6),
|
||||
constraints: const BoxConstraints(minHeight: 50),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ExtendedTextField(
|
||||
scrollController: _scrollController,
|
||||
autofocus: true,
|
||||
maxLines: widget.chatConfig.desktopMessageInputFieldLines,
|
||||
minLines: widget.chatConfig.desktopMessageInputFieldLines,
|
||||
focusNode: widget.focusNode,
|
||||
onChanged: debounceFunc,
|
||||
keyboardType: TextInputType.multiline,
|
||||
onEditingComplete: () {
|
||||
// // widget.onSubmitted();
|
||||
},
|
||||
textAlignVertical: TextAlignVertical.top,
|
||||
style: const TextStyle(fontSize: 14),
|
||||
decoration: InputDecoration(
|
||||
hoverColor: Colors.transparent,
|
||||
border: InputBorder.none,
|
||||
hintStyle: const TextStyle(
|
||||
color: Color(0xffAEA4A3),
|
||||
),
|
||||
fillColor:
|
||||
widget.backgroundColor ?? theme.desktopChatMessageInputBgColor ?? hexToColor("fafafa"),
|
||||
filled: true,
|
||||
isDense: true,
|
||||
hintText: widget.hintText ?? '',
|
||||
),
|
||||
controller: widget.textEditingController,
|
||||
specialTextSpanBuilder: PlatformUtils().isWeb
|
||||
? null
|
||||
: DefaultSpecialTextSpanBuilder(
|
||||
isUseTencentCloudChatPackage:
|
||||
widget.model.chatConfig.stickerPanelConfig?.useTencentCloudChatStickerPackage ??
|
||||
true,
|
||||
customEmojiStickerList: widget.customEmojiStickerList,
|
||||
showAtBackground: true,
|
||||
)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,703 @@
|
||||
// ignore_for_file: must_be_immutable, avoid_print
|
||||
|
||||
import 'package:desktop_drop/desktop_drop.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:provider/single_child_widget.dart';
|
||||
import 'package:scroll_to_index/scroll_to_index.dart';
|
||||
import 'package:tencent_chat_i18n_tool/tencent_chat_i18n_tool.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_conversation.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_application.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_group_application.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_at_info.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_group_at_info.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_member_full_info.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_group_member_full_info.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_message.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/tencent_im_sdk_plugin.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/life_cycle/chat_life_cycle.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/listener_model/tui_group_listener_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_chat_separate_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_chat_global_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_conversation_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_self_info_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/services_locatar.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/constants/history_message_constant.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/controller/tim_uikit_chat_controller.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/frame.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/logger.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/platform.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitTextField/at_member_panel.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/tim_uikit_multi_select_panel.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/tim_uikit_send_file.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/color.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/tui_theme.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/tui_theme_view_model.dart';
|
||||
import 'TIMUIKItMessageList/TIMUIKitTongue/tim_uikit_chat_history_message_list_tongue.dart';
|
||||
import 'TIMUIKItMessageList/tim_uikit_chat_history_message_list_config.dart';
|
||||
import 'TIMUIKItMessageList/tim_uikit_history_message_list_container.dart';
|
||||
|
||||
class TIMUIKitChat extends StatefulWidget {
|
||||
int startTime = 0;
|
||||
int endTime = 0;
|
||||
|
||||
/// The chat controller you tend to used.
|
||||
/// You have to provide this before using it since tencent_cloud_chat_uikit 0.1.4.
|
||||
final TIMUIKitChatController? controller;
|
||||
|
||||
/// [Update] It is suggested to provide the `V2TimConversation` once directly, since tencent_cloud_chat_uikit 1.5.0.
|
||||
/// `conversationID` / `conversationType` / `groupAtInfoList` / `conversationShowName` are not necessary to be provided, unless you want to cover these fields manually.
|
||||
final V2TimConversation conversation;
|
||||
|
||||
/// The ID of the Group that the topic belongs to, only need for topic.
|
||||
final String? groupID;
|
||||
|
||||
/// Conversation id, use for load history message list.
|
||||
/// This field is not necessary to be provided, when `conversation` is provided, unless you want to cover this field manually.
|
||||
final String? conversationID;
|
||||
|
||||
/// Conversation type.
|
||||
/// This field is not necessary to be provided, when `conversation` is provided, unless you want to cover this field manually.
|
||||
final ConvType? conversationType;
|
||||
|
||||
/// use for customize avatar
|
||||
final Widget Function(BuildContext context, V2TimMessage message)? userAvatarBuilder;
|
||||
|
||||
/// Use for show conversation name.
|
||||
/// This field is not necessary to be provided, when `conversation` is provided, unless you want to cover this field manually.
|
||||
final String? conversationShowName;
|
||||
|
||||
/// Avatar and name in message reaction tap callback.
|
||||
final void Function(String userID, TapDownDetails tapDetails)? onTapAvatar;
|
||||
|
||||
/// Avatar and name in message reaction secondary tap callback.
|
||||
final void Function(String userID, TapDownDetails tapDetails)? onSecondaryTapAvatar;
|
||||
|
||||
@Deprecated(
|
||||
"Nickname will not shows in one-to-one chat, if you tend to control it in group chat, please use `isShowSelfNameInGroup` and `isShowOthersNameInGroup` from `config: TIMUIKitChatConfig` instead")
|
||||
|
||||
/// Should show the nick name.
|
||||
final bool showNickName;
|
||||
|
||||
/// Message item builder, can customize partial message item for different types or the layout for the whole line.
|
||||
final MessageItemBuilder? messageItemBuilder;
|
||||
|
||||
/// Is show unread message count, default value is false
|
||||
final bool showTotalUnReadCount;
|
||||
|
||||
/// Deprecated("Please use [extraTipsActionItemBuilder] instead")
|
||||
final Widget? Function(V2TimMessage message, Function() closeTooltip, [Key? key, BuildContext? context])?
|
||||
exteraTipsActionItemBuilder;
|
||||
|
||||
/// The builder for extra tips action.
|
||||
final Widget? Function(V2TimMessage message, Function() closeTooltip, [Key? key, BuildContext? context])?
|
||||
extraTipsActionItemBuilder;
|
||||
|
||||
/// The text of draft shows in TextField.
|
||||
/// [Recommend]: You can specify this field with the draftText from V2TimConversation.
|
||||
final String? draftText;
|
||||
|
||||
/// The target message been jumped just after entering the chat page.
|
||||
final V2TimMessage? initFindingMsg;
|
||||
|
||||
/// The hint text shows at input field.
|
||||
final String? textFieldHintText;
|
||||
|
||||
/// The configuration for appbar.
|
||||
final AppBar? appBarConfig;
|
||||
|
||||
/// The configuration for historical message list.
|
||||
final TIMUIKitHistoryMessageListConfig? mainHistoryListConfig;
|
||||
|
||||
/// The configuration for more panel, can customize actions.
|
||||
final MorePanelConfig? morePanelConfig;
|
||||
|
||||
/// The builder for the tongue on the right bottom.
|
||||
/// Used for back to bottom, shows the count of unread new messages,
|
||||
/// and prompts the messages that @ user.
|
||||
final TongueItemBuilder? tongueItemBuilder;
|
||||
|
||||
/// The `groupAtInfoList` from `V2TimConversation`.
|
||||
/// This field is not necessary to be provided, when `conversation` is provided,
|
||||
/// unless you want to cover this field manually.
|
||||
final List<V2TimGroupAtInfo?>? groupAtInfoList;
|
||||
|
||||
/// The configuration for the whole `TIMUIKitChat` widget.
|
||||
final TIMUIKitChatConfig? config;
|
||||
|
||||
/// The callback for jumping to the page for `TIMUIKitGroupApplicationList`
|
||||
/// or other pages to deal with enter group application for group administrator manually,
|
||||
/// in the case of [public group].
|
||||
/// The parameter here is `String groupID`
|
||||
final ValueChanged<String>? onDealWithGroupApplication;
|
||||
|
||||
/// The generator for the abstract summary preview of a message,
|
||||
/// typically used in replied and forwarded messages.
|
||||
/// Returns `null` to use the default message summary.
|
||||
final String? Function(V2TimMessage message)? abstractMessageBuilder;
|
||||
|
||||
/// The configuration for tool tips panel, long press messages will show this panel.
|
||||
final ToolTipsConfig? toolTipsConfig;
|
||||
|
||||
/// The life cycle for chat business logic.
|
||||
final ChatLifeCycle? lifeCycle;
|
||||
|
||||
/// The top fixed widget.
|
||||
final Widget? topFixWidget;
|
||||
|
||||
/// Specify the custom small png emoji packages.
|
||||
final List<CustomEmojiFaceData> customEmojiStickerList;
|
||||
|
||||
final Widget? customAppBar;
|
||||
|
||||
final Widget? inputTopBuilder;
|
||||
|
||||
/// Custom emoji panel.
|
||||
final CustomStickerPanel? customStickerPanel;
|
||||
|
||||
/// This parameter accepts a custom widget to be displayed when the mouse hovers over a message,
|
||||
/// replacing the default message hover action bar.
|
||||
/// Applicable only on desktop platforms.
|
||||
/// If provided, the default message action functionality will appear in the right-click context menu instead.
|
||||
/// Returns `null` to use default hover bar.
|
||||
final Widget? Function(V2TimMessage message)? customMessageHoverBarOnDesktop;
|
||||
|
||||
/// Custom text field
|
||||
final Widget Function(BuildContext context)? textFieldBuilder;
|
||||
|
||||
/// An optional parameter `groupMemberList` can be provided.
|
||||
/// `groupMemberList` accepts a list of nullable `V2TimGroupMemberFullInfo` objects.
|
||||
/// The purpose of this parameter is to allow the client to supply a pre-fetched list
|
||||
/// of group member information. If this list is provided, it will not make
|
||||
/// additional network requests to fetch the group member information internally.
|
||||
List<V2TimGroupMemberFullInfo?>? groupMemberList;
|
||||
|
||||
TIMUIKitChat(
|
||||
{Key? key,
|
||||
this.groupID,
|
||||
required this.conversation,
|
||||
this.conversationID,
|
||||
this.conversationType,
|
||||
this.groupMemberList,
|
||||
this.conversationShowName,
|
||||
this.abstractMessageBuilder,
|
||||
this.onTapAvatar,
|
||||
@Deprecated(
|
||||
"Nickname will not show in one-to-one chat, if you tend to control it in group chat, please use `isShowSelfNameInGroup` and `isShowOthersNameInGroup` from `config: TIMUIKitChatConfig` instead")
|
||||
this.showNickName = false,
|
||||
this.showTotalUnReadCount = false,
|
||||
this.messageItemBuilder,
|
||||
@Deprecated("Please use [extraTipsActionItemBuilder] instead") this.exteraTipsActionItemBuilder,
|
||||
this.extraTipsActionItemBuilder,
|
||||
this.draftText,
|
||||
this.textFieldHintText,
|
||||
this.initFindingMsg,
|
||||
this.userAvatarBuilder,
|
||||
this.appBarConfig,
|
||||
this.controller,
|
||||
this.morePanelConfig,
|
||||
this.customStickerPanel,
|
||||
this.config = const TIMUIKitChatConfig(),
|
||||
this.tongueItemBuilder,
|
||||
this.groupAtInfoList,
|
||||
this.mainHistoryListConfig,
|
||||
this.onDealWithGroupApplication,
|
||||
this.toolTipsConfig,
|
||||
this.lifeCycle,
|
||||
this.topFixWidget = const SizedBox(),
|
||||
this.textFieldBuilder,
|
||||
this.customEmojiStickerList = const [],
|
||||
this.customAppBar,
|
||||
this.inputTopBuilder,
|
||||
this.onSecondaryTapAvatar,
|
||||
this.customMessageHoverBarOnDesktop})
|
||||
: super(key: key) {
|
||||
startTime = DateTime.now().millisecondsSinceEpoch;
|
||||
}
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _TUIChatState();
|
||||
}
|
||||
|
||||
class _TUIChatState extends TIMUIKitState<TIMUIKitChat> {
|
||||
TUIChatSeparateViewModel model = TUIChatSeparateViewModel();
|
||||
final TUISelfInfoViewModel selfInfoViewModel = serviceLocator<TUISelfInfoViewModel>();
|
||||
final TUIThemeViewModel themeViewModel = serviceLocator<TUIThemeViewModel>();
|
||||
final TUIConversationViewModel conversationViewModel = serviceLocator<TUIConversationViewModel>();
|
||||
TIMUIKitInputTextFieldController textFieldController = TIMUIKitInputTextFieldController();
|
||||
bool isInit = false;
|
||||
final TUIChatGlobalModel chatGlobalModel = serviceLocator<TUIChatGlobalModel>();
|
||||
bool _dragging = false;
|
||||
|
||||
final GlobalKey alignKey = GlobalKey();
|
||||
final GlobalKey listContainerKey = GlobalKey();
|
||||
|
||||
late AutoScrollController autoController = AutoScrollController(
|
||||
viewportBoundaryGetter: () => Rect.fromLTRB(0, 0, 0, MediaQuery.of(context).padding.bottom),
|
||||
axis: Axis.vertical,
|
||||
);
|
||||
|
||||
late AutoScrollController atMemberPanelScroll = AutoScrollController(
|
||||
viewportBoundaryGetter: () => Rect.fromLTRB(0, 0, 0, MediaQuery.of(context).padding.bottom),
|
||||
axis: Axis.vertical,
|
||||
);
|
||||
|
||||
Widget? _joinInGroupCallWidget;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (kProfileMode) {
|
||||
Frame.init();
|
||||
}
|
||||
model.abstractMessageBuilder = widget.abstractMessageBuilder;
|
||||
model.onTapAvatar = widget.onTapAvatar;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
widget.endTime = DateTime.now().millisecondsSinceEpoch;
|
||||
int timeSpend = widget.endTime - widget.startTime;
|
||||
outputLogger.i("Page render time:$timeSpend ms");
|
||||
});
|
||||
Future.delayed(const Duration(milliseconds: 500), () {
|
||||
updateDraft();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
if (kProfileMode) {
|
||||
Frame.destroy();
|
||||
}
|
||||
model.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(TIMUIKitChat oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.conversationID != oldWidget.conversationID) {
|
||||
isInit = false;
|
||||
chatGlobalModel.clearCurrentConversation();
|
||||
model = TUIChatSeparateViewModel();
|
||||
model.abstractMessageBuilder = widget.abstractMessageBuilder;
|
||||
model.onTapAvatar = widget.onTapAvatar;
|
||||
Future.delayed(const Duration(milliseconds: 50), () {
|
||||
updateDraft();
|
||||
textFieldController.requestFocus();
|
||||
try {
|
||||
autoController.jumpTo(
|
||||
autoController.position.minScrollExtent,
|
||||
);
|
||||
autoController.jumpTo(
|
||||
autoController.position.minScrollExtent,
|
||||
);
|
||||
// ignore: empty_catches
|
||||
} catch (e) {}
|
||||
});
|
||||
}
|
||||
if (oldWidget.textFieldBuilder != null && widget.textFieldBuilder == null) {
|
||||
textFieldController = TIMUIKitInputTextFieldController();
|
||||
}
|
||||
if (oldWidget.groupMemberList != widget.groupMemberList) {
|
||||
model.groupMemberList = widget.groupMemberList;
|
||||
}
|
||||
}
|
||||
|
||||
updateDraft() async {
|
||||
final isTopic = widget.conversation.conversationID.contains("@TOPIC#");
|
||||
if (isTopic) {
|
||||
final topicInfoList = await TencentImSDKPlugin.v2TIMManager
|
||||
.getGroupManager()
|
||||
.getTopicInfoList(groupID: widget.groupID!, topicIDList: [widget.conversation.conversationID]);
|
||||
final topicInfo = topicInfoList.data?.first.topicInfo;
|
||||
final draftText = topicInfo?.draftText;
|
||||
if (TencentUtils.checkString(draftText) != null) {
|
||||
textFieldController.setTextField(draftText!);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Widget _renderJoinGroupApplication(int amount, TUITheme theme) {
|
||||
String option1 = amount.toString();
|
||||
return Container(
|
||||
height: 36,
|
||||
decoration: BoxDecoration(color: hexToColor("f6eabc")),
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
if (widget.onDealWithGroupApplication != null) {
|
||||
widget.onDealWithGroupApplication!(_getConvID());
|
||||
}
|
||||
},
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
TIM_t_para("{{option1}} 条入群请求", "$option1 条入群请求")(option1: option1),
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
margin: const EdgeInsets.only(left: 12),
|
||||
child: Text(
|
||||
TIM_t("去处理"),
|
||||
style: TextStyle(fontSize: 12, color: theme.primaryColor),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _getTitle() {
|
||||
return TencentUtils.checkString(widget.conversationShowName) ?? widget.conversation.showName ?? "Chat";
|
||||
}
|
||||
|
||||
String _getConvID() {
|
||||
return TencentUtils.checkString(widget.conversationID) ??
|
||||
(widget.conversation.type == 1 ? widget.conversation.userID : widget.conversation.groupID) ??
|
||||
"";
|
||||
}
|
||||
|
||||
ConvType _getConvType() {
|
||||
return widget.conversation.type == 1 ? ConvType.c2c : ConvType.group;
|
||||
}
|
||||
|
||||
_updateJoinInGroupCallWidget() async {
|
||||
if (_getConvType() != ConvType.group) {
|
||||
return;
|
||||
}
|
||||
final w = await TUICore.instance.raiseExtension(TUIExtensionID.joinInGroup, {GROUP_ID: widget.conversationID!});
|
||||
if (w != _joinInGroupCallWidget) {
|
||||
setState(() {
|
||||
_joinInGroupCallWidget = w;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final TUITheme theme = value.theme;
|
||||
final isBuild = isInit;
|
||||
isInit = true;
|
||||
_updateJoinInGroupCallWidget();
|
||||
|
||||
return TIMUIKitChatProviderScope(
|
||||
model: model,
|
||||
groupID: widget.groupID,
|
||||
scrollController: autoController,
|
||||
textFieldController: textFieldController,
|
||||
conversationID: _getConvID(),
|
||||
groupMemberList: widget.groupMemberList,
|
||||
conversationType: _getConvType(),
|
||||
lifeCycle: widget.lifeCycle,
|
||||
config: widget.config,
|
||||
isBuild: isBuild,
|
||||
providers: [
|
||||
Provider(create: (_) => widget.config),
|
||||
],
|
||||
builder: (context, model, w) {
|
||||
final TUIChatGlobalModel chatGlobalModel = Provider.of<TUIChatGlobalModel>(context, listen: true);
|
||||
|
||||
widget.controller?.model = model;
|
||||
widget.controller?.textFieldController = textFieldController;
|
||||
widget.controller?.scrollController = autoController;
|
||||
List<V2TimGroupApplication> filteredApplicationList = [];
|
||||
if (widget.conversationType == ConvType.group && widget.onDealWithGroupApplication != null) {
|
||||
filteredApplicationList = chatGlobalModel.groupApplicationList.where((item) {
|
||||
return (item.groupID == widget.conversationID) && item.handleStatus == 0;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
final selfUserID = selfInfoViewModel.loginInfo?.userID;
|
||||
final TUIGroupListenerModel groupListenerModel = Provider.of<TUIGroupListenerModel>(context, listen: true);
|
||||
final NeedUpdate? needUpdate = groupListenerModel.needUpdate;
|
||||
if (needUpdate != null && needUpdate.groupID == widget.conversationID) {
|
||||
groupListenerModel.needUpdate = null;
|
||||
switch (needUpdate.updateType) {
|
||||
case UpdateType.groupInfo:
|
||||
model.loadGroupInfo(_getConvID());
|
||||
break;
|
||||
case UpdateType.memberList:
|
||||
if (widget.groupMemberList == null) {
|
||||
model.loadGroupMemberList(groupID: _getConvID());
|
||||
}
|
||||
model.loadGroupInfo(_getConvID());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
List<CustomEmojiFaceData> customImageSmallPngEmojiPackages = [];
|
||||
if (widget.config?.stickerPanelConfig?.customStickerPackages != null &&
|
||||
widget.config!.stickerPanelConfig!.customStickerPackages.isNotEmpty) {
|
||||
customImageSmallPngEmojiPackages = widget.config!.stickerPanelConfig!.customStickerPackages
|
||||
.where((element) => element.isEmoji == true)
|
||||
.map((e) {
|
||||
return CustomEmojiFaceData(
|
||||
name: e.name,
|
||||
isEmoji: true,
|
||||
icon: e.menuItem.url ?? "",
|
||||
list: e.stickerList.map((e) => e.url ?? "").toList());
|
||||
}).toList();
|
||||
}
|
||||
if (customImageSmallPngEmojiPackages.isEmpty) {
|
||||
customImageSmallPngEmojiPackages.addAll(widget.customEmojiStickerList);
|
||||
}
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
textFieldController.hideAllPanel();
|
||||
},
|
||||
child: Scaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
appBar: (widget.customAppBar == null)
|
||||
? TIMUIKitAppBar(
|
||||
showTotalUnReadCount: widget.showTotalUnReadCount,
|
||||
config: widget.appBarConfig,
|
||||
conversationShowName: _getTitle(),
|
||||
conversationID: _getConvID(),
|
||||
showC2cMessageEditStatus: widget.config?.showC2cMessageEditStatus ?? true,
|
||||
)
|
||||
: null,
|
||||
body: DropTarget(
|
||||
onDragDone: (detail) {
|
||||
setState(() {
|
||||
_dragging = false;
|
||||
sendFileWithConfirmation(
|
||||
files: detail.files,
|
||||
conversation: widget.conversation,
|
||||
conversationType: _getConvType(),
|
||||
model: model,
|
||||
theme: theme,
|
||||
context: context);
|
||||
});
|
||||
},
|
||||
onDragEntered: (detail) {
|
||||
setState(() {
|
||||
_dragging = true;
|
||||
});
|
||||
},
|
||||
onDragExited: (detail) {
|
||||
setState(() {
|
||||
_dragging = false;
|
||||
});
|
||||
},
|
||||
child: Stack(
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (widget.customAppBar != null) widget.customAppBar!,
|
||||
if (filteredApplicationList.isNotEmpty)
|
||||
_renderJoinGroupApplication(filteredApplicationList.length, theme),
|
||||
if (widget.topFixWidget != null) widget.topFixWidget!,
|
||||
if (_joinInGroupCallWidget != null) Center(child: _joinInGroupCallWidget!),
|
||||
Expanded(
|
||||
child: Container(
|
||||
color: theme.chatBgColor,
|
||||
child: Align(
|
||||
key: alignKey,
|
||||
alignment: Alignment.topCenter,
|
||||
child: Listener(
|
||||
child: TIMUIKitHistoryMessageListContainer(
|
||||
customMessageHoverBarOnDesktop: widget.customMessageHoverBarOnDesktop,
|
||||
conversation: widget.conversation,
|
||||
groupMemberInfo: model.groupMemberList
|
||||
?.firstWhere((element) => element?.userID == selfUserID, orElse: () => null),
|
||||
textFieldController: textFieldController,
|
||||
customEmojiStickerList: widget.customEmojiStickerList,
|
||||
key: listContainerKey,
|
||||
isAllowScroll: true,
|
||||
userAvatarBuilder: widget.userAvatarBuilder,
|
||||
toolTipsConfig: widget.toolTipsConfig,
|
||||
groupAtInfoList: widget.groupAtInfoList,
|
||||
tongueItemBuilder: widget.tongueItemBuilder,
|
||||
onLongPressForOthersHeadPortrait: (String? userId, String? nickName) {
|
||||
textFieldController.longPressToAt(nickName, userId);
|
||||
},
|
||||
mainHistoryListConfig: widget.mainHistoryListConfig,
|
||||
initFindingMsg: widget.initFindingMsg,
|
||||
extraTipsActionItemBuilder:
|
||||
widget.extraTipsActionItemBuilder ?? widget.exteraTipsActionItemBuilder,
|
||||
conversationType: _getConvType(),
|
||||
scrollController: autoController,
|
||||
onSecondaryTapAvatar: widget.onSecondaryTapAvatar,
|
||||
onTapAvatar: widget.onTapAvatar,
|
||||
// ignore: deprecated_member_use_from_same_package
|
||||
showNickName: widget.showNickName,
|
||||
messageItemBuilder: widget.messageItemBuilder,
|
||||
conversationID: _getConvID(),
|
||||
),
|
||||
)),
|
||||
)),
|
||||
widget.inputTopBuilder ?? Container(),
|
||||
Selector<TUIChatSeparateViewModel, bool>(
|
||||
builder: (context, value, child) {
|
||||
return value
|
||||
? MultiSelectPanel(
|
||||
conversationType: _getConvType(),
|
||||
)
|
||||
: (widget.textFieldBuilder != null
|
||||
? widget.textFieldBuilder!(context)
|
||||
: TIMUIKitInputTextField(
|
||||
chatConfig: widget.config,
|
||||
groupID: widget.groupID,
|
||||
atMemberPanelScroll: atMemberPanelScroll,
|
||||
groupType: widget.conversation.groupType,
|
||||
currentConversation: widget.conversation,
|
||||
model: model,
|
||||
controller: textFieldController,
|
||||
customEmojiStickerList: customImageSmallPngEmojiPackages,
|
||||
customStickerPanel: widget.customStickerPanel,
|
||||
morePanelConfig: widget.morePanelConfig,
|
||||
scrollController: autoController,
|
||||
conversationID: _getConvID(),
|
||||
conversationType: _getConvType(),
|
||||
initText: TencentUtils.checkString(widget.draftText) ??
|
||||
(PlatformUtils().isWeb
|
||||
? TencentUtils.checkString(conversationViewModel.getWebDraft(
|
||||
conversationID: widget.conversation.conversationID))
|
||||
: TencentUtils.checkString(widget.conversation.draftText)),
|
||||
hintText: widget.textFieldHintText,
|
||||
showMorePanel: widget.config?.isAllowShowMorePanel ?? true,
|
||||
showSendAudio: widget.config?.isAllowSoundMessage ?? true,
|
||||
showSendEmoji: widget.config?.isAllowEmojiPanel ?? true,
|
||||
));
|
||||
},
|
||||
selector: (c, model) {
|
||||
return model.isMultiSelect;
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
if (_dragging)
|
||||
TIMUIKitSendFile(
|
||||
conversation: widget.conversation,
|
||||
),
|
||||
AtMemberPanel(
|
||||
atMemberPanelScroll: atMemberPanelScroll,
|
||||
onSelectMember: (member) => textFieldController.handleAtMember(member),
|
||||
)
|
||||
],
|
||||
),
|
||||
)),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class TIMUIKitChatProviderScope extends StatelessWidget {
|
||||
final TUIChatGlobalModel globalModel = serviceLocator<TUIChatGlobalModel>();
|
||||
TUIChatSeparateViewModel? model;
|
||||
final TUIGroupListenerModel groupListenerModel = serviceLocator<TUIGroupListenerModel>();
|
||||
final TUIThemeViewModel themeViewModel = serviceLocator<TUIThemeViewModel>();
|
||||
final Widget? child;
|
||||
|
||||
/// You could get the model from here, and transfer it to other widget from TUIKit.
|
||||
final Widget Function(BuildContext, TUIChatSeparateViewModel, Widget?) builder;
|
||||
final List<SingleChildWidget>? providers;
|
||||
|
||||
/// `TIMUIKitChatController` needs to be provided if you use it outside.
|
||||
final TIMUIKitChatController? controller;
|
||||
|
||||
/// The global config for TIMUIKitChat.
|
||||
final TIMUIKitChatConfig? config;
|
||||
|
||||
/// Conversation id, use for get history message list.
|
||||
final String conversationID;
|
||||
|
||||
final String? groupID;
|
||||
|
||||
/// Conversation type
|
||||
final ConvType conversationType;
|
||||
|
||||
/// The life cycle for chat business logic.
|
||||
final ChatLifeCycle? lifeCycle;
|
||||
|
||||
/// The controller for text field.
|
||||
final TIMUIKitInputTextFieldController? textFieldController;
|
||||
|
||||
final bool? isBuild;
|
||||
|
||||
final AutoScrollController? scrollController;
|
||||
|
||||
/// An optional parameter `groupMemberList` can be provided.
|
||||
/// `groupMemberList` accepts a list of nullable `V2TimGroupMemberFullInfo` objects.
|
||||
/// The purpose of this parameter is to allow the client to supply a pre-fetched list
|
||||
/// of group member information. If this list is provided, it will not make
|
||||
/// additional network requests to fetch the group member information internally.
|
||||
List<V2TimGroupMemberFullInfo?>? groupMemberList;
|
||||
|
||||
TIMUIKitChatProviderScope(
|
||||
{Key? key,
|
||||
this.child,
|
||||
this.providers,
|
||||
this.groupMemberList,
|
||||
this.textFieldController,
|
||||
required this.builder,
|
||||
this.model,
|
||||
this.groupID,
|
||||
this.isBuild,
|
||||
required this.conversationID,
|
||||
required this.conversationType,
|
||||
this.controller,
|
||||
this.config,
|
||||
this.lifeCycle,
|
||||
this.scrollController})
|
||||
: super(key: key) {
|
||||
if (isBuild ?? false) {
|
||||
return;
|
||||
}
|
||||
model ??= TUIChatSeparateViewModel();
|
||||
controller?.model = model;
|
||||
controller?.textFieldController = textFieldController;
|
||||
controller?.scrollController = scrollController;
|
||||
if (config != null) {
|
||||
model?.chatConfig = config!;
|
||||
}
|
||||
model?.lifeCycle = lifeCycle;
|
||||
model?.initForEachConversation(
|
||||
conversationType,
|
||||
conversationID,
|
||||
(String value) {
|
||||
textFieldController?.textEditingController?.text = value;
|
||||
},
|
||||
preGroupMemberList: groupMemberList,
|
||||
groupID: groupID,
|
||||
);
|
||||
model?.showC2cMessageEditStatus =
|
||||
(conversationType == ConvType.c2c ? config?.showC2cMessageEditStatus ?? true : false);
|
||||
loadData();
|
||||
}
|
||||
|
||||
loadData() {
|
||||
// if (model!.haveMoreData) {
|
||||
model!.loadChatRecord(count: kIsWeb ? 15 : HistoryMessageDartConstant.getCount);
|
||||
// }
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider.value(value: model),
|
||||
ChangeNotifierProvider.value(value: globalModel),
|
||||
ChangeNotifierProvider.value(value: themeViewModel),
|
||||
ChangeNotifierProvider.value(value: groupListenerModel),
|
||||
Provider(create: (_) => const TIMUIKitChatConfig()),
|
||||
...?providers
|
||||
],
|
||||
child: child,
|
||||
builder: (context, w) => builder(context, model!, w),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,306 @@
|
||||
import 'package:tencent_cloud_chat_sdk/enum/offlinePushInfo.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_message.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_chat_global_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart';
|
||||
|
||||
import 'TIMUIKitTextField/tim_uikit_text_field_layout/wide.dart';
|
||||
|
||||
enum GroupReceptAllowType { work, public, meeting }
|
||||
|
||||
enum GroupReceiptAllowType { work, public, meeting }
|
||||
|
||||
enum UrlPreviewType { none, onlyHyperlink, previewCardAndHyperlink }
|
||||
|
||||
class TimeDividerConfig {
|
||||
/// Defines the interval of adding a time divider among the two messages.
|
||||
/// [Unit]: second.
|
||||
/// [Default]: 300.
|
||||
final int? timeInterval;
|
||||
|
||||
/// Defines the parser of a specific timestamp,
|
||||
/// transform it into a semantic time description.
|
||||
final String Function(int timeStamp)? timestampParser;
|
||||
|
||||
TimeDividerConfig({this.timeInterval, this.timestampParser});
|
||||
}
|
||||
|
||||
/// StickerPanelConfig is a configuration class for the sticker panel component.
|
||||
/// It allows customization of specific features such as display options for the
|
||||
/// message area, sticker packages, unicode emoji lists, and custom sticker packages.
|
||||
class StickerPanelConfig {
|
||||
/// Determines whether to use the QQ Sticker Package.
|
||||
/// Default value: true
|
||||
final bool useQQStickerPackage;
|
||||
|
||||
/// Determines whether to use the Tencent Cloud Chat Sticker Package.
|
||||
/// Default value: true
|
||||
final bool useTencentCloudChatStickerPackage;
|
||||
|
||||
/// Determines whether to compatible with the Tencent Cloud Chat Sticker Package 3.x version.
|
||||
/// Default value : false
|
||||
final bool useTencentCloudChatStickerPackageOldKeys;
|
||||
|
||||
/// A list of unicode emoji, represented as integers.
|
||||
/// Default value: a list of common Unicode Emojis.
|
||||
/// To exclude Unicode Emoji from the display, pass an empty list.
|
||||
final List<int> unicodeEmojiList;
|
||||
|
||||
/// A list of CustomStickerPackage instances, where each instance represents a sticker package.
|
||||
/// Default value: an empty list.
|
||||
final List<CustomStickerPackage> customStickerPackages;
|
||||
|
||||
StickerPanelConfig({
|
||||
this.useQQStickerPackage = true,
|
||||
this.useTencentCloudChatStickerPackage = true,
|
||||
this.useTencentCloudChatStickerPackageOldKeys = false,
|
||||
this.unicodeEmojiList = TUIKitStickerConstData.defaultUnicodeEmojiList,
|
||||
this.customStickerPackages = const [],
|
||||
});
|
||||
}
|
||||
|
||||
class TIMUIKitChatConfig {
|
||||
/// A StickerPanelConfig instance to configure the sticker panel's behavior and appearance.
|
||||
/// This includes options such as display settings, usage of specific sticker packages,
|
||||
/// unicode emoji lists, and custom sticker packages.
|
||||
final StickerPanelConfig? stickerPanelConfig;
|
||||
|
||||
/// Customize the time divider among the two messages.
|
||||
final TimeDividerConfig? timeDividerConfig;
|
||||
|
||||
/// Control if allowed to show reading status.
|
||||
/// [Default]: true.
|
||||
final bool isShowReadingStatus;
|
||||
|
||||
/// Control if allowed to show reading status for group.
|
||||
/// [Default]: true.
|
||||
/// [Deprecated: ] Please use [isShowReadingStatus] instead.
|
||||
final bool isShowGroupReadingStatus;
|
||||
|
||||
/// Control if allowed to report reading status for group.
|
||||
/// [Default]: true.
|
||||
/// [Deprecated: ] Please use [isShowReadingStatus] instead.
|
||||
final bool isReportGroupReadingStatus;
|
||||
|
||||
/// Control if allowed to show the message operation menu after long pressing message.
|
||||
/// [Default]: true.
|
||||
final bool isAllowLongPressMessage;
|
||||
|
||||
/// Control if allowed to callback after clicking the avatar.
|
||||
/// [Default]: true.
|
||||
final bool isAllowClickAvatar;
|
||||
|
||||
/// Control if allowed to show emoji face message panel.
|
||||
/// [Default]: true.
|
||||
final bool isAllowEmojiPanel;
|
||||
|
||||
/// Control if allowed to show more plus panel.
|
||||
/// [Default]: true.
|
||||
final bool isAllowShowMorePanel;
|
||||
|
||||
/// Control if allowed to send voice sound message.
|
||||
/// [Default]: true.
|
||||
final bool isAllowSoundMessage;
|
||||
|
||||
/// Control if allowed to at when reply automatically.
|
||||
/// [Default]: true.
|
||||
final bool isAtWhenReply;
|
||||
|
||||
/// Control if allowed to at when reply automatically.
|
||||
/// [Default]: true.
|
||||
final bool Function(V2TimMessage message)? isAtWhenReplyDynamic;
|
||||
|
||||
/// The main switch of the group read receipt.
|
||||
/// [Deprecated: ] Please use [isShowReadingStatus] instead.
|
||||
final bool isShowGroupMessageReadReceipt;
|
||||
|
||||
/// [Deprecated: ] not support.
|
||||
final List<GroupReceptAllowType>? groupReadReceiptPermisionList;
|
||||
|
||||
/// Control which group can send message read receipt.
|
||||
/// [Deprecated: ] not support.
|
||||
final List<GroupReceiptAllowType>? groupReadReceiptPermissionList;
|
||||
|
||||
/// Control if show self name in group chat.
|
||||
/// [Default]: false.
|
||||
final bool isShowSelfNameInGroup;
|
||||
|
||||
/// Control if others name in group chat.
|
||||
/// [Default]: true.
|
||||
final bool isShowOthersNameInGroup;
|
||||
|
||||
/// Configuration for offline push.
|
||||
/// If this field is specified, `notificationTitle`, `notificationOPPOChannelID`, `notificationIOSSound`, `notificationAndroidSound`, `notificationBody` and `notificationExt` will not work.
|
||||
final OfflinePushInfo? Function(V2TimMessage message, String convID, ConvType convType)? offlinePushInfo;
|
||||
|
||||
/// The title shows in push notification
|
||||
final String notificationTitle;
|
||||
|
||||
/// The channel ID for OPPO in push notification.
|
||||
final String notificationOPPOChannelID;
|
||||
|
||||
/// The notification sound in iOS devices.
|
||||
/// When `iOSSound` = `kIOSOfflinePushNoSound`, the sound will not play when message received. When `iOSSound` = `kIOSOfflinePushDefaultSound`, the system sound is played when message received. If you want to customize `iOSSound`, you need to link the voice file into the Xcode project, and then set the voice file name (with a suffix) to iOSSound.
|
||||
final String notificationIOSSound;
|
||||
|
||||
/// The notification sound in Android devices.
|
||||
final String notificationAndroidSound;
|
||||
|
||||
///Used to set the line height of text messages
|
||||
final double textHeight;
|
||||
|
||||
/// The body content shows in push notification.
|
||||
/// Returning `null` means using default body in this case.
|
||||
final String? Function(V2TimMessage message, String convID, ConvType convType)? notificationBody;
|
||||
|
||||
/// External information (String) for notification message, recommend used for jumping to target conversation with JSON format,
|
||||
/// Returning `null` means using default ext in this case.
|
||||
final String? Function(V2TimMessage message, String convID, ConvType convType)? notificationExt;
|
||||
|
||||
/// The type of URL preview level, none preview, only hyperlink in text, or shows a preview card for website.
|
||||
/// [Default]: UrlPreviewType.previewCardAndHyperlink.
|
||||
final UrlPreviewType urlPreviewType;
|
||||
|
||||
/// Whether to display the sending status of c2c messages
|
||||
/// [Default]: true.
|
||||
final bool showC2cMessageEditStatus;
|
||||
|
||||
/// Control if take emoji stickers as message reaction.
|
||||
/// [Default]: true.
|
||||
final bool isUseMessageReaction;
|
||||
|
||||
/// Determine how long a message is allowed to be recalled after it is sent.
|
||||
/// You must modify the configuration on control dashboard synchronized at: https://console.cloud.tencent.com/im/login-message.
|
||||
/// [Unit]: second.
|
||||
/// [Default]: 120.
|
||||
final int upperRecallTime;
|
||||
|
||||
/// The prefix of face sticker URI.
|
||||
final String Function(String data)? faceURIPrefix;
|
||||
|
||||
/// The suffix of face sticker URI.
|
||||
final String Function(String data)? faceURISuffix;
|
||||
|
||||
/// Controls whether text and replied messages can be displayed with Markdown formatting.
|
||||
/// Also, when enabled, `isEnableTextSelection` will not works.
|
||||
/// [Default]: false.
|
||||
final bool isSupportMarkdownForTextMessage;
|
||||
|
||||
/// The callback after user clicking the URL link in text messages.
|
||||
/// The default action is opening the link with the default browser of system.
|
||||
final void Function(String url)? onTapLink;
|
||||
|
||||
/// Whether shows avatar on history message list.
|
||||
/// [Default]: true.
|
||||
final bool isShowAvatar;
|
||||
|
||||
/// This list contains additional operation items that are displayed on the hover bar
|
||||
/// of a message on desktop (macOS, Windows, and desktop version of Web). These items
|
||||
/// are in addition to the default ones and do not affect them.
|
||||
final List<MessageHoverControlItem>? additionalDesktopMessageHoverBarItem;
|
||||
|
||||
/// This list contains additional items that are displayed
|
||||
/// on the message sending area control bar on desktop (macOS, Windows, and desktop version of Web).
|
||||
/// Use `desktopControlBarConfig` to configure whether or not to show the default control items.
|
||||
final List<DesktopControlBarItem>? additionalDesktopControlBarItems;
|
||||
|
||||
/// This configuration is used for the control bar
|
||||
/// on desktop (macOS, Windows, and desktop version of Web).
|
||||
/// Use `desktopControlBarConfig` to add additional items to the desktop message sending area control bar, in addition to the default ones.
|
||||
final DesktopControlBarConfig? desktopControlBarConfig;
|
||||
|
||||
/// Controls whether users are allowed to mention another user in the group by long-pressing on their avatar.
|
||||
/// [Default]: true.
|
||||
final bool isAllowLongPressAvatarToAt;
|
||||
|
||||
/// Controls whether auto report message read status when new messages come.
|
||||
/// [Default]: true.
|
||||
final bool isAutoReportRead;
|
||||
|
||||
/// Controls whether enable text selection.
|
||||
/// [Default]: true on Desktop while false on Mobile.
|
||||
final bool? isEnableTextSelection;
|
||||
|
||||
/// Controls whether enable the control bar shows when hovering a message on Desktop.
|
||||
/// [Default]: true.
|
||||
final bool isUseMessageHoverBarOnDesktop;
|
||||
|
||||
/// Define the lines in the text message input field on Desktop.
|
||||
final int desktopMessageInputFieldLines;
|
||||
|
||||
/// Specifies whether to use the draft feature on the Web, as the Chat SDK does not support this functionality.
|
||||
/// If enabled, draft data will be stored in TUIKit's memory.
|
||||
/// Note that the draft text will be lost upon refreshing the website.
|
||||
/// [Default]: true.
|
||||
final bool isUseDraftOnWeb;
|
||||
|
||||
/// Determines whether a group administrator is allowed to recall any
|
||||
/// message from any group member. If this capability is enabled,
|
||||
/// recalled messages will not be interoperable with Native clients
|
||||
/// and will only take effect on other Flutter clients.
|
||||
///
|
||||
/// [Default]: false
|
||||
final bool isGroupAdminRecallEnabled;
|
||||
|
||||
/// Defines the height of the sticker panel on desktop platforms.
|
||||
/// If the height of the sticker list exceeds this container height,
|
||||
/// the sticker list will automatically become scrollable.
|
||||
///
|
||||
/// [Default]: 400
|
||||
final double desktopStickerPanelHeight;
|
||||
|
||||
/// Determine whether the normal members can @All in a group chat.
|
||||
/// If enabled, normal members can @All in a group chat.
|
||||
/// If disabled, only the group owner or administrators can @All.
|
||||
///
|
||||
/// [Default]: false
|
||||
final bool isMemberCanAtAll;
|
||||
|
||||
const TIMUIKitChatConfig(
|
||||
{this.onTapLink,
|
||||
this.timeDividerConfig,
|
||||
this.desktopStickerPanelHeight = 400,
|
||||
this.stickerPanelConfig,
|
||||
this.isGroupAdminRecallEnabled = false,
|
||||
this.isAutoReportRead = true,
|
||||
this.faceURIPrefix,
|
||||
this.faceURISuffix,
|
||||
this.textHeight = 1.3,
|
||||
this.desktopMessageInputFieldLines = 6,
|
||||
this.isAtWhenReply = true,
|
||||
this.notificationAndroidSound = "",
|
||||
this.isUseMessageHoverBarOnDesktop = true,
|
||||
this.isSupportMarkdownForTextMessage = false,
|
||||
this.notificationExt,
|
||||
this.isUseMessageReaction = true,
|
||||
this.isShowAvatar = true,
|
||||
this.isShowSelfNameInGroup = false,
|
||||
this.isAtWhenReplyDynamic,
|
||||
this.offlinePushInfo,
|
||||
@Deprecated("Please use [isShowReadingStatus] instead") this.isShowGroupMessageReadReceipt = true,
|
||||
this.upperRecallTime = 120,
|
||||
this.isShowOthersNameInGroup = true,
|
||||
this.urlPreviewType = UrlPreviewType.onlyHyperlink,
|
||||
this.notificationBody,
|
||||
this.notificationOPPOChannelID = "",
|
||||
this.notificationTitle = "",
|
||||
this.notificationIOSSound = "",
|
||||
this.isAllowSoundMessage = true,
|
||||
@Deprecated("not support") this.groupReadReceiptPermisionList,
|
||||
@Deprecated("not support") this.groupReadReceiptPermissionList,
|
||||
this.isAllowEmojiPanel = true,
|
||||
this.isAllowShowMorePanel = true,
|
||||
this.isShowReadingStatus = true,
|
||||
this.desktopControlBarConfig,
|
||||
this.isAllowLongPressMessage = true,
|
||||
this.isUseDraftOnWeb = true,
|
||||
this.isAllowClickAvatar = true,
|
||||
this.isEnableTextSelection,
|
||||
this.additionalDesktopMessageHoverBarItem,
|
||||
this.isShowGroupReadingStatus = true,
|
||||
@Deprecated("Please use [isShowReadingStatus] instead") this.isReportGroupReadingStatus = true,
|
||||
this.showC2cMessageEditStatus = true,
|
||||
this.additionalDesktopControlBarItems,
|
||||
this.isAllowLongPressAvatarToAt = true,
|
||||
this.isMemberCanAtAll = false});
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart';
|
||||
|
||||
class MessageRepliedData {
|
||||
late String messageAbstract;
|
||||
late String messageSender;
|
||||
late String messageID;
|
||||
|
||||
MessageRepliedData.fromJson(Map messageReply) {
|
||||
messageAbstract = messageReply["messageAbstract"];
|
||||
messageSender = messageReply["messageSender"] ?? "";
|
||||
messageID = messageReply["messageID"];
|
||||
}
|
||||
}
|
||||
|
||||
class RepliedMessageAbstract {
|
||||
final int? elemType;
|
||||
final String? msgID;
|
||||
final int? timestamp;
|
||||
final String? seq;
|
||||
final String? summary;
|
||||
|
||||
RepliedMessageAbstract(
|
||||
{this.elemType, this.msgID, this.timestamp, this.seq, this.summary});
|
||||
|
||||
// fromJson constructor
|
||||
RepliedMessageAbstract.fromJson(Map<String, dynamic> json)
|
||||
: elemType = json['elemType'],
|
||||
msgID = json['msgID'],
|
||||
timestamp = json['timestamp'],
|
||||
seq = json['seq'],
|
||||
summary = json['summary'];
|
||||
|
||||
// toJson function
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'summary': summary,
|
||||
'elemType': elemType,
|
||||
'msgID': msgID,
|
||||
'timestamp': timestamp,
|
||||
'seq': seq,
|
||||
};
|
||||
}
|
||||
|
||||
// isNotEmpty method
|
||||
bool get isNotEmpty =>
|
||||
TencentUtils.checkString(msgID) != null &&
|
||||
TencentUtils.checkString(timestamp.toString()) != null &&
|
||||
TencentUtils.checkString(seq) != null;
|
||||
}
|
||||
|
||||
class CloudCustomData {
|
||||
Map<String, dynamic>? messageReply;
|
||||
Map<String, dynamic>? messageReaction = {};
|
||||
|
||||
CloudCustomData.fromJson(Map jsonMap) {
|
||||
messageReply = jsonMap["messageReply"];
|
||||
messageReaction = jsonMap["messageReaction"] ?? {};
|
||||
}
|
||||
|
||||
Map<String, Map?> toMap() {
|
||||
final Map<String, Map?> data = {};
|
||||
if (messageReply != null) {
|
||||
data['messageReply'] = messageReply;
|
||||
}
|
||||
data['messageReaction'] = messageReaction ?? {};
|
||||
return data;
|
||||
}
|
||||
|
||||
CloudCustomData();
|
||||
}
|
||||
@@ -0,0 +1,317 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:tencent_chat_i18n_tool/tencent_chat_i18n_tool.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/enum/message_status.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_statelesswidget.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_chat_separate_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_chat_global_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/core/tim_uikit_wide_modal_operation_key.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/forward_message_screen.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/wide_popup.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_callback.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/color.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/tui_theme.dart';
|
||||
|
||||
class MultiSelectPanel extends TIMUIKitStatelessWidget {
|
||||
final int forwardMsgNumLimit = 30;
|
||||
|
||||
final ConvType conversationType;
|
||||
|
||||
MultiSelectPanel({Key? key, required this.conversationType})
|
||||
: super(key: key);
|
||||
|
||||
_handleForwardMessage(BuildContext context, bool isMergerForward,
|
||||
TUIChatSeparateViewModel model) {
|
||||
|
||||
// 是否有选中消息
|
||||
if (model.getSelectedMessageList().isEmpty) {
|
||||
onTIMCallback(TIMCallback(
|
||||
type: TIMCallbackType.INFO,
|
||||
infoRecommendText: TIM_t("请选择要操作的消息!")));
|
||||
return;
|
||||
}
|
||||
|
||||
for (var v2TimMessage in model.getSelectedMessageList()) {
|
||||
// 失败消息不支持转发
|
||||
if (v2TimMessage.status == MessageStatus.V2TIM_MSG_STATUS_SEND_FAIL) {
|
||||
onTIMCallback(TIMCallback(
|
||||
type: TIMCallbackType.INFO,
|
||||
infoRecommendText: TIM_t("发送失败消息不支持转发!")));
|
||||
return;
|
||||
}
|
||||
|
||||
// 投票消息不支持转发
|
||||
if (model.isVoteMessage(v2TimMessage)) {
|
||||
onTIMCallback(TIMCallback(
|
||||
type: TIMCallbackType.INFO,
|
||||
infoRecommendText: TIM_t("投票消息不支持转发!")));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 逐条转发限制在 30 条以内
|
||||
if (!isMergerForward && model.getSelectedMessageList().length > forwardMsgNumLimit) {
|
||||
_showForwardLimitDialog(context);
|
||||
return;
|
||||
}
|
||||
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => ForwardMessageScreen(
|
||||
model: model,
|
||||
isMergerForward: isMergerForward,
|
||||
conversationType: conversationType,
|
||||
)));
|
||||
}
|
||||
|
||||
// 弹出逐条转发超限的对话框
|
||||
Future<bool?> _showForwardLimitDialog(BuildContext context) {
|
||||
return showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return CupertinoAlertDialog(
|
||||
title: Text(TIM_t("转发消息过多,暂不支持逐条转发")),
|
||||
actions: [
|
||||
CupertinoDialogAction(
|
||||
child: Text(TIM_t("确定")),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(true);
|
||||
},
|
||||
),
|
||||
CupertinoDialogAction(
|
||||
child: Text(TIM_t("取消")),
|
||||
isDestructiveAction: true,
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
_handleForwardMessageWide(BuildContext context, bool isMergerForward,
|
||||
TUIChatSeparateViewModel model) {
|
||||
TUIKitWidePopup.showPopupWindow(
|
||||
operationKey: TUIKitWideModalOperationKey.forward,
|
||||
context: context,
|
||||
isDarkBackground: false,
|
||||
title: TIM_t("转发"),
|
||||
submitWidget: Text(TIM_t("发送")),
|
||||
width: MediaQuery.of(context).size.width * 0.5,
|
||||
height: MediaQuery.of(context).size.height * 0.8,
|
||||
onSubmit: (){
|
||||
forwardMessageScreenKey.currentState?.handleForwardMessage();
|
||||
},
|
||||
child: (onClose) => Container(
|
||||
padding: const EdgeInsets.symmetric( horizontal: 10),
|
||||
child: ForwardMessageScreen(
|
||||
model: model,
|
||||
key: forwardMessageScreenKey,
|
||||
onClose: onClose,
|
||||
isMergerForward: isMergerForward,
|
||||
conversationType: conversationType,
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final TUITheme theme = value.theme;
|
||||
final TUIChatSeparateViewModel model =
|
||||
Provider.of<TUIChatSeparateViewModel>(context);
|
||||
|
||||
return TUIKitScreenUtils.getDeviceWidget(
|
||||
context: context,
|
||||
desktopWidget: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: theme.selectPanelBgColor ?? theme.primaryColor,
|
||||
border: Border(
|
||||
top: BorderSide(
|
||||
color: theme.weakDividerColor ?? Colors.grey,
|
||||
width: 1.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(vertical: 32),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Wrap(
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
alignment: WrapAlignment.center,
|
||||
spacing: 64,
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: Image.asset('images/forward.png',
|
||||
package: 'tencent_cloud_chat_uikit',
|
||||
color: theme.selectPanelTextIconColor),
|
||||
iconSize: 30,
|
||||
onPressed: () {
|
||||
_handleForwardMessageWide(context, false, model);
|
||||
},
|
||||
),
|
||||
Text(TIM_t("逐条转发"),
|
||||
style: TextStyle(
|
||||
color: hexToColor("646a73"), fontSize: 12))
|
||||
],
|
||||
),
|
||||
Column(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: Image.asset('images/merge_forward.png',
|
||||
package: 'tencent_cloud_chat_uikit',
|
||||
color: theme.selectPanelTextIconColor),
|
||||
iconSize: 30,
|
||||
onPressed: () {
|
||||
_handleForwardMessageWide(context, true, model);
|
||||
},
|
||||
),
|
||||
Text(
|
||||
TIM_t("合并转发"),
|
||||
style:
|
||||
TextStyle(color: theme.selectPanelTextIconColor, fontSize: 12),
|
||||
)
|
||||
],
|
||||
),
|
||||
Column(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: Image.asset('images/delete.png',
|
||||
package: 'tencent_cloud_chat_uikit',
|
||||
color: theme.selectPanelTextIconColor),
|
||||
iconSize: 30,
|
||||
onPressed: () {
|
||||
TUIKitWidePopup.showSecondaryConfirmDialog(
|
||||
operationKey: TUIKitWideModalOperationKey.confirmDeleteMessages,
|
||||
context: context,
|
||||
text: TIM_t("确定删除已选消息"),
|
||||
theme: theme,
|
||||
onCancel: () {},
|
||||
onConfirm: () async {
|
||||
model.deleteSelectedMsg();
|
||||
model.updateMultiSelectStatus(false);
|
||||
});
|
||||
},
|
||||
),
|
||||
Text(TIM_t("删除"),
|
||||
style: TextStyle(
|
||||
color: theme.selectPanelTextIconColor, fontSize: 12))
|
||||
],
|
||||
),
|
||||
InkWell(
|
||||
onTap: (){
|
||||
model.updateMultiSelectStatus(false);
|
||||
},
|
||||
child: Icon(Icons.close, color: theme.darkTextColor,),
|
||||
)
|
||||
],
|
||||
))
|
||||
],
|
||||
),
|
||||
),
|
||||
defaultWidget: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
top: BorderSide(
|
||||
color: theme.weakDividerColor ??
|
||||
CommonColor.weakDividerColor)),
|
||||
color: theme.selectPanelBgColor ?? theme.primaryColor,
|
||||
),
|
||||
padding: const EdgeInsets.only(top: 12, bottom: 48),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: Image.asset('images/forward.png',
|
||||
package: 'tencent_cloud_chat_uikit', color: theme.selectPanelTextIconColor),
|
||||
iconSize: 40,
|
||||
onPressed: () {
|
||||
_handleForwardMessage(context, false, model);
|
||||
},
|
||||
),
|
||||
Text(TIM_t("逐条转发"),
|
||||
style: TextStyle(color: theme.selectPanelTextIconColor, fontSize: 12))
|
||||
],
|
||||
),
|
||||
Column(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: Image.asset('images/merge_forward.png',
|
||||
package: 'tencent_cloud_chat_uikit', color: theme.selectPanelTextIconColor),
|
||||
iconSize: 40,
|
||||
onPressed: () {
|
||||
_handleForwardMessage(context, true, model);
|
||||
},
|
||||
),
|
||||
Text(
|
||||
TIM_t("合并转发"),
|
||||
style: TextStyle(color: theme.selectPanelTextIconColor, fontSize: 12),
|
||||
)
|
||||
],
|
||||
),
|
||||
Column(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: Image.asset('images/delete.png',
|
||||
package: 'tencent_cloud_chat_uikit', color: theme.selectPanelTextIconColor),
|
||||
iconSize: 40,
|
||||
onPressed: () {
|
||||
showCupertinoModalPopup<String>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return CupertinoActionSheet(
|
||||
title: Text(TIM_t("确定删除已选消息")),
|
||||
cancelButton: CupertinoActionSheetAction(
|
||||
onPressed: () {
|
||||
Navigator.pop(
|
||||
context,
|
||||
"cancel",
|
||||
);
|
||||
},
|
||||
child: Text(TIM_t("取消")),
|
||||
isDefaultAction: false,
|
||||
),
|
||||
actions: [
|
||||
CupertinoActionSheetAction(
|
||||
onPressed: () {
|
||||
model.deleteSelectedMsg();
|
||||
model.updateMultiSelectStatus(false);
|
||||
Navigator.pop(
|
||||
context,
|
||||
"cancel",
|
||||
);
|
||||
},
|
||||
child: Text(
|
||||
TIM_t("删除"),
|
||||
style: TextStyle(color: theme.cautionColor),
|
||||
),
|
||||
isDefaultAction: false,
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
Text(TIM_t("删除"),
|
||||
style: TextStyle(color: theme.selectPanelTextIconColor, fontSize: 12))
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
import 'dart:io';
|
||||
import 'package:dotted_border/dotted_border.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:tencent_chat_i18n_tool/tencent_chat_i18n_tool.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_conversation.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_statelesswidget.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_chat_separate_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_chat_global_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/core/tim_uikit_wide_modal_operation_key.dart';
|
||||
import 'package:cross_file/cross_file.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/message.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/platform.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/wide_popup.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/tui_theme.dart';
|
||||
import 'TIMUIKitMessageItem/tim_uikit_chat_file_icon.dart';
|
||||
|
||||
String _getConvID(V2TimConversation conversation) {
|
||||
return (conversation.type == 1 ? conversation.userID : conversation.groupID) ?? "";
|
||||
}
|
||||
|
||||
sendFileWithConfirmation(
|
||||
{required List<XFile> files,
|
||||
required V2TimConversation conversation,
|
||||
required ConvType conversationType,
|
||||
required TUIChatSeparateViewModel model,
|
||||
required TUITheme theme,
|
||||
required BuildContext context}) async {
|
||||
bool isCanSend = true;
|
||||
|
||||
if (!PlatformUtils().isWeb) {
|
||||
files.map((e) => e.path).any((filePath) {
|
||||
final directory = Directory(filePath);
|
||||
final isDirectoryExists = directory.existsSync();
|
||||
if (isDirectoryExists) {
|
||||
isCanSend = false;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
} else {
|
||||
files.map((e) => e.name).any((fileName) {
|
||||
String fileExtension = path.extension(fileName);
|
||||
bool hasNoExtension = fileExtension.isEmpty;
|
||||
if (hasNoExtension) {
|
||||
isCanSend = false;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
if (!isCanSend) {
|
||||
TUIKitWidePopup.showSecondaryConfirmDialog(
|
||||
text: TIM_t("无法发送,包含文件夹"),
|
||||
onConfirm: () {},
|
||||
operationKey: TUIKitWideModalOperationKey.unableToSendDueToFolders,
|
||||
context: context,
|
||||
theme: theme);
|
||||
return;
|
||||
}
|
||||
|
||||
final option1 = conversation.showName ?? (conversationType == ConvType.group ? TIM_t("群聊") : TIM_t("对方"));
|
||||
TUIKitWidePopup.showPopupWindow(
|
||||
operationKey: TUIKitWideModalOperationKey.beforeSendScreenShot,
|
||||
context: context,
|
||||
isDarkBackground: false,
|
||||
width: 600,
|
||||
height: files.length < 4 ? 300 : 500,
|
||||
title: TIM_t_para("发送给{{option1}}", "发送给$option1")(option1: option1),
|
||||
child: (closeFunc) => Container(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Scrollbar(
|
||||
child: ListView.separated(
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final file = files[index];
|
||||
final fileName = PlatformUtils().isWeb ? file.name : path.basename(file.path);
|
||||
return Material(
|
||||
color: theme.wideBackgroundColor,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
launchUrl(Uri.file(file.path));
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 20),
|
||||
child: Row(
|
||||
children: [
|
||||
TIMUIKitFileIcon(
|
||||
size: 44,
|
||||
fileFormat: fileName.split(".")[fileName.split(".").length - 1],
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Text(
|
||||
fileName,
|
||||
style: TextStyle(fontSize: 16, color: theme.darkTextColor),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
separatorBuilder: (BuildContext context, int index) {
|
||||
return Divider(
|
||||
height: 1,
|
||||
thickness: 1,
|
||||
color: theme.weakDividerColor,
|
||||
);
|
||||
},
|
||||
itemCount: files.length,
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 16, top: 16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
OutlinedButton(
|
||||
onPressed: () {
|
||||
closeFunc();
|
||||
},
|
||||
child: Text(TIM_t("取消"))),
|
||||
const SizedBox(
|
||||
width: 20,
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
sendFiles(files, model, conversation, conversationType, context);
|
||||
closeFunc();
|
||||
},
|
||||
child: Text(TIM_t("发送")))
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
Future<void> sendFiles(List<XFile> files, TUIChatSeparateViewModel model, V2TimConversation conversation,
|
||||
ConvType conversationType, BuildContext context) async {
|
||||
for (final file in files) {
|
||||
final fileName = file.name;
|
||||
final filePath = file.path;
|
||||
await MessageUtils.handleMessageError(
|
||||
model.sendFileMessage(
|
||||
fileName: fileName, filePath: filePath, convID: _getConvID(conversation), convType: conversationType),
|
||||
context);
|
||||
await Future.delayed(const Duration(microseconds: 300));
|
||||
}
|
||||
}
|
||||
|
||||
class TIMUIKitSendFile extends TIMUIKitStatelessWidget {
|
||||
final V2TimConversation conversation;
|
||||
|
||||
TIMUIKitSendFile({required this.conversation, Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final theme = value.theme;
|
||||
final conversationType = conversation.type;
|
||||
final option1 = conversation.showName ?? (conversationType == 2 ? TIM_t("群聊") : TIM_t("会话"));
|
||||
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Opacity(
|
||||
opacity: 0.85,
|
||||
child: Container(
|
||||
color: theme.wideBackgroundColor,
|
||||
padding: const EdgeInsets.all(40),
|
||||
child: DottedBorder(
|
||||
borderType: BorderType.RRect,
|
||||
radius: const Radius.circular(20),
|
||||
color: theme.primaryColor ?? theme.weakTextColor!,
|
||||
dashPattern: const [6, 3],
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.file_copy_outlined,
|
||||
size: 60,
|
||||
color: theme.primaryColor,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 40,
|
||||
),
|
||||
Text(
|
||||
TIM_t_para("发送给{{option1}}", "发送给$option1")(option1: option1),
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: theme.darkTextColor),
|
||||
)
|
||||
],
|
||||
))
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
))
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_friend_info.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_friend_info.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/life_cycle/friend_list_life_cycle.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_friendship_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/services_locatar.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart';
|
||||
|
||||
export 'package:tencent_cloud_chat_uikit/ui/widgets/contact_list.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart';
|
||||
|
||||
class TIMUIKitContact extends StatefulWidget {
|
||||
/// the callback after clicking contact item
|
||||
final void Function(V2TimFriendInfo item)? onTapItem;
|
||||
|
||||
/// the list on top
|
||||
final List<TopListItem>? topList;
|
||||
|
||||
/// the builder for the list on top
|
||||
final Widget? Function(TopListItem item)? topListItemBuilder;
|
||||
|
||||
/// The widget shows when no contacts exists.
|
||||
final Widget Function(BuildContext context)? emptyBuilder;
|
||||
|
||||
/// the life cycle hooks for friend list or contacts list business logic
|
||||
final FriendListLifeCycle? lifeCycle;
|
||||
|
||||
/// Control if shows the online status for each user on its avatar.
|
||||
final bool isShowOnlineStatus;
|
||||
|
||||
const TIMUIKitContact(
|
||||
{Key? key,
|
||||
this.onTapItem,
|
||||
this.lifeCycle,
|
||||
this.topList,
|
||||
this.topListItemBuilder,
|
||||
this.emptyBuilder,
|
||||
this.isShowOnlineStatus = true})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _TIMUIKitContactState();
|
||||
}
|
||||
|
||||
class _TIMUIKitContactState extends TIMUIKitState<TIMUIKitContact> {
|
||||
final TUIFriendShipViewModel model = serviceLocator<TUIFriendShipViewModel>();
|
||||
String currentItem = "";
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final theme = value.theme;
|
||||
final isDesktopScreen = TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop;
|
||||
return MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider.value(value: model),
|
||||
],
|
||||
builder: (context, w) {
|
||||
final model = Provider.of<TUIFriendShipViewModel>(context);
|
||||
model.contactListLifeCycle = widget.lifeCycle;
|
||||
final memberList = model.friendList ?? [];
|
||||
|
||||
return ContactList(
|
||||
currentItem: currentItem,
|
||||
emptyBuilder: widget.emptyBuilder,
|
||||
isShowOnlineStatus: widget.isShowOnlineStatus,
|
||||
contactList: memberList,
|
||||
onTapItem: (item) {
|
||||
if (isDesktopScreen) {
|
||||
setState(() {
|
||||
currentItem = item.userID;
|
||||
});
|
||||
}
|
||||
if (widget.onTapItem != null) {
|
||||
widget.onTapItem!(item);
|
||||
}
|
||||
},
|
||||
bgColor: isDesktopScreen ? theme.wideBackgroundColor : null,
|
||||
topList: widget.topList,
|
||||
topListItemBuilder: widget.topListItemBuilder,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// ignore_for_file: camel_case_types
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_statelesswidget.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_conversation_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/services_locatar.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/unread_message.dart';
|
||||
|
||||
typedef unreadCountBuilder = Widget Function(int unreadCount);
|
||||
|
||||
class TIMUIKitConversationTotalUnread extends TIMUIKitStatelessWidget {
|
||||
final TUIConversationViewModel model =
|
||||
serviceLocator<TUIConversationViewModel>();
|
||||
final int? unreadCount;
|
||||
final unreadCountBuilder? builder;
|
||||
final double? width;
|
||||
final double? height;
|
||||
|
||||
TIMUIKitConversationTotalUnread(
|
||||
{this.width = 22.0,
|
||||
this.height = 22.0,
|
||||
this.unreadCount,
|
||||
this.builder,
|
||||
Key? key})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
return MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider.value(value: model),
|
||||
],
|
||||
child: Consumer<TUIConversationViewModel>(
|
||||
builder: (context, value, child) {
|
||||
if (value.totalUnReadCount == 0) {
|
||||
return Container();
|
||||
}
|
||||
|
||||
if (builder != null) {
|
||||
return builder!(value.totalUnReadCount);
|
||||
}
|
||||
return UnreadMessage(
|
||||
unreadCount: unreadCount ?? value.totalUnReadCount,
|
||||
width: width,
|
||||
height: height);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,452 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_easyrefresh/easy_refresh.dart';
|
||||
import 'package:flutter_slidable_plus_plus/flutter_slidable_plus_plus.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:scroll_to_index/scroll_to_index.dart';
|
||||
import 'package:tencent_chat_i18n_tool/tencent_chat_i18n_tool.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_conversation.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_user_status.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_user_status.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_statelesswidget.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/life_cycle/conversation_life_cycle.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_conversation_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_friendship_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/listener_model/tui_group_listener_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/core/tim_uikit_wide_modal_operation_key.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/services_locatar.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/controller/tim_uikit_conversation_controller.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/platform.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitConversation/tim_uikit_conversation_item.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/customize_ball_pulse_header.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/wide_popup.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_callback.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/color.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/tui_theme_view_model.dart';
|
||||
|
||||
typedef ConversationItemBuilder = Widget Function(V2TimConversation conversationItem, [V2TimUserStatus? onlineStatus]);
|
||||
|
||||
typedef ConversationItemSlideBuilder = List<ConversationItemSlidePanel> Function(V2TimConversation conversationItem);
|
||||
|
||||
typedef ConversationItemSecondaryMenuBuilder = Widget Function(
|
||||
V2TimConversation conversationItem, VoidCallback onClose);
|
||||
|
||||
class TIMUIKitConversation extends StatefulWidget {
|
||||
/// the callback after clicking conversation item
|
||||
final ValueChanged<V2TimConversation>? onTapItem;
|
||||
|
||||
/// conversation controller
|
||||
final TIMUIKitConversationController? controller;
|
||||
|
||||
/// the builder for conversation item
|
||||
final ConversationItemBuilder? itemBuilder;
|
||||
|
||||
/// the builder for Slidable item for each conversation item, shows on narrow screens.
|
||||
final ConversationItemSlideBuilder? itemSlideBuilder;
|
||||
|
||||
/// the widget of secondary tap menu for each conversation item, shows on wide screens.
|
||||
final ConversationItemSecondaryMenuBuilder? itemSecondaryMenuBuilder;
|
||||
|
||||
/// the widget shows when no conversation exists
|
||||
final Widget Function()? emptyBuilder;
|
||||
|
||||
/// the filter for conversation
|
||||
final bool Function(V2TimConversation? conversation)? conversationCollector;
|
||||
|
||||
/// the builder for the second line in each conservation item,
|
||||
/// usually shows the summary of the last message
|
||||
final LastMessageBuilder? lastMessageBuilder;
|
||||
|
||||
/// The life cycle hooks for `TIMUIKitConversation`
|
||||
final ConversationLifeCycle? lifeCycle;
|
||||
|
||||
/// Control if shows the online status for each user on its avatar.
|
||||
final bool isShowOnlineStatus;
|
||||
|
||||
/// Control if shows the identifier that the conversation has a draft text, inputted in previous.
|
||||
final bool isShowDraft;
|
||||
|
||||
const TIMUIKitConversation(
|
||||
{Key? key,
|
||||
this.lifeCycle,
|
||||
this.onTapItem,
|
||||
this.controller,
|
||||
this.itemSecondaryMenuBuilder,
|
||||
this.itemBuilder,
|
||||
this.isShowDraft = true,
|
||||
this.itemSlideBuilder,
|
||||
this.conversationCollector,
|
||||
this.emptyBuilder,
|
||||
this.lastMessageBuilder,
|
||||
this.isShowOnlineStatus = true})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() {
|
||||
return _TIMUIKitConversationState();
|
||||
}
|
||||
}
|
||||
|
||||
class ConversationItemSlidePanel extends TIMUIKitStatelessWidget {
|
||||
ConversationItemSlidePanel({
|
||||
Key? key,
|
||||
this.flex = 1,
|
||||
this.backgroundColor = Colors.white,
|
||||
this.foregroundColor,
|
||||
this.autoClose = true,
|
||||
required this.onPressed,
|
||||
this.icon,
|
||||
this.spacing = 4,
|
||||
this.label,
|
||||
}) : assert(flex > 0),
|
||||
assert(icon != null || label != null),
|
||||
super(key: key);
|
||||
|
||||
/// {@macro slidable.actions.flex}
|
||||
final int flex;
|
||||
|
||||
/// {@macro slidable.actions.backgroundColor}
|
||||
final Color backgroundColor;
|
||||
|
||||
/// {@macro slidable.actions.foregroundColor}
|
||||
final Color? foregroundColor;
|
||||
|
||||
/// {@macro slidable.actions.autoClose}
|
||||
final bool autoClose;
|
||||
|
||||
/// {@macro slidable.actions.onPressed}
|
||||
final SlidableActionCallback? onPressed;
|
||||
|
||||
/// An icon to display above the [label].
|
||||
final IconData? icon;
|
||||
|
||||
/// The space between [icon] and [label] if both set.
|
||||
///
|
||||
/// Defaults to 4.
|
||||
final double spacing;
|
||||
|
||||
/// A label to display below the [icon].
|
||||
final String? label;
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
return SlidableAction(
|
||||
onPressed: onPressed,
|
||||
flex: flex,
|
||||
backgroundColor: backgroundColor,
|
||||
foregroundColor: foregroundColor,
|
||||
autoClose: autoClose,
|
||||
label: label,
|
||||
spacing: spacing,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TIMUIKitConversationState extends TIMUIKitState<TIMUIKitConversation> {
|
||||
final TUIConversationViewModel model = serviceLocator<TUIConversationViewModel>();
|
||||
late TIMUIKitConversationController _timuiKitConversationController;
|
||||
final TUIThemeViewModel themeViewModel = serviceLocator<TUIThemeViewModel>();
|
||||
final TUIFriendShipViewModel friendShipViewModel = serviceLocator<TUIFriendShipViewModel>();
|
||||
final TUIGroupListenerModel groupListenerModel = serviceLocator<TUIGroupListenerModel>();
|
||||
late AutoScrollController _autoScrollController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final controller = getController();
|
||||
_timuiKitConversationController = controller;
|
||||
_timuiKitConversationController.model = model;
|
||||
_autoScrollController = AutoScrollController();
|
||||
}
|
||||
|
||||
TIMUIKitConversationController getController() {
|
||||
return widget.controller ?? TIMUIKitConversationController();
|
||||
}
|
||||
|
||||
void onTapConvItem(V2TimConversation conversation) {
|
||||
if (widget.onTapItem != null) {
|
||||
widget.onTapItem!(conversation);
|
||||
}
|
||||
model.setSelectedConversation(conversation);
|
||||
}
|
||||
|
||||
_clearHistory(V2TimConversation conversationItem) {
|
||||
_timuiKitConversationController.clearHistoryMessage(conversation: conversationItem);
|
||||
}
|
||||
|
||||
_pinConversation(V2TimConversation conversation) {
|
||||
_timuiKitConversationController.pinConversation(
|
||||
conversationID: conversation.conversationID, isPinned: !conversation.isPinned!);
|
||||
}
|
||||
|
||||
_deleteConversation(V2TimConversation conversation) {
|
||||
_timuiKitConversationController.deleteConversation(conversationID: conversation.conversationID);
|
||||
}
|
||||
|
||||
List<V2TimConversation?> getFilteredConversation() {
|
||||
List<V2TimConversation?> filteredConversationList =
|
||||
model.conversationList.where((element) => (element?.groupID != null || element?.userID != null)).toList();
|
||||
if (widget.conversationCollector != null) {
|
||||
filteredConversationList = filteredConversationList.where(widget.conversationCollector!).toList();
|
||||
}
|
||||
return filteredConversationList;
|
||||
}
|
||||
|
||||
_onScrollToConversation(String conversationID) {
|
||||
final msgList = getFilteredConversation();
|
||||
bool isFound = false;
|
||||
int targetIndex = 1;
|
||||
for (int i = msgList.length - 1; i >= 0; i--) {
|
||||
final currentConversation = msgList[i];
|
||||
if (currentConversation?.conversationID == conversationID) {
|
||||
isFound = true;
|
||||
targetIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (isFound) {
|
||||
_autoScrollController.scrollToIndex(
|
||||
targetIndex,
|
||||
preferPosition: AutoScrollPosition.begin,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _defaultSecondaryMenu(V2TimConversation conversationItem, VoidCallback onClose) {
|
||||
return TUIKitColumnMenu(data: [
|
||||
if (!PlatformUtils().isWeb)
|
||||
ColumnMenuItem(
|
||||
label: TIM_t("清除消息"),
|
||||
icon: const Icon(Icons.clear_all, size: 16),
|
||||
onClick: () {
|
||||
onClose();
|
||||
_clearHistory(conversationItem);
|
||||
}),
|
||||
ColumnMenuItem(
|
||||
label: conversationItem.isPinned! ? TIM_t("取消置顶") : TIM_t("置顶"),
|
||||
icon: Icon(conversationItem.isPinned! ? Icons.vertical_align_bottom : Icons.vertical_align_top, size: 16),
|
||||
onClick: () {
|
||||
onClose();
|
||||
_pinConversation(conversationItem);
|
||||
}),
|
||||
ColumnMenuItem(
|
||||
label: TIM_t("删除会话"),
|
||||
icon: const Icon(Icons.delete_outline, size: 16),
|
||||
onClick: () {
|
||||
onClose();
|
||||
_deleteConversation(conversationItem);
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
List<ConversationItemSlidePanel> _defaultSlideBuilder(
|
||||
V2TimConversation conversationItem,
|
||||
) {
|
||||
final theme = themeViewModel.theme;
|
||||
return [
|
||||
if (!PlatformUtils().isWeb)
|
||||
ConversationItemSlidePanel(
|
||||
onPressed: (context) {
|
||||
_clearHistory(conversationItem);
|
||||
},
|
||||
backgroundColor: theme.conversationItemSliderClearBgColor ?? CommonColor.primaryColor,
|
||||
foregroundColor: theme.conversationItemSliderTextColor,
|
||||
label: TIM_t("清除"),
|
||||
spacing: 0,
|
||||
autoClose: true,
|
||||
),
|
||||
ConversationItemSlidePanel(
|
||||
onPressed: (context) {
|
||||
_pinConversation(conversationItem);
|
||||
},
|
||||
backgroundColor: theme.conversationItemSliderPinBgColor ?? CommonColor.infoColor,
|
||||
foregroundColor: theme.conversationItemSliderTextColor,
|
||||
label: conversationItem.isPinned! ? TIM_t("取消置顶") : TIM_t("置顶"),
|
||||
),
|
||||
ConversationItemSlidePanel(
|
||||
onPressed: (context) {
|
||||
_deleteConversation(conversationItem);
|
||||
},
|
||||
backgroundColor: theme.conversationItemSliderDeleteBgColor ?? Colors.red,
|
||||
foregroundColor: theme.conversationItemSliderTextColor,
|
||||
label: TIM_t("删除"),
|
||||
)
|
||||
];
|
||||
}
|
||||
|
||||
Widget _getSecondaryMenu(V2TimConversation conversation, VoidCallback onClose) {
|
||||
if (widget.itemSecondaryMenuBuilder != null) {
|
||||
return widget.itemSecondaryMenuBuilder!(conversation, onClose);
|
||||
}
|
||||
return _defaultSecondaryMenu(conversation, onClose);
|
||||
}
|
||||
|
||||
ConversationItemSlideBuilder _getSlideBuilder() {
|
||||
return widget.itemSlideBuilder ?? _defaultSlideBuilder;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final theme = value.theme;
|
||||
final isDesktopScreen = TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop;
|
||||
return MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider.value(value: model),
|
||||
ChangeNotifierProvider.value(value: friendShipViewModel),
|
||||
ChangeNotifierProvider.value(value: groupListenerModel)
|
||||
],
|
||||
builder: (BuildContext context, Widget? w) {
|
||||
final _model = Provider.of<TUIConversationViewModel>(context);
|
||||
bool haveMoreData = _model.haveMoreData;
|
||||
final _friendShipViewModel = Provider.of<TUIFriendShipViewModel>(context);
|
||||
_model.lifeCycle = widget.lifeCycle;
|
||||
|
||||
final TUIGroupListenerModel groupListenerModel = Provider.of<TUIGroupListenerModel>(context, listen: true);
|
||||
final NeedUpdate? needUpdate = groupListenerModel.needUpdate;
|
||||
if (needUpdate != null) {
|
||||
groupListenerModel.needUpdate = null;
|
||||
if (needUpdate.updateType == UpdateType.groupDismissed) {
|
||||
onTIMCallback(TIMCallback(
|
||||
type: TIMCallbackType.INFO,
|
||||
infoRecommendText: "${needUpdate!.extraData}${TIM_t("已解散")}",
|
||||
infoCode: 6661402));
|
||||
} else if (needUpdate.updateType == UpdateType.kickedFromGroup) {
|
||||
onTIMCallback(TIMCallback(
|
||||
type: TIMCallbackType.INFO,
|
||||
infoRecommendText: "${TIM_t("您已被踢出")}${needUpdate!.extraData}",
|
||||
infoCode: 6661402));
|
||||
}
|
||||
}
|
||||
|
||||
List<V2TimConversation?> filteredConversationList = getFilteredConversation();
|
||||
|
||||
if (TencentUtils.checkString(_model.scrollToConversation) != null) {
|
||||
_onScrollToConversation(_model.scrollToConversation!);
|
||||
_model.clearScrollToConversation();
|
||||
}
|
||||
|
||||
Widget conversationList() {
|
||||
return filteredConversationList.isNotEmpty
|
||||
? ListView.builder(
|
||||
controller: _autoScrollController,
|
||||
shrinkWrap: true,
|
||||
itemCount: filteredConversationList.length,
|
||||
itemBuilder: (context, index) {
|
||||
if (index == filteredConversationList.length - 1) {
|
||||
if (haveMoreData) {
|
||||
_timuiKitConversationController.loadData();
|
||||
}
|
||||
}
|
||||
|
||||
final conversationItem = filteredConversationList[index];
|
||||
|
||||
final V2TimUserStatus? onlineStatus = _friendShipViewModel.userStatusList.firstWhere(
|
||||
(item) => item.userID == conversationItem?.userID,
|
||||
orElse: () => V2TimUserStatus(statusType: 0));
|
||||
|
||||
if (widget.itemBuilder != null) {
|
||||
return widget.itemBuilder!(conversationItem!, onlineStatus);
|
||||
}
|
||||
|
||||
final slideChildren = _getSlideBuilder()(conversationItem!);
|
||||
|
||||
final isCurrent = conversationItem.conversationID == model.selectedConversation?.conversationID;
|
||||
|
||||
final isPined = conversationItem.isPinned ?? false;
|
||||
|
||||
Widget conversationLineItem() {
|
||||
return Material(
|
||||
color: (isCurrent && isDesktopScreen)
|
||||
? theme.conversationItemChooseBgColor
|
||||
: isPined
|
||||
? theme.conversationItemPinedBgColor
|
||||
: theme.conversationItemBgColor,
|
||||
child: GestureDetector(
|
||||
child: TIMUIKitConversationItem(
|
||||
isCurrent: isCurrent,
|
||||
lastMessageBuilder: widget.lastMessageBuilder,
|
||||
faceUrl: conversationItem.faceUrl ?? "",
|
||||
nickName: conversationItem.showName ?? "",
|
||||
isDisturb:
|
||||
(conversationItem.groupType == "Meeting" ? false : conversationItem.recvOpt != 0),
|
||||
lastMsg: conversationItem.lastMessage,
|
||||
isPined: isPined,
|
||||
groupAtInfoList: conversationItem.groupAtInfoList ?? [],
|
||||
unreadCount: conversationItem.unreadCount ?? 0,
|
||||
draftText: conversationItem.draftText,
|
||||
onlineStatus: (widget.isShowOnlineStatus &&
|
||||
conversationItem.userID != null &&
|
||||
conversationItem.userID!.isNotEmpty)
|
||||
? onlineStatus
|
||||
: null,
|
||||
draftTimestamp: conversationItem.draftTimestamp,
|
||||
convType: conversationItem.type),
|
||||
onTap: () => onTapConvItem(conversationItem),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return TUIKitScreenUtils.getDeviceWidget(
|
||||
context: context,
|
||||
desktopWidget: AutoScrollTag(
|
||||
key: ValueKey(conversationItem.conversationID),
|
||||
controller: _autoScrollController,
|
||||
index: index,
|
||||
child: InkWell(
|
||||
onSecondaryTapDown: (details) {
|
||||
TUIKitWidePopup.showPopupWindow(
|
||||
operationKey: TUIKitWideModalOperationKey.conversationSecondaryMenu,
|
||||
isDarkBackground: false,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(4)),
|
||||
context: context,
|
||||
offset: Offset(
|
||||
min(details.globalPosition.dx, MediaQuery.of(context).size.width - 80),
|
||||
min(details.globalPosition.dy, MediaQuery.of(context).size.height - 130)),
|
||||
child: (onClose) => _getSecondaryMenu(conversationItem, onClose));
|
||||
},
|
||||
child: conversationLineItem(),
|
||||
),
|
||||
),
|
||||
defaultWidget: AutoScrollTag(
|
||||
key: ValueKey(conversationItem.conversationID),
|
||||
controller: _autoScrollController,
|
||||
index: index,
|
||||
child: Slidable(
|
||||
groupTag: 'conversation-list',
|
||||
child: conversationLineItem(),
|
||||
endActionPane: ActionPane(
|
||||
extentRatio: slideChildren.length > 2 ? 0.77 : 0.5,
|
||||
motion: const DrawerMotion(),
|
||||
children: slideChildren)),
|
||||
));
|
||||
})
|
||||
: (widget.emptyBuilder != null ? widget.emptyBuilder!() : Container());
|
||||
}
|
||||
|
||||
return TUIKitScreenUtils.getDeviceWidget(
|
||||
context: context,
|
||||
defaultWidget: SlidableAutoCloseBehavior(
|
||||
child: EasyRefresh(
|
||||
header: CustomizeBallPulseHeader(color: theme.primaryColor),
|
||||
onRefresh: () async {
|
||||
model.refresh();
|
||||
},
|
||||
child: conversationList(),
|
||||
),
|
||||
),
|
||||
desktopWidget: Scrollbar(controller: _autoScrollController, child: conversationList()));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:tencent_chat_i18n_tool/tencent_chat_i18n_tool.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_statelesswidget.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/tui_theme.dart';
|
||||
|
||||
class TIMUIKitDraftText extends TIMUIKitStatelessWidget {
|
||||
final BuildContext context;
|
||||
final String draftText;
|
||||
final double fontSize;
|
||||
|
||||
TIMUIKitDraftText({
|
||||
Key? key,
|
||||
this.fontSize = 14.0,
|
||||
required this.context,
|
||||
required this.draftText,
|
||||
}) : super(key: key);
|
||||
|
||||
String _getDraftShowText() {
|
||||
final draftShowText = TIM_t("草稿");
|
||||
|
||||
return '[$draftShowText] ';
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final TUITheme theme = value.theme;
|
||||
return Row(children: [
|
||||
Text(_getDraftShowText(),
|
||||
style: TextStyle(
|
||||
color: theme.conversationItemDraftTextColor,
|
||||
)),
|
||||
Expanded(
|
||||
child: Text(
|
||||
draftText,
|
||||
softWrap: true,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
height: 1.5,
|
||||
color: theme.conversationItemLastMessageTextColor,
|
||||
fontSize: fontSize),
|
||||
)),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
// ignore_for_file: empty_catches
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_at_info.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_group_at_info.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_message.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_user_status.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_user_status.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_statelesswidget.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/time_ago.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitConversation/tim_uikit_conversation_last_msg.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/avatar.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/unread_message.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/color.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/tui_theme.dart';
|
||||
|
||||
typedef LastMessageBuilder = Widget? Function(V2TimMessage? lastMsg, List<V2TimGroupAtInfo?> groupAtInfoList);
|
||||
|
||||
class TIMUIKitConversationItem extends TIMUIKitStatelessWidget {
|
||||
final String faceUrl;
|
||||
final String nickName;
|
||||
final V2TimMessage? lastMsg;
|
||||
final int unreadCount;
|
||||
final bool isPined;
|
||||
final List<V2TimGroupAtInfo?> groupAtInfoList;
|
||||
final String? draftText;
|
||||
final int? draftTimestamp;
|
||||
final bool isDisturb;
|
||||
final LastMessageBuilder? lastMessageBuilder;
|
||||
final V2TimUserStatus? onlineStatus;
|
||||
final int? convType;
|
||||
final bool isCurrent;
|
||||
|
||||
TIMUIKitConversationItem({
|
||||
Key? key,
|
||||
required this.faceUrl,
|
||||
required this.nickName,
|
||||
required this.lastMsg,
|
||||
this.onlineStatus,
|
||||
required this.isPined,
|
||||
this.isCurrent = false,
|
||||
required this.unreadCount,
|
||||
required this.groupAtInfoList,
|
||||
required this.isDisturb,
|
||||
this.draftText,
|
||||
this.draftTimestamp,
|
||||
this.lastMessageBuilder,
|
||||
this.convType,
|
||||
}) : super(key: key);
|
||||
|
||||
Widget _getShowMsgWidget(BuildContext context) {
|
||||
final isDesktopScreen = TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop;
|
||||
if (lastMsg != null && lastMessageBuilder != null && lastMessageBuilder!(lastMsg, groupAtInfoList) != null) {
|
||||
return lastMessageBuilder!(lastMsg, groupAtInfoList)!;
|
||||
}
|
||||
|
||||
if (lastMsg != null || (draftText != null && draftText != "")) {
|
||||
return TIMUIKitLastMsg(
|
||||
fontSize: isDesktopScreen ? 12 : 14,
|
||||
groupAtInfoList: groupAtInfoList,
|
||||
lastMsg: lastMsg,
|
||||
isDisturb: isDisturb,
|
||||
unreadCount: unreadCount,
|
||||
context: context,
|
||||
draftText: draftText ?? "",
|
||||
);
|
||||
}
|
||||
|
||||
return Container(
|
||||
height: 0,
|
||||
);
|
||||
}
|
||||
|
||||
bool isHaveSecondLine() {
|
||||
return (draftText != null && draftText != "") || (lastMsg != null);
|
||||
}
|
||||
|
||||
Widget _getTimeStringForChatWidget(BuildContext context, TUITheme theme) {
|
||||
try {
|
||||
if (draftTimestamp != null && draftTimestamp != 0) {
|
||||
return Text(TimeAgo().getTimeStringForChat(draftTimestamp as int) ?? "",
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: theme.conversationItemTitmeTextColor,
|
||||
));
|
||||
} else if (lastMsg != null) {
|
||||
return Text(TimeAgo().getTimeStringForChat(lastMsg!.timestamp as int) ?? "",
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: theme.conversationItemTitmeTextColor,
|
||||
));
|
||||
}
|
||||
} catch (err) {}
|
||||
|
||||
return Container();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final TUITheme theme = value.theme;
|
||||
final isDesktopScreen = TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop;
|
||||
return Container(
|
||||
padding: const EdgeInsets.only(top: 6, bottom: 6, left: 16, right: 16),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: theme.conversationItemBorderColor ?? CommonColor.weakDividerColor,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.only(top: 0, bottom: 2, right: 0),
|
||||
child: SizedBox(
|
||||
width: isDesktopScreen ? 40 : 44,
|
||||
height: isDesktopScreen ? 40 : 44,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Avatar(onlineStatus: onlineStatus, faceUrl: faceUrl, showName: nickName, type: convType),
|
||||
if (unreadCount != 0)
|
||||
Positioned(
|
||||
top: isDisturb ? -2.5 : -4.5,
|
||||
right: isDisturb ? -2.5 : -4.5,
|
||||
child: UnconstrainedBox(
|
||||
child: UnreadMessage(
|
||||
width: isDisturb ? 10 : 18,
|
||||
height: isDisturb ? 10 : 18,
|
||||
unreadCount: isDisturb ? 0 : unreadCount),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Container(
|
||||
height: 60,
|
||||
margin: EdgeInsets.only(left: isDesktopScreen ? 10 : 12),
|
||||
padding: const EdgeInsets.only(top: 0, bottom: 0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
nickName,
|
||||
softWrap: true,
|
||||
textAlign: TextAlign.left,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
height: 1,
|
||||
color: theme.conversationItemTitleTextColor,
|
||||
fontSize: isDesktopScreen ? 14 : 18,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
)),
|
||||
_getTimeStringForChatWidget(context, theme),
|
||||
],
|
||||
),
|
||||
if (isHaveSecondLine())
|
||||
const SizedBox(
|
||||
height: 6,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: _getShowMsgWidget(context)),
|
||||
if (isDisturb)
|
||||
SizedBox(
|
||||
width: 18,
|
||||
height: 18,
|
||||
child: Icon(
|
||||
Icons.notifications_off,
|
||||
color: theme.conversationItemNoNotificationIconColor,
|
||||
size: isDesktopScreen ? 14 : 16.0,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
))
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
// ignore_for_file: unrelated_type_equality_checks
|
||||
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:extended_text/extended_text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:tencent_chat_i18n_tool/tencent_chat_i18n_tool.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/enum/message_elem_type.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/enum/message_status.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_at_info.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_group_at_info.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_message.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/common_utils.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/message.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitTextField/special_text/DefaultSpecialTextSpanBuilder.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/tui_theme.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/tui_theme_view_model.dart';
|
||||
|
||||
class TIMUIKitLastMsg extends StatefulWidget {
|
||||
final V2TimMessage? lastMsg;
|
||||
final List<V2TimGroupAtInfo?> groupAtInfoList;
|
||||
final BuildContext context;
|
||||
final double fontSize;
|
||||
bool isDisturb;
|
||||
int unreadCount;
|
||||
String draftText;
|
||||
|
||||
TIMUIKitLastMsg(
|
||||
{Key? key,
|
||||
this.lastMsg,
|
||||
required this.groupAtInfoList,
|
||||
this.isDisturb = false,
|
||||
this.unreadCount = 0,
|
||||
required this.draftText,
|
||||
required this.context,
|
||||
this.fontSize = 14.0})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<TIMUIKitLastMsg> createState() => _TIMUIKitLastMsgState();
|
||||
}
|
||||
|
||||
class _TIMUIKitLastMsgState extends TIMUIKitState<TIMUIKitLastMsg> {
|
||||
String groupTipsAbstractText = "";
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_getMsgElem();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant TIMUIKitLastMsg oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if ((oldWidget.lastMsg?.msgID != widget.lastMsg?.msgID) ||
|
||||
(oldWidget.lastMsg?.id != widget.lastMsg?.id) ||
|
||||
(oldWidget.lastMsg?.status != widget.lastMsg?.status) ||
|
||||
(oldWidget.unreadCount != widget.unreadCount)) {
|
||||
_getMsgElem();
|
||||
}
|
||||
}
|
||||
|
||||
(bool isRevoke, bool isRevokeByAdmin) isRevokeMessage(V2TimMessage? message) {
|
||||
if (message == null) {
|
||||
return (false, false);
|
||||
}
|
||||
if (message.status == 6) {
|
||||
return (true, false);
|
||||
} else {
|
||||
try {
|
||||
final customData = jsonDecode(message.cloudCustomData ?? "{}");
|
||||
final isRevoke = customData["isRevoke"] ?? false;
|
||||
final revokeByAdmin = customData["revokeByAdmin"] ?? false;
|
||||
return (isRevoke, revokeByAdmin);
|
||||
} catch (e) {
|
||||
return (false, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _getMsgElem() async {
|
||||
final revokeStatus = isRevokeMessage(widget.lastMsg);
|
||||
final isRevokedMessage = revokeStatus.$1;
|
||||
final isAdminRevoke = revokeStatus.$2;
|
||||
if (isRevokedMessage) {
|
||||
final isSelf = widget.lastMsg!.isSelf ?? true;
|
||||
final option1 =
|
||||
isAdminRevoke ? TIM_t("管理员") : (isSelf ? TIM_t("您") : widget.lastMsg!.nickName ?? widget.lastMsg?.sender);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
groupTipsAbstractText = TIM_t_para("{{option1}}撤回了一条消息", "$option1撤回了一条消息")(option1: option1);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
String msgShowText = await _getLastMsgShowText(widget.lastMsg, widget.context) ?? "";
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
groupTipsAbstractText = msgShowText;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String _getDisturbUnreadCountInfo() {
|
||||
if (widget.isDisturb && widget.unreadCount > 0) {
|
||||
final option1 = widget.unreadCount.toString();
|
||||
String unreadCountText = TIM_t_para("[{{option1}} 条]", "[$option1 条]")(option1: option1);
|
||||
return unreadCountText;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
Future<String?> _getLastMsgShowText(V2TimMessage? message, BuildContext context) async {
|
||||
final msgType = message!.elemType;
|
||||
switch (msgType) {
|
||||
case MessageElemType.V2TIM_ELEM_TYPE_CUSTOM:
|
||||
return TIM_t("[自定义]");
|
||||
case MessageElemType.V2TIM_ELEM_TYPE_SOUND:
|
||||
return TIM_t("[语音]");
|
||||
case MessageElemType.V2TIM_ELEM_TYPE_TEXT:
|
||||
return (widget.lastMsg?.textElem?.text)?.trim() ?? "";
|
||||
case MessageElemType.V2TIM_ELEM_TYPE_FACE:
|
||||
return TIM_t("[表情]");
|
||||
case MessageElemType.V2TIM_ELEM_TYPE_FILE:
|
||||
final option1 = widget.lastMsg!.fileElem!.fileName;
|
||||
return TIM_t_para("[文件] {{option1}}", "[文件] $option1")(option1: option1);
|
||||
case MessageElemType.V2TIM_ELEM_TYPE_GROUP_TIPS:
|
||||
return await MessageUtils.groupTipsMessageAbstract(widget.lastMsg!.groupTipsElem!, []);
|
||||
case MessageElemType.V2TIM_ELEM_TYPE_IMAGE:
|
||||
return TIM_t("[图片]");
|
||||
case MessageElemType.V2TIM_ELEM_TYPE_VIDEO:
|
||||
return TIM_t("[视频]");
|
||||
case MessageElemType.V2TIM_ELEM_TYPE_LOCATION:
|
||||
return TIM_t("[位置]");
|
||||
case MessageElemType.V2TIM_ELEM_TYPE_MERGER:
|
||||
return TIM_t("[聊天记录]");
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Icon? _getIconByMsgStatus(BuildContext context) {
|
||||
final msgStatus = widget.lastMsg!.status;
|
||||
final theme = Provider.of<TUIThemeViewModel>(context).theme;
|
||||
if (msgStatus == MessageStatus.V2TIM_MSG_STATUS_SEND_FAIL) {
|
||||
return Icon(Icons.error, color: theme.cautionColor, size: 16);
|
||||
}
|
||||
if (msgStatus == MessageStatus.V2TIM_MSG_STATUS_SENDING) {
|
||||
return Icon(Icons.arrow_back, color: theme.weakTextColor, size: 16);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String _getAtMessage() {
|
||||
bool atMe = false;
|
||||
bool atAll = false;
|
||||
String msg = "";
|
||||
for (var item in widget.groupAtInfoList) {
|
||||
if (item!.atType == 1) {
|
||||
atMe = true;
|
||||
continue;
|
||||
} else if (item!.atType == 2) {
|
||||
atAll = true;
|
||||
continue;
|
||||
} else if (item!.atType == 3) {
|
||||
atMe = true;
|
||||
atAll = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (atAll && atMe) {
|
||||
msg = TIM_t("[@所有人][有人@我]");
|
||||
} else if (atAll) {
|
||||
msg = TIM_t("[@所有人]");
|
||||
} else if (atMe) {
|
||||
msg = TIM_t("[有人@我]");
|
||||
}
|
||||
|
||||
return msg;
|
||||
}
|
||||
|
||||
String _getDraftShowText() {
|
||||
final draftShowText = TIM_t("草稿");
|
||||
return '[$draftShowText]';
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final isDesktopScreen = TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop;
|
||||
final TUITheme theme = value.theme;
|
||||
final icon = _getIconByMsgStatus(context);
|
||||
String disturbUnreadCountInfo = _getDisturbUnreadCountInfo();
|
||||
return Row(children: [
|
||||
if (icon != null)
|
||||
Container(
|
||||
margin: const EdgeInsets.only(right: 2),
|
||||
child: icon,
|
||||
),
|
||||
if (widget.groupAtInfoList.isNotEmpty)
|
||||
Text(_getAtMessage(), style: TextStyle(color: theme.cautionColor, fontSize: widget.fontSize)),
|
||||
if (widget.draftText != null && widget.draftText != "")
|
||||
Text(_getDraftShowText(),
|
||||
style: TextStyle(color: theme.conversationItemDraftTextColor, fontSize: widget.fontSize)),
|
||||
if (disturbUnreadCountInfo != "")
|
||||
Text(disturbUnreadCountInfo, style: TextStyle(color: theme.weakTextColor, fontSize: widget.fontSize)),
|
||||
if (widget.draftText != null && widget.draftText != "")
|
||||
Expanded(
|
||||
child: ExtendedText(groupTipsAbstractText,
|
||||
softWrap: true,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(height: 1, color: theme.weakTextColor, fontSize: widget.fontSize),
|
||||
specialTextSpanBuilder: DefaultSpecialTextSpanBuilder(
|
||||
isUseQQPackage: true,
|
||||
isUseTencentCloudChatPackage: true,
|
||||
showAtBackground: true,
|
||||
)),
|
||||
),
|
||||
if (widget.draftText == null || widget.draftText == "" && TencentUtils.checkString(groupTipsAbstractText) != null)
|
||||
Expanded(
|
||||
child: ExtendedText(groupTipsAbstractText,
|
||||
softWrap: true,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(height: 1, color: theme.weakTextColor, fontSize: widget.fontSize),
|
||||
specialTextSpanBuilder: DefaultSpecialTextSpanBuilder(
|
||||
isUseQQPackage: true,
|
||||
isUseTencentCloudChatPackage: true,
|
||||
showAtBackground: true,
|
||||
)),
|
||||
)
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
import 'package:azlistview_all_platforms/azlistview_all_platforms.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:lpinyin/lpinyin.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_conversation.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_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/tencent_im_sdk_plugin.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/listener_model/tui_group_listener_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_friendship_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/services_locatar.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/avatar.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/az_list_view.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/color.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/tui_theme_view_model.dart';
|
||||
|
||||
typedef GroupItemBuilder = Widget Function(BuildContext context, V2TimGroupInfo groupInfo);
|
||||
|
||||
class TIMUIKitGroup extends StatefulWidget {
|
||||
final void Function(V2TimGroupInfo groupInfo, V2TimConversation conversation)? onTapItem;
|
||||
final Widget Function(BuildContext context)? emptyBuilder;
|
||||
final GroupItemBuilder? itemBuilder;
|
||||
|
||||
/// the filter for group conversation
|
||||
final bool Function(V2TimGroupInfo? groupInfo)? groupCollector;
|
||||
|
||||
const TIMUIKitGroup({Key? key, this.onTapItem, this.emptyBuilder, this.itemBuilder, this.groupCollector})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _TIMUIKitGroupState();
|
||||
}
|
||||
|
||||
class _TIMUIKitGroupState extends TIMUIKitState<TIMUIKitGroup> {
|
||||
final TUIFriendShipViewModel _friendshipViewModel = serviceLocator<TUIFriendShipViewModel>();
|
||||
final TUIGroupListenerModel _groupListenerModel = serviceLocator<TUIGroupListenerModel>();
|
||||
|
||||
List<ISuspensionBeanImpl<V2TimGroupInfo>> _getShowList(List<V2TimGroupInfo> groupList) {
|
||||
final List<ISuspensionBeanImpl<V2TimGroupInfo>> showList = List.empty(growable: true);
|
||||
for (var i = 0; i < groupList.length; i++) {
|
||||
final item = groupList[i];
|
||||
|
||||
final showName = item.groupName ?? item.groupID;
|
||||
String pinyin = PinyinHelper.getPinyinE(showName);
|
||||
String tag = pinyin.substring(0, 1).toUpperCase();
|
||||
if (RegExp("[A-Z]").hasMatch(tag)) {
|
||||
showList.add(ISuspensionBeanImpl(memberInfo: item, tagIndex: tag));
|
||||
} else {
|
||||
showList.add(ISuspensionBeanImpl(memberInfo: item, tagIndex: "#"));
|
||||
}
|
||||
}
|
||||
|
||||
SuspensionUtil.sortListBySuspensionTag(showList);
|
||||
|
||||
return showList;
|
||||
}
|
||||
|
||||
Widget _itemBuilder(BuildContext context, V2TimGroupInfo groupInfo) {
|
||||
final theme = Provider.of<TUIThemeViewModel>(context).theme;
|
||||
final showName = groupInfo.groupName ?? groupInfo.groupID;
|
||||
final faceUrl = groupInfo.faceUrl ?? "";
|
||||
final isDesktopScreen = TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop;
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border(bottom: BorderSide(color: theme.weakDividerColor ?? CommonColor.weakDividerColor))),
|
||||
child: Material(
|
||||
color: isDesktopScreen ? theme.wideBackgroundColor : null,
|
||||
child: InkWell(
|
||||
onTap: (() async {
|
||||
if (widget.onTapItem != null) {
|
||||
V2TimConversation conversation = V2TimConversation(
|
||||
conversationID: "group_${groupInfo.groupID}",
|
||||
groupID: groupInfo.groupID,
|
||||
type: 2,
|
||||
showName: groupInfo.groupName,
|
||||
groupType: groupInfo.groupType,
|
||||
faceUrl: groupInfo.faceUrl,
|
||||
);
|
||||
final res = await TencentImSDKPlugin.v2TIMManager
|
||||
.getConversationManager()
|
||||
.getConversation(conversationID: "group_${groupInfo.groupID}");
|
||||
if (res.code == 0 && res.data != null) {
|
||||
conversation = res.data!;
|
||||
}
|
||||
widget.onTapItem!(groupInfo, conversation);
|
||||
}
|
||||
}),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(top: 10, left: 16),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
margin: const EdgeInsets.only(right: 12),
|
||||
child: SizedBox(
|
||||
height: isDesktopScreen ? 30 : 40,
|
||||
width: isDesktopScreen ? 30 : 40,
|
||||
child: Avatar(
|
||||
faceUrl: faceUrl,
|
||||
showName: showName,
|
||||
type: 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
padding: const EdgeInsets.only(top: 10, bottom: 20),
|
||||
child: Text(
|
||||
showName,
|
||||
style: TextStyle(color: Colors.black, fontSize: isDesktopScreen ? 14 : 18),
|
||||
),
|
||||
))
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
GroupItemBuilder _getItemBuilder() {
|
||||
return widget.itemBuilder ?? _itemBuilder;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_friendshipViewModel.loadGroupListData();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
return MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider.value(value: _friendshipViewModel),
|
||||
ChangeNotifierProvider.value(value: _groupListenerModel),
|
||||
ChangeNotifierProvider.value(value: serviceLocator<TUIThemeViewModel>()),
|
||||
],
|
||||
builder: (BuildContext context, Widget? w) {
|
||||
final NeedUpdate? needUpdate = Provider.of<TUIGroupListenerModel>(context).needUpdate;
|
||||
if (needUpdate != null) {
|
||||
_groupListenerModel.needUpdate = null;
|
||||
switch (needUpdate.updateType) {
|
||||
case UpdateType.groupInfo:
|
||||
Provider.of<TUIFriendShipViewModel>(context).loadGroupListData();
|
||||
break;
|
||||
case UpdateType.memberList:
|
||||
Provider.of<TUIFriendShipViewModel>(context).loadGroupListData();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
List<V2TimGroupInfo> groupList = Provider.of<TUIFriendShipViewModel>(context).groupList;
|
||||
if (widget.groupCollector != null) {
|
||||
groupList = groupList.where(widget.groupCollector!).toList();
|
||||
}
|
||||
if (groupList.isNotEmpty) {
|
||||
final showList = _getShowList(groupList);
|
||||
return AZListViewContainer(
|
||||
isShowIndexBar: false,
|
||||
memberList: showList,
|
||||
itemBuilder: (context, index) {
|
||||
final groupInfo = showList[index].memberInfo;
|
||||
final itemBuilder = _getItemBuilder();
|
||||
return itemBuilder(context, groupInfo);
|
||||
});
|
||||
}
|
||||
|
||||
if (widget.emptyBuilder != null) {
|
||||
return widget.emptyBuilder!(context);
|
||||
}
|
||||
|
||||
return Container();
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:tencent_chat_i18n_tool/tencent_chat_i18n_tool.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/enum/group_application_type_enum.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_application.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_group_application.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_chat_global_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/screen_utils.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/avatar.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/color.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/tui_theme.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/tui_theme_view_model.dart';
|
||||
|
||||
typedef GroupApplicationItemBuilder = Widget Function(
|
||||
BuildContext context, V2TimGroupApplication applicationInfo, int index);
|
||||
|
||||
enum ApplicationStatus {
|
||||
none,
|
||||
accept,
|
||||
reject,
|
||||
}
|
||||
|
||||
class TIMUIKitGroupApplicationList extends StatefulWidget {
|
||||
/// the builder for the request item
|
||||
final GroupApplicationItemBuilder? itemBuilder;
|
||||
|
||||
/// group ID
|
||||
final String groupID;
|
||||
|
||||
const TIMUIKitGroupApplicationList({Key? key, this.itemBuilder, required this.groupID}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => TIMUIKitGroupApplicationListState();
|
||||
}
|
||||
|
||||
class TIMUIKitGroupApplicationListState extends TIMUIKitState<TIMUIKitGroupApplicationList> {
|
||||
final TUIChatGlobalModel model = serviceLocator<TUIChatGlobalModel>();
|
||||
final GroupServices _groupServices = serviceLocator<GroupServices>();
|
||||
List<V2TimGroupApplication> groupApplicationList = [];
|
||||
List<ApplicationStatus> applicationStatusList = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
groupApplicationList = model.groupApplicationList.where((item) => (item.groupID == widget.groupID)).toList();
|
||||
applicationStatusList = groupApplicationList.map((item) => ApplicationStatus.none).toList();
|
||||
}
|
||||
|
||||
GroupApplicationItemBuilder _getItemBuilder() {
|
||||
return widget.itemBuilder ?? _defaultItemBuilder;
|
||||
}
|
||||
|
||||
Widget _defaultItemBuilder(BuildContext context, V2TimGroupApplication applicationInfo, int index) {
|
||||
final theme = Provider.of<TUIThemeViewModel>(context).theme;
|
||||
final ApplicationStatus currentStatus = applicationStatusList[index];
|
||||
|
||||
String _getUserName() {
|
||||
if (applicationInfo.fromUserNickName != null &&
|
||||
applicationInfo.fromUserNickName!.isNotEmpty &&
|
||||
applicationInfo.fromUserNickName != applicationInfo.fromUser) {
|
||||
return "${applicationInfo.fromUserNickName} (${applicationInfo.fromUser})";
|
||||
} else {
|
||||
return "${applicationInfo.fromUser}";
|
||||
}
|
||||
}
|
||||
|
||||
String _getRequestMessage() {
|
||||
String option2 = applicationInfo.requestMsg ?? "";
|
||||
return TIM_t_para("验证消息: {{option2}}", "验证消息: $option2")(option2: option2);
|
||||
}
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: Border(bottom: BorderSide(color: theme.weakDividerColor ?? const Color(0xFFDBDBDB))),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
margin: const EdgeInsets.only(right: 12),
|
||||
child: SizedBox(
|
||||
height: 40,
|
||||
width: 40,
|
||||
child: Avatar(
|
||||
faceUrl: applicationInfo.fromUserFaceUrl ?? "",
|
||||
showName: applicationInfo.fromUserNickName ?? applicationInfo.fromUser ?? ""),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
_getUserName(),
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: theme.darkTextColor),
|
||||
),
|
||||
Text(
|
||||
_getRequestMessage(),
|
||||
style: TextStyle(fontSize: 15, color: theme.weakTextColor),
|
||||
),
|
||||
],
|
||||
)),
|
||||
if (currentStatus == ApplicationStatus.none && applicationInfo.handleStatus == 0)
|
||||
Container(
|
||||
margin: const EdgeInsets.only(left: 8, right: 8),
|
||||
child: InkWell(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
color: theme.primaryColor,
|
||||
border: Border.all(width: 1, color: theme.weakTextColor ?? CommonColor.weakTextColor)),
|
||||
child: Text(
|
||||
TIM_t("同意"), // agree
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
onTap: () async {
|
||||
final res = await _groupServices.acceptGroupApplication(
|
||||
groupID: applicationInfo.groupID,
|
||||
fromUser: applicationInfo.fromUser!,
|
||||
toUser: applicationInfo.toUser!,
|
||||
type: applicationInfo.type,
|
||||
addTime: applicationInfo.addTime ?? 0,
|
||||
);
|
||||
if (res.code == 0) {
|
||||
setState(() {
|
||||
applicationStatusList[index] = ApplicationStatus.accept;
|
||||
});
|
||||
Future.delayed(const Duration(seconds: 1), () {
|
||||
model.refreshGroupApplicationList();
|
||||
});
|
||||
}
|
||||
}),
|
||||
),
|
||||
if (currentStatus == ApplicationStatus.none && applicationInfo.handleStatus == 0)
|
||||
InkWell(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
color: Colors.white,
|
||||
border: Border.all(width: 1, color: theme.weakTextColor ?? CommonColor.weakTextColor)),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
child: Text(
|
||||
TIM_t("拒绝"), // reject
|
||||
style: TextStyle(
|
||||
color: theme.primaryColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
onTap: () async {
|
||||
final res = await _groupServices.refuseGroupApplication(
|
||||
addTime: applicationInfo.addTime!,
|
||||
groupID: applicationInfo.groupID,
|
||||
fromUser: applicationInfo.fromUser!,
|
||||
toUser: applicationInfo.toUser!,
|
||||
type: GroupApplicationTypeEnum.values[applicationInfo.type]);
|
||||
if (res.code == 0) {
|
||||
setState(() {
|
||||
applicationStatusList[index] = ApplicationStatus.reject;
|
||||
});
|
||||
Future.delayed(const Duration(seconds: 1), () {
|
||||
model.refreshGroupApplicationList();
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
if (currentStatus == ApplicationStatus.accept || applicationInfo.handleResult == 1)
|
||||
Container(
|
||||
margin: const EdgeInsets.only(left: 8),
|
||||
child: Text(
|
||||
TIM_t("已同意"),
|
||||
style: TextStyle(fontSize: 15, color: theme.weakTextColor),
|
||||
),
|
||||
),
|
||||
if (currentStatus == ApplicationStatus.reject || applicationInfo.handleResult == 2)
|
||||
Container(
|
||||
margin: const EdgeInsets.only(left: 8),
|
||||
child: Text(
|
||||
TIM_t("已拒绝"),
|
||||
style: TextStyle(fontSize: 15, color: theme.weakTextColor),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final TUITheme theme = value.theme;
|
||||
|
||||
return MultiProvider(
|
||||
providers: [ChangeNotifierProvider.value(value: model)],
|
||||
builder: (context, w) {
|
||||
final isDesktopScreen = TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop;
|
||||
return Container(
|
||||
decoration: isDesktopScreen ? null : BoxDecoration(color: theme.weakBackgroundColor),
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: groupApplicationList.length,
|
||||
itemBuilder: (context, index) {
|
||||
final applicationInfo = groupApplicationList[index];
|
||||
final itemBuilder = _getItemBuilder();
|
||||
return itemBuilder(context, applicationInfo, index);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_application.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_group_application.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/avatar.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/tui_theme.dart';
|
||||
|
||||
enum ApplicationStatus {
|
||||
none,
|
||||
accept,
|
||||
refuse,
|
||||
}
|
||||
|
||||
class TIMUIKitGroupApplicationListItem extends StatefulWidget {
|
||||
final V2TimGroupApplication applicationInfo;
|
||||
|
||||
const TIMUIKitGroupApplicationListItem({Key? key, required this.applicationInfo}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => TIMUIKitGroupApplicationListItemState();
|
||||
}
|
||||
|
||||
class TIMUIKitGroupApplicationListItemState extends TIMUIKitState<TIMUIKitGroupApplicationListItem> {
|
||||
ApplicationStatus applicationStatus = ApplicationStatus.none;
|
||||
|
||||
String _getUserName() {
|
||||
if (widget.applicationInfo.fromUserNickName != null &&
|
||||
widget.applicationInfo.fromUserNickName!.isNotEmpty &&
|
||||
widget.applicationInfo.fromUserNickName != widget.applicationInfo.fromUser) {
|
||||
return "${widget.applicationInfo.fromUserNickName} (${widget.applicationInfo.fromUser})";
|
||||
} else {
|
||||
return "${widget.applicationInfo.fromUser}";
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final TUITheme theme = value.theme;
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: const BoxDecoration(color: Colors.white),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
margin: const EdgeInsets.only(right: 12),
|
||||
child: SizedBox(
|
||||
height: 40,
|
||||
width: 40,
|
||||
child: Avatar(
|
||||
faceUrl: widget.applicationInfo.fromUserFaceUrl ?? "",
|
||||
showName: widget.applicationInfo.fromUserNickName ?? widget.applicationInfo.fromUser ?? ""),
|
||||
),
|
||||
),
|
||||
Column(
|
||||
children: [
|
||||
Text(
|
||||
_getUserName(),
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: theme.darkTextColor),
|
||||
),
|
||||
Text(
|
||||
_getUserName(),
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: theme.darkTextColor),
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
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_friend_info.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_friend_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_group_profile_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/contact_list.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/tui_theme.dart';
|
||||
|
||||
GlobalKey<_AddGroupMemberPageState> addGroupMemberKey = GlobalKey();
|
||||
|
||||
class AddGroupMemberPage extends StatefulWidget {
|
||||
final TUIGroupProfileModel model;
|
||||
final VoidCallback? onClose;
|
||||
|
||||
const AddGroupMemberPage({Key? key, required this.model, this.onClose}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _AddGroupMemberPageState();
|
||||
}
|
||||
|
||||
class _AddGroupMemberPageState extends TIMUIKitState<AddGroupMemberPage> {
|
||||
List<V2TimFriendInfo> selectedContacts = [];
|
||||
|
||||
void submitAdd() async {
|
||||
if (selectedContacts.isNotEmpty) {
|
||||
final userIDs = selectedContacts.map((e) => e.userID).toList();
|
||||
await widget.model.inviteUserToGroup(userIDs);
|
||||
widget.onClose ?? Navigator.pop(context);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final TUITheme theme = value.theme;
|
||||
|
||||
return TUIKitScreenUtils.getDeviceWidget(
|
||||
context: context,
|
||||
desktopWidget: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: ContactList(
|
||||
bgColor: theme.wideBackgroundColor,
|
||||
groupMemberList: widget.model.groupMemberList,
|
||||
contactList: widget.model.contactList,
|
||||
isCanSelectMemberItem: true,
|
||||
onSelectedMemberItemChange: (selectedMember) {
|
||||
selectedContacts = selectedMember;
|
||||
},
|
||||
),
|
||||
),
|
||||
defaultWidget: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
TIM_t("添加群成员"),
|
||||
style: TextStyle(color: theme.appbarTextColor, fontSize: 17),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
submitAdd();
|
||||
},
|
||||
child: Text(
|
||||
TIM_t("确定"),
|
||||
style: TextStyle(
|
||||
color: theme.appbarTextColor,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
shadowColor: theme.weakDividerColor,
|
||||
backgroundColor: theme.appbarBgColor ?? theme.primaryColor,
|
||||
iconTheme: IconThemeData(
|
||||
color: theme.appbarTextColor,
|
||||
)),
|
||||
body: ContactList(
|
||||
groupMemberList: widget.model.groupMemberList,
|
||||
contactList: widget.model.contactList,
|
||||
isCanSelectMemberItem: true,
|
||||
onSelectedMemberItemChange: (selectedMember) {
|
||||
selectedContacts = selectedMember;
|
||||
},
|
||||
)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:tencent_chat_i18n_tool/tencent_chat_i18n_tool.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/enum/group_member_role.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_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_group_profile_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/group_member_list.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/tui_theme.dart';
|
||||
|
||||
GlobalKey<_DeleteGroupMemberPageState> deleteGroupMemberKey = GlobalKey();
|
||||
|
||||
class DeleteGroupMemberPage extends StatefulWidget {
|
||||
final TUIGroupProfileModel model;
|
||||
final VoidCallback? onClose;
|
||||
|
||||
const DeleteGroupMemberPage({Key? key, required this.model, this.onClose}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _DeleteGroupMemberPageState();
|
||||
}
|
||||
|
||||
class _DeleteGroupMemberPageState extends TIMUIKitState<DeleteGroupMemberPage> {
|
||||
List<V2TimGroupMemberFullInfo> selectedGroupMember = [];
|
||||
List<V2TimGroupMemberFullInfo?>? searchMemberList;
|
||||
|
||||
bool isSearchTextExist(String? searchText) {
|
||||
return searchText != null && searchText != "";
|
||||
}
|
||||
|
||||
handleSearchGroupMembers(String searchText, context) async {
|
||||
searchText = searchText;
|
||||
List<V2TimGroupMemberFullInfo?> currentGroupMember =
|
||||
Provider.of<TUIGroupProfileModel>(context, listen: false).groupMemberList;
|
||||
final res = await widget.model.searchGroupMember(V2TimGroupMemberSearchParam(
|
||||
keywordList: [searchText],
|
||||
groupIDList: [widget.model.groupInfo!.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(() {
|
||||
searchMemberList = isSearchTextExist(searchText) ? currentGroupMember : null;
|
||||
});
|
||||
}
|
||||
|
||||
handleRole(groupMemberList) {
|
||||
return groupMemberList
|
||||
?.where((value) => value?.role == GroupMemberRoleType.V2TIM_GROUP_MEMBER_ROLE_MEMBER)
|
||||
.toList() ??
|
||||
[];
|
||||
}
|
||||
|
||||
void submitDelete() async {
|
||||
if (selectedGroupMember.isNotEmpty) {
|
||||
final userIDs = selectedGroupMember.map((e) => e.userID).toList();
|
||||
widget.model.kickOffMember(userIDs);
|
||||
widget.onClose ?? Navigator.pop(context);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final TUITheme theme = value.theme;
|
||||
|
||||
return TUIKitScreenUtils.getDeviceWidget(
|
||||
context: context,
|
||||
desktopWidget: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: GroupProfileMemberList(
|
||||
memberList: handleRole(searchMemberList ?? widget.model.groupMemberList),
|
||||
canSelectMember: true,
|
||||
canSlideDelete: false,
|
||||
onSelectedMemberChange: (selectedMember) {
|
||||
selectedGroupMember = selectedMember;
|
||||
},
|
||||
touchBottomCallBack: () {},
|
||||
),
|
||||
),
|
||||
defaultWidget: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
TIM_t("删除群成员"),
|
||||
style: TextStyle(color: theme.appbarTextColor, fontSize: 17),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: submitDelete,
|
||||
child: Text(
|
||||
TIM_t("确定"),
|
||||
style: TextStyle(
|
||||
color: theme.appbarTextColor,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
shadowColor: theme.weakBackgroundColor,
|
||||
backgroundColor: theme.appbarBgColor ?? theme.primaryColor,
|
||||
iconTheme: IconThemeData(
|
||||
color: theme.appbarTextColor,
|
||||
)),
|
||||
body: GroupProfileMemberList(
|
||||
memberList: handleRole(searchMemberList ?? widget.model.groupMemberList),
|
||||
canSelectMember: true,
|
||||
canSlideDelete: false,
|
||||
onSelectedMemberChange: (selectedMember) {
|
||||
selectedGroupMember = selectedMember;
|
||||
},
|
||||
touchBottomCallBack: () {},
|
||||
)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
// ignore_for_file: must_be_immutable
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:tencent_chat_i18n_tool/tencent_chat_i18n_tool.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_member_full_info.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_group_member_full_info.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_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_uikit/ui/utils/screen_utils.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_group_profile_model.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 '../../../../theme/tui_theme.dart';
|
||||
|
||||
class GroupProfileMemberListPage extends StatefulWidget {
|
||||
List<V2TimGroupMemberFullInfo?> memberList;
|
||||
TUIGroupProfileModel model;
|
||||
|
||||
GroupProfileMemberListPage({
|
||||
Key? key,
|
||||
required this.memberList,
|
||||
required this.model,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => GroupProfileMemberListPageState();
|
||||
}
|
||||
|
||||
class GroupProfileMemberListPageState extends TIMUIKitState<GroupProfileMemberListPage> {
|
||||
List<V2TimGroupMemberFullInfo?>? searchMemberList;
|
||||
String? searchText;
|
||||
|
||||
_kickedOffMember(String userID) async {
|
||||
widget.model.kickOffMember([userID]);
|
||||
}
|
||||
|
||||
bool isSearchTextExist(String? searchText) {
|
||||
return searchText != null && searchText != "";
|
||||
}
|
||||
|
||||
handleSearchGroupMembers(String searchText, context) async {
|
||||
searchText = searchText;
|
||||
List<V2TimGroupMemberFullInfo?> currentGroupMember =
|
||||
Provider.of<TUIGroupProfileModel>(context, listen: false).groupMemberList;
|
||||
|
||||
if (!isSearchTextExist(searchText)) {
|
||||
setState(() {
|
||||
searchMemberList = null;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
final res = await widget.model.searchGroupMember(V2TimGroupMemberSearchParam(
|
||||
keywordList: [searchText],
|
||||
groupIDList: [widget.model.groupInfo!.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(() {
|
||||
searchMemberList = currentGroupMember;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final TUITheme theme = value.theme;
|
||||
final isDesktopScreen = TUIKitScreenUtils.getFormFactor() == DeviceType.Desktop;
|
||||
return MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider.value(value: widget.model),
|
||||
],
|
||||
builder: (BuildContext context, Widget? w) {
|
||||
final TUIGroupProfileModel groupProfileModel = Provider.of<TUIGroupProfileModel>(context);
|
||||
String option1 = groupProfileModel.groupInfo?.memberCount.toString() ?? widget.memberList.length.toString();
|
||||
if (isDesktopScreen) {
|
||||
return GroupProfileMemberList(
|
||||
customTopArea: PlatformUtils().isWeb
|
||||
? null
|
||||
: GroupMemberSearchTextField(
|
||||
onTextChange: (text) => handleSearchGroupMembers(text, context),
|
||||
),
|
||||
memberList: searchMemberList ?? groupProfileModel.groupMemberList,
|
||||
removeMember: _kickedOffMember,
|
||||
touchBottomCallBack: () {},
|
||||
onTapMemberItem: (memberInfo, details) {
|
||||
if (widget.model.onClickUser != null) {
|
||||
widget.model.onClickUser!(memberInfo, details);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
TIM_t_para("群成员({{option1}}人)", "群成员($option1人)")(option1: option1),
|
||||
style: TextStyle(color: theme.appbarTextColor, fontSize: 17),
|
||||
),
|
||||
shadowColor: theme.weakBackgroundColor,
|
||||
backgroundColor: theme.appbarBgColor ?? theme.primaryColor,
|
||||
iconTheme: IconThemeData(
|
||||
color: theme.appbarTextColor,
|
||||
)),
|
||||
body: GroupProfileMemberList(
|
||||
customTopArea: PlatformUtils().isWeb
|
||||
? null
|
||||
: GroupMemberSearchTextField(
|
||||
onTextChange: (text) => handleSearchGroupMembers(text, context),
|
||||
),
|
||||
memberList: searchMemberList ?? groupProfileModel.groupMemberList,
|
||||
removeMember: _kickedOffMember,
|
||||
touchBottomCallBack: () {},
|
||||
onTapMemberItem: (memberInfo, details) {
|
||||
if (widget.model.onClickUser != null) {
|
||||
widget.model.onClickUser!(memberInfo, details);
|
||||
}
|
||||
},
|
||||
));
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
import 'package:flutter/cupertino.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';
|
||||
|
||||
enum GroupProfileWidgetEnum {
|
||||
/// The detail card for group.
|
||||
detailCard,
|
||||
|
||||
/// The tile shows the members in the group.
|
||||
memberListTile,
|
||||
|
||||
/// The entrance to the page editing the group notice.
|
||||
groupNotice,
|
||||
|
||||
/// The entrance to the page managing the group.
|
||||
/// Works only for group owner and group admin.
|
||||
groupManage,
|
||||
|
||||
/// The entrance to the search page with conversation ID.
|
||||
searchMessage,
|
||||
|
||||
/// The divider between sets of profile widget.
|
||||
operationDivider,
|
||||
|
||||
/// Shows the group type,
|
||||
/// includes "work group", "public group", "meeting group" and "AVChatRoom".
|
||||
groupTypeBar,
|
||||
|
||||
/// Shows and modify the mode or options users add the group.
|
||||
/// Includes "disallow group joining", "automatic approval" and "admin approval".
|
||||
groupJoiningModeBar,
|
||||
|
||||
/// Shows and modify the nickname for user in a specific group,
|
||||
/// only shows and used in this group, as the name card here.
|
||||
nameCardBar,
|
||||
|
||||
/// The switch of if mute the message notification from a specific group
|
||||
muteGroupMessageBar,
|
||||
|
||||
/// The switch of if pin this group to the top in conversation list.
|
||||
pinedConversationBar,
|
||||
|
||||
/// The button area, includes
|
||||
/// "clear chat history", "transfer group owner", "disband group" and "quit group"
|
||||
/// as default.
|
||||
buttonArea,
|
||||
|
||||
/// Custom area, you may define anything you want here.
|
||||
customBuilderOne,
|
||||
|
||||
/// Custom area, you may define anything you want here.
|
||||
customBuilderTwo,
|
||||
|
||||
/// Custom area, you may define anything you want here.
|
||||
customBuilderThree,
|
||||
|
||||
/// Custom area, you may define anything you want here.
|
||||
customBuilderFour,
|
||||
|
||||
/// Custom area, you may define anything you want here.
|
||||
customBuilderFive
|
||||
}
|
||||
|
||||
class GroupProfileWidgetBuilder {
|
||||
/// The detail card for group.
|
||||
Widget Function(V2TimGroupInfo groupInfo, Function(String updateGroupName)? updateGroupName)? detailCard;
|
||||
|
||||
/// The tile shows the members in the group.
|
||||
Widget Function(List<V2TimGroupMemberFullInfo?> memberList)? memberListTile;
|
||||
|
||||
/// The entrance to the page editing the group notice.
|
||||
Widget Function(String currentNotice, Function() toDefaultNoticeEditPage, Function(String newNotice) setGroupNotice)?
|
||||
groupNotice;
|
||||
|
||||
/// The entrance to the page managing the group.
|
||||
/// Works only for group owner and group admin.
|
||||
Widget Function(Function() toDefaultGroupManagementPage)? groupManage;
|
||||
|
||||
/// The entrance to the search page with conversation ID.
|
||||
Widget Function()? searchMessage;
|
||||
|
||||
/// The divider between sets of profile widget.
|
||||
Widget Function()? operationDivider;
|
||||
|
||||
/// Shows the group type,
|
||||
/// includes "work group", "public group", "meeting group" and "AVChatRoom".
|
||||
Widget Function(String groupType)? groupTypeBar;
|
||||
|
||||
/// Shows and modify the mode or options users add the group.
|
||||
/// Includes "0: disallow group joining", "1: admin approval" and "2: automatic approval".
|
||||
Widget Function(int groupAddOptType, Function(int addOpt) handleActionTap)? groupJoiningModeBar;
|
||||
|
||||
/// Shows and modify the nickname for user in a specific group,
|
||||
/// only shows and used in this group, as the name card here.
|
||||
Widget Function(String nameCard, Function(String newName) setNameCard)? nameCardBar;
|
||||
|
||||
/// The switch of if mute the message notification from a specific group.
|
||||
Widget Function(bool isMute, Function(bool isMute) setMute)? muteGroupMessageBar;
|
||||
|
||||
/// The switch of if pin this group to the top in conversation list.
|
||||
Widget Function(bool isPined, Function(bool isMute) pinedConversation)? pinedConversationBar;
|
||||
|
||||
/// The button area, includes
|
||||
/// "clear chat history", "transfer group owner", "disband group" and "quit group"
|
||||
/// as default.
|
||||
Widget Function(V2TimGroupInfo groupInfo, List<V2TimGroupMemberFullInfo?> groupMemberList)? buttonArea;
|
||||
|
||||
/// Custom area, you may define anything you want here.
|
||||
Widget Function(V2TimGroupInfo groupInfo, List<V2TimGroupMemberFullInfo?> groupMemberList)? customBuilderOne;
|
||||
|
||||
/// Custom area, you may define anything you want here.
|
||||
Widget Function(V2TimGroupInfo groupInfo, List<V2TimGroupMemberFullInfo?> groupMemberList)? customBuilderTwo;
|
||||
|
||||
/// Custom area, you may define anything you want here.
|
||||
Widget Function(V2TimGroupInfo groupInfo, List<V2TimGroupMemberFullInfo?> groupMemberList)? customBuilderThree;
|
||||
|
||||
/// Custom area, you may define anything you want here.
|
||||
Widget Function(V2TimGroupInfo groupInfo, List<V2TimGroupMemberFullInfo?> groupMemberList)? customBuilderFour;
|
||||
|
||||
/// Custom area, you may define anything you want here.
|
||||
Widget Function(V2TimGroupInfo groupInfo, List<V2TimGroupMemberFullInfo?> groupMemberList)? customBuilderFive;
|
||||
|
||||
GroupProfileWidgetBuilder(
|
||||
{this.detailCard,
|
||||
this.memberListTile,
|
||||
this.groupNotice,
|
||||
this.groupManage,
|
||||
this.searchMessage,
|
||||
this.operationDivider,
|
||||
this.groupTypeBar,
|
||||
this.groupJoiningModeBar,
|
||||
this.nameCardBar,
|
||||
this.muteGroupMessageBar,
|
||||
this.pinedConversationBar,
|
||||
this.buttonArea,
|
||||
this.customBuilderOne,
|
||||
this.customBuilderTwo,
|
||||
this.customBuilderThree,
|
||||
this.customBuilderFour,
|
||||
this.customBuilderFive});
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_group_profile_model.dart';
|
||||
|
||||
class SharedDataWidget extends InheritedWidget {
|
||||
final TUIGroupProfileModel model;
|
||||
|
||||
const SharedDataWidget({Key? key, required Widget child, required this.model})
|
||||
: super(key: key, child: child);
|
||||
|
||||
// Define a method to get the shared data from sub-tree
|
||||
static SharedDataWidget? of(BuildContext context) {
|
||||
return context.dependOnInheritedWidgetOfExactType<SharedDataWidget>();
|
||||
}
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(covariant SharedDataWidget oldWidget) {
|
||||
return oldWidget.model != model;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,323 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:loading_animation_widget/loading_animation_widget.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:tencent_chat_i18n_tool/tencent_chat_i18n_tool.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/enum/group_change_info_type.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/enum/group_member_role.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_uikit/base_widgets/tim_ui_kit_state.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/life_cycle/group_profile_life_cycle.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/listener_model/tui_group_listener_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_group_profile_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/services_locatar.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitGroupProfile/group_profile_widget.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitGroupProfile/widgets/tim_ui_group_profile_widget.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitGroupProfile/widgets/tim_uikit_group_button_area.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitGroupProfile/widgets/tim_uikit_group_manage.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitGroupProfile/widgets/tim_uikit_group_notification.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/tui_theme.dart';
|
||||
export 'package:tencent_cloud_chat_uikit/ui/widgets/transimit_group_owner_select.dart';
|
||||
|
||||
typedef GroupProfileBuilder = Widget Function(
|
||||
BuildContext context, V2TimGroupInfo groupInfo, List<V2TimGroupMemberFullInfo?> groupMemberList);
|
||||
|
||||
class TIMUIKitGroupProfile extends StatefulWidget {
|
||||
/// Group ID
|
||||
final String groupID;
|
||||
final Color? backGroundColor;
|
||||
|
||||
/// [Deprecated:] The builder for custom bottom operation area.
|
||||
/// [operationListBuilder] and [bottomOperationBuilder] merged into [builder], please use it instead.
|
||||
final Widget Function(
|
||||
BuildContext context, V2TimGroupInfo groupInfo, List<V2TimGroupMemberFullInfo?> groupMemberList)?
|
||||
bottomOperationBuilder;
|
||||
|
||||
/// [Deprecated:] The builder for custom bottom operation area.
|
||||
/// [operationListBuilder] and [bottomOperationBuilder] merged into [builder], please use it instead.
|
||||
final Widget Function(
|
||||
BuildContext context, V2TimGroupInfo groupInfo, List<V2TimGroupMemberFullInfo?> groupMemberList)?
|
||||
operationListBuilder;
|
||||
|
||||
/// [If you tend to customize the profile page, use [profileWidgetBuilder] with [profileWidgetsOrder] as priority.]
|
||||
/// The builder for each widgets in profile page,
|
||||
/// you can customize some of it by pass your own widget into here.
|
||||
/// Or, you can add your custom widget to the three custom widgets.
|
||||
final GroupProfileWidgetBuilder? profileWidgetBuilder;
|
||||
|
||||
/// [If you tend to customize the profile page, use [profileWidgetBuilder] with [profileWidgetsOrder] as priority.]
|
||||
/// If the default widget order can not meet you needs,
|
||||
/// you may change the order by this array with widget enum.
|
||||
final List<GroupProfileWidgetEnum>? profileWidgetsOrder;
|
||||
|
||||
/// The builder for the whole group profile page, you can use this to customize all the element here.
|
||||
/// Mentioned: If you use this builder, [profileWidgetBuilder] and [profileWidgetsOrder] will no longer works.
|
||||
final GroupProfileBuilder? builder;
|
||||
|
||||
/// The life cycle hooks for group profile business logic.
|
||||
/// You have better to implement the `didLeaveGroup` in it.
|
||||
final GroupProfileLifeCycle? lifeCycle;
|
||||
|
||||
/// The callback after user clicking a user,
|
||||
/// you may navigating to the specific profile page, or anywhere you want.
|
||||
final Function(V2TimGroupMemberFullInfo groupMemberFullInfo, TapDownDetails? tapDetails)? onClickUser;
|
||||
|
||||
const TIMUIKitGroupProfile(
|
||||
{Key? key,
|
||||
required this.groupID,
|
||||
this.backGroundColor,
|
||||
@Deprecated("[operationListBuilder] and [bottomOperationBuilder] merged into [builder], please use it instead")
|
||||
this.bottomOperationBuilder,
|
||||
@Deprecated("[operationListBuilder] and [bottomOperationBuilder] merged into [builder], please use it instead")
|
||||
this.operationListBuilder,
|
||||
this.builder,
|
||||
this.profileWidgetBuilder,
|
||||
this.onClickUser,
|
||||
this.profileWidgetsOrder,
|
||||
this.lifeCycle})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _TIMUIKitGroupProfileState();
|
||||
}
|
||||
|
||||
class _TIMUIKitGroupProfileState extends TIMUIKitState<TIMUIKitGroupProfile> {
|
||||
bool isSingleUse = false;
|
||||
final model = TUIGroupProfileModel();
|
||||
final TUIGroupListenerModel groupListenerModel = serviceLocator<TUIGroupListenerModel>();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
model.loadData(widget.groupID);
|
||||
model.onClickUser = widget.onClickUser;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant TIMUIKitGroupProfile oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.groupID != widget.groupID) {
|
||||
model.loadData(widget.groupID);
|
||||
}
|
||||
}
|
||||
|
||||
final List<GroupProfileWidgetEnum> _defaultWidgetOrder = [
|
||||
GroupProfileWidgetEnum.detailCard,
|
||||
GroupProfileWidgetEnum.operationDivider,
|
||||
GroupProfileWidgetEnum.memberListTile,
|
||||
GroupProfileWidgetEnum.operationDivider,
|
||||
GroupProfileWidgetEnum.searchMessage,
|
||||
GroupProfileWidgetEnum.operationDivider,
|
||||
GroupProfileWidgetEnum.groupNotice,
|
||||
GroupProfileWidgetEnum.groupManage,
|
||||
GroupProfileWidgetEnum.groupJoiningModeBar,
|
||||
GroupProfileWidgetEnum.groupTypeBar,
|
||||
GroupProfileWidgetEnum.operationDivider,
|
||||
GroupProfileWidgetEnum.pinedConversationBar,
|
||||
GroupProfileWidgetEnum.muteGroupMessageBar,
|
||||
GroupProfileWidgetEnum.operationDivider,
|
||||
GroupProfileWidgetEnum.nameCardBar,
|
||||
GroupProfileWidgetEnum.operationDivider,
|
||||
GroupProfileWidgetEnum.buttonArea
|
||||
];
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue buildValue) {
|
||||
final TUITheme theme = buildValue.theme;
|
||||
|
||||
return MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider.value(value: model),
|
||||
ChangeNotifierProvider.value(value: groupListenerModel),
|
||||
],
|
||||
builder: (context, w) {
|
||||
final model = Provider.of<TUIGroupProfileModel>(context);
|
||||
model.lifeCycle = widget.lifeCycle;
|
||||
final V2TimGroupInfo? groupInfo = model.groupInfo;
|
||||
final memberList = model.groupMemberList;
|
||||
final isDesktopScreen = TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop;
|
||||
if (groupInfo == null) {
|
||||
return Center(
|
||||
child: LoadingAnimationWidget.staggeredDotsWave(
|
||||
color: theme.weakTextColor ?? Colors.grey,
|
||||
size: 48,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final TUIGroupListenerModel groupListenerModel = Provider.of<TUIGroupListenerModel>(context);
|
||||
final NeedUpdate? needUpdate = groupListenerModel.needUpdate;
|
||||
if (needUpdate != null && needUpdate.groupID == widget.groupID) {
|
||||
groupListenerModel.needUpdate = null;
|
||||
switch (needUpdate.updateType) {
|
||||
case UpdateType.groupInfo:
|
||||
if (needUpdate.groupInfoSubType == GroupChangeInfoType.V2TIM_GROUP_INFO_CHANGE_TYPE_OWNER) {
|
||||
model.onOwnerChanged(needUpdate.ownerID);
|
||||
}
|
||||
|
||||
model.loadGroupInfo(widget.groupID);
|
||||
break;
|
||||
case UpdateType.memberList:
|
||||
model.loadGroupMemberList(groupID: widget.groupID);
|
||||
model.loadGroupInfo(widget.groupID);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
final isGroupOwner = groupInfo.role == GroupMemberRoleType.V2TIM_GROUP_MEMBER_ROLE_OWNER;
|
||||
final isAdmin = groupInfo.role == GroupMemberRoleType.V2TIM_GROUP_MEMBER_ROLE_ADMIN;
|
||||
|
||||
Widget groupProfilePage({required Widget child}) {
|
||||
return SingleChildScrollView(
|
||||
child: Container(
|
||||
color:
|
||||
widget.backGroundColor ?? (isDesktopScreen ? theme.wideBackgroundColor : theme.weakBackgroundColor),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void toDefaultNoticePage() {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
GroupProfileNotificationPage(model: model, notification: groupInfo.notification ?? "")));
|
||||
}
|
||||
|
||||
void toDefaultManagePage() {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => GroupProfileGroupManagePage(
|
||||
model: model,
|
||||
)));
|
||||
}
|
||||
|
||||
List<Widget> _renderWidgetsWithOrder(List<GroupProfileWidgetEnum> order) {
|
||||
final GroupProfileWidgetBuilder? customBuilder = widget.profileWidgetBuilder;
|
||||
return order.map((element) {
|
||||
switch (element) {
|
||||
case GroupProfileWidgetEnum.detailCard:
|
||||
return (customBuilder?.detailCard != null
|
||||
? customBuilder?.detailCard!(groupInfo, model.setGroupName)
|
||||
: TIMUIKitGroupProfileWidget.detailCard(
|
||||
isHavePermission: isAdmin || isGroupOwner, groupInfo: groupInfo))!;
|
||||
case GroupProfileWidgetEnum.memberListTile:
|
||||
return (customBuilder?.memberListTile != null
|
||||
? customBuilder?.memberListTile!(memberList)
|
||||
: TIMUIKitGroupProfileWidget.memberTile())!;
|
||||
case GroupProfileWidgetEnum.groupNotice:
|
||||
return (customBuilder?.groupNotice != null
|
||||
? customBuilder?.groupNotice!(
|
||||
groupInfo.notification ?? "", toDefaultNoticePage, model.setGroupNotification)
|
||||
: TIMUIKitGroupProfileWidget.groupNotification(isHavePermission: isAdmin || isGroupOwner))!;
|
||||
case GroupProfileWidgetEnum.groupManage:
|
||||
if (isAdmin || isGroupOwner) {
|
||||
return (customBuilder?.groupManage != null
|
||||
? customBuilder?.groupManage!(toDefaultManagePage)
|
||||
: TIMUIKitGroupProfileWidget.groupManage())!;
|
||||
} else {
|
||||
return Container();
|
||||
}
|
||||
case GroupProfileWidgetEnum.searchMessage:
|
||||
return (customBuilder?.searchMessage != null
|
||||
? customBuilder?.searchMessage!()
|
||||
: Text(TIM_t("你必须自定义search bar,并处理点击跳转")))!;
|
||||
case GroupProfileWidgetEnum.operationDivider:
|
||||
return (customBuilder?.operationDivider != null
|
||||
? customBuilder?.operationDivider!()
|
||||
: TIMUIKitGroupProfileWidget.operationDivider(theme))!;
|
||||
case GroupProfileWidgetEnum.groupTypeBar:
|
||||
return (customBuilder?.groupTypeBar != null
|
||||
? customBuilder?.groupTypeBar!(groupInfo.groupType)
|
||||
: TIMUIKitGroupProfileWidget.groupType())!;
|
||||
case GroupProfileWidgetEnum.groupJoiningModeBar:
|
||||
final String groupType = groupInfo.groupType;
|
||||
if (!(isGroupOwner || isAdmin) ||
|
||||
groupType == "Work" ||
|
||||
groupType == "Meeting" ||
|
||||
groupType == "AVChatRoom") {
|
||||
return Container();
|
||||
}
|
||||
return (customBuilder?.groupJoiningModeBar != null
|
||||
? customBuilder?.groupJoiningModeBar!(groupInfo.groupAddOpt ?? 1, model.setGroupAddOpt)
|
||||
: TIMUIKitGroupProfileWidget.groupAddOpt())!;
|
||||
case GroupProfileWidgetEnum.nameCardBar:
|
||||
return (customBuilder?.nameCardBar != null
|
||||
? customBuilder?.nameCardBar!(model.getSelfNameCard(), model.setNameCard)
|
||||
: TIMUIKitGroupProfileWidget.nameCard())!;
|
||||
case GroupProfileWidgetEnum.muteGroupMessageBar:
|
||||
return (customBuilder?.muteGroupMessageBar != null
|
||||
? customBuilder?.muteGroupMessageBar!(model.conversation?.recvOpt != 0, model.setMessageDisturb)
|
||||
: TIMUIKitGroupProfileWidget.messageDisturb())!;
|
||||
case GroupProfileWidgetEnum.pinedConversationBar:
|
||||
return (customBuilder?.pinedConversationBar != null
|
||||
? customBuilder?.pinedConversationBar!(
|
||||
model.conversation?.isPinned ?? false, model.pinedConversation)
|
||||
: TIMUIKitGroupProfileWidget.pinedConversation())!;
|
||||
case GroupProfileWidgetEnum.buttonArea:
|
||||
return (customBuilder?.buttonArea != null
|
||||
? customBuilder?.buttonArea!(groupInfo, memberList)
|
||||
: GroupProfileButtonArea(groupInfo.groupID, model))!;
|
||||
case GroupProfileWidgetEnum.customBuilderOne:
|
||||
return (customBuilder?.customBuilderOne != null
|
||||
? customBuilder?.customBuilderOne!(groupInfo, memberList)
|
||||
// Please define the corresponding custom widget in `profileWidgetBuilder` before using it here.
|
||||
: Text(TIM_t("如使用自定义区域,请在profileWidgetBuilder传入对应组件")))!;
|
||||
case GroupProfileWidgetEnum.customBuilderTwo:
|
||||
return (customBuilder?.customBuilderTwo != null
|
||||
? customBuilder?.customBuilderTwo!(groupInfo, memberList)
|
||||
// Please define the corresponding custom widget in `profileWidgetBuilder` before using it here.
|
||||
: Text(TIM_t("如使用自定义区域,请在profileWidgetBuilder传入对应组件")))!;
|
||||
case GroupProfileWidgetEnum.customBuilderThree:
|
||||
return (customBuilder?.customBuilderThree != null
|
||||
? customBuilder?.customBuilderThree!(groupInfo, memberList)
|
||||
// Please define the corresponding custom widget in `profileWidgetBuilder` before using it here.
|
||||
: Text(TIM_t("如使用自定义区域,请在profileWidgetBuilder传入对应组件")))!;
|
||||
case GroupProfileWidgetEnum.customBuilderFour:
|
||||
return (customBuilder?.customBuilderFour != null
|
||||
? customBuilder?.customBuilderFour!(groupInfo, memberList)
|
||||
// Please define the corresponding custom widget in `profileWidgetBuilder` before using it here.
|
||||
: Text(TIM_t("如使用自定义区域,请在profileWidgetBuilder传入对应组件")))!;
|
||||
case GroupProfileWidgetEnum.customBuilderFive:
|
||||
return (customBuilder?.customBuilderFive != null
|
||||
? customBuilder?.customBuilderFive!(groupInfo, memberList)
|
||||
// Please define the corresponding custom widget in `profileWidgetBuilder` before using it here.
|
||||
: Text(TIM_t("如使用自定义区域,请在profileWidgetBuilder传入对应组件")))!;
|
||||
default:
|
||||
return Container();
|
||||
}
|
||||
}).toList();
|
||||
}
|
||||
|
||||
if (widget.builder != null) {
|
||||
return groupProfilePage(
|
||||
child: widget.builder!(context, groupInfo, memberList),
|
||||
);
|
||||
} else if (widget.profileWidgetsOrder != null) {
|
||||
return groupProfilePage(
|
||||
child: Column(
|
||||
children: [..._renderWidgetsWithOrder(widget.profileWidgetsOrder!)],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return groupProfilePage(
|
||||
child: Column(
|
||||
children: [..._renderWidgetsWithOrder(_defaultWidgetOrder)],
|
||||
));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
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/optimize_utils.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitSearch/pureUI/tim_uikit_search_input.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/color.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/tui_theme.dart';
|
||||
|
||||
class GroupMemberSearchTextField extends TIMUIKitStatelessWidget {
|
||||
final Function(String text) onTextChange;
|
||||
GroupMemberSearchTextField({Key? key, required this.onTextChange})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final TUITheme theme = value.theme;
|
||||
final isDesktopScreen =
|
||||
TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop;
|
||||
final FocusNode focusNode = FocusNode();
|
||||
|
||||
var debounceFunc = OptimizeUtils.debounce(
|
||||
(text) => onTextChange(text), const Duration(milliseconds: 300));
|
||||
|
||||
return Container(
|
||||
color: Colors.white,
|
||||
child: Column(children: [
|
||||
if(!isDesktopScreen) Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(5.0)),
|
||||
border: Border.all(color: theme.weakBackgroundColor!, width: 12)),
|
||||
child: TextField(
|
||||
onChanged: debounceFunc,
|
||||
decoration: InputDecoration(
|
||||
hintText: TIM_t("搜索"),
|
||||
prefixIcon: const Icon(Icons.search),
|
||||
),
|
||||
),
|
||||
),
|
||||
if(isDesktopScreen) TIMUIKitSearchInput(prefixIcon: Icon(
|
||||
Icons.search,
|
||||
size: 16,
|
||||
color: hexToColor("979797"),
|
||||
),
|
||||
onChange: (text){
|
||||
focusNode.requestFocus();
|
||||
debounceFunc(text);
|
||||
}, focusNode: focusNode,
|
||||
),
|
||||
Divider(
|
||||
thickness: 1,
|
||||
indent: 74,
|
||||
endIndent: 0,
|
||||
color: theme.weakBackgroundColor,
|
||||
height: 0)
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_conversation.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_info.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_group_info.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_search_msg.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitGroupProfile/widgets/tim_uikit_group_add_opt.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitGroupProfile/widgets/tim_uikit_group_detail_card.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitGroupProfile/widgets/tim_uikit_group_manage.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitGroupProfile/widgets/tim_uikit_group_member_title.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitGroupProfile/widgets/tim_uikit_group_message_disturb.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitGroupProfile/widgets/tim_uikit_group_name_card.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitGroupProfile/widgets/tim_uikit_group_notification.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitGroupProfile/widgets/tim_uikit_group_pin_conversation.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitGroupProfile/widgets/tim_uikit_group_type.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/tui_theme.dart';
|
||||
|
||||
class TIMUIKitGroupProfileWidget {
|
||||
static Widget detailCard(
|
||||
{required V2TimGroupInfo groupInfo,
|
||||
bool isHavePermission = false,
|
||||
|
||||
/// You can deal with updating group name manually, or UIKIt do it automatically.
|
||||
Function(String updateGroupName)? updateGroupName}) {
|
||||
return GroupProfileDetailCard(
|
||||
groupInfo: groupInfo,
|
||||
isHavePermission: isHavePermission,
|
||||
updateGroupName: updateGroupName,
|
||||
);
|
||||
}
|
||||
|
||||
static Widget memberTile() {
|
||||
return GroupMemberTitle();
|
||||
}
|
||||
|
||||
static Widget groupNotification({
|
||||
bool isHavePermission = false,
|
||||
}) {
|
||||
return GroupProfileNotification(
|
||||
isHavePermission: isHavePermission,
|
||||
);
|
||||
}
|
||||
|
||||
static Widget groupManage() {
|
||||
return const GroupProfileGroupManage();
|
||||
}
|
||||
|
||||
static Widget searchMessage(Function(V2TimConversation?) onJumpToSearch) {
|
||||
return GroupProfileGroupSearch(onJumpToSearch: onJumpToSearch);
|
||||
}
|
||||
|
||||
static Widget operationDivider(TUITheme theme) {
|
||||
final isDesktopScreen = TUIKitScreenUtils.getFormFactor() == DeviceType.Desktop;
|
||||
return Container(
|
||||
color: theme.weakDividerColor,
|
||||
height: isDesktopScreen ? 1 : 10,
|
||||
);
|
||||
}
|
||||
|
||||
static Widget groupType() {
|
||||
return GroupProfileType();
|
||||
}
|
||||
|
||||
static Widget groupAddOpt() {
|
||||
return GroupProfileAddOpt();
|
||||
}
|
||||
|
||||
static Widget nameCard() {
|
||||
return const GroupProfileNameCard();
|
||||
}
|
||||
|
||||
static Widget messageDisturb() {
|
||||
return GroupMessageDisturb();
|
||||
}
|
||||
|
||||
static Widget pinedConversation() {
|
||||
return GroupPinConversation();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// ignore_for_file: unused_import
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:tencent_chat_i18n_tool/tencent_chat_i18n_tool.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_conversation.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_statelesswidget.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_group_profile_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/services_locatar.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitSearch/tim_uikit_search.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/conversation/conversation_services.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitGroupProfile/shared_data_widget.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/color.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/tui_theme.dart';
|
||||
|
||||
class GroupProfileGroupSearch extends TIMUIKitStatelessWidget {
|
||||
GroupProfileGroupSearch({Key? key, required this.onJumpToSearch}) : super(key: key);
|
||||
final ConversationService _conversationService = serviceLocator<ConversationService>();
|
||||
|
||||
final Function(V2TimConversation?) onJumpToSearch;
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final TUITheme theme = value.theme;
|
||||
|
||||
final model = Provider.of<TUIGroupProfileModel>(context);
|
||||
|
||||
return InkWell(
|
||||
onTap: () async {
|
||||
V2TimConversation? conversation =
|
||||
await _conversationService.getConversation(conversationID: "group_${model.groupInfo!.groupID}");
|
||||
if (conversation != null) {
|
||||
onJumpToSearch(conversation);
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: Border(bottom: BorderSide(color: theme.weakDividerColor ?? CommonColor.weakDividerColor))),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
TIM_t("查找聊天内容"),
|
||||
style: TextStyle(fontSize: 16, color: theme.darkTextColor),
|
||||
),
|
||||
Icon(Icons.keyboard_arrow_right, color: theme.weakTextColor)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:tencent_chat_i18n_tool/tencent_chat_i18n_tool.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/enum/group_add_opt_type.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/core/tim_uikit_wide_modal_operation_key.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/column_menu.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/wide_popup.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_statelesswidget.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_group_profile_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/tui_theme.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/color.dart';
|
||||
|
||||
class GroupProfileAddOpt extends TIMUIKitStatelessWidget {
|
||||
GroupProfileAddOpt({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final TUITheme theme = value.theme;
|
||||
final model = Provider.of<TUIGroupProfileModel>(context);
|
||||
final isDesktopScreen =
|
||||
TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop;
|
||||
|
||||
String addOpt = TIM_t("未知");
|
||||
|
||||
final groupAddOpt = model.groupInfo?.groupAddOpt;
|
||||
switch (groupAddOpt) {
|
||||
case GroupAddOptType.V2TIM_GROUP_ADD_ANY:
|
||||
addOpt = TIM_t("自动审批");
|
||||
break;
|
||||
case GroupAddOptType.V2TIM_GROUP_ADD_AUTH:
|
||||
addOpt = TIM_t("管理员审批");
|
||||
break;
|
||||
case GroupAddOptType.V2TIM_GROUP_ADD_FORBID:
|
||||
addOpt = TIM_t("禁止加群");
|
||||
break;
|
||||
}
|
||||
|
||||
final actionList = [
|
||||
{"label": TIM_t("禁止加群"), "id": GroupAddOptType.V2TIM_GROUP_ADD_FORBID},
|
||||
{"label": TIM_t("自动审批"), "id": GroupAddOptType.V2TIM_GROUP_ADD_ANY},
|
||||
{"label": TIM_t("管理员审批"), "id": GroupAddOptType.V2TIM_GROUP_ADD_AUTH}
|
||||
];
|
||||
|
||||
_handleActionTap(int addOpt) async {
|
||||
model.setGroupAddOpt(addOpt).then((res) {});
|
||||
}
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: isDesktopScreen ? null : Border(
|
||||
bottom: BorderSide(
|
||||
color:
|
||||
theme.weakDividerColor ?? CommonColor.weakDividerColor))),
|
||||
child: InkWell(
|
||||
onTapDown: (details) async {
|
||||
if(isDesktopScreen){
|
||||
TUIKitWidePopup.showPopupWindow(
|
||||
operationKey: TUIKitWideModalOperationKey.groupAddOpt,
|
||||
isDarkBackground: false,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(4)),
|
||||
context: context,
|
||||
offset: Offset(min(details.globalPosition.dx,
|
||||
MediaQuery.of(context).size.width - 186), details.globalPosition.dy),
|
||||
child: (onClose) => TUIKitColumnMenu(
|
||||
data: [
|
||||
...actionList
|
||||
.map((e){
|
||||
return ColumnMenuItem(label: e["label"] as String, onClick: (){
|
||||
_handleActionTap(e["id"] as int);
|
||||
onClose();
|
||||
});
|
||||
}),
|
||||
],
|
||||
)
|
||||
);
|
||||
}else{
|
||||
showCupertinoModalPopup<String>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return CupertinoActionSheet(
|
||||
title: Text(TIM_t("加群方式")),
|
||||
cancelButton: CupertinoActionSheetAction(
|
||||
onPressed: () {
|
||||
Navigator.pop(
|
||||
context,
|
||||
"cancel",
|
||||
);
|
||||
},
|
||||
child: Text(TIM_t("取消")),
|
||||
isDefaultAction: false,
|
||||
),
|
||||
actions: actionList
|
||||
.map((e) => CupertinoActionSheetAction(
|
||||
onPressed: () {
|
||||
_handleActionTap(e["id"] as int);
|
||||
Navigator.pop(
|
||||
context,
|
||||
"cancel",
|
||||
);
|
||||
},
|
||||
child: Text(
|
||||
e["label"] as String,
|
||||
style: TextStyle(color: theme.primaryColor),
|
||||
),
|
||||
isDefaultAction: false,
|
||||
))
|
||||
.toList(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
TIM_t("加群方式"),
|
||||
style: TextStyle(fontSize: isDesktopScreen ? 14 : 16, color: theme.darkTextColor),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
addOpt,
|
||||
style: TextStyle(fontSize: isDesktopScreen ? 14 : 16, color: Colors.black),
|
||||
),
|
||||
Icon(Icons.keyboard_arrow_right, color: theme.weakTextColor)
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,355 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:tencent_chat_i18n_tool/tencent_chat_i18n_tool.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_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_statelesswidget.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_group_profile_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/core/tim_uikit_wide_modal_operation_key.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/controller/tim_uikit_chat_controller.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/platform.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/wide_popup.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_callback.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/color.dart';
|
||||
import '/theme/tui_theme.dart';
|
||||
|
||||
class GroupProfileButtonArea extends TIMUIKitStatelessWidget {
|
||||
final String groupID;
|
||||
final TUIGroupProfileModel model;
|
||||
final sdkInstance = TIMUIKitCore.getSDKInstance();
|
||||
final coreInstance = TIMUIKitCore.getInstance();
|
||||
final TIMUIKitChatController _timuiKitChatController = TIMUIKitChatController();
|
||||
|
||||
GroupProfileButtonArea(this.groupID, this.model, {Key? key}) : super(key: key);
|
||||
|
||||
final _operationList = [
|
||||
{"label": TIM_t("清空消息"), "id": "clearHistory"},
|
||||
{"label": TIM_t("转让群主"), "id": "transimitOwner"},
|
||||
{"label": TIM_t("退出群组"), "id": "quitGroup"},
|
||||
{"label": TIM_t("解散群组"), "id": "dismissGroup"}
|
||||
];
|
||||
|
||||
_clearHistory(BuildContext context, theme) async {
|
||||
final isDesktopScreen = TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop;
|
||||
|
||||
if (isDesktopScreen) {
|
||||
TUIKitWidePopup.showSecondaryConfirmDialog(
|
||||
operationKey: TUIKitWideModalOperationKey.confirmClearChatHistory,
|
||||
context: context,
|
||||
text: TIM_t("清空聊天记录"),
|
||||
theme: theme,
|
||||
onCancel: () {},
|
||||
onConfirm: () async {
|
||||
if (PlatformUtils().isWeb) {
|
||||
final res =
|
||||
await sdkInstance.getConversationManager().deleteConversation(conversationID: "group_$groupID");
|
||||
if (res.code == 0) {
|
||||
_timuiKitChatController.clearHistory(groupID);
|
||||
}
|
||||
} else {
|
||||
final res = await sdkInstance.getMessageManager().clearGroupHistoryMessage(groupID: groupID);
|
||||
if (res.code == 0) {
|
||||
_timuiKitChatController.clearHistory(groupID);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
showCupertinoModalPopup<String>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return CupertinoActionSheet(
|
||||
cancelButton: CupertinoActionSheetAction(
|
||||
onPressed: () {
|
||||
Navigator.pop(
|
||||
context,
|
||||
);
|
||||
},
|
||||
child: Text(TIM_t("取消")),
|
||||
isDefaultAction: false,
|
||||
),
|
||||
actions: [
|
||||
CupertinoActionSheetAction(
|
||||
onPressed: () async {
|
||||
Navigator.pop(
|
||||
context,
|
||||
);
|
||||
if (PlatformUtils().isWeb) {
|
||||
final res =
|
||||
await sdkInstance.getConversationManager().deleteConversation(conversationID: "group_$groupID");
|
||||
if (res.code == 0) {
|
||||
_timuiKitChatController.clearHistory(groupID);
|
||||
}
|
||||
} else {
|
||||
final res = await sdkInstance.getMessageManager().clearGroupHistoryMessage(groupID: groupID);
|
||||
if (res.code == 0) {
|
||||
_timuiKitChatController.clearHistory(groupID);
|
||||
}
|
||||
}
|
||||
},
|
||||
child: Text(
|
||||
TIM_t("确定"),
|
||||
style: TextStyle(color: theme.cautionColor),
|
||||
),
|
||||
isDefaultAction: false,
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_quitGroup(BuildContext context, TUITheme theme) async {
|
||||
final isDesktopScreen = TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop;
|
||||
|
||||
if (isDesktopScreen) {
|
||||
TUIKitWidePopup.showSecondaryConfirmDialog(
|
||||
operationKey: TUIKitWideModalOperationKey.confirmExitGroup,
|
||||
context: context,
|
||||
text: TIM_t("退出后不会接收到此群聊消息"),
|
||||
theme: theme,
|
||||
onCancel: () {},
|
||||
onConfirm: () async {
|
||||
final res = await sdkInstance.quitGroup(groupID: groupID);
|
||||
if (res.code == 0) {
|
||||
final deleteConvRes =
|
||||
await sdkInstance.getConversationManager().deleteConversation(conversationID: "group_$groupID");
|
||||
if (deleteConvRes.code == 0) {
|
||||
model.lifeCycle?.didLeaveGroup();
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
showCupertinoModalPopup<String>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return CupertinoActionSheet(
|
||||
title: Text(TIM_t("退出后不会接收到此群聊消息")),
|
||||
cancelButton: CupertinoActionSheetAction(
|
||||
onPressed: () {
|
||||
Navigator.pop(
|
||||
context,
|
||||
);
|
||||
},
|
||||
child: Text(TIM_t("取消")),
|
||||
isDefaultAction: false,
|
||||
),
|
||||
actions: [
|
||||
CupertinoActionSheetAction(
|
||||
onPressed: () async {
|
||||
Navigator.pop(
|
||||
context,
|
||||
);
|
||||
final res = await sdkInstance.quitGroup(groupID: groupID);
|
||||
if (res.code == 0) {
|
||||
final deleteConvRes =
|
||||
await sdkInstance.getConversationManager().deleteConversation(conversationID: "group_$groupID");
|
||||
if (deleteConvRes.code == 0) {
|
||||
model.lifeCycle?.didLeaveGroup();
|
||||
}
|
||||
onTIMCallback(TIMCallback(
|
||||
type: TIMCallbackType.INFO,
|
||||
infoRecommendText: "${TIM_t("您已退出")}${model.groupInfo?.groupName}",
|
||||
infoCode: 6661402));
|
||||
}
|
||||
},
|
||||
child: Text(
|
||||
TIM_t("确定"),
|
||||
style: TextStyle(color: theme.cautionColor),
|
||||
),
|
||||
isDefaultAction: false,
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_dismissGroup(BuildContext context, theme) async {
|
||||
final isDesktopScreen = TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop;
|
||||
|
||||
if (isDesktopScreen) {
|
||||
TUIKitWidePopup.showSecondaryConfirmDialog(
|
||||
operationKey: TUIKitWideModalOperationKey.confirmDisbandGroup,
|
||||
context: context,
|
||||
text: TIM_t("解散后不会接收到此群聊消息"),
|
||||
theme: theme,
|
||||
onCancel: () {},
|
||||
onConfirm: () async {
|
||||
final res = await sdkInstance.dismissGroup(groupID: groupID);
|
||||
if (res.code == 0) {
|
||||
await sdkInstance.getConversationManager().deleteConversation(conversationID: "group_$groupID");
|
||||
model.lifeCycle?.didLeaveGroup();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
showCupertinoModalPopup<String>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return CupertinoActionSheet(
|
||||
title: Text(TIM_t("解散后不会接收到此群聊消息")),
|
||||
cancelButton: CupertinoActionSheetAction(
|
||||
onPressed: () {
|
||||
Navigator.pop(
|
||||
context,
|
||||
);
|
||||
},
|
||||
child: Text(TIM_t("取消")),
|
||||
isDefaultAction: false,
|
||||
),
|
||||
actions: [
|
||||
CupertinoActionSheetAction(
|
||||
onPressed: () async {
|
||||
final res = await sdkInstance.dismissGroup(groupID: groupID);
|
||||
if (res.code == 0) {
|
||||
await sdkInstance.getConversationManager().deleteConversation(conversationID: "group_$groupID");
|
||||
model.lifeCycle?.didLeaveGroup();
|
||||
}
|
||||
},
|
||||
child: Text(
|
||||
TIM_t("确定"),
|
||||
style: TextStyle(color: theme.cautionColor),
|
||||
),
|
||||
isDefaultAction: false,
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_transmitOwner(BuildContext context, String groupID) async {
|
||||
final isDesktopScreen = TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop;
|
||||
|
||||
if (isDesktopScreen) {
|
||||
TUIKitWidePopup.showPopupWindow(
|
||||
operationKey: TUIKitWideModalOperationKey.setAdmins,
|
||||
context: context,
|
||||
title: TIM_t("转让群主"),
|
||||
width: MediaQuery.of(context).size.width * 0.5,
|
||||
height: MediaQuery.of(context).size.height * 0.8,
|
||||
onSubmit: () {
|
||||
selectNewGroupOwnerKey.currentState?.onSubmit();
|
||||
},
|
||||
child: (onClose) => SelectNewGroupOwner(
|
||||
model: model,
|
||||
key: selectNewGroupOwnerKey,
|
||||
groupID: groupID,
|
||||
onSelectedMember: (selectedMember) async {
|
||||
if (selectedMember.isNotEmpty) {
|
||||
final userID = selectedMember.first.userID;
|
||||
await sdkInstance.getGroupManager().transferGroupOwner(groupID: groupID, userID: userID);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
} else {
|
||||
List<V2TimGroupMemberFullInfo>? selectedMember = await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => SelectNewGroupOwner(
|
||||
model: model,
|
||||
groupID: groupID,
|
||||
),
|
||||
),
|
||||
);
|
||||
if (selectedMember != null) {
|
||||
final userID = selectedMember.first.userID;
|
||||
await sdkInstance.getGroupManager().transferGroupOwner(groupID: groupID, userID: userID);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<Widget> _renderGroupOperation(BuildContext context, TUITheme theme, bool isOwner, String groupType) {
|
||||
final isDesktopScreen = TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop;
|
||||
return _operationList
|
||||
.where((element) {
|
||||
if (!isOwner) {
|
||||
return ["quitGroup", "clearHistory"].contains(element["id"]);
|
||||
} else {
|
||||
if (groupType == "Work") {
|
||||
return ["clearHistory", "quitGroup", "transimitOwner"].contains(element["id"]);
|
||||
}
|
||||
if (groupType != "Work") {
|
||||
return ["clearHistory", "dismissGroup", "transimitOwner"].contains(element["id"]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
})
|
||||
.map((e) => isDesktopScreen
|
||||
? OutlinedButton(
|
||||
onPressed: () {
|
||||
if (e["id"]! == "clearHistory") {
|
||||
_clearHistory(context, theme);
|
||||
} else if (e["id"] == "quitGroup") {
|
||||
_quitGroup(context, theme);
|
||||
} else if (e["id"] == "dismissGroup") {
|
||||
_dismissGroup(context, theme);
|
||||
} else if (e["id"] == "transimitOwner") {
|
||||
_transmitOwner(context, groupID);
|
||||
}
|
||||
},
|
||||
child: Text(
|
||||
e["label"]!,
|
||||
style: TextStyle(color: theme.cautionColor),
|
||||
))
|
||||
: InkWell(
|
||||
onTap: () {
|
||||
if (e["id"]! == "clearHistory") {
|
||||
_clearHistory(context, theme);
|
||||
} else if (e["id"] == "quitGroup") {
|
||||
_quitGroup(context, theme);
|
||||
} else if (e["id"] == "dismissGroup") {
|
||||
_dismissGroup(context, theme);
|
||||
} else if (e["id"] == "transimitOwner") {
|
||||
_transmitOwner(context, groupID);
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
padding: const EdgeInsets.symmetric(vertical: 15),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
border:
|
||||
Border(bottom: BorderSide(color: theme.weakDividerColor ?? CommonColor.weakDividerColor))),
|
||||
child: Text(
|
||||
e["label"]!,
|
||||
style: TextStyle(color: theme.cautionColor, fontSize: 17),
|
||||
),
|
||||
),
|
||||
))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final theme = value.theme;
|
||||
final groupInfo = model.groupInfo;
|
||||
|
||||
final isDesktopScreen = TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop;
|
||||
if (isDesktopScreen) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Wrap(
|
||||
spacing: 28,
|
||||
children: [
|
||||
..._renderGroupOperation(
|
||||
context, theme, groupInfo?.owner == coreInstance.loginUserInfo?.userID, groupInfo?.groupType ?? "")
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
..._renderGroupOperation(
|
||||
context, theme, groupInfo?.owner == coreInstance.loginUserInfo?.userID, groupInfo?.groupType ?? "")
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:tencent_chat_i18n_tool/tencent_chat_i18n_tool.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/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_uikit/base_widgets/tim_ui_kit_statelesswidget.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_group_profile_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/text_input_bottom_sheet.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/avatar.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 GroupProfileDetailCard extends TIMUIKitStatelessWidget {
|
||||
final V2TimGroupInfo groupInfo;
|
||||
final void Function(String groupName)? updateGroupName;
|
||||
final TextEditingController controller = TextEditingController();
|
||||
final bool isHavePermission;
|
||||
|
||||
GroupProfileDetailCard({Key? key, required this.groupInfo, this.isHavePermission = false, this.updateGroupName})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final TUITheme theme = value.theme;
|
||||
final model = Provider.of<TUIGroupProfileModel>(context);
|
||||
final faceUrl = groupInfo.faceUrl ?? "";
|
||||
final groupID = groupInfo.groupID;
|
||||
final showName = groupInfo.groupName ?? groupID;
|
||||
final isDesktopScreen = TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop;
|
||||
|
||||
return InkWell(
|
||||
onTapDown: !isHavePermission
|
||||
? null
|
||||
: ((details) {
|
||||
if (isDesktopScreen) {
|
||||
TextInputBottomSheet.showTextInputBottomSheet(
|
||||
context: context,
|
||||
title: TIM_t("修改群名称"),
|
||||
initText: showName,
|
||||
initOffset: Offset(min(details.globalPosition.dx, MediaQuery.of(context).size.width - 350),
|
||||
min(details.globalPosition.dy + 20, MediaQuery.of(context).size.height - 470)),
|
||||
onSubmitted: (String newText) async {
|
||||
final text = newText.trim();
|
||||
if (updateGroupName != null) {
|
||||
updateGroupName!(text);
|
||||
} else {
|
||||
model.setGroupName(text);
|
||||
}
|
||||
},
|
||||
theme: theme);
|
||||
} else {
|
||||
showCupertinoModalPopup<String>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return CupertinoActionSheet(
|
||||
cancelButton: CupertinoActionSheetAction(
|
||||
onPressed: () {
|
||||
Navigator.pop(
|
||||
context,
|
||||
);
|
||||
},
|
||||
child: Text(TIM_t("取消")),
|
||||
isDefaultAction: false,
|
||||
),
|
||||
actions: [
|
||||
CupertinoActionSheetAction(
|
||||
onPressed: () {
|
||||
controller.text = groupInfo.groupName ?? "";
|
||||
showModalBottomSheet(
|
||||
isScrollControlled: true,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10.0),
|
||||
),
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return Container(
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(10.0), topRight: Radius.circular(10.0))),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 20),
|
||||
child: Text(TIM_t("修改群名称")),
|
||||
),
|
||||
Divider(height: 2, color: theme.weakDividerColor),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TextField(
|
||||
controller: controller,
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
fillColor: theme.weakBackgroundColor,
|
||||
filled: true,
|
||||
isDense: true,
|
||||
hintText: ''),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
Text(
|
||||
TIM_t("修改群名称"),
|
||||
style: TextStyle(fontSize: 13, color: theme.weakTextColor),
|
||||
textAlign: TextAlign.left,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 30,
|
||||
),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
final text = controller.text.trim();
|
||||
if (updateGroupName != null) {
|
||||
updateGroupName!(text);
|
||||
} else {
|
||||
model.setGroupName(text);
|
||||
}
|
||||
Navigator.pop(context);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: Text(TIM_t("确定")),
|
||||
)),
|
||||
const SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
Padding(
|
||||
padding:
|
||||
EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
child: Text(
|
||||
TIM_t("修改群名称"),
|
||||
style: TextStyle(color: theme.primaryColor),
|
||||
),
|
||||
isDefaultAction: false,
|
||||
)
|
||||
]);
|
||||
},
|
||||
);
|
||||
}
|
||||
}),
|
||||
child: Container(
|
||||
color: Colors.white,
|
||||
padding: EdgeInsets.only(
|
||||
top: isDesktopScreen ? 20 : 12,
|
||||
bottom: isDesktopScreen ? 20 : 12,
|
||||
right: isDesktopScreen ? 16 : 0,
|
||||
left: 16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: isDesktopScreen ? 40 : 48,
|
||||
height: isDesktopScreen ? 40 : 48,
|
||||
child: Avatar(
|
||||
faceUrl: faceUrl,
|
||||
showName: showName,
|
||||
type: 2,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(left: 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SelectableText(
|
||||
showName,
|
||||
style: TextStyle(fontSize: isDesktopScreen ? 15 : 18, fontWeight: FontWeight.w600),
|
||||
),
|
||||
SizedBox(
|
||||
height: isDesktopScreen ? 4 : 8,
|
||||
),
|
||||
SelectableText("ID: $groupID",
|
||||
style: TextStyle(fontSize: isDesktopScreen ? 13 : 13, color: theme.weakTextColor))
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (isHavePermission)
|
||||
Icon(
|
||||
Icons.keyboard_arrow_right,
|
||||
color: theme.weakTextColor,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,817 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_slidable_plus_plus/flutter_slidable_plus_plus.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:tencent_chat_i18n_tool/tencent_chat_i18n_tool.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/enum/group_member_role.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/enum/group_type.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/tencent_im_sdk_plugin.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_group_profile_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/ui/utils/screen_utils.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitProfile/widget/tim_uikit_operation_item.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/avatar.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/column_menu.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/radio_button.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/wide_popup.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_callback.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/color.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/tui_theme.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/tui_theme_view_model.dart';
|
||||
|
||||
GlobalKey<_GroupProfileAddAdminState> groupProfileAddAdminKey = GlobalKey();
|
||||
|
||||
class GroupProfileGroupManage extends StatefulWidget {
|
||||
const GroupProfileGroupManage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => GroupProfileGroupManageState();
|
||||
}
|
||||
|
||||
class GroupProfileGroupManageState extends TIMUIKitState<GroupProfileGroupManage> {
|
||||
bool isShowManageBox = false;
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final TUITheme theme = value.theme;
|
||||
final isDesktopScreen = TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop;
|
||||
final model = Provider.of<TUIGroupProfileModel>(context);
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: isDesktopScreen
|
||||
? null
|
||||
: Border(bottom: BorderSide(color: theme.weakDividerColor ?? CommonColor.weakDividerColor))),
|
||||
child: Column(
|
||||
children: [
|
||||
InkWell(
|
||||
onTap: () {
|
||||
final isDesktopScreen = TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop;
|
||||
if (!isDesktopScreen) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => GroupProfileGroupManagePage(
|
||||
model: model,
|
||||
)));
|
||||
} else {
|
||||
setState(() {
|
||||
isShowManageBox = !isShowManageBox;
|
||||
});
|
||||
}
|
||||
},
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
TIM_t("群管理"),
|
||||
style: TextStyle(fontSize: isDesktopScreen ? 14 : 16, color: theme.darkTextColor),
|
||||
),
|
||||
AnimatedRotation(
|
||||
turns: isShowManageBox ? 0.25 : 0,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: Icon(Icons.keyboard_arrow_right, color: theme.weakTextColor),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
if (isShowManageBox)
|
||||
GroupProfileGroupManagePage(
|
||||
model: model,
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 管理员设置页面
|
||||
class GroupProfileGroupManagePage extends StatefulWidget {
|
||||
final TUIGroupProfileModel model;
|
||||
|
||||
const GroupProfileGroupManagePage({
|
||||
Key? key,
|
||||
required this.model,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _GroupProfileGroupManagePageState();
|
||||
}
|
||||
|
||||
class _GroupProfileGroupManagePageState extends TIMUIKitState<GroupProfileGroupManagePage> {
|
||||
int? serverTime;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
getServerTime();
|
||||
}
|
||||
|
||||
void getServerTime() async {
|
||||
final res = await TencentImSDKPlugin.v2TIMManager.getServerTime();
|
||||
setState(() {
|
||||
serverTime = res.data;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
return MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider.value(value: widget.model),
|
||||
ChangeNotifierProvider.value(value: serviceLocator<TUIThemeViewModel>())
|
||||
],
|
||||
builder: (context, w) {
|
||||
final memberList = Provider.of<TUIGroupProfileModel>(context).groupMemberList;
|
||||
final theme = Provider.of<TUIThemeViewModel>(context).theme;
|
||||
final isAllMuted = widget.model.groupInfo?.isAllMuted ?? false;
|
||||
final bool isAllowMuteMember = (widget.model.groupInfo?.groupType ?? "") != GroupType.Work;
|
||||
final isDesktopScreen = TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop;
|
||||
|
||||
Widget managePage() {
|
||||
return Column(
|
||||
children: [
|
||||
Container(
|
||||
padding: EdgeInsets.only(
|
||||
top: 12,
|
||||
left: isDesktopScreen ? 0 : 16,
|
||||
bottom: isDesktopScreen ? 0 : 12,
|
||||
right: isDesktopScreen ? 0 : 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: isDesktopScreen
|
||||
? null
|
||||
: Border(bottom: BorderSide(color: theme.weakDividerColor ?? CommonColor.weakDividerColor))),
|
||||
child: InkWell(
|
||||
onTap: isDesktopScreen
|
||||
? null
|
||||
: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => GroupProfileSetManagerPage(
|
||||
model: widget.model,
|
||||
),
|
||||
));
|
||||
},
|
||||
child: isDesktopScreen
|
||||
? Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(TIM_t("群管理员"), style: TextStyle(fontSize: 14, color: theme.darkTextColor)),
|
||||
],
|
||||
)
|
||||
: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(TIM_t("设置管理员"),
|
||||
style: TextStyle(fontSize: isDesktopScreen ? 14 : 16, color: theme.darkTextColor)),
|
||||
Icon(Icons.keyboard_arrow_right, color: theme.weakTextColor)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (isDesktopScreen)
|
||||
GroupProfileSetManagerPage(
|
||||
model: widget.model,
|
||||
),
|
||||
if (!isDesktopScreen)
|
||||
Container(
|
||||
padding: const EdgeInsets.only(top: 12, left: 16, bottom: 12, right: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
border:
|
||||
Border(bottom: BorderSide(color: theme.weakDividerColor ?? CommonColor.weakDividerColor))),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
TIM_t("全员禁言"),
|
||||
style: TextStyle(fontSize: 16, color: theme.darkTextColor),
|
||||
),
|
||||
CupertinoSwitch(
|
||||
value: isAllMuted,
|
||||
onChanged: (value) async {
|
||||
widget.model.setMuteAll(value);
|
||||
},
|
||||
activeColor: theme.primaryColor)
|
||||
],
|
||||
),
|
||||
),
|
||||
if (isDesktopScreen && isAllowMuteMember)
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(TIM_t("禁言"), style: TextStyle(fontSize: 14, color: theme.darkTextColor)),
|
||||
],
|
||||
),
|
||||
if (isDesktopScreen)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: TIMUIKitOperationItem(
|
||||
isEmpty: false,
|
||||
operationName: TIM_t("全员禁言"),
|
||||
type: "switch",
|
||||
isUseCheckedBoxOnWide: true,
|
||||
operationDescription: TIM_t("全员禁言开启后,只允许群主和管理员发言。"),
|
||||
operationValue: isAllMuted,
|
||||
onSwitchChange: (value) {
|
||||
widget.model.setMuteAll(value);
|
||||
},
|
||||
),
|
||||
),
|
||||
if (!isDesktopScreen)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 16),
|
||||
color: theme.weakBackgroundColor,
|
||||
alignment: Alignment.topLeft,
|
||||
child: Text(
|
||||
TIM_t("全员禁言开启后,只允许群主和管理员发言。"),
|
||||
style: TextStyle(fontSize: 12, color: theme.weakTextColor),
|
||||
),
|
||||
),
|
||||
if (!isAllMuted && isAllowMuteMember)
|
||||
InkWell(
|
||||
child: Container(
|
||||
color: Colors.white,
|
||||
padding: const EdgeInsets.only(left: 16),
|
||||
child: Container(
|
||||
padding: !isDesktopScreen
|
||||
? const EdgeInsets.symmetric(
|
||||
vertical: 12,
|
||||
)
|
||||
: const EdgeInsets.only(
|
||||
bottom: 4,
|
||||
),
|
||||
decoration: isDesktopScreen
|
||||
? null
|
||||
: BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: Border(
|
||||
bottom:
|
||||
BorderSide(color: theme.weakDividerColor ?? CommonColor.weakDividerColor))),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.add_circle_outline,
|
||||
color: theme.primaryColor,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(
|
||||
width: 12,
|
||||
),
|
||||
Text(TIM_t("添加需要禁言的群成员"))
|
||||
],
|
||||
),
|
||||
)),
|
||||
onTap: () async {
|
||||
Widget muteMember() {
|
||||
return GroupProfileAddAdmin(
|
||||
key: groupProfileAddAdminKey,
|
||||
appbarTitle: TIM_t("设置禁言"),
|
||||
memberList: memberList.where((element) {
|
||||
final isMute = (serverTime != null ? (element?.muteUntil ?? 0) > serverTime! : false);
|
||||
final isMember = element!.role == GroupMemberRoleType.V2TIM_GROUP_MEMBER_ROLE_MEMBER;
|
||||
return !isMute && isMember;
|
||||
}).toList(),
|
||||
selectCompletedHandler: (context, selectedMember) async {
|
||||
if (selectedMember.isNotEmpty) {
|
||||
for (var member in selectedMember) {
|
||||
final userID = member!.userID;
|
||||
widget.model.muteGroupMember(userID, true, serverTime);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (isDesktopScreen) {
|
||||
TUIKitWidePopup.showPopupWindow(
|
||||
operationKey: TUIKitWideModalOperationKey.setMute,
|
||||
context: context,
|
||||
title: TIM_t("设置禁言"),
|
||||
width: MediaQuery.of(context).size.width * 0.5,
|
||||
height: MediaQuery.of(context).size.height * 0.8,
|
||||
onSubmit: () {
|
||||
groupProfileAddAdminKey.currentState?.onSubmit();
|
||||
},
|
||||
child: (onClose) => muteMember());
|
||||
} else {
|
||||
Navigator.push(context, MaterialPageRoute(builder: (context) => muteMember()));
|
||||
}
|
||||
},
|
||||
),
|
||||
if (!isAllMuted && isAllowMuteMember)
|
||||
...memberList
|
||||
.where((element) => (serverTime != null ? (element?.muteUntil ?? 0) > serverTime! : false))
|
||||
.map((e) => Container(
|
||||
padding: isDesktopScreen ? const EdgeInsets.only(left: 16) : null,
|
||||
child: GestureDetector(
|
||||
onSecondaryTapDown: (details) {
|
||||
TUIKitWidePopup.showPopupWindow(
|
||||
operationKey: TUIKitWideModalOperationKey.setUnmute,
|
||||
isDarkBackground: false,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(4)),
|
||||
context: context,
|
||||
offset: Offset(
|
||||
min(details.globalPosition.dx, MediaQuery.of(context).size.width - 80),
|
||||
details.globalPosition.dy),
|
||||
child: (onClose) => TUIKitColumnMenu(data: [
|
||||
ColumnMenuItem(
|
||||
label: TIM_t("删除"),
|
||||
icon: const Icon(Icons.remove_circle_outline, size: 16),
|
||||
onClick: () {
|
||||
widget.model.muteGroupMember(e.userID, false, serverTime);
|
||||
onClose();
|
||||
}),
|
||||
]));
|
||||
},
|
||||
child: _buildListItem(
|
||||
context,
|
||||
e!,
|
||||
ActionPane(motion: const DrawerMotion(), children: [
|
||||
SlidableAction(
|
||||
onPressed: (_) {
|
||||
widget.model.muteGroupMember(e.userID, false, serverTime);
|
||||
},
|
||||
flex: 1,
|
||||
backgroundColor: theme.cautionColor ?? CommonColor.cautionColor,
|
||||
autoClose: true,
|
||||
label: TIM_t("删除"),
|
||||
)
|
||||
])),
|
||||
),
|
||||
))
|
||||
.toList()
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return TUIKitScreenUtils.getDeviceWidget(
|
||||
context: context,
|
||||
desktopWidget: managePage(),
|
||||
defaultWidget: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
TIM_t("群管理"),
|
||||
style: TextStyle(color: theme.appbarTextColor, fontSize: 17),
|
||||
),
|
||||
backgroundColor: theme.appbarBgColor ?? theme.primaryColor,
|
||||
shadowColor: theme.weakDividerColor,
|
||||
iconTheme: IconThemeData(
|
||||
color: theme.appbarTextColor,
|
||||
),
|
||||
leading: 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 {
|
||||
if (isAllMuted != widget.model.groupInfo?.isAllMuted) {
|
||||
widget.model.setMuteAll(isAllMuted);
|
||||
}
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
),
|
||||
body: managePage(),
|
||||
));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_getShowName(V2TimGroupMemberFullInfo? item) {
|
||||
final friendRemark = item?.friendRemark ?? "";
|
||||
final nameCard = item?.nameCard ?? "";
|
||||
final nickName = item?.nickName ?? "";
|
||||
final userID = item?.userID ?? "";
|
||||
return friendRemark.isNotEmpty
|
||||
? friendRemark
|
||||
: nameCard.isNotEmpty
|
||||
? nameCard
|
||||
: nickName.isNotEmpty
|
||||
? nickName
|
||||
: userID;
|
||||
}
|
||||
|
||||
Widget _buildListItem(BuildContext context, V2TimGroupMemberFullInfo memberInfo, ActionPane? endActionPane) {
|
||||
final theme = Provider.of<TUIThemeViewModel>(context).theme;
|
||||
final isDesktopScreen = TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop;
|
||||
|
||||
Widget nameItem() {
|
||||
return Container(
|
||||
color: Colors.white,
|
||||
child: Column(children: [
|
||||
ListTile(
|
||||
tileColor: Colors.black,
|
||||
leading: SizedBox(
|
||||
width: isDesktopScreen ? 30 : 36,
|
||||
height: isDesktopScreen ? 30 : 36,
|
||||
child: Avatar(
|
||||
faceUrl: memberInfo.faceUrl ?? "",
|
||||
showName: _getShowName(memberInfo),
|
||||
type: 2,
|
||||
),
|
||||
),
|
||||
title: Row(
|
||||
children: [
|
||||
Text(_getShowName(memberInfo), style: TextStyle(fontSize: isDesktopScreen ? 14 : 16)),
|
||||
],
|
||||
),
|
||||
onTap: () {},
|
||||
),
|
||||
if (!isDesktopScreen) Divider(thickness: 1, indent: 74, endIndent: 0, color: theme.weakDividerColor, height: 0)
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
return TUIKitScreenUtils.getDeviceWidget(
|
||||
context: context,
|
||||
desktopWidget: nameItem(),
|
||||
defaultWidget: SingleChildScrollView(child: Slidable(endActionPane: endActionPane, child: nameItem())));
|
||||
}
|
||||
|
||||
/// 选择管理员
|
||||
class GroupProfileSetManagerPage extends StatefulWidget {
|
||||
final TUIGroupProfileModel model;
|
||||
|
||||
const GroupProfileSetManagerPage({Key? key, required this.model}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _GroupProfileSetManagerPageState();
|
||||
}
|
||||
|
||||
class _GroupProfileSetManagerPageState extends TIMUIKitState<GroupProfileSetManagerPage> {
|
||||
List<V2TimGroupMemberFullInfo?> _getAdminMemberList(List<V2TimGroupMemberFullInfo?> memberList) {
|
||||
return memberList.where((member) => member?.role == GroupMemberRoleType.V2TIM_GROUP_MEMBER_ROLE_ADMIN).toList();
|
||||
}
|
||||
|
||||
List<V2TimGroupMemberFullInfo?> _getOwnerList(List<V2TimGroupMemberFullInfo?> memberList) {
|
||||
return memberList.where((member) => member?.role == GroupMemberRoleType.V2TIM_GROUP_MEMBER_ROLE_OWNER).toList();
|
||||
}
|
||||
|
||||
_removeAdmin(BuildContext context, V2TimGroupMemberFullInfo memberFullInfo) async {
|
||||
final res = await widget.model.setMemberToNormal(memberFullInfo.userID);
|
||||
if (res.code == 0) {
|
||||
onTIMCallback(TIMCallback(type: TIMCallbackType.INFO, infoRecommendText: TIM_t("成功取消管理员身份"), infoCode: 6661003));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final TUITheme theme = value.theme;
|
||||
|
||||
return MultiProvider(
|
||||
providers: [ChangeNotifierProvider.value(value: widget.model)],
|
||||
builder: (context, w) {
|
||||
final model = Provider.of<TUIGroupProfileModel>(context);
|
||||
final memberList = model.groupMemberList;
|
||||
final adminList = _getAdminMemberList(memberList);
|
||||
final ownerList = _getOwnerList(memberList);
|
||||
final String option2 = adminList.length.toString();
|
||||
final isDesktopScreen = TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop;
|
||||
|
||||
Widget adminPage() {
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
if (!isDesktopScreen)
|
||||
Container(
|
||||
alignment: Alignment.topLeft,
|
||||
color: theme.weakDividerColor,
|
||||
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 16),
|
||||
child: Text(
|
||||
TIM_t("群主"),
|
||||
style: TextStyle(fontSize: 14, color: theme.weakTextColor),
|
||||
),
|
||||
),
|
||||
if (isDesktopScreen)
|
||||
Container(
|
||||
alignment: Alignment.topLeft,
|
||||
padding: const EdgeInsets.only(top: 10, bottom: 4, left: 16),
|
||||
child: Text(
|
||||
TIM_t("群主"),
|
||||
style: TextStyle(fontSize: 14, color: theme.primaryColor),
|
||||
),
|
||||
),
|
||||
...ownerList
|
||||
.map(
|
||||
(e) => Container(
|
||||
padding: isDesktopScreen ? const EdgeInsets.only(left: 16) : null,
|
||||
child: _buildListItem(context, e!, null),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
if (!isDesktopScreen)
|
||||
Container(
|
||||
alignment: Alignment.topLeft,
|
||||
color: theme.weakDividerColor,
|
||||
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 16),
|
||||
child: Text(
|
||||
TIM_t_para("管理员 ({{option2}}/10)", "管理员 ($option2/10)")(option2: option2),
|
||||
style: TextStyle(fontSize: 14, color: theme.weakTextColor),
|
||||
),
|
||||
),
|
||||
if (isDesktopScreen)
|
||||
Container(
|
||||
alignment: Alignment.topLeft,
|
||||
padding: const EdgeInsets.only(top: 10, bottom: 4, left: 16),
|
||||
child: Text(
|
||||
TIM_t_para("管理员 ({{option2}}/10)", "管理员 ($option2/10)")(option2: option2),
|
||||
style: TextStyle(fontSize: 14, color: theme.primaryColor),
|
||||
),
|
||||
),
|
||||
InkWell(
|
||||
child: Container(
|
||||
color: Colors.white,
|
||||
padding: const EdgeInsets.only(left: 16),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 12,
|
||||
),
|
||||
decoration: isDesktopScreen
|
||||
? null
|
||||
: BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: Border(
|
||||
bottom: BorderSide(color: theme.weakDividerColor ?? CommonColor.weakDividerColor))),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.add_circle_outline,
|
||||
color: theme.primaryColor,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(
|
||||
width: 12,
|
||||
),
|
||||
Text(TIM_t("添加管理员"))
|
||||
],
|
||||
),
|
||||
)),
|
||||
onTap: () async {
|
||||
if (isDesktopScreen) {
|
||||
TUIKitWidePopup.showPopupWindow(
|
||||
operationKey: TUIKitWideModalOperationKey.setAdmins,
|
||||
context: context,
|
||||
title: TIM_t("设置管理员"),
|
||||
width: MediaQuery.of(context).size.width * 0.5,
|
||||
height: MediaQuery.of(context).size.height * 0.8,
|
||||
onSubmit: () {
|
||||
groupProfileAddAdminKey.currentState?.onSubmit();
|
||||
},
|
||||
child: (onClose) => GroupProfileAddAdmin(
|
||||
key: groupProfileAddAdminKey,
|
||||
memberList: memberList
|
||||
.where(
|
||||
(element) => element?.role == GroupMemberRoleType.V2TIM_GROUP_MEMBER_ROLE_MEMBER)
|
||||
.toList(),
|
||||
appbarTitle: TIM_t("设置管理员"),
|
||||
selectCompletedHandler: (context, selectedMember) async {
|
||||
if (selectedMember.isNotEmpty) {
|
||||
for (var member in selectedMember) {
|
||||
final userID = member!.userID;
|
||||
widget.model.setMemberToAdmin(userID);
|
||||
}
|
||||
}
|
||||
},
|
||||
));
|
||||
} else {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => GroupProfileAddAdmin(
|
||||
key: groupProfileAddAdminKey,
|
||||
memberList: memberList
|
||||
.where((element) =>
|
||||
element?.role == GroupMemberRoleType.V2TIM_GROUP_MEMBER_ROLE_MEMBER)
|
||||
.toList(),
|
||||
appbarTitle: TIM_t("设置管理员"),
|
||||
selectCompletedHandler: (context, selectedMember) async {
|
||||
if (selectedMember.isNotEmpty) {
|
||||
for (var member in selectedMember) {
|
||||
final userID = member!.userID;
|
||||
widget.model.setMemberToAdmin(userID);
|
||||
}
|
||||
}
|
||||
},
|
||||
)));
|
||||
}
|
||||
},
|
||||
),
|
||||
...adminList
|
||||
.map((e) => GestureDetector(
|
||||
onSecondaryTapDown: (details) {
|
||||
TUIKitWidePopup.showPopupWindow(
|
||||
operationKey: TUIKitWideModalOperationKey.deleteAdmin,
|
||||
isDarkBackground: false,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(4)),
|
||||
context: context,
|
||||
offset: Offset(min(details.globalPosition.dx, MediaQuery.of(context).size.width - 80),
|
||||
details.globalPosition.dy),
|
||||
child: (onClose) => TUIKitColumnMenu(data: [
|
||||
ColumnMenuItem(
|
||||
label: TIM_t("删除"),
|
||||
icon: const Icon(Icons.remove_circle_outline, size: 16),
|
||||
onClick: () {
|
||||
_removeAdmin(context, e);
|
||||
onClose();
|
||||
}),
|
||||
]));
|
||||
},
|
||||
child: Container(
|
||||
padding: isDesktopScreen ? const EdgeInsets.only(left: 16) : null,
|
||||
child: _buildListItem(
|
||||
context,
|
||||
e!,
|
||||
ActionPane(motion: const DrawerMotion(), children: [
|
||||
SlidableAction(
|
||||
onPressed: (_) {
|
||||
_removeAdmin(context, e);
|
||||
},
|
||||
flex: 1,
|
||||
backgroundColor: theme.cautionColor ?? CommonColor.cautionColor,
|
||||
autoClose: true,
|
||||
label: TIM_t("删除"),
|
||||
)
|
||||
])),
|
||||
),
|
||||
))
|
||||
.toList(),
|
||||
],
|
||||
));
|
||||
}
|
||||
|
||||
return TUIKitScreenUtils.getDeviceWidget(
|
||||
context: context,
|
||||
desktopWidget: adminPage(),
|
||||
defaultWidget: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
TIM_t("设置管理员"),
|
||||
style: TextStyle(color: theme.appbarTextColor, fontSize: 17),
|
||||
),
|
||||
shadowColor: theme.weakDividerColor,
|
||||
backgroundColor: theme.appbarBgColor ?? theme.primaryColor,
|
||||
iconTheme: IconThemeData(
|
||||
color: theme.appbarTextColor,
|
||||
),
|
||||
),
|
||||
body: adminPage(),
|
||||
));
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 添加管理员
|
||||
class GroupProfileAddAdmin extends StatefulWidget {
|
||||
final List<V2TimGroupMemberFullInfo?> memberList;
|
||||
final String appbarTitle;
|
||||
final void Function(BuildContext context, List<V2TimGroupMemberFullInfo?> selectedMemberList)? selectCompletedHandler;
|
||||
|
||||
const GroupProfileAddAdmin(
|
||||
{Key? key, required this.memberList, this.selectCompletedHandler, required this.appbarTitle})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _GroupProfileAddAdminState();
|
||||
}
|
||||
|
||||
class _GroupProfileAddAdminState extends TIMUIKitState<GroupProfileAddAdmin> {
|
||||
List<V2TimGroupMemberFullInfo?> selectedMemberList = [];
|
||||
|
||||
void onSubmit() {
|
||||
if (widget.selectCompletedHandler != null) {
|
||||
widget.selectCompletedHandler!(context, selectedMemberList);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final TUITheme theme = value.theme;
|
||||
|
||||
Widget addAdminPage() {
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
alignment: Alignment.topLeft,
|
||||
color: theme.weakDividerColor,
|
||||
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 16),
|
||||
child: Text(
|
||||
TIM_t("群成员"),
|
||||
style: TextStyle(fontSize: 14, color: theme.weakTextColor),
|
||||
),
|
||||
),
|
||||
...widget.memberList
|
||||
.map((e) => Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
border:
|
||||
Border(bottom: BorderSide(color: theme.weakDividerColor ?? CommonColor.weakDividerColor))),
|
||||
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 16),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
final isChecked = selectedMemberList.contains(e);
|
||||
if (isChecked) {
|
||||
selectedMemberList.remove(e);
|
||||
} else {
|
||||
selectedMemberList.add(e);
|
||||
}
|
||||
setState(() {});
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
CheckBoxButton(
|
||||
onlyShow: true,
|
||||
isChecked: selectedMemberList.contains(e),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 10,
|
||||
),
|
||||
SizedBox(
|
||||
width: 36,
|
||||
height: 36,
|
||||
child: Avatar(
|
||||
faceUrl: e?.faceUrl ?? "",
|
||||
showName: _getShowName(e),
|
||||
type: 2,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 10,
|
||||
),
|
||||
Text(_getShowName(e), style: const TextStyle(fontSize: 16))
|
||||
],
|
||||
),
|
||||
),
|
||||
))
|
||||
.toList(),
|
||||
],
|
||||
));
|
||||
}
|
||||
|
||||
return TUIKitScreenUtils.getDeviceWidget(
|
||||
context: context,
|
||||
desktopWidget: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: addAdminPage(),
|
||||
),
|
||||
defaultWidget: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
widget.appbarTitle,
|
||||
style: TextStyle(color: theme.appbarTextColor, fontSize: 17),
|
||||
),
|
||||
shadowColor: theme.weakDividerColor,
|
||||
backgroundColor: theme.appbarBgColor ?? theme.primaryColor,
|
||||
iconTheme: IconThemeData(
|
||||
color: theme.appbarTextColor,
|
||||
),
|
||||
leadingWidth: 80,
|
||||
leading: TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(
|
||||
TIM_t("取消"),
|
||||
style: TextStyle(
|
||||
color: theme.appbarTextColor,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
onSubmit();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(
|
||||
TIM_t("完成"),
|
||||
style: TextStyle(
|
||||
color: theme.appbarTextColor,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
body: addAdminPage()));
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user