filesharing wip
This commit is contained in:
parent
7737085a28
commit
78ea12dff3
|
@ -40,6 +40,13 @@ abstract class Cwtch {
|
||||||
// ignore: non_constant_identifier_names
|
// ignore: non_constant_identifier_names
|
||||||
void SendInvitation(String profile, String handle, String target);
|
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
|
// ignore: non_constant_identifier_names
|
||||||
void ArchiveConversation(String profile, String handle);
|
void ArchiveConversation(String profile, String handle);
|
||||||
// ignore: non_constant_identifier_names
|
// ignore: non_constant_identifier_names
|
||||||
|
|
|
@ -321,6 +321,15 @@ class CwtchNotifier {
|
||||||
EnvironmentConfig.debugLog("unhandled peer attribute event: ${data['Path']}");
|
EnvironmentConfig.debugLog("unhandled peer attribute event: ${data['Path']}");
|
||||||
}
|
}
|
||||||
break;
|
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:
|
default:
|
||||||
EnvironmentConfig.debugLog("unhandled event: $type");
|
EnvironmentConfig.debugLog("unhandled event: $type");
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,6 +36,9 @@ typedef VoidFromStringStringStringFn = void Function(Pointer<Utf8>, int, Pointer
|
||||||
typedef void_from_string_string_string_string_function = Void Function(Pointer<Utf8>, Int32, Pointer<Utf8>, Int32, Pointer<Utf8>, Int32, Pointer<Utf8>, Int32);
|
typedef void_from_string_string_string_string_function = Void Function(Pointer<Utf8>, Int32, Pointer<Utf8>, Int32, Pointer<Utf8>, Int32, Pointer<Utf8>, Int32);
|
||||||
typedef VoidFromStringStringStringStringFn = void Function(Pointer<Utf8>, int, Pointer<Utf8>, int, Pointer<Utf8>, int, Pointer<Utf8>, int);
|
typedef VoidFromStringStringStringStringFn = void Function(Pointer<Utf8>, int, Pointer<Utf8>, int, Pointer<Utf8>, int, Pointer<Utf8>, int);
|
||||||
|
|
||||||
|
typedef void_from_string_string_string_string_string_function = Void Function(Pointer<Utf8>, Int32, Pointer<Utf8>, Int32, Pointer<Utf8>, Int32, Pointer<Utf8>, Int32, Pointer<Utf8>, Int32);
|
||||||
|
typedef VoidFromStringStringStringStringStringFn = void Function(Pointer<Utf8>, int, Pointer<Utf8>, int, Pointer<Utf8>, int, Pointer<Utf8>, int, Pointer<Utf8>, int);
|
||||||
|
|
||||||
typedef void_from_string_string_int_int_function = Void Function(Pointer<Utf8>, Int32, Pointer<Utf8>, Int32, Int64, Int64);
|
typedef void_from_string_string_int_int_function = Void Function(Pointer<Utf8>, Int32, Pointer<Utf8>, Int32, Int64, Int64);
|
||||||
typedef VoidFromStringStringIntIntFn = void Function(Pointer<Utf8>, int, Pointer<Utf8>, int, int, int);
|
typedef VoidFromStringStringIntIntFn = void Function(Pointer<Utf8>, int, Pointer<Utf8>, int, int, int);
|
||||||
|
|
||||||
|
@ -325,6 +328,46 @@ class CwtchFfi implements Cwtch {
|
||||||
malloc.free(u3);
|
malloc.free(u3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
// ignore: non_constant_identifier_names
|
||||||
|
void ShareFile(String profileOnion, String contactHandle, String filepath) {
|
||||||
|
var shareFile = library.lookup<NativeFunction<void_from_string_string_string_function>>("c_ShareFile");
|
||||||
|
// ignore: non_constant_identifier_names
|
||||||
|
final ShareFile = shareFile.asFunction<VoidFromStringStringStringFn>();
|
||||||
|
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<NativeFunction<void_from_string_string_string_string_string_function>>("c_DownloadFile");
|
||||||
|
// ignore: non_constant_identifier_names
|
||||||
|
final DownloadFile = dlFile.asFunction<VoidFromStringStringStringStringStringFn>();
|
||||||
|
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
|
@override
|
||||||
// ignore: non_constant_identifier_names
|
// ignore: non_constant_identifier_names
|
||||||
void ResetTor() {
|
void ResetTor() {
|
||||||
|
|
|
@ -131,6 +131,23 @@ class CwtchGomobile implements Cwtch {
|
||||||
cwtchPlatform.invokeMethod("SendInvitation", {"ProfileOnion": profileOnion, "handle": contactHandle, "target": target});
|
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
|
@override
|
||||||
// ignore: non_constant_identifier_names
|
// ignore: non_constant_identifier_names
|
||||||
void ResetTor() {
|
void ResetTor() {
|
||||||
|
|
|
@ -24,6 +24,7 @@ class ChatMessage {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class AppState extends ChangeNotifier {
|
class AppState extends ChangeNotifier {
|
||||||
bool cwtchInit = false;
|
bool cwtchInit = false;
|
||||||
bool cwtchIsClosing = false;
|
bool cwtchIsClosing = false;
|
||||||
|
@ -204,6 +205,7 @@ class ProfileInfoState extends ChangeNotifier {
|
||||||
String _imagePath = "";
|
String _imagePath = "";
|
||||||
int _unreadMessages = 0;
|
int _unreadMessages = 0;
|
||||||
bool _online = false;
|
bool _online = false;
|
||||||
|
Map<String, FileDownloadProgress> _downloads = Map<String, FileDownloadProgress>();
|
||||||
|
|
||||||
// assume profiles are encrypted...this will be set to false
|
// assume profiles are encrypted...this will be set to false
|
||||||
// in the constructor if the profile is encrypted with the defacto password.
|
// 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 }
|
enum ContactAuthorization { unknown, approved, blocked }
|
||||||
|
|
|
@ -4,6 +4,7 @@ import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import '../main.dart';
|
import '../main.dart';
|
||||||
import '../model.dart';
|
import '../model.dart';
|
||||||
|
import 'messages/filemessage.dart';
|
||||||
import 'messages/invitemessage.dart';
|
import 'messages/invitemessage.dart';
|
||||||
import 'messages/malformedmessage.dart';
|
import 'messages/malformedmessage.dart';
|
||||||
import 'messages/quotedmessage.dart';
|
import 'messages/quotedmessage.dart';
|
||||||
|
@ -14,6 +15,7 @@ const TextMessageOverlay = 1;
|
||||||
const QuotedMessageOverlay = 10;
|
const QuotedMessageOverlay = 10;
|
||||||
const SuggestContactOverlay = 100;
|
const SuggestContactOverlay = 100;
|
||||||
const InviteGroupOverlay = 101;
|
const InviteGroupOverlay = 101;
|
||||||
|
const FileShareOverlay = 200;
|
||||||
|
|
||||||
// Defines the length of the tor v3 onion address. Code using this constant will
|
// 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
|
// need to updated when we allow multiple different identifiers. At which time
|
||||||
|
@ -77,6 +79,8 @@ Future<Message> messageHandler(BuildContext context, String profileOnion, String
|
||||||
return InviteMessage(overlay, metadata, content);
|
return InviteMessage(overlay, metadata, content);
|
||||||
case QuotedMessageOverlay:
|
case QuotedMessageOverlay:
|
||||||
return QuotedMessage(metadata, content);
|
return QuotedMessage(metadata, content);
|
||||||
|
case FileShareOverlay:
|
||||||
|
return FileMessage(metadata, content);
|
||||||
default:
|
default:
|
||||||
// Metadata is valid, content is not..
|
// Metadata is valid, content is not..
|
||||||
return MalformedMessage(metadata);
|
return MalformedMessage(metadata);
|
||||||
|
|
|
@ -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<ContactInfoState>(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<ContactInfoState>(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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,6 +9,7 @@ import 'opaque.dart';
|
||||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
|
|
||||||
const TapirGroupsExperiment = "tapir-groups-experiment";
|
const TapirGroupsExperiment = "tapir-groups-experiment";
|
||||||
|
const FileSharingExperiment = "filesharing";
|
||||||
|
|
||||||
enum DualpaneMode {
|
enum DualpaneMode {
|
||||||
Single,
|
Single,
|
||||||
|
|
|
@ -175,6 +175,22 @@ class _GlobalSettingsViewState extends State<GlobalSettingsView> {
|
||||||
inactiveTrackColor: settings.theme.defaultButtonDisabledColor(),
|
inactiveTrackColor: settings.theme.defaultButtonDisabledColor(),
|
||||||
secondary: Icon(CwtchIcons.enable_groups, color: settings.current().mainTextColor()),
|
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(
|
AboutListTile(
|
||||||
|
|
|
@ -6,6 +6,9 @@ import 'package:cwtch/models/message.dart';
|
||||||
import 'package:cwtch/models/messages/quotedmessage.dart';
|
import 'package:cwtch/models/messages/quotedmessage.dart';
|
||||||
import 'package:cwtch/widgets/messageloadingbubble.dart';
|
import 'package:cwtch/widgets/messageloadingbubble.dart';
|
||||||
import 'package:cwtch/widgets/profileimage.dart';
|
import 'package:cwtch/widgets/profileimage.dart';
|
||||||
|
|
||||||
|
import 'package:file_picker/file_picker.dart';
|
||||||
|
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:cwtch/views/peersettingsview.dart';
|
import 'package:cwtch/views/peersettingsview.dart';
|
||||||
|
@ -14,6 +17,7 @@ import 'package:flutter/services.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||||
|
import 'package:path/path.dart' show basename;
|
||||||
|
|
||||||
import '../main.dart';
|
import '../main.dart';
|
||||||
import '../model.dart';
|
import '../model.dart';
|
||||||
|
@ -74,6 +78,26 @@ class _MessageViewState extends State<MessageView> {
|
||||||
return Card(child: Center(child: Text(AppLocalizations.of(context)!.addContactFirst)));
|
return Card(child: Center(child: Text(AppLocalizations.of(context)!.addContactFirst)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var appBarButtons = <Widget>[];
|
||||||
|
if (Provider.of<ContactInfoState>(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<ContactInfoState>(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<AppState>(context);
|
var appState = Provider.of<AppState>(context);
|
||||||
return WillPopScope(
|
return WillPopScope(
|
||||||
onWillPop: _onWillPop,
|
onWillPop: _onWillPop,
|
||||||
|
@ -105,21 +129,7 @@ class _MessageViewState extends State<MessageView> {
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
))
|
))
|
||||||
]),
|
]),
|
||||||
actions: [
|
actions: appBarButtons,
|
||||||
//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<ContactInfoState>(context, listen: false).isGroup == true ? Icon(CwtchIcons.group_settings_24px) : Icon(CwtchIcons.peer_settings_24px),
|
|
||||||
tooltip: AppLocalizations.of(context)!.conversationSettings,
|
|
||||||
onPressed: _pushContactSettings),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
body: Padding(padding: EdgeInsets.fromLTRB(8.0, 8.0, 8.0, 108.0), child: MessageList(scrollController, scrollListener)),
|
body: Padding(padding: EdgeInsets.fromLTRB(8.0, 8.0, 8.0, 108.0), child: MessageList(scrollController, scrollListener)),
|
||||||
bottomSheet: _buildComposeBox(),
|
bottomSheet: _buildComposeBox(),
|
||||||
|
@ -189,6 +199,13 @@ class _MessageViewState extends State<MessageView> {
|
||||||
_sendMessageHelper();
|
_sendMessageHelper();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _sendFile(String filePath) {
|
||||||
|
Provider.of<FlwtchState>(context, listen: false)
|
||||||
|
.cwtch
|
||||||
|
.ShareFile(Provider.of<ContactInfoState>(context, listen: false).profileOnion, Provider.of<ContactInfoState>(context, listen: false).onion, filePath);
|
||||||
|
_sendMessageHelper();
|
||||||
|
}
|
||||||
|
|
||||||
void _sendMessageHelper() {
|
void _sendMessageHelper() {
|
||||||
ctrlrCompose.clear();
|
ctrlrCompose.clear();
|
||||||
focusNode.requestFocus();
|
focusNode.requestFocus();
|
||||||
|
@ -342,4 +359,18 @@ class _MessageViewState extends State<MessageView> {
|
||||||
));
|
));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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<FileBubble> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var fromMe = Provider.of<MessageMetadata>(context).senderHandle == Provider.of<ProfileInfoState>(context).onion;
|
||||||
|
//isAccepted = Provider.of<ProfileInfoState>(context).contactList.getContact(widget.inviteTarget) != null;
|
||||||
|
var borderRadiousEh = 15.0;
|
||||||
|
var showFileSharing = Provider.of<Settings>(context).isExperimentEnabled(FileSharingExperiment);
|
||||||
|
var prettyDate = DateFormat.yMd(Platform.localeName).add_jm().format(Provider.of<MessageMetadata>(context).timestamp);
|
||||||
|
|
||||||
|
// If the sender is not us, then we want to give them a nickname...
|
||||||
|
var senderDisplayStr = "";
|
||||||
|
if (!fromMe) {
|
||||||
|
ContactInfoState? contact = Provider.of<ProfileInfoState>(context).contactList.getContact(Provider.of<MessageMetadata>(context).senderHandle);
|
||||||
|
if (contact != null) {
|
||||||
|
senderDisplayStr = contact.nickname;
|
||||||
|
} else {
|
||||||
|
senderDisplayStr = Provider.of<MessageMetadata>(context).senderHandle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var wdgSender = Center(
|
||||||
|
widthFactor: 1,
|
||||||
|
child: SelectableText(senderDisplayStr + '\u202F',
|
||||||
|
style: TextStyle(fontSize: 9.0, color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeTextColor() : Provider.of<Settings>(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<MessageMetadata>(context).ackd, errored: Provider.of<MessageMetadata>(context).error, fromMe: fromMe, prettyDate: prettyDate);
|
||||||
|
} else if (Provider.of<ProfileInfoState>(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<ProfileInfoState>(context).downloadActive(widget.fileKey())) {
|
||||||
|
if (!Provider.of<ProfileInfoState>(context).downloadGotManifest(widget.fileKey())) {
|
||||||
|
wdgDecorations = Text(AppLocalizations.of(context)!.retrievingManifestMessage + '\u202F');
|
||||||
|
} else {
|
||||||
|
wdgDecorations = LinearProgressIndicator(
|
||||||
|
value: Provider.of<ProfileInfoState>(context).downloadProgress(widget.fileKey()),
|
||||||
|
color: Provider.of<Settings>(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<Settings>(context).theme.messageFromMeBackgroundColor() : Provider.of<Settings>(context).theme.messageFromOtherBackgroundColor(),
|
||||||
|
border:
|
||||||
|
Border.all(color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeBackgroundColor() : Provider.of<Settings>(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<ProfileInfoState>(context, listen: false).onion;
|
||||||
|
var handle = Provider.of<MessageMetadata>(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<ProfileInfoState>(context, listen: false).downloadInit(widget.fileKey(), (widget.fileSize / 4096).ceil());
|
||||||
|
Provider.of<FlwtchState>(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<FlwtchState>(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<Settings>(context).theme.messageFromMeTextColor(),
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.left,
|
||||||
|
maxLines: 2,
|
||||||
|
textWidthBasis: TextWidthBasis.longestLine,
|
||||||
|
),
|
||||||
|
SelectableText(
|
||||||
|
fileName + '\u202F',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Provider.of<Settings>(context).theme.messageFromMeTextColor(),
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.left,
|
||||||
|
maxLines: 2,
|
||||||
|
textWidthBasis: TextWidthBasis.longestLine,
|
||||||
|
),
|
||||||
|
SelectableText(
|
||||||
|
fileSize.toString() + 'B\u202F',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Provider.of<Settings>(context).theme.messageFromMeTextColor(),
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.left,
|
||||||
|
maxLines: 2,
|
||||||
|
textWidthBasis: TextWidthBasis.longestLine,
|
||||||
|
),
|
||||||
|
SelectableText(
|
||||||
|
'sha512: ' + rootHash + '\u202F',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Provider.of<Settings>(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<Settings>(context).theme.messageFromOtherTextColor(),
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.left,
|
||||||
|
textWidthBasis: TextWidthBasis.longestLine,
|
||||||
|
maxLines: 2,
|
||||||
|
),
|
||||||
|
SelectableText(
|
||||||
|
AppLocalizations.of(context)!.labelFilename +': ' + fileName + '\u202F',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Provider.of<Settings>(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<Settings>(context).theme.messageFromMeTextColor(),
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.left,
|
||||||
|
maxLines: 2,
|
||||||
|
textWidthBasis: TextWidthBasis.longestLine,
|
||||||
|
),
|
||||||
|
SelectableText(
|
||||||
|
'sha512: ' + prettyHash + '\u202F',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Provider.of<Settings>(context).theme.messageFromMeTextColor(),
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.left,
|
||||||
|
maxLines: 4,
|
||||||
|
textWidthBasis: TextWidthBasis.longestLine,
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue