init
This commit is contained in:
@@ -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()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,312 @@
|
||||
// ignore_for_file: unused_element
|
||||
|
||||
import 'package:dotted_border/dotted_border.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_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/ui/utils/screen_utils.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitGroupProfile/group_member/tui_add_group_member.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitGroupProfile/group_member/tui_delete_group_member.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitGroupProfile/group_member/tui_group_member_list.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/avatar.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';
|
||||
|
||||
class GroupMemberTitle extends TIMUIKitStatelessWidget {
|
||||
GroupMemberTitle({
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
List<V2TimGroupMemberFullInfo?> _getMemberList(memberList, int showRange) {
|
||||
if (memberList.length > showRange) {
|
||||
return memberList.getRange(0, showRange).toList();
|
||||
} else {
|
||||
return memberList;
|
||||
}
|
||||
}
|
||||
|
||||
_getShowName(V2TimGroupMemberFullInfo? item) {
|
||||
final friendRemark = item?.friendRemark ?? "";
|
||||
final nickName = item?.nickName ?? "";
|
||||
final userID = item?.userID;
|
||||
final showName = nickName != "" ? nickName : userID;
|
||||
return friendRemark != "" ? friendRemark : showName;
|
||||
}
|
||||
|
||||
List<Widget> _groupMemberListBuilder(List memberList, TUITheme theme, TUIGroupProfileModel model, int showRange) {
|
||||
final isDesktopScreen = TUIKitScreenUtils.getFormFactor() == DeviceType.Desktop;
|
||||
return _getMemberList(memberList, showRange).map((element) {
|
||||
final faceUrl = element?.faceUrl ?? "";
|
||||
final showName = _getShowName(element);
|
||||
return InkWell(
|
||||
onTapDown: (details) {
|
||||
if (model.onClickUser != null && element?.userID != null) {
|
||||
model.onClickUser!(element!, details);
|
||||
}
|
||||
},
|
||||
child: SizedBox(
|
||||
width: isDesktopScreen ? 36 : 60,
|
||||
height: isDesktopScreen ? 36 : 76,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: isDesktopScreen ? 36 : 50,
|
||||
height: isDesktopScreen ? 36 : 50,
|
||||
child: Avatar(
|
||||
borderRadius: isDesktopScreen ? BorderRadius.circular(18) : null,
|
||||
faceUrl: faceUrl,
|
||||
showName: showName,
|
||||
type: 1,
|
||||
),
|
||||
),
|
||||
if (!isDesktopScreen)
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
if (!isDesktopScreen)
|
||||
Text(
|
||||
showName,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(overflow: TextOverflow.ellipsis, color: theme.weakTextColor, fontSize: 10),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
List<Widget> _inviteMemberBuilder(bool isCanInviteMember, bool isCanKickOffMember, theme, BuildContext context) {
|
||||
return [];
|
||||
}
|
||||
|
||||
void navigateToMemberList(
|
||||
BuildContext context, TUIGroupProfileModel model, List<V2TimGroupMemberFullInfo?> memberList) {
|
||||
final isDesktopScreen = TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop;
|
||||
if (!isDesktopScreen) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => GroupProfileMemberListPage(model: model, memberList: memberList),
|
||||
));
|
||||
} else {
|
||||
final option1 = memberList.length.toString();
|
||||
TUIKitWidePopup.showPopupWindow(
|
||||
operationKey: TUIKitWideModalOperationKey.groupMembersList,
|
||||
context: context,
|
||||
width: MediaQuery.of(context).size.width * 0.5,
|
||||
height: MediaQuery.of(context).size.height * 0.8,
|
||||
title: TIM_t_para("群成员({{option1}}人)", "群成员($option1人)")(option1: option1),
|
||||
child: (onClose) => GroupProfileMemberListPage(model: model, memberList: memberList));
|
||||
}
|
||||
}
|
||||
|
||||
@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);
|
||||
final memberAmount = model.groupInfo?.memberCount ?? 0;
|
||||
final option1 = memberAmount.toString();
|
||||
final memberList = model.groupMemberList;
|
||||
final isCanInviteMember = model.canInviteMember();
|
||||
final isCanKickOffMember = model.canKickOffMember();
|
||||
|
||||
int showRange = isDesktopScreen ? 7 : 8;
|
||||
if (isDesktopScreen && isCanInviteMember) {
|
||||
showRange--;
|
||||
}
|
||||
if (isDesktopScreen && isCanKickOffMember) {
|
||||
showRange--;
|
||||
}
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
color: Colors.white,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
decoration: isDesktopScreen
|
||||
? null
|
||||
: BoxDecoration(
|
||||
border: Border(bottom: BorderSide(color: theme.weakDividerColor ?? CommonColor.weakDividerColor))),
|
||||
child: InkWell(
|
||||
onTap: () async {
|
||||
navigateToMemberList(context, model, memberList);
|
||||
},
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(TIM_t("群成员"), style: TextStyle(color: theme.darkTextColor, fontSize: isDesktopScreen ? 14 : 16)),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
TIM_t_para("{{option1}}人", "$option1人")(option1: option1),
|
||||
style: TextStyle(color: theme.darkTextColor, fontSize: isDesktopScreen ? 14 : 16),
|
||||
),
|
||||
Icon(
|
||||
Icons.keyboard_arrow_right,
|
||||
color: theme.weakTextColor,
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (isDesktopScreen)
|
||||
InkWell(
|
||||
onTap: () async {
|
||||
navigateToMemberList(context, model, memberList);
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(width: 1, color: theme.weakDividerColor ?? CommonColor.weakDividerColor),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(4)),
|
||||
),
|
||||
// height: 30,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.search,
|
||||
color: hexToColor("979797"),
|
||||
size: 16,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(TIM_t("搜索"),
|
||||
style: TextStyle(
|
||||
color: theme.weakTextColor,
|
||||
fontSize: 12,
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
// height: 90,
|
||||
padding: const EdgeInsets.only(top: 12),
|
||||
child: Wrap(
|
||||
spacing: isDesktopScreen ? 10 : 20,
|
||||
runSpacing: 10,
|
||||
alignment: WrapAlignment.start,
|
||||
children: [
|
||||
..._groupMemberListBuilder(memberList, theme, model, showRange),
|
||||
if (isCanInviteMember)
|
||||
DottedBorder(
|
||||
borderType: BorderType.RRect,
|
||||
radius: Radius.circular(isDesktopScreen ? 18 : 4.5),
|
||||
color: theme.weakTextColor!,
|
||||
dashPattern: const [6, 3],
|
||||
child: SizedBox(
|
||||
width: isDesktopScreen ? 32 : 48,
|
||||
height: isDesktopScreen ? 32 : 48,
|
||||
child: IconButton(
|
||||
onPressed: () {
|
||||
if (isDesktopScreen) {
|
||||
TUIKitWidePopup.showPopupWindow(
|
||||
context: context,
|
||||
operationKey: TUIKitWideModalOperationKey.addGroupMembers,
|
||||
width: 350,
|
||||
title: TIM_t("添加群成员"),
|
||||
height: 460,
|
||||
onSubmit: () {
|
||||
addGroupMemberKey.currentState?.submitAdd();
|
||||
},
|
||||
child: (onClose) => AddGroupMemberPage(
|
||||
model: model,
|
||||
onClose: onClose,
|
||||
key: addGroupMemberKey,
|
||||
));
|
||||
} else {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => AddGroupMemberPage(
|
||||
model: model,
|
||||
),
|
||||
));
|
||||
}
|
||||
},
|
||||
icon: Icon(
|
||||
Icons.add,
|
||||
size: isDesktopScreen ? 16 : 18,
|
||||
),
|
||||
color: theme.weakTextColor,
|
||||
),
|
||||
)),
|
||||
if (isCanKickOffMember)
|
||||
DottedBorder(
|
||||
borderType: BorderType.RRect,
|
||||
radius: Radius.circular(isDesktopScreen ? 18 : 4.5),
|
||||
color: theme.weakTextColor!,
|
||||
dashPattern: const [6, 3],
|
||||
child: SizedBox(
|
||||
width: isDesktopScreen ? 32 : 48,
|
||||
height: isDesktopScreen ? 32 : 48,
|
||||
child: IconButton(
|
||||
onPressed: () {
|
||||
if (isDesktopScreen) {
|
||||
TUIKitWidePopup.showPopupWindow(
|
||||
operationKey: TUIKitWideModalOperationKey.kickOffGroupMembers,
|
||||
context: context,
|
||||
width: 350,
|
||||
title: TIM_t("删除群成员"),
|
||||
height: 460,
|
||||
onSubmit: () {
|
||||
deleteGroupMemberKey.currentState?.submitDelete();
|
||||
},
|
||||
child: (onClose) => DeleteGroupMemberPage(
|
||||
model: model,
|
||||
onClose: onClose,
|
||||
key: deleteGroupMemberKey,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => DeleteGroupMemberPage(model: model),
|
||||
));
|
||||
}
|
||||
},
|
||||
icon: Icon(
|
||||
Icons.remove,
|
||||
size: isDesktopScreen ? 16 : 18,
|
||||
),
|
||||
color: theme.weakTextColor,
|
||||
),
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (memberList.length > showRange)
|
||||
InkWell(
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
margin: EdgeInsets.only(top: isDesktopScreen ? 12 : 16),
|
||||
child: Text(
|
||||
TIM_t("查看更多群成员"),
|
||||
style: TextStyle(color: theme.weakTextColor, fontSize: isDesktopScreen ? 12 : 14),
|
||||
),
|
||||
),
|
||||
onTap: () async {
|
||||
navigateToMemberList(context, model, memberList);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
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/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/views/TIMUIKitProfile/widget/tim_uikit_operation_item.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart';
|
||||
|
||||
class GroupMessageDisturb extends TIMUIKitStatelessWidget {
|
||||
GroupMessageDisturb({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final model = Provider.of<TUIGroupProfileModel>(context);
|
||||
final isShowDisturb = model.groupInfo?.groupType == "Meeting" ? false : true;
|
||||
final isDisturb = model.conversation?.recvOpt != 0;
|
||||
if (!isShowDisturb) {
|
||||
return Container();
|
||||
}
|
||||
return TIMUIKitOperationItem(
|
||||
isEmpty: false,
|
||||
operationName: TIM_t("消息免打扰"),
|
||||
type: "switch",
|
||||
isUseCheckedBoxOnWide: true,
|
||||
operationValue: isDisturb,
|
||||
onSwitchChange: (value) {
|
||||
model.setMessageDisturb(value);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
// ignore_for_file: unnecessary_null_comparison
|
||||
|
||||
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/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/text_input_bottom_sheet.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 GroupProfileNameCard extends StatefulWidget {
|
||||
const GroupProfileNameCard({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => GroupProfileNameCardState();
|
||||
|
||||
}
|
||||
|
||||
class GroupProfileNameCardState extends TIMUIKitState<GroupProfileNameCard>{
|
||||
final TextEditingController controller = TextEditingController();
|
||||
String? nameCard;
|
||||
|
||||
@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);
|
||||
if (model == null) {
|
||||
return Container();
|
||||
}
|
||||
nameCard = model.getSelfNameCard();
|
||||
controller.text = nameCard ?? "";
|
||||
|
||||
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: GestureDetector(
|
||||
onTap: () async {
|
||||
if (!isDesktopScreen) {
|
||||
TextInputBottomSheet.showTextInputBottomSheet(
|
||||
context: context,
|
||||
title: TIM_t("修改我的群昵称"),
|
||||
tips: TIM_t("仅限中文、字母、数字和下划线,2-20个字"),
|
||||
onSubmitted: (String nameCard) async {
|
||||
final text = nameCard.trim();
|
||||
model.setNameCard(text);
|
||||
},
|
||||
theme: theme);
|
||||
}
|
||||
},
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Container(
|
||||
margin: const EdgeInsets.only(right: 10),
|
||||
child: Text(
|
||||
TIM_t("我的群昵称"),
|
||||
style: TextStyle(
|
||||
fontSize: isDesktopScreen ? 14 : 16,
|
||||
color: theme.darkTextColor),
|
||||
),
|
||||
),
|
||||
if (!isDesktopScreen)
|
||||
Expanded(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
nameCard ?? "",
|
||||
style: TextStyle(
|
||||
fontSize: isDesktopScreen ? 14 : 16,
|
||||
color: theme.darkTextColor),
|
||||
)),
|
||||
Icon(Icons.keyboard_arrow_right,
|
||||
color: theme.weakTextColor)
|
||||
],
|
||||
)),
|
||||
],
|
||||
),
|
||||
if (isDesktopScreen)
|
||||
Text(
|
||||
TIM_t("仅限中文、字母、数字和下划线,2-20个字"),
|
||||
style: TextStyle(color: theme.weakTextColor, fontSize: 12),
|
||||
),
|
||||
if (isDesktopScreen)
|
||||
Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 10),
|
||||
height: 30,
|
||||
child: TextField(
|
||||
minLines: 1,
|
||||
controller: controller,
|
||||
maxLines: 1,
|
||||
onSubmitted: (text) {
|
||||
model.setNameCard(text.trim());
|
||||
},
|
||||
keyboardType: TextInputType.multiline,
|
||||
autofocus: true,
|
||||
textAlignVertical: TextAlignVertical.center,
|
||||
textAlign: TextAlign.start,
|
||||
style: const TextStyle(fontSize: 12),
|
||||
decoration: InputDecoration(
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 10, vertical: 10),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(5.0),
|
||||
borderSide: BorderSide(
|
||||
color: theme.weakDividerColor ?? Colors.grey,
|
||||
)),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(5.0),
|
||||
borderSide: BorderSide(
|
||||
color: theme.weakDividerColor ?? Colors.grey,
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
//选中时外边框颜色
|
||||
borderRadius: BorderRadius.circular(5.0),
|
||||
borderSide: BorderSide(
|
||||
color: theme.weakTextColor ?? Colors.grey,
|
||||
),
|
||||
),
|
||||
hintStyle: const TextStyle(
|
||||
color: Color(0xFFAEA4A3),
|
||||
),
|
||||
hintText: TIM_t("修改我的群昵称"))),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
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/base_widgets/tim_ui_kit_state.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.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/color.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/tui_theme.dart';
|
||||
|
||||
class GroupProfileNotification extends StatefulWidget {
|
||||
final bool isHavePermission;
|
||||
|
||||
const GroupProfileNotification({Key? key, this.isHavePermission = false}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => GroupProfileNotificationState();
|
||||
}
|
||||
|
||||
class GroupProfileNotificationState extends TIMUIKitState<GroupProfileNotification> {
|
||||
bool isShowEditBox = false;
|
||||
final TextEditingController _controller = TextEditingController();
|
||||
|
||||
@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);
|
||||
final String notification = (model.groupInfo?.notification != null && model.groupInfo!.notification!.isNotEmpty)
|
||||
? model.groupInfo!.notification!
|
||||
: TIM_t("暂无群公告");
|
||||
|
||||
_setGroupNotification() async {
|
||||
setState(() {
|
||||
isShowEditBox = false;
|
||||
});
|
||||
final notification = _controller.text;
|
||||
await model.setGroupNotification(notification);
|
||||
}
|
||||
|
||||
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(
|
||||
onTap: !widget.isHavePermission
|
||||
? null
|
||||
: (() {
|
||||
final isDesktopScreen = TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop;
|
||||
if (!isDesktopScreen) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
GroupProfileNotificationPage(model: model, notification: notification)));
|
||||
} else {
|
||||
setState(() {
|
||||
isShowEditBox = !isShowEditBox;
|
||||
if (isShowEditBox) {
|
||||
_controller.text = notification;
|
||||
}
|
||||
});
|
||||
}
|
||||
}),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
TIM_t("群公告"),
|
||||
style: TextStyle(color: theme.darkTextColor, fontSize: isDesktopScreen ? 14 : 16),
|
||||
),
|
||||
),
|
||||
if (widget.isHavePermission)
|
||||
AnimatedRotation(
|
||||
turns: isShowEditBox ? 0.25 : 0,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: Icon(Icons.keyboard_arrow_right, color: theme.weakTextColor),
|
||||
)
|
||||
],
|
||||
),
|
||||
if (!isShowEditBox)
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: isDesktopScreen ? 4 : 0),
|
||||
child: SelectableText(notification,
|
||||
// overflow: isDesktopScreen ? null : TextOverflow.ellipsis,
|
||||
// softWrap: true,
|
||||
style: TextStyle(color: theme.weakTextColor, fontSize: 12)),
|
||||
),
|
||||
if (isShowEditBox)
|
||||
Container(
|
||||
margin: const EdgeInsets.only(top: 10, bottom: 10),
|
||||
// height: 150,
|
||||
child: TextField(
|
||||
minLines: 1,
|
||||
maxLines: 6,
|
||||
controller: _controller,
|
||||
keyboardType: TextInputType.multiline,
|
||||
autofocus: true,
|
||||
style: const TextStyle(fontSize: 13),
|
||||
decoration: InputDecoration(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(5.0),
|
||||
borderSide: BorderSide(
|
||||
color: theme.weakDividerColor ?? Colors.grey,
|
||||
)),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(5.0),
|
||||
borderSide: BorderSide(
|
||||
color: theme.weakDividerColor ?? Colors.grey,
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
//选中时外边框颜色
|
||||
borderRadius: BorderRadius.circular(5.0),
|
||||
borderSide: BorderSide(
|
||||
color: theme.weakTextColor ?? Colors.grey,
|
||||
),
|
||||
),
|
||||
hintStyle: const TextStyle(
|
||||
color: Color(0xFFAEA4A3),
|
||||
),
|
||||
hintText: '')),
|
||||
),
|
||||
if (isShowEditBox)
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
OutlinedButton(
|
||||
onPressed: _setGroupNotification,
|
||||
child: Text(
|
||||
TIM_t("保存"),
|
||||
style: TextStyle(fontSize: 13, color: theme.primaryColor),
|
||||
))
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class GroupProfileNotificationPage extends StatefulWidget {
|
||||
final String notification;
|
||||
final TUIGroupProfileModel model;
|
||||
|
||||
const GroupProfileNotificationPage({Key? key, required this.notification, required this.model}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _GroupProfileNotificationPageState();
|
||||
}
|
||||
|
||||
class _GroupProfileNotificationPageState extends TIMUIKitState<GroupProfileNotificationPage> {
|
||||
final TextEditingController _controller = TextEditingController();
|
||||
bool isUpdated = false;
|
||||
|
||||
_setGroupNotification() async {
|
||||
final notification = _controller.text;
|
||||
await widget.model.setGroupNotification(notification);
|
||||
setState(() {
|
||||
isUpdated = true;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_controller.text = widget.notification;
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final TUITheme theme = value.theme;
|
||||
|
||||
return 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,
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
if (isUpdated) {
|
||||
setState(() {
|
||||
isUpdated = false;
|
||||
});
|
||||
} else {
|
||||
_setGroupNotification();
|
||||
}
|
||||
},
|
||||
child: Text(
|
||||
isUpdated ? TIM_t("编辑") : TIM_t("完成"),
|
||||
style: TextStyle(
|
||||
color: theme.appbarTextColor,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: TextField(
|
||||
readOnly: isUpdated,
|
||||
controller: _controller,
|
||||
keyboardType: TextInputType.multiline,
|
||||
maxLines: null,
|
||||
expands: true,
|
||||
autofocus: true,
|
||||
decoration: const InputDecoration(
|
||||
border: InputBorder.none,
|
||||
hintStyle: TextStyle(
|
||||
// fontSize: 10,
|
||||
color: Color(0xFFAEA4A3),
|
||||
),
|
||||
hintText: '')),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
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/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/views/TIMUIKitProfile/widget/tim_uikit_operation_item.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart';
|
||||
|
||||
class GroupPinConversation extends TIMUIKitStatelessWidget {
|
||||
GroupPinConversation({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final model = Provider.of<TUIGroupProfileModel>(context);
|
||||
final isPined = model.conversation?.isPinned ?? false;
|
||||
return TIMUIKitOperationItem(
|
||||
isEmpty: false,
|
||||
operationName: TIM_t("置顶聊天"),
|
||||
type: "switch",
|
||||
isUseCheckedBoxOnWide: true,
|
||||
operationValue: isPined,
|
||||
onSwitchChange: (value) {
|
||||
model.pinedConversation(value);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
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_type.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/ui/utils/screen_utils.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/color.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/tui_theme.dart';
|
||||
|
||||
class GroupProfileType extends TIMUIKitStatelessWidget {
|
||||
GroupProfileType({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final TUITheme theme = value.theme;
|
||||
|
||||
String groupType;
|
||||
final model = Provider.of<TUIGroupProfileModel>(context);
|
||||
final isDesktopScreen =
|
||||
TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop;
|
||||
|
||||
final type = model.groupInfo?.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 Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: isDesktopScreen ? null : Border(
|
||||
bottom: BorderSide(
|
||||
color:
|
||||
theme.weakDividerColor ?? CommonColor.weakDividerColor))),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
TIM_t("群类型"),
|
||||
style: TextStyle(
|
||||
fontSize: isDesktopScreen ? 14 : 16, color: theme.darkTextColor),
|
||||
),
|
||||
Text(
|
||||
groupType,
|
||||
style: TextStyle(
|
||||
fontSize: isDesktopScreen ? 14 : 16, color: theme.weakTextColor),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
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_friend_application.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_friend_application.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/life_cycle/new_contact_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';
|
||||
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/ui/widgets/avatar.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/color.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/tui_theme_view_model.dart';
|
||||
|
||||
typedef NewContactItemBuilder = Widget Function(BuildContext context, V2TimFriendApplication applicationInfo);
|
||||
|
||||
class TIMUIKitNewContact extends StatefulWidget {
|
||||
/// the callback when accept friend request
|
||||
final void Function(V2TimFriendApplication applicationInfo)? onAccept;
|
||||
|
||||
/// the callback when reject friend request
|
||||
final void Function(V2TimFriendApplication applicationInfo)? onRefuse;
|
||||
|
||||
/// the widget builder when no friend request exists
|
||||
final Widget Function(BuildContext context)? emptyBuilder;
|
||||
|
||||
/// the builder for the request item
|
||||
final NewContactItemBuilder? itemBuilder;
|
||||
|
||||
/// the life cycle hooks for new contact business logic
|
||||
final NewContactLifeCycle? lifeCycle;
|
||||
|
||||
const TIMUIKitNewContact(
|
||||
{Key? key, this.lifeCycle, this.onAccept, this.onRefuse, this.emptyBuilder, this.itemBuilder})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _TIMUIKitNewContactState();
|
||||
}
|
||||
|
||||
class _TIMUIKitNewContactState extends TIMUIKitState<TIMUIKitNewContact> {
|
||||
late TUIFriendShipViewModel model = serviceLocator<TUIFriendShipViewModel>();
|
||||
|
||||
_getShowName(V2TimFriendApplication item) {
|
||||
return TencentUtils.checkString(item.nickname) ?? TencentUtils.checkString(item.userID);
|
||||
}
|
||||
|
||||
Widget _itemBuilder(BuildContext context, V2TimFriendApplication applicationInfo) {
|
||||
final theme = Provider.of<TUIThemeViewModel>(context).theme;
|
||||
final showName = _getShowName(applicationInfo);
|
||||
final faceUrl = applicationInfo.faceUrl ?? "";
|
||||
final applicationText = applicationInfo.addWording ?? "";
|
||||
final isDesktopScreen = TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop;
|
||||
|
||||
return Material(
|
||||
color: theme.wideBackgroundColor,
|
||||
child: InkWell(
|
||||
onTap: () {},
|
||||
child: Container(
|
||||
padding: EdgeInsets.only(top: isDesktopScreen ? 6 : 10, left: 16, right: isDesktopScreen ? 16 : 0),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: EdgeInsets.only(bottom: isDesktopScreen ? 10 : 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(
|
||||
padding: const EdgeInsets.only(top: 10, bottom: 20),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(bottom: BorderSide(color: theme.weakDividerColor ?? CommonColor.weakDividerColor))),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: (applicationText.isNotEmpty && isDesktopScreen) ? 10 : 0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
showName,
|
||||
style: TextStyle(color: theme.darkTextColor, fontSize: isDesktopScreen ? 14 : 18),
|
||||
),
|
||||
if (applicationText.isNotEmpty && isDesktopScreen)
|
||||
const SizedBox(
|
||||
height: 4,
|
||||
),
|
||||
if (applicationText.isNotEmpty && isDesktopScreen)
|
||||
Text(
|
||||
applicationText,
|
||||
style: TextStyle(color: theme.weakTextColor, fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(child: Container()),
|
||||
Container(
|
||||
margin: const EdgeInsets.only(right: 8),
|
||||
child: InkWell(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: theme.primaryColor,
|
||||
border: Border.all(width: 1, color: theme.weakTextColor ?? CommonColor.weakTextColor)),
|
||||
child: Text(
|
||||
TIM_t("同意"),
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: isDesktopScreen ? 12 : null,
|
||||
),
|
||||
),
|
||||
),
|
||||
onTap: () async {
|
||||
await model.acceptFriendApplication(
|
||||
applicationInfo.userID,
|
||||
applicationInfo.type,
|
||||
);
|
||||
model.loadData();
|
||||
if (widget.onAccept != null) {
|
||||
widget.onAccept!(applicationInfo);
|
||||
}
|
||||
// widget?.onAccept();
|
||||
},
|
||||
),
|
||||
),
|
||||
Container(
|
||||
margin: const EdgeInsets.only(right: 8),
|
||||
child: InkWell(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: Colors.white,
|
||||
border: Border.all(width: 1, color: theme.weakTextColor ?? CommonColor.weakTextColor)),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6),
|
||||
child: Text(
|
||||
TIM_t("拒绝"),
|
||||
style: TextStyle(
|
||||
color: theme.primaryColor,
|
||||
fontSize: isDesktopScreen ? 12 : null,
|
||||
),
|
||||
),
|
||||
),
|
||||
onTap: () async {
|
||||
await model.refuseFriendApplication(
|
||||
applicationInfo.userID,
|
||||
applicationInfo.type,
|
||||
);
|
||||
model.loadData();
|
||||
if (widget.onRefuse != null) {
|
||||
widget.onRefuse!(applicationInfo);
|
||||
}
|
||||
// refuse(context);
|
||||
},
|
||||
))
|
||||
],
|
||||
),
|
||||
))
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
NewContactItemBuilder _getItemBuilder() {
|
||||
return widget.itemBuilder ?? _itemBuilder;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
return MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider.value(value: model),
|
||||
],
|
||||
builder: (BuildContext context, Widget? w) {
|
||||
final model = Provider.of<TUIFriendShipViewModel>(context);
|
||||
model.newContactLifeCycle = widget.lifeCycle;
|
||||
final newContactList = model.friendApplicationList;
|
||||
if (newContactList != null && newContactList.isNotEmpty) {
|
||||
return ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: newContactList.length,
|
||||
itemBuilder: (context, index) {
|
||||
final friendInfo = newContactList[index]!;
|
||||
final itemBuilder = _getItemBuilder();
|
||||
return itemBuilder(context, friendInfo);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (widget.emptyBuilder != null) {
|
||||
return widget.emptyBuilder!(context);
|
||||
}
|
||||
|
||||
return Container();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.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_friendship_view_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/widgets/unread_message.dart';
|
||||
|
||||
class TIMUIKitUnreadCount extends StatefulWidget {
|
||||
final double width;
|
||||
final double height;
|
||||
|
||||
const TIMUIKitUnreadCount({Key? key, this.width = 22.0, this.height = 22.0})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _TIMUIKitUnreadCountState();
|
||||
}
|
||||
|
||||
class _TIMUIKitUnreadCountState extends TIMUIKitState<TIMUIKitUnreadCount> {
|
||||
final TUIFriendShipViewModel model = serviceLocator<TUIFriendShipViewModel>();
|
||||
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
return ChangeNotifierProvider.value(
|
||||
value: model,
|
||||
child:
|
||||
Consumer<TUIFriendShipViewModel>(builder: (context, value, child) {
|
||||
final friendApplicationAmount = value.friendApplicationAmount;
|
||||
if (friendApplicationAmount > 0) {
|
||||
return UnreadMessage(
|
||||
width: widget.width,
|
||||
height: widget.height,
|
||||
unreadCount: friendApplicationAmount);
|
||||
}
|
||||
return Container();
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
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_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_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';
|
||||
|
||||
enum ProfileWidgetEnum {
|
||||
/// THe card shows the user info of a specific user.
|
||||
userInfoCard,
|
||||
|
||||
/// The switch of if add another user to block list.
|
||||
/// This will not shows when friendship relation not exist as default.
|
||||
addToBlockListBar,
|
||||
|
||||
/// The switch of if pin the one-to-one conversation to the top of the conversation list.
|
||||
/// This will not shows when friendship relation not exist as default.
|
||||
pinConversationBar,
|
||||
|
||||
/// The switch of if mute the message notification from a specific user.
|
||||
/// This will not shows when friendship relation not exist as default.
|
||||
messageMute,
|
||||
|
||||
/// The entrance to search page, please set the `onTap` callback yourself, to the search page with conversation ID.
|
||||
/// You can develop it with `TIMUIKitProfileWidget.searchBar`.
|
||||
searchBar,
|
||||
|
||||
/// The bar shows the portrait.
|
||||
portraitBar,
|
||||
|
||||
/// The bar shows the nickname of a specific user.
|
||||
nicknameBar,
|
||||
|
||||
/// The bar shows the user account of a specific user.
|
||||
userAccountBar,
|
||||
|
||||
/// The bar shows the signature of a specific user.
|
||||
signatureBar,
|
||||
|
||||
/// The bar shows the gender of a specific user.
|
||||
/// 1 represent male, 2 represent female.
|
||||
genderBar,
|
||||
|
||||
/// The bar shows the birthday of a specific user.
|
||||
/// Int like "19981112", means November 12, 1998.
|
||||
birthdayBar,
|
||||
|
||||
/// The area shows the buttons,
|
||||
/// contains "Send message", "Voice/Video Call", "Delete friend" when has the friend relationship,
|
||||
/// while contains "Add friend" when no relationship exists, as default.
|
||||
addAndDeleteArea,
|
||||
|
||||
/// The divider between sets of profile widget
|
||||
operationDivider,
|
||||
|
||||
/// The setting of remark for a specific user.
|
||||
/// This will not shows when friendship relation not exist as default.
|
||||
remarkBar,
|
||||
|
||||
/// Custom area, you may define send message, make calling, search or anything you want here.
|
||||
customBuilderOne,
|
||||
|
||||
/// Custom area, you may define send message, make calling, search or anything you want here.
|
||||
customBuilderTwo,
|
||||
|
||||
/// Custom area, you may define send message, make calling, search or anything you want here.
|
||||
customBuilderThree,
|
||||
|
||||
/// Custom area, you may define send message, make calling, search or anything you want here.
|
||||
customBuilderFour,
|
||||
|
||||
/// Custom area, you may define send message, make calling, search or anything you want here.
|
||||
customBuilderFive
|
||||
}
|
||||
|
||||
typedef ProfileWidgetItemContent = Widget? Function(
|
||||
bool isShowJump,
|
||||
VoidCallback clearJump,
|
||||
);
|
||||
|
||||
class ProfileWidgetBuilder {
|
||||
/// The divider between sets of profile widget
|
||||
Widget Function()? operationDivider;
|
||||
|
||||
/// The setting of remark for a specific user
|
||||
Widget Function(String remark, Function()? handleTap)? remarkBar;
|
||||
|
||||
/// The switch of if add another user to block list.
|
||||
/// This will not shows when friendship relation not exist as default.
|
||||
Widget Function(bool isAsBlocked, Function(bool value)? onChange)? addToBlockListBar;
|
||||
|
||||
/// The switch of if pin the one-to-one conversation to the top of the conversation list.
|
||||
/// This will not shows when friendship relation not exist as default.
|
||||
Widget Function(bool isPinned, Function(bool value)? onChange)? pinConversationBar;
|
||||
|
||||
/// The switch of if mute the message notification from a specific user.
|
||||
/// This will not shows when friendship relation not exist as default.
|
||||
Widget Function(bool isMute, Function(bool value)? onChange)? messageMute;
|
||||
|
||||
/// Override the default operation item style for un-customized profile widget.
|
||||
Widget Function({
|
||||
required String operationName,
|
||||
required String type,
|
||||
bool? operationValue,
|
||||
String? operationText,
|
||||
void Function(bool newValue)? onSwitchChange,
|
||||
})? operationItem;
|
||||
|
||||
/// The entrance to search page, please set the `onTap` callback yourself, to the search page with conversation ID.
|
||||
/// You can develop it with `TIMUIKitProfileWidget.searchBar`.
|
||||
Widget Function(V2TimConversation conversation)? searchBar;
|
||||
|
||||
/// The bar shows the portrait.
|
||||
Widget Function(V2TimUserFullInfo? userInfo)? portraitBar;
|
||||
|
||||
/// The bar shows the nickname of a specific user.
|
||||
Widget Function(String nickName)? nicknameBar;
|
||||
|
||||
/// The bar shows the user account of a specific user.
|
||||
Widget Function(String userAccount)? userAccountBar;
|
||||
|
||||
/// The bar shows the signature of a specific user.
|
||||
Widget Function(String signature)? signatureBar;
|
||||
|
||||
/// The bar shows the gender of a specific user.
|
||||
/// 1 represent male, 2 represent female.
|
||||
Widget Function(int gender)? genderBar;
|
||||
|
||||
/// The bar shows the birthday of a specific user.
|
||||
/// Int like "19981111", means November 11, 1998.
|
||||
Widget Function(int? birthday)? birthdayBar;
|
||||
|
||||
/// THe card shows the user info of a specific user.
|
||||
Widget Function(V2TimUserFullInfo? userInfo)? userInfoCard;
|
||||
|
||||
/// The area shows the buttons,
|
||||
/// contains "Send message", "Voice/Video Call", "Delete friend" when has the friend relationship,
|
||||
/// while contains "Add friend" when no relationship exists, as default.
|
||||
Widget Function(V2TimFriendInfo friendInfo, V2TimConversation conversation, int friendType, bool isDisturb)?
|
||||
addAndDeleteArea;
|
||||
|
||||
/// Custom area, you may define send message, make calling, search or anything you want here.
|
||||
Widget Function(bool isFriend, V2TimFriendInfo friendInfo, V2TimConversation conversation)? customBuilderOne;
|
||||
|
||||
/// Custom area, you may define send message, make calling, search or anything you want here.
|
||||
Widget Function(bool isFriend, V2TimFriendInfo friendInfo, V2TimConversation conversation)? customBuilderTwo;
|
||||
|
||||
/// Custom area, you may define send message, make calling, search or anything you want here.
|
||||
Widget Function(bool isFriend, V2TimFriendInfo friendInfo, V2TimConversation conversation)? customBuilderThree;
|
||||
|
||||
/// Custom area, you may define send message, make calling, search or anything you want here.
|
||||
Widget Function(bool isFriend, V2TimFriendInfo friendInfo, V2TimConversation conversation)? customBuilderFour;
|
||||
|
||||
/// Custom area, you may define send message, make calling, search or anything you want here.
|
||||
Widget Function(bool isFriend, V2TimFriendInfo friendInfo, V2TimConversation conversation)? customBuilderFive;
|
||||
|
||||
ProfileWidgetBuilder(
|
||||
{this.operationDivider,
|
||||
this.remarkBar,
|
||||
this.addToBlockListBar,
|
||||
this.pinConversationBar,
|
||||
this.messageMute,
|
||||
this.operationItem,
|
||||
this.searchBar,
|
||||
this.portraitBar,
|
||||
this.nicknameBar,
|
||||
this.userAccountBar,
|
||||
this.signatureBar,
|
||||
this.genderBar,
|
||||
this.birthdayBar,
|
||||
this.userInfoCard,
|
||||
this.addAndDeleteArea,
|
||||
this.customBuilderOne,
|
||||
this.customBuilderTwo,
|
||||
this.customBuilderThree,
|
||||
this.customBuilderFive,
|
||||
this.customBuilderFour});
|
||||
}
|
||||
@@ -0,0 +1,403 @@
|
||||
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/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_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_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/base_widgets/tim_ui_kit_state.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/life_cycle/profile_life_cycle.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_profile_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/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/TIMUIKitProfile/profile_widget.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitProfile/widget/tim_uikit_profile_widget.dart';
|
||||
|
||||
typedef OnSelfAvatarTap = void Function();
|
||||
|
||||
typedef ProfileBuilder = Widget Function(
|
||||
BuildContext context, V2TimFriendInfo userInfo, V2TimConversation conversation, int friendType, bool isMute);
|
||||
|
||||
class TIMUIKitProfile extends StatefulWidget {
|
||||
/// user ID
|
||||
final String userID;
|
||||
|
||||
/// [Deprecated:] the builder for custom operation list.
|
||||
/// [operationListBuilder] and [bottomOperationBuilder] merged into [builder], please use it instead.
|
||||
final Widget Function(BuildContext context, V2TimFriendInfo friendInfo, V2TimConversation conversation,
|
||||
int friendType, bool isMute)? operationListBuilder;
|
||||
|
||||
/// [Deprecated:] The builder for custom bottom operation area.
|
||||
/// [operationListBuilder] and [bottomOperationBuilder] merged into [builder], please use it instead.
|
||||
final Widget Function(
|
||||
BuildContext context, V2TimFriendInfo? friendInfo, V2TimConversation? conversation, int friendType)?
|
||||
bottomOperationBuilder;
|
||||
|
||||
/// [Deprecated:] Callback when clicking profile detail card.
|
||||
/// This widget will no longer shows the personal info card and can not jump to personal info page automatically,
|
||||
/// please navigate to your custom personal info page manually and directly, you may refer to our demo.
|
||||
final void Function(BuildContext context, V2TimUserFullInfo? userFullInfo)? handleProfileDetailCardTap;
|
||||
|
||||
/// Profile Controller
|
||||
final TIMUIKitProfileController? controller;
|
||||
|
||||
/// [Deprecated:] If allows jump to personal profiler page.
|
||||
/// This widget will no longer shows the personal info card and can not jump to personal info page automatically,
|
||||
/// please navigate to your custom personal info page manually and directly, you may refer to our demo.
|
||||
final bool canJumpToPersonalProfile;
|
||||
|
||||
/// [Deprecated:] The callback when clicking self avatar.
|
||||
/// This widget will no longer shows the personal info card and will not support to change self avatar,
|
||||
/// please navigate to your custom personal info page manually and directly, you may refer to our demo.
|
||||
final OnSelfAvatarTap? onSelfAvatarTap;
|
||||
|
||||
/// [If you tend to customize the profile page, use [profileWidgetsBuilder] 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 ProfileWidgetBuilder? profileWidgetBuilder;
|
||||
|
||||
/// [If you tend to customize the profile page, use [profileWidgetsBuilder] 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<ProfileWidgetEnum>? profileWidgetsOrder;
|
||||
|
||||
/// The builder for the whole 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 ProfileBuilder? builder;
|
||||
|
||||
/// The life cycle hooks for user profile business logic
|
||||
final ProfileLifeCycle? lifeCycle;
|
||||
|
||||
/// Whether the specify user is current logged in user.
|
||||
/// Default: [false].
|
||||
final bool isSelf;
|
||||
|
||||
/// Whether use the small card mode on Desktop. Usually shows on the Chat page.
|
||||
final bool smallCardMode;
|
||||
|
||||
const TIMUIKitProfile(
|
||||
{Key? key,
|
||||
required this.userID,
|
||||
@Deprecated("[operationListBuilder] and [bottomOperationBuilder] merged into [builder], please use it instead")
|
||||
this.operationListBuilder,
|
||||
@Deprecated("[operationListBuilder] and [bottomOperationBuilder] merged into [builder], please use it instead")
|
||||
this.bottomOperationBuilder,
|
||||
@Deprecated(
|
||||
"This widget will no longer shows the personal info card and can not jump to personal info page automatically, please navigate to your custom personal info page manually and directly, you may refer to our demo")
|
||||
this.handleProfileDetailCardTap,
|
||||
@Deprecated(
|
||||
"This widget will no longer shows the personal info card and can not jump to personal info page automatically, please navigate to your custom personal info page manually and directly, you may refer to our demo")
|
||||
this.canJumpToPersonalProfile = false,
|
||||
@Deprecated(
|
||||
"This widget will no longer shows the personal info card and will not support to change self avatar, please navigate to your custom personal info page manually and directly, you may refer to our demo")
|
||||
this.onSelfAvatarTap,
|
||||
this.controller,
|
||||
this.profileWidgetBuilder,
|
||||
this.profileWidgetsOrder,
|
||||
this.builder,
|
||||
this.isSelf = false,
|
||||
this.lifeCycle,
|
||||
this.smallCardMode = false})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _TIMUIKitProfileState();
|
||||
}
|
||||
|
||||
class _TIMUIKitProfileState extends TIMUIKitState<TIMUIKitProfile> {
|
||||
final TUIProfileViewModel _model = TUIProfileViewModel();
|
||||
late TIMUIKitProfileController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_controller = widget.controller ?? TIMUIKitProfileController();
|
||||
_model.lifeCycle = widget.lifeCycle;
|
||||
_model.loadData(userID: widget.userID, isNeedConversation: !widget.isSelf);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant TIMUIKitProfile oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.userID != widget.userID || _model.userProfile?.friendInfo?.userID != widget.userID) {
|
||||
_model.userProfile = null;
|
||||
_model.loadData(userID: widget.userID, isNeedConversation: !widget.isSelf);
|
||||
}
|
||||
}
|
||||
|
||||
final List<ProfileWidgetEnum> _defaultWidgetOrder = [
|
||||
ProfileWidgetEnum.userInfoCard,
|
||||
ProfileWidgetEnum.operationDivider,
|
||||
ProfileWidgetEnum.remarkBar,
|
||||
ProfileWidgetEnum.operationDivider,
|
||||
ProfileWidgetEnum.addToBlockListBar,
|
||||
ProfileWidgetEnum.pinConversationBar,
|
||||
ProfileWidgetEnum.messageMute,
|
||||
ProfileWidgetEnum.operationDivider,
|
||||
ProfileWidgetEnum.addAndDeleteArea
|
||||
];
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final theme = value.theme;
|
||||
final isDesktopScreen = TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop;
|
||||
return ChangeNotifierProvider.value(
|
||||
value: _model,
|
||||
child: Consumer<TUIProfileViewModel>(
|
||||
builder: (context, value, child) {
|
||||
final TUIProfileViewModel model = Provider.of<TUIProfileViewModel>(context);
|
||||
_controller.model = model;
|
||||
final V2TimFriendInfo? userInfo = model.userProfile?.friendInfo;
|
||||
|
||||
if (userInfo == null) {
|
||||
return Center(
|
||||
child: LoadingAnimationWidget.staggeredDotsWave(
|
||||
color: theme.weakTextColor ?? Colors.grey,
|
||||
size: 48,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final conversation = model.userProfile?.conversation ??
|
||||
V2TimConversation(
|
||||
conversationID: "c2c_${widget.userID}",
|
||||
type: 1,
|
||||
userID: widget.userID,
|
||||
faceUrl: model.userProfile?.friendInfo?.userProfile?.faceUrl,
|
||||
showName: TencentUtils.checkString(model.userProfile?.friendInfo?.friendRemark) ??
|
||||
TencentUtils.checkString(model.userProfile?.friendInfo?.userProfile?.nickName) ??
|
||||
widget.userID);
|
||||
final TUISelfInfoViewModel _selfInfoViewModel = serviceLocator<TUISelfInfoViewModel>();
|
||||
|
||||
final isFriend = model.friendType != 0;
|
||||
final isSelf = (model.userProfile?.friendInfo?.userID == _selfInfoViewModel.loginInfo?.userID);
|
||||
final isMute = model.isDisturb ?? false;
|
||||
|
||||
Widget profilePage({required Widget child}) {
|
||||
return Container(
|
||||
color: isDesktopScreen ? theme.wideBackgroundColor : null,
|
||||
child: SingleChildScrollView(
|
||||
physics: const BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics()),
|
||||
child: Container(
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void handleAddToBlockList(bool value) async {
|
||||
model.addToBlackList(value, userInfo.userID);
|
||||
}
|
||||
|
||||
void handlePinConversation(bool value) async {
|
||||
model.pinedConversation(value, conversation.conversationID);
|
||||
}
|
||||
|
||||
void handleMuteMessage(bool value) async {
|
||||
model.setMessageDisturb(userInfo.userID, value);
|
||||
}
|
||||
|
||||
void handleTapRemarkBar({Offset? offset, String? initText}) {
|
||||
_controller.showTextInputBottomSheet(
|
||||
initOffset: offset,
|
||||
initText: initText,
|
||||
context: context,
|
||||
title: TIM_t("修改备注名"),
|
||||
tips: TIM_t("仅限汉字、英文、数字和下划线"),
|
||||
onSubmitted: (String remark) async {
|
||||
final res = await _controller.updateRemarks(widget.userID, remark);
|
||||
if (res.code == 0) {
|
||||
widget.lifeCycle?.didRemarkUpdated(remark);
|
||||
}
|
||||
},
|
||||
theme: theme);
|
||||
}
|
||||
|
||||
void handleAddFriend() async {
|
||||
model.addFriend(userInfo.userID);
|
||||
}
|
||||
|
||||
Future<void> handleDeleteFriend() async {
|
||||
var result = await model.deleteFriend(userInfo.userID, needUpdateData: false);
|
||||
if (result != null) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
}
|
||||
|
||||
List<Widget> _renderWidgetsWithOrder(List<ProfileWidgetEnum> order) {
|
||||
final ProfileWidgetBuilder? customBuilder = widget.profileWidgetBuilder;
|
||||
return order.map((element) {
|
||||
switch (element) {
|
||||
case ProfileWidgetEnum.userInfoCard:
|
||||
return (customBuilder?.userInfoCard != null
|
||||
? customBuilder?.userInfoCard!(userInfo.userProfile)
|
||||
: TIMUIKitProfileUserInfoCard(userInfo: userInfo.userProfile))!;
|
||||
case ProfileWidgetEnum.addToBlockListBar:
|
||||
if (isSelf) {
|
||||
return Container();
|
||||
}
|
||||
return (customBuilder?.addToBlockListBar != null
|
||||
? customBuilder?.addToBlockListBar!(model.isAddToBlackList ?? false, handleAddToBlockList)
|
||||
: TIMUIKitProfileWidget.addToBlackListBar(
|
||||
model.isAddToBlackList ?? false, context, handleAddToBlockList, widget.smallCardMode))!;
|
||||
case ProfileWidgetEnum.pinConversationBar:
|
||||
// if (!isFriend) {
|
||||
// return Container();
|
||||
// }
|
||||
return (customBuilder?.pinConversationBar != null
|
||||
? customBuilder?.pinConversationBar!(conversation.isPinned ?? false, handlePinConversation)
|
||||
: TIMUIKitProfileWidget.pinConversationBar(
|
||||
conversation.isPinned ?? false, context, handlePinConversation, widget.smallCardMode))!;
|
||||
case ProfileWidgetEnum.messageMute:
|
||||
// if (!isFriend) {
|
||||
// return Container();
|
||||
// }
|
||||
return (customBuilder?.messageMute != null
|
||||
? customBuilder?.messageMute!(isMute, handleMuteMessage)
|
||||
: TIMUIKitProfileWidget.messageDisturb(
|
||||
context, isMute, handleMuteMessage, widget.smallCardMode))!;
|
||||
case ProfileWidgetEnum.searchBar:
|
||||
return (customBuilder?.searchBar != null
|
||||
? customBuilder?.searchBar!(conversation)
|
||||
// Please define the search bar with navigating in `profileWidgetBuilder` before using it here.
|
||||
: Text(TIM_t("你必须自定义search bar,并处理点击跳转")))!;
|
||||
case ProfileWidgetEnum.portraitBar:
|
||||
return (customBuilder?.portraitBar != null
|
||||
? customBuilder?.portraitBar!(userInfo.userProfile)
|
||||
: TIMUIKitProfileWidget.portraitBar(
|
||||
TIMUIKitProfileWidget.defaultPortraitWidget(userInfo.userProfile, widget.smallCardMode),
|
||||
widget.smallCardMode))!;
|
||||
case ProfileWidgetEnum.nicknameBar:
|
||||
return (customBuilder?.nicknameBar != null
|
||||
? customBuilder?.nicknameBar!(userInfo.userProfile?.nickName ?? "")
|
||||
: TIMUIKitProfileWidget.nicknameBar(userInfo.userProfile?.nickName ?? "", widget.smallCardMode))!;
|
||||
case ProfileWidgetEnum.userAccountBar:
|
||||
return (customBuilder?.userAccountBar != null
|
||||
? customBuilder?.userAccountBar!(userInfo.userProfile?.userID ?? "")
|
||||
: TIMUIKitProfileWidget.userAccountBar(
|
||||
userInfo.userProfile?.userID ?? "", widget.smallCardMode))!;
|
||||
case ProfileWidgetEnum.signatureBar:
|
||||
return (customBuilder?.signatureBar != null
|
||||
? customBuilder?.signatureBar!(userInfo.userProfile?.selfSignature ?? "")
|
||||
: TIMUIKitProfileWidget.signatureBar(
|
||||
userInfo.userProfile?.selfSignature ?? "", widget.smallCardMode))!;
|
||||
case ProfileWidgetEnum.genderBar:
|
||||
return (customBuilder?.genderBar != null
|
||||
? customBuilder?.genderBar!(userInfo.userProfile?.gender ?? 0)
|
||||
: TIMUIKitProfileWidget.genderBar(userInfo.userProfile?.gender ?? 0, widget.smallCardMode))!;
|
||||
case ProfileWidgetEnum.birthdayBar:
|
||||
return (customBuilder?.birthdayBar != null
|
||||
? customBuilder?.birthdayBar!(userInfo.userProfile?.birthday)
|
||||
: TIMUIKitProfileWidget.birthdayBar(userInfo.userProfile?.birthday, widget.smallCardMode))!;
|
||||
case ProfileWidgetEnum.addAndDeleteArea:
|
||||
if (isSelf) {
|
||||
return Container();
|
||||
}
|
||||
return (customBuilder?.addAndDeleteArea != null
|
||||
? customBuilder?.addAndDeleteArea!(
|
||||
userInfo,
|
||||
conversation,
|
||||
value.friendType,
|
||||
isMute,
|
||||
)
|
||||
: isDesktopScreen
|
||||
? TIMUIKitProfileWidget.addAndDeleteAreaWide(
|
||||
userInfo,
|
||||
conversation,
|
||||
value.friendType,
|
||||
isMute,
|
||||
model.isAddToBlackList ?? false,
|
||||
theme,
|
||||
handleAddFriend,
|
||||
handleDeleteFriend,
|
||||
widget.smallCardMode)
|
||||
: TIMUIKitProfileWidget.addAndDeleteArea(
|
||||
userInfo,
|
||||
conversation,
|
||||
value.friendType,
|
||||
isMute,
|
||||
model.isAddToBlackList ?? false,
|
||||
theme,
|
||||
handleAddFriend,
|
||||
handleDeleteFriend,
|
||||
widget.smallCardMode))!;
|
||||
case ProfileWidgetEnum.operationDivider:
|
||||
return (customBuilder?.operationDivider != null
|
||||
? customBuilder?.operationDivider!()
|
||||
: TIMUIKitProfileWidget.operationDivider(
|
||||
color: theme.weakDividerColor,
|
||||
height: isDesktopScreen ? 1 : 10,
|
||||
margin:
|
||||
isDesktopScreen ? EdgeInsets.symmetric(vertical: widget.smallCardMode ? 4 : 20) : null))!;
|
||||
case ProfileWidgetEnum.remarkBar:
|
||||
if (!isFriend) {
|
||||
return Container();
|
||||
}
|
||||
return (customBuilder?.remarkBar != null
|
||||
? customBuilder?.remarkBar!(userInfo.friendRemark ?? "", handleTapRemarkBar)
|
||||
: TIMUIKitProfileWidget.remarkBar(
|
||||
context, userInfo.friendRemark ?? "", handleTapRemarkBar, widget.smallCardMode))!;
|
||||
case ProfileWidgetEnum.customBuilderOne:
|
||||
return (customBuilder?.customBuilderOne != null
|
||||
? customBuilder?.customBuilderOne!(isFriend, userInfo, conversation)
|
||||
// Please define the corresponding custom widget in `profileWidgetBuilder` before using it here.
|
||||
: Text(TIM_t("如使用自定义区域,请在profileWidgetBuilder传入对应组件")))!;
|
||||
case ProfileWidgetEnum.customBuilderTwo:
|
||||
return (customBuilder?.customBuilderTwo != null
|
||||
? customBuilder?.customBuilderTwo!(isFriend, userInfo, conversation)
|
||||
// Please define the corresponding custom widget in `profileWidgetBuilder` before using it here.
|
||||
: Text(TIM_t("如使用自定义区域,请在profileWidgetBuilder传入对应组件")))!;
|
||||
case ProfileWidgetEnum.customBuilderThree:
|
||||
return (customBuilder?.customBuilderThree != null
|
||||
? customBuilder?.customBuilderThree!(isFriend, userInfo, conversation)
|
||||
// Please define the corresponding custom widget in `profileWidgetBuilder` before using it here.
|
||||
: Text(TIM_t("如使用自定义区域,请在profileWidgetBuilder传入对应组件")))!;
|
||||
case ProfileWidgetEnum.customBuilderFour:
|
||||
return (customBuilder?.customBuilderFour != null
|
||||
? customBuilder?.customBuilderFour!(isFriend, userInfo, conversation)
|
||||
// Please define the corresponding custom widget in `profileWidgetBuilder` before using it here.
|
||||
: Text(TIM_t("如使用自定义区域,请在profileWidgetBuilder传入对应组件")))!;
|
||||
case ProfileWidgetEnum.customBuilderFive:
|
||||
return (customBuilder?.customBuilderFive != null
|
||||
? customBuilder?.customBuilderFive!(isFriend, userInfo, conversation)
|
||||
// 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 widget.builder!(context, userInfo, conversation, value.friendType, isMute);
|
||||
} else if (widget.profileWidgetsOrder != null) {
|
||||
return profilePage(
|
||||
child: Column(
|
||||
children: [..._renderWidgetsWithOrder(widget.profileWidgetsOrder!)],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return profilePage(
|
||||
child: Column(
|
||||
children: [..._renderWidgetsWithOrder(_defaultWidgetOrder)],
|
||||
));
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
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_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/utils/screen_utils.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/color.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/tui_theme.dart';
|
||||
|
||||
class TIMUIKitOperationItem extends TIMUIKitStatelessWidget {
|
||||
final String operationName;
|
||||
|
||||
/// shows on the second line
|
||||
final String? operationDescription;
|
||||
final bool? operationValue;
|
||||
|
||||
/// Is show allow edit status, while a right icon shows on mobile, and `showAllowEditStatus` shows on desktop.
|
||||
final bool showAllowEditStatus;
|
||||
|
||||
/// Used on wide screen.
|
||||
final String? wideEditText;
|
||||
|
||||
/// Used on wide screen.
|
||||
final bool isEmpty;
|
||||
|
||||
/// the operationText widget for replacement, for developers to define what to do
|
||||
final Widget? operationRightWidget;
|
||||
final String type;
|
||||
final void Function(bool newValue)? onSwitchChange;
|
||||
final Key? itemBoxKey;
|
||||
final bool isUseCheckedBoxOnWide;
|
||||
|
||||
/// Is use the small card mode on Desktop. Usually shows on the Chat page.
|
||||
final bool smallCardMode;
|
||||
|
||||
TIMUIKitOperationItem(
|
||||
{Key? key,
|
||||
this.wideEditText,
|
||||
this.itemBoxKey,
|
||||
this.operationDescription,
|
||||
required this.isEmpty,
|
||||
required this.operationName,
|
||||
this.smallCardMode = false,
|
||||
this.operationValue,
|
||||
this.type = "arrow",
|
||||
this.isUseCheckedBoxOnWide = false,
|
||||
this.onSwitchChange,
|
||||
this.operationRightWidget,
|
||||
this.showAllowEditStatus = true})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final TUITheme theme = value.theme;
|
||||
final isDesktopScreen =
|
||||
TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop;
|
||||
|
||||
return Container(
|
||||
padding: isDesktopScreen
|
||||
? EdgeInsets.symmetric(
|
||||
horizontal: isUseCheckedBoxOnWide ? 6 : 16,
|
||||
vertical: smallCardMode ? 0 : 4)
|
||||
: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
|
||||
margin: isDesktopScreen ? null : const EdgeInsets.only(bottom: 1),
|
||||
color: Colors.white,
|
||||
child: (isDesktopScreen && isUseCheckedBoxOnWide)
|
||||
? Row(
|
||||
children: [
|
||||
Transform.scale(
|
||||
scale: 0.8,
|
||||
child: Checkbox(
|
||||
fillColor: MaterialStateProperty.all(theme.primaryColor),
|
||||
value: operationValue ?? false,
|
||||
onChanged: (val) {
|
||||
if (onSwitchChange != null) {
|
||||
onSwitchChange!(val ?? false);
|
||||
}
|
||||
}),
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
operationName,
|
||||
style: const TextStyle(fontSize: 14),
|
||||
),
|
||||
if (operationDescription != null)
|
||||
Text(
|
||||
operationDescription!,
|
||||
style:
|
||||
TextStyle(color: theme.weakTextColor, fontSize: 12),
|
||||
)
|
||||
],
|
||||
)),
|
||||
],
|
||||
)
|
||||
: Row(
|
||||
mainAxisAlignment: isDesktopScreen
|
||||
? MainAxisAlignment.start
|
||||
: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
if (isDesktopScreen)
|
||||
SizedBox(
|
||||
width: 130,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
operationName,
|
||||
style: TextStyle(
|
||||
color:
|
||||
isDesktopScreen ? hexToColor("7f7f7f") : null),
|
||||
),
|
||||
if (operationDescription != null)
|
||||
Text(
|
||||
operationDescription!,
|
||||
style: TextStyle(
|
||||
color: theme.weakTextColor, fontSize: 12),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
if (!isDesktopScreen)
|
||||
Expanded(
|
||||
child: SizedBox(
|
||||
width: 130,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
operationName,
|
||||
style: TextStyle(
|
||||
color:
|
||||
isDesktopScreen ? hexToColor("7f7f7f") : null),
|
||||
),
|
||||
if (operationDescription != null)
|
||||
Text(
|
||||
operationDescription!,
|
||||
style: TextStyle(
|
||||
color: theme.weakTextColor, fontSize: 12),
|
||||
)
|
||||
],
|
||||
),
|
||||
)),
|
||||
if (type == "switch")
|
||||
Transform.scale(
|
||||
key: itemBoxKey,
|
||||
scale: 0.8,
|
||||
child: CupertinoSwitch(
|
||||
value: operationValue ?? false,
|
||||
onChanged: onSwitchChange,
|
||||
activeColor: theme.primaryColor,
|
||||
),
|
||||
),
|
||||
if (type != "switch" && !isDesktopScreen)
|
||||
Transform.scale(
|
||||
scale: 0,
|
||||
child: CupertinoSwitch(
|
||||
value: false,
|
||||
onChanged: onSwitchChange,
|
||||
),
|
||||
),
|
||||
(type != "switch" &&
|
||||
isDesktopScreen &&
|
||||
showAllowEditStatus &&
|
||||
isEmpty)
|
||||
? MouseRegion(
|
||||
key: itemBoxKey,
|
||||
child: Text(
|
||||
wideEditText ?? TIM_t("编辑"),
|
||||
style: TextStyle(color: theme.weakTextColor),
|
||||
),
|
||||
)
|
||||
: Container(
|
||||
width: 0,
|
||||
),
|
||||
if (type != "switch")
|
||||
Expanded(
|
||||
child: Row(
|
||||
mainAxisAlignment: isDesktopScreen
|
||||
? MainAxisAlignment.start
|
||||
: MainAxisAlignment.end,
|
||||
children: [Expanded(child: operationRightWidget ?? const Text(""))],
|
||||
)),
|
||||
(type != "switch" && !isDesktopScreen && showAllowEditStatus)
|
||||
? const Icon(Icons.keyboard_arrow_right)
|
||||
: Container(
|
||||
width: 0,
|
||||
),
|
||||
if (type != "switch" && isDesktopScreen)
|
||||
SizedBox(
|
||||
width: 0,
|
||||
child: Transform.scale(
|
||||
scale: 0,
|
||||
child: CupertinoSwitch(
|
||||
value: false,
|
||||
onChanged: onSwitchChange,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
// ignore_for_file: deprecated_member_use_from_same_package
|
||||
|
||||
import 'package:flutter/cupertino.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/ui/views/TIMUIKitProfile/widget/tim_uikit_profile_userinfo_card/tim_uikit_profile_userinfo_card_narrow.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitProfile/widget/tim_uikit_profile_userinfo_card/tim_uikit_profile_userinfo_card_wide.dart';
|
||||
|
||||
class TIMUIKitProfileUserInfoCard extends StatelessWidget {
|
||||
/// User info
|
||||
final V2TimUserFullInfo? userInfo;
|
||||
final bool isJumpToPersonalProfile;
|
||||
final VoidCallback? onClickAvatar;
|
||||
|
||||
/// If shows the arrow icon on the right
|
||||
final bool showArrowRightIcon;
|
||||
|
||||
const TIMUIKitProfileUserInfoCard(
|
||||
{Key? key,
|
||||
this.userInfo,
|
||||
@Deprecated(
|
||||
"This info card can no longer navigate to default personal profile page automatically, please deal with it manually.")
|
||||
this.isJumpToPersonalProfile = false,
|
||||
this.showArrowRightIcon = false,
|
||||
this.onClickAvatar})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TUIKitScreenUtils.getDeviceWidget(
|
||||
context: context,
|
||||
defaultWidget: TIMUIKitProfileUserInfoCardNarrow(
|
||||
userInfo: userInfo,
|
||||
isJumpToPersonalProfile: isJumpToPersonalProfile,
|
||||
showArrowRightIcon: showArrowRightIcon,
|
||||
onClickAvatar: onClickAvatar,
|
||||
),
|
||||
desktopWidget: TIMUIKitProfileUserInfoCardWide(
|
||||
userInfo: userInfo,
|
||||
onClickAvatar: onClickAvatar,
|
||||
isJumpToPersonalProfile: isJumpToPersonalProfile,
|
||||
showArrowRightIcon: showArrowRightIcon,
|
||||
),
|
||||
mobileWidget: TIMUIKitProfileUserInfoCardNarrow(
|
||||
userInfo: userInfo,
|
||||
onClickAvatar: onClickAvatar,
|
||||
isJumpToPersonalProfile: isJumpToPersonalProfile,
|
||||
showArrowRightIcon: showArrowRightIcon,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
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_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/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/ui/widgets/avatar.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/tui_theme.dart';
|
||||
|
||||
class TIMUIKitProfileUserInfoCardNarrow extends TIMUIKitStatelessWidget {
|
||||
/// User info
|
||||
final V2TimUserFullInfo? userInfo;
|
||||
final bool isJumpToPersonalProfile;
|
||||
final VoidCallback? onClickAvatar;
|
||||
|
||||
/// If shows the arrow icon on the right
|
||||
final bool showArrowRightIcon;
|
||||
|
||||
TIMUIKitProfileUserInfoCardNarrow(
|
||||
{Key? key,
|
||||
this.onClickAvatar,
|
||||
this.userInfo,
|
||||
@Deprecated(
|
||||
"This info card can no longer navigate to default personal profile page automatically, please deal with it manually.")
|
||||
this.isJumpToPersonalProfile = false,
|
||||
this.showArrowRightIcon = false})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final TUITheme theme = value.theme;
|
||||
final faceUrl = userInfo?.faceUrl ?? "";
|
||||
final nickName = userInfo?.nickName ?? "";
|
||||
final signature = userInfo?.selfSignature;
|
||||
final showName = nickName != "" ? nickName : userInfo?.userID;
|
||||
final option1 = signature;
|
||||
final signatureText =
|
||||
option1 != null ? TIM_t_para("个性签名: {{option1}}", "个性签名: $option1")(option1: option1) : TIM_t("暂无个性签名");
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
|
||||
color: Colors.white,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 48,
|
||||
height: 48,
|
||||
child: GestureDetector(
|
||||
onTap: onClickAvatar,
|
||||
child: Avatar(
|
||||
faceUrl: faceUrl,
|
||||
isShowBigWhenClick: onClickAvatar == null,
|
||||
showName: showName ?? "",
|
||||
type: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 12,
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
child: SelectableText(
|
||||
showName ?? "",
|
||||
style: const TextStyle(fontSize: 18, color: Colors.black),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
"ID: ",
|
||||
style: TextStyle(fontSize: 13, color: theme.weakTextColor),
|
||||
),
|
||||
SelectableText(
|
||||
userInfo?.userID ?? "",
|
||||
style: TextStyle(fontSize: 13, color: theme.weakTextColor),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SelectableText(signatureText, style: TextStyle(fontSize: 13, color: theme.weakTextColor))
|
||||
],
|
||||
),
|
||||
),
|
||||
showArrowRightIcon ? const Icon(Icons.keyboard_arrow_right) : Container()
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
import 'package:flutter/material.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/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/ui/widgets/avatar.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/tui_theme.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/color.dart';
|
||||
|
||||
class TIMUIKitProfileUserInfoCardWide extends TIMUIKitStatelessWidget {
|
||||
/// User info
|
||||
final V2TimUserFullInfo? userInfo;
|
||||
final bool isJumpToPersonalProfile;
|
||||
final VoidCallback? onClickAvatar;
|
||||
|
||||
/// If shows the arrow icon on the right
|
||||
final bool showArrowRightIcon;
|
||||
|
||||
TIMUIKitProfileUserInfoCardWide(
|
||||
{Key? key,
|
||||
this.userInfo,
|
||||
this.onClickAvatar,
|
||||
@Deprecated(
|
||||
"This info card can no longer navigate to default personal profile page automatically, please deal with it manually.")
|
||||
this.isJumpToPersonalProfile = false,
|
||||
this.showArrowRightIcon = false})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final TUITheme theme = value.theme;
|
||||
final faceUrl = userInfo?.faceUrl ?? "";
|
||||
final nickName = userInfo?.nickName ?? "";
|
||||
final signature = userInfo?.selfSignature;
|
||||
final showName = nickName != "" ? nickName : userInfo?.userID;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.only(bottom: 10, left: 16, right: 16),
|
||||
color: Colors.white,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
child: SelectableText(
|
||||
showName ?? "",
|
||||
style: const TextStyle(fontSize: 20, color: Colors.black),
|
||||
),
|
||||
margin: const EdgeInsets.only(right: 10),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
"ID: ",
|
||||
style: TextStyle(fontSize: 12, color: theme.weakTextColor),
|
||||
),
|
||||
Expanded(
|
||||
child: SelectableText(
|
||||
userInfo?.userID ?? "",
|
||||
style: TextStyle(fontSize: 12, color: theme.weakTextColor),
|
||||
)),
|
||||
],
|
||||
),
|
||||
if (signature != null)
|
||||
Container(
|
||||
margin: const EdgeInsets.only(top: 18),
|
||||
child: SelectableText(signature, style: TextStyle(fontSize: 14, color: hexToColor("7f7f7f"))),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
const SizedBox(
|
||||
width: 40,
|
||||
),
|
||||
SizedBox(
|
||||
width: 80,
|
||||
height: 80,
|
||||
child: InkWell(
|
||||
onTap: onClickAvatar,
|
||||
child: Avatar(
|
||||
faceUrl: faceUrl,
|
||||
isShowBigWhenClick: onClickAvatar == null,
|
||||
showName: showName ?? "",
|
||||
type: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
showArrowRightIcon ? const Icon(Icons.keyboard_arrow_right) : Container()
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,395 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.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_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_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/base_widgets/tim_ui_kit_class.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/ui/widgets/avatar.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/color.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/tui_theme.dart';
|
||||
|
||||
class TIMUIKitProfileWidget extends TIMUIKitClass {
|
||||
static final bool isDesktopScreen = TUIKitScreenUtils.getFormFactor() == DeviceType.Desktop;
|
||||
|
||||
static Widget operationDivider({Color? color, double? height, EdgeInsetsGeometry? margin}) {
|
||||
return Container(
|
||||
color: color,
|
||||
margin: margin,
|
||||
height: height ?? 10,
|
||||
);
|
||||
}
|
||||
|
||||
/// Remarks
|
||||
static Widget remarkBar(BuildContext context, String remark, Function({Offset? offset, String? initText})? handleTap,
|
||||
bool smallCardMode) {
|
||||
final GlobalKey key = GlobalKey();
|
||||
return InkWell(
|
||||
onTapDown: (details) {
|
||||
if (handleTap != null) {
|
||||
handleTap(
|
||||
offset: Offset(min(details.globalPosition.dx, MediaQuery.of(context).size.width - 400),
|
||||
min(details.globalPosition.dy, MediaQuery.of(context).size.height - 100)),
|
||||
initText: remark);
|
||||
}
|
||||
},
|
||||
child: TIMUIKitOperationItem(
|
||||
smallCardMode: smallCardMode,
|
||||
itemBoxKey: key,
|
||||
isEmpty: remark.isEmpty,
|
||||
wideEditText: TIM_t("设置备注名"),
|
||||
operationName: TIM_t("备注名"),
|
||||
operationRightWidget: Text(remark, textAlign: isDesktopScreen ? null : TextAlign.end),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// add to block list
|
||||
static Widget addToBlackListBar(
|
||||
bool value, BuildContext context, Function(bool value)? onChanged, bool smallCardMode) {
|
||||
return TIMUIKitOperationItem(
|
||||
smallCardMode: smallCardMode,
|
||||
isEmpty: false,
|
||||
operationName: TIM_t("加入黑名单"),
|
||||
type: "switch",
|
||||
operationValue: value,
|
||||
onSwitchChange: (value) {
|
||||
if (onChanged != null) {
|
||||
onChanged(value);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// pin the conversation to the top
|
||||
static Widget pinConversationBar(
|
||||
bool value, BuildContext context, Function(bool value)? onChanged, bool smallCardMode) {
|
||||
return TIMUIKitOperationItem(
|
||||
smallCardMode: smallCardMode,
|
||||
isEmpty: false,
|
||||
operationName: TIM_t("置顶聊天"),
|
||||
type: "switch",
|
||||
operationValue: value,
|
||||
onSwitchChange: (value) {
|
||||
if (onChanged != null) {
|
||||
onChanged(value);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// message disturb
|
||||
static Widget messageDisturb(
|
||||
BuildContext context, bool isDisturb, Function(bool value)? onChanged, bool smallCardMode) {
|
||||
return TIMUIKitOperationItem(
|
||||
smallCardMode: smallCardMode,
|
||||
isEmpty: false,
|
||||
operationName: TIM_t("消息免打扰"),
|
||||
type: "switch",
|
||||
operationValue: isDisturb,
|
||||
onSwitchChange: (value) {
|
||||
if (onChanged != null) {
|
||||
onChanged(value);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
static Widget operationItem(
|
||||
{required String operationName,
|
||||
required String type,
|
||||
bool? operationValue,
|
||||
String? operationText,
|
||||
required bool isEmpty,
|
||||
void Function(bool newValue)? onSwitchChange,
|
||||
required bool smallCardMode}) {
|
||||
return TIMUIKitOperationItem(
|
||||
smallCardMode: smallCardMode,
|
||||
isEmpty: isEmpty,
|
||||
operationName: operationName,
|
||||
type: type,
|
||||
operationRightWidget: Text(operationText ?? "", textAlign: isDesktopScreen ? null : TextAlign.end),
|
||||
operationValue: operationValue,
|
||||
onSwitchChange: onSwitchChange,
|
||||
);
|
||||
}
|
||||
|
||||
/// find history message
|
||||
static Widget searchBar(
|
||||
BuildContext context,
|
||||
V2TimConversation conversation,
|
||||
bool smallCardMode, {
|
||||
Function()? handleTap,
|
||||
}) {
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
if (handleTap != null) {
|
||||
handleTap();
|
||||
}
|
||||
},
|
||||
child: TIMUIKitOperationItem(
|
||||
isEmpty: true,
|
||||
wideEditText: TIM_t("立即搜索"),
|
||||
operationName: TIM_t("查找聊天内容"),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// portrait
|
||||
static Widget portraitBar(Widget portraitWidget, bool smallCardMode) {
|
||||
return SizedBox(
|
||||
child: TIMUIKitOperationItem(
|
||||
smallCardMode: smallCardMode,
|
||||
isEmpty: false,
|
||||
operationName: TIM_t("头像"),
|
||||
operationRightWidget: portraitWidget,
|
||||
showAllowEditStatus: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// defaultPortraitWidget
|
||||
static Widget defaultPortraitWidget(V2TimUserFullInfo? userInfo, bool smallCardMode) {
|
||||
return SizedBox(
|
||||
width: 48,
|
||||
height: 48,
|
||||
child: userInfo != null
|
||||
? Avatar(
|
||||
faceUrl: userInfo.faceUrl ?? "",
|
||||
showName: userInfo.nickName ?? "",
|
||||
type: 1,
|
||||
)
|
||||
: Container(),
|
||||
);
|
||||
}
|
||||
|
||||
/// nickname
|
||||
static Widget nicknameBar(
|
||||
String nickName,
|
||||
bool smallCardMode,
|
||||
) {
|
||||
return SizedBox(
|
||||
child: TIMUIKitOperationItem(
|
||||
smallCardMode: smallCardMode,
|
||||
isEmpty: nickName.isEmpty,
|
||||
showAllowEditStatus: false,
|
||||
operationName: TIM_t("昵称"),
|
||||
operationRightWidget: Text(nickName, textAlign: isDesktopScreen ? null : TextAlign.end),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// user account
|
||||
static Widget userAccountBar(String userNum, bool smallCardMode) {
|
||||
return SizedBox(
|
||||
child: TIMUIKitOperationItem(
|
||||
smallCardMode: smallCardMode,
|
||||
isEmpty: false,
|
||||
showAllowEditStatus: false,
|
||||
operationName: TIM_t("账号"),
|
||||
operationRightWidget: SelectableText(userNum, textAlign: isDesktopScreen ? null : TextAlign.end),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// signature
|
||||
static Widget signatureBar(String signature, bool smallCardMode) {
|
||||
return SizedBox(
|
||||
child: TIMUIKitOperationItem(
|
||||
smallCardMode: smallCardMode,
|
||||
isEmpty: false,
|
||||
showAllowEditStatus: false,
|
||||
operationName: TIM_t("个性签名"),
|
||||
operationRightWidget: Text(signature, textAlign: isDesktopScreen ? null : TextAlign.end),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// gender
|
||||
static Widget genderBar(int gender, bool smallCardMode) {
|
||||
Map genderMap = {
|
||||
0: TIM_t("未填写"),
|
||||
1: TIM_t("男"),
|
||||
2: TIM_t("女"),
|
||||
};
|
||||
return SizedBox(
|
||||
child: TIMUIKitOperationItem(
|
||||
smallCardMode: smallCardMode,
|
||||
isEmpty: false,
|
||||
showAllowEditStatus: false,
|
||||
operationName: TIM_t("性别"),
|
||||
operationRightWidget: Text(genderMap[gender], textAlign: isDesktopScreen ? null : TextAlign.end),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// gender
|
||||
static Widget genderBarWithArrow(int gender, bool smallCardMode) {
|
||||
Map genderMap = {
|
||||
0: TIM_t("未填写"),
|
||||
1: TIM_t("男"),
|
||||
2: TIM_t("女"),
|
||||
};
|
||||
return SizedBox(
|
||||
child: TIMUIKitOperationItem(
|
||||
smallCardMode: smallCardMode,
|
||||
isEmpty: false,
|
||||
operationName: TIM_t("性别"),
|
||||
operationRightWidget: Text(genderMap[gender], textAlign: isDesktopScreen ? null : TextAlign.end),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// birthday
|
||||
static Widget birthdayBar(int? birthday, bool smallCardMode) {
|
||||
try {
|
||||
final date = DateTime.parse(birthday.toString());
|
||||
DateFormat formatter = DateFormat('yyyy-MM-dd');
|
||||
return TIMUIKitOperationItem(
|
||||
smallCardMode: smallCardMode,
|
||||
isEmpty: false,
|
||||
operationName: TIM_t("生日"),
|
||||
operationRightWidget: Text(formatter.format(date), textAlign: isDesktopScreen ? null : TextAlign.end),
|
||||
);
|
||||
} catch (e) {
|
||||
return TIMUIKitOperationItem(
|
||||
smallCardMode: smallCardMode,
|
||||
isEmpty: false,
|
||||
operationName: TIM_t("生日"),
|
||||
operationRightWidget: Text(TIM_t("未填写"), textAlign: isDesktopScreen ? null : TextAlign.end),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// default button area
|
||||
static Widget addAndDeleteArea(
|
||||
V2TimFriendInfo friendInfo,
|
||||
V2TimConversation conversation,
|
||||
int friendType,
|
||||
bool isDisturb,
|
||||
bool isBlocked,
|
||||
TUITheme theme,
|
||||
VoidCallback handleAddFriend,
|
||||
VoidCallback handleDeleteFriend,
|
||||
bool smallCardMode) {
|
||||
_buildDeleteFriend(V2TimConversation conversation, theme) {
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
handleDeleteFriend();
|
||||
},
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
padding: const EdgeInsets.symmetric(vertical: 15),
|
||||
decoration:
|
||||
BoxDecoration(color: Colors.white, border: Border(bottom: BorderSide(color: theme.weakDividerColor))),
|
||||
child: Text(
|
||||
TIM_t("删除好友"),
|
||||
style: TextStyle(color: theme.cautionColor, fontSize: 17),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_buildAddOperation() {
|
||||
return 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: Row(children: [
|
||||
Expanded(
|
||||
child: TextButton(
|
||||
child: Text(TIM_t("加为好友"), style: TextStyle(color: theme.primaryColor, fontSize: 17)),
|
||||
onPressed: () {
|
||||
handleAddFriend();
|
||||
}),
|
||||
)
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
if (friendType != 0) _buildDeleteFriend(conversation, theme),
|
||||
if (friendType == 0 && !isBlocked) _buildAddOperation()
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
static Widget wideButton({
|
||||
required VoidCallback onPressed,
|
||||
required String text,
|
||||
required Color color,
|
||||
required bool smallCardMode,
|
||||
EdgeInsets? margin,
|
||||
}) {
|
||||
return Container(
|
||||
margin: margin ?? const EdgeInsets.symmetric(vertical: 10),
|
||||
child: smallCardMode
|
||||
? OutlinedButton(
|
||||
onPressed: onPressed,
|
||||
child: Text(
|
||||
text,
|
||||
style: TextStyle(color: color),
|
||||
),
|
||||
style: ButtonStyle(
|
||||
minimumSize: MaterialStateProperty.all<Size>(const Size(160, 40)),
|
||||
))
|
||||
: ElevatedButton(
|
||||
onPressed: onPressed,
|
||||
child: Text(text),
|
||||
style: ButtonStyle(
|
||||
minimumSize: MaterialStateProperty.all<Size>(const Size(180, 46)),
|
||||
backgroundColor: MaterialStateProperty.all<Color>(color)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// default button area
|
||||
static Widget addAndDeleteAreaWide(
|
||||
V2TimFriendInfo friendInfo,
|
||||
V2TimConversation conversation,
|
||||
int friendType,
|
||||
bool isDisturb,
|
||||
bool isBlocked,
|
||||
TUITheme theme,
|
||||
VoidCallback handleAddFriend,
|
||||
VoidCallback handleDeleteFriend,
|
||||
bool smallCardMode,
|
||||
) {
|
||||
_buildDeleteFriend(V2TimConversation conversation, theme) {
|
||||
return wideButton(
|
||||
smallCardMode: smallCardMode,
|
||||
onPressed: () {
|
||||
handleDeleteFriend();
|
||||
},
|
||||
color: theme.cautionColor ?? Colors.red,
|
||||
text: TIM_t("删除好友"),
|
||||
);
|
||||
}
|
||||
|
||||
_buildAddOperation() {
|
||||
return wideButton(
|
||||
smallCardMode: smallCardMode,
|
||||
onPressed: handleAddFriend,
|
||||
color: theme.primaryColor ?? hexToColor("3e4b67"),
|
||||
text: TIM_t("加为好友"),
|
||||
);
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
if (friendType != 0) _buildDeleteFriend(conversation, theme),
|
||||
if (friendType == 0 && !isBlocked) _buildAddOperation()
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
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';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/tui_theme.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/color.dart';
|
||||
|
||||
class TIMUIKitSearchFolder extends TIMUIKitStatelessWidget {
|
||||
final String folderName;
|
||||
final List<Widget> children;
|
||||
|
||||
TIMUIKitSearchFolder(
|
||||
{Key? key, required this.folderName, required this.children})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final TUITheme theme = value.theme;
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 0),
|
||||
width: MediaQuery.of(context).size.width,
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
width: MediaQuery.of(context).size.width,
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom:
|
||||
BorderSide(color: hexToColor("DBDBDB"), width: 0.5))),
|
||||
padding: const EdgeInsets.fromLTRB(0, 6, 0, 6),
|
||||
child: Text(
|
||||
folderName,
|
||||
style: TextStyle(
|
||||
color: theme.weakTextColor, height: 1.5, fontSize: 14),
|
||||
),
|
||||
),
|
||||
...children
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
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_base.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_statelesswidget.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/tui_theme.dart';
|
||||
|
||||
enum SearchType { contact, group, history }
|
||||
|
||||
class TIMUIKitSearchIndicator extends TIMUIKitStatelessWidget {
|
||||
final List<SearchType> typeList;
|
||||
final ValueChanged<List<SearchType>> onChange;
|
||||
|
||||
TIMUIKitSearchIndicator(
|
||||
{required this.typeList, required this.onChange, Key? key})
|
||||
: super(key: key);
|
||||
|
||||
final titleMap = {
|
||||
SearchType.contact: "联系人",
|
||||
SearchType.group: "群聊",
|
||||
SearchType.history: "聊天记录"
|
||||
};
|
||||
|
||||
Widget renderItemBox(
|
||||
IconData icon, SearchType item, bool isSelect, TUITheme theme) {
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
if (isSelect) {
|
||||
typeList.remove(item);
|
||||
} else {
|
||||
typeList.add(item);
|
||||
}
|
||||
onChange(typeList);
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(6),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Stack(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(6),
|
||||
child: Icon(
|
||||
icon,
|
||||
color: theme.weakTextColor,
|
||||
size: 30,
|
||||
),
|
||||
),
|
||||
if (isSelect)
|
||||
Positioned(
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: Container(
|
||||
height: 16,
|
||||
width: 16,
|
||||
decoration: BoxDecoration(shape: BoxShape.circle, color: theme.primaryColor),
|
||||
child: const Icon(
|
||||
Icons.check,
|
||||
size: 8,
|
||||
color: Colors.white,
|
||||
),
|
||||
))
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
TIM_t(titleMap[item]!),
|
||||
style: TextStyle(color: theme.textColor, fontSize: 13),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final theme = value.theme;
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 20),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(TIM_t("搜索指定内容"),
|
||||
style: TextStyle(color: theme.weakTextColor, fontSize: 12)),
|
||||
)
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 1),
|
||||
Divider(thickness: 0.8, color: theme.weakDividerColor),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
renderItemBox(Icons.person, SearchType.contact,
|
||||
typeList.contains(SearchType.contact), theme),
|
||||
renderItemBox(Icons.people, SearchType.group,
|
||||
typeList.contains(SearchType.group), theme),
|
||||
renderItemBox(Icons.message, SearchType.history,
|
||||
typeList.contains(SearchType.history), theme),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
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_state.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/theme/color.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/tui_theme.dart';
|
||||
|
||||
class TIMUIKitSearchInput extends StatefulWidget {
|
||||
final ValueChanged<String> onChange;
|
||||
final String? initValue;
|
||||
final TextEditingController? controller;
|
||||
final Widget? prefixIcon;
|
||||
final Widget? prefixText;
|
||||
final bool? isAutoFocus;
|
||||
final FocusNode focusNode;
|
||||
|
||||
const TIMUIKitSearchInput({
|
||||
required this.onChange,
|
||||
this.initValue,
|
||||
this.controller,
|
||||
Key? key,
|
||||
this.prefixIcon,
|
||||
this.isAutoFocus = true,
|
||||
this.prefixText,
|
||||
required this.focusNode,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => TIMUIKitSearchInputState();
|
||||
}
|
||||
|
||||
class TIMUIKitSearchInputState extends TIMUIKitState<TIMUIKitSearchInput> {
|
||||
late TextEditingController textEditingController =
|
||||
widget.controller ?? TextEditingController();
|
||||
bool isEmptyInput = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
textEditingController.text = widget.initValue ?? "";
|
||||
isEmptyInput = textEditingController.text.isEmpty;
|
||||
}
|
||||
|
||||
hideAllPanel() {
|
||||
widget.focusNode.unfocus();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final TUITheme theme = value.theme;
|
||||
final isDesktopScreen = TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop;
|
||||
return Container(
|
||||
// height: 64,
|
||||
padding: EdgeInsets.fromLTRB(16, isDesktopScreen ? 16 : 8, 16, 16),
|
||||
margin: isDesktopScreen ? const EdgeInsets.only(bottom: 2) : null,
|
||||
decoration: BoxDecoration(
|
||||
color: isDesktopScreen
|
||||
? theme.wideBackgroundColor
|
||||
: theme.primaryColor,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: theme.weakBackgroundColor ?? hexToColor("E6E9EB"),
|
||||
offset: const Offset(0.0, 2.0),
|
||||
)
|
||||
]
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(maxHeight: isDesktopScreen ? 30 : 36),
|
||||
child: TextField(
|
||||
autofocus: widget.isAutoFocus ?? true,
|
||||
onChanged: (value) async {
|
||||
final trimValue = value.trim();
|
||||
final isEmpty = trimValue.isEmpty;
|
||||
if(isEmpty != isEmptyInput){
|
||||
setState(() {
|
||||
isEmptyInput = isEmpty ? true : false;
|
||||
});
|
||||
}
|
||||
widget.onChange(trimValue);
|
||||
},
|
||||
keyboardType: TextInputType.text,
|
||||
textInputAction: TextInputAction.search,
|
||||
maxLines: 4,
|
||||
minLines: 1,
|
||||
focusNode: widget.focusNode,
|
||||
controller: textEditingController,
|
||||
textAlignVertical: TextAlignVertical.center,
|
||||
textAlign: TextAlign.start,
|
||||
style: isDesktopScreen ? const TextStyle(
|
||||
fontSize: 12
|
||||
) : null,
|
||||
decoration: InputDecoration(
|
||||
contentPadding: const EdgeInsets.all(0),
|
||||
border: const OutlineInputBorder(borderSide: BorderSide.none),
|
||||
hintStyle: TextStyle(
|
||||
fontSize: isDesktopScreen ? 12 : 14,
|
||||
color: hexToColor("CCCCCC"),
|
||||
),
|
||||
fillColor: isDesktopScreen ? hexToColor("f3f3f4") : Colors.white,
|
||||
filled: true,
|
||||
isDense: true,
|
||||
hintText: TIM_t("搜索"),
|
||||
prefix: widget.prefixText != null
|
||||
? Container(
|
||||
margin: const EdgeInsets.only(right: 8),
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth:
|
||||
MediaQuery.of(context).size.width * 0.2),
|
||||
child: widget.prefixText,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
prefixIcon: widget.prefixIcon,
|
||||
suffixIcon: isEmptyInput
|
||||
? null
|
||||
: IconButton(
|
||||
onPressed: () {
|
||||
textEditingController.clear();
|
||||
setState(() {
|
||||
isEmptyInput = true;
|
||||
});
|
||||
widget.onChange("");
|
||||
},
|
||||
icon: Icon(Icons.cancel, color: hexToColor("979797")),
|
||||
),
|
||||
),
|
||||
),
|
||||
)),
|
||||
if(!isDesktopScreen) Container(
|
||||
margin: const EdgeInsets.fromLTRB(10, 0, 0, 0),
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: Text(TIM_t("取消"),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
)),
|
||||
))
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
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/utils/screen_utils.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitSearch/tim_uikit_search_item_wide.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.dart';
|
||||
|
||||
class TIMUIKitSearchItem extends TIMUIKitStatelessWidget {
|
||||
final String faceUrl;
|
||||
final String showName;
|
||||
final String lineOne;
|
||||
final String? lineOneRight;
|
||||
final String? lineTwo;
|
||||
final VoidCallback? onClick;
|
||||
|
||||
TIMUIKitSearchItem(
|
||||
{Key? key,
|
||||
required this.faceUrl,
|
||||
required this.showName,
|
||||
required this.lineOne,
|
||||
this.lineTwo,
|
||||
this.lineOneRight,
|
||||
this.onClick})
|
||||
: super(key: key);
|
||||
|
||||
_renderLineOneRight(String? text, TUITheme theme) {
|
||||
if (text != null) {
|
||||
return Text(text,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: theme.weakTextColor,
|
||||
));
|
||||
} else {
|
||||
return Container();
|
||||
}
|
||||
}
|
||||
|
||||
_renderLineTwo(String? text, TUITheme theme) {
|
||||
return (text != null)
|
||||
? Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
color: theme.weakTextColor, height: 1.5, fontSize: 14),
|
||||
)
|
||||
: Container(
|
||||
height: 0,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final TUITheme theme = value.theme;
|
||||
|
||||
return TUIKitScreenUtils.getDeviceWidget(
|
||||
context: context,
|
||||
defaultWidget: GestureDetector(
|
||||
onTap: onClick,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(color: hexToColor("DBDBDB"), width: 0.5))),
|
||||
padding: const EdgeInsets.fromLTRB(0, 12, 0, 12),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 40,
|
||||
height: 40,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
clipBehavior: Clip.none,
|
||||
children: [Avatar(faceUrl: faceUrl, showName: showName)],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(left: 12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
// height: 24,
|
||||
padding: const EdgeInsets.symmetric(vertical: 2),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
lineOne,
|
||||
style: TextStyle(
|
||||
color: theme.darkTextColor,
|
||||
fontSize: 18.0,
|
||||
fontWeight: FontWeight.w400),
|
||||
),
|
||||
_renderLineOneRight(lineOneRight, theme),
|
||||
],
|
||||
),
|
||||
),
|
||||
_renderLineTwo(lineTwo, theme),
|
||||
],
|
||||
),
|
||||
))
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
desktopWidget: TIMUIKitSearchWideItem(
|
||||
lineOneRight: lineOneRight,
|
||||
key: key,
|
||||
lineTwo: lineTwo,
|
||||
onClick: onClick,
|
||||
faceUrl: faceUrl,
|
||||
showName: showName,
|
||||
lineOne: lineOne),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
// ignore_for_file: file_names
|
||||
|
||||
import 'package:flutter/material.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/utils/screen_utils.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/color.dart';
|
||||
|
||||
class TIMUIKitSearchShowALl extends TIMUIKitStatelessWidget {
|
||||
final String textShow;
|
||||
final VoidCallback? onClick;
|
||||
final bool isNeedMoreBottom;
|
||||
|
||||
TIMUIKitSearchShowALl(
|
||||
{Key? key,
|
||||
this.onClick,
|
||||
required this.textShow,
|
||||
this.isNeedMoreBottom = false})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final isDesktopScreen = TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop;
|
||||
return InkWell(
|
||||
onTap: onClick,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: Border(
|
||||
bottom: BorderSide(color: hexToColor("DBDBDB"), width: 0.5))),
|
||||
padding: EdgeInsets.fromLTRB(0, 8, 0, (isNeedMoreBottom ? 24 : 8)),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.search,
|
||||
color: hexToColor("979797"),
|
||||
),
|
||||
Expanded(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(left: 12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
// height: 24,
|
||||
padding: const EdgeInsets.symmetric(vertical: 2),
|
||||
child: Text(
|
||||
textShow,
|
||||
style: TextStyle(
|
||||
color: Colors.black,
|
||||
fontSize: isDesktopScreen ? 14 : 16.0,
|
||||
fontWeight: FontWeight.w400),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)),
|
||||
Icon(
|
||||
Icons.expand_more,
|
||||
color: hexToColor("979797"),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
import 'package:flutter/material.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_friend_info_result.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_friend_info_result.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_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_search_result_item.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_message_search_result_item.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/services_locatar.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_search_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/platform.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitSearch/pureUI/tim_uikit_search_indicator.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitSearch/tim_uikit_search_friend.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitSearch/pureUI/tim_uikit_search_input.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitSearch/tim_uikit_search_group.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitSearch/tim_uikit_search_msg.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/ui/views/TIMUIKitSearch/tim_uikit_search_not_support.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/color.dart';
|
||||
|
||||
class TIMUIKitSearch extends StatefulWidget {
|
||||
/// the callback after clicking the conversation item
|
||||
final Function(V2TimConversation, V2TimMessage?) onTapConversation;
|
||||
|
||||
/// [Deprecated] : You are supposed to use [TIMUIKitSearchMsgDetail],
|
||||
/// if you tend to search inside a specific conversation, includes c2c and group.
|
||||
final V2TimConversation? conversation;
|
||||
|
||||
/// [Deprecated] : You are supposed to use [onEnterSearchInConversation],
|
||||
/// though the effects are the same.
|
||||
final Function(V2TimConversation conversation, String initKeyword)? onEnterConversation;
|
||||
|
||||
/// On click each conversation from 'Chat history' and searching for historical message in it.
|
||||
final Function(V2TimConversation conversation, String initKeyword)? onEnterSearchInConversation;
|
||||
|
||||
final VoidCallback? onBack;
|
||||
|
||||
final bool? isAutoFocus;
|
||||
|
||||
const TIMUIKitSearch(
|
||||
{required this.onTapConversation,
|
||||
Key? key,
|
||||
@Deprecated(
|
||||
"You are supposed to use [TIMUIKitSearchMsgDetail], if you tend to search inside a specific conversation, includes c2c and group")
|
||||
this.conversation,
|
||||
@Deprecated("You are supposed to use [onEnterSearchInConversation], though the effects are the same.")
|
||||
this.onEnterConversation,
|
||||
this.isAutoFocus = true,
|
||||
this.onEnterSearchInConversation,
|
||||
this.onBack})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => TIMUIKitSearchState();
|
||||
}
|
||||
|
||||
class TIMUIKitSearchState extends TIMUIKitState<TIMUIKitSearch> {
|
||||
late TextEditingController textEditingController = TextEditingController();
|
||||
final model = serviceLocator<TUISearchViewModel>();
|
||||
final FocusNode focusNode = FocusNode();
|
||||
GlobalKey<dynamic> inputTextField = GlobalKey();
|
||||
List<SearchType> searchTypes = [SearchType.group, SearchType.contact, SearchType.history];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
model.initSearch();
|
||||
model.initConversationMsg();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
if (PlatformUtils().isWeb) {
|
||||
return TIMUIKitSearchNotSupport();
|
||||
}
|
||||
return MultiProvider(
|
||||
providers: [ChangeNotifierProvider.value(value: serviceLocator<TUISearchViewModel>())],
|
||||
builder: (context, w) {
|
||||
List<V2TimFriendInfoResult> friendResultList = Provider.of<TUISearchViewModel>(context).friendList ?? [];
|
||||
List<V2TimMessageSearchResultItem> msgList = Provider.of<TUISearchViewModel>(context).msgList ?? [];
|
||||
List<V2TimGroupInfo> groupList = Provider.of<TUISearchViewModel>(context).groupList ?? [];
|
||||
int totalMsgCount = Provider.of<TUISearchViewModel>(context).totalMsgCount;
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
FocusScopeNode currentFocus = FocusScope.of(context);
|
||||
if (!currentFocus.hasPrimaryFocus) {
|
||||
currentFocus.unfocus();
|
||||
}
|
||||
},
|
||||
child: Scaffold(
|
||||
body: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
TIMUIKitSearchInput(
|
||||
focusNode: focusNode,
|
||||
key: inputTextField,
|
||||
isAutoFocus: widget.isAutoFocus,
|
||||
onChange: (String value) {
|
||||
model.searchByKey(value);
|
||||
},
|
||||
controller: textEditingController,
|
||||
prefixIcon: Icon(
|
||||
Icons.search,
|
||||
size: 16,
|
||||
color: hexToColor("979797"),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
if ((friendResultList.isEmpty || !(searchTypes.contains(SearchType.contact))) &&
|
||||
(groupList.isEmpty || !(searchTypes.contains(SearchType.group))) &&
|
||||
(totalMsgCount == 0 || !(searchTypes.contains(SearchType.history))))
|
||||
TIMUIKitSearchIndicator(
|
||||
typeList: searchTypes,
|
||||
onChange: (list) {
|
||||
setState(() {
|
||||
searchTypes = list;
|
||||
});
|
||||
},
|
||||
),
|
||||
if (searchTypes.contains(SearchType.contact))
|
||||
TIMUIKitSearchFriend(
|
||||
onTapConversation: (conversation, message) {
|
||||
focusNode.unfocus();
|
||||
Future.delayed(const Duration(milliseconds: 100), () {
|
||||
widget.onTapConversation(conversation, message);
|
||||
});
|
||||
},
|
||||
friendResultList: friendResultList),
|
||||
if (searchTypes.contains(SearchType.group))
|
||||
TIMUIKitSearchGroup(
|
||||
groupList: groupList,
|
||||
onTapConversation: (conversation, message) {
|
||||
focusNode.unfocus();
|
||||
Future.delayed(const Duration(milliseconds: 100), () {
|
||||
widget.onTapConversation(conversation, message);
|
||||
});
|
||||
},
|
||||
),
|
||||
if (searchTypes.contains(SearchType.history))
|
||||
TIMUIKitSearchMsg(
|
||||
onTapConversation: widget.onTapConversation,
|
||||
keyword: textEditingController.text,
|
||||
totalMsgCount: totalMsgCount,
|
||||
msgList: msgList,
|
||||
onEnterConversation: (V2TimConversation conversation, String keyword) {
|
||||
if (widget.onEnterSearchInConversation != null) {
|
||||
widget.onEnterSearchInConversation!(conversation, keyword);
|
||||
} else if (widget.onEnterConversation != null) {
|
||||
widget.onEnterConversation!(conversation, keyword);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
if (widget.onBack != null) {
|
||||
widget.onBack!();
|
||||
}
|
||||
},
|
||||
))
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
// ignore_for_file: must_be_immutable
|
||||
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/cupertino.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_sdk/models/v2_tim_friend_info_result.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_friend_info_result.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/ui/views/TIMUIKitSearch/pureUI/tim_uikit_search_item.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitSearch/pureUI/tim_uikit_search_folder.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_search_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitSearch/pureUI/tim_uikit_search_showAll.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart';
|
||||
|
||||
class TIMUIKitSearchFriend extends StatefulWidget {
|
||||
List<V2TimFriendInfoResult> friendResultList;
|
||||
final Function(V2TimConversation, V2TimMessage?) onTapConversation;
|
||||
|
||||
TIMUIKitSearchFriend({required this.friendResultList, Key? key, required this.onTapConversation}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => TIMUIKitSearchFriendState();
|
||||
}
|
||||
|
||||
class TIMUIKitSearchFriendState extends TIMUIKitState<TIMUIKitSearchFriend> {
|
||||
bool isShowAll = false;
|
||||
int defaultShowLines = 3;
|
||||
|
||||
Widget _renderShowALl(int currentLines) {
|
||||
return (isShowAll == false && currentLines > defaultShowLines)
|
||||
? TIMUIKitSearchShowALl(
|
||||
textShow: TIM_t("全部联系人"),
|
||||
onClick: () => setState(() {
|
||||
isShowAll = true;
|
||||
}),
|
||||
)
|
||||
: Container();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
List<V2TimConversation?> _conversationList = Provider.of<TUISearchViewModel>(context).conversationList;
|
||||
|
||||
List<V2TimFriendInfoResult> filteredFriendResultList = widget.friendResultList.where((friend) {
|
||||
int index = _conversationList.indexWhere((conv) => friend.friendInfo?.userID == conv?.userID);
|
||||
return index == -1 ? false : true;
|
||||
}).toList();
|
||||
|
||||
List<V2TimFriendInfoResult> halfFilteredFriendResultList = isShowAll
|
||||
? filteredFriendResultList
|
||||
: filteredFriendResultList.sublist(0, min(defaultShowLines, filteredFriendResultList.length));
|
||||
|
||||
if (filteredFriendResultList.isNotEmpty) {
|
||||
return TIMUIKitSearchFolder(folderName: TIM_t("联系人"), children: [
|
||||
...halfFilteredFriendResultList.map((conv) {
|
||||
int convIndex = _conversationList.indexWhere((item) => conv.friendInfo?.userID == item?.userID);
|
||||
V2TimConversation conversation = _conversationList[convIndex]!;
|
||||
late String? showNickName;
|
||||
if (conv.friendInfo?.friendRemark != null && conv.friendInfo?.friendRemark != "") {
|
||||
showNickName = conv.friendInfo?.friendRemark;
|
||||
} else if (conv.friendInfo?.userProfile?.nickName != null && conv.friendInfo?.userProfile?.nickName != "") {
|
||||
showNickName = conv.friendInfo?.userProfile?.nickName;
|
||||
} else {
|
||||
showNickName = conv.friendInfo?.userID;
|
||||
}
|
||||
|
||||
return TIMUIKitSearchItem(
|
||||
onClick: () {
|
||||
widget.onTapConversation(conversation, null);
|
||||
},
|
||||
faceUrl: conv.friendInfo?.userProfile?.faceUrl ?? "",
|
||||
showName: "",
|
||||
lineOne: conversation.userID!,
|
||||
lineTwo: TIM_t("昵称") + ":" + showNickName!,
|
||||
);
|
||||
}).toList(),
|
||||
_renderShowALl(filteredFriendResultList.length),
|
||||
]);
|
||||
} else {
|
||||
return Container();
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user