Compare commits
	
		
			6 Commits
		
	
	
		
			b6eb12cf30
			...
			4b1942b949
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 4b1942b949 | |||
|   | 2f03c02b58 | ||
|   | 639143934f | ||
|   | 81bbbcd8e4 | ||
|   | bedd46756d | ||
|   | bb6b342d82 | 
| @ -183,7 +183,7 @@ class HttpFileTransferService { | ||||
|     } | ||||
| 
 | ||||
|     final file = File(path); | ||||
|     final data = await file.readAsBytes(); | ||||
|     final data = file.openRead(); | ||||
|     final stat = file.statSync(); | ||||
| 
 | ||||
|     // Request the upload slot | ||||
| @ -206,7 +206,6 @@ class HttpFileTransferService { | ||||
|         options: dio.Options( | ||||
|           headers: slot.headers, | ||||
|           contentType: 'application/octet-stream', | ||||
|           requestEncoder: (_, __) => data, | ||||
|         ), | ||||
|         data: data, | ||||
|         onSendProgress: (count, total) { | ||||
| @ -359,31 +358,62 @@ class HttpFileTransferService { | ||||
|       downloadPath = pathlib.join(tempDir.path, filename); | ||||
|     } | ||||
| 
 | ||||
|     dio.Response<dynamic>? response; | ||||
|     // Prepare file and completer. | ||||
|     final file = await File(downloadedPath).create(); | ||||
|     final fileSink = file.openWrite(mode: FileMode.writeOnlyAppend); | ||||
|     final downloadCompleter = Completer(); | ||||
| 
 | ||||
|     dio.Response<dio.ResponseBody>? response; | ||||
| 
 | ||||
|     try { | ||||
|       response = await dio.Dio().downloadUri( | ||||
|         Uri.parse(job.location.url), | ||||
|         downloadPath, | ||||
|         onReceiveProgress: (count, total) { | ||||
|           final progress = count.toDouble() / total.toDouble(); | ||||
|           sendEvent( | ||||
|             ProgressEvent( | ||||
|               id: job.mId, | ||||
|               progress: progress == 1 ? 0.99 : progress, | ||||
|             ), | ||||
|           ); | ||||
|         }, | ||||
|       response = await dio.Dio().get<dio.ResponseBody>( | ||||
|         job.location.url, | ||||
|         options: dio.Options( | ||||
|           responseType: dio.ResponseType.stream, | ||||
|         ), | ||||
|       ); | ||||
|     } on dio.DioError catch(err) { | ||||
|       // TODO(PapaTutuWawa): React if we received an error that is not related to the | ||||
|       //                     connection. | ||||
| 
 | ||||
|       final downloadStream = response.data?.stream; | ||||
| 
 | ||||
|       if (downloadStream != null) { | ||||
|         final totalFileSizeString = response.headers['Content-Length']?.first; | ||||
|         final totalFileSize = int.parse(totalFileSizeString!); | ||||
| 
 | ||||
|         // Since acting on downloadStream events like to fire progress events | ||||
|         // causes memory spikes relative to the file size, I chose to listen to | ||||
|         // the created file instead and wait for its completion. | ||||
| 
 | ||||
|         file.watch().listen((FileSystemEvent event) async { | ||||
|           if (event is FileSystemCreateEvent || | ||||
|               event is FileSystemModifyEvent) { | ||||
|             final fileSize = await File(downloadedPath).length(); | ||||
|             final progress = fileSize / totalFileSize; | ||||
|             sendEvent( | ||||
|               ProgressEvent( | ||||
|                 id: job.mId, | ||||
|                 progress: progress == 1 ? 0.99 : progress, | ||||
|               ), | ||||
|             ); | ||||
|             if (progress >= 1 && !downloadCompleter.isCompleted) { | ||||
|               downloadCompleter.complete(); | ||||
|             } | ||||
|           } | ||||
|         }); | ||||
|         downloadStream.listen(fileSink.add); | ||||
| 
 | ||||
|         await downloadCompleter.future; | ||||
|         await fileSink.flush(); | ||||
|         await fileSink.close(); | ||||
|       } | ||||
|     } on dio.DioError catch (err) { | ||||
|       _log.finest('Failed to download: $err'); | ||||
|       await _fileDownloadFailed(job, fileDownloadFailedError); | ||||
|       return; | ||||
|       if (response.runtimeType != dio.Response<dio.ResponseBody>) { | ||||
|         response = null; | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     if (!isRequestOkay(response.statusCode)) { | ||||
|       _log.warning('HTTP GET of ${job.location.url} returned ${response.statusCode}'); | ||||
|     if (!isRequestOkay(response?.statusCode)) { | ||||
|       _log.warning('HTTP GET of ${job.location.url} returned ${response?.statusCode}'); | ||||
|       await _fileDownloadFailed(job, fileDownloadFailedError); | ||||
|       return; | ||||
|     } else { | ||||
|  | ||||
| @ -18,6 +18,7 @@ import 'package:moxxyv2/ui/pages/conversation/bottom.dart'; | ||||
| import 'package:moxxyv2/ui/pages/conversation/helpers.dart'; | ||||
| import 'package:moxxyv2/ui/pages/conversation/topbar.dart'; | ||||
| import 'package:moxxyv2/ui/widgets/chat/chatbubble.dart'; | ||||
| import 'package:moxxyv2/ui/widgets/chat/datebubble.dart'; | ||||
| import 'package:moxxyv2/ui/widgets/overview_menu.dart'; | ||||
| 
 | ||||
| class ConversationPage extends StatefulWidget { | ||||
| @ -104,11 +105,42 @@ class ConversationPageState extends State<ConversationPage> with TickerProviderS | ||||
|       Navigator.of(context).pop(); | ||||
|     } | ||||
|   } | ||||
|    | ||||
| 
 | ||||
|   Widget _renderBubble(ConversationState state, BuildContext context, int _index, double maxWidth, String jid) { | ||||
|     if (_index.isEven) { | ||||
|       // Check if we have to render a date bubble | ||||
|       final nextMessageDateTime = DateTime.fromMillisecondsSinceEpoch( | ||||
|         state.messages[state.messages.length - 1 - _index ~/ 2].timestamp, | ||||
|       ); | ||||
|       final nextIndex = state.messages.length - 2 - _index ~/ 2; | ||||
|       final lastMessageDateTime = nextIndex > 0 ? | ||||
|         DateTime.fromMillisecondsSinceEpoch(state.messages[nextIndex].timestamp) : | ||||
|         null; | ||||
|          | ||||
|       if (lastMessageDateTime == null) { | ||||
|         return const SizedBox(); | ||||
|       } | ||||
| 
 | ||||
|       if (lastMessageDateTime.day != nextMessageDateTime.day || | ||||
|           lastMessageDateTime.month != nextMessageDateTime.month || | ||||
|           lastMessageDateTime.year != nextMessageDateTime.year) { | ||||
|         return Row( | ||||
|           mainAxisAlignment: MainAxisAlignment.center, | ||||
|           children: [ | ||||
|             DateBubble( | ||||
|               formatDateBubble(nextMessageDateTime, DateTime.now()), | ||||
|             ), | ||||
|           ], | ||||
|         ); | ||||
|       } | ||||
| 
 | ||||
|       return const SizedBox(); | ||||
|     } | ||||
| 
 | ||||
|     // TODO(Unknown): Since we reverse the list: Fix start, end and between | ||||
|     final index = state.messages.length - 1 - _index; | ||||
|     final index = state.messages.length - 1 - (_index - 1) ~/ 2; | ||||
|     final item = state.messages[index]; | ||||
| 
 | ||||
|     final start = index - 1 < 0 ? | ||||
|       true : | ||||
|       isSent(state.messages[index - 1], jid) != isSent(item, jid); | ||||
| @ -116,7 +148,6 @@ class ConversationPageState extends State<ConversationPage> with TickerProviderS | ||||
|       true : | ||||
|       isSent(state.messages[index + 1], jid) != isSent(item, jid); | ||||
|     final between = !start && !end; | ||||
|     final lastMessageTimestamp = index > 0 ? state.messages[index - 1].timestamp : null; | ||||
|     final sentBySelf = isSent(item, jid); | ||||
|      | ||||
|     final bubble = RawChatBubble( | ||||
| @ -134,7 +165,6 @@ class ConversationPageState extends State<ConversationPage> with TickerProviderS | ||||
|       message: item, | ||||
|       sentBySelf: sentBySelf, | ||||
|       maxWidth: maxWidth, | ||||
|       lastMessageTimestamp: lastMessageTimestamp, | ||||
|       onSwipedCallback: (_) => _quoteMessage(context, item), | ||||
|       onReactionTap: (reaction) { | ||||
|         final bloc = context.read<ConversationBloc>(); | ||||
| @ -390,7 +420,6 @@ class ConversationPageState extends State<ConversationPage> with TickerProviderS | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final maxWidth = MediaQuery.of(context).size.width * 0.6; | ||||
|      | ||||
|     return WillPopScope( | ||||
|       onWillPop: () async { | ||||
|         // TODO(PapaTutuWawa): Check if we are recording an audio message and handle | ||||
| @ -478,8 +507,7 @@ class ConversationPageState extends State<ConversationPage> with TickerProviderS | ||||
|                     buildWhen: (prev, next) => prev.messages != next.messages || prev.conversation!.encrypted != next.conversation!.encrypted, | ||||
|                     builder: (context, state) => Expanded( | ||||
|                       child: ListView.builder( | ||||
|                         reverse: true, | ||||
|                         itemCount: state.messages.length, | ||||
|                         itemCount: state.messages.length * 2, | ||||
|                         itemBuilder: (context, index) => _renderBubble( | ||||
|                           state, | ||||
|                           context, | ||||
| @ -488,6 +516,7 @@ class ConversationPageState extends State<ConversationPage> with TickerProviderS | ||||
|                           state.jid, | ||||
|                         ), | ||||
|                         shrinkWrap: true, | ||||
|                         reverse: true, | ||||
|                         controller: _scrollController, | ||||
|                       ), | ||||
|                     ), | ||||
|  | ||||
| @ -2,11 +2,9 @@ | ||||
| // TODO(Unknown): The timestamp is too small | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_vibrate/flutter_vibrate.dart'; | ||||
| import 'package:moxxyv2/shared/helpers.dart'; | ||||
| import 'package:moxxyv2/shared/models/message.dart'; | ||||
| import 'package:moxxyv2/shared/models/reaction.dart'; | ||||
| import 'package:moxxyv2/ui/constants.dart'; | ||||
| import 'package:moxxyv2/ui/widgets/chat/datebubble.dart'; | ||||
| import 'package:moxxyv2/ui/widgets/chat/media/media.dart'; | ||||
| import 'package:moxxyv2/ui/widgets/chat/reactionbubble.dart'; | ||||
| import 'package:swipeable_tile/swipeable_tile.dart'; | ||||
| @ -112,7 +110,6 @@ class ChatBubble extends StatefulWidget { | ||||
|     required this.message, | ||||
|     required this.sentBySelf, | ||||
|     required this.maxWidth, | ||||
|     required this.lastMessageTimestamp, | ||||
|     required this.onSwipedCallback, | ||||
|     required this.bubble, | ||||
|     this.onLongPressed, | ||||
| @ -123,8 +120,6 @@ class ChatBubble extends StatefulWidget { | ||||
|   final bool sentBySelf; | ||||
|   // For rendering the corners | ||||
|   final double maxWidth; | ||||
|   // For rendering the date bubble | ||||
|   final int? lastMessageTimestamp; | ||||
|   // For acting on swiping | ||||
|   final void Function(Message) onSwipedCallback; | ||||
|   // For acting on long-pressing the message | ||||
| @ -177,7 +172,10 @@ class ChatBubbleState extends State<ChatBubble> | ||||
|     ); | ||||
|   } | ||||
|    | ||||
|   Widget _buildBubble(BuildContext context) { | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     super.build(context); | ||||
| 
 | ||||
|     return SwipeableTile.swipeToTrigger( | ||||
|       direction: _getSwipeDirection(), | ||||
|       swipeThreshold: 0.2, | ||||
| @ -263,41 +261,4 @@ class ChatBubbleState extends State<ChatBubble> | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   Widget _buildWithDateBubble(Widget widget, String dateString) { | ||||
|     return IntrinsicHeight( | ||||
|       child: Column( | ||||
|         children: [ | ||||
|           DateBubble(dateString), | ||||
|           widget, | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|    | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     super.build(context); | ||||
|     // lastMessageTimestamp == null means that there is no previous message | ||||
|     final thisMessageDateTime = DateTime.fromMillisecondsSinceEpoch(widget.message.timestamp); | ||||
|     if (widget.lastMessageTimestamp == null) { | ||||
|       return _buildWithDateBubble( | ||||
|         _buildBubble(context), | ||||
|         formatDateBubble(thisMessageDateTime, DateTime.now()), | ||||
|       ); | ||||
|     } | ||||
| 
 | ||||
|     final lastMessageDateTime = DateTime.fromMillisecondsSinceEpoch(widget.lastMessageTimestamp!); | ||||
| 
 | ||||
|     if (lastMessageDateTime.day != thisMessageDateTime.day || | ||||
|         lastMessageDateTime.month != thisMessageDateTime.month || | ||||
|         lastMessageDateTime.year != thisMessageDateTime.year) { | ||||
|       return _buildWithDateBubble( | ||||
|         _buildBubble(context), | ||||
|         formatDateBubble(thisMessageDateTime, DateTime.now()), | ||||
|       ); | ||||
|     } | ||||
| 
 | ||||
|     return _buildBubble(context); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -1,5 +1,4 @@ | ||||
| import 'dart:core'; | ||||
| import 'package:auto_size_text/auto_size_text.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:moxplatform/moxplatform.dart'; | ||||
| import 'package:moxxyv2/shared/commands.dart'; | ||||
| @ -43,7 +42,6 @@ class FileChatBaseWidget extends StatelessWidget { | ||||
|         Padding( | ||||
|           padding: const EdgeInsets.all(16), | ||||
|           child: Row( | ||||
|             mainAxisSize: MainAxisSize.min, | ||||
|             children: [ | ||||
|               Icon( | ||||
|                 icon, | ||||
| @ -52,7 +50,7 @@ class FileChatBaseWidget extends StatelessWidget { | ||||
| 
 | ||||
|               Padding( | ||||
|                 padding: const EdgeInsets.only(left: 6), | ||||
|                 child: AutoSizeText( | ||||
|                 child: Text( | ||||
|                   filename, | ||||
|                   maxLines: 1, | ||||
|                   overflow: TextOverflow.ellipsis, | ||||
| @ -94,7 +92,9 @@ class FileChatWidget extends StatelessWidget { | ||||
|     return FileChatBaseWidget( | ||||
|       message, | ||||
|       Icons.file_present, | ||||
|       message.isFileUploadNotification ? (message.filename ?? '') : filenameFromUrl(message.srcUrl!), | ||||
|       message.isFileUploadNotification ? | ||||
|         (message.filename ?? '') : | ||||
|         filenameFromUrl(message.srcUrl!), | ||||
|       radius, | ||||
|       maxWidth, | ||||
|       sent, | ||||
| @ -113,7 +113,9 @@ class FileChatWidget extends StatelessWidget { | ||||
|     return FileChatBaseWidget( | ||||
|       message, | ||||
|       Icons.file_present, | ||||
|       message.isFileUploadNotification ? (message.filename ?? '') : filenameFromUrl(message.srcUrl!), | ||||
|       message.isFileUploadNotification ? | ||||
|         (message.filename ?? '') : | ||||
|         filenameFromUrl(message.srcUrl ?? ''), | ||||
|       radius, | ||||
|       maxWidth, | ||||
|       sent, | ||||
|  | ||||
| @ -85,6 +85,10 @@ Widget buildMessageWidget(Message message, double maxWidth, BorderRadius radius, | ||||
|       return AudioChatWidget(message, radius, maxWidth, sent); | ||||
|     case MessageType.file: { | ||||
|       return FileChatWidget(message, radius, maxWidth, sent); | ||||
|       /*return TextChatWidget( | ||||
|         message, | ||||
|         sent, | ||||
|       );*/ | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user