From 76bd3f94fd03cbd08db2e3ba1cb8189ff6c4b94a Mon Sep 17 00:00:00 2001 From: Jason Date: Wed, 26 Oct 2022 14:05:58 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=9A=E8=AF=9D=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/constants/message_constant.dart | 37 + lib/controllers/user_controller.dart | 4 - lib/models/im/contact_info_model.dart | 2 + lib/providers/user_provider.dart | 22 + lib/routes/contact_routes.dart | 2 + lib/services/tim/friend_service.dart | 26 +- lib/utils/sound_record.dart | 66 ++ .../contact/firend/request/index_page.dart | 3 +- lib/views/contact/group/index_page.dart | 90 ++- lib/views/contact/index/index_page.dart | 6 +- .../conversation/preview/image_widget.dart | 78 ++ .../conversation/preview/video_widget.dart | 119 +++ .../conversation/widgets/message_area.dart | 80 +++ .../conversation/widgets/message_field.dart | 676 ++++++++++++++++++ .../conversation/widgets/message_list.dart | 126 ++++ .../conversation/widgets/message_widget.dart | 274 +++++++ .../widgets/show_call_message.dart | 61 ++ .../widgets/show_custom_message.dart | 61 ++ .../widgets/show_face_message.dart | 12 + .../widgets/show_file_message.dart | 70 ++ .../widgets/show_group_card_message.dart | 59 ++ .../widgets/show_image_message.dart | 123 ++++ .../widgets/show_image_message_copy.dart | 283 ++++++++ .../widgets/show_location_message.dart | 101 +++ .../widgets/show_merger_message.dart | 12 + .../widgets/show_name_card_message.dart | 76 ++ .../widgets/show_sound_message.dart | 140 ++++ .../widgets/show_text_message.dart | 32 + .../widgets/show_transfer_message.dart | 113 +++ .../widgets/show_video_message.dart | 68 ++ .../conversation/widgets/tim_emoji_panel.dart | 69 ++ pubspec.lock | 91 +++ pubspec.yaml | 5 + 33 files changed, 2956 insertions(+), 31 deletions(-) create mode 100644 lib/constants/message_constant.dart create mode 100644 lib/utils/sound_record.dart create mode 100644 lib/views/conversation/preview/image_widget.dart create mode 100644 lib/views/conversation/preview/video_widget.dart create mode 100644 lib/views/conversation/widgets/message_area.dart create mode 100644 lib/views/conversation/widgets/message_field.dart create mode 100644 lib/views/conversation/widgets/message_list.dart create mode 100644 lib/views/conversation/widgets/message_widget.dart create mode 100644 lib/views/conversation/widgets/show_call_message.dart create mode 100644 lib/views/conversation/widgets/show_custom_message.dart create mode 100644 lib/views/conversation/widgets/show_face_message.dart create mode 100644 lib/views/conversation/widgets/show_file_message.dart create mode 100644 lib/views/conversation/widgets/show_group_card_message.dart create mode 100644 lib/views/conversation/widgets/show_image_message.dart create mode 100644 lib/views/conversation/widgets/show_image_message_copy.dart create mode 100644 lib/views/conversation/widgets/show_location_message.dart create mode 100644 lib/views/conversation/widgets/show_merger_message.dart create mode 100644 lib/views/conversation/widgets/show_name_card_message.dart create mode 100644 lib/views/conversation/widgets/show_sound_message.dart create mode 100644 lib/views/conversation/widgets/show_text_message.dart create mode 100644 lib/views/conversation/widgets/show_transfer_message.dart create mode 100644 lib/views/conversation/widgets/show_video_message.dart create mode 100644 lib/views/conversation/widgets/tim_emoji_panel.dart diff --git a/lib/constants/message_constant.dart b/lib/constants/message_constant.dart new file mode 100644 index 0000000..2a5bfa9 --- /dev/null +++ b/lib/constants/message_constant.dart @@ -0,0 +1,37 @@ +class HistoryMessageDartConstant { + static const getCount = 20; + + // ignore: constant_identifier_names + static const V2_TIM_IMAGE_TYPES = { + 'ORIGINAL': 0, + 'BIG': 1, + 'SMALL': 2, + }; + + static Map> imgPriorMap = { + V2_TIM_IMAGE_TYPES_ENUM.original: oriImgPrior, + V2_TIM_IMAGE_TYPES_ENUM.big: bigImgPrior, + V2_TIM_IMAGE_TYPES_ENUM.small: smallImgPrior, + }; + + // 缩略图优先,大图次之,最后是原图 + static const smallImgPrior = ['ORIGINAL', 'BIG', 'SMALL']; + // 大图优先,原图次之,最后是缩略图 + static const bigImgPrior = ['SMALL', 'ORIGINAL', 'BIG']; + // 原图优先,大图次之,最后是缩略图 + static const oriImgPrior = ['SMALL', 'BIG', 'ORIGINAL']; + + // 视频、音频已读状态 + static const int read = 1; +} + +enum V2_TIM_IMAGE_TYPES_ENUM { + original, + big, + small, +} + +enum IMG_PREVIEW_TYPE { + local, + url, +} diff --git a/lib/controllers/user_controller.dart b/lib/controllers/user_controller.dart index ce599ae..3b30ddf 100644 --- a/lib/controllers/user_controller.dart +++ b/lib/controllers/user_controller.dart @@ -1,5 +1,4 @@ import 'package:chat/models/upload_model.dart'; -import 'package:chat/models/user_info_model.dart'; import 'package:chat/providers/user_provider.dart'; import 'package:chat/services/auth_service.dart'; import 'package:chat/services/tim/friend_service.dart'; @@ -9,9 +8,6 @@ import 'package:get/get.dart'; class UserController extends GetxController { static UserController get to => Get.find(); - /// 用户信息,这个数据,在更新用户资料的时候,也应该更新 - Rx userInfo = UserInfoModel.empty().obs; - Future updateNickname(String nickname) async { var result = await UserProvider.updateNickname(nickname); if (result) { diff --git a/lib/models/im/contact_info_model.dart b/lib/models/im/contact_info_model.dart index da8c2c4..bf88e4b 100644 --- a/lib/models/im/contact_info_model.dart +++ b/lib/models/im/contact_info_model.dart @@ -10,6 +10,7 @@ class ContactInfoModel extends ISuspensionBean { V2TimFriendInfo? friendInfo; IconData? icon; Color? color; + String? route; ContactInfoModel({ required this.name, @@ -19,6 +20,7 @@ class ContactInfoModel extends ISuspensionBean { this.friendInfo, this.icon, this.color, + this.route, }); @override diff --git a/lib/providers/user_provider.dart b/lib/providers/user_provider.dart index c85d4cd..50b0cb6 100644 --- a/lib/providers/user_provider.dart +++ b/lib/providers/user_provider.dart @@ -1,3 +1,4 @@ +import 'package:chat/models/im/search_user_model.dart'; import 'package:chat/models/upload_model.dart'; import 'package:chat/utils/network/http.dart'; import 'package:chat/utils/ui_tools.dart'; @@ -32,4 +33,25 @@ class UserProvider { return false; } } + + /// 从服务器查找用户 + static Future?> searchUser(String keyword) async { + try { + var json = await Http.get( + 'user/search', + params: { + 'keyword': keyword, + }, + ); + + return List.from( + json.map( + (x) => SearchUserModel.fromJson(x), + ), + ); + } catch (err) { + UiTools.toast(err.toString()); + } + return null; + } } diff --git a/lib/routes/contact_routes.dart b/lib/routes/contact_routes.dart index 492306f..102506c 100644 --- a/lib/routes/contact_routes.dart +++ b/lib/routes/contact_routes.dart @@ -24,6 +24,7 @@ abstract class ContactRoutes { static const String friendProfile = '/contact/friend/profile'; static const String friendProfileMore = '/contact/friend/profile/more'; static const String friendRemark = '/contact/friend/remark'; + static const String friendRequest = '/contact/friend/request'; static const String friendRequestApply = '/contact/friend/request/apply'; static const String friendRecommend = '/contact/friend/recommend'; static const String friendRecommendFriend = @@ -31,6 +32,7 @@ abstract class ContactRoutes { static const String friendRecommendGroup = '/contact/friend/recommend/group'; static const String group = '/contact/group'; + static const String groupSearch = '/contact/group/search'; static const String groupQrCode = '/contact/group/qrCode'; static const String groupCreate = '/contact/group/create'; static const String groupNotification = '/contact/group/notification'; diff --git a/lib/services/tim/friend_service.dart b/lib/services/tim/friend_service.dart index 5650d31..5a8dde4 100644 --- a/lib/services/tim/friend_service.dart +++ b/lib/services/tim/friend_service.dart @@ -1,9 +1,8 @@ import 'package:azlistview/azlistview.dart'; import 'package:chat/models/im/contact_info_model.dart'; -import 'package:chat/models/im/search_user_model.dart'; +import 'package:chat/routes/contact_routes.dart'; import 'package:chat/services/tim_service.dart'; import 'package:chat/utils/im_tools.dart'; -import 'package:chat/utils/network/http.dart'; import 'package:chat/utils/ui_tools.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; @@ -35,6 +34,7 @@ class TimFriendService extends GetxService { tagIndex: '@', icon: Icons.person_add_alt, color: Colors.amber, + route: ContactRoutes.friendRequest, ), ContactInfoModel( name: '群聊', @@ -42,6 +42,7 @@ class TimFriendService extends GetxService { tagIndex: '@', icon: Icons.group, color: Colors.green, + route: ContactRoutes.group, ), ContactInfoModel( name: '订阅消息', @@ -243,25 +244,4 @@ class TimFriendService extends GetxService { UiTools.toast(result.desc); return false; } - - /// 从服务器查找用户 - Future?> searchUser(String keyword) async { - try { - var json = await Http.get( - 'user/search', - params: { - 'keyword': keyword, - }, - ); - - return List.from( - json.map( - (x) => SearchUserModel.fromJson(x), - ), - ); - } catch (err) { - UiTools.toast(err.toString()); - } - return null; - } } diff --git a/lib/utils/sound_record.dart b/lib/utils/sound_record.dart new file mode 100644 index 0000000..faf0a40 --- /dev/null +++ b/lib/utils/sound_record.dart @@ -0,0 +1,66 @@ +import 'dart:async'; + +import 'package:flutter_plugin_record_plus/const/play_state.dart'; +import 'package:flutter_plugin_record_plus/const/response.dart'; +import 'package:flutter_plugin_record_plus/index.dart'; + +typedef PlayStateListener = void Function(PlayState playState); +typedef SoundInterruptListener = void Function(); +typedef ResponseListener = void Function(RecordResponse recordResponse); + +class SoundPlayer { + static final FlutterPluginRecord _recorder = FlutterPluginRecord(); + static SoundInterruptListener? _soundInterruptListener; + static bool isInited = false; + + static initSoundPlayer() { + if (!isInited) { + _recorder.init(); + isInited = true; + } + } + + static play({required String url}) { + _recorder.stopPlay(); + if (_soundInterruptListener != null) { + _soundInterruptListener!(); + } + _recorder.playByPath(url, 'url'); + } + + static stop() { + _recorder.stopPlay(); + } + + static dispose() { + _recorder.dispose(); + } + + static StreamSubscription playStateListener( + {required PlayStateListener listener}) => + _recorder.responsePlayStateController.listen(listener); + + static setSoundInterruptListener(SoundInterruptListener listener) { + _soundInterruptListener = listener; + } + + static removeSoundInterruptListener() { + _soundInterruptListener = null; + } + + static StreamSubscription responseListener( + ResponseListener listener) => + _recorder.response.listen(listener); + + static StreamSubscription responseFromAmplitudeListener( + ResponseListener listener) => + _recorder.responseFromAmplitude.listen(listener); + + static startRecord() { + _recorder.start(); + } + + static stopRecord() { + _recorder.stop(); + } +} diff --git a/lib/views/contact/firend/request/index_page.dart b/lib/views/contact/firend/request/index_page.dart index dc14cf5..edf58ac 100644 --- a/lib/views/contact/firend/request/index_page.dart +++ b/lib/views/contact/firend/request/index_page.dart @@ -1,6 +1,7 @@ import 'package:chat/configs/app_colors.dart'; import 'package:chat/controllers/private_controller.dart'; import 'package:chat/models/im/search_user_model.dart'; +import 'package:chat/providers/user_provider.dart'; import 'package:chat/routes/contact_routes.dart'; import 'package:chat/services/tim/apply_service.dart'; import 'package:chat/services/tim/friend_service.dart'; @@ -38,7 +39,7 @@ class _ImFriendRequestState extends State { searchList = null; }); } else { - var result = await TimFriendService.to.searchUser(e); + var result = await UserProvider.searchUser(e); setState(() { searchList = result; diff --git a/lib/views/contact/group/index_page.dart b/lib/views/contact/group/index_page.dart index be0e925..2801f76 100644 --- a/lib/views/contact/group/index_page.dart +++ b/lib/views/contact/group/index_page.dart @@ -1,15 +1,101 @@ +import 'package:chat/configs/app_colors.dart'; +import 'package:chat/routes/contact_routes.dart'; +import 'package:chat/routes/conversation_routes.dart'; +import 'package:chat/services/tim/conversation_service.dart'; +import 'package:chat/services/tim/group_service.dart'; +import 'package:chat/views/home/widgets/group_avatar.dart'; +import 'package:chat/widgets/custom_easy_refresh.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_easyrefresh/easy_refresh.dart'; +import 'package:get/get.dart'; +import 'package:tencent_im_sdk_plugin/models/v2_tim_group_info.dart'; class ContactGroupPage extends StatefulWidget { const ContactGroupPage({Key? key}) : super(key: key); @override - _ContactGroupPageState createState() => _ContactGroupPageState(); + State createState() => _ContactGroupPageState(); } class _ContactGroupPageState extends State { @override Widget build(BuildContext context) { - return Container(); + return Scaffold( + appBar: AppBar( + title: const Text('群聊'), + actions: [ + IconButton( + icon: const Icon(Icons.search_outlined), + onPressed: () { + Get.toNamed(ContactRoutes.groupSearch); + }, + ), + IconButton( + icon: const Icon(Icons.add_outlined), + onPressed: () { + Get.toNamed(ContactRoutes.groupCreate); + }, + ), + ], + ), + body: GetX(builder: (_) { + return EasyRefresh( + onRefresh: () async { + _.fetchList(); + }, + header: CustomEasyRefresh.header, + child: Column( + children: [ + ListView.separated( + shrinkWrap: true, + physics: const ClampingScrollPhysics(), + itemBuilder: (context, index) { + V2TimGroupInfo group = _.groups[index]; + return ListTile( + onTap: () async { + var conversation = await TimConversationService.to + .getById('group_' + group.groupID); + Get.toNamed( + ConversationRoutes.index, + arguments: { + 'conversation': conversation, + }, + ); + }, + leading: GroupAvatar(group.groupID), + tileColor: AppColors.white, + title: Text(group.groupName!), + subtitle: Text('成员数: ${group.memberCount}'), + ); + }, + separatorBuilder: (context, index) { + return const Divider( + height: 0, + indent: 72, + ); + }, + itemCount: _.groups.length, + ), + const Divider( + height: 0, + indent: 72, + ), + Container( + color: AppColors.white, + height: 54, + child: Center( + child: Text( + '${_.groups.length} 个群聊', + style: const TextStyle( + color: AppColors.unactive, + ), + ), + ), + ), + ], + ), + ); + }), + ); } } diff --git a/lib/views/contact/index/index_page.dart b/lib/views/contact/index/index_page.dart index 5abdd4a..30875cd 100644 --- a/lib/views/contact/index/index_page.dart +++ b/lib/views/contact/index/index_page.dart @@ -48,7 +48,11 @@ class _ContactPageState extends State { return Column( children: [ ListTile( - onTap: () async {}, + onTap: () async { + if (info.route != null) { + Get.toNamed(info.route!); + } + }, leading: Container( width: 40, height: 40, diff --git a/lib/views/conversation/preview/image_widget.dart b/lib/views/conversation/preview/image_widget.dart new file mode 100644 index 0000000..a984595 --- /dev/null +++ b/lib/views/conversation/preview/image_widget.dart @@ -0,0 +1,78 @@ +import 'dart:io'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:chat/configs/app_colors.dart'; +import 'package:chat/constants/message_constant.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:photo_view/photo_view.dart'; +import 'package:photo_view/photo_view_gallery.dart'; + +class PreviewImageWidget extends StatelessWidget { + final IMG_PREVIEW_TYPE type; + + final String path; + + final String original; + + const PreviewImageWidget({ + Key? key, + required this.type, + required this.path, + required this.original, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Material( + color: AppColors.black, + child: SafeArea( + child: Stack( + children: [ + Positioned.fill( + child: PhotoViewGallery.builder( + pageController: PageController(initialPage: 0), + itemCount: 1, + builder: (context, index) { + if (type == IMG_PREVIEW_TYPE.local) { + return PhotoViewGalleryPageOptions( + imageProvider: FileImage(File(path)), + minScale: PhotoViewComputedScale.contained, + maxScale: PhotoViewComputedScale.covered * 2, + ); + } else { + if (path.split('?').first.isImageFileName) { + return PhotoViewGalleryPageOptions( + imageProvider: CachedNetworkImageProvider(path), + minScale: PhotoViewComputedScale.contained, + maxScale: PhotoViewComputedScale.covered * 2, + ); + } else { + return PhotoViewGalleryPageOptions.customChild( + child: const Center( + child: Text( + '格式不支持', + style: TextStyle( + color: AppColors.white, + ), + ), + ), + ); + } + } + }, + ), + ), + const SafeArea( + child: Padding( + padding: EdgeInsets.all(4.0), + child: BackButton( + color: AppColors.white, + ), + ), + ) + ], + ), + ), + ); + } +} diff --git a/lib/views/conversation/preview/video_widget.dart b/lib/views/conversation/preview/video_widget.dart new file mode 100644 index 0000000..e091ee9 --- /dev/null +++ b/lib/views/conversation/preview/video_widget.dart @@ -0,0 +1,119 @@ +import 'dart:io'; + +import 'package:chat/configs/app_colors.dart'; +import 'package:chat/utils/ui_tools.dart'; +import 'package:flutter/material.dart'; +import 'package:tencent_im_sdk_plugin/models/v2_tim_video_elem.dart'; +import 'package:video_player/video_player.dart'; + +class PreviewVideoWidget extends StatefulWidget { + final V2TimVideoElem video; + const PreviewVideoWidget(this.video, {Key? key}) : super(key: key); + + @override + State createState() => _PreviewVideoWidgetState(); +} + +class _PreviewVideoWidgetState extends State { + late VideoPlayerController _controller; + bool isPause = false; + + @override + void initState() { + super.initState(); + + var lv = widget.video.localVideoUrl; + + if (lv != null && lv.isNotEmpty && File(lv).existsSync()) { + _controller = + VideoPlayerController.file(File(widget.video.localVideoUrl!)) + ..initialize().then((value) { + setState(() { + _controller.play(); + }); + }).catchError((e) { + UiTools.toast(e.toString()); + }); + } else { + _controller = VideoPlayerController.network(widget.video.videoUrl!) + ..initialize().then((value) { + setState(() { + _controller.play(); + }); + }).catchError((e) { + UiTools.toast(e.toString()); + }); + } + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.mainBlack, + appBar: AppBar( + backgroundColor: AppColors.transparent, + foregroundColor: AppColors.white, + ), + body: GestureDetector( + onTap: () { + setState(() { + if (_controller.value.isInitialized) { + if (_controller.value.isPlaying) { + _controller.pause(); + isPause = true; + } else { + _controller.play(); + isPause = false; + } + } + }); + }, + child: Stack( + children: [ + Center( + child: _controller.value.isInitialized + ? AspectRatio( + aspectRatio: _controller.value.aspectRatio, + child: VideoPlayer(_controller), + ) + : _controller.value.hasError + ? const Text( + '视频加载失败', + style: TextStyle( + color: AppColors.white, + ), + ) + : const CircularProgressIndicator( + color: AppColors.primary, + ), + ), + Align( + alignment: Alignment.bottomCenter, + child: VideoProgressIndicator( + _controller, + allowScrubbing: true, + ), + ), + Align( + alignment: Alignment.center, + child: Visibility( + visible: isPause, + child: const Icon( + Icons.play_circle_outline, + size: 96, + color: AppColors.white, + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/views/conversation/widgets/message_area.dart b/lib/views/conversation/widgets/message_area.dart new file mode 100644 index 0000000..690f35a --- /dev/null +++ b/lib/views/conversation/widgets/message_area.dart @@ -0,0 +1,80 @@ +import 'package:chat/services/tim/message_service.dart'; +import 'package:chat/views/conversation/widgets/message_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:scroll_to_index/scroll_to_index.dart'; +import 'package:tencent_im_sdk_plugin/models/v2_tim_conversation.dart'; +import 'package:tencent_im_sdk_plugin/models/v2_tim_message.dart'; + +class MessageArea extends StatefulWidget { + final V2TimConversation conversation; + + const MessageArea(this.conversation, {Key? key}) : super(key: key); + + @override + State createState() => _MessageAreaState(); +} + +class _MessageAreaState extends State { + late AutoScrollController _scrollController; + String? _lastMessageId; + + @override + void initState() { + super.initState(); + _scrollController = AutoScrollController( + viewportBoundaryGetter: () => Rect.fromLTRB( + 0, + 0, + 0, + MediaQuery.of(context).padding.bottom, + ), + axis: Axis.vertical, + ); + + _loadMessages(); + } + + Future _loadMessages() async { + TimMessageService.to + .loadMessagesFromService( + widget.conversation, + _lastMessageId, + _scrollController, + ) + .then((value) { + _lastMessageId = value; + }); + } + + @override + Widget build(BuildContext context) { + return GetX(builder: (service) { + return ListView.separated( + scrollDirection: Axis.vertical, + controller: _scrollController, + shrinkWrap: true, + physics: const ClampingScrollPhysics(), + padding: const EdgeInsets.all(8), + reverse: true, + cacheExtent: 1200, + addAutomaticKeepAlives: true, + itemCount: service.messages.length, + itemBuilder: (_, index) { + V2TimMessage? message = service.messages[index]; + return AutoScrollTag( + key: ValueKey(index), + controller: _scrollController, + index: index, + child: MessageWidget(message), + ); + }, + separatorBuilder: (c, i) { + return Container( + height: 8, + ); + }, + ); + }); + } +} diff --git a/lib/views/conversation/widgets/message_field.dart b/lib/views/conversation/widgets/message_field.dart new file mode 100644 index 0000000..b799290 --- /dev/null +++ b/lib/views/conversation/widgets/message_field.dart @@ -0,0 +1,676 @@ +import 'dart:async'; +import 'package:chat/configs/app_colors.dart'; +import 'package:chat/routes/contact_routes.dart'; +import 'package:chat/services/tim/conversation_service.dart'; +import 'package:chat/utils/sound_record.dart'; +import 'package:chat/utils/ui_tools.dart'; +import 'package:chat/views/conversation/widgets/tim_emoji_panel.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/material.dart'; +// import 'package:flutter_baidu_mapapi_search/flutter_baidu_mapapi_search.dart'; +import 'package:get/get.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:tencent_im_sdk_plugin/enum/conversation_type.dart'; +import 'package:tencent_im_sdk_plugin/models/v2_tim_conversation.dart'; +import 'package:wechat_assets_picker/wechat_assets_picker.dart'; +import 'package:wechat_camera_picker/wechat_camera_picker.dart'; + +class MessageField extends StatefulWidget { + final V2TimConversation conversation; + + const MessageField(this.conversation, {Key? key}) : super(key: key); + + @override + State createState() => _MessageFieldState(); +} + +class _MessageFieldState extends State { + final TextEditingController _controller = TextEditingController(); + + bool isVoice = false; + String _sendText = ''; + double paddingBottom = 0.0; + String soundTipsText = "手指上滑,取消发送"; + bool isRecording = false; + bool isInit = false; + bool isCancelSend = false; + DateTime startTime = DateTime.now(); + List> subscriptions = []; + + OverlayEntry? overlayEntry; + String voiceIcon = "assets/chats/voice_volume_1.png"; + final FocusNode _focusNode = FocusNode(); + bool showMore = false; + bool showEmojiPanel = false; + bool showKeyboard = false; + double lastkeyboardHeight = 0; + + @override + void dispose() { + _controller.dispose(); + for (var subscription in subscriptions) { + subscription.cancel(); + } + super.dispose(); + } + + Future sendMessage() async { + var text = _controller.text; + + if (text.isEmpty) { + return; + } + + TimConversationService.to.sendTextMessage(widget.conversation, text); + _controller.text = ''; + } + + /// 发送媒体消息 + Future sendMeidaMessage(AssetEntity asset) async { + if (asset.type == AssetType.image) { + TimConversationService.to.sendImageMessage( + widget.conversation, + asset, + ); + } else if (asset.type == AssetType.video) { + TimConversationService.to.sendVideoMessage( + widget.conversation, + asset, + ); + } else { + UiTools.toast('暂不支持的类型'); + } + } + + hideAllPanel() { + _focusNode.unfocus(); + setState(() { + showKeyboard = false; + showMore = false; + showEmojiPanel = false; + }); + } + + double _getBottomHeight() { + listenKeyBoardStatus(); + if (showMore || showEmojiPanel) { + return 248.0; + } + // 在文本框多行拓展时增加保护区高度 + else if (_controller.text.length >= 46 && showKeyboard == false) { + return 25; + } else { + return 0; + } + } + + listenKeyBoardStatus() { + final currentKeyboardHeight = MediaQuery.of(context).viewInsets.bottom; + // 键盘弹出 + if (currentKeyboardHeight - lastkeyboardHeight > 0) { + // 保证弹出时showKeyboard为true + setState(() { + showKeyboard = true; + }); + + /// 键盘收回 + } else if (currentKeyboardHeight - lastkeyboardHeight < 0) {} + + lastkeyboardHeight = MediaQuery.of(context).viewInsets.bottom; + } + + _openMore() { + if (showMore) { + _focusNode.requestFocus(); + } else { + _focusNode.unfocus(); + } + setState(() { + showKeyboard = showMore; + showEmojiPanel = false; + showMore = !showMore; + }); + } + + @override + Widget build(BuildContext context) { + return SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Divider(height: 0), + Container( + padding: EdgeInsets.only( + left: 8, + right: 8, + top: 8, + bottom: 8 + Get.mediaQuery.viewInsets.bottom, + ), + child: Row( + children: [ + _voiceButton(), + const SizedBox(width: 8), + Expanded(child: _inputArea()), + const SizedBox(width: 8), + _emojiButton(), + const SizedBox(width: 8), + _sendButton() + ], + ), + ), + const Divider(height: 0), + AnimatedContainer( + height: _getBottomHeight(), + duration: const Duration(milliseconds: 150), + padding: showEmojiPanel + ? const EdgeInsets.all(0) + : const EdgeInsets.all(16), + child: _getBottomContainer(), + ), + ], + ), + ); + } + + Widget _actionWidget( + String text, { + required IconData icon, + VoidCallback? onTap, + }) { + return InkWell( + onTap: () { + onTap?.call(); + }, + child: Column( + children: [ + Container( + padding: const EdgeInsets.all(15), + decoration: BoxDecoration( + color: AppColors.white, + borderRadius: BorderRadius.circular(5), + ), + child: Icon( + icon, + color: AppColors.unactive, + ), + ), + const SizedBox(height: 3), + Text( + text, + style: const TextStyle( + color: AppColors.unactive, + fontSize: 12, + ), + ), + ], + ), + ); + } + + _openEmojiPanel() { + if (showEmojiPanel) { + _focusNode.requestFocus(); + } else { + _focusNode.unfocus(); + } + setState(() { + showKeyboard = showEmojiPanel; + showMore = false; + showEmojiPanel = !showEmojiPanel; + }); + } + + Widget _emojiButton() { + return InkWell( + onTap: () { + _openEmojiPanel(); + }, + child: const Icon( + Icons.emoji_emotions_outlined, + size: 28, + ), + ); + } + + Widget _voiceButton() { + if (!isVoice) { + return InkWell( + onTap: () { + setState(() { + isVoice = true; + }); + }, + child: const Icon( + Icons.mic_outlined, + size: 28, + ), + ); + } else { + return InkWell( + onTap: () { + setState(() { + isVoice = false; + }); + }, + child: const Icon( + Icons.keyboard_outlined, + size: 28, + ), + ); + } + } + + Widget _inputArea() { + if (isVoice) { + return GestureDetector( + onTapDown: (_) async { + if (!isInit) { + var result = await Permission.microphone.request().isGranted; + if (result) initRecordSound(); + } + }, + onLongPressStart: onLongPressStart, + onLongPressMoveUpdate: onLongPressUpdate, + onLongPressEnd: onLongPressEnd, + onLongPressCancel: onLonePressCancel, + child: Container( + alignment: Alignment.center, + padding: const EdgeInsets.only( + left: 8.0, + right: 8.0, + top: 8.0, + bottom: 8.0, + ), + decoration: const BoxDecoration( + color: AppColors.white, + borderRadius: BorderRadius.all( + Radius.circular( + 4.0, + ), + ), + ), + child: const Text('按住说话'), + ), + ); + } else { + return Container( + padding: const EdgeInsets.only( + left: 8.0, + right: 8.0, + top: 8.0, + bottom: 8.0, + ), + decoration: const BoxDecoration( + color: AppColors.white, + borderRadius: BorderRadius.all( + Radius.circular( + 4.0, + ), + ), + ), + child: TextField( + controller: _controller, + focusNode: _focusNode, + decoration: null, + onChanged: (e) { + setState(() { + _sendText = e; + }); + }, + onTap: () { + showKeyboard = true; + showMore = false; + showEmojiPanel = false; + }, + onSubmitted: (value) { + sendMessage(); + }, + ), + ); + } + } + + Widget _sendButton() { + if (_sendText.isEmpty) { + return InkWell( + onTap: () { + _openMore(); + }, + child: const Icon( + Icons.add_circle_outline_outlined, + size: 28, + color: AppColors.active, + ), + ); + } else { + return InkWell( + onTap: () { + sendMessage(); + }, + child: Container( + height: 28.0, + width: 54.0, + alignment: Alignment.center, + decoration: const BoxDecoration( + color: AppColors.primary, + borderRadius: BorderRadius.all( + Radius.circular( + 8.0, + ), + ), + ), + child: const Text( + '发送', + style: TextStyle( + letterSpacing: 2, + color: AppColors.white, + fontSize: 14.0, + ), + ), + ), + ); + } + } + + initRecordSound() { + final responseSubscription = SoundPlayer.responseListener((recordResponse) { + final status = recordResponse.msg; + if (status == "onStop") { + if (!isCancelSend) { + final soundPath = recordResponse.path; + final recordDuration = recordResponse.audioTimeLength; + TimConversationService.to.sendSoundMessage( + widget.conversation, soundPath!, recordDuration!.toInt()); + } + } else if (status == "onStart") { + // print("start record"); + setState(() { + isRecording = true; + }); + } else { + // print(status); + } + }); + final amplitutdeResponseSubscription = + SoundPlayer.responseFromAmplitudeListener((recordResponse) { + final voiceData = double.parse(recordResponse.msg!); + setState(() { + if (voiceData > 0 && voiceData < 0.1) { + voiceIcon = "assets/chats/voice_volume_2.png"; + } else if (voiceData > 0.2 && voiceData < 0.3) { + voiceIcon = "assets/chats/voice_volume_3.png"; + } else if (voiceData > 0.3 && voiceData < 0.4) { + voiceIcon = "assets/chats/voice_volume_4.png"; + } else if (voiceData > 0.4 && voiceData < 0.5) { + voiceIcon = "assets/chats/voice_volume_5.png"; + } else if (voiceData > 0.5 && voiceData < 0.6) { + voiceIcon = "assets/chats/voice_volume_6.png"; + } else if (voiceData > 0.6 && voiceData < 0.7) { + voiceIcon = "assets/chats/voice_volume_7.png"; + } else if (voiceData > 0.7 && voiceData < 1) { + voiceIcon = "assets/chats/voice_volume_7.png"; + } else { + voiceIcon = "assets/chats/voice_volume_1.png"; + } + if (overlayEntry != null) { + overlayEntry!.markNeedsBuild(); + } + }); + }); + subscriptions = [responseSubscription, amplitutdeResponseSubscription]; + SoundPlayer.initSoundPlayer(); + isInit = true; + } + + buildOverLayView(BuildContext context) { + if (overlayEntry == null) { + overlayEntry = OverlayEntry(builder: (content) { + return Positioned( + top: MediaQuery.of(context).size.height * 0.5 - 80, + left: MediaQuery.of(context).size.width * 0.5 - 80, + child: Material( + type: MaterialType.transparency, + 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( + children: [ + Container( + margin: const EdgeInsets.only(top: 10), + child: Image.asset( + voiceIcon, + width: 100, + height: 100, + // package: 'flutter_plugin_record', + ), + ), + Text( + soundTipsText, + style: const TextStyle( + fontStyle: FontStyle.normal, + color: Colors.white, + fontSize: 14, + ), + ) + ], + ), + ), + ), + ), + ), + ); + }); + Overlay.of(context)!.insert(overlayEntry!); + } + } + + onLongPressStart(_) { + if (isInit) { + 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 != '松开取消') { + setState(() { + soundTipsText = '松开取消'; + }); + } + } else { + if (mounted && soundTipsText == '松开取消') { + setState(() { + soundTipsText = '手指上滑,取消发送'; + }); + } + } + } + + 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; + UiTools.toast('说话时间太短!'); + } + stop(); + } + + onLonePressCancel() { + if (isRecording) { + isCancelSend = true; + if (overlayEntry != null) { + overlayEntry!.remove(); + overlayEntry = null; + } + stop(); + } + } + + void stop() { + setState(() { + isRecording = false; + }); + SoundPlayer.stopRecord(); + setState(() { + soundTipsText = '手指上滑,取消发送'; + }); + } + + _getMoreActions() { + var list = List.empty(growable: true); + list.add( + _actionWidget('相册', icon: Icons.photo, onTap: () async { + var result = await AssetPicker.pickAssets( + context, + pickerConfig: const AssetPickerConfig( + maxAssets: 9, + requestType: RequestType.common, + ), + ); + if (result == null) { + return; + } + for (var asset in result) { + sendMeidaMessage(asset); + } + }), + ); + list.add( + _actionWidget('拍照', icon: Icons.photo_camera, onTap: () async { + var asset = await CameraPicker.pickFromCamera( + context, + pickerConfig: const CameraPickerConfig( + enableRecording: true, + ), + ); + if (asset == null) { + return; + } + sendMeidaMessage(asset); + }), + ); + if (widget.conversation.type == ConversationType.V2TIM_C2C) { + list.add( + _actionWidget( + '视频通话', + icon: Icons.videocam, + onTap: () { + // ImTools.showTrtcMessage(widget.conversation.userID!); + }, + ), + ); + } + list.add( + _actionWidget( + '位置', + icon: Icons.pin_drop, + onTap: () { + // Get.toNamed(ImRoutes.conversationMap)?.then((value) { + // var _bmfPoiInfo = value['result'] as BMFPoiInfo; + // var snapshot = value['snapshot'] as Uint8List?; + // var model = LocationModel( + // name: _bmfPoiInfo.name!, + // address: _bmfPoiInfo.address!, + // list: snapshot!, + // latitude: _bmfPoiInfo.pt?.latitude ?? 0, + // longitude: _bmfPoiInfo.pt?.longitude ?? 0, + // ); + // TimConversationService.to.sendLocationMessage( + // widget.conversation, + // model, + // ); + // }); + }, + ), + ); + list.add( + _actionWidget( + '名片', + icon: Icons.person, + onTap: () { + Get.toNamed( + ContactRoutes.friend, + arguments: { + 'name_card': true, + }, + )?.then((value) { + var model = value?['result']; + if (model != null) { + TimConversationService.to.sendCustomMessage( + widget.conversation, + model, + 'NAME_CARD', + ); + } + }); + }, + ), + ); + list.add( + _actionWidget( + '文件', + icon: Icons.folder, + onTap: () async { + FilePickerResult? result = await FilePicker.platform.pickFiles(); + if (result != null) { + TimConversationService.to.sendFileMessage( + widget.conversation, + result.names.first!, + result.paths.first!, + ); + } + }, + ), + ); + return list; + } + + Widget _getBottomContainer() { + if (showEmojiPanel) { + // eventBus.fire('scrollToBottom'); + return EmojiPanel( + onTapEmoji: (unicode) { + final oldText = _controller.text; + final newText = String.fromCharCode(unicode); + _controller.text = "$oldText$newText"; + setState(() { + _sendText = _controller.text; + }); + }, + ); + } + if (showMore) { + // eventBus.fire('scrollToBottom'); + return GridView.count( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + crossAxisCount: 4, + children: _getMoreActions(), + ); + } + return Container(); + } +} diff --git a/lib/views/conversation/widgets/message_list.dart b/lib/views/conversation/widgets/message_list.dart new file mode 100644 index 0000000..8b9b5c3 --- /dev/null +++ b/lib/views/conversation/widgets/message_list.dart @@ -0,0 +1,126 @@ +import 'package:chat/services/tim/message_service.dart'; +import 'package:chat/widgets/custom_easy_refresh.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_easyrefresh/easy_refresh.dart'; +import 'package:get/get.dart'; +import 'package:scroll_to_index/scroll_to_index.dart'; +import 'package:tencent_im_sdk_plugin/models/v2_tim_conversation.dart'; +import 'package:tencent_im_sdk_plugin/models/v2_tim_message.dart'; +import 'message_widget.dart'; + +class MessageList extends StatefulWidget { + final V2TimConversation conversation; + + const MessageList(this.conversation, {Key? key}) : super(key: key); + + @override + State createState() => _MessageListState(); +} + +class _MessageListState extends State { + final EasyRefreshController _refreshController = EasyRefreshController(); + late AutoScrollController _scrollController; + String? _lastMessageId; + bool isFirst = true; + + @override + void dispose() { + super.dispose(); + } + + @override + void initState() { + super.initState(); + + _scrollController = AutoScrollController( + viewportBoundaryGetter: () => Rect.fromLTRB( + 0, + 0, + 0, + MediaQuery.of(context).padding.bottom, + ), + axis: Axis.vertical, + ); + + /// 接收到消息 和 自己发送消息 使列表滚动到最底部 + // eventBus.on().listen((event) { + // if (TimMessageService.to.curConversationId == + // widget.conversation.conversationID) { + // if (event is V2TimMessage) { + // if (mounted) { + // setState(() { + // TimMessageService.to.addMessage(event); + // }); + // } + // _scrollController.scrollToIndex( + // TimMessageService.to.messages.length - 1, + // preferPosition: AutoScrollPosition.begin, + // ); + // } + // } + // if (event is String && event == 'scrollToBottom') { + // Future.delayed(const Duration(milliseconds: 200), () { + // _scrollController.animateTo( + // _scrollController.position.maxScrollExtent, + // duration: const Duration(milliseconds: 200), + // curve: Curves.easeOut, + // ); + // }); + // } + // }); + _loadMessages(); + } + + Future _loadMessages() async { + TimMessageService.to + .loadMessagesFromService( + widget.conversation, + _lastMessageId, + _scrollController, + ) + .then((value) { + _lastMessageId = value; + }); + } + + @override + Widget build(BuildContext context) { + return GetX(builder: (service) { + return EasyRefresh( + controller: _refreshController, + header: CustomEasyRefresh.header, + footer: CustomEasyRefresh.footer, + firstRefresh: false, + onRefresh: _loadMessages, + child: ListView.separated( + scrollDirection: Axis.vertical, + controller: _scrollController, + shrinkWrap: true, + physics: const ClampingScrollPhysics(), + padding: const EdgeInsets.only( + left: 8, + right: 8, + top: 8, + bottom: 8, + ), + reverse: false, + itemCount: service.messages.length, + itemBuilder: (_, index) { + V2TimMessage? message = service.messages[index]; + return AutoScrollTag( + key: ValueKey(index), + controller: _scrollController, + index: index, + child: MessageWidget(message), + ); + }, + separatorBuilder: (c, i) { + return Container( + height: 8, + ); + }, + ), + ); + }); + } +} diff --git a/lib/views/conversation/widgets/message_widget.dart b/lib/views/conversation/widgets/message_widget.dart new file mode 100644 index 0000000..19c860d --- /dev/null +++ b/lib/views/conversation/widgets/message_widget.dart @@ -0,0 +1,274 @@ +import 'package:chat/configs/app_colors.dart'; +import 'package:chat/routes/contact_routes.dart'; +import 'package:chat/routes/user_routes.dart'; +import 'package:chat/services/auth_service.dart'; +import 'package:chat/views/conversation/widgets/show_custom_message.dart'; +import 'package:chat/views/conversation/widgets/show_face_message.dart'; +import 'package:chat/views/conversation/widgets/show_file_message.dart'; +import 'package:chat/views/conversation/widgets/show_image_message.dart'; +import 'package:chat/views/conversation/widgets/show_location_message.dart'; +import 'package:chat/views/conversation/widgets/show_merger_message.dart'; +import 'package:chat/views/conversation/widgets/show_sound_message.dart'; +import 'package:chat/views/conversation/widgets/show_text_message.dart'; +import 'package:chat/views/conversation/widgets/show_video_message.dart'; +import 'package:chat/widgets/custom_avatar.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:tencent_im_sdk_plugin/enum/group_change_info_type.dart'; +import 'package:tencent_im_sdk_plugin/enum/group_tips_elem_type.dart'; +import 'package:tencent_im_sdk_plugin/enum/message_elem_type.dart'; +import 'package:tencent_im_sdk_plugin/models/v2_tim_message.dart'; + +class MessageWidget extends StatelessWidget { + final V2TimMessage message; + + const MessageWidget(this.message, {Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + switch (message.elemType) { + case MessageElemType.V2TIM_ELEM_TYPE_TEXT: + return _buildMessage(ShowTextMessage(message)); + case MessageElemType.V2TIM_ELEM_TYPE_IMAGE: + return _buildMessage(ShowImageMessage(message)); + case MessageElemType.V2TIM_ELEM_TYPE_SOUND: + return _buildMessage(ShowSoundMessage(message)); + case MessageElemType.V2TIM_ELEM_TYPE_VIDEO: + return _buildMessage(ShowVideoMessage(message)); + case MessageElemType.V2TIM_ELEM_TYPE_FILE: + return _buildMessage(ShowFileMessage(message)); + case MessageElemType.V2TIM_ELEM_TYPE_LOCATION: + return _buildMessage(ShowLocationMessage(message)); + case MessageElemType.V2TIM_ELEM_TYPE_FACE: + return _buildMessage(ShowFaceMessage(message)); + case MessageElemType.V2TIM_ELEM_TYPE_MERGER: + return _buildMessage(ShowMergerMessage(message)); + case MessageElemType.V2TIM_ELEM_TYPE_CUSTOM: + return _buildMessage(ShowCustomMessage(message)); + case MessageElemType.V2TIM_ELEM_TYPE_GROUP_TIPS: + return _buildGroupTipMessage(); + default: + return Text('未识别的消息类型 ${message.elemType}'); + } + } + + /// 构造群提示消息 + Widget _buildGroupTipMessage() { + switch (message.groupTipsElem!.type) { + case GroupTipsElemType.V2TIM_GROUP_TIPS_TYPE_GROUP_INFO_CHANGE: + return _buildGroupInfoChangeMessage(); + case GroupTipsElemType.V2TIM_GROUP_TIPS_TYPE_JOIN: + return _buildGroupTypeJoinMessage(); + case GroupTipsElemType.V2TIM_GROUP_TIPS_TYPE_INVITE: + return _buildGroupTypeInviteMessage(); + case GroupTipsElemType.V2TIM_GROUP_TIPS_TYPE_QUIT: + return _buildGroupTypeQuitMessage(); + case GroupTipsElemType.V2TIM_GROUP_TIPS_TYPE_KICKED: + return _buildGroupTypeKickedMessage(); + case GroupTipsElemType.V2TIM_GROUP_TIPS_TYPE_SET_ADMIN: + return _buildGroupTypeSetAdminMessage(); + case GroupTipsElemType.V2TIM_GROUP_TIPS_TYPE_CANCEL_ADMIN: + return _buildGroupTypeCancelAdminMessage(); + case GroupTipsElemType.V2TIM_GROUP_TIPS_TYPE_MEMBER_INFO_CHANGE: + return _buildGroupTypeMemberInfoChangeMessage(); + default: + return Text(message.groupTipsElem!.type.toString()); + } + } + + Widget _buildGroupTypeMemberInfoChangeMessage() { + return Center( + child: Text( + '${message.groupTipsElem!.opMember.nickName}修改了群成员资料', + style: const TextStyle( + color: AppColors.unactive, + fontSize: 12, + ), + ), + ); + } + + Widget _buildGroupTypeCancelAdminMessage() { + String op = message.groupTipsElem!.opMember.nickName ?? ''; + String text = ''; + for (var e in message.groupTipsElem!.memberList!) { + text += e?.nickName ?? '\t'; + } + text = '$op取消了$text的管理员身份'; + return Center( + child: Text( + text, + style: const TextStyle( + color: AppColors.unactive, + fontSize: 12, + ), + ), + ); + } + + Widget _buildGroupTypeSetAdminMessage() { + String op = message.groupTipsElem!.opMember.nickName ?? ''; + String text = ''; + for (var e in message.groupTipsElem!.memberList!) { + text += e?.nickName ?? '\t'; + } + text = '$op将$text设置为管理员'; + return Center( + child: Text( + text, + style: const TextStyle( + color: AppColors.unactive, + fontSize: 12, + ), + ), + ); + } + + Widget _buildGroupTypeKickedMessage() { + String op = message.groupTipsElem!.opMember.nickName ?? ''; + String text = ''; + for (var e in message.groupTipsElem!.memberList!) { + text += e?.nickName ?? '\t'; + } + text = '$op将$text踢出群组'; + return Center( + child: Text( + text, + style: const TextStyle( + color: AppColors.unactive, + fontSize: 12, + ), + ), + ); + } + + /// 退出群组的消息解析 + Widget _buildGroupTypeQuitMessage() { + if (message.groupTipsElem!.memberList!.isEmpty) { + return Container(); + } + return Center( + child: Text( + '${message.groupTipsElem!.memberList!.first?.nickName}退出了群组', + style: const TextStyle( + color: AppColors.unactive, + fontSize: 12, + ), + ), + ); + } + + Widget _buildGroupTypeInviteMessage() { + String op = message.groupTipsElem!.opMember.nickName ?? ''; + String text = ''; + for (var e in message.groupTipsElem!.memberList!) { + text += e?.nickName ?? '\t'; + } + text = '$op邀请$text加入群组'; + return Center( + child: Text( + text, + style: const TextStyle( + color: AppColors.unactive, + fontSize: 12, + ), + ), + ); + } + + Widget _buildGroupTypeJoinMessage() { + String text = ''; + for (var e in message.groupTipsElem!.memberList!) { + text += e?.nickName ?? '\t'; + } + text = '$text加入群组'; + return Center( + child: Text( + text, + style: const TextStyle( + color: AppColors.unactive, + fontSize: 12, + ), + ), + ); + } + + /// 解析群资料修改的内容 + Widget _buildGroupInfoChangeMessage() { + String text = ''; + switch (message.groupTipsElem!.groupChangeInfoList!.first!.type) { + case GroupChangeInfoType.V2TIM_GROUP_INFO_CHANGE_TYPE_NAME: + text = + '${message.groupTipsElem!.opMember.nickName} 修改群名称 ${message.groupTipsElem!.groupChangeInfoList!.first!.value}'; + break; + case GroupChangeInfoType.V2TIM_GROUP_INFO_CHANGE_TYPE_INTRODUCTION: + text = '群简介修改'; + break; + case GroupChangeInfoType.V2TIM_GROUP_INFO_CHANGE_TYPE_NOTIFICATION: + text = '${message.groupTipsElem!.opMember.nickName} 更新了群公告'; + break; + case GroupChangeInfoType.V2TIM_GROUP_INFO_CHANGE_TYPE_FACE_URL: + text = '群头像修改'; + break; + case GroupChangeInfoType.V2TIM_GROUP_INFO_CHANGE_TYPE_OWNER: + text = '${message.groupTipsElem!.memberList!.first!.nickName} 成为新群主'; + break; + case GroupChangeInfoType.V2TIM_GROUP_INFO_CHANGE_TYPE_CUSTOM: + text = '群自定义字段变更'; + break; + default: + text = message.groupTipsElem!.groupChangeInfoList![0]!.type.toString(); + break; + } + + return Center( + child: Text( + text, + style: const TextStyle( + color: AppColors.unactive, + fontSize: 12, + ), + ), + ); + } + + /// 构造普通内容的消息 + Widget _buildMessage(Widget child) { + return Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + textDirection: message.isSelf! ? TextDirection.rtl : TextDirection.ltr, + children: [ + InkWell( + onTap: () { + if (message.isSelf!) { + Get.toNamed(UserRoutes.info); + } else { + Get.toNamed( + ContactRoutes.friendProfile, + ); + } + }, + child: message.isSelf! + ? GetX(builder: (_) { + return CustomAvatar( + _.userInfo.value.avatar ?? '', + size: 43, + radius: 4, + ); + }) + : CustomAvatar( + message.faceUrl, + size: 43, + radius: 4, + ), + ), + const SizedBox(width: 8), + GestureDetector( + onLongPress: () {}, + child: child, + ), + ], + ); + } +} diff --git a/lib/views/conversation/widgets/show_call_message.dart b/lib/views/conversation/widgets/show_call_message.dart new file mode 100644 index 0000000..a7d4344 --- /dev/null +++ b/lib/views/conversation/widgets/show_call_message.dart @@ -0,0 +1,61 @@ +import 'dart:convert'; + +import 'package:chat/configs/app_colors.dart'; +import 'package:chat/models/im/calling_model.dart'; +import 'package:chat/models/im/custom_message_model.dart'; +import 'package:flutter/material.dart'; +import 'package:tencent_im_sdk_plugin/models/v2_tim_message.dart'; + +class ShowCallMessage extends StatefulWidget { + final V2TimMessage message; + const ShowCallMessage(this.message, {Key? key}) : super(key: key); + + @override + State createState() => _ShowCallMessageState(); +} + +class _ShowCallMessageState extends State { + @override + Widget build(BuildContext context) { + var model = CallingModel.fromJson( + json.decode(widget.message.customElem!.data!), + ); + + final isVoiceCall = model.callType == CallingType.audioCall; + return Container( + constraints: const BoxConstraints(minHeight: 43), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + color: widget.message.isSelf! ? AppColors.primary : AppColors.white, + ), + child: Row( + children: [ + Icon( + isVoiceCall ? Icons.call_end : Icons.videocam, + size: 20, + color: widget.message.isSelf! ? AppColors.white : AppColors.active, + ), + const SizedBox(width: 4), + model.timeout > 0 + ? Text( + '通话时长', + style: TextStyle( + color: widget.message.isSelf! + ? AppColors.white + : AppColors.active, + ), + ) + : Text( + model.actionTypeText, + style: TextStyle( + color: widget.message.isSelf! + ? AppColors.white + : AppColors.active, + ), + ), + ], + ), + ); + } +} diff --git a/lib/views/conversation/widgets/show_custom_message.dart b/lib/views/conversation/widgets/show_custom_message.dart new file mode 100644 index 0000000..45f5cb1 --- /dev/null +++ b/lib/views/conversation/widgets/show_custom_message.dart @@ -0,0 +1,61 @@ +import 'dart:convert'; + +import 'package:chat/configs/app_colors.dart'; +import 'package:chat/models/im/custom_message_model.dart'; +import 'package:chat/views/conversation/widgets/show_call_message.dart'; +import 'package:chat/views/conversation/widgets/show_group_card_message.dart'; +import 'package:chat/views/conversation/widgets/show_name_card_message.dart'; +import 'package:chat/views/conversation/widgets/show_transfer_message.dart'; +import 'package:flutter/material.dart'; +import 'package:tencent_im_sdk_plugin/models/v2_tim_message.dart'; + +class ShowCustomMessage extends StatelessWidget { + final V2TimMessage message; + const ShowCustomMessage(this.message, {Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + try { + String type = + json.decode(message.customElem!.data!)['businessID'].toString(); + + switch (type) { + // case CustomMessageType.EVALUATION: + // return ShowEvaluationMessage(message); + case CustomMessageType.NAME_CARD: + return ShowNameCardMessage(message); + case CustomMessageType.DT_TRANSFER: + return ShowTransferMessage(message); + case CustomMessageType.CALL: + return ShowCallMessage(message); + case CustomMessageType.GROUP_CARD: + return ShowGroupCardMessage(message); + default: + return _unknowMessage(); + } + } catch (e) { + return _unknowMessage(); + } + } + + Widget _unknowMessage() { + return Container( + height: 44, + padding: const EdgeInsets.symmetric(horizontal: 8), + decoration: BoxDecoration( + color: AppColors.white, + borderRadius: BorderRadius.circular(4), + ), + alignment: Alignment.center, + child: Column( + children: [ + const Text('【自定义消息】'), + Text( + message.customElem!.data!, + maxLines: 5, + ), + ], + ), + ); + } +} diff --git a/lib/views/conversation/widgets/show_face_message.dart b/lib/views/conversation/widgets/show_face_message.dart new file mode 100644 index 0000000..0bedd90 --- /dev/null +++ b/lib/views/conversation/widgets/show_face_message.dart @@ -0,0 +1,12 @@ +import 'package:flutter/material.dart'; +import 'package:tencent_im_sdk_plugin/models/v2_tim_message.dart'; + +class ShowFaceMessage extends StatelessWidget { + final V2TimMessage message; + const ShowFaceMessage(this.message, {Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container(); + } +} diff --git a/lib/views/conversation/widgets/show_file_message.dart b/lib/views/conversation/widgets/show_file_message.dart new file mode 100644 index 0000000..7204724 --- /dev/null +++ b/lib/views/conversation/widgets/show_file_message.dart @@ -0,0 +1,70 @@ +import 'package:chat/configs/app_colors.dart'; +import 'package:chat/configs/app_size.dart'; +import 'package:filesize/filesize.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:tencent_im_sdk_plugin/models/v2_tim_message.dart'; + +class ShowFileMessage extends StatelessWidget { + final V2TimMessage message; + const ShowFileMessage(this.message, {Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: () async { + // try { + // var result = await OpenFile.open(message.fileElem!.localUrl); + // if (result.type != ResultType.done) { + // UiTools.toast(result.message); + // } + // } catch (e) { + // UiTools.toast(e.toString()); + // } + }, + child: Container( + padding: const EdgeInsets.all(12), + width: Get.width * 0.618, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + color: AppColors.white, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + message.fileElem!.fileName!, + overflow: TextOverflow.visible, + style: const TextStyle( + fontSize: AppSize.fontSize, + color: AppColors.active, + ), + ), + const SizedBox(height: 8), + Text( + filesize(message.fileElem!.fileSize), + overflow: TextOverflow.visible, + style: const TextStyle( + fontSize: AppSize.smallFontSize, + color: AppColors.unactive, + ), + ), + ], + ), + ), + const SizedBox(width: 8), + Image.asset( + 'assets/chats/file_msg.png', + width: 44, + ), + ], + ), + ), + ); + } +} diff --git a/lib/views/conversation/widgets/show_group_card_message.dart b/lib/views/conversation/widgets/show_group_card_message.dart new file mode 100644 index 0000000..132d4a5 --- /dev/null +++ b/lib/views/conversation/widgets/show_group_card_message.dart @@ -0,0 +1,59 @@ +import 'dart:convert'; + +import 'package:chat/configs/app_colors.dart'; +import 'package:chat/configs/app_size.dart'; +import 'package:chat/models/im/group_card_model.dart'; +import 'package:chat/views/home/widgets/group_avatar.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:tencent_im_sdk_plugin/models/v2_tim_message.dart'; + +class ShowGroupCardMessage extends StatelessWidget { + final V2TimMessage message; + const ShowGroupCardMessage(this.message, {Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + var model = GroupCardModel.fromJson( + json.decode(message.customElem!.data!), + ); + return Container( + width: Get.width * 0.618, + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + color: AppColors.white, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + GroupAvatar(model.groupID), + const SizedBox( + width: 8, + ), + Text( + model.groupName, + style: const TextStyle( + color: AppColors.primary, + overflow: TextOverflow.ellipsis, + ), + maxLines: 1, + ) + ], + ), + const Divider(height: 16), + const Text( + '个人名片', + style: TextStyle( + fontSize: AppSize.smallFontSize, + color: AppColors.primary, + ), + ), + ], + ), + ); + } +} diff --git a/lib/views/conversation/widgets/show_image_message.dart b/lib/views/conversation/widgets/show_image_message.dart new file mode 100644 index 0000000..0e1e019 --- /dev/null +++ b/lib/views/conversation/widgets/show_image_message.dart @@ -0,0 +1,123 @@ +import 'dart:io'; + +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:chat/configs/app_colors.dart'; +import 'package:chat/constants/message_constant.dart'; +import 'package:chat/views/conversation/preview/image_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:tencent_im_sdk_plugin/models/v2_tim_image.dart'; +import 'package:tencent_im_sdk_plugin/models/v2_tim_message.dart'; + +class ShowImageMessage extends StatelessWidget { + final V2TimMessage message; + + const ShowImageMessage(this.message, {Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + decoration: const BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(4)), + ), + constraints: BoxConstraints( + maxWidth: Get.width * 0.362, + minWidth: 64, + maxHeight: 192, + ), + clipBehavior: Clip.hardEdge, + child: _imageBuilder(), + ); + } + + Widget _imageBuilder() { + var path = message.imageElem!.path!; + var list = message.imageElem!.imageList!; + + V2TimImage? small = list.where((e) => e!.type == 2).first; + V2TimImage? big = list.where((e) => e!.type == 1).first; + V2TimImage? original = list.where((e) => e!.type == 0).first; + + /// 如果是从本机发出去的图片消息,并且图片还存在的情况 + if (path.isNotEmpty && File(path).existsSync()) { + return _showLocalImageFile(path, original!.localUrl!); + } else if (small != null && File(small.localUrl!).existsSync()) { + return _showLocalImageFile(small.localUrl!, original!.localUrl!); + } else if (big != null && File(big.localUrl!).existsSync()) { + return _showLocalImageFile(big.localUrl!, original!.localUrl!); + } else if (original != null && File(original.localUrl!).existsSync()) { + return _showLocalImageFile(original.localUrl!, original.localUrl!); + } else { + return GestureDetector( + onTap: () { + Get.to( + PreviewImageWidget( + type: IMG_PREVIEW_TYPE.url, + path: big!.url!, + original: original!.url!, + ), + transition: Transition.zoom, + ); + }, + child: CachedNetworkImage( + imageUrl: small!.url!, + cacheKey: 'CHAT_IMAGE', + alignment: Alignment.topCenter, + errorWidget: (context, error, stackTrace) => _errorDisplay(), + ), + ); + } + } + + Widget _showLocalImageFile(String path, String original) { + return GestureDetector( + onTap: () { + Get.to( + PreviewImageWidget( + type: IMG_PREVIEW_TYPE.local, + path: path, + original: original, + ), + transition: Transition.zoom, + ); + }, + child: Image.file( + File(path), + fit: BoxFit.fitWidth, + ), + ); + } + + Widget _errorDisplay() { + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + border: Border.all( + width: 0.4, + color: AppColors.unactive, + ), + ), + height: 96, + child: Center( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: const [ + Icon( + Icons.warning_amber_outlined, + size: 20, + color: AppColors.unactive, + ), + SizedBox(width: 4), + Text( + '图片加载失败', + style: TextStyle( + color: AppColors.unactive, + fontSize: 12, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/views/conversation/widgets/show_image_message_copy.dart b/lib/views/conversation/widgets/show_image_message_copy.dart new file mode 100644 index 0000000..db85052 --- /dev/null +++ b/lib/views/conversation/widgets/show_image_message_copy.dart @@ -0,0 +1,283 @@ +// import 'dart:io'; +// import 'dart:math'; +// import 'package:cached_network_image/cached_network_image.dart'; +// import 'package:flutter/material.dart'; +// import 'package:get/get.dart'; +// import 'package:scaffold/constants/message_constant.dart'; +// import 'package:scaffold/utils/ui_tools.dart'; +// import 'package:scaffold/views/im/conversation/preview/index_page.dart'; +// import 'package:scaffold/views/moments/index/widgets/media_preview.dart'; +// import 'package:tencent_im_sdk_plugin/models/v2_tim_image.dart'; +// import 'package:tencent_im_sdk_plugin/models/v2_tim_message.dart'; +// import 'package:transparent_image/transparent_image.dart'; + +// class ShowImageMessage extends StatefulWidget { +// final V2TimMessage message; + +// const ShowImageMessage(this.message, {Key? key}) : super(key: key); + +// @override +// State createState() => _ShowImageMessageState(); +// } + +// class _ShowImageMessageState extends State { +// bool imageIsRender = false; + +// @override +// didUpdateWidget(oldWidget) { +// var oldImgListLength = oldWidget.message.imageElem?.imageList?.length ?? 0; +// var currImgListLength = widget.message.imageElem?.imageList?.length ?? 0; +// if (currImgListLength == 1 && oldImgListLength == 0) { +// setState(() { +// imageIsRender = true; +// }); +// } +// super.didUpdateWidget(oldWidget); +// } + +// @override +// Widget build(BuildContext context) { +// V2TimImage? originalImg = +// getImageFromList(V2_TIM_IMAGE_TYPES_ENUM.original); +// V2TimImage? smallImg = getImageFromList(V2_TIM_IMAGE_TYPES_ENUM.small); + +// return Container( +// decoration: BoxDecoration( +// borderRadius: const BorderRadius.all(Radius.circular(4)), +// border: Border.all( +// color: const Color.fromRGBO(245, 166, 35, 0), +// width: 2, +// ), +// ), +// child: LayoutBuilder( +// builder: (BuildContext context, BoxConstraints constraints) { +// return ConstrainedBox( +// constraints: BoxConstraints( +// maxWidth: constraints.maxWidth * 0.5, +// minWidth: 64, +// maxHeight: 256, +// ), +// child: imageBuilder( +// originalImg: originalImg, +// smallImg: smallImg, +// ), +// ); +// }, +// ), +// ); +// } + +// Widget imageBuilder({V2TimImage? originalImg, V2TimImage? smallImg}) { +// if (originalImg == null) { +// // 有path +// if (widget.message.imageElem!.path!.isNotEmpty && +// File(widget.message.imageElem!.path!).existsSync()) { +// return getImage( +// GestureDetector( +// onTap: () { +// Get.to(ImagePreviewPage( +// type: IMG_PREVIEW_TYPE.local, +// path: widget.message.imageElem!.path!, +// )); +// }, +// child: Image.file( +// File(widget.message.imageElem!.path!), +// fit: BoxFit.fitWidth, +// ), +// ), +// imageElem: null, +// ); +// } else { +// return errorDisplay(); +// } +// } else if (!Platform.isAndroid && +// widget.message.imageElem!.path!.isNotEmpty && +// File(widget.message.imageElem!.path!).existsSync() && +// !imageIsRender) { +// return getImage( +// GestureDetector( +// onTap: () { +// Get.to(ImagePreviewPage( +// type: IMG_PREVIEW_TYPE.local, +// path: widget.message.imageElem!.path!, +// )); +// }, +// child: Image.file( +// File(widget.message.imageElem!.path!), +// fit: BoxFit.fitWidth, +// ), +// ), +// imageElem: e, +// ); +// } else if ((smallImg?.url ?? originalImg.url) != null && !imageIsRender) { +// double positionRadio = 1.0; +// if (smallImg?.width != null && +// smallImg?.height != null && +// smallImg?.width != 0 && +// smallImg?.height != 0) { +// positionRadio = (smallImg!.width! / smallImg.height!); +// } +// String bigImgUrl = originalImg.url ?? getBigPicUrl(); +// if (bigImgUrl.isEmpty && smallImg?.url != null) { +// bigImgUrl = smallImg!.url!; +// } +// return Stack( +// alignment: AlignmentDirectional.topStart, +// children: [ +// AspectRatio( +// aspectRatio: positionRadio, +// child: Container( +// decoration: const BoxDecoration(color: Colors.white), +// ), +// ), +// getImage( +// GestureDetector( +// onTap: () { +// Get.to(ImagePreviewPage( +// type: IMG_PREVIEW_TYPE.url, +// path: smallImg?.url ?? originalImg.url!, +// )); +// }, +// child: CachedNetworkImage( +// alignment: Alignment.topCenter, +// imageUrl: smallImg?.url ?? originalImg.url!, +// errorWidget: (context, error, stackTrace) => errorDisplay(), +// fit: BoxFit.fitWidth, +// cacheKey: smallImg?.uuid ?? originalImg.uuid!, +// placeholder: (context, url) => +// Image(image: MemoryImage(kTransparentImage)), +// fadeInDuration: const Duration(milliseconds: 0), +// ), +// ), +// imageElem: e, +// ), +// ], +// ); +// } else { +// // 有path +// if (widget.message.imageElem!.path!.isNotEmpty && +// File(widget.message.imageElem!.path!).existsSync()) { +// return getImage( +// GestureDetector( +// onTap: () { +// Get.to(ImagePreviewPage( +// type: IMG_PREVIEW_TYPE.local, +// path: widget.message.imageElem!.path!, +// )); +// }, +// child: Image.file( +// File(widget.message.imageElem!.path!), +// fit: BoxFit.fitWidth, +// ), +// ), +// imageElem: null, +// ); +// } else { +// return errorDisplay(); +// } +// } +// } + +// Widget errorDisplay() { +// return Container( +// decoration: BoxDecoration( +// borderRadius: const BorderRadius.all(Radius.circular(5)), +// border: Border.all( +// width: 1, +// color: Colors.black12, +// ), +// ), +// height: 100, +// child: Center( +// child: Row( +// mainAxisAlignment: MainAxisAlignment.center, +// children: const [ +// Icon( +// Icons.warning_amber_outlined, +// size: 16, +// ), +// Text( +// '图片加载失败', +// ), +// ], +// ), +// ), +// ); +// } + +// void pushToPreview(List mediaList) { +// Get.dialog( +// MomentMediaPreview( +// mediaSourceList: mediaList, +// initialPage: 0, +// ), +// useSafeArea: false, +// ); +// } + +// Widget getImage(image, {imageElem}) { +// Widget res = ClipRRect( +// clipper: ImageClipper(), +// child: image, +// ); +// return res; +// } + +// V2TimImage? getImageFromList(V2_TIM_IMAGE_TYPES_ENUM imgType) { +// V2TimImage? img = getImageFromImgList( +// widget.message.imageElem!.imageList, +// HistoryMessageDartConstant.imgPriorMap[imgType] ?? +// HistoryMessageDartConstant.oriImgPrior, +// ); +// return img; +// } + +// String getBigPicUrl() { +// // 实际拿的是原图 +// V2TimImage? img = getImageFromImgList( +// widget.message.imageElem!.imageList, +// HistoryMessageDartConstant.oriImgPrior, +// ); +// if (img == null) { +// setState(() { +// imageIsRender = true; +// }); +// } +// return img == null ? widget.message.imageElem!.path! : img.url!; +// } + +// V2TimImage? getImageFromImgList(List? list, List order) { +// V2TimImage? img; +// try { +// for (String type in order) { +// img = list?.firstWhere( +// (e) => e?.type == HistoryMessageDartConstant.V2_TIM_IMAGE_TYPES[type], +// orElse: () => null, +// ); +// } +// } catch (e) { +// UiTools.toast('getImageFromImgList error ${e.toString()}'); +// } +// return img; +// } +// } + +// class ImageClipper extends CustomClipper { +// @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 oldClipper) { +// return oldClipper != this; +// } +// } diff --git a/lib/views/conversation/widgets/show_location_message.dart b/lib/views/conversation/widgets/show_location_message.dart new file mode 100644 index 0000000..2ed9122 --- /dev/null +++ b/lib/views/conversation/widgets/show_location_message.dart @@ -0,0 +1,101 @@ +import 'package:chat/configs/app_colors.dart'; +import 'package:chat/configs/app_size.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:tencent_im_sdk_plugin/models/v2_tim_message.dart'; + +class ShowLocationMessage extends StatelessWidget { + final V2TimMessage message; + const ShowLocationMessage(this.message, {Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: () { + Permission.location.request().isGranted.then((value) { + if (value) { + // Get.toNamed( + // ConversationRoutes.conversationMapShow, + // arguments: { + // 'message': message, + // }, + // ); + } + }); + }, + child: Container( + width: Get.width * 0.618, + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + color: message.isSelf! ? AppColors.primary : AppColors.white, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.pin_drop_outlined, + size: 44, + color: message.isSelf! ? AppColors.white : AppColors.primary, + ), + const SizedBox( + width: 8, + ), + Expanded( + child: Text( + message.locationElem!.desc!, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: + message.isSelf! ? AppColors.white : AppColors.primary, + fontSize: AppSize.fontSize, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + Divider( + height: 16, + color: message.isSelf! ? AppColors.white : null, + ), + Text( + '位置消息', + style: TextStyle( + color: message.isSelf! ? AppColors.white : AppColors.primary, + fontSize: AppSize.smallFontSize, + ), + ), + ], + ), + ), + // child: Container( + // padding: const EdgeInsets.all(12), + // decoration: BoxDecoration( + // borderRadius: BorderRadius.circular(4), + // color: message.isSelf! ? AppColors.primary : AppColors.white, + // ), + // child: Row( + // children: [ + // Icon( + // Icons.pin_drop_outlined, + // size: 20, + // color: message.isSelf! ? AppColors.white : AppColors.active, + // ), + // Text( + // '【位置消息】点击查看', + // style: TextStyle( + // fontSize: AppSize.fontSize, + // color: message.isSelf! ? AppColors.white : AppColors.active, + // ), + // ), + // ], + // ), + // ), + ); + } +} diff --git a/lib/views/conversation/widgets/show_merger_message.dart b/lib/views/conversation/widgets/show_merger_message.dart new file mode 100644 index 0000000..bbf9185 --- /dev/null +++ b/lib/views/conversation/widgets/show_merger_message.dart @@ -0,0 +1,12 @@ +import 'package:flutter/material.dart'; +import 'package:tencent_im_sdk_plugin/models/v2_tim_message.dart'; + +class ShowMergerMessage extends StatelessWidget { + final V2TimMessage message; + const ShowMergerMessage(this.message, {Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container(); + } +} diff --git a/lib/views/conversation/widgets/show_name_card_message.dart b/lib/views/conversation/widgets/show_name_card_message.dart new file mode 100644 index 0000000..3672024 --- /dev/null +++ b/lib/views/conversation/widgets/show_name_card_message.dart @@ -0,0 +1,76 @@ +import 'dart:convert'; +import 'package:chat/configs/app_colors.dart'; +import 'package:chat/configs/app_size.dart'; +import 'package:chat/controllers/private_controller.dart'; +import 'package:chat/models/im/name_card_model.dart'; +import 'package:chat/routes/contact_routes.dart'; +import 'package:chat/widgets/custom_avatar.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:tencent_im_sdk_plugin/models/v2_tim_message.dart'; + +/// 个人名片消息 +class ShowNameCardMessage extends StatelessWidget { + final V2TimMessage message; + + const ShowNameCardMessage(this.message, {Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + var model = NameCardModel.fromJson( + json.decode(message.customElem!.data!), + ); + + return InkWell( + onTap: () async { + await PrivateController.to.setCurrentFriend( + model.userID, + ); + Get.toNamed( + ContactRoutes.friendProfile, + ); + }, + child: Container( + width: Get.width * 0.618, + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + color: AppColors.white, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + CustomAvatar( + model.avatar, + size: 44, + ), + const SizedBox( + width: 8, + ), + Text( + model.userName, + style: const TextStyle( + color: AppColors.primary, + overflow: TextOverflow.ellipsis, + ), + maxLines: 1, + ) + ], + ), + const Divider(height: 16), + const Text( + '个人名片', + style: TextStyle( + fontSize: AppSize.smallFontSize, + color: AppColors.unactive, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/views/conversation/widgets/show_sound_message.dart b/lib/views/conversation/widgets/show_sound_message.dart new file mode 100644 index 0000000..51a90fc --- /dev/null +++ b/lib/views/conversation/widgets/show_sound_message.dart @@ -0,0 +1,140 @@ +import 'dart:io'; +import 'dart:math'; +import 'package:chat/configs/app_colors.dart'; +import 'package:chat/configs/app_size.dart'; +import 'package:chat/utils/sound_record.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:tencent_im_sdk_plugin/models/v2_tim_message.dart'; + +class ShowSoundMessage extends StatefulWidget { + final V2TimMessage message; + + const ShowSoundMessage(this.message, {Key? key}) : super(key: key); + + @override + State createState() => _ShowSoundMessageState(); +} + +class _ShowSoundMessageState extends State { + bool isPlaying = false; + + @override + void initState() { + super.initState(); + SoundPlayer.playStateListener(listener: (_) { + if (_.playState == "complete") { + if (mounted) { + setState(() { + isPlaying = false; + }); + } + } + }); + SoundPlayer.setSoundInterruptListener(() { + if (mounted) { + setState(() { + isPlaying = false; + }); + } + }); + // eventBus.on().listen((event) { + // if (event == 'stop') { + // stopAndDispose(); + // } + // }); + } + + void stopAndDispose() async { + if (isPlaying) { + await SoundPlayer.stop(); + SoundPlayer.dispose(); + } + } + + _playSound() async { + if (!SoundPlayer.isInited) { + bool hasMicrophonePermission = + await Permission.microphone.request().isGranted; + bool hasStoragePermission = + Platform.isIOS || await Permission.storage.request().isGranted; + if (hasMicrophonePermission && hasStoragePermission) { + SoundPlayer.initSoundPlayer(); + } + } + if (isPlaying) { + SoundPlayer.stop(); + setState(() { + isPlaying = false; + }); + } else { + SoundPlayer.play(url: widget.message.soundElem!.url!); + setState(() { + isPlaying = true; + }); + } + } + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: () { + _playSound(); + }, + child: Container( + constraints: BoxConstraints( + maxWidth: Get.width * 0.618, + minWidth: 64, + minHeight: 43, + ), + width: + (widget.message.soundElem!.duration! / 60) * (Get.width * 0.618) + + 64, + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + color: widget.message.isSelf! ? AppColors.primary : AppColors.white, + ), + child: Row( + mainAxisAlignment: widget.message.isSelf! + ? MainAxisAlignment.start + : MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + '${widget.message.soundElem!.duration}"', + style: TextStyle( + fontSize: AppSize.fontSize, + color: widget.message.isSelf! + ? AppColors.white + : AppColors.primary, + ), + ), + const SizedBox(width: 4), + Transform.rotate( + angle: widget.message.isSelf! ? pi : 0, + child: const Icon( + Icons.volume_up, + size: 18, + color: AppColors.white, + ), + ), + // Image.asset( + // widget.message.isSelf! + // ? isPlaying + // ? 'assets/chats/play_voice_send.gif' + // : 'assets/chats/voice_send.png' + // : isPlaying + // ? 'assets/chats/play_voice_receive.gif' + // : 'assets/chats/voice_receive.png', + // height: 16, + // color: + // widget.message.isSelf! ? AppColors.white : AppColors.primary, + // ), + ], + ), + ), + ); + } +} diff --git a/lib/views/conversation/widgets/show_text_message.dart b/lib/views/conversation/widgets/show_text_message.dart new file mode 100644 index 0000000..a06b0b4 --- /dev/null +++ b/lib/views/conversation/widgets/show_text_message.dart @@ -0,0 +1,32 @@ +import 'package:chat/configs/app_colors.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:tencent_im_sdk_plugin/models/v2_tim_message.dart'; + +class ShowTextMessage extends StatelessWidget { + final V2TimMessage message; + const ShowTextMessage(this.message, {Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + constraints: BoxConstraints( + maxWidth: Get.width - 128, + minHeight: 43, + ), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + color: message.isSelf! ? AppColors.primary : AppColors.white, + ), + child: Text( + message.textElem!.text!, + overflow: TextOverflow.visible, + style: TextStyle( + fontSize: 14, + color: message.isSelf! ? AppColors.white : AppColors.active, + ), + ), + ); + } +} diff --git a/lib/views/conversation/widgets/show_transfer_message.dart b/lib/views/conversation/widgets/show_transfer_message.dart new file mode 100644 index 0000000..bdabcf7 --- /dev/null +++ b/lib/views/conversation/widgets/show_transfer_message.dart @@ -0,0 +1,113 @@ +import 'dart:convert'; +import 'package:chat/configs/app_colors.dart'; +import 'package:chat/configs/app_size.dart'; +import 'package:chat/models/im/transfer_model.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:tencent_im_sdk_plugin_platform_interface/models/v2_tim_message.dart'; + +class ShowTransferMessage extends StatefulWidget { + final V2TimMessage message; + + const ShowTransferMessage(this.message, {Key? key}) : super(key: key); + + @override + State createState() => _ShowTransferMessageState(); +} + +class _ShowTransferMessageState extends State { + @override + Widget build(BuildContext context) { + var model = TransferModel.fromJson( + json.decode(widget.message.customElem!.data!), + ); + + return InkWell( + onTap: () { + // Get.toNamed( + // ConversationRoutes.conversationTransferDetail, + // arguments: { + // 'order_id': model.orderId, + // }, + // ); + }, + child: Container( + width: Get.width * 0.618, + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + color: widget.message.isSelf! ? AppColors.primary : AppColors.white, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.currency_exchange, + size: 44, + color: widget.message.isSelf! + ? AppColors.white + : AppColors.primary, + ), + const SizedBox( + width: 8, + ), + Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + '¥', + style: TextStyle( + color: widget.message.isSelf! + ? AppColors.white + : AppColors.primary, + ), + ), + Text( + model.amount, + style: TextStyle( + color: widget.message.isSelf! + ? AppColors.white + : AppColors.primary, + fontSize: AppSize.titleFontSize, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + Text( + '发起积分转账', + style: TextStyle( + color: widget.message.isSelf! + ? AppColors.white + : AppColors.primary, + fontSize: AppSize.fontSize, + ), + ), + ], + ) + ], + ), + Divider( + height: 16, + color: widget.message.isSelf! ? AppColors.white : null, + ), + Text( + 'DT积分转账', + style: TextStyle( + color: widget.message.isSelf! + ? AppColors.white + : AppColors.primary, + fontSize: AppSize.smallFontSize, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/views/conversation/widgets/show_video_message.dart b/lib/views/conversation/widgets/show_video_message.dart new file mode 100644 index 0000000..e454e91 --- /dev/null +++ b/lib/views/conversation/widgets/show_video_message.dart @@ -0,0 +1,68 @@ +import 'dart:io'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:chat/configs/app_colors.dart'; +import 'package:chat/views/conversation/preview/video_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:tencent_im_sdk_plugin/models/v2_tim_message.dart'; + +class ShowVideoMessage extends StatelessWidget { + final V2TimMessage message; + const ShowVideoMessage(this.message, {Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: () { + Get.to( + PreviewVideoWidget(message.videoElem!), + transition: Transition.zoom, + ); + }, + child: Container( + constraints: BoxConstraints( + maxWidth: Get.width * 0.382, + maxHeight: 192, + minHeight: 96, + minWidth: 96, + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Stack( + children: [ + _showVideSnapshot(), + const Positioned.fill( + child: Center( + child: Icon( + Icons.play_circle_outline, + size: 64, + color: AppColors.white, + ), + ), + ), + ], + ), + ), + ), + ); + } + + Widget _showVideSnapshot() { + String ss = message.videoElem!.localSnapshotUrl!; + String? su = message.videoElem!.snapshotUrl; + + if (ss.isNotEmpty && File(ss).existsSync()) { + return Image.file( + File(ss), + fit: BoxFit.cover, + ); + } else if (su != null && su.isNotEmpty) { + return CachedNetworkImage( + imageUrl: su, + fit: BoxFit.cover, + ); + } else { + return Container(); + } + } +} diff --git a/lib/views/conversation/widgets/tim_emoji_panel.dart b/lib/views/conversation/widgets/tim_emoji_panel.dart new file mode 100644 index 0000000..923b060 --- /dev/null +++ b/lib/views/conversation/widgets/tim_emoji_panel.dart @@ -0,0 +1,69 @@ +import 'package:chat/configs/app_colors.dart'; +import 'package:chat/configs/emoji.dart'; +import 'package:chat/models/im/emoji_model.dart'; +import 'package:flutter/material.dart'; + +class EmojiPanel extends StatelessWidget { + final void Function(int unicode) onTapEmoji; + + const EmojiPanel({ + Key? key, + required this.onTapEmoji, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + child: Column( + children: [ + Container( + height: 248, + color: AppColors.page, + child: Stack( + children: [ + GridView( + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 8, + childAspectRatio: 1, + ), + children: emojiData.map( + (e) { + var item = EmojiModel.fromJson(e); + return InkWell( + onTap: () { + onTapEmoji(item.unicode); + }, + child: Center( + child: EmojiItem( + name: item.name, + unicode: item.unicode, + ), + ), + ); + }, + ).toList(), + ), + ], + ), + ), + ], + ), + ); + } +} + +class EmojiItem extends StatelessWidget { + const EmojiItem({Key? key, required this.name, required this.unicode}) + : super(key: key); + final String name; + final int unicode; + @override + Widget build(BuildContext context) { + return Text( + String.fromCharCode(unicode), + style: const TextStyle( + fontSize: 26, + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 4e90839..aa0cd23 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -94,6 +94,41 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.0.1" + camera: + dependency: transitive + description: + name: camera + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.9.8+1" + camera_android: + dependency: transitive + description: + name: camera_android + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.9.8+3" + camera_avfoundation: + dependency: transitive + description: + name: camera_avfoundation + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.9.8+6" + camera_platform_interface: + dependency: transitive + description: + name: camera_platform_interface + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.2" + camera_web: + dependency: "direct main" + description: + name: camera_web + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.2.1+6" characters: dependency: transitive description: @@ -136,6 +171,13 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "3.1.0" + cross_file: + dependency: transitive + description: + name: cross_file + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.3.3+2" crypto: dependency: transitive description: @@ -227,6 +269,20 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "6.1.4" + file_picker: + dependency: "direct main" + description: + name: file_picker + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.6.1" + filesize: + dependency: "direct main" + description: + name: filesize + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.1" flutter: dependency: "direct main" description: flutter @@ -267,6 +323,20 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.0.4" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.7" + flutter_plugin_record_plus: + dependency: "direct main" + description: + name: flutter_plugin_record_plus + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.0.11" flutter_spinkit: dependency: "direct main" description: @@ -669,6 +739,13 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "4.0.0" + quiver: + dependency: transitive + description: + name: quiver + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.0" rxdart: dependency: transitive description: @@ -744,6 +821,13 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "2.1.0" + stream_transform: + dependency: transitive + description: + name: stream_transform + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.1" string_scanner: dependency: transitive description: @@ -905,6 +989,13 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "7.2.0" + wechat_camera_picker: + dependency: "direct main" + description: + name: wechat_camera_picker + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.5.0+1" win32: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index afc5b09..657495f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -49,6 +49,11 @@ dependencies: photo_manager: 2.2.1 chewie: ^1.3.5 encrypt: ^5.0.1 + flutter_plugin_record_plus: ^0.0.11 + wechat_camera_picker: ^3.5.0+1 + camera_web: ^0.2.1+6 + filesize: ^2.0.1 + file_picker: ^4.6.1 dev_dependencies: flutter_test: