Compare commits

...

6 Commits

5 changed files with 103 additions and 77 deletions

View File

@ -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 {

View File

@ -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,
),
),

View File

@ -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);
}
}

View File

@ -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,

View File

@ -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,
);*/
}
}
}