Merge branch 'trunk' into winInstructions
continuous-integration/drone/pr Build is pending
Details
continuous-integration/drone/pr Build is pending
Details
This commit is contained in:
commit
5139846f31
|
@ -229,7 +229,7 @@ steps:
|
||||||
status: [ success ]
|
status: [ success ]
|
||||||
environment:
|
environment:
|
||||||
pfx:
|
pfx:
|
||||||
from_secret: pfx
|
from_secret: pfx2022_b64
|
||||||
pfx_pass:
|
pfx_pass:
|
||||||
from_secret: pfx_pass
|
from_secret: pfx_pass
|
||||||
commands:
|
commands:
|
||||||
|
@ -245,6 +245,8 @@ steps:
|
||||||
- echo $Env:pfx > codesign.pfx.b64
|
- echo $Env:pfx > codesign.pfx.b64
|
||||||
- certutil -decode codesign.pfx.b64 codesign.pfx
|
- 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\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
|
- copy windows\runner\resources\knot_128.ico $Env:releasedir\cwtch.ico
|
||||||
- makensis windows\nsis\cwtch-installer.nsi
|
- makensis windows\nsis\cwtch-installer.nsi
|
||||||
- move windows\nsis\cwtch-installer.exe cwtch-installer.exe
|
- move windows\nsis\cwtch-installer.exe cwtch-installer.exe
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
2022-06-22-15-36-1.7.1-10-g231e27d
|
2022-07-22-12-41-v1.8.0-7-g7b3e842
|
|
@ -1 +1 @@
|
||||||
2022-06-21-22-49-1.7.1-2-gff23465
|
2022-07-22-16-41-v1.8.0-7-g7b3e842
|
|
@ -99,7 +99,10 @@ dependencies {
|
||||||
//implementation("androidx.work:work-runtime:$work_version")
|
//implementation("androidx.work:work-runtime:$work_version")
|
||||||
|
|
||||||
// Kotlin + coroutines
|
// 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
|
// optional - RxJava2 support
|
||||||
//implementation("androidx.work:work-rxjava2:$work_version")
|
//implementation("androidx.work:work-rxjava2:$work_version")
|
||||||
|
|
|
@ -16,7 +16,8 @@
|
||||||
android:theme="@style/NormalTheme"
|
android:theme="@style/NormalTheme"
|
||||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||||
android:hardwareAccelerated="true"
|
android:hardwareAccelerated="true"
|
||||||
android:windowSoftInputMode="adjustResize">
|
android:windowSoftInputMode="adjustResize"
|
||||||
|
android:exported="true">
|
||||||
<!-- Specifies an Android theme to apply to this Activity as soon as
|
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||||
the Android process has started. This theme is visible to the user
|
the Android process has started. This theme is visible to the user
|
||||||
while the Flutter UI initializes. After that, this theme continues
|
while the Flutter UI initializes. After that, this theme continues
|
||||||
|
|
|
@ -1,25 +1,27 @@
|
||||||
package im.cwtch.flwtch
|
package im.cwtch.flwtch
|
||||||
|
|
||||||
import android.app.*
|
import android.app.Notification
|
||||||
import android.os.Environment
|
import android.app.NotificationChannel
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import android.app.PendingIntent
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||||
import androidx.work.*
|
import androidx.work.CoroutineWorker
|
||||||
|
import androidx.work.ForegroundInfo
|
||||||
|
import androidx.work.WorkerParameters
|
||||||
import cwtch.Cwtch
|
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.Files
|
||||||
import java.nio.file.Paths
|
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) {
|
||||||
|
@ -82,6 +84,10 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) :
|
||||||
|
|
||||||
Log.i(TAG, "startCwtch success, starting coroutine AppbusEvent loop...")
|
Log.i(TAG, "startCwtch success, starting coroutine AppbusEvent loop...")
|
||||||
val downloadIDs = mutableMapOf<String, Int>()
|
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) {
|
while (true) {
|
||||||
try {
|
try {
|
||||||
val evt = MainActivity.AppbusEvent(Cwtch.getAppBusEvent())
|
val evt = MainActivity.AppbusEvent(Cwtch.getAppBusEvent())
|
||||||
|
@ -109,12 +115,11 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) :
|
||||||
intent.action = Intent.ACTION_RUN
|
intent.action = Intent.ACTION_RUN
|
||||||
intent.putExtra("EventType", "NotificationClicked")
|
intent.putExtra("EventType", "NotificationClicked")
|
||||||
}
|
}
|
||||||
|
|
||||||
val newNotification = NotificationCompat.Builder(applicationContext, channelId)
|
val newNotification = NotificationCompat.Builder(applicationContext, channelId)
|
||||||
.setContentTitle("Cwtch")
|
.setContentTitle("Cwtch")
|
||||||
.setContentText(notificationSimple ?: "New Message")
|
.setContentText(notificationSimple ?: "New Message")
|
||||||
.setSmallIcon(R.mipmap.knott_transparent)
|
.setSmallIcon(R.mipmap.knott_transparent)
|
||||||
.setContentIntent(PendingIntent.getActivity(applicationContext, 1, clickIntent, PendingIntent.FLAG_UPDATE_CURRENT))
|
.setContentIntent(PendingIntent.getActivity(applicationContext, 1, clickIntent, flags))
|
||||||
.setAutoCancel(true)
|
.setAutoCancel(true)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
|
@ -147,7 +152,7 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) :
|
||||||
?: "New Message From %1").replace("%1", data.getString("Nick")))
|
?: "New Message From %1").replace("%1", data.getString("Nick")))
|
||||||
.setLargeIcon(BitmapFactory.decodeStream(fh))
|
.setLargeIcon(BitmapFactory.decodeStream(fh))
|
||||||
.setSmallIcon(R.mipmap.knott_transparent)
|
.setSmallIcon(R.mipmap.knott_transparent)
|
||||||
.setContentIntent(PendingIntent.getActivity(applicationContext, 1, clickIntent, PendingIntent.FLAG_UPDATE_CURRENT))
|
.setContentIntent(PendingIntent.getActivity(applicationContext, 1, clickIntent, flags))
|
||||||
.setAutoCancel(true)
|
.setAutoCancel(true)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
|
@ -265,6 +270,10 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) :
|
||||||
intent.action = Intent.ACTION_RUN
|
intent.action = Intent.ACTION_RUN
|
||||||
intent.putExtra("EventType", "ShutdownClicked")
|
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)
|
val notification = NotificationCompat.Builder(applicationContext, channelId)
|
||||||
.setContentTitle(title)
|
.setContentTitle(title)
|
||||||
|
@ -274,7 +283,7 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) :
|
||||||
.setOngoing(true)
|
.setOngoing(true)
|
||||||
// Add the cancel action to the notification which can
|
// Add the cancel action to the notification which can
|
||||||
// be used to cancel the worker
|
// 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()
|
.build()
|
||||||
|
|
||||||
return ForegroundInfo(101, notification)
|
return ForegroundInfo(101, notification)
|
||||||
|
|
|
@ -313,6 +313,7 @@ class MainActivity: FlutterActivity() {
|
||||||
result.success(Cwtch.sendInvitation(profile, conversation.toLong(), target.toLong()))
|
result.success(Cwtch.sendInvitation(profile, conversation.toLong(), target.toLong()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
"ShareFile" -> {
|
"ShareFile" -> {
|
||||||
val profile: String = call.argument("ProfileOnion") ?: ""
|
val profile: String = call.argument("ProfileOnion") ?: ""
|
||||||
val conversation: Int = call.argument("conversation") ?: 0
|
val conversation: Int = call.argument("conversation") ?: 0
|
||||||
|
@ -321,6 +322,27 @@ class MainActivity: FlutterActivity() {
|
||||||
return
|
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" -> {
|
"CreateProfile" -> {
|
||||||
val nick: String = call.argument("nick") ?: ""
|
val nick: String = call.argument("nick") ?: ""
|
||||||
val pass: String = call.argument("pass") ?: ""
|
val pass: String = call.argument("pass") ?: ""
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
app:lottie_autoPlay="true"
|
app:lottie_autoPlay="true"
|
||||||
app:lottie_rawRes="@raw/cwtch_animated_logo_op"
|
app:lottie_rawRes="@raw/cwtch_animated_logo_op"
|
||||||
app:lottie_loop="true"
|
app:lottie_loop="true"
|
||||||
app:lottie_speed="1.00" />
|
app:lottie_speed="1.00"
|
||||||
|
app:lottie_enableMergePathsForKitKatAndAbove="true" />
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -59,6 +59,16 @@ abstract class Cwtch {
|
||||||
|
|
||||||
// ignore: non_constant_identifier_names
|
// ignore: non_constant_identifier_names
|
||||||
Future<dynamic> ShareFile(String profile, int handle, String filepath);
|
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
|
// ignore: non_constant_identifier_names
|
||||||
void DownloadFile(String profile, int handle, String filepath, String manifestpath, String filekey);
|
void DownloadFile(String profile, int handle, String filepath, String manifestpath, String filekey);
|
||||||
// android-only
|
// android-only
|
||||||
|
|
|
@ -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 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 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 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);
|
typedef GetJsonBlobFromStrIntIntFn = Pointer<Utf8> Function(Pointer<Utf8>, int, int, int);
|
||||||
|
|
||||||
|
@ -841,4 +844,40 @@ class CwtchFfi implements Cwtch {
|
||||||
_UnsafeFreePointerAnyUseOfThisFunctionMustBeDoubleApproved(result);
|
_UnsafeFreePointerAnyUseOfThisFunctionMustBeDoubleApproved(result);
|
||||||
return debugResult;
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -333,4 +333,19 @@ class CwtchGomobile implements Cwtch {
|
||||||
// we don't implement it
|
// we don't implement it
|
||||||
return Future.value("{}");
|
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});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,16 @@
|
||||||
{
|
{
|
||||||
"@@locale": "cy",
|
"@@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",
|
"localeDe": "Almaeneg \/ Deutsch",
|
||||||
"localePt": "Portiwgaleg \/ Portuguesa",
|
"localePt": "Portiwgaleg \/ Portuguesa",
|
||||||
"localeRo": "Rwmaneg \/ Română",
|
"localeRo": "Rwmaneg \/ Română",
|
||||||
|
|
|
@ -1,6 +1,16 @@
|
||||||
{
|
{
|
||||||
"@@locale": "da",
|
"@@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",
|
"localeDe": "Tysk \/ Deutsch",
|
||||||
"localeEn": "Engelsk \/ English",
|
"localeEn": "Engelsk \/ English",
|
||||||
"localeFr": "Fransk \/ Français",
|
"localeFr": "Fransk \/ Français",
|
||||||
|
|
|
@ -1,6 +1,16 @@
|
||||||
{
|
{
|
||||||
"@@locale": "de",
|
"@@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",
|
"localeEn": "Englisch \/ English",
|
||||||
"localePl": "Polnisch \/ Polski",
|
"localePl": "Polnisch \/ Polski",
|
||||||
"localeIt": "Italienisch \/ Italiana",
|
"localeIt": "Italienisch \/ Italiana",
|
||||||
|
|
|
@ -1,6 +1,16 @@
|
||||||
{
|
{
|
||||||
"@@locale": "el",
|
"@@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",
|
"localeDe": "Γερμανός \/ Deutsch",
|
||||||
"localeEn": "English \/ English",
|
"localeEn": "English \/ English",
|
||||||
"localeLb": "Λουξεμβουργιανά",
|
"localeLb": "Λουξεμβουργιανά",
|
||||||
|
|
|
@ -1,6 +1,16 @@
|
||||||
{
|
{
|
||||||
"@@locale": "en",
|
"@@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",
|
"localeDe": "German \/ Deutsch",
|
||||||
"localeEn": "English \/ English",
|
"localeEn": "English \/ English",
|
||||||
"localeLb": "Luxembourgish \/ Lëtzebuergesch",
|
"localeLb": "Luxembourgish \/ Lëtzebuergesch",
|
||||||
|
|
|
@ -1,6 +1,16 @@
|
||||||
{
|
{
|
||||||
"@@locale": "es",
|
"@@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",
|
"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.",
|
"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",
|
"tooltipBackToMessageEditing": "Volver a Edición de mensajes",
|
||||||
|
|
|
@ -1,6 +1,16 @@
|
||||||
{
|
{
|
||||||
"@@locale": "fr",
|
"@@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",
|
"localeDe": "Allemand \/ Deutsch",
|
||||||
"localeDa": "Danois \/ Dansk",
|
"localeDa": "Danois \/ Dansk",
|
||||||
"localePt": "Portugais \/ Portuguesa",
|
"localePt": "Portugais \/ Portuguesa",
|
||||||
|
|
|
@ -1,6 +1,16 @@
|
||||||
{
|
{
|
||||||
"@@locale": "it",
|
"@@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",
|
"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.",
|
"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",
|
"localeNo": "Norvegese \/ Norsk",
|
||||||
|
|
|
@ -1,6 +1,16 @@
|
||||||
{
|
{
|
||||||
"@@locale": "lb",
|
"@@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",
|
"localeDe": "Däitsch \/ Deutsch",
|
||||||
"localeEn": "Englesch",
|
"localeEn": "Englesch",
|
||||||
"localeLb": "Lëtzebuergesch",
|
"localeLb": "Lëtzebuergesch",
|
||||||
|
|
|
@ -1,6 +1,16 @@
|
||||||
{
|
{
|
||||||
"@@locale": "no",
|
"@@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",
|
"localeDe": "Tysk \/ Deutsch",
|
||||||
"localeEn": "Engelsk",
|
"localeEn": "Engelsk",
|
||||||
"localeLb": "Luxemburgsk",
|
"localeLb": "Luxemburgsk",
|
||||||
|
|
|
@ -1,6 +1,16 @@
|
||||||
{
|
{
|
||||||
"@@locale": "pl",
|
"@@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",
|
"localeDe": "Niemiecki \/ Deutsch",
|
||||||
"serverLabel": "Server",
|
"serverLabel": "Server",
|
||||||
"deleteBtn": "Usuń",
|
"deleteBtn": "Usuń",
|
||||||
|
|
|
@ -1,6 +1,16 @@
|
||||||
{
|
{
|
||||||
"@@locale": "pt",
|
"@@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",
|
"localeDe": "Alemao \/ Deutsch",
|
||||||
"localeEn": "English \/ English",
|
"localeEn": "English \/ English",
|
||||||
"localeLb": "Luxembourgish \/ Lëtzebuergesch",
|
"localeLb": "Luxembourgish \/ Lëtzebuergesch",
|
||||||
|
|
|
@ -1,6 +1,16 @@
|
||||||
{
|
{
|
||||||
"@@locale": "ro",
|
"@@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ă",
|
"localeDe": "Germană",
|
||||||
"localeEn": "Engleză",
|
"localeEn": "Engleză",
|
||||||
"localeLb": "Luxemburgheză",
|
"localeLb": "Luxemburgheză",
|
||||||
|
|
|
@ -1,6 +1,28 @@
|
||||||
{
|
{
|
||||||
"@@locale": "ru",
|
"@@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",
|
"localeDe": "Немецкий \/ Deutsch",
|
||||||
"localeDa": "Датский язык \/ Dansk",
|
"localeDa": "Датский язык \/ Dansk",
|
||||||
"settingImagePreviewsDescription": "Автоматическая загрузка изображений. Обратите внимание, что предварительный просмотр изображений часто может использоваться для взлома или деаномизации. Не используйте данную функцию если Вы контактируете с ненадежными контактами.",
|
"settingImagePreviewsDescription": "Автоматическая загрузка изображений. Обратите внимание, что предварительный просмотр изображений часто может использоваться для взлома или деаномизации. Не используйте данную функцию если Вы контактируете с ненадежными контактами.",
|
||||||
|
@ -42,19 +64,13 @@
|
||||||
"importProfileTooltip": "Используйте зашифрованную резервную копию Cwtch, чтобы перенести профиль, созданный на другом устройстве где установлен Cwtch..",
|
"importProfileTooltip": "Используйте зашифрованную резервную копию Cwtch, чтобы перенести профиль, созданный на другом устройстве где установлен Cwtch..",
|
||||||
"importProfile": "Загрузить профиль",
|
"importProfile": "Загрузить профиль",
|
||||||
"exportProfileTooltip": "Сделать зашифрованную резервную копию в файл. Его потом потом можно импортировать на других устройствах где установлен Cwtch.",
|
"exportProfileTooltip": "Сделать зашифрованную резервную копию в файл. Его потом потом можно импортировать на других устройствах где установлен Cwtch.",
|
||||||
"exportProfile": "Сохранить профиль",
|
|
||||||
"notificationContentContactInfo": "Информация о разговоре",
|
|
||||||
"notificationContentSimpleEvent": "Обычное событие",
|
|
||||||
"conversationNotificationPolicySettingDescription": "Настройка уведомлений",
|
"conversationNotificationPolicySettingDescription": "Настройка уведомлений",
|
||||||
"conversationNotificationPolicySettingLabel": "Настройка уведомлений",
|
"conversationNotificationPolicySettingLabel": "Настройка уведомлений",
|
||||||
"settingsGroupAppearance": "Настройки отображения",
|
|
||||||
"notificationContentSettingDescription": "Управление уведомлениями в данной теме",
|
|
||||||
"notificationPolicySettingDescription": "Настройка уведомлений по-умолчанию",
|
"notificationPolicySettingDescription": "Настройка уведомлений по-умолчанию",
|
||||||
"notificationContentSettingLabel": "Содержимое уведомления",
|
"notificationContentSettingLabel": "Содержимое уведомления",
|
||||||
"notificationPolicySettingLabel": "Уведомления",
|
"notificationPolicySettingLabel": "Уведомления",
|
||||||
"conversationNotificationPolicyOptIn": "Включить",
|
"conversationNotificationPolicyOptIn": "Включить",
|
||||||
"conversationNotificationPolicyDefault": "По-умолчанию",
|
"conversationNotificationPolicyDefault": "По-умолчанию",
|
||||||
"notificationPolicyDefaultAll": "Всё по-умолчанию",
|
|
||||||
"notificationPolicyOptIn": "Включить",
|
"notificationPolicyOptIn": "Включить",
|
||||||
"notificationPolicyMute": "Без звука",
|
"notificationPolicyMute": "Без звука",
|
||||||
"tooltipSelectACustomProfileImage": "Сменить изображение профиля",
|
"tooltipSelectACustomProfileImage": "Сменить изображение профиля",
|
||||||
|
@ -81,7 +97,6 @@
|
||||||
"unlockServerTip": "Создайте или импортируйте сервер, чтобы начать",
|
"unlockServerTip": "Создайте или импортируйте сервер, чтобы начать",
|
||||||
"saveServerButton": "Сохранить",
|
"saveServerButton": "Сохранить",
|
||||||
"serverEnabled": "Состояние сервера",
|
"serverEnabled": "Состояние сервера",
|
||||||
"descriptionFileSharing": "Данная функция позволяет обмениваться файлами напрямую с контактами и группами в Cwtch. Отправляемый файл будет напрямую скачиваться с вашего устройства через Cwtch, поверх скрытой сети Tor",
|
|
||||||
"settingUIColumnOptionSame": "Как в портретном режиме",
|
"settingUIColumnOptionSame": "Как в портретном режиме",
|
||||||
"settingUIColumnSingle": "Один столбец",
|
"settingUIColumnSingle": "Один столбец",
|
||||||
"createProfileToBegin": "Пожалуйста, создайте или импортируйте профиль, чтобы начать",
|
"createProfileToBegin": "Пожалуйста, создайте или импортируйте профиль, чтобы начать",
|
||||||
|
@ -90,15 +105,11 @@
|
||||||
"enableGroups": "Групповые чаты",
|
"enableGroups": "Групповые чаты",
|
||||||
"settingTheme": "Ночной режим",
|
"settingTheme": "Ночной режим",
|
||||||
"addNewProfileBtn": "Создать новый профиль",
|
"addNewProfileBtn": "Создать новый профиль",
|
||||||
"profileOnionLabel": "Send this address to contacts you want to connect with",
|
|
||||||
"savePeerHistoryDescription": "Определяет политику хранения или удаления сообщений с данным контактом",
|
"savePeerHistoryDescription": "Определяет политику хранения или удаления сообщений с данным контактом",
|
||||||
"savePeerHistory": "Настройка истории",
|
"savePeerHistory": "Настройка истории",
|
||||||
"deleteConfirmLabel": "Введите УДАЛИТЬ, чтобы продолжить",
|
"deleteConfirmLabel": "Введите УДАЛИТЬ, чтобы продолжить",
|
||||||
"deleteConfirmText": "УДАЛИТЬ",
|
"deleteConfirmText": "УДАЛИТЬ",
|
||||||
"settingGroupBehaviour": "Поведение",
|
|
||||||
"settingsGroupExperiments": "Эксперименты",
|
|
||||||
"labelTorNetwork": "Сеть Tor",
|
"labelTorNetwork": "Сеть Tor",
|
||||||
"conversationNotificationPolicyNever": "Никогда",
|
|
||||||
"newMessageNotificationConversationInfo": "Новое сообщение от %1",
|
"newMessageNotificationConversationInfo": "Новое сообщение от %1",
|
||||||
"newMessageNotificationSimple": "Новое сообщение",
|
"newMessageNotificationSimple": "Новое сообщение",
|
||||||
"msgAddToAccept": "Добавьте учетную запись в контакты, чтобы принять этот файл.",
|
"msgAddToAccept": "Добавьте учетную запись в контакты, чтобы принять этот файл.",
|
||||||
|
@ -117,7 +128,6 @@
|
||||||
"resetTor": "Сброс",
|
"resetTor": "Сброс",
|
||||||
"descriptionBlockUnknownConnections": "Если включить этот параметр, все соединения от людей, не состоящих в ваших контактах будут отклонены.",
|
"descriptionBlockUnknownConnections": "Если включить этот параметр, все соединения от людей, не состоящих в ваших контактах будут отклонены.",
|
||||||
"descriptionExperimentsGroups": "Данная экспериментальная функция позволяет создавать группы в Cwtch, чтобы облегчить Вам общение с более чем одним контактом. Для создания групп необходимо включить функцию создания сервера и создать сервер в главном меню программы.",
|
"descriptionExperimentsGroups": "Данная экспериментальная функция позволяет создавать группы в Cwtch, чтобы облегчить Вам общение с более чем одним контактом. Для создания групп необходимо включить функцию создания сервера и создать сервер в главном меню программы.",
|
||||||
"descriptionExperiments": "Экспериментальные функции Cwtch это необязательные дополнительные функции, которые добавляют некоторые возможности, но не имеют такой же устойчивости к метаданным как если бы вы общались через традиционный чат 1 на 1..",
|
|
||||||
"settingLanguage": "Выбрать язык",
|
"settingLanguage": "Выбрать язык",
|
||||||
"profileName": "Введите имя...",
|
"profileName": "Введите имя...",
|
||||||
"themeNameNeon2": "Неон2",
|
"themeNameNeon2": "Неон2",
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
|
import 'package:cwtch/main.dart';
|
||||||
|
import 'package:cwtch/models/profile.dart';
|
||||||
import 'package:cwtch/widgets/messagerow.dart';
|
import 'package:cwtch/widgets/messagerow.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:flutter_gen/gen_l10n/app_localizations.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 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||||
|
|
||||||
import 'message.dart';
|
import 'message.dart';
|
||||||
|
@ -51,6 +54,7 @@ class ContactInfoState extends ChangeNotifier {
|
||||||
late bool _isGroup;
|
late bool _isGroup;
|
||||||
String? _server;
|
String? _server;
|
||||||
late bool _archived;
|
late bool _archived;
|
||||||
|
late bool _pinned;
|
||||||
|
|
||||||
String? _acnCircuit;
|
String? _acnCircuit;
|
||||||
|
|
||||||
|
@ -68,7 +72,8 @@ class ContactInfoState extends ChangeNotifier {
|
||||||
lastMessageTime,
|
lastMessageTime,
|
||||||
server,
|
server,
|
||||||
archived = false,
|
archived = false,
|
||||||
notificationPolicy = "ConversationNotificationPolicy.Default"}) {
|
notificationPolicy = "ConversationNotificationPolicy.Default",
|
||||||
|
pinned = false}) {
|
||||||
this._nickname = nickname;
|
this._nickname = nickname;
|
||||||
this._isGroup = isGroup;
|
this._isGroup = isGroup;
|
||||||
this._accepted = accepted;
|
this._accepted = accepted;
|
||||||
|
@ -84,6 +89,7 @@ class ContactInfoState extends ChangeNotifier {
|
||||||
this._archived = archived;
|
this._archived = archived;
|
||||||
this._notificationPolicy = notificationPolicyFromString(notificationPolicy);
|
this._notificationPolicy = notificationPolicyFromString(notificationPolicy);
|
||||||
this.messageCache = new MessageCache(_totalMessages);
|
this.messageCache = new MessageCache(_totalMessages);
|
||||||
|
this._pinned = pinned;
|
||||||
keys = Map<String, GlobalKey<MessageRowState>>();
|
keys = Map<String, GlobalKey<MessageRowState>>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -285,4 +291,27 @@ class ContactInfoState extends ChangeNotifier {
|
||||||
}
|
}
|
||||||
return ConversationNotificationPolicy.Never;
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,6 +51,9 @@ class ContactListState extends ChangeNotifier {
|
||||||
// return -1 = a first in list
|
// return -1 = a first in list
|
||||||
// return 1 = b 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
|
// blocked contacts last
|
||||||
if (a.isBlocked == true && b.isBlocked != true) return 1;
|
if (a.isBlocked == true && b.isBlocked != true) return 1;
|
||||||
if (a.isBlocked != true && b.isBlocked == true) return -1;
|
if (a.isBlocked != true && b.isBlocked == true) return -1;
|
||||||
|
|
|
@ -7,8 +7,10 @@ class FileDownloadProgress {
|
||||||
String? downloadedTo;
|
String? downloadedTo;
|
||||||
DateTime? timeStart;
|
DateTime? timeStart;
|
||||||
DateTime? timeEnd;
|
DateTime? timeEnd;
|
||||||
|
DateTime? requested;
|
||||||
|
|
||||||
FileDownloadProgress(this.chunksTotal, this.timeStart);
|
FileDownloadProgress(this.chunksTotal, this.timeStart);
|
||||||
|
|
||||||
double progress() {
|
double progress() {
|
||||||
return 1.0 * chunksDownloaded / chunksTotal;
|
return 1.0 * chunksDownloaded / chunksTotal;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
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 malformedMetadata = MessageMetadata(profileOnion, conversationIdentifier, 0, DateTime.now(), "", "", "", <String, String>{}, false, true, false, "");
|
||||||
var cwtch = Provider.of<FlwtchState>(context, listen: false).cwtch;
|
var cwtch = Provider.of<FlwtchState>(context, listen: false).cwtch;
|
||||||
|
|
|
@ -36,7 +36,6 @@ class QuotedMessage extends Message {
|
||||||
var content = message["body"];
|
var content = message["body"];
|
||||||
return Text(
|
return Text(
|
||||||
content,
|
content,
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return MalformedBubble();
|
return MalformedBubble();
|
||||||
|
|
|
@ -67,6 +67,7 @@ class ProfileInfoState extends ChangeNotifier {
|
||||||
server: contact["groupServer"],
|
server: contact["groupServer"],
|
||||||
archived: contact["isArchived"] == true,
|
archived: contact["isArchived"] == true,
|
||||||
lastMessageTime: DateTime.fromMillisecondsSinceEpoch(1000 * int.parse(contact["lastMsgTime"])),
|
lastMessageTime: DateTime.fromMillisecondsSinceEpoch(1000 * int.parse(contact["lastMsgTime"])),
|
||||||
|
pinned: contact["attributes"]?["local.profile.pinned"] == "true",
|
||||||
notificationPolicy: contact["notificationPolicy"] ?? "ConversationNotificationPolicy.Default");
|
notificationPolicy: contact["notificationPolicy"] ?? "ConversationNotificationPolicy.Default");
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
@ -291,12 +292,27 @@ class ProfileInfoState extends ChangeNotifier {
|
||||||
}
|
}
|
||||||
|
|
||||||
bool downloadInterrupted(String fileKey) {
|
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) {
|
void downloadMarkResumed(String fileKey) {
|
||||||
if (this._downloads.containsKey(fileKey)) {
|
if (this._downloads.containsKey(fileKey)) {
|
||||||
this._downloads[fileKey]!.interrupted = false;
|
this._downloads[fileKey]!.interrupted = false;
|
||||||
|
this._downloads[fileKey]!.requested = DateTime.now();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import 'package:cwtch/config.dart';
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
|
|
||||||
import 'contact.dart';
|
import 'contact.dart';
|
||||||
|
@ -13,7 +14,7 @@ class RemoteServerInfoState extends ChangeNotifier {
|
||||||
DateTime lastPreSyncMessagTime = new DateTime(2020);
|
DateTime lastPreSyncMessagTime = new DateTime(2020);
|
||||||
|
|
||||||
RemoteServerInfoState(this.onion, this.identifier, this.description, this._status, {lastPreSyncMessageTime, mostRecentMessageTime}) {
|
RemoteServerInfoState(this.onion, this.identifier, this.description, this._status, {lastPreSyncMessageTime, mostRecentMessageTime}) {
|
||||||
if (_status == "Authenticated") {
|
if (_status == "Authenticated" || _status == "Synced") {
|
||||||
this.lastPreSyncMessagTime = lastPreSyncMessageTime;
|
this.lastPreSyncMessagTime = lastPreSyncMessageTime;
|
||||||
updateSyncProgressFor(mostRecentMessageTime);
|
updateSyncProgressFor(mostRecentMessageTime);
|
||||||
}
|
}
|
||||||
|
|
|
@ -279,8 +279,8 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: _createPressed,
|
onPressed: _createPressed,
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
minimumSize: Size(400, 50),
|
minimumSize: Size(400, 75),
|
||||||
maximumSize: Size(800, 50),
|
maximumSize: Size(800, 75),
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.horizontal(left: Radius.circular(180), right: Radius.circular(180))),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.horizontal(left: Radius.circular(180), right: Radius.circular(180))),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
|
@ -297,8 +297,8 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
|
||||||
message: AppLocalizations.of(context)!.exportProfileTooltip,
|
message: AppLocalizations.of(context)!.exportProfileTooltip,
|
||||||
child: ElevatedButton.icon(
|
child: ElevatedButton.icon(
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
minimumSize: Size(400, 50),
|
minimumSize: Size(400, 75),
|
||||||
maximumSize: Size(800, 50),
|
maximumSize: Size(800, 75),
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.horizontal(left: Radius.circular(180), right: Radius.circular(180))),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.horizontal(left: Radius.circular(180), right: Radius.circular(180))),
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
|
@ -328,8 +328,8 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
|
||||||
message: AppLocalizations.of(context)!.enterCurrentPasswordForDelete,
|
message: AppLocalizations.of(context)!.enterCurrentPasswordForDelete,
|
||||||
child: ElevatedButton.icon(
|
child: ElevatedButton.icon(
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
minimumSize: Size(400, 50),
|
minimumSize: Size(400, 75),
|
||||||
maximumSize: Size(800, 50),
|
maximumSize: Size(800, 75),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
side: BorderSide(color: Provider.of<Settings>(context).theme.defaultButtonActiveColor, width: 2.0),
|
side: BorderSide(color: Provider.of<Settings>(context).theme.defaultButtonActiveColor, width: 2.0),
|
||||||
borderRadius: BorderRadius.horizontal(left: Radius.circular(180), right: Radius.circular(180))),
|
borderRadius: BorderRadius.horizontal(left: Radius.circular(180), right: Radius.circular(180))),
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:cwtch/cwtch/cwtch.dart';
|
import 'package:cwtch/cwtch/cwtch.dart';
|
||||||
import 'package:cwtch/cwtch_icons_icons.dart';
|
import 'package:cwtch/cwtch_icons_icons.dart';
|
||||||
import 'package:cwtch/models/appstate.dart';
|
import 'package:cwtch/models/appstate.dart';
|
||||||
|
@ -289,7 +291,7 @@ class _ContactsViewState extends State<ContactsView> {
|
||||||
padding: MediaQuery.of(context).viewInsets,
|
padding: MediaQuery.of(context).viewInsets,
|
||||||
child: RepaintBoundary(
|
child: RepaintBoundary(
|
||||||
child: Container(
|
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: Center(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.all(2.0),
|
padding: EdgeInsets.all(2.0),
|
||||||
|
@ -306,7 +308,7 @@ class _ContactsViewState extends State<ContactsView> {
|
||||||
message: AppLocalizations.of(context)!.tooltipAddContact,
|
message: AppLocalizations.of(context)!.tooltipAddContact,
|
||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
minimumSize: Size.fromWidth(double.infinity),
|
minimumSize: Size.fromWidth(399),
|
||||||
maximumSize: Size.fromWidth(400),
|
maximumSize: Size.fromWidth(400),
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.horizontal(left: Radius.circular(180), right: Radius.circular(180))),
|
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,
|
message: groupsEnabled ? AppLocalizations.of(context)!.addServerTooltip : AppLocalizations.of(context)!.thisFeatureRequiresGroupExpermientsToBeEnabled,
|
||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
minimumSize: Size.fromWidth(double.infinity),
|
minimumSize: Size.fromWidth(399),
|
||||||
maximumSize: Size.fromWidth(400),
|
maximumSize: Size.fromWidth(400),
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.horizontal(left: Radius.circular(180), right: Radius.circular(180))),
|
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,
|
message: groupsEnabled ? AppLocalizations.of(context)!.createGroupTitle : AppLocalizations.of(context)!.thisFeatureRequiresGroupExpermientsToBeEnabled,
|
||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
minimumSize: Size.fromWidth(double.infinity),
|
minimumSize: Size.fromWidth(399),
|
||||||
maximumSize: Size.fromWidth(400),
|
maximumSize: Size.fromWidth(400),
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.horizontal(left: Radius.circular(180), right: Radius.circular(180))),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.horizontal(left: Radius.circular(180), right: Radius.circular(180))),
|
||||||
),
|
),
|
||||||
|
|
|
@ -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();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -32,6 +32,7 @@ import '../constants.dart';
|
||||||
import '../main.dart';
|
import '../main.dart';
|
||||||
import '../settings.dart';
|
import '../settings.dart';
|
||||||
import '../widgets/messagelist.dart';
|
import '../widgets/messagelist.dart';
|
||||||
|
import 'filesharingview.dart';
|
||||||
import 'groupsettingsview.dart';
|
import 'groupsettingsview.dart';
|
||||||
|
|
||||||
class MessageView extends StatefulWidget {
|
class MessageView extends StatefulWidget {
|
||||||
|
@ -97,6 +98,12 @@ class _MessageViewState extends State<MessageView> {
|
||||||
var showMessageFormattingPreview = Provider.of<Settings>(context).isExperimentEnabled(FormattingExperiment);
|
var showMessageFormattingPreview = Provider.of<Settings>(context).isExperimentEnabled(FormattingExperiment);
|
||||||
var showFileSharing = Provider.of<Settings>(context).isExperimentEnabled(FileSharingExperiment);
|
var showFileSharing = Provider.of<Settings>(context).isExperimentEnabled(FileSharingExperiment);
|
||||||
var appBarButtons = <Widget>[];
|
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 (Provider.of<ContactInfoState>(context).isOnline()) {
|
||||||
if (showFileSharing) {
|
if (showFileSharing) {
|
||||||
appBarButtons.add(IconButton(
|
appBarButtons.add(IconButton(
|
||||||
|
@ -119,6 +126,7 @@ class _MessageViewState extends State<MessageView> {
|
||||||
},
|
},
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
appBarButtons.add(IconButton(
|
appBarButtons.add(IconButton(
|
||||||
splashRadius: Material.defaultSplashRadius / 2,
|
splashRadius: Material.defaultSplashRadius / 2,
|
||||||
icon: Icon(CwtchIcons.send_invite, size: 24),
|
icon: Icon(CwtchIcons.send_invite, size: 24),
|
||||||
|
@ -200,6 +208,23 @@ class _MessageViewState extends State<MessageView> {
|
||||||
return true;
|
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() {
|
void _pushContactSettings() {
|
||||||
var profileInfoState = Provider.of<ProfileInfoState>(context, listen: false);
|
var profileInfoState = Provider.of<ProfileInfoState>(context, listen: false);
|
||||||
var contactInfoState = Provider.of<ContactInfoState>(context, listen: false);
|
var contactInfoState = Provider.of<ContactInfoState>(context, listen: false);
|
||||||
|
@ -603,6 +628,10 @@ class _MessageViewState extends State<MessageView> {
|
||||||
var data = event.data;
|
var data = event.data;
|
||||||
if (event is RawKeyUpEvent) {
|
if (event is RawKeyUpEvent) {
|
||||||
if ((data.logicalKey == LogicalKeyboardKey.enter && !event.isShiftPressed) || data.logicalKey == LogicalKeyboardKey.numpadEnter && !event.isShiftPressed) {
|
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();
|
_sendMessage();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -625,6 +654,7 @@ class _MessageViewState extends State<MessageView> {
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Text(AppLocalizations.of(bcontext)!.invitationLabel),
|
Text(AppLocalizations.of(bcontext)!.invitationLabel),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
|
@ -633,10 +663,10 @@ class _MessageViewState extends State<MessageView> {
|
||||||
ChangeNotifierProvider.value(
|
ChangeNotifierProvider.value(
|
||||||
value: Provider.of<ProfileInfoState>(ctx, listen: false),
|
value: Provider.of<ProfileInfoState>(ctx, listen: false),
|
||||||
child: DropdownContacts(filter: (contact) {
|
child: DropdownContacts(filter: (contact) {
|
||||||
return contact.onion != Provider.of<ContactInfoState>(context).onion;
|
return contact.onion != Provider.of<ContactInfoState>(ctx).onion;
|
||||||
}, onChanged: (newVal) {
|
}, onChanged: (newVal) {
|
||||||
setState(() {
|
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(
|
SizedBox(
|
||||||
|
|
|
@ -206,7 +206,7 @@ class _ProfileMgrViewState extends State<ProfileMgrView> {
|
||||||
padding: MediaQuery.of(context).viewInsets,
|
padding: MediaQuery.of(context).viewInsets,
|
||||||
child: RepaintBoundary(
|
child: RepaintBoundary(
|
||||||
child: Container(
|
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: Center(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.all(10.0),
|
padding: EdgeInsets.all(10.0),
|
||||||
|
@ -221,7 +221,7 @@ class _ProfileMgrViewState extends State<ProfileMgrView> {
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
minimumSize: Size(double.infinity, 20),
|
minimumSize: Size(399, 20),
|
||||||
maximumSize: Size(400, 20),
|
maximumSize: Size(400, 20),
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.horizontal(left: Radius.circular(180), right: Radius.circular(180))),
|
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,
|
message: AppLocalizations.of(context)!.importProfileTooltip,
|
||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
minimumSize: Size(double.infinity, 20),
|
minimumSize: Size(399, 20),
|
||||||
maximumSize: Size(400, 20),
|
maximumSize: Size(400, 20),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
side: BorderSide(color: Provider.of<Settings>(context).theme.defaultButtonActiveColor, width: 2.0),
|
side: BorderSide(color: Provider.of<Settings>(context).theme.defaultButtonActiveColor, width: 2.0),
|
||||||
borderRadius: BorderRadius.horizontal(left: Radius.circular(180), right: Radius.circular(180))),
|
borderRadius: BorderRadius.horizontal(left: Radius.circular(180), right: Radius.circular(180))),
|
||||||
primary: Provider.of<Settings>(context).theme.backgroundMainColor,
|
primary: Provider.of<Settings>(context).theme.backgroundMainColor,
|
||||||
),
|
),
|
||||||
child:
|
child: Text(AppLocalizations.of(context)!.importProfile,
|
||||||
Text(AppLocalizations.of(context)!.importProfile, semanticsLabel: AppLocalizations.of(context)!.importProfile, style: TextStyle(fontWeight: FontWeight.bold)),
|
semanticsLabel: AppLocalizations.of(context)!.importProfile,
|
||||||
|
style: TextStyle(color: Provider.of<Settings>(context).theme.mainTextColor, fontWeight: FontWeight.bold)),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
// 10GB profiles should be enough for anyone?
|
// 10GB profiles should be enough for anyone?
|
||||||
showFilePicker(context, MaxGeneralFileSharingSize, (file) {
|
showFilePicker(context, MaxGeneralFileSharingSize, (file) {
|
||||||
|
@ -287,7 +288,7 @@ class _ProfileMgrViewState extends State<ProfileMgrView> {
|
||||||
padding: MediaQuery.of(context).viewInsets,
|
padding: MediaQuery.of(context).viewInsets,
|
||||||
child: RepaintBoundary(
|
child: RepaintBoundary(
|
||||||
child: Container(
|
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: Center(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.all(10.0),
|
padding: EdgeInsets.all(10.0),
|
||||||
|
|
|
@ -29,6 +29,7 @@ class _DropdownContactsState extends State<DropdownContacts> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return DropdownButton(
|
return DropdownButton(
|
||||||
|
isExpanded: true, // magic property
|
||||||
value: this.selected,
|
value: this.selected,
|
||||||
items: Provider.of<ProfileInfoState>(context, listen: false).contactList.contacts.where(widget.filter).map<DropdownMenuItem<String>>((ContactInfoState contact) {
|
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));
|
return DropdownMenuItem<String>(value: contact.onion, child: Text(contact.nickname));
|
||||||
|
|
|
@ -2,6 +2,7 @@ import 'dart:io';
|
||||||
|
|
||||||
import 'package:cwtch/models/appstate.dart';
|
import 'package:cwtch/models/appstate.dart';
|
||||||
import 'package:cwtch/models/contact.dart';
|
import 'package:cwtch/models/contact.dart';
|
||||||
|
import 'package:cwtch/models/contactlist.dart';
|
||||||
import 'package:cwtch/models/profile.dart';
|
import 'package:cwtch/models/profile.dart';
|
||||||
import 'package:cwtch/views/contactsview.dart';
|
import 'package:cwtch/views/contactsview.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
@ -18,6 +19,9 @@ class ContactRow extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ContactRowState extends State<ContactRow> {
|
class _ContactRowState extends State<ContactRow> {
|
||||||
|
|
||||||
|
bool isHover = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var contact = Provider.of<ContactInfoState>(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: () {
|
onTap: () {
|
||||||
selectConversation(context, contact.identifier);
|
selectConversation(context, contact.identifier);
|
||||||
},
|
},
|
||||||
|
onHover: (onHover) {
|
||||||
|
setState(() {
|
||||||
|
isHover = onHover;
|
||||||
|
});
|
||||||
|
},
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:cwtch/config.dart';
|
import 'package:cwtch/config.dart';
|
||||||
import 'package:cwtch/models/contact.dart';
|
import 'package:cwtch/models/contact.dart';
|
||||||
|
@ -53,7 +54,7 @@ class FileBubbleState extends State<FileBubble> {
|
||||||
filterQuality: FilterQuality.medium,
|
filterQuality: FilterQuality.medium,
|
||||||
fit: BoxFit.scaleDown,
|
fit: BoxFit.scaleDown,
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
height: MediaQuery.of(context).size.height * 0.30,
|
height: min(MediaQuery.of(context).size.height * 0.30, 150),
|
||||||
isAntiAlias: false,
|
isAntiAlias: false,
|
||||||
errorBuilder: (context, error, stackTrace) {
|
errorBuilder: (context, error, stackTrace) {
|
||||||
return MalformedBubble();
|
return MalformedBubble();
|
||||||
|
@ -153,6 +154,7 @@ class FileBubbleState extends State<FileBubble> {
|
||||||
if (Provider.of<Settings>(context).shouldPreview(path)) {
|
if (Provider.of<Settings>(context).shouldPreview(path)) {
|
||||||
isPreview = true;
|
isPreview = true;
|
||||||
wdgDecorations = Center(
|
wdgDecorations = Center(
|
||||||
|
widthFactor: 1.0,
|
||||||
child: MouseRegion(
|
child: MouseRegion(
|
||||||
cursor: SystemMouseCursors.click,
|
cursor: SystemMouseCursors.click,
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
|
@ -223,7 +225,18 @@ class FileBubbleState extends State<FileBubble> {
|
||||||
crossAxisAlignment: fromMe ? CrossAxisAlignment.end : CrossAxisAlignment.start,
|
crossAxisAlignment: fromMe ? CrossAxisAlignment.end : CrossAxisAlignment.start,
|
||||||
mainAxisAlignment: fromMe ? MainAxisAlignment.end : MainAxisAlignment.start,
|
mainAxisAlignment: fromMe ? MainAxisAlignment.end : MainAxisAlignment.start,
|
||||||
mainAxisSize: MainAxisSize.min,
|
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";
|
var manifestPath = file.path + ".manifest";
|
||||||
Provider.of<ProfileInfoState>(context, listen: false).downloadInit(widget.fileKey(), (widget.fileSize / 4096).ceil());
|
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");
|
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) {
|
if (contact != null) {
|
||||||
Provider.of<FlwtchState>(context, listen: false).cwtch.DownloadFile(profileOnion, contact.identifier, file.path, manifestPath, widget.fileKey());
|
Provider.of<FlwtchState>(context, listen: false).cwtch.DownloadFile(profileOnion, contact.identifier, file.path, manifestPath, widget.fileKey());
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import 'package:cwtch/models/contact.dart';
|
||||||
import 'package:cwtch/models/message.dart';
|
import 'package:cwtch/models/message.dart';
|
||||||
import 'package:cwtch/models/profile.dart';
|
import 'package:cwtch/models/profile.dart';
|
||||||
import 'package:cwtch/views/contactsview.dart';
|
import 'package:cwtch/views/contactsview.dart';
|
||||||
|
import 'package:cwtch/widgets/staticmessagebubble.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:cwtch/widgets/profileimage.dart';
|
import 'package:cwtch/widgets/profileimage.dart';
|
||||||
import 'package:flutter/physics.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 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||||
|
|
||||||
import '../main.dart';
|
import '../main.dart';
|
||||||
|
import '../models/messagecache.dart';
|
||||||
import '../settings.dart';
|
import '../settings.dart';
|
||||||
|
|
||||||
class MessageRow extends StatefulWidget {
|
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()
|
? SizedBox.shrink()
|
||||||
: Visibility(
|
: Visibility(
|
||||||
visible: EnvironmentConfig.TEST_MODE || Provider.of<AppState>(context).hoveredIndex == Provider.of<MessageMetadata>(context).messageID,
|
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)));
|
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));
|
Widget wdgSpacer = Flexible(flex: 1, child: SizedBox(width: Platform.isAndroid ? 20 : 60, height: 10));
|
||||||
var widgetRow = <Widget>[];
|
var widgetRow = <Widget>[];
|
||||||
|
|
||||||
if (fromMe) {
|
if (fromMe) {
|
||||||
widgetRow = <Widget>[
|
widgetRow = <Widget>[
|
||||||
wdgSpacer,
|
wdgSpacer,
|
||||||
wdgIcons,
|
wdgSeeReplies,
|
||||||
|
wdgReply,
|
||||||
actualMessage,
|
actualMessage,
|
||||||
];
|
];
|
||||||
} else if (isBlocked && !showBlockedMessage) {
|
} else if (isBlocked && !showBlockedMessage) {
|
||||||
|
@ -143,7 +169,8 @@ class MessageRowState extends State<MessageRow> with SingleTickerProviderStateMi
|
||||||
});
|
});
|
||||||
})),
|
})),
|
||||||
]))),
|
]))),
|
||||||
wdgIcons,
|
wdgReply,
|
||||||
|
wdgSeeReplies,
|
||||||
wdgSpacer,
|
wdgSpacer,
|
||||||
];
|
];
|
||||||
} else {
|
} else {
|
||||||
|
@ -179,7 +206,8 @@ class MessageRowState extends State<MessageRow> with SingleTickerProviderStateMi
|
||||||
widgetRow = <Widget>[
|
widgetRow = <Widget>[
|
||||||
wdgPortrait,
|
wdgPortrait,
|
||||||
actualMessage,
|
actualMessage,
|
||||||
wdgIcons,
|
wdgReply,
|
||||||
|
wdgSeeReplies,
|
||||||
wdgSpacer,
|
wdgSpacer,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -212,6 +240,9 @@ class MessageRowState extends State<MessageRow> with SingleTickerProviderStateMi
|
||||||
_runAnimation(details.velocity.pixelsPerSecond, size);
|
_runAnimation(details.velocity.pixelsPerSecond, size);
|
||||||
Provider.of<AppState>(context, listen: false).selectedIndex = Provider.of<MessageMetadata>(context, listen: false).messageID;
|
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(
|
child: Padding(
|
||||||
padding: EdgeInsets.all(2),
|
padding: EdgeInsets.all(2),
|
||||||
child: Align(
|
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,
|
||||||
|
)))));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -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])))));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -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.
|
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
|
||||||
# Read more about iOS versioning at
|
# Read more about iOS versioning at
|
||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
version: 1.7.1+29
|
version: 1.8.0+34
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=2.15.0 <3.0.0"
|
sdk: ">=2.15.0 <3.0.0"
|
||||||
|
|
Loading…
Reference in New Issue