Merge branch 'trunk' into winInstructions
continuous-integration/drone/pr Build is pending Details

This commit is contained in:
Sarah Jamie Lewis 2022-07-25 16:27:28 +00:00
commit 5139846f31
43 changed files with 742 additions and 72 deletions

View File

@ -229,7 +229,7 @@ steps:
status: [ success ]
environment:
pfx:
from_secret: pfx
from_secret: pfx2022_b64
pfx_pass:
from_secret: pfx_pass
commands:
@ -245,6 +245,8 @@ steps:
- echo $Env:pfx > codesign.pfx.b64
- certutil -decode codesign.pfx.b64 codesign.pfx
- signtool sign /v /fd sha256 /a /f codesign.pfx /p $Env:pfx_pass /tr http://timestamp.digicert.com $Env:releasedir\cwtch.exe
- signtool sign /v /fd sha256 /a /f codesign.pfx /p $Env:pfx_pass /tr http://timestamp.digicert.com $Env:releasedir\libCwtch.dll
- signtool sign /v /fd sha256 /a /f codesign.pfx /p $Env:pfx_pass /tr http://timestamp.digicert.com $Env:releasedir\flutter_windows.dll
- copy windows\runner\resources\knot_128.ico $Env:releasedir\cwtch.ico
- makensis windows\nsis\cwtch-installer.nsi
- move windows\nsis\cwtch-installer.exe cwtch-installer.exe

View File

@ -1 +1 @@
2022-06-22-15-36-1.7.1-10-g231e27d
2022-07-22-12-41-v1.8.0-7-g7b3e842

View File

@ -1 +1 @@
2022-06-21-22-49-1.7.1-2-gff23465
2022-07-22-16-41-v1.8.0-7-g7b3e842

View File

@ -99,7 +99,10 @@ dependencies {
//implementation("androidx.work:work-runtime:$work_version")
// Kotlin + coroutines
implementation("androidx.work:work-runtime-ktx:2.5.0")
// 2022.06: upgraded from 2.5 to 2.7 for android 12
// err: "requires that one of FLAG_IMMUTABLE or FLAG_MUTABLE be specified when creating a PendingIntent"
// as per https://github.com/flutter/flutter/issues/93609
implementation 'androidx.work:work-runtime-ktx:2.7.0'
// optional - RxJava2 support
//implementation("androidx.work:work-rxjava2:$work_version")

View File

@ -16,7 +16,8 @@
android:theme="@style/NormalTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
android:windowSoftInputMode="adjustResize"
android:exported="true">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues

View File

@ -1,25 +1,27 @@
package im.cwtch.flwtch
import android.app.*
import android.os.Environment
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.graphics.BitmapFactory
import android.graphics.Color
import android.net.Uri
import android.os.Build
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import androidx.work.*
import androidx.work.CoroutineWorker
import androidx.work.ForegroundInfo
import androidx.work.WorkerParameters
import cwtch.Cwtch
import io.flutter.FlutterInjector
import org.json.JSONObject
import java.nio.file.Files
import java.nio.file.Paths
import java.nio.file.StandardCopyOption
import android.net.Uri
class FlwtchWorker(context: Context, parameters: WorkerParameters) :
CoroutineWorker(context, parameters) {
@ -82,6 +84,10 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) :
Log.i(TAG, "startCwtch success, starting coroutine AppbusEvent loop...")
val downloadIDs = mutableMapOf<String, Int>()
var flags = PendingIntent.FLAG_UPDATE_CURRENT
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
flags = flags or PendingIntent.FLAG_IMMUTABLE
}
while (true) {
try {
val evt = MainActivity.AppbusEvent(Cwtch.getAppBusEvent())
@ -109,12 +115,11 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) :
intent.action = Intent.ACTION_RUN
intent.putExtra("EventType", "NotificationClicked")
}
val newNotification = NotificationCompat.Builder(applicationContext, channelId)
.setContentTitle("Cwtch")
.setContentText(notificationSimple ?: "New Message")
.setSmallIcon(R.mipmap.knott_transparent)
.setContentIntent(PendingIntent.getActivity(applicationContext, 1, clickIntent, PendingIntent.FLAG_UPDATE_CURRENT))
.setContentIntent(PendingIntent.getActivity(applicationContext, 1, clickIntent, flags))
.setAutoCancel(true)
.build()
@ -147,7 +152,7 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) :
?: "New Message From %1").replace("%1", data.getString("Nick")))
.setLargeIcon(BitmapFactory.decodeStream(fh))
.setSmallIcon(R.mipmap.knott_transparent)
.setContentIntent(PendingIntent.getActivity(applicationContext, 1, clickIntent, PendingIntent.FLAG_UPDATE_CURRENT))
.setContentIntent(PendingIntent.getActivity(applicationContext, 1, clickIntent, flags))
.setAutoCancel(true)
.build()
@ -265,6 +270,10 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) :
intent.action = Intent.ACTION_RUN
intent.putExtra("EventType", "ShutdownClicked")
}
var flags = PendingIntent.FLAG_UPDATE_CURRENT
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
flags = flags or PendingIntent.FLAG_IMMUTABLE
}
val notification = NotificationCompat.Builder(applicationContext, channelId)
.setContentTitle(title)
@ -274,7 +283,7 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) :
.setOngoing(true)
// Add the cancel action to the notification which can
// be used to cancel the worker
.addAction(android.R.drawable.ic_delete, cancel, PendingIntent.getActivity(applicationContext, 2, cancelIntent, PendingIntent.FLAG_UPDATE_CURRENT))
.addAction(android.R.drawable.ic_delete, cancel, PendingIntent.getActivity(applicationContext, 2, cancelIntent, flags))
.build()
return ForegroundInfo(101, notification)

View File

