From 78ea12dff3317055902168b3f8455f2d5b3000bb Mon Sep 17 00:00:00 2001 From: erinn Date: Tue, 21 Sep 2021 14:57:40 -0700 Subject: [PATCH 01/10] filesharing wip --- lib/cwtch/cwtch.dart | 7 + lib/cwtch/cwtchNotifier.dart | 9 + lib/cwtch/ffi.dart | 43 +++++ lib/cwtch/gomobile.dart | 17 ++ lib/model.dart | 61 +++++++ lib/models/message.dart | 4 + lib/models/messages/filemessage.dart | 59 +++++++ lib/settings.dart | 1 + lib/views/globalsettingsview.dart | 16 ++ lib/views/messageview.dart | 61 +++++-- lib/widgets/filebubble.dart | 252 +++++++++++++++++++++++++++ 11 files changed, 515 insertions(+), 15 deletions(-) create mode 100644 lib/models/messages/filemessage.dart create mode 100644 lib/widgets/filebubble.dart diff --git a/lib/cwtch/cwtch.dart b/lib/cwtch/cwtch.dart index 22868c44..faa4fcec 100644 --- a/lib/cwtch/cwtch.dart +++ b/lib/cwtch/cwtch.dart @@ -40,6 +40,13 @@ abstract class Cwtch { // ignore: non_constant_identifier_names void SendInvitation(String profile, String handle, String target); + // ignore: non_constant_identifier_names + void ShareFile(String profile, String handle, String filepath); + // ignore: non_constant_identifier_names + void DownloadFile(String profile, String handle, String filepath, String manifestpath, String filekey); + // ignore: non_constant_identifier_names + void CreateDownloadableFile(String profile, String handle, String filenameSuggestion, String filekey); + // ignore: non_constant_identifier_names void ArchiveConversation(String profile, String handle); // ignore: non_constant_identifier_names diff --git a/lib/cwtch/cwtchNotifier.dart b/lib/cwtch/cwtchNotifier.dart index 93024c76..25d6c526 100644 --- a/lib/cwtch/cwtchNotifier.dart +++ b/lib/cwtch/cwtchNotifier.dart @@ -321,6 +321,15 @@ class CwtchNotifier { EnvironmentConfig.debugLog("unhandled peer attribute event: ${data['Path']}"); } break; + case "ManifestSaved": + profileCN.getProfile(data["ProfileOnion"])?.downloadMarkManifest(data["FileKey"]); + break; + case "FileDownloadProgressUpdate": + profileCN.getProfile(data["ProfileOnion"])?.downloadUpdate(data["FileKey"], int.parse(data["Progress"])); + break; + case "FileDownloaded": + profileCN.getProfile(data["ProfileOnion"])?.downloadMarkFinished(data["FileKey"]); + break; default: EnvironmentConfig.debugLog("unhandled event: $type"); } diff --git a/lib/cwtch/ffi.dart b/lib/cwtch/ffi.dart index 82de6c74..11c392ec 100644 --- a/lib/cwtch/ffi.dart +++ b/lib/cwtch/ffi.dart @@ -36,6 +36,9 @@ typedef VoidFromStringStringStringFn = void Function(Pointer, int, Pointer typedef void_from_string_string_string_string_function = Void Function(Pointer, Int32, Pointer, Int32, Pointer, Int32, Pointer, Int32); typedef VoidFromStringStringStringStringFn = void Function(Pointer, int, Pointer, int, Pointer, int, Pointer, int); +typedef void_from_string_string_string_string_string_function = Void Function(Pointer, Int32, Pointer, Int32, Pointer, Int32, Pointer, Int32, Pointer, Int32); +typedef VoidFromStringStringStringStringStringFn = void Function(Pointer, int, Pointer, int, Pointer, int, Pointer, int, Pointer, int); + typedef void_from_string_string_int_int_function = Void Function(Pointer, Int32, Pointer, Int32, Int64, Int64); typedef VoidFromStringStringIntIntFn = void Function(Pointer, int, Pointer, int, int, int); @@ -325,6 +328,46 @@ class CwtchFfi implements Cwtch { malloc.free(u3); } + @override + // ignore: non_constant_identifier_names + void ShareFile(String profileOnion, String contactHandle, String filepath) { + var shareFile = library.lookup>("c_ShareFile"); + // ignore: non_constant_identifier_names + final ShareFile = shareFile.asFunction(); + final u1 = profileOnion.toNativeUtf8(); + final u2 = contactHandle.toNativeUtf8(); + final u3 = filepath.toNativeUtf8(); + ShareFile(u1, u1.length, u2, u2.length, u3, u3.length); + malloc.free(u1); + malloc.free(u2); + malloc.free(u3); + } + + @override + // ignore: non_constant_identifier_names + void DownloadFile(String profileOnion, String contactHandle, String filepath, String manifestpath, String filekey) { + var dlFile = library.lookup>("c_DownloadFile"); + // ignore: non_constant_identifier_names + final DownloadFile = dlFile.asFunction(); + final u1 = profileOnion.toNativeUtf8(); + final u2 = contactHandle.toNativeUtf8(); + final u3 = filepath.toNativeUtf8(); + final u4 = manifestpath.toNativeUtf8(); + final u5 = filekey.toNativeUtf8(); + DownloadFile(u1, u1.length, u2, u2.length, u3, u3.length, u4, u4.length, u5, u5.length); + malloc.free(u1); + malloc.free(u2); + malloc.free(u3); + malloc.free(u4); + malloc.free(u5); + } + + @override + // ignore: non_constant_identifier_names + void CreateDownloadableFile(String profileOnion, String contactHandle, String filenameSuggestion, String filekey) { + // android only - do nothing + } + @override // ignore: non_constant_identifier_names void ResetTor() { diff --git a/lib/cwtch/gomobile.dart b/lib/cwtch/gomobile.dart index 1ca6b1d2..40437cf7 100644 --- a/lib/cwtch/gomobile.dart +++ b/lib/cwtch/gomobile.dart @@ -131,6 +131,23 @@ class CwtchGomobile implements Cwtch { cwtchPlatform.invokeMethod("SendInvitation", {"ProfileOnion": profileOnion, "handle": contactHandle, "target": target}); } + @override + // ignore: non_constant_identifier_names + void ShareFile(String profileOnion, String contactHandle, String filepath) { + cwtchPlatform.invokeMethod("ShareFile", {"ProfileOnion": profileOnion, "handle": contactHandle, "filepath": filepath}); + } + + @override + // ignore: non_constant_identifier_names + void DownloadFile(String profileOnion, String contactHandle, String filepath, String manifestpath, String filekey) { + cwtchPlatform.invokeMethod("DownloadFile", {"ProfileOnion": profileOnion, "handle": contactHandle, "filepath": filepath, "manifestpath": manifestpath, "filekey": filekey}); + } + + // ignore: non_constant_identifier_names + void CreateDownloadableFile(String profileOnion, String contactHandle, String filenameSuggestion, String filekey) { + cwtchPlatform.invokeMethod("CreateDownloadableFile", {"ProfileOnion": profileOnion, "handle": contactHandle, "filename": filenameSuggestion, "filekey": filekey}); + } + @override // ignore: non_constant_identifier_names void ResetTor() { diff --git a/lib/model.dart b/lib/model.dart index 603fd0bb..f5d250d4 100644 --- a/lib/model.dart +++ b/lib/model.dart @@ -24,6 +24,7 @@ class ChatMessage { }; } + class AppState extends ChangeNotifier { bool cwtchInit = false; bool cwtchIsClosing = false; @@ -204,6 +205,7 @@ class ProfileInfoState extends ChangeNotifier { String _imagePath = ""; int _unreadMessages = 0; bool _online = false; + Map _downloads = Map(); // assume profiles are encrypted...this will be set to false // in the constructor if the profile is encrypted with the defacto password. @@ -347,6 +349,65 @@ class ProfileInfoState extends ChangeNotifier { }); } } + + void downloadInit(String fileKey, int numChunks) { + this._downloads[fileKey] = FileDownloadProgress(numChunks); + } + + void downloadUpdate(String fileKey, int progress) { + if (!downloadActive(fileKey)) { + print("error: received progress for unknown download "+fileKey); + } else { + this._downloads[fileKey]!.chunksDownloaded = progress; + notifyListeners(); + } + } + + void downloadMarkManifest(String fileKey) { + if (!downloadActive(fileKey)) { + print("error: received download completion notice for unknown download "+fileKey); + } else { + this._downloads[fileKey]!.gotManifest = true; + notifyListeners(); + } + } + + void downloadMarkFinished(String fileKey) { + if (!downloadActive(fileKey)) { + print("error: received download completion notice for unknown download "+fileKey); + } else { + this._downloads[fileKey]!.complete = true; + notifyListeners(); + } + } + + bool downloadActive(String fileKey) { + return this._downloads.containsKey(fileKey); + } + + bool downloadGotManifest(String fileKey) { + return this._downloads.containsKey(fileKey) && this._downloads[fileKey]!.gotManifest; + } + + bool downloadComplete(String fileKey) { + return this._downloads.containsKey(fileKey) && this._downloads[fileKey]!.complete; + } + + double downloadProgress(String fileKey) { + return this._downloads.containsKey(fileKey) ? this._downloads[fileKey]!.progress() : 0.0; + } +} + +class FileDownloadProgress { + int chunksDownloaded = 0; + int chunksTotal = 1; + bool complete = false; + bool gotManifest = false; + + FileDownloadProgress(this.chunksTotal); + double progress() { + return 1.0 * chunksDownloaded / chunksTotal; + } } enum ContactAuthorization { unknown, approved, blocked } diff --git a/lib/models/message.dart b/lib/models/message.dart index b93ce649..a6c28282 100644 --- a/lib/models/message.dart +++ b/lib/models/message.dart @@ -4,6 +4,7 @@ import 'package:provider/provider.dart'; import '../main.dart'; import '../model.dart'; +import 'messages/filemessage.dart'; import 'messages/invitemessage.dart'; import 'messages/malformedmessage.dart'; import 'messages/quotedmessage.dart'; @@ -14,6 +15,7 @@ const TextMessageOverlay = 1; const QuotedMessageOverlay = 10; const SuggestContactOverlay = 100; const InviteGroupOverlay = 101; +const FileShareOverlay = 200; // Defines the length of the tor v3 onion address. Code using this constant will // need to updated when we allow multiple different identifiers. At which time @@ -77,6 +79,8 @@ Future messageHandler(BuildContext context, String profileOnion, String return InviteMessage(overlay, metadata, content); case QuotedMessageOverlay: return QuotedMessage(metadata, content); + case FileShareOverlay: + return FileMessage(metadata, content); default: // Metadata is valid, content is not.. return MalformedMessage(metadata); diff --git a/lib/models/messages/filemessage.dart b/lib/models/messages/filemessage.dart new file mode 100644 index 00000000..a7e1cca0 --- /dev/null +++ b/lib/models/messages/filemessage.dart @@ -0,0 +1,59 @@ +import 'dart:convert'; + +import 'package:cwtch/models/message.dart'; +import 'package:cwtch/widgets/filebubble.dart'; +import 'package:cwtch/widgets/invitationbubble.dart'; +import 'package:cwtch/widgets/malformedbubble.dart'; +import 'package:cwtch/widgets/messagerow.dart'; +import 'package:flutter/widgets.dart'; +import 'package:provider/provider.dart'; + +import '../../model.dart'; + +class FileMessage extends Message { + final MessageMetadata metadata; + final String content; + + FileMessage(this.metadata, this.content); + + @override + Widget getWidget(BuildContext context) { + return ChangeNotifierProvider.value( + value: this.metadata, + builder: (bcontext, child) { + String idx = Provider.of(context).isGroup == true && this.metadata.signature != null ? this.metadata.signature! : this.metadata.messageIndex.toString(); + dynamic shareObj = jsonDecode(this.content); + if (shareObj == null) { + return MessageRow(MalformedBubble()); + } + String nameSuggestion = shareObj['f'] as String; + String rootHash = shareObj['h'] as String; + String nonce = shareObj['n'] as String; + int fileSize = shareObj['s'] as int; + + return MessageRow(FileBubble(nameSuggestion, rootHash, nonce, fileSize), key: Provider.of(bcontext).getMessageKey(idx)); + }); + } + + @override + Widget getPreviewWidget(BuildContext context) { + return ChangeNotifierProvider.value( + value: this.metadata, + builder: (bcontext, child) { + dynamic shareObj = jsonDecode(this.content); + if (shareObj == null) { + return MessageRow(MalformedBubble()); + } + String nameSuggestion = shareObj['n'] as String; + String rootHash = shareObj['h'] as String; + String nonce = shareObj['n'] as String; + int fileSize = shareObj['s'] as int; + return FileBubble(nameSuggestion, rootHash, nonce, fileSize); + }); + } + + @override + MessageMetadata getMetadata() { + return this.metadata; + } +} diff --git a/lib/settings.dart b/lib/settings.dart index df42b083..4c1721f7 100644 --- a/lib/settings.dart +++ b/lib/settings.dart @@ -9,6 +9,7 @@ import 'opaque.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; const TapirGroupsExperiment = "tapir-groups-experiment"; +const FileSharingExperiment = "filesharing"; enum DualpaneMode { Single, diff --git a/lib/views/globalsettingsview.dart b/lib/views/globalsettingsview.dart index ce655ed3..8a31ef7a 100644 --- a/lib/views/globalsettingsview.dart +++ b/lib/views/globalsettingsview.dart @@ -175,6 +175,22 @@ class _GlobalSettingsViewState extends State { inactiveTrackColor: settings.theme.defaultButtonDisabledColor(), secondary: Icon(CwtchIcons.enable_groups, color: settings.current().mainTextColor()), ), + SwitchListTile( + title: Text(AppLocalizations.of(context)!.labelFileSharing, style: TextStyle(color: settings.current().mainTextColor())), + subtitle: Text(AppLocalizations.of(context)!.descriptionFileSharing), + value: settings.isExperimentEnabled(FileSharingExperiment), + onChanged: (bool value) { + if (value) { + settings.enableExperiment(FileSharingExperiment); + } else { + settings.disableExperiment(FileSharingExperiment); + } + saveSettings(context); + }, + activeTrackColor: settings.theme.defaultButtonActiveColor(), + inactiveTrackColor: settings.theme.defaultButtonDisabledColor(), + secondary: Icon(Icons.attach_file, color: settings.current().mainTextColor()), + ), ], )), AboutListTile( diff --git a/lib/views/messageview.dart b/lib/views/messageview.dart index 625608bc..2cfd1b8c 100644 --- a/lib/views/messageview.dart +++ b/lib/views/messageview.dart @@ -6,6 +6,9 @@ import 'package:cwtch/models/message.dart'; import 'package:cwtch/models/messages/quotedmessage.dart'; import 'package:cwtch/widgets/messageloadingbubble.dart'; import 'package:cwtch/widgets/profileimage.dart'; + +import 'package:file_picker/file_picker.dart'; + import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:cwtch/views/peersettingsview.dart'; @@ -14,6 +17,7 @@ import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; +import 'package:path/path.dart' show basename; import '../main.dart'; import '../model.dart'; @@ -74,6 +78,26 @@ class _MessageViewState extends State { return Card(child: Center(child: Text(AppLocalizations.of(context)!.addContactFirst))); } + var appBarButtons = []; + if (Provider.of(context).isOnline()) { + appBarButtons.add(IconButton( + icon: Icon(Icons.attach_file, size: 24), + tooltip: AppLocalizations.of(context)!.tooltipSendFile, + onPressed: _showFilePicker, + )); + appBarButtons.add(IconButton( + icon: Icon(CwtchIcons.send_invite, size: 24), + tooltip: AppLocalizations.of(context)!.sendInvite, + onPressed: () { + _modalSendInvitation(context); + })); + } + appBarButtons.add(IconButton( + icon: Provider.of(context, listen: false).isGroup == true ? Icon(CwtchIcons.group_settings_24px) : Icon(CwtchIcons.peer_settings_24px), + tooltip: AppLocalizations.of(context)!.conversationSettings, + onPressed: _pushContactSettings + )); + var appState = Provider.of(context); return WillPopScope( onWillPop: _onWillPop, @@ -105,21 +129,7 @@ class _MessageViewState extends State { overflow: TextOverflow.ellipsis, )) ]), - actions: [ - //IconButton(icon: Icon(Icons.chat), onPressed: _pushContactSettings), - //IconButton(icon: Icon(Icons.list), onPressed: _pushContactSettings), - //IconButton(icon: Icon(Icons.push_pin), onPressed: _pushContactSettings), - IconButton( - icon: Icon(CwtchIcons.send_invite, size: 24), - tooltip: AppLocalizations.of(context)!.sendInvite, - onPressed: () { - _modalSendInvitation(context); - }), - IconButton( - icon: Provider.of(context, listen: false).isGroup == true ? Icon(CwtchIcons.group_settings_24px) : Icon(CwtchIcons.peer_settings_24px), - tooltip: AppLocalizations.of(context)!.conversationSettings, - onPressed: _pushContactSettings), - ], + actions: appBarButtons, ), body: Padding(padding: EdgeInsets.fromLTRB(8.0, 8.0, 8.0, 108.0), child: MessageList(scrollController, scrollListener)), bottomSheet: _buildComposeBox(), @@ -189,6 +199,13 @@ class _MessageViewState extends State { _sendMessageHelper(); } + void _sendFile(String filePath) { + Provider.of(context, listen: false) + .cwtch + .ShareFile(Provider.of(context, listen: false).profileOnion, Provider.of(context, listen: false).onion, filePath); + _sendMessageHelper(); + } + void _sendMessageHelper() { ctrlrCompose.clear(); focusNode.requestFocus(); @@ -342,4 +359,18 @@ class _MessageViewState extends State { )); }); } + + void _showFilePicker() async { + FilePickerResult? result = await FilePicker.platform.pickFiles(); + if(result != null) { + File file = File(result.files.first.path); + if (file.lengthSync() <= 10737418240) { + print("Sending " +file.path); + _sendFile(file.path); + } else { + print("file size cannot exceed 10 gigabytes"); + //todo: toast error + } + } + } } diff --git a/lib/widgets/filebubble.dart b/lib/widgets/filebubble.dart new file mode 100644 index 00000000..698325e5 --- /dev/null +++ b/lib/widgets/filebubble.dart @@ -0,0 +1,252 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:cwtch/cwtch_icons_icons.dart'; +import 'package:cwtch/models/message.dart'; +import 'package:cwtch/widgets/malformedbubble.dart'; +import 'package:file_picker/file_picker.dart' as androidPicker; +import 'package:file_picker_desktop/file_picker_desktop.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:provider/provider.dart'; +import '../main.dart'; +import '../model.dart'; +import 'package:intl/intl.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import '../settings.dart'; +import 'messagebubbledecorations.dart'; + +// Like MessageBubble but for displaying chat overlay 100/101 invitations +// Offers the user an accept/reject button if they don't have a matching contact already +class FileBubble extends StatefulWidget { + final String nameSuggestion; + final String rootHash; + final String nonce; + final int fileSize; + + FileBubble(this.nameSuggestion, this.rootHash, this.nonce, this.fileSize); + + @override + FileBubbleState createState() => FileBubbleState(); + + String fileKey() { + return this.rootHash + "." + this.nonce; + } +} + +class FileBubbleState extends State { + @override + Widget build(BuildContext context) { + var fromMe = Provider.of(context).senderHandle == Provider.of(context).onion; + //isAccepted = Provider.of(context).contactList.getContact(widget.inviteTarget) != null; + var borderRadiousEh = 15.0; + var showFileSharing = Provider.of(context).isExperimentEnabled(FileSharingExperiment); + var prettyDate = DateFormat.yMd(Platform.localeName).add_jm().format(Provider.of(context).timestamp); + + // If the sender is not us, then we want to give them a nickname... + var senderDisplayStr = ""; + if (!fromMe) { + ContactInfoState? contact = Provider.of(context).contactList.getContact(Provider.of(context).senderHandle); + if (contact != null) { + senderDisplayStr = contact.nickname; + } else { + senderDisplayStr = Provider.of(context).senderHandle; + } + } + + var wdgSender = Center( + widthFactor: 1, + child: SelectableText(senderDisplayStr + '\u202F', + style: TextStyle(fontSize: 9.0, color: fromMe ? Provider.of(context).theme.messageFromMeTextColor() : Provider.of(context).theme.messageFromOtherTextColor()))); + + var wdgMessage = !showFileSharing + ? Text(AppLocalizations.of(context)!.messageEnableFileSharing) + : fromMe + ? senderInviteChrome( + AppLocalizations.of(context)!.messageFileSent, widget.nameSuggestion, widget.rootHash, widget.fileSize) + : (inviteChrome(AppLocalizations.of(context)!.messageFileOffered + ":", widget.nameSuggestion, widget.rootHash, widget.fileSize)); + + Widget wdgDecorations; + if (!showFileSharing) { + wdgDecorations = Text('\u202F'); + } else if (fromMe) { + wdgDecorations = MessageBubbleDecoration(ackd: Provider.of(context).ackd, errored: Provider.of(context).error, fromMe: fromMe, prettyDate: prettyDate); + } else if (Provider.of(context).downloadComplete(widget.fileKey())) { + wdgDecorations = Center( + widthFactor: 1, + child: Wrap(children: [ + Padding(padding: EdgeInsets.all(5), child: ElevatedButton(child: Text(AppLocalizations.of(context)!.openFolderButton + '\u202F'), onPressed: _btnAccept)), + ])); + } else if (Provider.of(context).downloadActive(widget.fileKey())) { + if (!Provider.of(context).downloadGotManifest(widget.fileKey())) { + wdgDecorations = Text(AppLocalizations.of(context)!.retrievingManifestMessage + '\u202F'); + } else { + wdgDecorations = LinearProgressIndicator( + value: Provider.of(context).downloadProgress(widget.fileKey()), + color: Provider.of(context).theme.defaultButtonActiveColor(), + ); + } + } else { + wdgDecorations = Center( + widthFactor: 1, + child: Wrap(children: [ + Padding(padding: EdgeInsets.all(5), child: ElevatedButton(child: Text(AppLocalizations.of(context)!.downloadFileButton + '\u202F'), onPressed: _btnAccept)), + ])); + } + + return LayoutBuilder(builder: (context, constraints) { + //print(constraints.toString()+", "+constraints.maxWidth.toString()); + return Center( + widthFactor: 1.0, + child: Container( + decoration: BoxDecoration( + color: fromMe ? Provider.of(context).theme.messageFromMeBackgroundColor() : Provider.of(context).theme.messageFromOtherBackgroundColor(), + border: + Border.all(color: fromMe ? Provider.of(context).theme.messageFromMeBackgroundColor() : Provider.of(context).theme.messageFromOtherBackgroundColor(), width: 1), + borderRadius: BorderRadius.only( + topLeft: Radius.circular(borderRadiousEh), + topRight: Radius.circular(borderRadiousEh), + bottomLeft: fromMe ? Radius.circular(borderRadiousEh) : Radius.zero, + bottomRight: fromMe ? Radius.zero : Radius.circular(borderRadiousEh), + ), + ), + child: Center( + widthFactor: 1.0, + child: Padding( + padding: EdgeInsets.all(9.0), + child: Wrap(runAlignment: WrapAlignment.spaceEvenly, alignment: WrapAlignment.spaceEvenly, runSpacing: 1.0, crossAxisAlignment: WrapCrossAlignment.center, children: [ + Center( + widthFactor: 1, child: Padding(padding: EdgeInsets.all(10.0), child: Icon(Icons.attach_file, size: 32))), + Center( + widthFactor: 1.0, + child: Column( + crossAxisAlignment: fromMe ? CrossAxisAlignment.end : CrossAxisAlignment.start, + mainAxisAlignment: fromMe ? MainAxisAlignment.end : MainAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: fromMe ? [wdgMessage, wdgDecorations] : [wdgSender, wdgMessage, wdgDecorations]), + ) + ]))))); + }); + } + + void _btnAccept() async { + String? selectedFileName; + File? file; + var profileOnion = Provider.of(context, listen: false).onion; + var handle = Provider.of(context, listen: false).senderHandle; + + if (Platform.isAndroid) { + //todo: would be better to only call downloadInit if CreateDownloadableFile results in a user-pick (they might cancel) + Provider.of(context, listen: false).downloadInit(widget.fileKey(), (widget.fileSize / 4096).ceil()); + Provider.of(context, listen: false).cwtch.CreateDownloadableFile(profileOnion, handle, widget.nameSuggestion, widget.fileKey()); + } else { + try { + selectedFileName = await saveFile(defaultFileName: widget.nameSuggestion,); + if (selectedFileName != null) { + file = File(selectedFileName); + print("saving to " + file.path); + var manifestPath = file.path + ".manifest"; + setState(() { + Provider.of(context, listen: false).cwtch.DownloadFile(profileOnion, handle, file!.path, manifestPath, widget.fileKey()); + }); + } + } catch (e) { + print(e); + } + } + } + + // Construct an invite chrome for the sender + Widget senderInviteChrome(String chrome, String fileName, String rootHash, int fileSize) { + return Wrap(children: [ + SelectableText( + chrome + '\u202F', + style: TextStyle( + color: Provider.of(context).theme.messageFromMeTextColor(), + ), + textAlign: TextAlign.left, + maxLines: 2, + textWidthBasis: TextWidthBasis.longestLine, + ), + SelectableText( + fileName + '\u202F', + style: TextStyle( + color: Provider.of(context).theme.messageFromMeTextColor(), + ), + textAlign: TextAlign.left, + maxLines: 2, + textWidthBasis: TextWidthBasis.longestLine, + ), + SelectableText( + fileSize.toString() + 'B\u202F', + style: TextStyle( + color: Provider.of(context).theme.messageFromMeTextColor(), + ), + textAlign: TextAlign.left, + maxLines: 2, + textWidthBasis: TextWidthBasis.longestLine, + ), + SelectableText( + 'sha512: ' + rootHash + '\u202F', + style: TextStyle( + color: Provider.of(context).theme.messageFromMeTextColor(), + ), + textAlign: TextAlign.left, + maxLines: 2, + textWidthBasis: TextWidthBasis.longestLine, + ), + ]); + } + + // Construct an invite chrome + Widget inviteChrome(String chrome, String fileName, String rootHash, int fileSize) { + var prettyHash = rootHash; + if (rootHash.length == 128) { + prettyHash = rootHash.substring(0, 32) + '\n' + + rootHash.substring(32, 64) + '\n' + + rootHash.substring(64, 96) + '\n' + + rootHash.substring(96); + } + + return Wrap(direction: Axis.vertical, + children: [ + SelectableText( + chrome + '\u202F', + style: TextStyle( + color: Provider.of(context).theme.messageFromOtherTextColor(), + ), + textAlign: TextAlign.left, + textWidthBasis: TextWidthBasis.longestLine, + maxLines: 2, + ), + SelectableText( + AppLocalizations.of(context)!.labelFilename +': ' + fileName + '\u202F', + style: TextStyle( + color: Provider.of(context).theme.messageFromMeTextColor(), + ), + textAlign: TextAlign.left, + maxLines: 2, + textWidthBasis: TextWidthBasis.longestLine, + ), + SelectableText( + AppLocalizations.of(context)!.labelFilesize + ': ' + fileSize.toString() + 'B\u202F', + style: TextStyle( + color: Provider.of(context).theme.messageFromMeTextColor(), + ), + textAlign: TextAlign.left, + maxLines: 2, + textWidthBasis: TextWidthBasis.longestLine, + ), + SelectableText( + 'sha512: ' + prettyHash + '\u202F', + style: TextStyle( + color: Provider.of(context).theme.messageFromMeTextColor(), + ), + textAlign: TextAlign.left, + maxLines: 4, + textWidthBasis: TextWidthBasis.longestLine, + ), + ]); + } +} From cb3c16127770586918b1b1ced5e414edbaa6d02b Mon Sep 17 00:00:00 2001 From: erinn Date: Mon, 27 Sep 2021 12:53:21 -0700 Subject: [PATCH 02/10] wip: filesharing ui dev --- android/app/build.gradle | 4 +- android/app/local.properties | 8 +++ android/app/src/main/AndroidManifest.xml | 9 ++- .../kotlin/im/cwtch/flwtch/FlwtchWorker.kt | 37 ++++++++++++ .../kotlin/im/cwtch/flwtch/MainActivity.kt | 60 ++++++++++++++----- android/build.gradle | 2 +- lib/model.dart | 31 +++++++++- lib/views/globalsettingsview.dart | 2 +- lib/views/messageview.dart | 3 + lib/widgets/filebubble.dart | 28 +++++---- lib/widgets/messagerow.dart | 2 +- pubspec.lock | 29 +++++++-- pubspec.yaml | 2 + 13 files changed, 180 insertions(+), 37 deletions(-) create mode 100644 android/app/local.properties diff --git a/android/app/build.gradle b/android/app/build.gradle index 58459a0b..5ac09ca3 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -33,7 +33,7 @@ if (keystorePropertiesFile.exists()) { } android { - compileSdkVersion 29 + compileSdkVersion 30 sourceSets { main.java.srcDirs += 'src/main/kotlin' @@ -48,7 +48,7 @@ android { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "im.cwtch.flwtch" minSdkVersion 16 - targetSdkVersion 29 + targetSdkVersion 30 versionCode flutterVersionCode.toInteger() versionName flutterVersionName } diff --git a/android/app/local.properties b/android/app/local.properties new file mode 100644 index 00000000..3b474cda --- /dev/null +++ b/android/app/local.properties @@ -0,0 +1,8 @@ +## This file must *NOT* be checked into Version Control Systems, +# as it contains information specific to your local configuration. +# +# Location of the SDK. This is only used by Gradle. +# For customization when using a Version Control System, please read the +# header note. +#Fri Jul 02 15:08:54 PDT 2021 +sdk.dir=/home/erinn/Android/Sdk diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index ec51df3e..72121fd4 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -9,7 +9,8 @@ android:name="io.flutter.app.FlutterApplication" android:label="Cwtch" android:extractNativeLibs="true" - android:icon="@mipmap/knott"> + android:icon="@mipmap/knott" + android:requestLegacyExternalStorage="true"> + + + + + + diff --git a/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt b/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt index 5a71ea8b..acb83e5f 100644 --- a/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt +++ b/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt @@ -15,6 +15,10 @@ import cwtch.Cwtch import io.flutter.FlutterInjector import org.json.JSONObject +import java.nio.file.Files +import java.nio.file.Paths +import java.nio.file.StandardCopyOption +import android.net.Uri class FlwtchWorker(context: Context, parameters: WorkerParameters) : CoroutineWorker(context, parameters) { @@ -93,6 +97,24 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) : .build() notificationManager.notify(getNotificationID(data.getString("ProfileOnion"), handle), newNotification) } + } else if (evt.EventType == "FileDownloaded") { + Log.i("FlwtchWorker", "file downloaded!"); + val data = JSONObject(evt.Data); + val tempFile = data.getString("TempFile"); + if (tempFile != "") { + val filePath = data.getString("FilePath"); + Log.i("FlwtchWorker", "moving "+tempFile+" to "+filePath); + val sourcePath = Paths.get(tempFile); + val targetUri = Uri.parse(filePath); + val os = this.applicationContext.getContentResolver().openOutputStream(targetUri); + val bytesWritten = Files.copy(sourcePath, os); + Log.i("FlwtchWorker", "copied " + bytesWritten.toString() + " bytes"); + if (bytesWritten != 0L) { + os?.flush(); + os?.close(); + Files.delete(sourcePath); + } + } } Intent().also { intent -> @@ -157,6 +179,21 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) : val target = (a.get("target") as? String) ?: "" Cwtch.sendInvitation(profile, handle, target) } + "ShareFile" -> { + val profile = (a.get("ProfileOnion") as? String) ?: "" + val handle = (a.get("handle") as? String) ?: "" + val filepath = (a.get("filepath") as? String) ?: "" + Cwtch.shareFile(profile, handle, filepath) + } + "DownloadFile" -> { + val profile = (a.get("ProfileOnion") as? String) ?: "" + val handle = (a.get("handle") as? String) ?: "" + val filepath = (a.get("filepath") as? String) ?: "" + val manifestpath = (a.get("manifestpath") as? String) ?: "" + val filekey = (a.get("filekey") as? String) ?: "" + Log.i("FlwtchWorker::DownloadFile", "DownloadFile("+filepath+", "+manifestpath+")") + Cwtch.downloadFile(profile, handle, filepath, manifestpath, filekey) + } "SendProfileEvent" -> { val onion = (a.get("onion") as? String) ?: "" val jsonEvent = (a.get("jsonEvent") as? String) ?: "" diff --git a/android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt b/android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt index 4025131f..8c7d1f47 100644 --- a/android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt +++ b/android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt @@ -12,17 +12,25 @@ import android.view.Window import androidx.lifecycle.Observer import androidx.localbroadcastmanager.content.LocalBroadcastManager import androidx.work.* - import io.flutter.embedding.android.SplashScreen import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.engine.FlutterEngine import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel.Result +import io.flutter.plugin.common.ErrorLogResult import org.json.JSONObject import java.util.concurrent.TimeUnit +import android.net.Uri +import android.provider.DocumentsContract +import android.content.ContentUris +import android.os.Build +import android.os.Environment +import android.database.Cursor +import android.provider.MediaStore + class MainActivity: FlutterActivity() { override fun provideSplashScreen(): SplashScreen? = SplashView() @@ -47,6 +55,11 @@ class MainActivity: FlutterActivity() { private var notificationClickChannel: MethodChannel? = null private var shutdownClickChannel: MethodChannel? = null + // "Download to..." prompt extra arguments + private var dlToProfile = "" + private var dlToHandle = "" + private var dlToFileKey = "" + override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) if (notificationClickChannel == null || intent.extras == null) return @@ -68,6 +81,25 @@ class MainActivity: FlutterActivity() { } } + override fun onActivityResult(requestCode: Int, result: Int, intent: Intent?) { + if (intent == null || intent!!.getData() == null) { + Log.i("MainActivity:onActivityResult", "user canceled activity"); + return; + } + + val filePath = intent!!.getData().toString(); + val manifestPath = StringBuilder().append(this.applicationContext.cacheDir).append("/").append(this.dlToFileKey).toString(); + Log.i("onActivityResult", "got download path: " + filePath); + Log.i("onActivityResult", "got manifest path: " + manifestPath); + handleCwtch(MethodCall("DownloadFile", mapOf( + "ProfileOnion" to this.dlToProfile, + "handle" to this.dlToHandle, + "filepath" to filePath, + "manifestpath" to manifestPath, + "filekey" to this.dlToFileKey + )), ErrorLogResult(""));//placeholder; result is never actually invoked + } + override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) // Note: this methods are invoked on the main thread. @@ -125,6 +157,18 @@ class MainActivity: FlutterActivity() { val workRequest = PeriodicWorkRequestBuilder(15, TimeUnit.MINUTES).setInputData(data).addTag(WORKER_TAG).addTag(uniqueTag).build() WorkManager.getInstance(this).enqueueUniquePeriodicWork("req_$uniqueTag", ExistingPeriodicWorkPolicy.REPLACE, workRequest) return + } else if (call.method == "CreateDownloadableFile") { + this.dlToProfile = argmap["ProfileOnion"] ?: "" + this.dlToHandle = argmap["handle"] ?: "" + val suggestedName = argmap["filename"] ?: "filename.ext" + this.dlToFileKey = argmap["filekey"] ?: "" + val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply { + addCategory(Intent.CATEGORY_OPENABLE) + type = "application/octet-stream" + putExtra(Intent.EXTRA_TITLE, suggestedName) + } + startActivityForResult(intent, 1) + return } // ...otherwise fallthru to a normal ffi method call (and return the result using the result callback) @@ -178,20 +222,6 @@ class MainActivity: FlutterActivity() { WorkManager.getInstance(this).pruneWork() } -// source: https://web.archive.org/web/20210203022531/https://stackoverflow.com/questions/41928803/how-to-parse-json-in-kotlin/50468095 -// for reference: -// -// class Response(json: String) : JSONObject(json) { -// val type: String? = this.optString("type") -// val data = this.optJSONArray("data") -// ?.let { 0.until(it.length()).map { i -> it.optJSONObject(i) } } // returns an array of JSONObject -// ?.map { Foo(it.toString()) } // transforms each JSONObject of the array into Foo -// } -// -// class Foo(json: String) : JSONObject(json) { -// val id = this.optInt("id") -// val title: String? = this.optString("title") -// } class AppbusEvent(json: String) : JSONObject(json) { val EventType = this.optString("EventType") val EventID = this.optString("EventID") diff --git a/android/build.gradle b/android/build.gradle index c887697b..5a12dade 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -6,7 +6,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:3.5.0' + classpath 'com.android.tools.build:gradle:3.5.4' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } diff --git a/lib/model.dart b/lib/model.dart index ff7a00a0..9dcac693 100644 --- a/lib/model.dart +++ b/lib/model.dart @@ -360,7 +360,7 @@ class ProfileInfoState extends ChangeNotifier { } void downloadInit(String fileKey, int numChunks) { - this._downloads[fileKey] = FileDownloadProgress(numChunks); + this._downloads[fileKey] = FileDownloadProgress(numChunks, DateTime.now()); } void downloadUpdate(String fileKey, int progress) { @@ -385,6 +385,7 @@ class ProfileInfoState extends ChangeNotifier { if (!downloadActive(fileKey)) { print("error: received download completion notice for unknown download "+fileKey); } else { + this._downloads[fileKey]!.timeEnd = DateTime.now(); this._downloads[fileKey]!.complete = true; notifyListeners(); } @@ -405,6 +406,18 @@ class ProfileInfoState extends ChangeNotifier { double downloadProgress(String fileKey) { return this._downloads.containsKey(fileKey) ? this._downloads[fileKey]!.progress() : 0.0; } + + String downloadSpeed(String fileKey) { + if (!downloadActive(fileKey)) { + return "0 B/s"; + } + var bytes = this._downloads[fileKey]!.chunksDownloaded * 4096; + var seconds = (this._downloads[fileKey]!.timeEnd ?? DateTime.now()).difference(this._downloads[fileKey]!.timeStart!).inSeconds; + if (seconds == 0) { + return "0 B/s"; + } + return prettyBytes((bytes / seconds).round()) + "/s"; + } } class FileDownloadProgress { @@ -412,13 +425,27 @@ class FileDownloadProgress { int chunksTotal = 1; bool complete = false; bool gotManifest = false; + DateTime? timeStart; + DateTime? timeEnd; - FileDownloadProgress(this.chunksTotal); + FileDownloadProgress(this.chunksTotal, this.timeStart); double progress() { return 1.0 * chunksDownloaded / chunksTotal; } } +String prettyBytes(int bytes) { + if (bytes > 1000000000) { + return (1.0 * bytes / 1000000000).toStringAsFixed(1) + " GB"; + } else if (bytes > 1000000) { + return (1.0 * bytes / 1000000).toStringAsFixed(1) + " MB"; + } else if (bytes > 1000) { + return (1.0 * bytes / 1000).toStringAsFixed(1) + " kB"; + } else { + return bytes.toString() + " B"; + } +} + enum ContactAuthorization { unknown, approved, blocked } ContactAuthorization stringToContactAuthorization(String authStr) { diff --git a/lib/views/globalsettingsview.dart b/lib/views/globalsettingsview.dart index 373100d7..fb85e3b1 100644 --- a/lib/views/globalsettingsview.dart +++ b/lib/views/globalsettingsview.dart @@ -189,7 +189,7 @@ class _GlobalSettingsViewState extends State { secondary: Icon(CwtchIcons.enable_groups, color: settings.current().mainTextColor()), ), SwitchListTile( - title: Text(AppLocalizations.of(context)!.labelFileSharing, style: TextStyle(color: settings.current().mainTextColor())), + title: Text(AppLocalizations.of(context)!.settingFileSharing, style: TextStyle(color: settings.current().mainTextColor())), subtitle: Text(AppLocalizations.of(context)!.descriptionFileSharing), value: settings.isExperimentEnabled(FileSharingExperiment), onChanged: (bool value) { diff --git a/lib/views/messageview.dart b/lib/views/messageview.dart index 2c1c158c..0dcb9e07 100644 --- a/lib/views/messageview.dart +++ b/lib/views/messageview.dart @@ -40,6 +40,9 @@ class _MessageViewState extends State { @override void initState() { scrollListener.itemPositions.addListener(() { + if (scrollListener.itemPositions.value.length == 0) { + return; + } var first = scrollListener.itemPositions.value.first.index; var last = scrollListener.itemPositions.value.last.index; // sometimes these go hi->lo and sometimes they go lo->hi because [who tf knows] diff --git a/lib/widgets/filebubble.dart b/lib/widgets/filebubble.dart index 698325e5..7286f7cf 100644 --- a/lib/widgets/filebubble.dart +++ b/lib/widgets/filebubble.dart @@ -63,9 +63,9 @@ class FileBubbleState extends State { var wdgMessage = !showFileSharing ? Text(AppLocalizations.of(context)!.messageEnableFileSharing) : fromMe - ? senderInviteChrome( + ? senderFileChrome( AppLocalizations.of(context)!.messageFileSent, widget.nameSuggestion, widget.rootHash, widget.fileSize) - : (inviteChrome(AppLocalizations.of(context)!.messageFileOffered + ":", widget.nameSuggestion, widget.rootHash, widget.fileSize)); + : (fileChrome(AppLocalizations.of(context)!.messageFileOffered + ":", widget.nameSuggestion, widget.rootHash, widget.fileSize, Provider.of(context).downloadSpeed(widget.fileKey()))); Widget wdgDecorations; if (!showFileSharing) { @@ -147,9 +147,8 @@ class FileBubbleState extends State { file = File(selectedFileName); print("saving to " + file.path); var manifestPath = file.path + ".manifest"; - setState(() { - Provider.of(context, listen: false).cwtch.DownloadFile(profileOnion, handle, file!.path, manifestPath, widget.fileKey()); - }); + Provider.of(context, listen: false).downloadInit(widget.fileKey(), (widget.fileSize / 4096).ceil()); + Provider.of(context, listen: false).cwtch.DownloadFile(profileOnion, handle, file.path, manifestPath, widget.fileKey()); } } catch (e) { print(e); @@ -158,8 +157,8 @@ class FileBubbleState extends State { } // Construct an invite chrome for the sender - Widget senderInviteChrome(String chrome, String fileName, String rootHash, int fileSize) { - return Wrap(children: [ + Widget senderFileChrome(String chrome, String fileName, String rootHash, int fileSize) { + return Wrap(direction: Axis.vertical,children: [ SelectableText( chrome + '\u202F', style: TextStyle( @@ -179,7 +178,7 @@ class FileBubbleState extends State { textWidthBasis: TextWidthBasis.longestLine, ), SelectableText( - fileSize.toString() + 'B\u202F', + prettyBytes(fileSize) + '\u202F', style: TextStyle( color: Provider.of(context).theme.messageFromMeTextColor(), ), @@ -200,7 +199,7 @@ class FileBubbleState extends State { } // Construct an invite chrome - Widget inviteChrome(String chrome, String fileName, String rootHash, int fileSize) { + Widget fileChrome(String chrome, String fileName, String rootHash, int fileSize, String speed) { var prettyHash = rootHash; if (rootHash.length == 128) { prettyHash = rootHash.substring(0, 32) + '\n' + @@ -230,7 +229,7 @@ class FileBubbleState extends State { textWidthBasis: TextWidthBasis.longestLine, ), SelectableText( - AppLocalizations.of(context)!.labelFilesize + ': ' + fileSize.toString() + 'B\u202F', + AppLocalizations.of(context)!.labelFilesize + ': ' + prettyBytes(fileSize) + '\u202F', style: TextStyle( color: Provider.of(context).theme.messageFromMeTextColor(), ), @@ -247,6 +246,15 @@ class FileBubbleState extends State { maxLines: 4, textWidthBasis: TextWidthBasis.longestLine, ), + SelectableText( + speed + '\u202F', + style: TextStyle( + color: Provider.of(context).theme.messageFromMeTextColor(), + ), + textAlign: TextAlign.left, + maxLines: 1, + textWidthBasis: TextWidthBasis.longestLine, + ), ]); } } diff --git a/lib/widgets/messagerow.dart b/lib/widgets/messagerow.dart index 69e97a00..efaa670f 100644 --- a/lib/widgets/messagerow.dart +++ b/lib/widgets/messagerow.dart @@ -163,7 +163,7 @@ class MessageRowState extends State with SingleTickerProviderStateMi // For desktop... onHover: (event) { setState(() { - Provider.of(context, listen: false).hoveredIndex = Provider.of(context).messageIndex; + Provider.of(context, listen: false).hoveredIndex = Provider.of(context, listen: false).messageIndex; }); }, onExit: (event) { diff --git a/pubspec.lock b/pubspec.lock index b8117b89..41cdef38 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -21,7 +21,7 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.8.1" + version: "2.8.2" boolean_selector: dependency: transitive description: @@ -106,6 +106,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "6.1.2" + file_picker: + dependency: "direct main" + description: + name: file_picker + url: "https://pub.dartlang.org" + source: hosted + version: "4.0.1" + file_picker_desktop: + dependency: "direct main" + description: + name: file_picker_desktop + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" flutter: dependency: "direct main" description: flutter @@ -116,6 +130,13 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.3" flutter_test: dependency: "direct main" description: flutter @@ -174,7 +195,7 @@ packages: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.10" + version: "0.12.11" meta: dependency: transitive description: @@ -382,7 +403,7 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.4.2" + version: "0.4.3" typed_data: dependency: transitive description: @@ -427,4 +448,4 @@ packages: version: "3.1.0" sdks: dart: ">=2.13.0 <3.0.0" - flutter: ">=1.20.0" + flutter: ">=2.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index c7d1f0fb..d0ffcee0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -41,6 +41,8 @@ dependencies: flutter_test: sdk: flutter scrollable_positioned_list: ^0.2.0-nullsafety.0 + file_picker: ^4.0.1 + file_picker_desktop: ^1.1.0 dev_dependencies: msix: ^2.1.3 From 8fe577afd44067229061885f93db2abb2544b0dd Mon Sep 17 00:00:00 2001 From: erinn Date: Wed, 29 Sep 2021 13:31:01 -0700 Subject: [PATCH 03/10] moar fileshare plz --- .../kotlin/im/cwtch/flwtch/FlwtchWorker.kt | 5 ++ lib/cwtch/cwtch.dart | 2 + lib/cwtch/cwtchNotifier.dart | 2 +- lib/cwtch/ffi.dart | 13 ++++ lib/cwtch/gomobile.dart | 6 ++ lib/model.dart | 22 +++++-- lib/models/message.dart | 64 ++++++++++--------- lib/widgets/filebubble.dart | 42 ++++++++---- lib/widgets/invitationbubble.dart | 2 +- lib/widgets/messagelist.dart | 2 +- 10 files changed, 107 insertions(+), 53 deletions(-) diff --git a/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt b/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt index acb83e5f..b5727649 100644 --- a/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt +++ b/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt @@ -194,6 +194,11 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) : Log.i("FlwtchWorker::DownloadFile", "DownloadFile("+filepath+", "+manifestpath+")") Cwtch.downloadFile(profile, handle, filepath, manifestpath, filekey) } + "CheckDownloadStatus" -> { + val profile = (a.get("ProfileOnion") as? String) ?: "" + val fileKey = (a.get("fileKey") as? String) ?: "" + Cwtch.checkDownloadStatus(profile, fileKey) + } "SendProfileEvent" -> { val onion = (a.get("onion") as? String) ?: "" val jsonEvent = (a.get("jsonEvent") as? String) ?: "" diff --git a/lib/cwtch/cwtch.dart b/lib/cwtch/cwtch.dart index faa4fcec..0df9c0b0 100644 --- a/lib/cwtch/cwtch.dart +++ b/lib/cwtch/cwtch.dart @@ -46,6 +46,8 @@ abstract class Cwtch { void DownloadFile(String profile, String handle, String filepath, String manifestpath, String filekey); // ignore: non_constant_identifier_names void CreateDownloadableFile(String profile, String handle, String filenameSuggestion, String filekey); + // ignore: non_constant_identifier_names + void CheckDownloadStatus(String profile, String fileKey); // ignore: non_constant_identifier_names void ArchiveConversation(String profile, String handle); diff --git a/lib/cwtch/cwtchNotifier.dart b/lib/cwtch/cwtchNotifier.dart index 818d694b..0a3644ae 100644 --- a/lib/cwtch/cwtchNotifier.dart +++ b/lib/cwtch/cwtchNotifier.dart @@ -330,7 +330,7 @@ class CwtchNotifier { profileCN.getProfile(data["ProfileOnion"])?.downloadUpdate(data["FileKey"], int.parse(data["Progress"])); break; case "FileDownloaded": - profileCN.getProfile(data["ProfileOnion"])?.downloadMarkFinished(data["FileKey"]); + profileCN.getProfile(data["ProfileOnion"])?.downloadMarkFinished(data["FileKey"], data["FilePath"]); break; default: EnvironmentConfig.debugLog("unhandled event: $type"); diff --git a/lib/cwtch/ffi.dart b/lib/cwtch/ffi.dart index 9dfc7328..6b7cb552 100644 --- a/lib/cwtch/ffi.dart +++ b/lib/cwtch/ffi.dart @@ -397,6 +397,19 @@ class CwtchFfi implements Cwtch { // android only - do nothing } + @override + // ignore: non_constant_identifier_names + void CheckDownloadStatus(String profileOnion, String fileKey) { + var checkDownloadStatus = library.lookup>("c_CheckDownloadStatus"); + // ignore: non_constant_identifier_names + final CheckDownloadStatus = checkDownloadStatus.asFunction(); + final u1 = profileOnion.toNativeUtf8(); + final u2 = fileKey.toNativeUtf8(); + CheckDownloadStatus(u1, u1.length, u2, u2.length); + malloc.free(u1); + malloc.free(u2); + } + @override // ignore: non_constant_identifier_names void ResetTor() { diff --git a/lib/cwtch/gomobile.dart b/lib/cwtch/gomobile.dart index 40437cf7..7bab3150 100644 --- a/lib/cwtch/gomobile.dart +++ b/lib/cwtch/gomobile.dart @@ -148,6 +148,12 @@ class CwtchGomobile implements Cwtch { cwtchPlatform.invokeMethod("CreateDownloadableFile", {"ProfileOnion": profileOnion, "handle": contactHandle, "filename": filenameSuggestion, "filekey": filekey}); } + @override + // ignore: non_constant_identifier_names + void CheckDownloadStatus(String profileOnion, String fileKey) { + cwtchPlatform.invokeMethod("CheckDownloadStatus", {"ProfileOnion": profileOnion, "fileKey": fileKey}); + } + @override // ignore: non_constant_identifier_names void ResetTor() { diff --git a/lib/model.dart b/lib/model.dart index 9dcac693..ae1d9c1f 100644 --- a/lib/model.dart +++ b/lib/model.dart @@ -381,14 +381,17 @@ class ProfileInfoState extends ChangeNotifier { } } - void downloadMarkFinished(String fileKey) { + void downloadMarkFinished(String fileKey, String finalPath) { if (!downloadActive(fileKey)) { - print("error: received download completion notice for unknown download "+fileKey); - } else { - this._downloads[fileKey]!.timeEnd = DateTime.now(); - this._downloads[fileKey]!.complete = true; - notifyListeners(); + // happens as a result of a CheckDownloadStatus call, + // invoked from a historical (timeline) download message + // so setting numChunks correctly shouldn't matter + this.downloadInit(fileKey, 1); } + this._downloads[fileKey]!.timeEnd = DateTime.now(); + this._downloads[fileKey]!.downloadedTo = finalPath; + this._downloads[fileKey]!.complete = true; + notifyListeners(); } bool downloadActive(String fileKey) { @@ -407,8 +410,12 @@ class ProfileInfoState extends ChangeNotifier { return this._downloads.containsKey(fileKey) ? this._downloads[fileKey]!.progress() : 0.0; } + String? downloadFinalPath(String fileKey) { + return this._downloads.containsKey(fileKey) ? this._downloads[fileKey]!.downloadedTo : null; + } + String downloadSpeed(String fileKey) { - if (!downloadActive(fileKey)) { + if (!downloadActive(fileKey) || this._downloads[fileKey]!.chunksDownloaded == 0) { return "0 B/s"; } var bytes = this._downloads[fileKey]!.chunksDownloaded * 4096; @@ -425,6 +432,7 @@ class FileDownloadProgress { int chunksTotal = 1; bool complete = false; bool gotManifest = false; + String? downloadedTo; DateTime? timeStart; DateTime? timeEnd; diff --git a/lib/models/message.dart b/lib/models/message.dart index a6c28282..2156b8d6 100644 --- a/lib/models/message.dart +++ b/lib/models/message.dart @@ -35,38 +35,39 @@ Future messageHandler(BuildContext context, String profileOnion, String try { var rawMessageEnvelopeFuture = Provider.of(context, listen: false).cwtch.GetMessage(profileOnion, contactHandle, index); return rawMessageEnvelopeFuture.then((dynamic rawMessageEnvelope) { - dynamic messageWrapper = jsonDecode(rawMessageEnvelope); - // There are 2 conditions in which this error condition can be met: - // 1. The application == nil, in which case this instance of the UI is already - // broken beyond repair, and will either be replaced by a new version, or requires a complete - // restart. - // 2. This index was incremented and we happened to fetch the timeline prior to the messages inclusion. - // This should be rare as Timeline addition/fetching is mutex protected and Dart itself will pipeline the - // calls to libCwtch-go - however because we use goroutines on the backend there is always a chance that one - // will find itself delayed. - // The second case is recoverable by tail-recursing this future. - if (messageWrapper['Message'] == null || messageWrapper['Message'] == '' || messageWrapper['Message'] == '{}') { - return Future.delayed(Duration(seconds: 2), () { - print("Tail recursive call to messageHandler called. This should be a rare event. If you see multiples of this log over a short period of time please log it as a bug."); - return messageHandler(context, profileOnion, contactHandle, index).then((value) => value); - }); - } - - // Construct the initial metadata - var timestamp = DateTime.tryParse(messageWrapper['Timestamp'])!; - var senderHandle = messageWrapper['PeerID']; - var senderImage = messageWrapper['ContactImage']; - var flags = int.parse(messageWrapper['Flags'].toString(), radix: 2); - var ackd = messageWrapper['Acknowledged']; - var error = messageWrapper['Error'] != null; - String? signature; - // If this is a group, store the signature - if (contactHandle.length == GroupConversationHandleLength) { - signature = messageWrapper['Signature']; - } - var metadata = MessageMetadata(profileOnion, contactHandle, index, timestamp, senderHandle, senderImage, signature, flags, ackd, error); - + var metadata = MessageMetadata(profileOnion, contactHandle, index, DateTime.now(), "", "", null, 0, false, true); try { + dynamic messageWrapper = jsonDecode(rawMessageEnvelope); + // There are 2 conditions in which this error condition can be met: + // 1. The application == nil, in which case this instance of the UI is already + // broken beyond repair, and will either be replaced by a new version, or requires a complete + // restart. + // 2. This index was incremented and we happened to fetch the timeline prior to the messages inclusion. + // This should be rare as Timeline addition/fetching is mutex protected and Dart itself will pipeline the + // calls to libCwtch-go - however because we use goroutines on the backend there is always a chance that one + // will find itself delayed. + // The second case is recoverable by tail-recursing this future. + if (messageWrapper['Message'] == null || messageWrapper['Message'] == '' || messageWrapper['Message'] == '{}') { + return Future.delayed(Duration(seconds: 2), () { + print("Tail recursive call to messageHandler called. This should be a rare event. If you see multiples of this log over a short period of time please log it as a bug."); + return messageHandler(context, profileOnion, contactHandle, index).then((value) => value); + }); + } + + // Construct the initial metadata + var timestamp = DateTime.tryParse(messageWrapper['Timestamp'])!; + var senderHandle = messageWrapper['PeerID']; + var senderImage = messageWrapper['ContactImage']; + var flags = int.parse(messageWrapper['Flags'].toString()); + var ackd = messageWrapper['Acknowledged']; + var error = messageWrapper['Error'] != null; + String? signature; + // If this is a group, store the signature + if (contactHandle.length == GroupConversationHandleLength) { + signature = messageWrapper['Signature']; + } + metadata = MessageMetadata(profileOnion, contactHandle, index, timestamp, senderHandle, senderImage, signature, flags, ackd, error); + dynamic message = jsonDecode(messageWrapper['Message']); var content = message['d'] as dynamic; var overlay = int.parse(message['o'].toString()); @@ -86,6 +87,7 @@ Future messageHandler(BuildContext context, String profileOnion, String return MalformedMessage(metadata); } } catch (e) { + print("an error! " + e.toString()); return MalformedMessage(metadata); } }); diff --git a/lib/widgets/filebubble.dart b/lib/widgets/filebubble.dart index 7286f7cf..49aef579 100644 --- a/lib/widgets/filebubble.dart +++ b/lib/widgets/filebubble.dart @@ -39,7 +39,7 @@ class FileBubbleState extends State { @override Widget build(BuildContext context) { var fromMe = Provider.of(context).senderHandle == Provider.of(context).onion; - //isAccepted = Provider.of(context).contactList.getContact(widget.inviteTarget) != null; + var flagStarted = Provider.of(context).flags & 0x02 > 0; var borderRadiousEh = 15.0; var showFileSharing = Provider.of(context).isExperimentEnabled(FileSharingExperiment); var prettyDate = DateFormat.yMd(Platform.localeName).add_jm().format(Provider.of(context).timestamp); @@ -66,27 +66,40 @@ class FileBubbleState extends State { ? senderFileChrome( AppLocalizations.of(context)!.messageFileSent, widget.nameSuggestion, widget.rootHash, widget.fileSize) : (fileChrome(AppLocalizations.of(context)!.messageFileOffered + ":", widget.nameSuggestion, widget.rootHash, widget.fileSize, Provider.of(context).downloadSpeed(widget.fileKey()))); - Widget wdgDecorations; if (!showFileSharing) { wdgDecorations = Text('\u202F'); } else if (fromMe) { wdgDecorations = MessageBubbleDecoration(ackd: Provider.of(context).ackd, errored: Provider.of(context).error, fromMe: fromMe, prettyDate: prettyDate); } else if (Provider.of(context).downloadComplete(widget.fileKey())) { - wdgDecorations = Center( - widthFactor: 1, - child: Wrap(children: [ - Padding(padding: EdgeInsets.all(5), child: ElevatedButton(child: Text(AppLocalizations.of(context)!.openFolderButton + '\u202F'), onPressed: _btnAccept)), - ])); + // in this case, whatever marked download.complete would have also set the path + var path = Provider.of(context).downloadFinalPath(widget.fileKey())!; + wdgDecorations = Text('Saved to: ' + path + '\u202F'); } else if (Provider.of(context).downloadActive(widget.fileKey())) { - if (!Provider.of(context).downloadGotManifest(widget.fileKey())) { - wdgDecorations = Text(AppLocalizations.of(context)!.retrievingManifestMessage + '\u202F'); + if (!Provider.of(context).downloadGotManifest( + widget.fileKey())) { + wdgDecorations = Text( + AppLocalizations.of(context)!.retrievingManifestMessage + '\u202F'); } else { wdgDecorations = LinearProgressIndicator( - value: Provider.of(context).downloadProgress(widget.fileKey()), - color: Provider.of(context).theme.defaultButtonActiveColor(), + value: Provider.of(context).downloadProgress( + widget.fileKey()), + color: Provider + .of(context) + .theme + .defaultButtonActiveColor(), ); } + } else if (flagStarted) { + // in this case, the download was done in a previous application launch, + // so we probably have to request an info lookup + var path = Provider.of(context).downloadFinalPath(widget.fileKey()); + if (path == null) { + wdgDecorations = Text('Checking download status...' + '\u202F'); + Provider.of(context, listen: false).cwtch.CheckDownloadStatus(Provider.of(context, listen: false).onion, widget.fileKey()); + } else { + wdgDecorations = Text('Saved to: ' + (path??"null") + '\u202F'); + } } else { wdgDecorations = Center( widthFactor: 1, @@ -135,10 +148,13 @@ class FileBubbleState extends State { File? file; var profileOnion = Provider.of(context, listen: false).onion; var handle = Provider.of(context, listen: false).senderHandle; + var contact = Provider.of(context, listen: false).onion; + var idx = Provider.of(context, listen: false).messageIndex; if (Platform.isAndroid) { - //todo: would be better to only call downloadInit if CreateDownloadableFile results in a user-pick (they might cancel) Provider.of(context, listen: false).downloadInit(widget.fileKey(), (widget.fileSize / 4096).ceil()); + Provider.of(context, listen: false).cwtch.UpdateMessageFlags(profileOnion, contact, idx, Provider.of(context, listen: false).flags | 0x02); + Provider.of(context, listen: false).flags |= 0x02; Provider.of(context, listen: false).cwtch.CreateDownloadableFile(profileOnion, handle, widget.nameSuggestion, widget.fileKey()); } else { try { @@ -148,6 +164,8 @@ class FileBubbleState extends State { print("saving to " + file.path); var manifestPath = file.path + ".manifest"; Provider.of(context, listen: false).downloadInit(widget.fileKey(), (widget.fileSize / 4096).ceil()); + Provider.of(context, listen: false).cwtch.UpdateMessageFlags(profileOnion, contact, idx, Provider.of(context, listen: false).flags | 0x02); + Provider.of(context, listen: false).flags |= 0x02; Provider.of(context, listen: false).cwtch.DownloadFile(profileOnion, handle, file.path, manifestPath, widget.fileKey()); } } catch (e) { diff --git a/lib/widgets/invitationbubble.dart b/lib/widgets/invitationbubble.dart index 3a9c2585..0e6b9219 100644 --- a/lib/widgets/invitationbubble.dart +++ b/lib/widgets/invitationbubble.dart @@ -131,7 +131,7 @@ class InvitationBubbleState extends State { var contact = Provider.of(context, listen: false).onion; var idx = Provider.of(context, listen: false).messageIndex; Provider.of(context, listen: false).cwtch.UpdateMessageFlags(profileOnion, contact, idx, Provider.of(context, listen: false).flags | 0x01); - Provider.of(context).flags |= 0x01; + Provider.of(context, listen: false).flags |= 0x01; }); } diff --git a/lib/widgets/messagelist.dart b/lib/widgets/messagelist.dart index a6a8353d..ac42a8a7 100644 --- a/lib/widgets/messagelist.dart +++ b/lib/widgets/messagelist.dart @@ -87,7 +87,7 @@ class _MessageListState extends State { // Already includes MessageRow,, return message.getWidget(context); } else { - return MessageLoadingBubble(); + return Text('');//MessageLoadingBubble(); } }, ); From d4aa1cb3971da96c524cfba2325752a9102f466a Mon Sep 17 00:00:00 2001 From: erinn Date: Wed, 29 Sep 2021 17:16:00 -0700 Subject: [PATCH 04/10] android download notification, also fix updatemessageflags didnt work on android --- .../kotlin/im/cwtch/flwtch/FlwtchWorker.kt | 49 +++++++++++++++++++ .../kotlin/im/cwtch/flwtch/MainActivity.kt | 6 +-- lib/cwtch/gomobile.dart | 2 +- lib/widgets/filebubble.dart | 2 +- 4 files changed, 54 insertions(+), 5 deletions(-) diff --git a/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt b/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt index b5727649..d18bb0d9 100644 --- a/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt +++ b/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt @@ -60,6 +60,7 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) : if (Cwtch.startCwtch(appDir, torPath) != 0.toLong()) return Result.failure() Log.i("FlwtchWorker.kt", "startCwtch success, starting coroutine AppbusEvent loop...") + val downloadIDs = mutableMapOf() while(true) { val evt = MainActivity.AppbusEvent(Cwtch.getAppBusEvent()) if (evt.EventType == "NewMessageFromPeer" || evt.EventType == "NewMessageFromGroup") { @@ -97,10 +98,46 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) : .build() notificationManager.notify(getNotificationID(data.getString("ProfileOnion"), handle), newNotification) } + } else if (evt.EventType == "FileDownloadProgressUpdate") { + try { + val data = JSONObject(evt.Data); + val fileKey = data.getString("FileKey"); + val title = data.getString("NameSuggestion"); + val progress = data.getString("Progress").toInt(); + val progressMax = data.getString("FileSizeInChunks").toInt(); + if (!downloadIDs.containsKey(fileKey)) { + downloadIDs.put(fileKey, downloadIDs.count()); + } + var dlID = downloadIDs.get(fileKey); + if (dlID == null) { + dlID = 0; + } + val channelId = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + createDownloadNotificationChannel(fileKey, fileKey) + } else { + // If earlier version channel ID is not used + // https://developer.android.com/reference/android/support/v4/app/NotificationCompat.Builder.html#NotificationCompat.Builder(android.content.Context) + "" + }; + val newNotification = NotificationCompat.Builder(applicationContext, channelId) + .setOngoing(true) + .setContentTitle("Downloading")//todo: translate + .setContentText(title) + .setSmallIcon(android.R.drawable.stat_sys_download) + .setProgress(progressMax, progress, false) + .setSound(null) + //.setSilent(true) + .build(); + notificationManager.notify(dlID, newNotification); + } catch (e: Exception) { + Log.i("FlwtchWorker->FileDownloadProgressUpdate", e.toString() + " :: " + e.getStackTrace()); + } } else if (evt.EventType == "FileDownloaded") { Log.i("FlwtchWorker", "file downloaded!"); val data = JSONObject(evt.Data); val tempFile = data.getString("TempFile"); + val fileKey = data.getString("FileKey"); if (tempFile != "") { val filePath = data.getString("FilePath"); Log.i("FlwtchWorker", "moving "+tempFile+" to "+filePath); @@ -115,6 +152,9 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) : Files.delete(sourcePath); } } + if (downloadIDs.containsKey(fileKey)) { + notificationManager.cancel(downloadIDs.get(fileKey)?:0); + } } Intent().also { intent -> @@ -310,6 +350,15 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) : return channelId } + @RequiresApi(Build.VERSION_CODES.O) + private fun createDownloadNotificationChannel(channelId: String, channelName: String): String{ + val chan = NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_LOW) + chan.lightColor = Color.MAGENTA + chan.lockscreenVisibility = Notification.VISIBILITY_PRIVATE + notificationManager.createNotificationChannel(chan) + return channelId + } + companion object { const val KEY_METHOD = "KEY_METHOD" const val KEY_ARGS = "KEY_ARGS" diff --git a/android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt b/android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt index 8c7d1f47..5ae7408b 100644 --- a/android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt +++ b/android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt @@ -60,6 +60,7 @@ class MainActivity: FlutterActivity() { private var dlToHandle = "" private var dlToFileKey = "" + // handles clicks received from outside the app (ie, notifications) override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) if (notificationClickChannel == null || intent.extras == null) return @@ -81,6 +82,7 @@ class MainActivity: FlutterActivity() { } } + // handles return values from the system file picker override fun onActivityResult(requestCode: Int, result: Int, intent: Intent?) { if (intent == null || intent!!.getData() == null) { Log.i("MainActivity:onActivityResult", "user canceled activity"); @@ -89,15 +91,13 @@ class MainActivity: FlutterActivity() { val filePath = intent!!.getData().toString(); val manifestPath = StringBuilder().append(this.applicationContext.cacheDir).append("/").append(this.dlToFileKey).toString(); - Log.i("onActivityResult", "got download path: " + filePath); - Log.i("onActivityResult", "got manifest path: " + manifestPath); handleCwtch(MethodCall("DownloadFile", mapOf( "ProfileOnion" to this.dlToProfile, "handle" to this.dlToHandle, "filepath" to filePath, "manifestpath" to manifestPath, "filekey" to this.dlToFileKey - )), ErrorLogResult(""));//placeholder; result is never actually invoked + )), ErrorLogResult(""));//placeholder; this Result is never actually invoked } override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { diff --git a/lib/cwtch/gomobile.dart b/lib/cwtch/gomobile.dart index 7bab3150..bf73813e 100644 --- a/lib/cwtch/gomobile.dart +++ b/lib/cwtch/gomobile.dart @@ -198,7 +198,7 @@ class CwtchGomobile implements Cwtch { @override void UpdateMessageFlags(String profile, String handle, int index, int flags) { print("gomobile.dart UpdateMessageFlags " + index.toString()); - cwtchPlatform.invokeMethod("UpdateMessageFlags", {"profile": profile, "contact": handle, "index": index, "flags": flags}); + cwtchPlatform.invokeMethod("UpdateMessageFlags", {"profile": profile, "contact": handle, "midx": index, "flags": flags}); } @override diff --git a/lib/widgets/filebubble.dart b/lib/widgets/filebubble.dart index 49aef579..e58f2400 100644 --- a/lib/widgets/filebubble.dart +++ b/lib/widgets/filebubble.dart @@ -98,7 +98,7 @@ class FileBubbleState extends State { wdgDecorations = Text('Checking download status...' + '\u202F'); Provider.of(context, listen: false).cwtch.CheckDownloadStatus(Provider.of(context, listen: false).onion, widget.fileKey()); } else { - wdgDecorations = Text('Saved to: ' + (path??"null") + '\u202F'); + wdgDecorations = Text('Saved to: ' + path + '\u202F'); } } else { wdgDecorations = Center( From a9cc4b74253a000caa073cd90a8d6222d17d032f Mon Sep 17 00:00:00 2001 From: erinn Date: Wed, 29 Sep 2021 17:18:53 -0700 Subject: [PATCH 05/10] revert accidental add --- android/app/local.properties | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 android/app/local.properties diff --git a/android/app/local.properties b/android/app/local.properties deleted file mode 100644 index 3b474cda..00000000 --- a/android/app/local.properties +++ /dev/null @@ -1,8 +0,0 @@ -## This file must *NOT* be checked into Version Control Systems, -# as it contains information specific to your local configuration. -# -# Location of the SDK. This is only used by Gradle. -# For customization when using a Version Control System, please read the -# header note. -#Fri Jul 02 15:08:54 PDT 2021 -sdk.dir=/home/erinn/Android/Sdk From 01d816209b66553d5692b9b1eeb2c235fbc675b3 Mon Sep 17 00:00:00 2001 From: erinn Date: Wed, 29 Sep 2021 17:20:35 -0700 Subject: [PATCH 06/10] flutter format --- lib/model.dart | 7 ++-- lib/models/messages/filemessage.dart | 2 +- lib/views/messageview.dart | 21 +++++----- lib/widgets/filebubble.dart | 57 ++++++++++++---------------- lib/widgets/messagelist.dart | 2 +- 5 files changed, 39 insertions(+), 50 deletions(-) diff --git a/lib/model.dart b/lib/model.dart index ae1d9c1f..f036be0f 100644 --- a/lib/model.dart +++ b/lib/model.dart @@ -24,7 +24,6 @@ class ChatMessage { }; } - class AppState extends ChangeNotifier { bool cwtchInit = false; bool cwtchIsClosing = false; @@ -365,7 +364,7 @@ class ProfileInfoState extends ChangeNotifier { void downloadUpdate(String fileKey, int progress) { if (!downloadActive(fileKey)) { - print("error: received progress for unknown download "+fileKey); + print("error: received progress for unknown download " + fileKey); } else { this._downloads[fileKey]!.chunksDownloaded = progress; notifyListeners(); @@ -374,7 +373,7 @@ class ProfileInfoState extends ChangeNotifier { void downloadMarkManifest(String fileKey) { if (!downloadActive(fileKey)) { - print("error: received download completion notice for unknown download "+fileKey); + print("error: received download completion notice for unknown download " + fileKey); } else { this._downloads[fileKey]!.gotManifest = true; notifyListeners(); @@ -407,7 +406,7 @@ class ProfileInfoState extends ChangeNotifier { } double downloadProgress(String fileKey) { - return this._downloads.containsKey(fileKey) ? this._downloads[fileKey]!.progress() : 0.0; + return this._downloads.containsKey(fileKey) ? this._downloads[fileKey]!.progress() : 0.0; } String? downloadFinalPath(String fileKey) { diff --git a/lib/models/messages/filemessage.dart b/lib/models/messages/filemessage.dart index a7e1cca0..49b23667 100644 --- a/lib/models/messages/filemessage.dart +++ b/lib/models/messages/filemessage.dart @@ -47,7 +47,7 @@ class FileMessage extends Message { String nameSuggestion = shareObj['n'] as String; String rootHash = shareObj['h'] as String; String nonce = shareObj['n'] as String; - int fileSize = shareObj['s'] as int; + int fileSize = shareObj['s'] as int; return FileBubble(nameSuggestion, rootHash, nonce, fileSize); }); } diff --git a/lib/views/messageview.dart b/lib/views/messageview.dart index 0dcb9e07..21ecbb32 100644 --- a/lib/views/messageview.dart +++ b/lib/views/messageview.dart @@ -89,17 +89,16 @@ class _MessageViewState extends State { onPressed: _showFilePicker, )); appBarButtons.add(IconButton( - icon: Icon(CwtchIcons.send_invite, size: 24), - tooltip: AppLocalizations.of(context)!.sendInvite, - onPressed: () { - _modalSendInvitation(context); - })); + icon: Icon(CwtchIcons.send_invite, size: 24), + tooltip: AppLocalizations.of(context)!.sendInvite, + onPressed: () { + _modalSendInvitation(context); + })); } appBarButtons.add(IconButton( - icon: Provider.of(context, listen: false).isGroup == true ? Icon(CwtchIcons.group_settings_24px) : Icon(CwtchIcons.peer_settings_24px), - tooltip: AppLocalizations.of(context)!.conversationSettings, - onPressed: _pushContactSettings - )); + icon: Provider.of(context, listen: false).isGroup == true ? Icon(CwtchIcons.group_settings_24px) : Icon(CwtchIcons.peer_settings_24px), + tooltip: AppLocalizations.of(context)!.conversationSettings, + onPressed: _pushContactSettings)); var appState = Provider.of(context); return WillPopScope( @@ -374,10 +373,10 @@ class _MessageViewState extends State { void _showFilePicker() async { FilePickerResult? result = await FilePicker.platform.pickFiles(); - if(result != null) { + if (result != null) { File file = File(result.files.first.path); if (file.lengthSync() <= 10737418240) { - print("Sending " +file.path); + print("Sending " + file.path); _sendFile(file.path); } else { print("file size cannot exceed 10 gigabytes"); diff --git a/lib/widgets/filebubble.dart b/lib/widgets/filebubble.dart index e58f2400..166ec164 100644 --- a/lib/widgets/filebubble.dart +++ b/lib/widgets/filebubble.dart @@ -63,9 +63,9 @@ class FileBubbleState extends State { var wdgMessage = !showFileSharing ? Text(AppLocalizations.of(context)!.messageEnableFileSharing) : fromMe - ? senderFileChrome( - AppLocalizations.of(context)!.messageFileSent, widget.nameSuggestion, widget.rootHash, widget.fileSize) - : (fileChrome(AppLocalizations.of(context)!.messageFileOffered + ":", widget.nameSuggestion, widget.rootHash, widget.fileSize, Provider.of(context).downloadSpeed(widget.fileKey()))); + ? senderFileChrome(AppLocalizations.of(context)!.messageFileSent, widget.nameSuggestion, widget.rootHash, widget.fileSize) + : (fileChrome(AppLocalizations.of(context)!.messageFileOffered + ":", widget.nameSuggestion, widget.rootHash, widget.fileSize, + Provider.of(context).downloadSpeed(widget.fileKey()))); Widget wdgDecorations; if (!showFileSharing) { wdgDecorations = Text('\u202F'); @@ -76,18 +76,12 @@ class FileBubbleState extends State { var path = Provider.of(context).downloadFinalPath(widget.fileKey())!; wdgDecorations = Text('Saved to: ' + path + '\u202F'); } else if (Provider.of(context).downloadActive(widget.fileKey())) { - if (!Provider.of(context).downloadGotManifest( - widget.fileKey())) { - wdgDecorations = Text( - AppLocalizations.of(context)!.retrievingManifestMessage + '\u202F'); + if (!Provider.of(context).downloadGotManifest(widget.fileKey())) { + wdgDecorations = Text(AppLocalizations.of(context)!.retrievingManifestMessage + '\u202F'); } else { wdgDecorations = LinearProgressIndicator( - value: Provider.of(context).downloadProgress( - widget.fileKey()), - color: Provider - .of(context) - .theme - .defaultButtonActiveColor(), + value: Provider.of(context).downloadProgress(widget.fileKey()), + color: Provider.of(context).theme.defaultButtonActiveColor(), ); } } else if (flagStarted) { @@ -129,8 +123,7 @@ class FileBubbleState extends State { child: Padding( padding: EdgeInsets.all(9.0), child: Wrap(runAlignment: WrapAlignment.spaceEvenly, alignment: WrapAlignment.spaceEvenly, runSpacing: 1.0, crossAxisAlignment: WrapCrossAlignment.center, children: [ - Center( - widthFactor: 1, child: Padding(padding: EdgeInsets.all(10.0), child: Icon(Icons.attach_file, size: 32))), + Center(widthFactor: 1, child: Padding(padding: EdgeInsets.all(10.0), child: Icon(Icons.attach_file, size: 32))), Center( widthFactor: 1.0, child: Column( @@ -158,16 +151,18 @@ class FileBubbleState extends State { Provider.of(context, listen: false).cwtch.CreateDownloadableFile(profileOnion, handle, widget.nameSuggestion, widget.fileKey()); } else { try { - selectedFileName = await saveFile(defaultFileName: widget.nameSuggestion,); - if (selectedFileName != null) { - file = File(selectedFileName); - print("saving to " + file.path); - var manifestPath = file.path + ".manifest"; - Provider.of(context, listen: false).downloadInit(widget.fileKey(), (widget.fileSize / 4096).ceil()); - Provider.of(context, listen: false).cwtch.UpdateMessageFlags(profileOnion, contact, idx, Provider.of(context, listen: false).flags | 0x02); - Provider.of(context, listen: false).flags |= 0x02; - Provider.of(context, listen: false).cwtch.DownloadFile(profileOnion, handle, file.path, manifestPath, widget.fileKey()); - } + selectedFileName = await saveFile( + defaultFileName: widget.nameSuggestion, + ); + if (selectedFileName != null) { + file = File(selectedFileName); + print("saving to " + file.path); + var manifestPath = file.path + ".manifest"; + Provider.of(context, listen: false).downloadInit(widget.fileKey(), (widget.fileSize / 4096).ceil()); + Provider.of(context, listen: false).cwtch.UpdateMessageFlags(profileOnion, contact, idx, Provider.of(context, listen: false).flags | 0x02); + Provider.of(context, listen: false).flags |= 0x02; + Provider.of(context, listen: false).cwtch.DownloadFile(profileOnion, handle, file.path, manifestPath, widget.fileKey()); + } } catch (e) { print(e); } @@ -176,7 +171,7 @@ class FileBubbleState extends State { // Construct an invite chrome for the sender Widget senderFileChrome(String chrome, String fileName, String rootHash, int fileSize) { - return Wrap(direction: Axis.vertical,children: [ + return Wrap(direction: Axis.vertical, children: [ SelectableText( chrome + '\u202F', style: TextStyle( @@ -220,14 +215,10 @@ class FileBubbleState extends State { Widget fileChrome(String chrome, String fileName, String rootHash, int fileSize, String speed) { var prettyHash = rootHash; if (rootHash.length == 128) { - prettyHash = rootHash.substring(0, 32) + '\n' + - rootHash.substring(32, 64) + '\n' + - rootHash.substring(64, 96) + '\n' + - rootHash.substring(96); + prettyHash = rootHash.substring(0, 32) + '\n' + rootHash.substring(32, 64) + '\n' + rootHash.substring(64, 96) + '\n' + rootHash.substring(96); } - return Wrap(direction: Axis.vertical, - children: [ + return Wrap(direction: Axis.vertical, children: [ SelectableText( chrome + '\u202F', style: TextStyle( @@ -238,7 +229,7 @@ class FileBubbleState extends State { maxLines: 2, ), SelectableText( - AppLocalizations.of(context)!.labelFilename +': ' + fileName + '\u202F', + AppLocalizations.of(context)!.labelFilename + ': ' + fileName + '\u202F', style: TextStyle( color: Provider.of(context).theme.messageFromMeTextColor(), ), diff --git a/lib/widgets/messagelist.dart b/lib/widgets/messagelist.dart index ac42a8a7..f541a12d 100644 --- a/lib/widgets/messagelist.dart +++ b/lib/widgets/messagelist.dart @@ -87,7 +87,7 @@ class _MessageListState extends State { // Already includes MessageRow,, return message.getWidget(context); } else { - return Text('');//MessageLoadingBubble(); + return Text(''); //MessageLoadingBubble(); } }, ); From bf31a2b06219a79789fc873e5e966688bf375772 Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Wed, 29 Sep 2021 13:19:56 -0700 Subject: [PATCH 07/10] Filesharing UI Updates --- lib/cwtch/cwtchNotifier.dart | 4 +- lib/model.dart | 9 +- lib/models/messages/filemessage.dart | 9 +- lib/widgets/filebubble.dart | 185 ++++++++++++++------------- pubspec.lock | 6 +- 5 files changed, 117 insertions(+), 96 deletions(-) diff --git a/lib/cwtch/cwtchNotifier.dart b/lib/cwtch/cwtchNotifier.dart index 0a3644ae..90071154 100644 --- a/lib/cwtch/cwtchNotifier.dart +++ b/lib/cwtch/cwtchNotifier.dart @@ -182,8 +182,10 @@ class CwtchNotifier { if (contactHandle == null || contactHandle == "") contactHandle = data["GroupID"]; profileCN.getProfile(data["Identity"])?.contactList.getContact(contactHandle)!.totalMessages = int.parse(data["Data"]); break; + case "SendMessageToPeerError": + // Ignore + break; case "IndexedFailure": - EnvironmentConfig.debugLog("IndexedFailure"); var idx = data["Index"]; var key = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["RemotePeer"])?.getMessageKey(idx); try { diff --git a/lib/model.dart b/lib/model.dart index f036be0f..ff781d90 100644 --- a/lib/model.dart +++ b/lib/model.dart @@ -382,10 +382,11 @@ class ProfileInfoState extends ChangeNotifier { void downloadMarkFinished(String fileKey, String finalPath) { if (!downloadActive(fileKey)) { - // happens as a result of a CheckDownloadStatus call, - // invoked from a historical (timeline) download message - // so setting numChunks correctly shouldn't matter - this.downloadInit(fileKey, 1); + print("error: received download completion notice for unknown download " + fileKey); + } else { + this._downloads[fileKey]!.timeEnd = DateTime.now(); + this._downloads[fileKey]!.complete = true; + notifyListeners(); } this._downloads[fileKey]!.timeEnd = DateTime.now(); this._downloads[fileKey]!.downloadedTo = finalPath; diff --git a/lib/models/messages/filemessage.dart b/lib/models/messages/filemessage.dart index 49b23667..fa178dd4 100644 --- a/lib/models/messages/filemessage.dart +++ b/lib/models/messages/filemessage.dart @@ -2,7 +2,6 @@ import 'dart:convert'; import 'package:cwtch/models/message.dart'; import 'package:cwtch/widgets/filebubble.dart'; -import 'package:cwtch/widgets/invitationbubble.dart'; import 'package:cwtch/widgets/malformedbubble.dart'; import 'package:cwtch/widgets/messagerow.dart'; import 'package:flutter/widgets.dart'; @@ -48,7 +47,13 @@ class FileMessage extends Message { String rootHash = shareObj['h'] as String; String nonce = shareObj['n'] as String; int fileSize = shareObj['s'] as int; - return FileBubble(nameSuggestion, rootHash, nonce, fileSize); + return FileBubble( + nameSuggestion, + rootHash, + nonce, + fileSize, + interactive: false, + ); }); } diff --git a/lib/widgets/filebubble.dart b/lib/widgets/filebubble.dart index 166ec164..1366c9c6 100644 --- a/lib/widgets/filebubble.dart +++ b/lib/widgets/filebubble.dart @@ -6,6 +6,7 @@ import 'package:cwtch/models/message.dart'; import 'package:cwtch/widgets/malformedbubble.dart'; import 'package:file_picker/file_picker.dart' as androidPicker; import 'package:file_picker_desktop/file_picker_desktop.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; @@ -24,8 +25,9 @@ class FileBubble extends StatefulWidget { final String rootHash; final String nonce; final int fileSize; + final bool interactive; - FileBubble(this.nameSuggestion, this.rootHash, this.nonce, this.fileSize); + FileBubble(this.nameSuggestion, this.rootHash, this.nonce, this.fileSize, {this.interactive = true}); @override FileBubbleState createState() => FileBubbleState(); @@ -122,15 +124,16 @@ class FileBubbleState extends State { widthFactor: 1.0, child: Padding( padding: EdgeInsets.all(9.0), - child: Wrap(runAlignment: WrapAlignment.spaceEvenly, alignment: WrapAlignment.spaceEvenly, runSpacing: 1.0, crossAxisAlignment: WrapCrossAlignment.center, children: [ - Center(widthFactor: 1, child: Padding(padding: EdgeInsets.all(10.0), child: Icon(Icons.attach_file, size: 32))), + child: Wrap(alignment: WrapAlignment.start, children: [ Center( widthFactor: 1.0, child: Column( crossAxisAlignment: fromMe ? CrossAxisAlignment.end : CrossAxisAlignment.start, mainAxisAlignment: fromMe ? MainAxisAlignment.end : MainAxisAlignment.start, mainAxisSize: MainAxisSize.min, - children: fromMe ? [wdgMessage, wdgDecorations] : [wdgSender, wdgMessage, wdgDecorations]), + children: fromMe + ? [wdgMessage, Visibility(visible: widget.interactive, child: wdgDecorations)] + : [wdgSender, wdgMessage, Visibility(visible: widget.interactive, child: wdgDecorations)]), ) ]))))); }); @@ -171,99 +174,109 @@ class FileBubbleState extends State { // Construct an invite chrome for the sender Widget senderFileChrome(String chrome, String fileName, String rootHash, int fileSize) { - return Wrap(direction: Axis.vertical, children: [ - SelectableText( - chrome + '\u202F', - style: TextStyle( - color: Provider.of(context).theme.messageFromMeTextColor(), + return ListTile( + visualDensity: VisualDensity.compact, + title: Wrap(direction: Axis.horizontal, alignment: WrapAlignment.start, children: [ + SelectableText( + chrome + '\u202F', + style: TextStyle( + color: Provider.of(context).theme.messageFromMeTextColor(), + ), + textAlign: TextAlign.left, + maxLines: 2, + textWidthBasis: TextWidthBasis.longestLine, + ), + SelectableText( + fileName + '\u202F', + style: TextStyle( + color: Provider.of(context).theme.messageFromMeTextColor(), + fontWeight: FontWeight.bold, + overflow: TextOverflow.ellipsis, + ), + textAlign: TextAlign.left, + textWidthBasis: TextWidthBasis.parent, + maxLines: 2, + ), + SelectableText( + prettyBytes(fileSize) + '\u202F' + '\n', + style: TextStyle( + color: Provider.of(context).theme.messageFromMeTextColor(), + ), + textAlign: TextAlign.left, + maxLines: 2, + ) + ]), + subtitle: SelectableText( + 'sha512: ' + rootHash + '\u202F', + style: TextStyle( + color: Provider.of(context).theme.messageFromMeTextColor(), + fontSize: 10, + fontFamily: "monospace", + ), + textAlign: TextAlign.left, + maxLines: 4, + textWidthBasis: TextWidthBasis.parent, ), - textAlign: TextAlign.left, - maxLines: 2, - textWidthBasis: TextWidthBasis.longestLine, - ), - SelectableText( - fileName + '\u202F', - style: TextStyle( - color: Provider.of(context).theme.messageFromMeTextColor(), - ), - textAlign: TextAlign.left, - maxLines: 2, - textWidthBasis: TextWidthBasis.longestLine, - ), - SelectableText( - prettyBytes(fileSize) + '\u202F', - style: TextStyle( - color: Provider.of(context).theme.messageFromMeTextColor(), - ), - textAlign: TextAlign.left, - maxLines: 2, - textWidthBasis: TextWidthBasis.longestLine, - ), - SelectableText( - 'sha512: ' + rootHash + '\u202F', - style: TextStyle( - color: Provider.of(context).theme.messageFromMeTextColor(), - ), - textAlign: TextAlign.left, - maxLines: 2, - textWidthBasis: TextWidthBasis.longestLine, - ), - ]); + leading: Icon(Icons.attach_file, size: 32, color: Provider.of(context).theme.messageFromMeTextColor())); } // Construct an invite chrome Widget fileChrome(String chrome, String fileName, String rootHash, int fileSize, String speed) { - var prettyHash = rootHash; - if (rootHash.length == 128) { - prettyHash = rootHash.substring(0, 32) + '\n' + rootHash.substring(32, 64) + '\n' + rootHash.substring(64, 96) + '\n' + rootHash.substring(96); - } - - return Wrap(direction: Axis.vertical, children: [ - SelectableText( - chrome + '\u202F', - style: TextStyle( - color: Provider.of(context).theme.messageFromOtherTextColor(), + return ListTile( + visualDensity: VisualDensity.compact, + title: Wrap(direction: Axis.horizontal, alignment: WrapAlignment.start, children: [ + SelectableText( + chrome + '\u202F', + style: TextStyle( + color: Provider.of(context).theme.messageFromOtherTextColor(), + ), + textAlign: TextAlign.left, + maxLines: 2, + textWidthBasis: TextWidthBasis.longestLine, ), - textAlign: TextAlign.left, - textWidthBasis: TextWidthBasis.longestLine, - maxLines: 2, - ), - SelectableText( - AppLocalizations.of(context)!.labelFilename + ': ' + fileName + '\u202F', - style: TextStyle( - color: Provider.of(context).theme.messageFromMeTextColor(), - ), - textAlign: TextAlign.left, - maxLines: 2, - textWidthBasis: TextWidthBasis.longestLine, - ), - SelectableText( - AppLocalizations.of(context)!.labelFilesize + ': ' + prettyBytes(fileSize) + '\u202F', - style: TextStyle( - color: Provider.of(context).theme.messageFromMeTextColor(), - ), - textAlign: TextAlign.left, - maxLines: 2, - textWidthBasis: TextWidthBasis.longestLine, - ), - SelectableText( - 'sha512: ' + prettyHash + '\u202F', + SelectableText( + fileName + '\u202F', + style: TextStyle( + color: Provider.of(context).theme.messageFromOtherTextColor(), + fontWeight: FontWeight.bold, + overflow: TextOverflow.ellipsis, + ), + textAlign: TextAlign.left, + textWidthBasis: TextWidthBasis.parent, + maxLines: 2, + ), + SelectableText( + AppLocalizations.of(context)!.labelFilesize + ': ' + prettyBytes(fileSize) + '\u202F' + '\n', + style: TextStyle( + color: Provider.of(context).theme.messageFromOtherTextColor(), + ), + textAlign: TextAlign.left, + maxLines: 2, + ) + ]), + subtitle: SelectableText( + 'sha512: ' + rootHash + '\u202F', style: TextStyle( color: Provider.of(context).theme.messageFromMeTextColor(), + fontSize: 10, + fontFamily: "monospace", ), textAlign: TextAlign.left, maxLines: 4, - textWidthBasis: TextWidthBasis.longestLine, + textWidthBasis: TextWidthBasis.parent, ), - SelectableText( - speed + '\u202F', - style: TextStyle( - color: Provider.of(context).theme.messageFromMeTextColor(), - ), - textAlign: TextAlign.left, - maxLines: 1, - textWidthBasis: TextWidthBasis.longestLine, - ), - ]); + leading: Icon(Icons.attach_file, size: 32, color: Provider.of(context).theme.messageFromOtherTextColor()), + trailing: Visibility( + visible: speed != "0 B/s", + child: SelectableText( + speed + '\u202F', + style: TextStyle( + color: Provider.of(context).theme.messageFromMeTextColor(), + ), + textAlign: TextAlign.left, + maxLines: 1, + textWidthBasis: TextWidthBasis.longestLine, + )), + ); } } diff --git a/pubspec.lock b/pubspec.lock index 41cdef38..ca0185ec 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -21,7 +21,7 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.8.2" + version: "2.8.1" boolean_selector: dependency: transitive description: @@ -195,7 +195,7 @@ packages: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.11" + version: "0.12.10" meta: dependency: transitive description: @@ -403,7 +403,7 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.4.3" + version: "0.4.2" typed_data: dependency: transitive description: From 3240e41d494716e2f66791e0dad5d0ff65270339 Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Thu, 30 Sep 2021 10:50:27 -0700 Subject: [PATCH 08/10] Upgrade libCwtch-go --- LIBCWTCH-GO.version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LIBCWTCH-GO.version b/LIBCWTCH-GO.version index 5e60f32c..c0a8e890 100644 --- a/LIBCWTCH-GO.version +++ b/LIBCWTCH-GO.version @@ -1 +1 @@ -v1.2.1-2-ga8e7bba-2021-09-14-21-04 +v1.2.1-7-g1d18559-2021-09-30-17-39 From efbf7f5bff4f3a397ec431b04de4600b01191266 Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Thu, 30 Sep 2021 10:53:32 -0700 Subject: [PATCH 09/10] Small file sharing cleanup --- lib/views/messageview.dart | 2 ++ lib/widgets/filebubble.dart | 9 ++------- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/lib/views/messageview.dart b/lib/views/messageview.dart index 21ecbb32..d279db17 100644 --- a/lib/views/messageview.dart +++ b/lib/views/messageview.dart @@ -375,6 +375,8 @@ class _MessageViewState extends State { FilePickerResult? result = await FilePicker.platform.pickFiles(); if (result != null) { File file = File(result.files.first.path); + // We have a maximum number of bytes we can represent in terms of + // a manifest (see : https://git.openprivacy.ca/cwtch.im/cwtch/src/branch/master/protocol/files/manifest.go#L25) if (file.lengthSync() <= 10737418240) { print("Sending " + file.path); _sendFile(file.path); diff --git a/lib/widgets/filebubble.dart b/lib/widgets/filebubble.dart index 1366c9c6..496e0924 100644 --- a/lib/widgets/filebubble.dart +++ b/lib/widgets/filebubble.dart @@ -1,14 +1,9 @@ -import 'dart:convert'; import 'dart:io'; -import 'package:cwtch/cwtch_icons_icons.dart'; import 'package:cwtch/models/message.dart'; -import 'package:cwtch/widgets/malformedbubble.dart'; -import 'package:file_picker/file_picker.dart' as androidPicker; import 'package:file_picker_desktop/file_picker_desktop.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import '../main.dart'; import '../model.dart'; @@ -172,7 +167,7 @@ class FileBubbleState extends State { } } - // Construct an invite chrome for the sender + // Construct an file chrome for the sender Widget senderFileChrome(String chrome, String fileName, String rootHash, int fileSize) { return ListTile( visualDensity: VisualDensity.compact, @@ -220,7 +215,7 @@ class FileBubbleState extends State { leading: Icon(Icons.attach_file, size: 32, color: Provider.of(context).theme.messageFromMeTextColor())); } - // Construct an invite chrome + // Construct an file chrome Widget fileChrome(String chrome, String fileName, String rootHash, int fileSize, String speed) { return ListTile( visualDensity: VisualDensity.compact, From 9556150c055d100860faa90b226c908061937143 Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Thu, 30 Sep 2021 13:29:52 -0700 Subject: [PATCH 10/10] Upgrade libCwtch-go + Fix Model merge issue --- LIBCWTCH-GO.version | 2 +- lib/model.dart | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/LIBCWTCH-GO.version b/LIBCWTCH-GO.version index c0a8e890..fc6ed486 100644 --- a/LIBCWTCH-GO.version +++ b/LIBCWTCH-GO.version @@ -1 +1 @@ -v1.2.1-7-g1d18559-2021-09-30-17-39 +v1.3.0-2021-09-30-20-24 \ No newline at end of file diff --git a/lib/model.dart b/lib/model.dart index ff781d90..f036be0f 100644 --- a/lib/model.dart +++ b/lib/model.dart @@ -382,11 +382,10 @@ class ProfileInfoState extends ChangeNotifier { void downloadMarkFinished(String fileKey, String finalPath) { if (!downloadActive(fileKey)) { - print("error: received download completion notice for unknown download " + fileKey); - } else { - this._downloads[fileKey]!.timeEnd = DateTime.now(); - this._downloads[fileKey]!.complete = true; - notifyListeners(); + // happens as a result of a CheckDownloadStatus call, + // invoked from a historical (timeline) download message + // so setting numChunks correctly shouldn't matter + this.downloadInit(fileKey, 1); } this._downloads[fileKey]!.timeEnd = DateTime.now(); this._downloads[fileKey]!.downloadedTo = finalPath;