Merge pull request 'filesharing' (#188) from filesharing into trunk
continuous-integration/drone/push Build is passing
Details
continuous-integration/drone/push Build is passing
Details
Reviewed-on: #188 Reviewed-by: Dan Ballard <dan@openprivacy.ca>
This commit is contained in:
commit
0c81b4f9d0
|
@ -1 +1 @@
|
||||||
v1.2.1-2-ga8e7bba-2021-09-14-21-04
|
v1.3.0-2021-09-30-20-24
|
|
@ -33,7 +33,7 @@ if (keystorePropertiesFile.exists()) {
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 29
|
compileSdkVersion 30
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
main.java.srcDirs += 'src/main/kotlin'
|
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).
|
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||||
applicationId "im.cwtch.flwtch"
|
applicationId "im.cwtch.flwtch"
|
||||||
minSdkVersion 16
|
minSdkVersion 16
|
||||||
targetSdkVersion 29
|
targetSdkVersion 30
|
||||||
versionCode flutterVersionCode.toInteger()
|
versionCode flutterVersionCode.toInteger()
|
||||||
versionName flutterVersionName
|
versionName flutterVersionName
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,8 @@
|
||||||
android:name="io.flutter.app.FlutterApplication"
|
android:name="io.flutter.app.FlutterApplication"
|
||||||
android:label="Cwtch"
|
android:label="Cwtch"
|
||||||
android:extractNativeLibs="true"
|
android:extractNativeLibs="true"
|
||||||
android:icon="@mipmap/knott">
|
android:icon="@mipmap/knott"
|
||||||
|
android:requestLegacyExternalStorage="true">
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:launchMode="singleTop"
|
android:launchMode="singleTop"
|
||||||
|
@ -48,4 +49,10 @@
|
||||||
|
|
||||||
<!--Meeded to check if activity is foregrounded or if messages from the service should be queued-->
|
<!--Meeded to check if activity is foregrounded or if messages from the service should be queued-->
|
||||||
<uses-permission android:name="android.permission.GET_TASKS" />
|
<uses-permission android:name="android.permission.GET_TASKS" />
|
||||||
|
|
||||||
|
<!--Needed to download files-->
|
||||||
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||||
|
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
|
||||||
|
<uses-permission android:name="android.permission.PERMISSIONS_STORAGE" />
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|
|
@ -15,6 +15,10 @@ import cwtch.Cwtch
|
||||||
import io.flutter.FlutterInjector
|
import io.flutter.FlutterInjector
|
||||||
import org.json.JSONObject
|
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) :
|
class FlwtchWorker(context: Context, parameters: WorkerParameters) :
|
||||||
CoroutineWorker(context, parameters) {
|
CoroutineWorker(context, parameters) {
|
||||||
|
@ -56,6 +60,7 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) :
|
||||||
if (Cwtch.startCwtch(appDir, torPath) != 0.toLong()) return Result.failure()
|
if (Cwtch.startCwtch(appDir, torPath) != 0.toLong()) return Result.failure()
|
||||||
|
|
||||||
Log.i("FlwtchWorker.kt", "startCwtch success, starting coroutine AppbusEvent loop...")
|
Log.i("FlwtchWorker.kt", "startCwtch success, starting coroutine AppbusEvent loop...")
|
||||||
|
val downloadIDs = mutableMapOf<String, Int>()
|
||||||
while(true) {
|
while(true) {
|
||||||
val evt = MainActivity.AppbusEvent(Cwtch.getAppBusEvent())
|
val evt = MainActivity.AppbusEvent(Cwtch.getAppBusEvent())
|
||||||
if (evt.EventType == "NewMessageFromPeer" || evt.EventType == "NewMessageFromGroup") {
|
if (evt.EventType == "NewMessageFromPeer" || evt.EventType == "NewMessageFromGroup") {
|
||||||
|
@ -93,6 +98,63 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) :
|
||||||
.build()
|
.build()
|
||||||
notificationManager.notify(getNotificationID(data.getString("ProfileOnion"), handle), newNotification)
|
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);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (downloadIDs.containsKey(fileKey)) {
|
||||||
|
notificationManager.cancel(downloadIDs.get(fileKey)?:0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Intent().also { intent ->
|
Intent().also { intent ->
|
||||||
|
@ -157,6 +219,26 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) :
|
||||||
val target = (a.get("target") as? String) ?: ""
|
val target = (a.get("target") as? String) ?: ""
|
||||||
Cwtch.sendInvitation(profile, handle, target)
|
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)
|
||||||
|
}
|
||||||
|
"CheckDownloadStatus" -> {
|
||||||
|
val profile = (a.get("ProfileOnion") as? String) ?: ""
|
||||||
|
val fileKey = (a.get("fileKey") as? String) ?: ""
|
||||||
|
Cwtch.checkDownloadStatus(profile, fileKey)
|
||||||
|
}
|
||||||
"SendProfileEvent" -> {
|
"SendProfileEvent" -> {
|
||||||
val onion = (a.get("onion") as? String) ?: ""
|
val onion = (a.get("onion") as? String) ?: ""
|
||||||
val jsonEvent = (a.get("jsonEvent") as? String) ?: ""
|
val jsonEvent = (a.get("jsonEvent") as? String) ?: ""
|
||||||
|
@ -268,6 +350,15 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) :
|
||||||
return channelId
|
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 {
|
companion object {
|
||||||
const val KEY_METHOD = "KEY_METHOD"
|
const val KEY_METHOD = "KEY_METHOD"
|
||||||
const val KEY_ARGS = "KEY_ARGS"
|
const val KEY_ARGS = "KEY_ARGS"
|
||||||
|
|
|
@ -12,17 +12,25 @@ import android.view.Window
|
||||||
import androidx.lifecycle.Observer
|
import androidx.lifecycle.Observer
|
||||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||||
import androidx.work.*
|
import androidx.work.*
|
||||||
|
|
||||||
import io.flutter.embedding.android.SplashScreen
|
import io.flutter.embedding.android.SplashScreen
|
||||||
import io.flutter.embedding.android.FlutterActivity
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
import io.flutter.embedding.engine.FlutterEngine
|
import io.flutter.embedding.engine.FlutterEngine
|
||||||
import io.flutter.plugin.common.MethodChannel
|
import io.flutter.plugin.common.MethodChannel
|
||||||
import io.flutter.plugin.common.MethodCall
|
import io.flutter.plugin.common.MethodCall
|
||||||
import io.flutter.plugin.common.MethodChannel.Result
|
import io.flutter.plugin.common.MethodChannel.Result
|
||||||
|
import io.flutter.plugin.common.ErrorLogResult
|
||||||
|
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import java.util.concurrent.TimeUnit
|
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() {
|
class MainActivity: FlutterActivity() {
|
||||||
override fun provideSplashScreen(): SplashScreen? = SplashView()
|
override fun provideSplashScreen(): SplashScreen? = SplashView()
|
||||||
|
|
||||||
|
@ -47,6 +55,12 @@ class MainActivity: FlutterActivity() {
|
||||||
private var notificationClickChannel: MethodChannel? = null
|
private var notificationClickChannel: MethodChannel? = null
|
||||||
private var shutdownClickChannel: MethodChannel? = null
|
private var shutdownClickChannel: MethodChannel? = null
|
||||||
|
|
||||||
|
// "Download to..." prompt extra arguments
|
||||||
|
private var dlToProfile = ""
|
||||||
|
private var dlToHandle = ""
|
||||||
|
private var dlToFileKey = ""
|
||||||
|
|
||||||
|
// handles clicks received from outside the app (ie, notifications)
|
||||||
override fun onNewIntent(intent: Intent) {
|
override fun onNewIntent(intent: Intent) {
|
||||||
super.onNewIntent(intent)
|
super.onNewIntent(intent)
|
||||||
if (notificationClickChannel == null || intent.extras == null) return
|
if (notificationClickChannel == null || intent.extras == null) return
|
||||||
|
@ -68,6 +82,24 @@ 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");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
val filePath = intent!!.getData().toString();
|
||||||
|
val manifestPath = StringBuilder().append(this.applicationContext.cacheDir).append("/").append(this.dlToFileKey).toString();
|
||||||
|
handleCwtch(MethodCall("DownloadFile", mapOf(
|
||||||
|
"ProfileOnion" to this.dlToProfile,
|
||||||
|
"handle" to this.dlToHandle,
|
||||||
|
"filepath" to filePath,
|
||||||
|
"manifestpath" to manifestPath,
|
||||||
|
"filekey" to this.dlToFileKey
|
||||||
|
)), ErrorLogResult(""));//placeholder; this Result is never actually invoked
|
||||||
|
}
|
||||||
|
|
||||||
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
|
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
|
||||||
super.configureFlutterEngine(flutterEngine)
|
super.configureFlutterEngine(flutterEngine)
|
||||||
// Note: this methods are invoked on the main thread.
|
// Note: this methods are invoked on the main thread.
|
||||||
|
@ -125,6 +157,18 @@ class MainActivity: FlutterActivity() {
|
||||||
val workRequest = PeriodicWorkRequestBuilder<FlwtchWorker>(15, TimeUnit.MINUTES).setInputData(data).addTag(WORKER_TAG).addTag(uniqueTag).build()
|
val workRequest = PeriodicWorkRequestBuilder<FlwtchWorker>(15, TimeUnit.MINUTES).setInputData(data).addTag(WORKER_TAG).addTag(uniqueTag).build()
|
||||||
WorkManager.getInstance(this).enqueueUniquePeriodicWork("req_$uniqueTag", ExistingPeriodicWorkPolicy.REPLACE, workRequest)
|
WorkManager.getInstance(this).enqueueUniquePeriodicWork("req_$uniqueTag", ExistingPeriodicWorkPolicy.REPLACE, workRequest)
|
||||||
return
|
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)
|
// ...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()
|
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) {
|
class AppbusEvent(json: String) : JSONObject(json) {
|
||||||
val EventType = this.optString("EventType")
|
val EventType = this.optString("EventType")
|
||||||
val EventID = this.optString("EventID")
|
val EventID = this.optString("EventID")
|
||||||
|
|
|
@ -6,7 +6,7 @@ buildscript {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
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"
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,6 +40,15 @@ 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
|
||||||
|
void CheckDownloadStatus(String profile, 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
|
||||||
|
|
|
@ -182,8 +182,10 @@ class CwtchNotifier {
|
||||||
if (contactHandle == null || contactHandle == "") contactHandle = data["GroupID"];
|
if (contactHandle == null || contactHandle == "") contactHandle = data["GroupID"];
|
||||||
profileCN.getProfile(data["Identity"])?.contactList.getContact(contactHandle)!.totalMessages = int.parse(data["Data"]);
|
profileCN.getProfile(data["Identity"])?.contactList.getContact(contactHandle)!.totalMessages = int.parse(data["Data"]);
|
||||||
break;
|
break;
|
||||||
|
case "SendMessageToPeerError":
|
||||||
|
// Ignore
|
||||||
|
break;
|
||||||
case "IndexedFailure":
|
case "IndexedFailure":
|
||||||
EnvironmentConfig.debugLog("IndexedFailure");
|
|
||||||
var idx = data["Index"];
|
var idx = data["Index"];
|
||||||
var key = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["RemotePeer"])?.getMessageKey(idx);
|
var key = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["RemotePeer"])?.getMessageKey(idx);
|
||||||
try {
|
try {
|
||||||
|
@ -323,6 +325,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"], data["FilePath"]);
|
||||||
|
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);
|
||||||
|
|
||||||
|
@ -354,6 +357,59 @@ 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
|
||||||
|
// ignore: non_constant_identifier_names
|
||||||
|
void CheckDownloadStatus(String profileOnion, String fileKey) {
|
||||||
|
var checkDownloadStatus = library.lookup<NativeFunction<string_string_to_void_function>>("c_CheckDownloadStatus");
|
||||||
|
// ignore: non_constant_identifier_names
|
||||||
|
final CheckDownloadStatus = checkDownloadStatus.asFunction<VoidFromStringStringFn>();
|
||||||
|
final u1 = profileOnion.toNativeUtf8();
|
||||||
|
final u2 = fileKey.toNativeUtf8();
|
||||||
|
CheckDownloadStatus(u1, u1.length, u2, u2.length);
|
||||||
|
malloc.free(u1);
|
||||||
|
malloc.free(u2);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
// ignore: non_constant_identifier_names
|
// ignore: non_constant_identifier_names
|
||||||
void ResetTor() {
|
void ResetTor() {
|
||||||
|
|
|
@ -131,6 +131,29 @@ 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
|
||||||
|
// ignore: non_constant_identifier_names
|
||||||
|
void CheckDownloadStatus(String profileOnion, String fileKey) {
|
||||||
|
cwtchPlatform.invokeMethod("CheckDownloadStatus", {"ProfileOnion": profileOnion, "fileKey": fileKey});
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
// ignore: non_constant_identifier_names
|
// ignore: non_constant_identifier_names
|
||||||
void ResetTor() {
|
void ResetTor() {
|
||||||
|
@ -175,7 +198,7 @@ class CwtchGomobile implements Cwtch {
|
||||||
@override
|
@override
|
||||||
void UpdateMessageFlags(String profile, String handle, int index, int flags) {
|
void UpdateMessageFlags(String profile, String handle, int index, int flags) {
|
||||||
print("gomobile.dart UpdateMessageFlags " + index.toString());
|
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
|
@override
|
||||||
|
|
|
@ -213,6 +213,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.
|
||||||
|
@ -356,6 +357,100 @@ class ProfileInfoState extends ChangeNotifier {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void downloadInit(String fileKey, int numChunks) {
|
||||||
|
this._downloads[fileKey] = FileDownloadProgress(numChunks, DateTime.now());
|
||||||
|
}
|
||||||
|
|
||||||
|
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, 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);
|
||||||
|
}
|
||||||
|
this._downloads[fileKey]!.timeEnd = DateTime.now();
|
||||||
|
this._downloads[fileKey]!.downloadedTo = finalPath;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
String? downloadFinalPath(String fileKey) {
|
||||||
|
return this._downloads.containsKey(fileKey) ? this._downloads[fileKey]!.downloadedTo : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String downloadSpeed(String fileKey) {
|
||||||
|
if (!downloadActive(fileKey) || this._downloads[fileKey]!.chunksDownloaded == 0) {
|
||||||
|
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 {
|
||||||
|
int chunksDownloaded = 0;
|
||||||
|
int chunksTotal = 1;
|
||||||
|
bool complete = false;
|
||||||
|
bool gotManifest = false;
|
||||||
|
String? downloadedTo;
|
||||||
|
DateTime? timeStart;
|
||||||
|
DateTime? timeEnd;
|
||||||
|
|
||||||
|
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 }
|
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
|
||||||
|
@ -33,38 +35,39 @@ Future<Message> messageHandler(BuildContext context, String profileOnion, String
|
||||||
try {
|
try {
|
||||||
var rawMessageEnvelopeFuture = Provider.of<FlwtchState>(context, listen: false).cwtch.GetMessage(profileOnion, contactHandle, index);
|
var rawMessageEnvelopeFuture = Provider.of<FlwtchState>(context, listen: false).cwtch.GetMessage(profileOnion, contactHandle, index);
|
||||||
return rawMessageEnvelopeFuture.then((dynamic rawMessageEnvelope) {
|
return rawMessageEnvelopeFuture.then((dynamic rawMessageEnvelope) {
|
||||||
dynamic messageWrapper = jsonDecode(rawMessageEnvelope);
|
var metadata = MessageMetadata(profileOnion, contactHandle, index, DateTime.now(), "", "", null, 0, false, true);
|
||||||
// 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);
|
|
||||||
|
|
||||||
try {
|
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']);
|
dynamic message = jsonDecode(messageWrapper['Message']);
|
||||||
var content = message['d'] as dynamic;
|
var content = message['d'] as dynamic;
|
||||||
var overlay = int.parse(message['o'].toString());
|
var overlay = int.parse(message['o'].toString());
|
||||||
|
@ -77,11 +80,14 @@ 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);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
print("an error! " + e.toString());
|
||||||
return MalformedMessage(metadata);
|
return MalformedMessage(metadata);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,64 @@
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:cwtch/models/message.dart';
|
||||||
|
import 'package:cwtch/widgets/filebubble.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,
|
||||||
|
interactive: false,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@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,
|
||||||
|
|
|
@ -188,6 +188,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)!.settingFileSharing, 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';
|
||||||
|
@ -36,6 +40,9 @@ class _MessageViewState extends State<MessageView> {
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
scrollListener.itemPositions.addListener(() {
|
scrollListener.itemPositions.addListener(() {
|
||||||
|
if (scrollListener.itemPositions.value.length == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
var first = scrollListener.itemPositions.value.first.index;
|
var first = scrollListener.itemPositions.value.first.index;
|
||||||
var last = scrollListener.itemPositions.value.last.index;
|
var last = scrollListener.itemPositions.value.last.index;
|
||||||
// sometimes these go hi->lo and sometimes they go lo->hi because [who tf knows]
|
// sometimes these go hi->lo and sometimes they go lo->hi because [who tf knows]
|
||||||
|
@ -74,6 +81,25 @@ 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 +131,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 +201,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();
|
||||||
|
@ -351,4 +370,20 @@ class _MessageViewState extends State<MessageView> {
|
||||||
));
|
));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _showFilePicker() async {
|
||||||
|
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);
|
||||||
|
} else {
|
||||||
|
print("file size cannot exceed 10 gigabytes");
|
||||||
|
//todo: toast error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,277 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:cwtch/models/message.dart';
|
||||||
|
import 'package:file_picker_desktop/file_picker_desktop.dart';
|
||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
|
import 'package:flutter/material.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;
|
||||||
|
final bool interactive;
|
||||||
|
|
||||||
|
FileBubble(this.nameSuggestion, this.rootHash, this.nonce, this.fileSize, {this.interactive = true});
|
||||||
|
|
||||||
|
@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;
|
||||||
|
var flagStarted = Provider.of<MessageMetadata>(context).flags & 0x02 > 0;
|
||||||
|
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
|
||||||
|
? senderFileChrome(AppLocalizations.of(context)!.messageFileSent, widget.nameSuggestion, widget.rootHash, widget.fileSize)
|
||||||
|
: (fileChrome(AppLocalizations.of(context)!.messageFileOffered + ":", widget.nameSuggestion, widget.rootHash, widget.fileSize,
|
||||||
|
Provider.of<ProfileInfoState>(context).downloadSpeed(widget.fileKey())));
|
||||||
|
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())) {
|
||||||
|
// in this case, whatever marked download.complete would have also set the path
|
||||||
|
var path = Provider.of<ProfileInfoState>(context).downloadFinalPath(widget.fileKey())!;
|
||||||
|
wdgDecorations = Text('Saved to: ' + path + '\u202F');
|
||||||
|
} 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 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<ProfileInfoState>(context).downloadFinalPath(widget.fileKey());
|
||||||
|
if (path == null) {
|
||||||
|
wdgDecorations = Text('Checking download status...' + '\u202F');
|
||||||
|
Provider.of<FlwtchState>(context, listen: false).cwtch.CheckDownloadStatus(Provider.of<ProfileInfoState>(context, listen: false).onion, widget.fileKey());
|
||||||
|
} else {
|
||||||
|
wdgDecorations = Text('Saved to: ' + path + '\u202F');
|
||||||
|
}
|
||||||
|
} 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(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, Visibility(visible: widget.interactive, child: wdgDecorations)]
|
||||||
|
: [wdgSender, wdgMessage, Visibility(visible: widget.interactive, child: 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;
|
||||||
|
var contact = Provider.of<ContactInfoState>(context, listen: false).onion;
|
||||||
|
var idx = Provider.of<MessageMetadata>(context, listen: false).messageIndex;
|
||||||
|
|
||||||
|
if (Platform.isAndroid) {
|
||||||
|
Provider.of<ProfileInfoState>(context, listen: false).downloadInit(widget.fileKey(), (widget.fileSize / 4096).ceil());
|
||||||
|
Provider.of<FlwtchState>(context, listen: false).cwtch.UpdateMessageFlags(profileOnion, contact, idx, Provider.of<MessageMetadata>(context, listen: false).flags | 0x02);
|
||||||
|
Provider.of<MessageMetadata>(context, listen: false).flags |= 0x02;
|
||||||
|
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";
|
||||||
|
Provider.of<ProfileInfoState>(context, listen: false).downloadInit(widget.fileKey(), (widget.fileSize / 4096).ceil());
|
||||||
|
Provider.of<FlwtchState>(context, listen: false).cwtch.UpdateMessageFlags(profileOnion, contact, idx, Provider.of<MessageMetadata>(context, listen: false).flags | 0x02);
|
||||||
|
Provider.of<MessageMetadata>(context, listen: false).flags |= 0x02;
|
||||||
|
Provider.of<FlwtchState>(context, listen: false).cwtch.DownloadFile(profileOnion, handle, file.path, manifestPath, widget.fileKey());
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct an file chrome for the sender
|
||||||
|
Widget senderFileChrome(String chrome, String fileName, String rootHash, int fileSize) {
|
||||||
|
return ListTile(
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
title: Wrap(direction: Axis.horizontal, alignment: WrapAlignment.start, 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(),
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.left,
|
||||||
|
textWidthBasis: TextWidthBasis.parent,
|
||||||
|
maxLines: 2,
|
||||||
|
),
|
||||||
|
SelectableText(
|
||||||
|
prettyBytes(fileSize) + '\u202F' + '\n',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Provider.of<Settings>(context).theme.messageFromMeTextColor(),
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.left,
|
||||||
|
maxLines: 2,
|
||||||
|
)
|
||||||
|
]),
|
||||||
|
subtitle: SelectableText(
|
||||||
|
'sha512: ' + rootHash + '\u202F',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Provider.of<Settings>(context).theme.messageFromMeTextColor(),
|
||||||
|
fontSize: 10,
|
||||||
|
fontFamily: "monospace",
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.left,
|
||||||
|
maxLines: 4,
|
||||||
|
textWidthBasis: TextWidthBasis.parent,
|
||||||
|
),
|
||||||
|
leading: Icon(Icons.attach_file, size: 32, color: Provider.of<Settings>(context).theme.messageFromMeTextColor()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct an file chrome
|
||||||
|
Widget fileChrome(String chrome, String fileName, String rootHash, int fileSize, String speed) {
|
||||||
|
return ListTile(
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
title: Wrap(direction: Axis.horizontal, alignment: WrapAlignment.start, children: [
|
||||||
|
SelectableText(
|
||||||
|
chrome + '\u202F',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Provider.of<Settings>(context).theme.messageFromOtherTextColor(),
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.left,
|
||||||
|
maxLines: 2,
|
||||||
|
textWidthBasis: TextWidthBasis.longestLine,
|
||||||
|
),
|
||||||
|
SelectableText(
|
||||||
|
fileName + '\u202F',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Provider.of<Settings>(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<Settings>(context).theme.messageFromOtherTextColor(),
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.left,
|
||||||
|
maxLines: 2,
|
||||||
|
)
|
||||||
|
]),
|
||||||
|
subtitle: SelectableText(
|
||||||
|
'sha512: ' + rootHash + '\u202F',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Provider.of<Settings>(context).theme.messageFromMeTextColor(),
|
||||||
|
fontSize: 10,
|
||||||
|
fontFamily: "monospace",
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.left,
|
||||||
|
maxLines: 4,
|
||||||
|
textWidthBasis: TextWidthBasis.parent,
|
||||||
|
),
|
||||||
|
leading: Icon(Icons.attach_file, size: 32, color: Provider.of<Settings>(context).theme.messageFromOtherTextColor()),
|
||||||
|
trailing: Visibility(
|
||||||
|
visible: speed != "0 B/s",
|
||||||
|
child: SelectableText(
|
||||||
|
speed + '\u202F',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Provider.of<Settings>(context).theme.messageFromMeTextColor(),
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.left,
|
||||||
|
maxLines: 1,
|
||||||
|
textWidthBasis: TextWidthBasis.longestLine,
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -131,7 +131,7 @@ class InvitationBubbleState extends State<InvitationBubble> {
|
||||||
var contact = Provider.of<ContactInfoState>(context, listen: false).onion;
|
var contact = Provider.of<ContactInfoState>(context, listen: false).onion;
|
||||||
var idx = Provider.of<MessageMetadata>(context, listen: false).messageIndex;
|
var idx = Provider.of<MessageMetadata>(context, listen: false).messageIndex;
|
||||||
Provider.of<FlwtchState>(context, listen: false).cwtch.UpdateMessageFlags(profileOnion, contact, idx, Provider.of<MessageMetadata>(context, listen: false).flags | 0x01);
|
Provider.of<FlwtchState>(context, listen: false).cwtch.UpdateMessageFlags(profileOnion, contact, idx, Provider.of<MessageMetadata>(context, listen: false).flags | 0x01);
|
||||||
Provider.of<MessageMetadata>(context).flags |= 0x01;
|
Provider.of<MessageMetadata>(context, listen: false).flags |= 0x01;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -87,7 +87,7 @@ class _MessageListState extends State<MessageList> {
|
||||||
// Already includes MessageRow,,
|
// Already includes MessageRow,,
|
||||||
return message.getWidget(context);
|
return message.getWidget(context);
|
||||||
} else {
|
} else {
|
||||||
return MessageLoadingBubble();
|
return Text(''); //MessageLoadingBubble();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
@ -163,7 +163,7 @@ class MessageRowState extends State<MessageRow> with SingleTickerProviderStateMi
|
||||||
// For desktop...
|
// For desktop...
|
||||||
onHover: (event) {
|
onHover: (event) {
|
||||||
setState(() {
|
setState(() {
|
||||||
Provider.of<AppState>(context, listen: false).hoveredIndex = Provider.of<MessageMetadata>(context).messageIndex;
|
Provider.of<AppState>(context, listen: false).hoveredIndex = Provider.of<MessageMetadata>(context, listen: false).messageIndex;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onExit: (event) {
|
onExit: (event) {
|
||||||
|
|
23
pubspec.lock
23
pubspec.lock
|
@ -106,6 +106,20 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.1.2"
|
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:
|
flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description: flutter
|
description: flutter
|
||||||
|
@ -116,6 +130,13 @@ packages:
|
||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
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:
|
flutter_test:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description: flutter
|
description: flutter
|
||||||
|
@ -427,4 +448,4 @@ packages:
|
||||||
version: "3.1.0"
|
version: "3.1.0"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=2.13.0 <3.0.0"
|
dart: ">=2.13.0 <3.0.0"
|
||||||
flutter: ">=1.20.0"
|
flutter: ">=2.0.0"
|
||||||
|
|
|
@ -41,6 +41,8 @@ dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
scrollable_positioned_list: ^0.2.0-nullsafety.0
|
scrollable_positioned_list: ^0.2.0-nullsafety.0
|
||||||
|
file_picker: ^4.0.1
|
||||||
|
file_picker_desktop: ^1.1.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
msix: ^2.1.3
|
msix: ^2.1.3
|
||||||
|
|
Loading…
Reference in New Issue