From 830a16bd2755e3f06fb3279dd2662d59b4b5e251 Mon Sep 17 00:00:00 2001 From: htylight Date: Sun, 1 Oct 2023 18:40:14 +0800 Subject: [PATCH] implement slice transmitting of chat image --- assets/images/loading.gif | Bin 0 -> 32763 bytes lib/database/box_type.dart | 32 +- lib/database/box_type.g.dart | 48 +- lib/database/hive_database.dart | 5 + lib/models/websocket_model.dart | 298 +++--------- lib/request/message.dart | 11 + lib/router/chat_router.dart | 7 +- lib/screens/chat/chat_screen.dart | 1 + .../components/row_floating_buttons.dart | 4 +- .../components/attachment_container.dart | 119 +++++ .../components/friend_message_bubble.dart | 124 +---- .../message/components/message_input_box.dart | 176 +++---- .../message/friend_message_screen.dart | 19 +- .../image_view_screen/image_view_screen.dart | 14 +- lib/use_animation.dart | 93 ++++ lib/utils/format_datetime.dart | 4 +- lib/utils/ws_receive_callback.dart | 449 ++++++++++++++++++ pubspec.lock | 20 +- pubspec.yaml | 1 + 19 files changed, 904 insertions(+), 521 deletions(-) create mode 100644 assets/images/loading.gif create mode 100644 lib/screens/message/components/attachment_container.dart create mode 100644 lib/use_animation.dart create mode 100644 lib/utils/ws_receive_callback.dart diff --git a/assets/images/loading.gif b/assets/images/loading.gif new file mode 100644 index 0000000000000000000000000000000000000000..8052b1b658c478b294bba4cd68dc0d4c6eb8018b GIT binary patch literal 32763 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 PPk{6UNKfFOJ%RrMLTy@K literal 0 HcmV?d00001 diff --git a/lib/database/box_type.dart b/lib/database/box_type.dart index f356e3a..89b729a 100644 --- a/lib/database/box_type.dart +++ b/lib/database/box_type.dart @@ -39,7 +39,7 @@ class ChatSetting { @HiveType(typeId: 1) class MessageT { @HiveField(0) - int msgId; + String msgId; @HiveField(1) String senderId; @@ -70,9 +70,37 @@ class MessageT { ); } +@HiveType(typeId: 2) +class AttachmentProgress { + // denote the number of chunk has sent or recieved. + // if 0, means no chunk has received or sent. + @HiveField(0) + int hasChunkNum; + + @HiveField(1) + int totalChunkNum; + + @HiveField(2) + double progress; + + @HiveField(3) + bool isValid; + + @HiveField(4) + bool isPause; + + AttachmentProgress( + this.hasChunkNum, + this.totalChunkNum, + this.progress, + this.isValid, + this.isPause, + ); +} + /// message type: /// 1. text/multipart: text with/without images or videos /// 2. voice /// 3. video /// 4. voice-call -/// 5. video-call \ No newline at end of file +/// 5. video-call diff --git a/lib/database/box_type.g.dart b/lib/database/box_type.g.dart index b98d13b..7eacd31 100644 --- a/lib/database/box_type.g.dart +++ b/lib/database/box_type.g.dart @@ -69,7 +69,7 @@ class MessageTAdapter extends TypeAdapter { for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), }; return MessageT( - fields[0] as int, + fields[0] as String, fields[1] as String, fields[2] == null ? '' : fields[2] as String, fields[3] as String, @@ -109,3 +109,49 @@ class MessageTAdapter extends TypeAdapter { runtimeType == other.runtimeType && typeId == other.typeId; } + +class AttachmentProgressAdapter extends TypeAdapter { + @override + final int typeId = 2; + + @override + AttachmentProgress read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return AttachmentProgress( + fields[0] as int, + fields[1] as int, + fields[2] as double, + fields[3] as bool, + fields[4] as bool, + ); + } + + @override + void write(BinaryWriter writer, AttachmentProgress obj) { + writer + ..writeByte(5) + ..writeByte(0) + ..write(obj.hasChunkNum) + ..writeByte(1) + ..write(obj.totalChunkNum) + ..writeByte(2) + ..write(obj.progress) + ..writeByte(3) + ..write(obj.isValid) + ..writeByte(4) + ..write(obj.isPause); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is AttachmentProgressAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} diff --git a/lib/database/hive_database.dart b/lib/database/hive_database.dart index b14b19a..1fc2603 100644 --- a/lib/database/hive_database.dart +++ b/lib/database/hive_database.dart @@ -32,14 +32,19 @@ class HiveDatabase { encryptionCipher: HiveAesCipher(encryptionKeyUint8List), compactionStrategy: (entries, deletedEntries) => entries > 200, ); + await Hive.openBox('msg_index_${chatBox.contactId}'); } + await Hive.openBox('attachment_send'); + await Hive.openBox('attachment_receive'); + _isInitialised = true; } static void registerAdapter() { Hive.registerAdapter(ChatSettingAdapter()); Hive.registerAdapter(MessageTAdapter()); + Hive.registerAdapter(AttachmentProgressAdapter()); } static Future> openMessageBox(String contactId) async { diff --git a/lib/models/websocket_model.dart b/lib/models/websocket_model.dart index 39fcb78..8980864 100644 --- a/lib/models/websocket_model.dart +++ b/lib/models/websocket_model.dart @@ -1,20 +1,14 @@ import 'dart:async'; import 'dart:convert'; -import 'dart:io'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:hive_flutter/hive_flutter.dart'; -import 'package:together_mobile/database/hive_database.dart'; import 'package:web_socket_channel/web_socket_channel.dart'; import 'package:web_socket_channel/status.dart' as status; -import 'package:together_mobile/models/route_state_model.dart'; import 'package:together_mobile/database/box_type.dart'; -import 'package:together_mobile/models/apply_list_model.dart'; -import 'package:together_mobile/models/contact_model.dart'; -import 'package:together_mobile/models/init_get_it.dart'; -import 'package:together_mobile/models/user_model.dart'; -import 'package:together_mobile/notification_api.dart'; +import 'package:together_mobile/utils/ws_receive_callback.dart'; enum SocketStatus { connected, @@ -23,6 +17,8 @@ enum SocketStatus { closed, } +// const chunkSize = 1024 * 1024; + class WebSocketManager extends ChangeNotifier { late Uri wsUrl; late WebSocketChannel channel; @@ -35,6 +31,7 @@ class WebSocketManager extends ChangeNotifier { int reconnectTimes = 0; Duration heartBeatTimeout = const Duration(seconds: 4); Duration reconnectTimeout = const Duration(seconds: 3); + Map sendImageTimer = {}; void connect(String userId, bool isReconnect) { id = userId; @@ -66,7 +63,7 @@ class WebSocketManager extends ChangeNotifier { reconnectTimes = 0; } - void onData(jsonData) { + void onData(jsonData) async { // If socket can receive msg, that means connection is estabilished socketStatus = SocketStatus.connected; notifyListeners(); @@ -79,10 +76,12 @@ class WebSocketManager extends ChangeNotifier { heartBeatInspect(); - Map data = json.decode(jsonData); + // Map data = json.decode(jsonData); + Map data = + await compute((message) => json.decode(message), jsonData); switch (data['event']) { case 'friend-chat-msg': - receiveFriendMsg(data, true); + await receiveFriendMsg(data, true); case 'apply-friend': receiveApplyFriend(data); case 'friend-added': @@ -95,6 +94,10 @@ class WebSocketManager extends ChangeNotifier { receiveGroupChatCreation(data); case 'group-chat-msg': receiveGroupChatMsg(data, true); + case 'pull-chat-image': + receivePullChatImage(data); + case 'chat-image-send-ok': + receiveCheckChatImage(data); } } @@ -198,245 +201,50 @@ class WebSocketManager extends ChangeNotifier { } }); } -} -void receiveFriendMsg(Map msg, bool isShowNotification) async { - print('=================收到了好友信息事件=================='); - print(msg); - print('======================================='); - String senderId = msg['senderId'] as String; - Box chatSettingBox = Hive.box('chat_setting'); - Box messageTBox = await HiveDatabase.openMessageBox(senderId); - ChatSetting? chatSetting = chatSettingBox.get(senderId); - DateTime dateTime = DateTime.parse(msg['dateTime'] as String); - - if (chatSetting == null) { - chatSettingBox.put( - senderId, - ChatSetting( - senderId, - 0, - false, - true, - false, - dateTime, - 1, - ), - ); - } else { - chatSetting.isOpen = true; - chatSetting.latestDateTime = dateTime; - chatSetting.unreadCount++; - chatSettingBox.put(senderId, chatSetting); - } - - List attachments = List.from(msg['attachments']); - final DateTime now = DateTime.parse(msg['dateTime'] as String); - - messageTBox.add( - MessageT( - msg['msgId'] as int, - senderId, - msg['text'], - msg['type'], - now, - msg['isShowTime'], - attachments, - ), - ); - - if (!isShowNotification) { - return; - } - - String name = getIt.get().friends[senderId]!.friendRemark.isEmpty - ? getIt.get().friends[senderId]!.nickname - : getIt.get().friends[senderId]!.friendRemark; - - String routeName = getIt.get().currentPathName; - - String avatar = getIt.get().friends[senderId]!.avatar; - - if (!getIt.get().isVisible) { - NotificationAPI.showMessageNotifications( - senderId: senderId, - name: name, - avatar: avatar, - text: msg['text'] as String, - ); - } else if (routeName == 'Message') { - if (!getIt.get().query.containsValue(senderId)) { - NotificationAPI.showMessageNotifications( - senderId: senderId, - name: name, - avatar: avatar, - text: msg['text'] as String, - ); + void addSendImageTimer(String filename, int totalChunkNum) { + if (sendImageTimer.containsKey(filename)) { + sendImageTimer[filename]!.cancel(); + sendImageTimer.remove(filename); } - } else if (routeName != 'Chat') { - NotificationAPI.showMessageNotifications( - senderId: senderId, - name: name, - avatar: avatar, - text: msg['text'] as String, + + sendImageTimer[filename] = Timer( + heartBeatTimeout, + () async { + late Box attachmentLoadingBox; + try { + attachmentLoadingBox = + Hive.box('attachment_receive'); + } catch (_) { + attachmentLoadingBox = + await Hive.openBox('attachment_receive'); + } + + AttachmentProgress? at = attachmentLoadingBox.get(filename); + + if (at == null) { + attachmentLoadingBox.put( + filename, + AttachmentProgress( + 0, + totalChunkNum, + 0, + false, + true, + ), + ); + } else { + at.isPause = true; + attachmentLoadingBox.put(filename, at); + } + }, ); } -} -void receiveApplyFriend(Map msg) { - print('=================收到了申请好友事件=================='); - print(msg); - print('======================================='); - getIt.get().addJson(msg); -} - -void receiveFriendAdded(Map msg) { - print('=================收到了申请好友通过事件=================='); - print(msg); - print('======================================='); - getIt.get().addFriend(msg['friendId'], msg['setting']); - getIt - .get() - .addFriendAccountProfile(msg['friendId'], msg['accountProfile']); -} - -void receiveFriendDeleted(Map msg) { - print('=================收到了解除好友事件=================='); - print(msg); - print('======================================='); - getIt.get().removeFriend(msg['friendId']); - getIt.get().removeFriend(msg['friendId']); -} - -void receiveChatImages(Map msg) async { - print('=================收到了聊天图片事件=================='); - print(msg); - print('======================================='); - String chatImageDir = getIt.get().baseImageDir; - File file = File('$chatImageDir/${msg['filename']}'); - if (await file.exists()) { - return; - } else { - await file.create(recursive: true); - await file.writeAsBytes(List.from(msg['bytes'])); - } -} - -void receiveGroupChatCreation(Map msg) { - print('=================收到了群聊邀请事件=================='); - print(msg); - print('======================================='); - String groupChatId = msg['groupChat']['id']; - getIt.get().addGroupChat(groupChatId); - getIt.get().addGroupChatProfile(groupChatId, msg); -} - -void receiveGroupChatMsg( - Map msg, bool isShowNotification) async { - print('=================收到了群聊信息事件=================='); - print(msg); - print('======================================='); - String senderId = msg['senderId'] as String; - String groupChatId = msg['groupChatId'] as String; - Box chatSettingBox = Hive.box('chat_setting'); - Box messageTBox = await HiveDatabase.openMessageBox(groupChatId); - ChatSetting? chatSetting = chatSettingBox.get(groupChatId); - DateTime dateTime = DateTime.parse(msg['dateTime'] as String); - - getIt.get().addGroupChatMemberProfile( - groupChatId, - msg['senderId'], - { - 'avatar': msg['avatar'], - 'nickname': msg['nickname'], - 'remarkInGroupChat': msg['remarkInGroupChat'], - }, - ); - - if (chatSetting == null) { - chatSettingBox.put( - groupChatId, - ChatSetting( - groupChatId, - 1, - false, - true, - false, - dateTime, - 1, - ), - ); - } else { - chatSetting.isOpen = true; - chatSetting.latestDateTime = dateTime; - chatSetting.unreadCount++; - chatSettingBox.put(groupChatId, chatSetting); - } - - List attachments = List.from(msg['attachments']); - final DateTime now = DateTime.parse(msg['dateTime'] as String); - - messageTBox.add( - MessageT( - msg['msgId'] as int, - senderId, - msg['text'], - msg['type'], - now, - msg['isShowTime'], - attachments, - ), - ); - - if (!isShowNotification) { - return; - } - - String avatar = - getIt.get().groupChats[groupChatId]!.avatar; - late String name; - - if (getIt.get().friends.containsKey(senderId)) { - if (getIt.get().friends[senderId]!.friendRemark.isNotEmpty) { - name = getIt.get().friends[senderId]!.friendRemark; - } else if ((msg['remarkInGroupChat'] as String).isNotEmpty) { - name = msg['remarkInGroupChat']; - } else { - name = msg['nickname']; + void removeSendImageTimer(String filename) { + if (sendImageTimer.containsKey(filename)) { + sendImageTimer[filename]!.cancel(); } - } else { - name = (msg['remarkInGroupChat'] as String).isNotEmpty - ? msg['remarkInGroupChat'] - : msg['nickname']; - } - - String routeName = getIt.get().currentPathName; - - if (!getIt.get().isVisible) { - NotificationAPI.showMessageNotifications( - senderId: senderId, - name: name, - avatar: avatar, - text: msg['text'] as String, - groupChatId: groupChatId, - ); - } else if (routeName == 'Message') { - if (!getIt.get().query.containsValue(senderId)) { - NotificationAPI.showMessageNotifications( - senderId: senderId, - name: name, - avatar: avatar, - text: msg['text'] as String, - groupChatId: groupChatId, - ); - } - } else if (routeName != 'Chat') { - NotificationAPI.showMessageNotifications( - senderId: senderId, - name: name, - avatar: avatar, - text: msg['text'] as String, - groupChatId: groupChatId, - ); + sendImageTimer.remove(filename); } } diff --git a/lib/request/message.dart b/lib/request/message.dart index a749df1..d014f0d 100644 --- a/lib/request/message.dart +++ b/lib/request/message.dart @@ -12,3 +12,14 @@ Future> getUnreceivedMsg(String userId) async { return response.data; } + +Future> uploadChatAttachment( + Map data, +) async { + Response response = await request.post( + '/message/attachment', + data: data, + ); + + return response.data; +} diff --git a/lib/router/chat_router.dart b/lib/router/chat_router.dart index d33ee9d..32fe679 100644 --- a/lib/router/chat_router.dart +++ b/lib/router/chat_router.dart @@ -39,12 +39,9 @@ final chatRouter = GoRoute( parentNavigatorKey: rootNavigatorKey, builder: (context, state) { Map extra = state.extra as Map; - for (var a in extra['attachmentItems']! as List) { - print(a.id); - } return ImageViewScreen( - attachmentItems: - extra['attachmentItems']! as List, + attachments: + extra['attachments']! as List, initialIndex: extra['index']! as int, ); }, diff --git a/lib/screens/chat/chat_screen.dart b/lib/screens/chat/chat_screen.dart index 4d7fd0d..9f10dd2 100755 --- a/lib/screens/chat/chat_screen.dart +++ b/lib/screens/chat/chat_screen.dart @@ -10,6 +10,7 @@ import 'package:together_mobile/database/hive_database.dart'; import 'package:together_mobile/request/message.dart'; import 'package:together_mobile/screens/chat/components/group_chat_chat_tile.dart'; +import 'package:together_mobile/utils/ws_receive_callback.dart'; import 'components/friend_chat_tile.dart'; import 'components/add_menu.dart'; import 'package:together_mobile/database/box_type.dart'; diff --git a/lib/screens/friend_profile/components/row_floating_buttons.dart b/lib/screens/friend_profile/components/row_floating_buttons.dart index b59ec76..7c5dca4 100755 --- a/lib/screens/friend_profile/components/row_floating_buttons.dart +++ b/lib/screens/friend_profile/components/row_floating_buttons.dart @@ -1,10 +1,8 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; -import 'package:hive/hive.dart'; import 'package:together_mobile/common/constants.dart'; -import 'package:together_mobile/database/box_type.dart'; import 'package:together_mobile/database/hive_database.dart'; class RowFloatingButtons extends StatelessWidget { @@ -75,7 +73,7 @@ class RowFloatingButtons extends StatelessWidget { ), FloatingActionButton( onPressed: () async { - HiveDatabase.openMessageBox(friendId); + await HiveDatabase.openMessageBox(friendId); // ignore: use_build_context_synchronously context.pushReplacementNamed( 'Message', diff --git a/lib/screens/message/components/attachment_container.dart b/lib/screens/message/components/attachment_container.dart new file mode 100644 index 0000000..4a27462 --- /dev/null +++ b/lib/screens/message/components/attachment_container.dart @@ -0,0 +1,119 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:hive_flutter/hive_flutter.dart'; +import 'package:together_mobile/common/constants.dart'; +import 'package:together_mobile/database/box_type.dart'; +import 'package:together_mobile/models/init_get_it.dart'; +import 'package:together_mobile/models/user_model.dart'; + +class AttachmentContainer extends StatefulWidget { + const AttachmentContainer({ + super.key, + required this.isMyself, + required this.filename, + required this.onTapImage, + }); + + final bool isMyself; + final String filename; + final Function(String) onTapImage; + + @override + State createState() => _AttachmentContainerState(); +} + +class _AttachmentContainerState extends State + with SingleTickerProviderStateMixin, AutomaticKeepAliveClientMixin { + late AnimationController _controller; + late Animation _animation; + + @override + bool get wantKeepAlive => true; + + @override + void initState() { + super.initState(); + _controller = AnimationController(vsync: this); + _animation = Tween(begin: 0.0, end: 1.0).animate(_controller); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + super.build(context); + + return Container( + margin: const EdgeInsets.only( + bottom: 15, + ), + constraints: const BoxConstraints( + maxHeight: 150, + ), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10.0), + ), + child: ValueListenableBuilder( + valueListenable: widget.isMyself + ? Hive.box('attachment_send') + .listenable(keys: [widget.filename]) + : Hive.box('attachment_receive') + .listenable(keys: [widget.filename]), + builder: (context, apBox, _) { + double progress = apBox.get(widget.filename)!.progress; + String filePath = + '${getIt.get().baseImageDir}/${widget.filename}'; + bool isFileExists = File(filePath).existsSync(); + + _controller.animateTo( + progress, + duration: const Duration(milliseconds: 500), + ); + return Stack( + children: [ + GestureDetector( + onTap: () { + widget.onTapImage(widget.filename); + }, + child: progress == 1.0 || isFileExists + ? Image.file(File(filePath)) + : Image.asset('assets/images/loading.gif'), + ), + if (progress < 1) + Positioned.fill( + child: Stack( + alignment: Alignment.center, + children: [ + Container( + alignment: Alignment.center, + constraints: const BoxConstraints.expand(), + color: const Color.fromARGB(255, 32, 32, 32) + .withOpacity(0.3), + child: Text( + '${(_animation.value * 100).toStringAsFixed(0)}%', + style: const TextStyle( + fontSize: 20, + color: kUnAvailableColor, + ), + ), + ), + CircularProgressIndicator( + value: _animation.value, + color: kSecondaryColor, + strokeWidth: 4, + ), + ], + ), + ), + ], + ); + }, + ), + ); + } +} diff --git a/lib/screens/message/components/friend_message_bubble.dart b/lib/screens/message/components/friend_message_bubble.dart index 65834a5..3db6fab 100644 --- a/lib/screens/message/components/friend_message_bubble.dart +++ b/lib/screens/message/components/friend_message_bubble.dart @@ -1,6 +1,3 @@ -import 'dart:async'; -import 'dart:io'; - import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; @@ -12,7 +9,7 @@ import 'package:together_mobile/models/contact_model.dart'; import 'package:together_mobile/models/init_get_it.dart'; import 'package:together_mobile/models/user_model.dart'; import 'package:together_mobile/request/server.dart'; -import 'package:together_mobile/screens/message/image_view_screen/image_view_screen.dart'; +import 'package:together_mobile/screens/message/components/attachment_container.dart'; class FriendMessageBubble extends StatefulWidget { const FriendMessageBubble({ @@ -26,14 +23,13 @@ class FriendMessageBubble extends StatefulWidget { required this.isShowTime, required this.text, required this.attachments, - required this.attachmentItems, + required this.allAttachments, }); final int index, length; final String contactId, senderId, type, dateTime, text; final bool isShowTime; - final List attachments; - final List attachmentItems; + final List attachments, allAttachments; @override State createState() => _FriendMessageBubbleState(); @@ -41,65 +37,14 @@ class FriendMessageBubble extends StatefulWidget { class _FriendMessageBubbleState extends State { // add late here so you can access widget instance - List _isImagesLoaded = []; - final List _timerList = []; late bool _isHideMsg; @override void initState() { super.initState(); - _isImagesLoaded = List.filled(widget.attachments.length, false); - - for (var i = 0; i < widget.attachments.length; i++) { - String imagePath = - '${getIt.get().baseImageDir}/${widget.attachments[i]}'; - File file = File(imagePath); - if (file.existsSync()) { - _isImagesLoaded[i] = true; - } else { - _isImagesLoaded[i] = false; - _timerList.add( - Timer.periodic( - const Duration(milliseconds: 200), - (timer) { - if ((file.existsSync())) { - setState(() { - _isImagesLoaded[i] = true; - }); - timer.cancel(); - } - }, - ), - ); - } - } - final chatSettingBox = Hive.box('chat_setting'); late final chatSetting = chatSettingBox.get(widget.contactId); _isHideMsg = chatSetting!.isHideMsg; - - if (_isHideMsg) { - if (widget.index + 1 == widget.length) { - _isHideMsg = false; - Timer( - const Duration(milliseconds: 3500), - () { - setState(() { - _isHideMsg = true; - }); - }, - ); - } - } - } - - @override - void dispose() { - for (var element in _timerList) { - element.cancel(); - } - - super.dispose(); } @override @@ -170,7 +115,6 @@ class _FriendMessageBubbleState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ if (_isHideMsg) const Text('消息已隐藏'), - // text message content if (widget.text.isNotEmpty && !_isHideMsg) Text( @@ -180,58 +124,16 @@ class _FriendMessageBubbleState extends State { const SizedBox( height: 10, ), - // image content if have + // image content if has if (widget.attachments.isNotEmpty && !_isHideMsg) ...List.generate( widget.attachments.length, (int index) { - int attachmentItemIndex = - widget.attachmentItems.indexWhere( - (element) => - element.resource == - widget.attachments[index], - ); - int heroTagId = widget - .attachmentItems[attachmentItemIndex].id; - return Container( - margin: const EdgeInsets.only( - bottom: 15, - ), - constraints: const BoxConstraints( - maxHeight: 120, - ), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10.0), - ), - child: _isImagesLoaded[index] - ? GestureDetector( - onTap: () { - _onTapImage( - context, - widget.attachments[index], - ); - }, - child: Hero( - tag: heroTagId, - child: Image.file( - File( - '${getIt.get().baseImageDir}/${widget.attachments[index]}', - ), - ), - ), - ) - : Container( - width: 20, - height: 40, - padding: const EdgeInsets.symmetric( - vertical: 10, - ), - child: - const CircularProgressIndicator( - color: kSecondaryColor, - strokeWidth: 3.0, - ), - ), + String filename = widget.attachments[index]; + return AttachmentContainer( + isMyself: !isFriend, + filename: filename, + onTapImage: _onTapImage, ); }, ), @@ -261,15 +163,15 @@ class _FriendMessageBubbleState extends State { ); } - void _onTapImage(BuildContext context, String attachment) { - int attachmentIndex = widget.attachmentItems.indexWhere( - (element) => element.resource == attachment, + void _onTapImage(String attachment) { + int attachmentIndex = widget.allAttachments.indexWhere( + (element) => element == attachment, ); context.pushNamed( 'ImageView', extra: { - 'attachmentItems': widget.attachmentItems, + 'attachments': widget.allAttachments, 'index': attachmentIndex, }, ); diff --git a/lib/screens/message/components/message_input_box.dart b/lib/screens/message/components/message_input_box.dart index 18cd4d8..8bfd9f7 100755 --- a/lib/screens/message/components/message_input_box.dart +++ b/lib/screens/message/components/message_input_box.dart @@ -16,23 +16,9 @@ import 'package:together_mobile/models/user_model.dart'; import 'package:together_mobile/models/websocket_model.dart'; import 'package:together_mobile/utils/app_dir.dart'; import 'package:together_mobile/utils/format_datetime.dart'; +import 'package:together_mobile/utils/ws_receive_callback.dart'; import 'input_icon_button.dart'; -class SendMessage { - final int type; - final List receivers; - final List attachments; - final String dir; - - SendMessage(this.type, this.receivers, this.attachments, this.dir); - - SendMessage.fromJson(Map json) - : type = json['type'], - receivers = json['receivers'], - attachments = json['attachments'], - dir = json['dir']; -} - class MessageInputBox extends StatefulWidget { const MessageInputBox({ super.key, @@ -54,7 +40,7 @@ class _MessageInputBoxState extends State { final ImagePicker _picker = ImagePicker(); bool _hasMsg = false; - late Box _chatBox; + late Box _chatSettingBox; late Box _messageTBox; List _imageFileList = []; @@ -62,7 +48,7 @@ class _MessageInputBoxState extends State { @override void initState() { super.initState(); - _chatBox = Hive.box('chat_setting'); + _chatSettingBox = Hive.box('chat_setting'); _messageTBox = Hive.box('message_${widget.contactId}'); } @@ -231,20 +217,43 @@ class _MessageInputBoxState extends State { String text = _controller.text; String type = 'text/multipart'; List attachments = []; + Box apsBox = Hive.box('attachment_send'); + + var chatSetting = _chatSettingBox.get(widget.contactId); + if (chatSetting == null) { + _chatSettingBox.put( + widget.contactId, + ChatSetting( + widget.contactId, + widget.chatType, + false, + true, + false, + now, + 0, + ), + ); + } else { + chatSetting.latestDateTime = now; + chatSetting.unreadCount = 0; + chatSetting.isOpen = true; + _chatSettingBox.put(widget.contactId, chatSetting); + } + + String dirTime = formatDirTime(now); if (_imageFileList.isNotEmpty) { - String dirTime = formatDirTime(now); for (var i = 0; i < _imageFileList.length; i++) { - attachments.add('$dirTime/${getRandomFilename()}'); + String filename = '$dirTime/${getRandomFilename()}'; + int totalChunkNum = + ((await _imageFileList[i].length()) / chunkSize).ceil(); + attachments.add(filename); + apsBox.put( + filename, AttachmentProgress(0, totalChunkNum, 0, true, false)); } } - late int msgId; - if (_messageTBox.length == 0) { - msgId = 0; - } else { - msgId = _messageTBox.length - 1; - } + final msgId = formatMsgIDFromTime(now); _messageTBox.add( MessageT( @@ -258,6 +267,10 @@ class _MessageInputBoxState extends State { ), ); + Box msgIndexBox = + await Hive.openBox('msg_index_${widget.contactId}'); + msgIndexBox.put(msgId, _messageTBox.length - 1); + Future.delayed( const Duration(milliseconds: 50), () => widget.scrollController.animateTo( @@ -278,32 +291,31 @@ class _MessageInputBoxState extends State { }; if (widget.chatType == 0) { + if (attachments.isNotEmpty) { + String baseImageDir = getIt.get().baseImageDir; + for (var i = 0; i < attachments.length; i++) { + final dir = Directory('$baseImageDir/$dirTime'); + if (!(await dir.exists())) { + await dir.create(recursive: true); + } + await _imageFileList[i].saveTo('$baseImageDir/${attachments[i]}'); + } + } msg['event'] = 'friend-chat-msg'; msg['receiverId'] = widget.contactId; getIt.get().channel.sink.add(json.encode(msg)); - if (attachments.isNotEmpty) { - String baseImageDir = getIt.get().baseImageDir; - List encodedDatas = await compute( - bytes2json, - ( - 0, - [widget.contactId], - attachments, - _imageFileList, - baseImageDir, - ), - ); - - for (final data in encodedDatas) { - getIt.get().channel.sink.add(data); - } - } } else { - String baseImageDir = getIt.get().baseImageDir; List receiverIds = getIt .get() .groupChats[widget.contactId]! .members; + + if (attachments.isNotEmpty) { + String baseImageDir = getIt.get().baseImageDir; + for (var i = 0; i < attachments.length; i++) { + await _imageFileList[i].saveTo('$baseImageDir/${attachments[i]}'); + } + } receiverIds.remove(senderId); msg['event'] = 'group-chat-msg'; msg['groupChatId'] = widget.contactId; @@ -313,22 +325,6 @@ class _MessageInputBoxState extends State { getIt.get().groupChats[widget.contactId]!.remarkInGroupChat; msg['avatar'] = getIt.get().avatar; getIt.get().channel.sink.add(json.encode(msg)); - if (attachments.isNotEmpty) { - List encodedDatas = await compute( - bytes2json, - ( - 1, - receiverIds, - attachments, - _imageFileList, - baseImageDir, - ), - ); - - for (final data in encodedDatas) { - getIt.get().channel.sink.add(data); - } - } } _controller.text = ''; @@ -336,27 +332,6 @@ class _MessageInputBoxState extends State { _imageFileList = []; _hasMsg = false; }); - - var chatSetting = _chatBox.get(widget.contactId); - if (chatSetting == null) { - _chatBox.put( - widget.contactId, - ChatSetting( - widget.contactId, - widget.chatType, - false, - true, - false, - now, - 0, - ), - ); - } else { - chatSetting.latestDateTime = now; - chatSetting.unreadCount = 0; - chatSetting.isOpen = true; - _chatBox.put(widget.contactId, chatSetting); - } } bool _isShowTime(DateTime now) { @@ -372,44 +347,3 @@ class _MessageInputBoxState extends State { } } } - -Future> bytes2json( - ( - int, - List, - List, - List, - String, - ) args) async { - List encodedJson = []; - for (var i = 0; i < args.$3.length; i++) { - Uint8List bytes = await args.$4[i].readAsBytes(); - File file = File('${args.$5}/${args.$3[i]}'); - await file.create(recursive: true); - await file.writeAsBytes(bytes); - if (args.$1 == 0) { - encodedJson.add( - json.encode( - { - 'event': 'chat-image', - 'receiverId': args.$2[0], - 'filename': args.$3[i], - 'bytes': bytes, - }, - ), - ); - } else { - encodedJson.add( - json.encode( - { - 'event': 'chat-image', - 'receiverIds': args.$2, - 'filename': args.$3[i], - 'bytes': bytes, - }, - ), - ); - } - } - return encodedJson; -} diff --git a/lib/screens/message/friend_message_screen.dart b/lib/screens/message/friend_message_screen.dart index 98aae1c..eaec4cc 100755 --- a/lib/screens/message/friend_message_screen.dart +++ b/lib/screens/message/friend_message_screen.dart @@ -12,7 +12,6 @@ import 'package:together_mobile/models/route_state_model.dart'; import 'package:together_mobile/utils/format_datetime.dart'; import 'components/friend_message_bubble.dart'; import 'components/message_input_box.dart'; -import 'image_view_screen/image_view_screen.dart'; class FriendMessageScreen extends StatefulWidget { const FriendMessageScreen({ @@ -110,6 +109,7 @@ class _FriendMessageScreenState extends State { physics: const BouncingScrollPhysics( parent: AlwaysScrollableScrollPhysics(), ), + // addAutomaticKeepAlives: false, controller: _controller, shrinkWrap: true, reverse: true, @@ -123,24 +123,13 @@ class _FriendMessageScreenState extends State { MessageT messageT = messageTBox.getAt(i)!; List allAttachments = []; + // Do not reverse the list, cause what i want is + // rigth slide to next image, left to last otherwise for (var element in messageTBox.values) { if (element.attachments.isNotEmpty) { allAttachments.addAll(element.attachments); } } - // Do not reverse the list, cause what i want is - // rigth slide to next image, left to last otherwise - // allAttachments = List.from(allAttachments.reversed); - - List attachmentItems = List.generate( - allAttachments.length, - (int index) { - return AttachmentItem( - id: index, - resource: allAttachments[index], - ); - }, - ); return FriendMessageBubble( key: ValueKey(messageT.msgId), @@ -153,7 +142,7 @@ class _FriendMessageScreenState extends State { type: messageT.type, text: messageT.text, attachments: messageT.attachments, - attachmentItems: attachmentItems, + allAttachments: allAttachments, ); }, ); diff --git a/lib/screens/message/image_view_screen/image_view_screen.dart b/lib/screens/message/image_view_screen/image_view_screen.dart index e700a68..ad8ba67 100644 --- a/lib/screens/message/image_view_screen/image_view_screen.dart +++ b/lib/screens/message/image_view_screen/image_view_screen.dart @@ -27,7 +27,7 @@ class ImageViewScreen extends StatefulWidget { this.minScale, this.maxScale, this.initialIndex = 0, - required this.attachmentItems, + required this.attachments, this.scrollDirection = Axis.horizontal, }) : pageController = PageController(initialPage: initialIndex); @@ -36,7 +36,7 @@ class ImageViewScreen extends StatefulWidget { final dynamic maxScale; final int initialIndex; final PageController pageController; - final List attachmentItems; + final List attachments; final Axis scrollDirection; @override @@ -70,7 +70,7 @@ class _ImageViewScreenState extends State { PhotoViewGallery.builder( scrollPhysics: const BouncingScrollPhysics(), builder: _buildItem, - itemCount: widget.attachmentItems.length, + itemCount: widget.attachments.length, loadingBuilder: widget.loadingBuilder, backgroundDecoration: const BoxDecoration(color: Colors.black), pageController: widget.pageController, @@ -96,15 +96,15 @@ class _ImageViewScreenState extends State { } PhotoViewGalleryPageOptions _buildItem(BuildContext context, int index) { - final AttachmentItem item = widget.attachmentItems[index]; + final String item = widget.attachments[index]; return PhotoViewGalleryPageOptions( imageProvider: FileImage( - File('${getIt.get().baseImageDir}/${item.resource}'), + File('${getIt.get().baseImageDir}/$item'), ), initialScale: PhotoViewComputedScale.contained, - minScale: PhotoViewComputedScale.contained * (0.5 + index / 10), + minScale: PhotoViewComputedScale.contained * 0.5, maxScale: PhotoViewComputedScale.covered * 4.1, - heroAttributes: PhotoViewHeroAttributes(tag: item.id), + // heroAttributes: PhotoViewHeroAttributes(tag: widget.heroTag), ); } } diff --git a/lib/use_animation.dart b/lib/use_animation.dart new file mode 100644 index 0000000..c70fbbf --- /dev/null +++ b/lib/use_animation.dart @@ -0,0 +1,93 @@ +import 'package:flutter/material.dart'; + +void main() { + runApp(const UseAnimation()); +} + +class UseAnimation extends StatefulWidget { + const UseAnimation({super.key}); + + @override + State createState() => _UseAnimationState(); +} + +class _UseAnimationState extends State + with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _tween; + + @override + void initState() { + _controller = AnimationController(vsync: this); + _tween = Tween(begin: 0, end: 1).animate(_controller) + ..addStatusListener((status) { + // print(status); + }) + ..addListener(() { + setState(() {}); + }); + // _controller.forward(); + super.initState(); + } + + @override + void dispose() { + super.dispose(); + _controller.dispose(); + } + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Use Animation', + home: Scaffold( + appBar: AppBar(title: const Text('use animation')), + body: Center( + child: SizedBox( + height: 300, + width: 200, + child: Stack( + alignment: Alignment.center, + children: [ + Image.asset('assets/images/user_2.png'), + Positioned.fill( + child: Container( + color: _tween.value == 1.0 + ? null + : const Color.fromARGB(255, 36, 36, 36) + .withOpacity(0.3), + child: Center( + child: Text( + (_tween.value * 100).toStringAsFixed(1), + style: const TextStyle( + color: Colors.green, + fontSize: 24, + ), + ), + ), + ), + ), + CircularProgressIndicator( + value: _tween.value, + ), + ], + ), + ), + ), + floatingActionButton: FloatingActionButton( + onPressed: () { + if (_tween.value == 1.0) { + _controller.reset(); + } + print(_tween.value); + // print(_controller.value); + _controller.animateTo(_tween.value + 0.1, + duration: const Duration(milliseconds: 200)); + print(_tween.value); + }, + child: const Icon(Icons.plus_one), + ), + ), + ); + } +} diff --git a/lib/utils/format_datetime.dart b/lib/utils/format_datetime.dart index 46844cc..5853801 100644 --- a/lib/utils/format_datetime.dart +++ b/lib/utils/format_datetime.dart @@ -94,6 +94,8 @@ String formatMsgIDFromTime(DateTime dateTime) { dateTime.month < 10 ? '0${dateTime.month}' : '${dateTime.month}'; String day = dateTime.day < 10 ? '0${dateTime.day}' : '${dateTime.day}'; String hour = dateTime.hour < 10 ? '0${dateTime.hour}' : '${dateTime.hour}'; + String second = + dateTime.second < 10 ? '0${dateTime.second}' : '${dateTime.second}'; String minute = dateTime.minute < 10 ? '0${dateTime.minute}' : '${dateTime.minute}'; String millisecond = dateTime.millisecond >= 100 @@ -102,5 +104,5 @@ String formatMsgIDFromTime(DateTime dateTime) { ? '0${dateTime.minute}' : '00${dateTime.minute}'; - return '$year$month$day$hour$minute$millisecond'; + return '$year$month$day$hour$minute$second$millisecond'; } diff --git a/lib/utils/ws_receive_callback.dart b/lib/utils/ws_receive_callback.dart new file mode 100644 index 0000000..d6a4b20 --- /dev/null +++ b/lib/utils/ws_receive_callback.dart @@ -0,0 +1,449 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:hive/hive.dart'; +import 'package:together_mobile/database/box_type.dart'; +import 'package:together_mobile/database/hive_database.dart'; +import 'package:together_mobile/models/apply_list_model.dart'; +import 'package:together_mobile/models/contact_model.dart'; +import 'package:together_mobile/models/init_get_it.dart'; +import 'package:together_mobile/models/route_state_model.dart'; +import 'package:together_mobile/models/user_model.dart'; +import 'package:together_mobile/models/websocket_model.dart'; +import 'package:together_mobile/notification_api.dart'; +import 'package:together_mobile/request/message.dart'; + +const int chunkSize = 1024 * 1024 * 2; + +Future receiveFriendMsg( + Map msg, + bool isShowNotification, +) async { + print('=================收到了好友信息事件=================='); + print(msg); + print('======================================='); + String senderId = msg['senderId'] as String; + Box chatSettingBox = Hive.box('chat_setting'); + Box messageTBox = await HiveDatabase.openMessageBox(senderId); + Box msgIndexBox = await Hive.openBox('msg_index_$senderId'); + ChatSetting? chatSetting = chatSettingBox.get(senderId); + DateTime dateTime = DateTime.parse(msg['dateTime'] as String); + + if (chatSetting == null) { + chatSettingBox.put( + senderId, + ChatSetting( + senderId, + 0, + false, + true, + false, + dateTime, + 1, + ), + ); + } else if (msgIndexBox.get(msg['msgId'] as String) != null) { + return; + } else { + chatSetting.isOpen = true; + chatSetting.latestDateTime = dateTime; + chatSetting.unreadCount++; + chatSettingBox.put(senderId, chatSetting); + } + + List attachments = List.from(msg['attachments']); + final DateTime now = DateTime.parse(msg['dateTime'] as String); + + for (var attachment in attachments) { + Box apr = + Hive.box('attachment_receive'); + apr.put(attachment, AttachmentProgress(0, 1000, 0, true, false)); + } + + messageTBox.add( + MessageT( + msg['msgId'] as String, + senderId, + msg['text'], + msg['type'], + now, + msg['isShowTime'], + attachments, + ), + ); + + if (!isShowNotification) { + return; + } + + String name = getIt.get().friends[senderId]!.friendRemark.isEmpty + ? getIt.get().friends[senderId]!.nickname + : getIt.get().friends[senderId]!.friendRemark; + + String routeName = getIt.get().currentPathName; + + String avatar = getIt.get().friends[senderId]!.avatar; + + if (!getIt.get().isVisible) { + NotificationAPI.showMessageNotifications( + senderId: senderId, + name: name, + avatar: avatar, + text: msg['text'] as String, + ); + } else if (routeName == 'Message') { + if (!getIt.get().query.containsValue(senderId)) { + NotificationAPI.showMessageNotifications( + senderId: senderId, + name: name, + avatar: avatar, + text: msg['text'] as String, + ); + } + } else if (routeName != 'Chat') { + NotificationAPI.showMessageNotifications( + senderId: senderId, + name: name, + avatar: avatar, + text: msg['text'] as String, + ); + } +} + +void receiveApplyFriend(Map msg) { + print('=================收到了申请好友事件=================='); + print(msg); + print('======================================='); + getIt.get().addJson(msg); +} + +void receiveFriendAdded(Map msg) { + print('=================收到了申请好友通过事件=================='); + print(msg); + print('======================================='); + getIt.get().addFriend(msg['friendId'], msg['setting']); + getIt + .get() + .addFriendAccountProfile(msg['friendId'], msg['accountProfile']); +} + +void receiveFriendDeleted(Map msg) { + print('=================收到了解除好友事件=================='); + print(msg); + print('======================================='); + getIt.get().removeFriend(msg['friendId']); + getIt.get().removeFriend(msg['friendId']); +} + +void receiveGroupChatCreation(Map msg) { + print('=================收到了群聊邀请事件=================='); + print(msg); + print('======================================='); + String groupChatId = msg['groupChat']['id']; + getIt.get().addGroupChat(groupChatId); + getIt.get().addGroupChatProfile(groupChatId, msg); +} + +void receiveGroupChatMsg( + Map msg, + bool isShowNotification, +) async { + print('=================收到了群聊信息事件=================='); + print(msg); + print('======================================='); + String senderId = msg['senderId'] as String; + String groupChatId = msg['groupChatId'] as String; + Box chatSettingBox = Hive.box('chat_setting'); + Box messageTBox = await HiveDatabase.openMessageBox(groupChatId); + Box msgIndexBox = await Hive.openBox('msg_index_$groupChatId'); + ChatSetting? chatSetting = chatSettingBox.get(groupChatId); + DateTime dateTime = DateTime.parse(msg['dateTime'] as String); + + getIt.get().addGroupChatMemberProfile( + groupChatId, + msg['senderId'], + { + 'avatar': msg['avatar'], + 'nickname': msg['nickname'], + 'remarkInGroupChat': msg['remarkInGroupChat'], + }, + ); + + if (chatSetting == null) { + chatSettingBox.put( + groupChatId, + ChatSetting( + groupChatId, + 1, + false, + true, + false, + dateTime, + 1, + ), + ); + } else if (msgIndexBox.get(msg['msgId'] as String) != null) { + return; + } else { + chatSetting.isOpen = true; + chatSetting.latestDateTime = dateTime; + chatSetting.unreadCount++; + chatSettingBox.put(groupChatId, chatSetting); + } + + List attachments = List.from(msg['attachments']); + final DateTime now = DateTime.parse(msg['dateTime'] as String); + + messageTBox.add( + MessageT( + msg['msgId'] as String, + senderId, + msg['text'], + msg['type'], + now, + msg['isShowTime'], + attachments, + ), + ); + + if (!isShowNotification) { + return; + } + + String avatar = + getIt.get().groupChats[groupChatId]!.avatar; + late String name; + + if (getIt.get().friends.containsKey(senderId)) { + if (getIt.get().friends[senderId]!.friendRemark.isNotEmpty) { + name = getIt.get().friends[senderId]!.friendRemark; + } else if ((msg['remarkInGroupChat'] as String).isNotEmpty) { + name = msg['remarkInGroupChat']; + } else { + name = msg['nickname']; + } + } else { + name = (msg['remarkInGroupChat'] as String).isNotEmpty + ? msg['remarkInGroupChat'] + : msg['nickname']; + } + + String routeName = getIt.get().currentPathName; + + if (!getIt.get().isVisible) { + NotificationAPI.showMessageNotifications( + senderId: senderId, + name: name, + avatar: avatar, + text: msg['text'] as String, + groupChatId: groupChatId, + ); + } else if (routeName == 'Message') { + if (!getIt.get().query.containsValue(senderId)) { + NotificationAPI.showMessageNotifications( + senderId: senderId, + name: name, + avatar: avatar, + text: msg['text'] as String, + groupChatId: groupChatId, + ); + } + } else if (routeName != 'Chat') { + NotificationAPI.showMessageNotifications( + senderId: senderId, + name: name, + avatar: avatar, + text: msg['text'] as String, + groupChatId: groupChatId, + ); + } +} + +void receiveChatImages(Map msg) async { + print('=================收到了聊天图片事件=================='); + // print(msg); + print('======================================='); + String chatImageDir = getIt.get().baseImageDir; + String filename = msg['filename']; + String filePath = '$chatImageDir/$filename'; + File file = File(filePath); + + if (await file.exists()) { + return; + } + + late Box aprBox; + try { + aprBox = Hive.box('attachment_receive'); + } catch (_) { + aprBox = await Hive.openBox('attachment_receive'); + } + String dirTime = filename.split('/')[0]; + Directory dir = Directory('$chatImageDir/$dirTime'); + + if (!(await dir.exists())) { + await dir.create(recursive: true); + } + + int totalChunkNum = msg['totalChunkNum']; + + if (totalChunkNum == 1) { + await file.writeAsBytes(List.from(msg['bytes'])); + Future.delayed( + const Duration(milliseconds: 200), + () { + aprBox.put( + filename, + AttachmentProgress(1, 1, 1.0, true, false), + ); + }, + ); + return; + } + + File tempFile = File('$chatImageDir/${msg['tempFilename']}'); + AttachmentProgress? ap = aprBox.get(filename); + + if (!(await tempFile.exists())) { + await tempFile.writeAsBytes(List.from(msg['bytes'])); + } + + if (ap == null) { + aprBox.put( + filename, + AttachmentProgress( + 1, + totalChunkNum, + 1 / totalChunkNum, + true, + false, + ), + ); + } else { + ap.hasChunkNum += 1; + ap.progress = ap.hasChunkNum / totalChunkNum; + ap.isValid = true; + + List tempFiles = List.generate( + totalChunkNum, + (index) { + String tempFilePath = + '$chatImageDir/temp/$filename-$totalChunkNum-$index'; + return File(tempFilePath); + }, + ); + + // assemble chunks when all the chunks are arrived + if (tempFiles.every((element) => element.existsSync())) { + final openedFile = file.openWrite(mode: FileMode.append); + + for (var i = 0; i < totalChunkNum; i++) { + Uint8List bytes = await tempFiles[i].readAsBytes(); + openedFile.add(bytes); + await tempFiles[i].delete(); + } + openedFile.close(); + ap.hasChunkNum = totalChunkNum; + ap.progress = 1.0; + ap.isValid = true; + } + + aprBox.put(filename, ap); + } +} + +Future receivePullChatImage(Map msg) async { + print('=================收到了拉取图片请求=================='); + print(msg); + print('======================================='); + String baseImageDir = getIt.get().baseImageDir; + Map sentMsg = { + 'event': 'chat-image', + 'senderId': getIt.get().id, + }; + + if (msg['chatType'] == 0) { + sentMsg['receiverId'] = msg['receiverId']; + } else { + sentMsg['receiverIds'] = msg['receiverIds']; + } + + for (var filename in msg['attachments']) { + File file = File('$baseImageDir/$filename'); + int fileSize = await file.length(); + int totalChunkNum = (fileSize / chunkSize).ceil(); + int start = 0; + int end = fileSize >= chunkSize ? start + chunkSize : start + fileSize; + List bytes = []; + await file.openRead(start, end).forEach((element) => bytes.addAll(element)); + + sentMsg['filename'] = filename; + sentMsg['totalChunkNum'] = totalChunkNum; + sentMsg['tempFilename'] = 'temp/$filename-$totalChunkNum-0'; + sentMsg['currentChunkNum'] = 0; + sentMsg['bytes'] = bytes; + + await uploadChatAttachment(sentMsg); + getIt.get().addSendImageTimer(filename, totalChunkNum); + } +} + +Future receiveCheckChatImage(Map msg) async { + print('=================收到了服务端确认收到图片事件=================='); + // print(msg); + print('======================================='); + int nextChunkNum = msg['currentChunkNum'] + 1; + int totalChunkNum = msg['totalChunkNum']; + String baseImageDir = getIt.get().baseImageDir; + String filename = msg['filename']; + Box asBox = + Hive.box('attachment_send'); + AttachmentProgress aps = asBox.get(filename)!; + + getIt.get().removeSendImageTimer(filename); + aps.hasChunkNum += 1; + aps.progress = aps.hasChunkNum / aps.totalChunkNum; + asBox.put(filename, aps); + // print(aps.hasChunkNum); + // print(aps.totalChunkNum); + // print('已发送了: ${aps.progress}'); + if (nextChunkNum == totalChunkNum) { + return; + } + + // Start to send next chunk + int start = nextChunkNum * chunkSize; + File file = File('$baseImageDir/$filename'); + List bytes = []; + + if (nextChunkNum + 1 == totalChunkNum) { + await file + .openRead(start) + .forEach((element) => bytes.addAll(element as List)); + } else { + await file + .openRead(start, start + chunkSize) + .forEach((element) => bytes.addAll(element as List)); + } + + Map sentMsg = { + 'event': 'chat-image', + 'senderId': getIt.get().id, + }; + + if (msg['chatType'] == 0) { + sentMsg['receiverId'] = msg['receiverId']; + } else { + sentMsg['receiverIds'] = msg['receiverIds']; + } + + sentMsg['filename'] = filename; + sentMsg['totalChunkNum'] = totalChunkNum; + sentMsg['tempFilename'] = 'temp/$filename-$totalChunkNum-$nextChunkNum'; + sentMsg['currentChunkNum'] = nextChunkNum; + sentMsg['bytes'] = bytes; + + uploadChatAttachment(sentMsg); + getIt.get().addSendImageTimer(filename, totalChunkNum); +} diff --git a/pubspec.lock b/pubspec.lock index 6233a3c..176adf0 100755 --- a/pubspec.lock +++ b/pubspec.lock @@ -1157,14 +1157,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.0" - sprintf: - dependency: transitive - description: - name: sprintf - sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" - url: "https://pub.dev" - source: hosted - version: "7.0.0" sqflite: dependency: transitive description: @@ -1265,10 +1257,10 @@ packages: dependency: transitive description: name: uuid - sha256: e03928880bdbcbf496fb415573f5ab7b1ea99b9b04f669c01104d085893c3134 + sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313" url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "3.0.7" vector_math: dependency: transitive description: @@ -1357,6 +1349,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.2" + worker_manager: + dependency: "direct main" + description: + name: worker_manager + sha256: caab0544cb95471e8d1417d21e25838ee4f614f4651a9e8e5a4f26bfeff5f312 + url: "https://pub.dev" + source: hosted + version: "6.3.1" xdg_directories: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index c8ccce7..a870c68 100755 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -61,6 +61,7 @@ dependencies: timezone: ^0.9.2 video_player: ^2.6.1 web_socket_channel: ^2.4.0 + worker_manager: ^6.3.1 dev_dependencies: build_runner: ^2.4.5