Compare commits

...

2 Commits

8 changed files with 403 additions and 323 deletions

View File

@ -0,0 +1,137 @@
import 'dart:async';
import 'dart:io';
import 'package:meta/meta.dart';
import 'package:moxxyv2/service/httpfiletransfer/helpers.dart';
typedef ProgressCallback = void Function(int total, int current);
@immutable
class HttpPeekResult {
const HttpPeekResult(this.contentType, this.contentLength);
final String? contentType;
final int? contentLength;
}
/// Download the file found at [uri] into the file [destination]. [onProgress] is
/// called whenever new data has been downloaded.
///
/// Returns the status code if the server responded. If an error occurs, returns null.
Future<int?> downloadFile(Uri uri, String destination, ProgressCallback onProgress) async {
// TODO(Unknown): How do we close fileSink? Do we have to?
IOSink? fileSink;
final client = HttpClient();
try {
final req = await client.getUrl(uri);
final resp = await req.close();
if (!isRequestOkay(resp.statusCode)) {
client.close(force: true);
return resp.statusCode;
}
// The size of the remote file
final length = resp.contentLength;
fileSink = File(destination).openWrite(mode: FileMode.append);
var bytes = 0;
final downloadCompleter = Completer<void>();
unawaited(
resp.transform(
StreamTransformer<List<int>, List<int>>.fromHandlers(
handleData: (data, sink) {
bytes += data.length;
onProgress(length, bytes);
sink.add(data);
},
handleDone: (sink) {
downloadCompleter.complete();
},
),
).pipe(fileSink),
);
// Wait for the download to complete
await downloadCompleter.future;
client.close(force: true);
//await fileSink.close();
return resp.statusCode;
} catch (ex) {
client.close(force: true);
//await fileSink?.close();
return null;
}
}
/// Upload the file found at [filePath] to [destination]. [headers] are HTTP headers
/// that are added to the PUT request. [onProgress] is called whenever new data has
/// been downloaded.
///
/// Returns the status code if the server responded. If an error occurs, returns null.
Future<int?> uploadFile(Uri destination, Map<String, String> headers, String filePath, ProgressCallback onProgress) async {
final client = HttpClient();
try {
final req = await client.putUrl(destination);
final file = File(filePath);
final length = await file.length();
req.contentLength = length;
// Set all known headers
headers.forEach((headerName, headerValue) {
req.headers.set(headerName, headerValue);
});
var bytes = 0;
final stream = file.openRead().transform(
StreamTransformer<List<int>, List<int>>.fromHandlers(
handleData: (data, sink) {
bytes += data.length;
onProgress(length, bytes);
sink.add(data);
},
handleDone: (sink) {
sink.close();
},
),
);
await req.addStream(stream);
final resp = await req.close();
return resp.statusCode;
} catch (ex) {
client.close(force: true);
return null;
}
}
/// Sends a HEAD request to [uri].
///
/// Returns the content type and content length if the server responded. If an error
/// occurs, returns null.
Future<HttpPeekResult?> peekUrl(Uri uri) async {
final client = HttpClient();
try {
final req = await client.headUrl(uri);
final resp = await req.close();
if (!isRequestOkay(resp.statusCode)) {
client.close(force: true);
return null;
}
client.close(force: true);
final contentType = resp.headers['Content-Type'];
return HttpPeekResult(
contentType != null && contentType.isNotEmpty ?
contentType.first :
null,
resp.contentLength,
);
} catch (ex) {
client.close(force: true);
return null;
}
}

View File

