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