@ -313,6 +313,7 @@ class MainActivity: FlutterActivity() {
result.success(Cwtch.sendInvitation(profile, conversation.toLong(), target.toLong()))
return
}
"ShareFile" -> {
val profile: String = call.argument("ProfileOnion") ?: ""
val conversation: Int = call.argument("conversation") ?: 0
@ -321,6 +322,27 @@ class MainActivity: FlutterActivity() {
return
}
"GetSharedFiles" -> {
val profile: String = call.argument("ProfileOnion") ?: ""
val conversation: Int = call.argument("conversation") ?: 0
result.success(Cwtch.getSharedFiles(profile, conversation.toLong()))
return
}
"RestartSharing" -> {
val profile: String = call.argument("ProfileOnion") ?: ""
val filepath: String = call.argument("filekey") ?: ""
result.success(Cwtch.restartSharing(profile, filepath))
return
}
"StopSharing" -> {
val profile: String = call.argument("ProfileOnion") ?: ""
val filepath: String = call.argument("filekey") ?: ""
result.success(Cwtch.stopSharing(profile, filepath))
return
}
"CreateProfile" -> {
val nick: String = call.argument("nick") ?: ""
val pass: String = call.argument("pass") ?: ""

View File

@ -13,6 +13,7 @@
app:lottie_autoPlay="true"
app:lottie_rawRes="@raw/cwtch_animated_logo_op"
app:lottie_loop="true"
app:lottie_speed="1.00" />
app:lottie_speed="1.00"
app:lottie_enableMergePathsForKitKatAndAbove="true" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -59,6 +59,16 @@ abstract class Cwtch {
// ignore: non_constant_identifier_names
Future<dynamic> ShareFile(String profile, int handle, String filepath);
// ignore: non_constant_identifier_names
Future<dynamic> GetSharedFiles(String profile, int handle);
// ignore: non_constant_identifier_names
void RestartSharing(String profile, String filekey);
// ignore: non_constant_identifier_names
void StopSharing(String profile, String filekey);
// ignore: non_constant_identifier_names
void DownloadFile(String profile, int handle, String filepath, String manifestpath, String filekey);
// android-only

View File

@ -68,6 +68,9 @@ typedef GetJsonBlobFromStrIntStrFn = Pointer<Utf8> Function(Pointer<Utf8>, int,
typedef get_json_blob_from_str_str_int_function = Pointer<Utf8> Function(Pointer<Utf8>, Int32, Pointer<Utf8>, Int32, Int32);
typedef GetJsonBlobFromStrStrIntFn = Pointer<Utf8> Function(Pointer<Utf8>, int, Pointer<Utf8>, int, int);
typedef get_json_blob_from_str_int_function = Pointer<Utf8> Function(Pointer<Utf8>, Int32, Int32);
typedef GetJsonBlobFromStrIntFn = Pointer<Utf8> Function(Pointer<Utf8>, int, int);
typedef get_json_blob_from_str_int_int_function = Pointer<Utf8> Function(Pointer<Utf8>, Int32, Int32, Int32);
typedef GetJsonBlobFromStrIntIntFn = Pointer<Utf8> Function(Pointer<Utf8>, int, int, int);
@ -841,4 +844,40 @@ class CwtchFfi implements Cwtch {
_UnsafeFreePointerAnyUseOfThisFunctionMustBeDoubleApproved(result);
return debugResult;
}
@override
Future<String> GetSharedFiles(String profile, int handle) async {
var getSharedFiles = library.lookup<NativeFunction<get_json_blob_from_str_int_function>>("c_GetSharedFiles");
final GetSharedFiles = getSharedFiles.asFunction<GetJsonBlobFromStrIntFn>();
final utf8profile = profile.toNativeUtf8();
Pointer<Utf8> jsonMessageBytes = GetSharedFiles(utf8profile, utf8profile.length, handle);
String jsonMessage = jsonMessageBytes.toDartString();
_UnsafeFreePointerAnyUseOfThisFunctionMustBeDoubleApproved(jsonMessageBytes);
malloc.free(utf8profile);
return jsonMessage;
}
@override
void RestartSharing(String profile, String filekey) {
var restartSharingC = library.lookup<NativeFunction<void_from_string_string_function>>("c_RestartSharing");
// ignore: non_constant_identifier_names
final RestartSharing = restartSharingC.asFunction<VoidFromStringStringFn>();
final utf8profile = profile.toNativeUtf8();
final ut8filekey = filekey.toNativeUtf8();
RestartSharing(utf8profile, utf8profile.length, ut8filekey, ut8filekey.length);
malloc.free(utf8profile);
malloc.free(ut8filekey);
}
@override
void StopSharing(String profile, String filekey) {
var stopSharingC = library.lookup<NativeFunction<void_from_string_string_function>>("c_StopSharing");
// ignore: non_constant_identifier_names
final StopSharing = stopSharingC.asFunction<VoidFromStringStringFn>();
final utf8profile = profile.toNativeUtf8();
final ut8filekey = filekey.toNativeUtf8();
StopSharing(utf8profile, utf8profile.length, ut8filekey, ut8filekey.length);
malloc.free(utf8profile);
malloc.free(ut8filekey);
}
}

View File

@ -333,4 +333,19 @@ class CwtchGomobile implements Cwtch {
// we don't implement it
return Future.value("{}");
}
@override
Future GetSharedFiles(String profile, int handle) {
return cwtchPlatform.invokeMethod("GetSharedFiles", {"ProfileOnion": profile, "conversation": handle});
}
@override
void RestartSharing(String profile, String filekey) {
cwtchPlatform.invokeMethod("RestartSharing", {"ProfileOnion": profile, "filekey": filekey});
}
@override
void StopSharing(String profile, String filekey) {
cwtchPlatform.invokeMethod("StopSharing", {"ProfileOnion": profile, "filekey": filekey});
}
}

View File

@ -1,6 +1,16 @@
{
"@@locale": "cy",
"@@last_modified": "2022-06-22T00:46:01+02:00",
"@@last_modified": "2022-07-21T19:54:08+02:00",
"tooltipUnpinConversation": "Unpin conversation from the top of \"Conversations\"",
"tooltipPinConversation": "Pin conversation to the top of \"Conversations\"",
"replyingTo": "Replying to %1",
"fileDownloadUnavailable": "This file appears unavailable for download. The sender may have disabled downloads for this file.",
"messageNoReplies": "There are no replies to this message.",
"headingReplies": "Replies",
"viewReplies": "View replies to this message",
"restartFileShare": "Start Sharing File",
"stopSharingFile": "Stop Sharing File",
"manageSharedFiles": "Manage Shared Files",
"localeDe": "Almaeneg \/ Deutsch",
"localePt": "Portiwgaleg \/ Portuguesa",
"localeRo": "Rwmaneg \/ Română",

View File

@ -1,6 +1,16 @@
{
"@@locale": "da",
"@@last_modified": "2022-06-22T00:46:01+02:00",
"@@last_modified": "2022-07-21T19:54:08+02:00",
"tooltipUnpinConversation": "Unpin conversation from the top of \"Conversations\"",
"tooltipPinConversation": "Pin conversation to the top of \"Conversations\"",
"replyingTo": "Replying to %1",
"fileDownloadUnavailable": "This file appears unavailable for download. The sender may have disabled downloads for this file.",
"messageNoReplies": "There are no replies to this message.",
"headingReplies": "Replies",
"viewReplies": "View replies to this message",
"restartFileShare": "Start Sharing File",
"stopSharingFile": "Stop Sharing File",
"manageSharedFiles": "Manage Shared Files",
"localeDe": "Tysk \/ Deutsch",
"localeEn": "Engelsk \/ English",
"localeFr": "Fransk \/ Français",

View File

@ -1,6 +1,16 @@
{
"@@locale": "de",
"@@last_modified": "2022-06-22T00:46:01+02:00",
"@@last_modified": "2022-07-21T19:54:08+02:00",
"tooltipUnpinConversation": "Unpin conversation from the top of \"Conversations\"",
"tooltipPinConversation": "Pin conversation to the top of \"Conversations\"",
"replyingTo": "Replying to %1",
"fileDownloadUnavailable": "This file appears unavailable for download. The sender may have disabled downloads for this file.",
"messageNoReplies": "There are no replies to this message.",
"headingReplies": "Replies",
"viewReplies": "View replies to this message",
"restartFileShare": "Start Sharing File",
"stopSharingFile": "Stop Sharing File",
"manageSharedFiles": "Manage Shared Files",
"localeEn": "Englisch \/ English",
"localePl": "Polnisch \/ Polski",
"localeIt": "Italienisch \/ Italiana",

View File

@ -1,6 +1,16 @@
{
"@@locale": "el",
"@@last_modified": "2022-06-22T00:46:01+02:00",
"@@last_modified": "2022-07-21T19:54:08+02:00",
"tooltipUnpinConversation": "Unpin conversation from the top of \"Conversations\"",
"tooltipPinConversation": "Pin conversation to the top of \"Conversations\"",
"replyingTo": "Replying to %1",
"fileDownloadUnavailable": "This file appears unavailable for download. The sender may have disabled downloads for this file.",
"messageNoReplies": "There are no replies to this message.",
"headingReplies": "Replies",
"viewReplies": "View replies to this message",
"restartFileShare": "Start Sharing File",
"stopSharingFile": "Stop Sharing File",
"manageSharedFiles": "Manage Shared Files",
"localeDe": "Γερμανός \/ Deutsch",
"localeEn": "English \/ English",
"localeLb": "Λουξεμβουργιανά",

View File

@ -1,6 +1,16 @@
{
"@@locale": "en",
"@@last_modified": "2022-06-22T00:46:01+02:00",
"@@last_modified": "2022-07-21T19:54:08+02:00",
"tooltipUnpinConversation": "Unpin conversation from the top of \"Conversations\"",
"tooltipPinConversation": "Pin conversation to the top of \"Conversations\"",
"replyingTo": "Replying to %1",
"fileDownloadUnavailable": "This file appears unavailable for download. The sender may have disabled downloads for this file.",
"messageNoReplies": "There are no replies to this message.",
"headingReplies": "Replies",
"viewReplies": "View replies to this message",
"restartFileShare": "Start Sharing File",
"stopSharingFile": "Stop Sharing File",
"manageSharedFiles": "Manage Shared Files",
"localeDe": "German \/ Deutsch",
"localeEn": "English \/ English",
"localeLb": "Luxembourgish \/ Lëtzebuergesch",

View File

@ -1,6 +1,16 @@
{
"@@locale": "es",
"@@last_modified": "2022-06-22T00:46:01+02:00",
"@@last_modified": "2022-07-21T19:54:08+02:00",
"tooltipUnpinConversation": "Unpin conversation from the top of \"Conversations\"",
"tooltipPinConversation": "Pin conversation to the top of \"Conversations\"",
"replyingTo": "Replying to %1",
"fileDownloadUnavailable": "This file appears unavailable for download. The sender may have disabled downloads for this file.",
"messageNoReplies": "There are no replies to this message.",
"headingReplies": "Replies",
"viewReplies": "View replies to this message",
"restartFileShare": "Start Sharing File",
"stopSharingFile": "Stop Sharing File",
"manageSharedFiles": "Manage Shared Files",
"localeDe": "Alemán \/ Deutsch",
"settingImagePreviewsDescription": "Las imágenes se descargarán y visualizarán automáticamente. Ten en cuenta que las previsualizaciones pueden generar vulnerabilidades de seguridad, no deberías habilitar este experimento si usas Cwtch con contactos que no son de confianza. Las imágenes de perfil están planeadas para Cwtch 1.6.",
"tooltipBackToMessageEditing": "Volver a Edición de mensajes",

View File

@ -1,6 +1,16 @@
{
"@@locale": "fr",
"@@last_modified": "2022-06-22T00:46:01+02:00",
"@@last_modified": "2022-07-21T19:54:08+02:00",
"tooltipUnpinConversation": "Unpin conversation from the top of \"Conversations\"",
"tooltipPinConversation": "Pin conversation to the top of \"Conversations\"",
"viewReplies": "Voir les réponses à ce message",
"stopSharingFile": "Arrêter le partage de fichiers",
"restartFileShare": "Démarrer le partage de fichiers",
"replyingTo": "Répondre à %1",
"messageNoReplies": "Il n'y a pas de réponses à ce message.",
"manageSharedFiles": "Gérer les fichiers partagés",
"headingReplies": "Réponses",
"fileDownloadUnavailable": "Ce fichier semble indisponible pour le téléchargement. L'expéditeur a peut-être désactivé les téléchargements pour ce fichier.",
"localeDe": "Allemand \/ Deutsch",
"localeDa": "Danois \/ Dansk",
"localePt": "Portugais \/ Portuguesa",

View File

@ -1,6 +1,16 @@
{
"@@locale": "it",
"@@last_modified": "2022-06-22T00:46:01+02:00",
"@@last_modified": "2022-07-21T19:54:08+02:00",
"tooltipUnpinConversation": "Unpin conversation from the top of \"Conversations\"",
"tooltipPinConversation": "Pin conversation to the top of \"Conversations\"",
"fileDownloadUnavailable": "Questo file non sembra disponibile per il download. Il mittente potrebbe aver disabilitato i download per questo file.",
"headingReplies": "Risposte",
"manageSharedFiles": "Gestisci file condivisi",
"messageNoReplies": "Non ci sono risposte a questo messaggio.",
"replyingTo": "Risposta a %1",
"restartFileShare": "Avvia la condivisione del file",
"stopSharingFile": "Interrompi la condivisione del file",
"viewReplies": "Visualizza le risposte a questo messaggio",
"localeDe": "Tedesco \/ Deutsch",
"settingImagePreviewsDescription": "Le immagini e le immagini del profilo verranno scaricate e visualizzate in anteprima automaticamente. Ti consigliamo di non abilitare questo esperimento se usi Cwtch con contatti non attendibili.",
"localeNo": "Norvegese \/ Norsk",

View File

@ -1,6 +1,16 @@
{
"@@locale": "lb",
"@@last_modified": "2022-06-22T00:46:01+02:00",
"@@last_modified": "2022-07-21T19:54:08+02:00",
"tooltipUnpinConversation": "Unpin conversation from the top of \"Conversations\"",
"tooltipPinConversation": "Pin conversation to the top of \"Conversations\"",
"replyingTo": "Replying to %1",
"fileDownloadUnavailable": "This file appears unavailable for download. The sender may have disabled downloads for this file.",
"messageNoReplies": "There are no replies to this message.",
"headingReplies": "Replies",
"viewReplies": "View replies to this message",
"restartFileShare": "Start Sharing File",
"stopSharingFile": "Stop Sharing File",
"manageSharedFiles": "Manage Shared Files",
"localeDe": "Däitsch \/ Deutsch",
"localeEn": "Englesch",
"localeLb": "Lëtzebuergesch",

View File

@ -1,6 +1,16 @@
{
"@@locale": "no",
"@@last_modified": "2022-06-22T00:46:01+02:00",
"@@last_modified": "2022-07-21T19:54:08+02:00",
"tooltipUnpinConversation": "Unpin conversation from the top of \"Conversations\"",
"tooltipPinConversation": "Pin conversation to the top of \"Conversations\"",
"replyingTo": "Replying to %1",
"fileDownloadUnavailable": "This file appears unavailable for download. The sender may have disabled downloads for this file.",
"messageNoReplies": "There are no replies to this message.",
"headingReplies": "Replies",
"viewReplies": "View replies to this message",
"restartFileShare": "Start Sharing File",
"stopSharingFile": "Stop Sharing File",
"manageSharedFiles": "Manage Shared Files",
"localeDe": "Tysk \/ Deutsch",
"localeEn": "Engelsk",
"localeLb": "Luxemburgsk",

View File

@ -1,6 +1,16 @@
{
"@@locale": "pl",
"@@last_modified": "2022-06-22T00:46:01+02:00",
"@@last_modified": "2022-07-21T19:54:08+02:00",
"tooltipUnpinConversation": "Unpin conversation from the top of \"Conversations\"",
"tooltipPinConversation": "Pin conversation to the top of \"Conversations\"",
"replyingTo": "Replying to %1",
"fileDownloadUnavailable": "This file appears unavailable for download. The sender may have disabled downloads for this file.",
"messageNoReplies": "There are no replies to this message.",
"headingReplies": "Replies",
"viewReplies": "View replies to this message",
"restartFileShare": "Start Sharing File",
"stopSharingFile": "Stop Sharing File",
"manageSharedFiles": "Manage Shared Files",
"localeDe": "Niemiecki \/ Deutsch",
"serverLabel": "Server",
"deleteBtn": "Usuń",

View File

@ -1,6 +1,16 @@
{
"@@locale": "pt",
"@@last_modified": "2022-06-22T00:46:01+02:00",
"@@last_modified": "2022-07-21T19:54:08+02:00",
"tooltipUnpinConversation": "Unpin conversation from the top of \"Conversations\"",
"tooltipPinConversation": "Pin conversation to the top of \"Conversations\"",
"replyingTo": "Replying to %1",
"fileDownloadUnavailable": "This file appears unavailable for download. The sender may have disabled downloads for this file.",
"messageNoReplies": "There are no replies to this message.",
"headingReplies": "Replies",
"viewReplies": "View replies to this message",
"restartFileShare": "Start Sharing File",
"stopSharingFile": "Stop Sharing File",
"manageSharedFiles": "Manage Shared Files",
"localeDe": "Alemao \/ Deutsch",
"localeEn": "English \/ English",
"localeLb": "Luxembourgish \/ Lëtzebuergesch",

View File

@ -1,6 +1,16 @@
{
"@@locale": "ro",
"@@last_modified": "2022-06-22T00:46:01+02:00",
"@@last_modified": "2022-07-21T19:54:08+02:00",
"tooltipUnpinConversation": "Unpin conversation from the top of \"Conversations\"",
"tooltipPinConversation": "Pin conversation to the top of \"Conversations\"",
"replyingTo": "Replying to %1",
"fileDownloadUnavailable": "This file appears unavailable for download. The sender may have disabled downloads for this file.",
"messageNoReplies": "There are no replies to this message.",
"headingReplies": "Replies",
"viewReplies": "View replies to this message",
"restartFileShare": "Start Sharing File",
"stopSharingFile": "Stop Sharing File",
"manageSharedFiles": "Manage Shared Files",
"localeDe": "Germană",
"localeEn": "Engleză",
"localeLb": "Luxemburgheză",

View File

@ -1,6 +1,28 @@
{
"@@locale": "ru",
"@@last_modified": "2022-06-22T00:46:01+02:00",
"@@last_modified": "2022-07-21T19:54:08+02:00",
"tooltipUnpinConversation": "Unpin conversation from the top of \"Conversations\"",
"tooltipPinConversation": "Pin conversation to the top of \"Conversations\"",
"replyingTo": "Replying to %1",
"fileDownloadUnavailable": "This file appears unavailable for download. The sender may have disabled downloads for this file.",
"messageNoReplies": "There are no replies to this message.",
"headingReplies": "Replies",
"viewReplies": "View replies to this message",
"restartFileShare": "Start Sharing File",
"stopSharingFile": "Stop Sharing File",
"manageSharedFiles": "Manage Shared Files",
"exportProfile": "Экспорт профиля",
"notificationContentContactInfo": "Показать текст сообщения",
"notificationContentSimpleEvent": "Без подробностей",
"settingsGroupExperiments": "ЭКСПЕРИМЕНТЫ",
"settingsGroupAppearance": "НАСТРОЙКИ ОТОБРАЖЕНИЯ",
"settingGroupBehaviour": "ПОВЕДЕНИЕ",
"notificationContentSettingDescription": "Управление уведомлениями чатов",
"conversationNotificationPolicyNever": "Отключить",
"notificationPolicyDefaultAll": "По-умолчанию",
"descriptionFileSharing": "Данная функция позволяет обмениваться файлами напрямую с контактами и группами в Cwtch. Отправляемый файл будет напрямую скачиваться с вашего устройства через Cwtch, внутри сети Tor",
"descriptionExperiments": "Экспериментальные функции Cwtch это необязательные дополнительные функции, которые добавляют некоторые возможности, но не имеют такой же устойчивости к метаданным как если бы вы общались через традиционный чат 1 на 1.",
"profileOnionLabel": "Send this address to contacts you want to connect with",
"localeDe": "Немецкий \/ Deutsch",
"localeDa": "Датский язык \/ Dansk",
"settingImagePreviewsDescription": "Автоматическая загрузка изображений. Обратите внимание, что предварительный просмотр изображений часто может использоваться для взлома или деаномизации. Не используйте данную функцию если Вы контактируете с ненадежными контактами.",
@ -42,19 +64,13 @@
"importProfileTooltip": "Используйте зашифрованную резервную копию Cwtch, чтобы перенести профиль, созданный на другом устройстве где установлен Cwtch..",
"importProfile": "Загрузить профиль",
"exportProfileTooltip": "Сделать зашифрованную резервную копию в файл. Его потом потом можно импортировать на других устройствах где установлен Cwtch.",
"exportProfile": "Сохранить профиль",
"notificationContentContactInfo": "Информация о разговоре",
"notificationContentSimpleEvent": "Обычное событие",
"conversationNotificationPolicySettingDescription": "Настройка уведомлений",
"conversationNotificationPolicySettingLabel": "Настройка уведомлений",
"settingsGroupAppearance": "Настройки отображения",
"notificationContentSettingDescription": "Управление уведомлениями в данной теме",
"notificationPolicySettingDescription": "Настройка уведомлений по-умолчанию",
"notificationContentSettingLabel": "Содержимое уведомления",
"notificationPolicySettingLabel": "Уведомления",
"conversationNotificationPolicyOptIn": "Включить",
"conversationNotificationPolicyDefault": "По-умолчанию",
"notificationPolicyDefaultAll": "Всё по-умолчанию",
"notificationPolicyOptIn": "Включить",
"notificationPolicyMute": "Без звука",
"tooltipSelectACustomProfileImage": "Сменить изображение профиля",
@ -81,7 +97,6 @@
"unlockServerTip": "Создайте или импортируйте сервер, чтобы начать",
"saveServerButton": "Сохранить",
"serverEnabled": "Состояние сервера",
"descriptionFileSharing": "Данная функция позволяет обмениваться файлами напрямую с контактами и группами в Cwtch. Отправляемый файл будет напрямую скачиваться с вашего устройства через Cwtch, поверх скрытой сети Tor",
"settingUIColumnOptionSame": "Как в портретном режиме",
"settingUIColumnSingle": "Один столбец",
"createProfileToBegin": "Пожалуйста, создайте или импортируйте профиль, чтобы начать",
@ -90,15 +105,11 @@
"enableGroups": "Групповые чаты",
"settingTheme": "Ночной режим",
"addNewProfileBtn": "Создать новый профиль",
"profileOnionLabel": "Send this address to contacts you want to connect with",
"savePeerHistoryDescription": "Определяет политику хранения или удаления сообщений с данным контактом",
"savePeerHistory": "Настройка истории",
"deleteConfirmLabel": "Введите УДАЛИТЬ, чтобы продолжить",
"deleteConfirmText": "УДАЛИТЬ",
"settingGroupBehaviour": "Поведение",
"settingsGroupExperiments": "Эксперименты",
"labelTorNetwork": "Сеть Tor",
"conversationNotificationPolicyNever": "Никогда",
"newMessageNotificationConversationInfo": "Новое сообщение от %1",
"newMessageNotificationSimple": "Новое сообщение",
"msgAddToAccept": "Добавьте учетную запись в контакты, чтобы принять этот файл.",
@ -117,7 +128,6 @@
"resetTor": "Сброс",
"descriptionBlockUnknownConnections": "Если включить этот параметр, все соединения от людей, не состоящих в ваших контактах будут отклонены.",
"descriptionExperimentsGroups": "Данная экспериментальная функция позволяет создавать группы в Cwtch, чтобы облегчить Вам общение с более чем одним контактом. Для создания групп необходимо включить функцию создания сервера и создать сервер в главном меню программы.",
"descriptionExperiments": "Экспериментальные функции Cwtch это необязательные дополнительные функции, которые добавляют некоторые возможности, но не имеют такой же устойчивости к метаданным как если бы вы общались через традиционный чат 1 на 1..",
"settingLanguage": "Выбрать язык",
"profileName": "Введите имя...",
"themeNameNeon2": "Неон2",

View File

@ -1,6 +1,9 @@
import 'package:cwtch/main.dart';
import 'package:cwtch/models/profile.dart';
import 'package:cwtch/widgets/messagerow.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:provider/provider.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
import 'message.dart';
@ -51,6 +54,7 @@ class ContactInfoState extends ChangeNotifier {
late bool _isGroup;
String? _server;
late bool _archived;
late bool _pinned;
String? _acnCircuit;
@ -68,7 +72,8 @@ class ContactInfoState extends ChangeNotifier {
lastMessageTime,
server,
archived = false,
notificationPolicy = "ConversationNotificationPolicy.Default"}) {
notificationPolicy = "ConversationNotificationPolicy.Default",
pinned = false}) {
this._nickname = nickname;
this._isGroup = isGroup;
this._accepted = accepted;
@ -84,6 +89,7 @@ class ContactInfoState extends ChangeNotifier {
this._archived = archived;
this._notificationPolicy = notificationPolicyFromString(notificationPolicy);
this.messageCache = new MessageCache(_totalMessages);
this._pinned = pinned;
keys = Map<String, GlobalKey<MessageRowState>>();
}
@ -285,4 +291,27 @@ class ContactInfoState extends ChangeNotifier {
}
return ConversationNotificationPolicy.Never;
}
bool get pinned {
return _pinned;
}
// Pin the conversation to the top of the conversation list
// Requires caller tree to contain a FlwtchState and ProfileInfoState provider.
void pin(context) {
_pinned = true;
var profileHandle = Provider.of<ProfileInfoState>(context, listen: false).onion;
Provider.of<FlwtchState>(context,listen: false).cwtch.SetConversationAttribute(profileHandle, identifier, "profile.pinned", "true");
notifyListeners();
}
// Unpin the conversation from the top of the conversation list
// Requires caller tree to contain a FlwtchState and ProfileInfoState provider.
void unpin(context) {
_pinned = false;
var profileHandle = Provider.of<ProfileInfoState>(context,listen: false).onion;
Provider.of<FlwtchState>(context,listen: false).cwtch.SetConversationAttribute(profileHandle, identifier, "profile.pinned", "false");
notifyListeners();
}
}

View File

@ -51,6 +51,9 @@ class ContactListState extends ChangeNotifier {
// return -1 = a first in list
// return 1 = b first in list
// pinned contacts first
if (a.pinned != true && b.pinned == true) return 1;
if (a.pinned == true && b.pinned != true) return -1;
// blocked contacts last
if (a.isBlocked == true && b.isBlocked != true) return 1;
if (a.isBlocked != true && b.isBlocked == true) return -1;

View File

@ -7,8 +7,10 @@ class FileDownloadProgress {
String? downloadedTo;
DateTime? timeStart;
DateTime? timeEnd;
DateTime? requested;
FileDownloadProgress(this.chunksTotal, this.timeStart);
double progress() {
return 1.0 * chunksDownloaded / chunksTotal;
}

View File

@ -201,6 +201,42 @@ class ByContentHash implements CacheHandler {
}
}
List<Message> getReplies(MessageCache cache, int messageIdentifier) {
List<Message> replies = List.empty(growable: true);
try {
MessageInfo original = cache.cache[messageIdentifier]!;
String hash = original.metadata.contenthash;
cache.cache.forEach((key, messageInfo) {
// only bother searching for identifiers that came *after*
if (key > messageIdentifier) {
try {
dynamic message = jsonDecode(messageInfo.wrapper);
var content = message['d'] as dynamic;
dynamic qmessage = jsonDecode(content);
if (qmessage["body"] == null || qmessage["quotedHash"] == null) {
return;
}
if (qmessage["quotedHash"] == hash) {
replies.add(compileOverlay(messageInfo));
}
} catch (e) {
// ignore
}
}
});
} catch (e) {
EnvironmentConfig.debugLog("message handler exception on get from cache: $e");
}
replies.sort((a, b) {
return a.getMetadata().messageID.compareTo(b.getMetadata().messageID);
});
return replies;
}
Future<Message> messageHandler(BuildContext context, String profileOnion, int conversationIdentifier, CacheHandler cacheHandler) async {
var malformedMetadata = MessageMetadata(profileOnion, conversationIdentifier, 0, DateTime.now(), "", "", "", <String, String>{}, false, true, false, "");
var cwtch = Provider.of<FlwtchState>(context, listen: false).cwtch;

View File

@ -36,7 +36,6 @@ class QuotedMessage extends Message {
var content = message["body"];
return Text(
content,
overflow: TextOverflow.ellipsis,
);
} catch (e) {
return MalformedBubble();

View File

@ -67,6 +67,7 @@ class ProfileInfoState extends ChangeNotifier {
server: contact["groupServer"],
archived: contact["isArchived"] == true,
lastMessageTime: DateTime.fromMillisecondsSinceEpoch(1000 * int.parse(contact["lastMsgTime"])),
pinned: contact["attributes"]?["local.profile.pinned"] == "true",
notificationPolicy: contact["notificationPolicy"] ?? "ConversationNotificationPolicy.Default");
}));
@ -291,12 +292,27 @@ class ProfileInfoState extends ChangeNotifier {
}
bool downloadInterrupted(String fileKey) {
return this._downloads.containsKey(fileKey) && this._downloads[fileKey]!.interrupted;
if (this._downloads.containsKey(fileKey)) {
if (this._downloads[fileKey]!.interrupted) {
return true;
}
if (this._downloads[fileKey]!.requested != null) {
if (DateTime.now().difference(this._downloads[fileKey]!.requested!) > Duration(minutes: 1)) {
this._downloads[fileKey]!.requested = null;
this._downloads[fileKey]!.interrupted = true;
return true;
}
}
}
return false;
}
void downloadMarkResumed(String fileKey) {
if (this._downloads.containsKey(fileKey)) {
this._downloads[fileKey]!.interrupted = false;
this._downloads[fileKey]!.requested = DateTime.now();
notifyListeners();
}
}

View File

@ -1,3 +1,4 @@
import 'package:cwtch/config.dart';
import 'package:flutter/cupertino.dart';
import 'contact.dart';
@ -13,7 +14,7 @@ class RemoteServerInfoState extends ChangeNotifier {
DateTime lastPreSyncMessagTime = new DateTime(2020);
RemoteServerInfoState(this.onion, this.identifier, this.description, this._status, {lastPreSyncMessageTime, mostRecentMessageTime}) {
if (_status == "Authenticated") {
if (_status == "Authenticated" || _status == "Synced") {
this.lastPreSyncMessagTime = lastPreSyncMessageTime;
updateSyncProgressFor(mostRecentMessageTime);
}

View File

@ -279,8 +279,8 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
ElevatedButton(
onPressed: _createPressed,
style: ElevatedButton.styleFrom(
minimumSize: Size(400, 50),
maximumSize: Size(800, 50),
minimumSize: Size(400, 75),
maximumSize: Size(800, 75),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.horizontal(left: Radius.circular(180), right: Radius.circular(180))),
),
child: Text(
@ -297,8 +297,8 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
message: AppLocalizations.of(context)!.exportProfileTooltip,
child: ElevatedButton.icon(
style: ElevatedButton.styleFrom(
minimumSize: Size(400, 50),
maximumSize: Size(800, 50),
minimumSize: Size(400, 75),
maximumSize: Size(800, 75),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.horizontal(left: Radius.circular(180), right: Radius.circular(180))),
),
onPressed: () {
@ -328,8 +328,8 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
message: AppLocalizations.of(context)!.enterCurrentPasswordForDelete,
child: ElevatedButton.icon(
style: ElevatedButton.styleFrom(
minimumSize: Size(400, 50),
maximumSize: Size(800, 50),
minimumSize: Size(400, 75),
maximumSize: Size(800, 75),
shape: RoundedRectangleBorder(
side: BorderSide(color: Provider.of<Settings>(context).theme.defaultButtonActiveColor, width: 2.0),
borderRadius: BorderRadius.horizontal(left: Radius.circular(180), right: Radius.circular(180))),

View File

@ -1,3 +1,5 @@
import 'dart:io';
import 'package:cwtch/cwtch/cwtch.dart';
import 'package:cwtch/cwtch_icons_icons.dart';
import 'package:cwtch/models/appstate.dart';
@ -289,7 +291,7 @@ class _ContactsViewState extends State<ContactsView> {
padding: MediaQuery.of(context).viewInsets,
child: RepaintBoundary(
child: Container(
height: 200, // bespoke value courtesy of the [TextField] docs
height: Platform.isAndroid ? 250 : 200, // bespoke value courtesy of the [TextField] docs
child: Center(
child: Padding(
padding: EdgeInsets.all(2.0),
@ -306,7 +308,7 @@ class _ContactsViewState extends State<ContactsView> {
message: AppLocalizations.of(context)!.tooltipAddContact,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
minimumSize: Size.fromWidth(double.infinity),
minimumSize: Size.fromWidth(399),
maximumSize: Size.fromWidth(400),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.horizontal(left: Radius.circular(180), right: Radius.circular(180))),
),
@ -328,7 +330,7 @@ class _ContactsViewState extends State<ContactsView> {
message: groupsEnabled ? AppLocalizations.of(context)!.addServerTooltip : AppLocalizations.of(context)!.thisFeatureRequiresGroupExpermientsToBeEnabled,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
minimumSize: Size.fromWidth(double.infinity),
minimumSize: Size.fromWidth(399),
maximumSize: Size.fromWidth(400),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.horizontal(left: Radius.circular(180), right: Radius.circular(180))),
),
@ -353,7 +355,7 @@ class _ContactsViewState extends State<ContactsView> {
message: groupsEnabled ? AppLocalizations.of(context)!.createGroupTitle : AppLocalizations.of(context)!.thisFeatureRequiresGroupExpermientsToBeEnabled,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
minimumSize: Size.fromWidth(double.infinity),
minimumSize: Size.fromWidth(399),
maximumSize: Size.fromWidth(400),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.horizontal(left: Radius.circular(180), right: Radius.circular(180))),
),

View File

@ -0,0 +1,82 @@
import 'dart:convert';
import 'package:cwtch/config.dart';
import 'package:cwtch/cwtch/cwtch.dart';
import 'package:cwtch/main.dart';
import 'package:cwtch/models/appstate.dart';
import 'package:cwtch/models/contact.dart';
import 'package:cwtch/models/profile.dart';
import 'package:cwtch/settings.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
import '../cwtch_icons_icons.dart';
class FileSharingView extends StatefulWidget {
@override
_FileSharingViewState createState() => _FileSharingViewState();
}
class _FileSharingViewState extends State<FileSharingView> {
@override
Widget build(BuildContext context) {
var handle = Provider.of<ContactInfoState>(context).nickname;
if (handle.isEmpty) {
handle = Provider.of<ContactInfoState>(context).onion;
}
var profileHandle = Provider.of<ProfileInfoState>(context).onion;
return Scaffold(
appBar: AppBar(
title: Text(handle + " » " + AppLocalizations.of(context)!.manageSharedFiles),
),
body: FutureBuilder(
future: Provider.of<FlwtchState>(context, listen: false).cwtch.GetSharedFiles(profileHandle, Provider.of<ContactInfoState>(context).identifier),
builder: (context, snapshot) {
if (snapshot.hasData) {
List<dynamic> sharedFiles = jsonDecode(snapshot.data as String);
sharedFiles.sort((a, b) {
return a["DateShared"].toString().compareTo(b["DateShared"].toString());
});
var fileList = ScrollablePositionedList.separated(
itemScrollController: ItemScrollController(),
itemCount: sharedFiles.length,
shrinkWrap: true,
physics: BouncingScrollPhysics(),
semanticChildCount: sharedFiles.length,
itemBuilder: (context, index) {
String filekey = sharedFiles[index]["FileKey"];
EnvironmentConfig.debugLog("$sharedFiles " + sharedFiles[index].toString());
return SwitchListTile(
title: Text(sharedFiles[index]["Path"]),
subtitle: Text(sharedFiles[index]["DateShared"]),
value: sharedFiles[index]["Active"],
activeTrackColor: Provider.of<Settings>(context).theme.defaultButtonColor,
inactiveTrackColor: Provider.of<Settings>(context).theme.defaultButtonDisabledColor,
secondary: Icon(CwtchIcons.attach_file_24px, color: Provider.of<Settings>(context).current().mainTextColor),
onChanged: (newValue) {
setState(() {
if (newValue) {
Provider.of<FlwtchState>(context, listen: false).cwtch.RestartSharing(profileHandle, filekey);
} else {
Provider.of<FlwtchState>(context, listen: false).cwtch.StopSharing(profileHandle, filekey);
}
});
});
},
separatorBuilder: (BuildContext context, int index) {
return Divider(height: 1);
},
);
return fileList;
}
return Container();
},
),
);
}
}

View File

@ -32,6 +32,7 @@ import '../constants.dart';
import '../main.dart';
import '../settings.dart';
import '../widgets/messagelist.dart';
import 'filesharingview.dart';
import 'groupsettingsview.dart';
class MessageView extends StatefulWidget {
@ -97,6 +98,12 @@ class _MessageViewState extends State<MessageView> {
var showMessageFormattingPreview = Provider.of<Settings>(context).isExperimentEnabled(FormattingExperiment);
var showFileSharing = Provider.of<Settings>(context).isExperimentEnabled(FileSharingExperiment);
var appBarButtons = <Widget>[];
if (showFileSharing) {
appBarButtons.add(
IconButton(splashRadius: Material.defaultSplashRadius / 2, icon: Icon(Icons.folder_shared), tooltip: AppLocalizations.of(context)!.manageSharedFiles, onPressed: _pushFileSharingSettings));
}
if (Provider.of<ContactInfoState>(context).isOnline()) {
if (showFileSharing) {
appBarButtons.add(IconButton(
@ -119,6 +126,7 @@ class _MessageViewState extends State<MessageView> {
},
));
}
appBarButtons.add(IconButton(
splashRadius: Material.defaultSplashRadius / 2,
icon: Icon(CwtchIcons.send_invite, size: 24),
@ -200,6 +208,23 @@ class _MessageViewState extends State<MessageView> {
return true;
}
void _pushFileSharingSettings() {
var profileInfoState = Provider.of<ProfileInfoState>(context, listen: false);
var contactInfoState = Provider.of<ContactInfoState>(context, listen: false);
Navigator.of(context).push(
PageRouteBuilder(
pageBuilder: (builderContext, a1, a2) {
return MultiProvider(
providers: [ChangeNotifierProvider.value(value: profileInfoState), ChangeNotifierProvider.value(value: contactInfoState)],
child: FileSharingView(),
);
},
transitionsBuilder: (c, anim, a2, child) => FadeTransition(opacity: anim, child: child),
transitionDuration: Duration(milliseconds: 200),
),
);
}
void _pushContactSettings() {
var profileInfoState = Provider.of<ProfileInfoState>(context, listen: false);
var contactInfoState = Provider.of<ContactInfoState>(context, listen: false);
@ -603,6 +628,10 @@ class _MessageViewState extends State<MessageView> {
var data = event.data;
if (event is RawKeyUpEvent) {
if ((data.logicalKey == LogicalKeyboardKey.enter && !event.isShiftPressed) || data.logicalKey == LogicalKeyboardKey.numpadEnter && !event.isShiftPressed) {
// Don't send when inserting a new line that is not at the end of the message
if (ctrlrCompose.selection.baseOffset != ctrlrCompose.text.length) {
return;
}
_sendMessage();
}
}
@ -625,6 +654,7 @@ class _MessageViewState extends State<MessageView> {
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Text(AppLocalizations.of(bcontext)!.invitationLabel),
SizedBox(
@ -633,10 +663,10 @@ class _MessageViewState extends State<MessageView> {
ChangeNotifierProvider.value(
value: Provider.of<ProfileInfoState>(ctx, listen: false),
child: DropdownContacts(filter: (contact) {
return contact.onion != Provider.of<ContactInfoState>(context).onion;
return contact.onion != Provider.of<ContactInfoState>(ctx).onion;
}, onChanged: (newVal) {
setState(() {
this.selectedContact = Provider.of<ProfileInfoState>(context, listen: false).contactList.findContact(newVal)!.identifier;
this.selectedContact = Provider.of<ProfileInfoState>(ctx, listen: false).contactList.findContact(newVal)!.identifier;
});
})),
SizedBox(

View File

@ -206,7 +206,7 @@ class _ProfileMgrViewState extends State<ProfileMgrView> {
padding: MediaQuery.of(context).viewInsets,
child: RepaintBoundary(
child: Container(
height: 200, // bespoke value courtesy of the [TextField] docs
height: Platform.isAndroid ? 250 : 200, // bespoke value courtesy of the [TextField] docs
child: Center(
child: Padding(
padding: EdgeInsets.all(10.0),
@ -221,7 +221,7 @@ class _ProfileMgrViewState extends State<ProfileMgrView> {
Expanded(
child: ElevatedButton(
style: ElevatedButton.styleFrom(
minimumSize: Size(double.infinity, 20),
minimumSize: Size(399, 20),
maximumSize: Size(400, 20),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.horizontal(left: Radius.circular(180), right: Radius.circular(180))),
),
@ -242,15 +242,16 @@ class _ProfileMgrViewState extends State<ProfileMgrView> {
message: AppLocalizations.of(context)!.importProfileTooltip,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
minimumSize: Size(double.infinity, 20),
minimumSize: Size(399, 20),
maximumSize: Size(400, 20),
shape: RoundedRectangleBorder(
side: BorderSide(color: Provider.of<Settings>(context).theme.defaultButtonActiveColor, width: 2.0),
borderRadius: BorderRadius.horizontal(left: Radius.circular(180), right: Radius.circular(180))),
primary: Provider.of<Settings>(context).theme.backgroundMainColor,
),
child:
Text(AppLocalizations.of(context)!.importProfile, semanticsLabel: AppLocalizations.of(context)!.importProfile, style: TextStyle(fontWeight: FontWeight.bold)),
child: Text(AppLocalizations.of(context)!.importProfile,
semanticsLabel: AppLocalizations.of(context)!.importProfile,
style: TextStyle(color: Provider.of<Settings>(context).theme.mainTextColor, fontWeight: FontWeight.bold)),
onPressed: () {
// 10GB profiles should be enough for anyone?
showFilePicker(context, MaxGeneralFileSharingSize, (file) {
@ -287,7 +288,7 @@ class _ProfileMgrViewState extends State<ProfileMgrView> {
padding: MediaQuery.of(context).viewInsets,
child: RepaintBoundary(
child: Container(
height: 200, // bespoke value courtesy of the [TextField] docs
height: Platform.isAndroid ? 250 : 200, // bespoke value courtesy of the [TextField] docs
child: Center(
child: Padding(
padding: EdgeInsets.all(10.0),

View File

@ -29,6 +29,7 @@ class _DropdownContactsState extends State<DropdownContacts> {
@override
Widget build(BuildContext context) {
return DropdownButton(
isExpanded: true, // magic property
value: this.selected,
items: Provider.of<ProfileInfoState>(context, listen: false).contactList.contacts.where(widget.filter).map<DropdownMenuItem<String>>((ContactInfoState contact) {
return DropdownMenuItem<String>(value: contact.onion, child: Text(contact.nickname));

View File

@ -2,6 +2,7 @@ import 'dart:io';
import 'package:cwtch/models/appstate.dart';
import 'package:cwtch/models/contact.dart';
import 'package:cwtch/models/contactlist.dart';
import 'package:cwtch/models/profile.dart';
import 'package:cwtch/views/contactsview.dart';
import 'package:flutter/material.dart';
@ -18,6 +19,9 @@ class ContactRow extends StatefulWidget {
}
class _ContactRowState extends State<ContactRow> {
bool isHover = false;
@override
Widget build(BuildContext context) {
var contact = Provider.of<ContactInfoState>(context);
@ -123,10 +127,30 @@ class _ContactRowState extends State<ContactRow> {
),
],
))),
Visibility(visible: Platform.isAndroid || (!Platform.isAndroid && isHover) || contact.pinned, child:
IconButton(
tooltip: contact.pinned ? AppLocalizations.of(context)!.tooltipUnpinConversation :AppLocalizations.of(context)!.tooltipPinConversation ,
icon: Icon(contact.pinned ? Icons.push_pin : Icons.push_pin_outlined,
color: Provider.of<Settings>(context).theme.mainTextColor,),
onPressed: () {
if (contact.pinned) {
contact.unpin(context);
} else {
contact.pin(context);
}
Provider.of<ContactListState>(context, listen: false).resort();
},
))
]),
onTap: () {
selectConversation(context, contact.identifier);
},
onHover: (onHover) {
setState(() {
isHover = onHover;
});
},
));
}

View File

@ -1,4 +1,5 @@
import 'dart:io';
import 'dart:math';
import 'package:cwtch/config.dart';
import 'package:cwtch/models/contact.dart';
@ -53,7 +54,7 @@ class FileBubbleState extends State<FileBubble> {
filterQuality: FilterQuality.medium,
fit: BoxFit.scaleDown,
alignment: Alignment.center,
height: MediaQuery.of(context).size.height * 0.30,
height: min(MediaQuery.of(context).size.height * 0.30, 150),
isAntiAlias: false,
errorBuilder: (context, error, stackTrace) {
return MalformedBubble();
@ -153,6 +154,7 @@ class FileBubbleState extends State<FileBubble> {
if (Provider.of<Settings>(context).shouldPreview(path)) {
isPreview = true;
wdgDecorations = Center(
widthFactor: 1.0,
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
@ -223,7 +225,18 @@ class FileBubbleState extends State<FileBubble> {
crossAxisAlignment: fromMe ? CrossAxisAlignment.end : CrossAxisAlignment.start,
mainAxisAlignment: fromMe ? MainAxisAlignment.end : MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [wdgSender, isPreview ? Container() : wdgMessage, wdgDecorations, messageStatusWidget]),
children: [
wdgSender,
isPreview
? Container(
width: 0,
padding: EdgeInsets.zero,
margin: EdgeInsets.zero,
)
: wdgMessage,
wdgDecorations,
messageStatusWidget
]),
));
});
}
@ -253,7 +266,7 @@ class FileBubbleState extends State<FileBubble> {
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.SetMessageAttribute(profileOnion, conversation, 0, idx, "file-downloaded", "true");
ContactInfoState? contact = Provider.of<ProfileInfoState>(context, listen: false).contactList.findContact(Provider.of<MessageMetadata>(context).senderHandle);
ContactInfoState? contact = Provider.of<ProfileInfoState>(context, listen: false).contactList.findContact(Provider.of<MessageMetadata>(context, listen: false).senderHandle);
if (contact != null) {
Provider.of<FlwtchState>(context, listen: false).cwtch.DownloadFile(profileOnion, contact.identifier, file.path, manifestPath, widget.fileKey());
}

View File

@ -7,6 +7,7 @@ import 'package:cwtch/models/contact.dart';
import 'package:cwtch/models/message.dart';
import 'package:cwtch/models/profile.dart';
import 'package:cwtch/views/contactsview.dart';
import 'package:cwtch/widgets/staticmessagebubble.dart';
import 'package:flutter/material.dart';
import 'package:cwtch/widgets/profileimage.dart';
import 'package:flutter/physics.dart';
@ -15,6 +16,7 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
import '../main.dart';
import '../models/messagecache.dart';
import '../settings.dart';
class MessageRow extends StatefulWidget {
@ -74,7 +76,7 @@ class MessageRowState extends State<MessageRow> with SingleTickerProviderStateMi
}
}
Widget wdgIcons = Platform.isAndroid
Widget wdgReply = Platform.isAndroid
? SizedBox.shrink()
: Visibility(
visible: EnvironmentConfig.TEST_MODE || Provider.of<AppState>(context).hoveredIndex == Provider.of<MessageMetadata>(context).messageID,
@ -90,13 +92,37 @@ class MessageRowState extends State<MessageRow> with SingleTickerProviderStateMi
},
icon: Icon(Icons.reply, color: Provider.of<Settings>(context).theme.dropShadowColor)));
var settings = Provider.of<Settings>(context);
var pis = Provider.of<ProfileInfoState>(context);
var cis = Provider.of<ContactInfoState>(context);
var borderColor = Provider.of<Settings>(context).theme.portraitOnlineBorderColor;
var messageID = Provider.of<MessageMetadata>(context).messageID;
var cache = Provider.of<ContactInfoState>(context).messageCache;
Widget wdgSeeReplies = Platform.isAndroid
? SizedBox.shrink()
: Visibility(
visible: EnvironmentConfig.TEST_MODE || Provider.of<AppState>(context).hoveredIndex == Provider.of<MessageMetadata>(context).messageID,
maintainSize: true,
maintainAnimation: true,
maintainState: true,
maintainInteractivity: false,
child: IconButton(
tooltip: AppLocalizations.of(context)!.viewReplies,
splashRadius: Material.defaultSplashRadius / 2,
onPressed: () {
modalShowReplies(context, AppLocalizations.of(context)!.headingReplies, AppLocalizations.of(context)!.messageNoReplies, settings, pis, cis, borderColor, cache, messageID);
},
icon: Icon(Icons.message_rounded, color: Provider.of<Settings>(context).theme.dropShadowColor)));
Widget wdgSpacer = Flexible(flex: 1, child: SizedBox(width: Platform.isAndroid ? 20 : 60, height: 10));
var widgetRow = <Widget>[];
if (fromMe) {
widgetRow = <Widget>[
wdgSpacer,
wdgIcons,
wdgSeeReplies,
wdgReply,
actualMessage,
];
} else if (isBlocked && !showBlockedMessage) {
@ -143,7 +169,8 @@ class MessageRowState extends State<MessageRow> with SingleTickerProviderStateMi
});
})),
]))),
wdgIcons,
wdgReply,
wdgSeeReplies,
wdgSpacer,
];
} else {
@ -179,7 +206,8 @@ class MessageRowState extends State<MessageRow> with SingleTickerProviderStateMi
widgetRow = <Widget>[
wdgPortrait,
actualMessage,
wdgIcons,
wdgReply,
wdgSeeReplies,
wdgSpacer,
];
}
@ -212,6 +240,9 @@ class MessageRowState extends State<MessageRow> with SingleTickerProviderStateMi
_runAnimation(details.velocity.pixelsPerSecond, size);
Provider.of<AppState>(context, listen: false).selectedIndex = Provider.of<MessageMetadata>(context, listen: false).messageID;
},
onLongPress: () async {
modalShowReplies(context, AppLocalizations.of(context)!.headingReplies, AppLocalizations.of(context)!.messageNoReplies, settings, pis, cis, borderColor, cache, messageID);
},
child: Padding(
padding: EdgeInsets.all(2),
child: Align(
@ -332,3 +363,89 @@ class MessageRowState extends State<MessageRow> with SingleTickerProviderStateMi
);
}
}
void modalShowReplies(
BuildContext ctx, String replyHeader, String noRepliesText, Settings settings, ProfileInfoState profile, ContactInfoState cis, Color borderColor, MessageCache cache, int messageID,
{bool showImage = true}) {
showModalBottomSheet<void>(
context: ctx,
builder: (BuildContext bcontext) {
List<Message> replies = getReplies(cache, messageID);
return ChangeNotifierProvider.value(
value: profile,
builder: (bcontext, child) {
return LayoutBuilder(builder: (BuildContext context, BoxConstraints viewportConstraints) {
var replyWidgets = replies.map((e) {
var fromMe = e.getMetadata().senderHandle == profile.onion;
var bubble = StaticMessageBubble(profile, settings, e.getMetadata(), Row(children: [Flexible(child: e.getPreviewWidget(context))]));
String imagePath = e.getMetadata().senderImage!;
var sender = profile.contactList.findContact(e.getMetadata().senderHandle);
if (sender != null) {
imagePath = showImage ? sender.imagePath : sender.defaultImagePath;
}
if (fromMe) {
imagePath = profile.imagePath;
}
var image = Padding(
padding: EdgeInsets.all(4.0),
child: ProfileImage(
imagePath: imagePath,
diameter: 48.0,
border: borderColor,
badgeTextColor: Colors.red,
badgeColor: Colors.red,
));
return Padding(
padding: EdgeInsets.all(10.0),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [image, Flexible(child: bubble)],
));
}).toList();
var withHeader = replyWidgets;
var original =
StaticMessageBubble(profile, settings, cache.cache[messageID]!.metadata, Row(children: [Flexible(child: compileOverlay(cache.cache[messageID]!).getPreviewWidget(context))]));
withHeader.insert(0, Padding(padding: EdgeInsets.fromLTRB(10.0, 10.0, 2.0, 15.0), child: Center(child: original)));
withHeader.insert(
1,
Padding(
padding: EdgeInsets.fromLTRB(10.0, 10.0, 2.0, 15.0),
child: Divider(
color: settings.theme.mainTextColor,
)));
if (replies.isNotEmpty) {
withHeader.insert(2, Padding(padding: EdgeInsets.fromLTRB(10.0, 10.0, 2.0, 15.0), child: Text(replyHeader, style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold))));
} else {
withHeader.insert(
2, Padding(padding: EdgeInsets.fromLTRB(10.0, 10.0, 2.0, 15.0), child: Center(child: Text(noRepliesText, style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)))));
}
return Scrollbar(
isAlwaysShown: true,
child: SingleChildScrollView(
clipBehavior: Clip.antiAlias,
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: viewportConstraints.maxHeight,
),
child: Padding(
padding: EdgeInsets.symmetric(vertical: 0.0, horizontal: 20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: withHeader,
)))));
});
});
});
}

View File

@ -0,0 +1,72 @@
import 'package:cwtch/models/contact.dart';
import 'package:cwtch/models/message.dart';
import 'package:cwtch/models/profile.dart';
import 'package:cwtch/widgets/malformedbubble.dart';
import 'package:flutter/material.dart';
import '../settings.dart';
import 'messagebubbledecorations.dart';
class StaticMessageBubble extends StatefulWidget {
final ProfileInfoState profile;
final Settings settings;
final MessageMetadata metadata;
final Widget child;
StaticMessageBubble(this.profile, this.settings, this.metadata, this.child);
@override
StaticMessageBubbleState createState() => StaticMessageBubbleState();
}
class StaticMessageBubbleState extends State<StaticMessageBubble> {
@override
Widget build(BuildContext context) {
var fromMe = widget.metadata.senderHandle == widget.profile.onion;
var borderRadiousEh = 15.0;
DateTime messageDate = widget.metadata.timestamp;
// If the sender is not us, then we want to give them a nickname...
var senderDisplayStr = "";
if (!fromMe) {
ContactInfoState? contact = widget.profile.contactList.findContact(widget.metadata.senderHandle);
if (contact != null) {
senderDisplayStr = contact.nickname;
} else {
senderDisplayStr = widget.metadata.senderHandle;
}
} else {
senderDisplayStr = widget.profile.nickname;
}
var wdgSender = SelectableText(senderDisplayStr, style: TextStyle(fontSize: 9.0, color: fromMe ? widget.settings.theme.messageFromMeTextColor : widget.settings.theme.messageFromOtherTextColor));
var wdgDecorations = MessageBubbleDecoration(ackd: widget.metadata.ackd, errored: widget.metadata.error, fromMe: fromMe, messageDate: messageDate);
var error = widget.metadata.error;
return LayoutBuilder(builder: (context, constraints) {
//print(constraints.toString()+", "+constraints.maxWidth.toString());
return RepaintBoundary(
child: Container(
child: Container(
decoration: BoxDecoration(
color: error ? malformedColor : (fromMe ? widget.settings.theme.messageFromMeBackgroundColor : widget.settings.theme.messageFromOtherBackgroundColor),
border: Border.all(color: error ? malformedColor : (fromMe ? widget.settings.theme.messageFromMeBackgroundColor : widget.settings.theme.messageFromOtherBackgroundColor), width: 1),
borderRadius: BorderRadius.only(
topLeft: Radius.circular(borderRadiousEh),
topRight: Radius.circular(borderRadiousEh),
bottomLeft: Radius.zero,
bottomRight: Radius.circular(borderRadiousEh),
),
),
child: Padding(
padding: EdgeInsets.all(9.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [wdgSender, widget.child, wdgDecorations])))));
});
}
}

View File

@ -15,7 +15,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 1.7.1+29
version: 1.8.0+34
environment:
sdk: ">=2.15.0 <3.0.0"