@ -1,6 +1,6 @@
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:external_path/external_path.dart';
import 'package:moxxyv2/service/httpfiletransfer/client.dart';
import 'package:moxxyv2/shared/helpers.dart';
import 'package:path/path.dart' as path;
@ -43,7 +43,6 @@ bool isRequestOkay(int? statusCode) {
}
class FileMetadata {
const FileMetadata({ this.mime, this.size });
final String? mime;
final int? size;
@ -53,15 +52,10 @@ class FileMetadata {
/// does not specify the Content-Length header, null is returned.
/// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Length
Future<FileMetadata> peekFile(String url) async {
final response = await Dio().headUri<dynamic>(Uri.parse(url));
if (!isRequestOkay(response.statusCode)) return const FileMetadata();
final contentLengthHeaders = response.headers['Content-Length'];
final contentTypeHeaders = response.headers['Content-Type'];
final result = await peekUrl(Uri.parse(url));
return FileMetadata(
mime: contentTypeHeaders?.first,
size: contentLengthHeaders != null && contentLengthHeaders.isNotEmpty ? int.parse(contentLengthHeaders.first) : null,
mime: result?.contentType,
size: result?.contentLength,
);
}

View File

@ -4,7 +4,6 @@ import 'dart:convert';
import 'dart:io';
import 'dart:math';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:dio/dio.dart' as dio;
import 'package:get_it/get_it.dart';
import 'package:logging/logging.dart';
import 'package:mime/mime.dart';
@ -15,6 +14,7 @@ import 'package:moxxyv2/service/conversation.dart';
import 'package:moxxyv2/service/cryptography/cryptography.dart';
import 'package:moxxyv2/service/cryptography/types.dart';
import 'package:moxxyv2/service/database/database.dart';
import 'package:moxxyv2/service/httpfiletransfer/client.dart' as client;
import 'package:moxxyv2/service/httpfiletransfer/helpers.dart';
import 'package:moxxyv2/service/httpfiletransfer/jobs.dart';
import 'package:moxxyv2/service/message.dart';
@ -102,19 +102,17 @@ class HttpFileTransferService {
/// Queue the download job [job] to be performed.
Future<void> downloadFile(FileDownloadJob job) async {
var canDownload = false;
await _uploadLock.synchronized(() async {
if (_currentDownloadJob != null) {
_log.finest('Queuing up download task.');
_downloadQueue.add(job);
} else {
_log.finest('Executing download task.');
_currentDownloadJob = job;
canDownload = true;
unawaited(_performFileDownload(job));
}
});
if (canDownload) {
unawaited(_performFileDownload(job));
}
}
Future<void> _copyFile(FileUploadJob job) async {
@ -183,7 +181,6 @@ class HttpFileTransferService {
}
final file = File(path);
final data = file.openRead();
final stat = file.statSync();
// Request the upload slot
@ -200,119 +197,110 @@ class HttpFileTransferService {
return;
}
final slot = slotResult.get<HttpFileUploadSlot>();
try {
final response = await dio.Dio().putUri<dynamic>(
Uri.parse(slot.putUrl),
options: dio.Options(
headers: slot.headers,
contentType: 'application/octet-stream',
),
data: data,
onSendProgress: (count, total) {
// TODO(PapaTutuWawa): Make this smarter by also checking if one of those chats
// is open.
if (job.recipients.length == 1) {
final progress = count.toDouble() / total.toDouble();
sendEvent(
ProgressEvent(
id: job.messageMap.values.first.id,
progress: progress == 1 ? 0.99 : progress,
),
);
}
},
);
final ms = GetIt.I.get<MessageService>();
if (response.statusCode != 201) {
// TODO(PapaTutuWawa): Trigger event
_log.severe('Upload failed');
await _fileUploadFailed(job, fileUploadFailedError);
return;
} else {
_log.fine('Upload was successful');
const uuid = Uuid();
for (final recipient in job.recipients) {
// Notify UI of upload completion
var msg = await ms.updateMessage(
job.messageMap[recipient]!.id,
mediaSize: stat.size,
errorType: noError,
encryptionScheme: encryption != null ?
SFSEncryptionType.aes256GcmNoPadding.toNamespace() :
null,
key: encryption != null ? base64Encode(encryption.key) : null,
iv: encryption != null ? base64Encode(encryption.iv) : null,
isUploading: false,
srcUrl: slot.getUrl,
);
// TODO(Unknown): Maybe batch those two together?
final oldSid = msg.sid;
msg = await ms.updateMessage(
msg.id,
sid: uuid.v4(),
originId: uuid.v4(),
);
sendEvent(MessageUpdatedEvent(message: msg));
StatelessFileSharingSource source;
final plaintextHashes = <String, String>{};
if (encryption != null) {
source = StatelessFileSharingEncryptedSource(
SFSEncryptionType.aes256GcmNoPadding,
encryption.key,
encryption.iv,
encryption.ciphertextHashes,
StatelessFileSharingUrlSource(slot.getUrl),
);
plaintextHashes.addAll(encryption.plaintextHashes);
} else {
source = StatelessFileSharingUrlSource(slot.getUrl);
try {
plaintextHashes[hashSha256] = await GetIt.I.get<CryptographyService>()
.hashFile(job.path, HashFunction.sha256);
} catch (ex) {
_log.warning('Failed to hash file ${job.path} using SHA-256: $ex');
}
}
// Send the message to the recipient
conn.getManagerById<MessageManager>(messageManager)!.sendMessage(
MessageDetails(
to: recipient,
body: slot.getUrl,
requestDeliveryReceipt: true,
id: msg.sid,
originId: msg.originId,
sfs: StatelessFileSharingData(
FileMetadataData(
mediaType: job.mime,
size: stat.size,
name: pathlib.basename(job.path),
thumbnails: job.thumbnails,
hashes: plaintextHashes,
),
<StatelessFileSharingSource>[source],
),
shouldEncrypt: job.encryptMap[recipient]!,
funReplacement: oldSid,
final uploadStatusCode = await client.uploadFile(
Uri.parse(slot.putUrl),
slot.headers,
path,
(total, current) {
// TODO(PapaTutuWawa): Make this smarter by also checking if one of those chats
// is open.
if (job.recipients.length == 1) {
final progress = current.toDouble() / total.toDouble();
sendEvent(
ProgressEvent(
id: job.messageMap.values.first.id,
progress: progress == 1 ? 0.99 : progress,
),
);
_log.finest('Sent message with file upload for ${job.path} to $recipient');
final isMultiMedia = job.mime?.startsWith('image/') == true || job.mime?.startsWith('video/') == true;
if (isMultiMedia) {
_log.finest('File appears to be either an image or a video. Copying it to the correct directory...');
unawaited(_copyFile(job));
}
}
}
} on dio.DioError {
_log.finest('Upload failed due to connection error');
},
);
final ms = GetIt.I.get<MessageService>();
if (!isRequestOkay(uploadStatusCode)) {
_log.severe('Upload failed');
await _fileUploadFailed(job, fileUploadFailedError);
return;
} else {
_log.fine('Upload was successful');
const uuid = Uuid();
for (final recipient in job.recipients) {
// Notify UI of upload completion
var msg = await ms.updateMessage(
job.messageMap[recipient]!.id,
mediaSize: stat.size,
errorType: noError,
encryptionScheme: encryption != null ?
SFSEncryptionType.aes256GcmNoPadding.toNamespace() :
null,
key: encryption != null ? base64Encode(encryption.key) : null,
iv: encryption != null ? base64Encode(encryption.iv) : null,
isUploading: false,
srcUrl: slot.getUrl,
);
// TODO(Unknown): Maybe batch those two together?
final oldSid = msg.sid;
msg = await ms.updateMessage(
msg.id,
sid: uuid.v4(),
originId: uuid.v4(),
);
sendEvent(MessageUpdatedEvent(message: msg));
StatelessFileSharingSource source;
final plaintextHashes = <String, String>{};
if (encryption != null) {
source = StatelessFileSharingEncryptedSource(
SFSEncryptionType.aes256GcmNoPadding,
encryption.key,
encryption.iv,
encryption.ciphertextHashes,
StatelessFileSharingUrlSource(slot.getUrl),
);
plaintextHashes.addAll(encryption.plaintextHashes);
} else {
source = StatelessFileSharingUrlSource(slot.getUrl);
try {
plaintextHashes[hashSha256] = await GetIt.I.get<CryptographyService>()
.hashFile(job.path, HashFunction.sha256);
} catch (ex) {
_log.warning('Failed to hash file ${job.path} using SHA-256: $ex');
}
}
// Send the message to the recipient
conn.getManagerById<MessageManager>(messageManager)!.sendMessage(
MessageDetails(
to: recipient,
body: slot.getUrl,
requestDeliveryReceipt: true,
id: msg.sid,
originId: msg.originId,
sfs: StatelessFileSharingData(
FileMetadataData(
mediaType: job.mime,
size: stat.size,
name: pathlib.basename(job.path),
thumbnails: job.thumbnails,
hashes: plaintextHashes,
),
<StatelessFileSharingSource>[source],
),
shouldEncrypt: job.encryptMap[recipient]!,
funReplacement: oldSid,
),
);
_log.finest('Sent message with file upload for ${job.path} to $recipient');
final isMultiMedia = job.mime?.startsWith('image/') == true || job.mime?.startsWith('video/') == true;
if (isMultiMedia) {
_log.finest('File appears to be either an image or a video. Copying it to the correct directory...');
unawaited(_copyFile(job));
}
}
}
await _pickNextUploadTask();
@ -348,7 +336,6 @@ class HttpFileTransferService {
/// Actually attempt to download the file described by the job [job].
Future<void> _performFileDownload(FileDownloadJob job) async {
final filename = job.location.filename;
_log.finest('Downloading ${job.location.url} as $filename');
final downloadedPath = await getDownloadPath(filename, job.conversationJid, job.mimeGuess);
var downloadPath = downloadedPath;
@ -358,202 +345,173 @@ class HttpFileTransferService {
downloadPath = pathlib.join(tempDir.path, filename);
}
// Prepare file and completer.
final file = await File(downloadedPath).create();
final fileSink = file.openWrite(mode: FileMode.writeOnlyAppend);
final downloadCompleter = Completer<void>();
dio.Response<dio.ResponseBody>? response;
_log.finest('Downloading ${job.location.url} as $filename (MIME guess ${job.mimeGuess}) to $downloadPath (-> $downloadedPath)');
int? downloadStatusCode;
try {
response = await dio.Dio().get<dio.ResponseBody>(
job.location.url,
options: dio.Options(
responseType: dio.ResponseType.stream,
_log.finest('Beginning download...');
downloadStatusCode = await client.downloadFile(
Uri.parse(job.location.url),
downloadPath,
(total, current) {
final progress = current.toDouble() / total.toDouble();
sendEvent(
ProgressEvent(
id: job.mId,
progress: progress == 1 ? 0.99 : progress,
),
);
},
);
_log.finest('Download done...');
} catch (err) {
_log.finest('Failed to download: $err');
}
if (!isRequestOkay(downloadStatusCode)) {
_log.warning('HTTP GET of ${job.location.url} returned $downloadStatusCode');
await _fileDownloadFailed(job, fileDownloadFailedError);
return;
}
var integrityCheckPassed = true;
final conv = (await GetIt.I.get<ConversationService>()
.getConversationByJid(job.conversationJid))!;
final decryptionKeysAvailable = job.location.key != null && job.location.iv != null;
if (decryptionKeysAvailable) {
// The file was downloaded and is now being decrypted
sendEvent(
ProgressEvent(
id: job.mId,
),
);
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');
if (response.runtimeType != dio.Response<dio.ResponseBody>) {
response = null;
}
}
if (!isRequestOkay(response?.statusCode)) {
_log.warning('HTTP GET of ${job.location.url} returned ${response?.statusCode}');
await _fileDownloadFailed(job, fileDownloadFailedError);
return;
} else {
var integrityCheckPassed = true;
final conv = (await GetIt.I.get<ConversationService>()
.getConversationByJid(job.conversationJid))!;
final decryptionKeysAvailable = job.location.key != null && job.location.iv != null;
if (decryptionKeysAvailable) {
// The file was downloaded and is now being decrypted
sendEvent(
ProgressEvent(
id: job.mId,
),
try {
final result = await GetIt.I.get<CryptographyService>().decryptFile(
downloadPath,
downloadedPath,
encryptionTypeFromNamespace(job.location.encryptionScheme!),
job.location.key!,
job.location.iv!,
job.location.plaintextHashes ?? {},
job.location.ciphertextHashes ?? {},
);
try {
final result = await GetIt.I.get<CryptographyService>().decryptFile(
downloadPath,
downloadedPath,
encryptionTypeFromNamespace(job.location.encryptionScheme!),
job.location.key!,
job.location.iv!,
job.location.plaintextHashes ?? {},
job.location.ciphertextHashes ?? {},
);
if (!result.decryptionOkay) {
_log.warning('Failed to decrypt $downloadPath');
await _fileDownloadFailed(job, messageFailedToDecryptFile);
return;
}
integrityCheckPassed = result.plaintextOkay && result.ciphertextOkay;
} catch (ex) {
_log.warning('Decryption of $downloadPath ($downloadedPath) failed: $ex');
if (!result.decryptionOkay) {
_log.warning('Failed to decrypt $downloadPath');
await _fileDownloadFailed(job, messageFailedToDecryptFile);
return;
}
unawaited(Directory(pathlib.dirname(downloadPath)).delete(recursive: true));
integrityCheckPassed = result.plaintextOkay && result.ciphertextOkay;
} catch (ex) {
_log.warning('Decryption of $downloadPath ($downloadedPath) failed: $ex');
await _fileDownloadFailed(job, messageFailedToDecryptFile);
return;
}
// Check the MIME type
final notification = GetIt.I.get<NotificationsService>();
final mime = job.mimeGuess ?? lookupMimeType(downloadedPath);
int? mediaWidth;
int? mediaHeight;
if (mime != null) {
if (mime.startsWith('image/')) {
MoxplatformPlugin.media.scanFile(downloadedPath);
// Find out the dimensions
final imageSize = await getImageSizeFromPath(downloadedPath);
if (imageSize == null) {
_log.warning('Failed to get image size for $downloadedPath');
}
mediaWidth = imageSize?.width.toInt();
mediaHeight = imageSize?.height.toInt();
} else if (mime.startsWith('video/')) {
MoxplatformPlugin.media.scanFile(downloadedPath);
/*
// Generate thumbnail
final thumbnailPath = await getVideoThumbnailPath(
downloadedPath,
job.conversationJid,
);
// Find out the dimensions
final imageSize = await getImageSizeFromPath(thumbnailPath);
if (imageSize == null) {
_log.warning('Failed to get image size for $downloadedPath ($thumbnailPath)');
}
mediaWidth = imageSize?.width.toInt();
mediaHeight = imageSize?.height.toInt();*/
} else if (mime.startsWith('audio/')) {
MoxplatformPlugin.media.scanFile(downloadedPath);
}
}
final msg = await GetIt.I.get<MessageService>().updateMessage(
job.mId,
mediaUrl: downloadedPath,
mediaType: mime,
mediaWidth: mediaWidth,
mediaHeight: mediaHeight,
mediaSize: File(downloadedPath).lengthSync(),
isFileUploadNotification: false,
warningType: integrityCheckPassed ?
null :
warningFileIntegrityCheckFailed,
errorType: conv.encrypted && !decryptionKeysAvailable ?
messageChatEncryptedButFileNot :
null,
isDownloading: false,
);
sendEvent(MessageUpdatedEvent(message: msg));
final sharedMedium = await GetIt.I.get<DatabaseService>().addSharedMediumFromData(
downloadedPath,
msg.timestamp,
conv.id,
job.mId,
mime: mime,
);
final newConv = conv.copyWith(
lastMessage: conv.lastMessage?.id == job.mId ?
msg :
conv.lastMessage,
sharedMedia: [
sharedMedium,
...conv.sharedMedia,
],
);
GetIt.I.get<ConversationService>().setConversation(newConv);
// Show a notification
if (notification.shouldShowNotification(msg.conversationJid) && job.shouldShowNotification) {
_log.finest('Creating notification with bigPicture $downloadedPath');
await notification.showNotification(newConv, msg, '');
}
sendEvent(ConversationUpdatedEvent(conversation: newConv));
unawaited(Directory(pathlib.dirname(downloadPath)).delete(recursive: true));
}
// Check the MIME type
final notification = GetIt.I.get<NotificationsService>();
final mime = job.mimeGuess ?? lookupMimeType(downloadedPath);
int? mediaWidth;
int? mediaHeight;
if (mime != null) {
if (mime.startsWith('image/')) {
MoxplatformPlugin.media.scanFile(downloadedPath);
// Find out the dimensions
final imageSize = await getImageSizeFromPath(downloadedPath);
if (imageSize == null) {
_log.warning('Failed to get image size for $downloadedPath');
}
mediaWidth = imageSize?.width.toInt();
mediaHeight = imageSize?.height.toInt();
} else if (mime.startsWith('video/')) {
MoxplatformPlugin.media.scanFile(downloadedPath);
/*
// Generate thumbnail
final thumbnailPath = await getVideoThumbnailPath(
downloadedPath,
job.conversationJid,
);
// Find out the dimensions
final imageSize = await getImageSizeFromPath(thumbnailPath);
if (imageSize == null) {
_log.warning('Failed to get image size for $downloadedPath ($thumbnailPath)');
}
mediaWidth = imageSize?.width.toInt();
mediaHeight = imageSize?.height.toInt();*/
} else if (mime.startsWith('audio/')) {
MoxplatformPlugin.media.scanFile(downloadedPath);
}
}
final msg = await GetIt.I.get<MessageService>().updateMessage(
job.mId,
mediaUrl: downloadedPath,
mediaType: mime,
mediaWidth: mediaWidth,
mediaHeight: mediaHeight,
mediaSize: File(downloadedPath).lengthSync(),
isFileUploadNotification: false,
warningType: integrityCheckPassed ?
null :
warningFileIntegrityCheckFailed,
errorType: conv.encrypted && !decryptionKeysAvailable ?
messageChatEncryptedButFileNot :
null,
isDownloading: false,
);
sendEvent(MessageUpdatedEvent(message: msg));
final sharedMedium = await GetIt.I.get<DatabaseService>().addSharedMediumFromData(
downloadedPath,
msg.timestamp,
conv.id,
job.mId,
mime: mime,
);
final newConv = conv.copyWith(
lastMessage: conv.lastMessage?.id == job.mId ?
msg :
conv.lastMessage,
sharedMedia: [
sharedMedium,
...conv.sharedMedia,
],
);
GetIt.I.get<ConversationService>().setConversation(newConv);
// Show a notification
if (notification.shouldShowNotification(msg.conversationJid) && job.shouldShowNotification) {
_log.finest('Creating notification with bigPicture $downloadedPath');
await notification.showNotification(newConv, msg, '');
}
sendEvent(ConversationUpdatedEvent(conversation: newConv));
// Free the download resources for the next one
await _pickNextDownloadTask();
}
Future<void> _pickNextDownloadTask() async {
if (GetIt.I.get<ConnectivityService>().currentState == ConnectivityResult.none) return;
await _downloadLock.synchronized(() async {
if (_downloadQueue.isNotEmpty) {
_currentDownloadJob = _downloadQueue.removeFirst();
unawaited(_performFileDownload(_currentDownloadJob!));
// Only download if we have a connection
if (GetIt.I.get<ConnectivityService>().currentState != ConnectivityResult.none) {
unawaited(_performFileDownload(_currentDownloadJob!));
}
} else {
_currentDownloadJob = null;
}

View File

@ -2,13 +2,13 @@ import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:archive/archive.dart';
import 'package:dio/dio.dart' as dio;
import 'package:get_it/get_it.dart';
import 'package:logging/logging.dart';
import 'package:moxlib/moxlib.dart';
import 'package:moxxmpp/moxxmpp.dart' as moxxmpp;
import 'package:moxxyv2/service/database/database.dart';
import 'package:moxxyv2/service/helpers.dart';
import 'package:moxxyv2/service/httpfiletransfer/client.dart';
import 'package:moxxyv2/service/httpfiletransfer/helpers.dart';
import 'package:moxxyv2/service/preferences.dart';
import 'package:moxxyv2/service/service.dart';
@ -167,20 +167,15 @@ class StickersService {
stickerPackPath,
sticker.hashes.values.first,
);
dio.Response<dynamic>? response;
try {
response = await dio.Dio().downloadUri(
Uri.parse(sticker.urlSources.first),
stickerPath,
);
} on dio.DioError catch(err) {
_log.severe('Error downloading ${sticker.urlSources.first}: $err');
success = false;
break;
}
final downloadStatusCode = await downloadFile(
Uri.parse(sticker.urlSources.first),
stickerPath,
(_, __) {},
);
if (!isRequestOkay(response.statusCode)) {
_log.severe('Request not okay: $response');
if (!isRequestOkay(downloadStatusCode)) {
_log.severe('Request not okay: $downloadStatusCode');
success = false;
break;
}
stickers[i] = sticker.copyWith(

View File

@ -1237,6 +1237,7 @@ class XmppService {
final fts = GetIt.I.get<HttpFileTransferService>();
final metadata = await peekFile(embeddedFile!.url);
_log.finest('Advertised file MIME: ${metadata.mime}');
if (metadata.mime != null) mimeGuess = metadata.mime;
// Auto-download only if the file is below the set limit, if the limit is not set to
@ -1388,12 +1389,13 @@ class XmppService {
sendEvent(MessageUpdatedEvent(message: message));
if (shouldDownload) {
_log.finest('Advertised file MIME: ${_getMimeGuess(event)}');
await GetIt.I.get<HttpFileTransferService>().downloadFile(
FileDownloadJob(
embeddedFile,
message.id,
conversationJid,
null,
_getMimeGuess(event),
shouldShowNotification: false,
),
);

View File

@ -551,7 +551,9 @@ class ConversationPageState extends State<ConversationPage> with TickerProviderS
),
ColoredBox(
color: Theme.of(context).scaffoldBackgroundColor,
color: Theme.of(context)
.scaffoldBackgroundColor
.withOpacity(0.4),
child: ConversationBottomRow(
_controller,
_textfieldFocus,

View File

@ -337,13 +337,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.1"
dio:
dependency: "direct main"
description:
name: dio
url: "https://pub.dartlang.org"
source: hosted
version: "4.0.6"
emoji_picker_flutter:
dependency: "direct main"
description:

View File

@ -23,7 +23,6 @@ dependencies:
#cupertino_icons: 1.0.2
dart_emoji: 0.2.0+2
decorated_icon: 1.2.1
dio: 4.0.6
emoji_picker_flutter: 1.3.1
external_path: 1.0.1
file_picker: 5.0.1