Merge branch 'trunk' into russian

This commit is contained in:
Sarah Jamie Lewis 2021-12-10 21:02:46 +00:00
commit 405c4aeb4c
46 changed files with 1086 additions and 919 deletions

View File

@ -1 +1 @@
2021-11-09-18-25-v1.4.2 2021-12-08-00-32-v1.5.0-7-g28a13aa

View File

@ -1 +1 @@
2021-11-09-23-25-v1.4.2 2021-12-08-05-32-v1.5.0-7-g28a13aa

View File

@ -28,7 +28,7 @@ Cwtch processes the following environment variables:
First you will need a valid [flutter sdk installation](https://flutter.dev/docs/get-started/install). First you will need a valid [flutter sdk installation](https://flutter.dev/docs/get-started/install).
You will probably want to disable Analytics on the Flutter Tool: `flutter config --no-analytics` You will probably want to disable Analytics on the Flutter Tool: `flutter config --no-analytics`
This project uses the flutter `dev` channel, which you will need to switch to: `flutter channel dev; flutter upgrade`. This project uses the flutter `stable` channel
Once flutter is set up, run `flutter pub get` from this project folder to fetch dependencies. Once flutter is set up, run `flutter pub get` from this project folder to fetch dependencies.
@ -42,17 +42,20 @@ To build a release version and load normal profiles, use `build-release.sh X` in
- set `LD_LIBRARY_PATH="$PWD/linux"` - set `LD_LIBRARY_PATH="$PWD/linux"`
- copy a `tor` binary to `linux/` or run `fetch-tor.sh` to download one - copy a `tor` binary to `linux/` or run `fetch-tor.sh` to download one
- run `flutter config --enable-linux-desktop` if you've never done so before - run `flutter config --enable-linux-desktop` if you've never done so before
- optional: launch cwtch-ui directly by running `flutter run -d linux` - optional: launch cwtch-ui debug build by running `flutter run -d linux`
- to build cwtch-ui, run `flutter build linux` - to build cwtch-ui, run `flutter build linux`
- optional: launch cwtch-ui build with `env LD_LIBRARY_PATH=linux ./build/linux/x64/release/bundle/cwtch` - optional: launch cwtch-ui release build with `env LD_LIBRARY_PATH=linux ./build/linux/x64/release/bundle/cwtch`
- to package the build, run `linux/package-release.sh` - to package the build, run `linux/package-release.sh`
### Building on Windows (for Windows) ### Building on Windows (for Windows)
- copy `libCwtch.dll` to `windows/`, or run `fetch-libcwtch-go.ps1` to download it - copy `libCwtch.dll` to `windows/`, or run `fetch-libcwtch-go.ps1` to download it
- run `fetch-tor-win.ps1` to fetch Tor for windows - run `fetch-tor-win.ps1` to fetch Tor for windows
- optional: launch cwtch-ui directly by running `flutter run -d windows` - optional: launch cwtch-ui debug build by running `flutter run -d windows`
- to build cwtch-ui, run `flutter build windows` - to build cwtch-ui, run `flutter build windows`
- optional: to run the release build:
- `cp windows/libCwtch.dll .`
- `./build/windows/runner/Release/cwtch.exe`
### Building on Linux/Windows (for Android) ### Building on Linux/Windows (for Android)
@ -65,10 +68,8 @@ To build a release version and load normal profiles, use `build-release.sh X` in
- copy `libCwtch.dylib` into the root folder, or run `fetch-libcwtch-go-macos.sh` to download it - copy `libCwtch.dylib` into the root folder, or run `fetch-libcwtch-go-macos.sh` to download it
- run `fetch-tor-macos.sh` to fetch Tor or Download and install Tor Browser and `cp -r /Applications/Tor\ Browser.app/Contents/MacOS/Tor ./macos/` - run `fetch-tor-macos.sh` to fetch Tor or Download and install Tor Browser and `cp -r /Applications/Tor\ Browser.app/Contents/MacOS/Tor ./macos/`
- `flutter build macos` - `flutter build macos`
- optional: launch cwtch-ui build with `./build/linux/x64/release/bundle/cwtch` - optional: launch cwtch-ui release build with `./build/macos/Build/Products/Release/Cwtch.app/Contents/MacOS/Cwtch`
- `./macos/package-release.sh` - To package the UI: `./macos/package-release.sh`, which results in a Cwtch.dmg that has libCwtch.dylib and tor in it as well and can be installed into Applications
results in a Cwtch.dmg that has libCwtch.dylib and tor in it as well and can be installed into Applications
### Known Platform Issues ### Known Platform Issues

View File

@ -48,4 +48,11 @@
<!--Meeded to check if activity is foregrounded or if messages from the service should be queued--> <!--Meeded to check if activity is foregrounded or if messages from the service should be queued-->
<uses-permission android:name="android.permission.GET_TASKS" /> <uses-permission android:name="android.permission.GET_TASKS" />
<queries>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="https" />
</intent>
</queries>
</manifest> </manifest>

View File

@ -133,10 +133,10 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) :
notificationManager.notify(dlID, newNotification); notificationManager.notify(dlID, newNotification);
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.i("FlwtchWorker->FileDownloadProgressUpdate", e.toString() + " :: " + e.getStackTrace()); Log.d("FlwtchWorker->FileDownloadProgressUpdate", e.toString() + " :: " + e.getStackTrace());
} }
} else if (evt.EventType == "FileDownloaded") { } else if (evt.EventType == "FileDownloaded") {
Log.i("FlwtchWorker", "file downloaded!"); Log.d("FlwtchWorker", "file downloaded!");
val data = JSONObject(evt.Data); val data = JSONObject(evt.Data);
val tempFile = data.getString("TempFile"); val tempFile = data.getString("TempFile");
val fileKey = data.getString("FileKey"); val fileKey = data.getString("FileKey");
@ -147,7 +147,7 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) :
val targetUri = Uri.parse(filePath); val targetUri = Uri.parse(filePath);
val os = this.applicationContext.getContentResolver().openOutputStream(targetUri); val os = this.applicationContext.getContentResolver().openOutputStream(targetUri);
val bytesWritten = Files.copy(sourcePath, os); val bytesWritten = Files.copy(sourcePath, os);
Log.i("FlwtchWorker", "copied " + bytesWritten.toString() + " bytes"); Log.d("FlwtchWorker", "copied " + bytesWritten.toString() + " bytes");
if (bytesWritten != 0L) { if (bytesWritten != 0L) {
os?.flush(); os?.flush();
os?.close(); os?.close();
@ -181,61 +181,70 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) :
Cwtch.loadProfiles(pass) Cwtch.loadProfiles(pass)
} }
"GetMessage" -> { "GetMessage" -> {
val profile = (a.get("profile") as? String) ?: "" val profile = (a.get("ProfileOnion") as? String) ?: ""
val handle = (a.get("contact") as? String) ?: "" val conversation = a.getInt("conversation").toLong()
val indexI = a.getInt("index") val indexI = a.getInt("index").toLong()
return Result.success(Data.Builder().putString("result", Cwtch.getMessage(profile, handle, indexI.toLong())).build()) Log.d("FlwtchWorker", "Cwtch GetMessage " + profile + " " + conversation.toString() + " " + indexI.toString())
return Result.success(Data.Builder().putString("result", Cwtch.getMessage(profile, conversation, indexI)).build())
}
"GetMessageByID" -> {
val profile = (a.get("ProfileOnion") as? String) ?: ""
val conversation = a.getInt("conversation").toLong()
val id = a.getInt("id").toLong()
return Result.success(Data.Builder().putString("result", Cwtch.getMessageByID(profile, conversation, id)).build())
} }
"GetMessageByContentHash" -> { "GetMessageByContentHash" -> {
val profile = (a.get("profile") as? String) ?: ""
val handle = (a.get("contact") as? String) ?: ""
val contentHash = (a.get("contentHash") as? String) ?: ""
return Result.success(Data.Builder().putString("result", Cwtch.getMessagesByContentHash(profile, handle, contentHash)).build())
}
"UpdateMessageFlags" -> {
val profile = (a.get("profile") as? String) ?: ""
val handle = (a.get("contact") as? String) ?: ""
val midx = (a.get("midx") as? Long) ?: 0
val flags = (a.get("flags") as? Long) ?: 0
Cwtch.updateMessageFlags(profile, handle, midx, flags)
}
"AcceptContact" -> {
val profile = (a.get("ProfileOnion") as? String) ?: "" val profile = (a.get("ProfileOnion") as? String) ?: ""
val handle = (a.get("handle") as? String) ?: "" val conversation = a.getInt("conversation").toLong()
Cwtch.acceptContact(profile, handle) val contentHash = (a.get("contentHash") as? String) ?: ""
return Result.success(Data.Builder().putString("result", Cwtch.getMessagesByContentHash(profile, conversation, contentHash)).build())
}
"UpdateMessageAttribute" -> {
val profile = (a.get("ProfileOnion") as? String) ?: ""
val conversation = a.getInt("conversation").toLong()
val channel = a.getInt("chanenl").toLong()
val midx = a.getInt("midx").toLong()
val key = (a.get("key") as? String) ?: ""
val value = (a.get("value") as? String) ?: ""
Cwtch.setMessageAttribute(profile, conversation, channel, midx, key, value)
}
"AcceptConversation" -> {
val profile = (a.get("ProfileOnion") as? String) ?: ""
val conversation = a.getInt("conversation").toLong()
Cwtch.acceptConversation(profile, conversation)
} }
"BlockContact" -> { "BlockContact" -> {
val profile = (a.get("ProfileOnion") as? String) ?: "" val profile = (a.get("ProfileOnion") as? String) ?: ""
val handle = (a.get("handle") as? String) ?: "" val conversation = a.getInt("conversation").toLong()
Cwtch.blockContact(profile, handle) Cwtch.blockContact(profile, conversation)
} }
"SendMessage" -> { "SendMessage" -> {
val profile = (a.get("ProfileOnion") as? String) ?: "" val profile = (a.get("ProfileOnion") as? String) ?: ""
val handle = (a.get("handle") as? String) ?: "" val conversation = a.getInt("conversation").toLong()
val message = (a.get("message") as? String) ?: "" val message = (a.get("message") as? String) ?: ""
Cwtch.sendMessage(profile, handle, message) Cwtch.sendMessage(profile, conversation, message)
} }
"SendInvitation" -> { "SendInvitation" -> {
val profile = (a.get("ProfileOnion") as? String) ?: "" val profile = (a.get("ProfileOnion") as? String) ?: ""
val handle = (a.get("handle") as? String) ?: "" val conversation = a.getInt("conversation").toLong()
val target = (a.get("target") as? String) ?: "" val target = (a.get("target") as? Long) ?: -1
Cwtch.sendInvitation(profile, handle, target) Cwtch.sendInvitation(profile, conversation, target)
} }
"ShareFile" -> { "ShareFile" -> {
val profile = (a.get("ProfileOnion") as? String) ?: "" val profile = (a.get("ProfileOnion") as? String) ?: ""
val handle = (a.get("handle") as? String) ?: "" val conversation = a.getInt("conversation").toLong()
val filepath = (a.get("filepath") as? String) ?: "" val filepath = (a.get("filepath") as? String) ?: ""
Cwtch.shareFile(profile, handle, filepath) Cwtch.shareFile(profile, conversation, filepath)
} }
"DownloadFile" -> { "DownloadFile" -> {
val profile = (a.get("ProfileOnion") as? String) ?: "" val profile = (a.get("ProfileOnion") as? String) ?: ""
val handle = (a.get("handle") as? String) ?: "" val conversation = a.getInt("conversation").toLong()
val filepath = (a.get("filepath") as? String) ?: "" val filepath = (a.get("filepath") as? String) ?: ""
val manifestpath = (a.get("manifestpath") as? String) ?: "" val manifestpath = (a.get("manifestpath") as? String) ?: ""
val filekey = (a.get("filekey") as? String) ?: "" val filekey = (a.get("filekey") as? String) ?: ""
// FIXME: Prevent spurious calls by Intent // FIXME: Prevent spurious calls by Intent
if (profile != "") { if (profile != "") {
Cwtch.downloadFile(profile, handle, filepath, manifestpath, filekey) Cwtch.downloadFile(profile, conversation, filepath, manifestpath, filekey)
} }
} }
"CheckDownloadStatus" -> { "CheckDownloadStatus" -> {
@ -245,9 +254,9 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) :
} }
"VerifyOrResumeDownload" -> { "VerifyOrResumeDownload" -> {
val profile = (a.get("ProfileOnion") as? String) ?: "" val profile = (a.get("ProfileOnion") as? String) ?: ""
val handle = (a.get("handle") as? String) ?: "" val conversation = a.getInt("conversation").toLong()
val fileKey = (a.get("fileKey") as? String) ?: "" val fileKey = (a.get("fileKey") as? String) ?: ""
Cwtch.verifyOrResumeDownload(profile, handle, fileKey) Cwtch.verifyOrResumeDownload(profile, conversation, fileKey)
} }
"SendProfileEvent" -> { "SendProfileEvent" -> {
val onion = (a.get("onion") as? String) ?: "" val onion = (a.get("onion") as? String) ?: ""
@ -266,13 +275,6 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) :
val bundle = (a.get("bundle") as? String) ?: "" val bundle = (a.get("bundle") as? String) ?: ""
Cwtch.importBundle(profile, bundle) Cwtch.importBundle(profile, bundle)
} }
"SetGroupAttribute" -> {
val profile = (a.get("ProfileOnion") as? String) ?: ""
val groupHandle = (a.get("groupHandle") as? String) ?: ""
val key = (a.get("key") as? String) ?: ""
val value = (a.get("value") as? String) ?: ""
Cwtch.setGroupAttribute(profile, groupHandle, key, value)
}
"CreateGroup" -> { "CreateGroup" -> {
val profile = (a.get("ProfileOnion") as? String) ?: "" val profile = (a.get("ProfileOnion") as? String) ?: ""
val server = (a.get("server") as? String) ?: "" val server = (a.get("server") as? String) ?: ""
@ -286,18 +288,13 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) :
} }
"ArchiveConversation" -> { "ArchiveConversation" -> {
val profile = (a.get("ProfileOnion") as? String) ?: "" val profile = (a.get("ProfileOnion") as? String) ?: ""
val contactHandle = (a.get("handle") as? String) ?: "" val conversation = (a.get("conversation") as? Long) ?: -1
Cwtch.archiveConversation(profile, contactHandle) Cwtch.archiveConversation(profile, conversation)
} }
"DeleteContact" -> { "DeleteConversation" -> {
val profile = (a.get("ProfileOnion") as? String) ?: "" val profile = (a.get("ProfileOnion") as? String) ?: ""
val handle = (a.get("handle") as? String) ?: "" val conversation = (a.get("conversation") as? Long) ?: -1
Cwtch.deleteContact(profile, handle) Cwtch.deleteContact(profile, conversation)
}
"RejectInvite" -> {
val profile = (a.get("ProfileOnion") as? String) ?: ""
val groupHandle = (a.get("groupHandle") as? String) ?: ""
Cwtch.rejectInvite(profile, groupHandle)
} }
"SetProfileAttribute" -> { "SetProfileAttribute" -> {
val profile = (a.get("ProfileOnion") as? String) ?: "" val profile = (a.get("ProfileOnion") as? String) ?: ""
@ -305,12 +302,12 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) :
val v = (a.get("Val") as? String) ?: "" val v = (a.get("Val") as? String) ?: ""
Cwtch.setProfileAttribute(profile, key, v) Cwtch.setProfileAttribute(profile, key, v)
} }
"SetContactAttribute" -> { "SetConversationAttribute" -> {
val profile = (a.get("ProfileOnion") as? String) ?: "" val profile = (a.get("ProfileOnion") as? String) ?: ""
val contact = (a.get("Contact") as? String) ?: "" val conversation = (a.get("conversation") as? Long) ?: -1
val key = (a.get("Key") as? String) ?: "" val key = (a.get("Key") as? String) ?: ""
val v = (a.get("Val") as? String) ?: "" val v = (a.get("Val") as? String) ?: ""
Cwtch.setContactAttribute(profile, contact, key, v) Cwtch.setConversationAttribute(profile, conversation, key, v)
} }
"Shutdown" -> { "Shutdown" -> {
Cwtch.shutdownCwtch(); Cwtch.shutdownCwtch();
@ -354,7 +351,10 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) :
val v = (a.get("Val") as? String) ?: "" val v = (a.get("Val") as? String) ?: ""
Cwtch.setServerAttribute(serverOnion, key, v) Cwtch.setServerAttribute(serverOnion, key, v)
} }
else -> return Result.failure() else -> {
Log.i("FlwtchWorker", "unknown command: " + method);
return Result.failure()
}
} }
return Result.success() return Result.success()
} }

View File

@ -147,11 +147,7 @@ class MainActivity: FlutterActivity() {
// that we can divert this method call to ReconnectCwtchForeground instead if so. // that we can divert this method call to ReconnectCwtchForeground instead if so.
val works = WorkManager.getInstance(this).getWorkInfosByTag(WORKER_TAG).get() val works = WorkManager.getInstance(this).getWorkInfosByTag(WORKER_TAG).get()
for (workInfo in works) { for (workInfo in works) {
Log.i("handleCwtch:WorkManager", "$workInfo") WorkManager.getInstance(this).cancelWorkById(workInfo.id)
if (!workInfo.tags.contains(uniqueTag)) {
Log.i("handleCwtch:WorkManager", "canceling ${workInfo.id} bc tags don't include $uniqueTag")
WorkManager.getInstance(this).cancelWorkById(workInfo.id)
}
} }
WorkManager.getInstance(this).pruneWork() WorkManager.getInstance(this).pruneWork()

View File

@ -10,8 +10,6 @@ abstract class Cwtch {
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
Future<void> ReconnectCwtchForeground(); Future<void> ReconnectCwtchForeground();
// ignore: non_constant_identifier_names
void SelectProfile(String onion);
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void CreateProfile(String nick, String pass); void CreateProfile(String nick, String pass);
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
@ -29,36 +27,39 @@ abstract class Cwtch {
void SendAppEvent(String jsonEvent); void SendAppEvent(String jsonEvent);
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void AcceptContact(String profileOnion, String contactHandle); void AcceptContact(String profileOnion, int contactHandle);
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void BlockContact(String profileOnion, String contactHandle); void BlockContact(String profileOnion, int contactHandle);
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
Future<dynamic> GetMessage(String profile, String handle, int index); Future<dynamic> GetMessage(String profile, int handle, int index);
// ignore: non_constant_identifier_names
Future<dynamic> GetMessageByContentHash(String profile, String handle, String contentHash);
// ignore: non_constant_identifier_names
void UpdateMessageFlags(String profile, String handle, int index, int flags);
// ignore: non_constant_identifier_names
void SendMessage(String profile, String handle, String message);
// ignore: non_constant_identifier_names
void SendInvitation(String profile, String handle, String target);
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void ShareFile(String profile, String handle, String filepath); Future<dynamic> GetMessageByID(String profile, int handle, int index);
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void DownloadFile(String profile, String handle, String filepath, String manifestpath, String filekey); Future<dynamic> GetMessageByContentHash(String profile, int handle, String contentHash);
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void CreateDownloadableFile(String profile, String handle, String filenameSuggestion, String filekey); void SendMessage(String profile, int handle, String message);
// ignore: non_constant_identifier_names
void SendInvitation(String profile, int handle, int target);
// ignore: non_constant_identifier_names
void ShareFile(String profile, int handle, String filepath);
// ignore: non_constant_identifier_names
void DownloadFile(String profile, int handle, String filepath, String manifestpath, String filekey);
// ignore: non_constant_identifier_names
void CreateDownloadableFile(String profile, int handle, String filenameSuggestion, String filekey);
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void CheckDownloadStatus(String profile, String fileKey); void CheckDownloadStatus(String profile, String fileKey);
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void VerifyOrResumeDownload(String profile, String handle, String filekey); void VerifyOrResumeDownload(String profile, int handle, String filekey);
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void ArchiveConversation(String profile, String handle); void ArchiveConversation(String profile, int handle);
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void DeleteContact(String profile, String handle); void DeleteContact(String profile, int handle);
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void CreateGroup(String profile, String server, String groupName); void CreateGroup(String profile, String server, String groupName);
@ -66,14 +67,11 @@ abstract class Cwtch {
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void ImportBundle(String profile, String bundle); void ImportBundle(String profile, String bundle);
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void SetGroupAttribute(String profile, String groupHandle, String key, String value);
// ignore: non_constant_identifier_names
void RejectInvite(String profileOnion, String groupHandle);
// ignore: non_constant_identifier_names
void SetProfileAttribute(String profile, String key, String val); void SetProfileAttribute(String profile, String key, String val);
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void SetContactAttribute(String profile, String contact, String key, String val); void SetConversationAttribute(String profile, int conversation, String key, String val);
// ignore: non_constant_identifier_names
void SetMessageAttribute(String profile, int conversation, int channel, int message, String key, String val);
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void LoadServers(String password); void LoadServers(String password);
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names

View File

@ -1,4 +1,5 @@
import 'dart:convert'; import 'dart:convert';
import 'package:cwtch/main.dart';
import 'package:cwtch/models/message.dart'; import 'package:cwtch/models/message.dart';
import 'package:cwtch/models/profileservers.dart'; import 'package:cwtch/models/profileservers.dart';
import 'package:cwtch/models/servers.dart'; import 'package:cwtch/models/servers.dart';
@ -23,7 +24,8 @@ class CwtchNotifier {
late AppState appState; late AppState appState;
late ServerListState serverListState; late ServerListState serverListState;
CwtchNotifier(ProfileListState pcn, Settings settingsCN, ErrorHandler errorCN, TorStatus torStatusCN, NotificationsManager notificationManagerP, AppState appStateCN, ServerListState serverListStateCN) { CwtchNotifier(
ProfileListState pcn, Settings settingsCN, ErrorHandler errorCN, TorStatus torStatusCN, NotificationsManager notificationManagerP, AppState appStateCN, ServerListState serverListStateCN) {
profileCN = pcn; profileCN = pcn;
settings = settingsCN; settings = settingsCN;
error = errorCN; error = errorCN;
@ -34,6 +36,7 @@ class CwtchNotifier {
} }
void handleMessage(String type, dynamic data) { void handleMessage(String type, dynamic data) {
//EnvironmentConfig.debugLog("NewEvent $type $data");
switch (type) { switch (type) {
case "CwtchStarted": case "CwtchStarted":
appState.SetCwtchInit(); appState.SetCwtchInit();
@ -42,12 +45,15 @@ class CwtchNotifier {
appState.SetAppError(data["Error"]); appState.SetAppError(data["Error"]);
break; break;
case "NewPeer": case "NewPeer":
EnvironmentConfig.debugLog("NewPeer $data");
// if tag != v1-defaultPassword then it is either encrypted OR it is an unencrypted account created during pre-beta... // if tag != v1-defaultPassword then it is either encrypted OR it is an unencrypted account created during pre-beta...
profileCN.add(data["Identity"], data["name"], data["picture"], data["ContactsJson"], data["ServerList"], data["Online"] == "true", data["tag"] != "v1-defaultPassword"); profileCN.add(data["Identity"], data["name"], data["picture"], data["ContactsJson"], data["ServerList"], data["Online"] == "true", data["tag"] != "v1-defaultPassword");
break; break;
case "PeerCreated": case "ContactCreated":
EnvironmentConfig.debugLog("NewServer $data");
profileCN.getProfile(data["ProfileOnion"])?.contactList.add(ContactInfoState( profileCN.getProfile(data["ProfileOnion"])?.contactList.add(ContactInfoState(
data["ProfileOnion"], data["ProfileOnion"],
int.parse(data["ConversationID"]),
data["RemotePeer"], data["RemotePeer"],
nickname: data["nick"], nickname: data["nick"],
status: data["status"], status: data["status"],
@ -64,13 +70,7 @@ class CwtchNotifier {
break; break;
case "NewServer": case "NewServer":
EnvironmentConfig.debugLog("NewServer $data"); EnvironmentConfig.debugLog("NewServer $data");
serverListState.add( serverListState.add(data["Onion"], data["ServerBundle"], data["Running"] == "true", data["Description"], data["Autostart"] == "true", data["StorageType"] == "storage-password");
data["Onion"],
data["ServerBundle"],
data["Running"] == "true",
data["Description"],
data["Autostart"] == "true",
data["StorageType"] == "storage-password");
break; break;
case "ServerIntentUpdate": case "ServerIntentUpdate":
EnvironmentConfig.debugLog("ServerIntentUpdate $data"); EnvironmentConfig.debugLog("ServerIntentUpdate $data");
@ -80,7 +80,6 @@ class CwtchNotifier {
} }
break; break;
case "GroupCreated": case "GroupCreated":
// Retrieve Server Status from Cache... // Retrieve Server Status from Cache...
String status = ""; String status = "";
RemoteServerInfoState? serverInfoState = profileCN.getProfile(data["ProfileOnion"])?.serverList.getServer(data["GroupServer"]); RemoteServerInfoState? serverInfoState = profileCN.getProfile(data["ProfileOnion"])?.serverList.getServer(data["GroupServer"]);
@ -88,7 +87,7 @@ class CwtchNotifier {
status = serverInfoState.status; status = serverInfoState.status;
} }
if (profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["GroupID"]) == null) { if (profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["GroupID"]) == null) {
profileCN.getProfile(data["ProfileOnion"])?.contactList.add(ContactInfoState(data["ProfileOnion"], data["GroupID"], profileCN.getProfile(data["ProfileOnion"])?.contactList.add(ContactInfoState(data["ProfileOnion"], data["ConversationID"], data["GroupID"],
authorization: ContactAuthorization.approved, authorization: ContactAuthorization.approved,
imagePath: data["PicturePath"], imagePath: data["PicturePath"],
nickname: data["GroupName"], nickname: data["GroupName"],
@ -111,13 +110,13 @@ class CwtchNotifier {
} }
break; break;
case "DeleteContact": case "DeleteContact":
profileCN.getProfile(data["ProfileOnion"])?.contactList.removeContact(data["RemotePeer"]); profileCN.getProfile(data["ProfileOnion"])?.contactList.removeContact(data["ConversationID"]);
break; break;
case "DeleteGroup": case "DeleteGroup":
profileCN.getProfile(data["ProfileOnion"])?.contactList.removeContact(data["GroupID"]); profileCN.getProfile(data["ProfileOnion"])?.contactList.removeContact(data["ConversationID"]);
break; break;
case "PeerStateChange": case "PeerStateChange":
ContactInfoState? contact = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["RemotePeer"]); ContactInfoState? contact = profileCN.getProfile(data["ProfileOnion"])?.contactList.findContact(data["RemotePeer"]);
if (contact != null) { if (contact != null) {
if (data["ConnectionState"] != null) { if (data["ConnectionState"] != null) {
contact.status = data["ConnectionState"]; contact.status = data["ConnectionState"];
@ -131,19 +130,29 @@ class CwtchNotifier {
break; break;
case "NewMessageFromPeer": case "NewMessageFromPeer":
notificationManager.notify("New Message From Peer!"); notificationManager.notify("New Message From Peer!");
if (appState.selectedProfile != data["ProfileOnion"] || appState.selectedConversation != data["RemotePeer"]) { var identifier = int.parse(data["ConversationID"]);
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["RemotePeer"])!.unreadMessages++; var messageID = int.parse(data["Index"]);
} else { var timestamp = DateTime.tryParse(data['TimestampReceived'])!;
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["RemotePeer"])!.newMarker++; var senderHandle = data['RemotePeer'];
} var senderImage = data['Picture'];
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["RemotePeer"])!.totalMessages++;
profileCN.getProfile(data["ProfileOnion"])?.contactList.updateLastMessageTime(data["RemotePeer"], DateTime.now());
// We only ever see messages from authenticated peers. // We might not have received a contact created for this contact yet...
// If the contact is marked as offline then override this - can happen when the contact is removed from the front // In that case the **next** event we receive will actually update these values...
// end during syncing. if (profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier) != null) {
if (profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["RemotePeer"])!.isOnline() == false) { if (appState.selectedProfile != data["ProfileOnion"] || appState.selectedConversation != identifier) {
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["RemotePeer"])!.status = "Authenticated"; profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.unreadMessages++;
} else {
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.newMarker++;
}
profileCN.getProfile(data["ProfileOnion"])?.contactList.updateLastMessageTime(identifier, DateTime.now());
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.updateMessageCache(identifier, messageID, timestamp, senderHandle, senderImage, data["Data"]);
// We only ever see messages from authenticated peers.
// If the contact is marked as offline then override this - can happen when the contact is removed from the front
// end during syncing.
if (profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.isOnline() == false) {
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.status = "Authenticated";
}
} }
break; break;
@ -151,43 +160,49 @@ class CwtchNotifier {
// We don't use these anymore, IndexedAcknowledgement is more suited to the UI front end... // We don't use these anymore, IndexedAcknowledgement is more suited to the UI front end...
break; break;
case "IndexedAcknowledgement": case "IndexedAcknowledgement":
var idx = data["Index"]; var conversation = int.parse(data["ConversationID"]);
var messageID = int.parse(data["Index"]);
var contact = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(conversation);
// We return -1 for protocol message acks if there is no message // We return -1 for protocol message acks if there is no message
if (idx == "-1") break; if (messageID == -1) break;
var key = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["RemotePeer"])!.getMessageKey(idx); var key = contact!.getMessageKeyOrFail(conversation, messageID);
if (key == null) break; if (key == null) break;
try { try {
var message = Provider.of<MessageMetadata>(key.currentContext!, listen: false); var message = Provider.of<MessageMetadata>(key.currentContext!, listen: false);
if (message == null) break; message.ackd = true;
// We only ever see acks from authenticated peers. // We only ever see acks from authenticated peers.
// If the contact is marked as offline then override this - can happen when the contact is removed from the front // If the contact is marked as offline then override this - can happen when the contact is removed from the front
// end during syncing. // end during syncing.
if (profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["RemotePeer"])!.isOnline() == false) { if (profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(conversation)!.isOnline() == false) {
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["RemotePeer"])!.status = "Authenticated"; profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(conversation)!.status = "Authenticated";
} }
message.ackd = true; profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(conversation)!.ackCache(messageID);
} catch (e) { } catch (e) {
// ignore, we received an ack for a message that hasn't loaded onto the screen yet... // ignore, most likely cause is the key got optimized out...
// the protocol was faster than the ui....yay?
} }
break; break;
case "NewMessageFromGroup": case "NewMessageFromGroup":
var identifier = int.parse(data["ConversationID"]);
if (data["ProfileOnion"] != data["RemotePeer"]) { if (data["ProfileOnion"] != data["RemotePeer"]) {
var idx = int.parse(data["Index"]); var idx = int.parse(data["Index"]);
var currentTotal = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["GroupID"])!.totalMessages; var senderHandle = data['RemotePeer'];
var senderImage = data['Picture'];
var timestampSent = DateTime.tryParse(data['TimestampSent'])!;
var currentTotal = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.totalMessages;
// Only bother to do anything if we know about the group and the provided index is greater than our current total... // Only bother to do anything if we know about the group and the provided index is greater than our current total...
if (currentTotal != null && idx >= currentTotal) { if (currentTotal != null && idx >= currentTotal) {
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["GroupID"])!.totalMessages = idx + 1; profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.updateMessageCache(identifier, idx, timestampSent, senderHandle, senderImage, data["Data"]);
//if not currently open //if not currently open
if (appState.selectedProfile != data["ProfileOnion"] || appState.selectedConversation != data["GroupID"]) { if (appState.selectedProfile != data["ProfileOnion"] || appState.selectedConversation != identifier) {
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["GroupID"])!.unreadMessages++; profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.unreadMessages++;
} else { } else {
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["GroupID"])!.newMarker++; profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.newMarker++;
} }
var timestampSent = DateTime.tryParse(data['TimestampSent'])!;
// TODO: There are 2 timestamps associated with a new group message - time sent and time received. // TODO: There are 2 timestamps associated with a new group message - time sent and time received.
// Sent refers to the time a profile alleges they sent a message // Sent refers to the time a profile alleges they sent a message
// Received refers to the time we actually saw the message from the server // Received refers to the time we actually saw the message from the server
@ -198,56 +213,24 @@ class CwtchNotifier {
// For now we perform some minimal checks on the sent timestamp to use to provide a useful ordering for honest contacts // For now we perform some minimal checks on the sent timestamp to use to provide a useful ordering for honest contacts
// and ensure that malicious contacts in groups can only set this timestamp to a value within the range of `last seen message time` // and ensure that malicious contacts in groups can only set this timestamp to a value within the range of `last seen message time`
// and `local now`. // and `local now`.
profileCN.getProfile(data["ProfileOnion"])?.contactList.updateLastMessageTime(data["GroupID"], timestampSent.toLocal()); profileCN.getProfile(data["ProfileOnion"])?.contactList.updateLastMessageTime(identifier, timestampSent.toLocal());
notificationManager.notify("New Message From Group!"); notificationManager.notify("New Message From Group!");
} }
} else { } else {
// from me (already displayed - do not update counter) // This is dealt with by IndexedAcknowledgment
var idx = data["Signature"]; EnvironmentConfig.debugLog("new message from group from yourself - this should not happen");
var key = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["GroupID"])?.getMessageKey(idx);
if (key == null) break;
try {
var message = Provider.of<MessageMetadata>(key.currentContext!, listen: false);
if (message == null) break;
message.ackd = true;
} catch (e) {
// ignore, we likely have an old key that has been replaced with an actual signature
}
}
break;
case "MessageCounterResync":
var contactHandle = data["RemotePeer"];
if (contactHandle == null || contactHandle == "") contactHandle = data["GroupID"];
var total = int.parse(data["Data"]);
if (total != profileCN.getProfile(data["Identity"])?.contactList.getContact(contactHandle)!.totalMessages) {
profileCN.getProfile(data["Identity"])?.contactList.getContact(contactHandle)!.totalMessages = total;
} }
break; break;
case "SendMessageToPeerError": case "SendMessageToPeerError":
// Ignore // Ignore
break; break;
case "IndexedFailure": case "IndexedFailure":
var idx = data["Index"]; var contact = profileCN.getProfile(data["ProfileOnion"])?.contactList.findContact(data["RemotePeer"]);
var key = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["RemotePeer"])?.getMessageKey(idx); var idx = int.parse(data["Index"]);
try { var key = contact?.getMessageKeyOrFail(contact.identifier, idx);
var message = Provider.of<MessageMetadata>(key!.currentContext!, listen: false); if (key != null) {
message.error = true;
} catch (e) {
// ignore, we likely have an old key that has been replaced with an actual signature
}
break;
case "SendMessageToGroupError":
// from me (already displayed - do not update counter)
EnvironmentConfig.debugLog("SendMessageToGroupError");
var idx = data["Signature"];
var key = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["GroupID"])!.getMessageKey(idx);
if (key == null) break;
try {
var message = Provider.of<MessageMetadata>(key.currentContext!, listen: false); var message = Provider.of<MessageMetadata>(key.currentContext!, listen: false);
if (message == null) break;
message.error = true; message.error = true;
} catch (e) {
// ignore, we likely have an old key that has been replaced with an actual signature
} }
break; break;
case "AppError": case "AppError":
@ -262,8 +245,8 @@ class CwtchNotifier {
case "UpdateGlobalSettings": case "UpdateGlobalSettings":
settings.handleUpdate(jsonDecode(data["Data"])); settings.handleUpdate(jsonDecode(data["Data"]));
break; break;
case "SetAttribute": case "UpdatedProfileAttribute":
if (data["Key"] == "public.name") { if (data["Key"] == "public.profile.name") {
profileCN.getProfile(data["ProfileOnion"])?.nickname = data["Data"]; profileCN.getProfile(data["ProfileOnion"])?.nickname = data["Data"];
} else { } else {
EnvironmentConfig.debugLog("unhandled set attribute event: ${data['Key']}"); EnvironmentConfig.debugLog("unhandled set attribute event: ${data['Key']}");
@ -285,7 +268,6 @@ class CwtchNotifier {
profileCN.getProfile(data["ProfileOnion"])?.replaceServers(data["ServerList"]); profileCN.getProfile(data["ProfileOnion"])?.replaceServers(data["ServerList"]);
break; break;
case "NewGroup": case "NewGroup":
EnvironmentConfig.debugLog("new group");
String invite = data["GroupInvite"].toString(); String invite = data["GroupInvite"].toString();
if (invite.startsWith("torv3")) { if (invite.startsWith("torv3")) {
String inviteJson = new String.fromCharCodes(base64Decode(invite.substring(5))); String inviteJson = new String.fromCharCodes(base64Decode(invite.substring(5)));
@ -298,8 +280,9 @@ class CwtchNotifier {
status = serverInfoState.status; status = serverInfoState.status;
} }
if (profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(groupInvite["GroupID"]) == null) { if (profileCN.getProfile(data["ProfileOnion"])?.contactList.findContact(groupInvite["GroupID"]) == null) {
profileCN.getProfile(data["ProfileOnion"])?.contactList.add(ContactInfoState(data["ProfileOnion"], groupInvite["GroupID"], var identifier = int.parse(data["ConversationID"]);
profileCN.getProfile(data["ProfileOnion"])?.contactList.add(ContactInfoState(data["ProfileOnion"], identifier, groupInvite["GroupID"],
authorization: ContactAuthorization.approved, authorization: ContactAuthorization.approved,
imagePath: data["PicturePath"], imagePath: data["PicturePath"],
nickname: groupInvite["GroupName"], nickname: groupInvite["GroupName"],
@ -307,7 +290,7 @@ class CwtchNotifier {
status: status, status: status,
isGroup: true, isGroup: true,
lastMessageTime: DateTime.fromMillisecondsSinceEpoch(0))); lastMessageTime: DateTime.fromMillisecondsSinceEpoch(0)));
profileCN.getProfile(data["ProfileOnion"])?.contactList.updateLastMessageTime(groupInvite["GroupID"], DateTime.fromMillisecondsSinceEpoch(0)); profileCN.getProfile(data["ProfileOnion"])?.contactList.updateLastMessageTime(identifier, DateTime.fromMillisecondsSinceEpoch(0));
} }
} }
break; break;
@ -319,7 +302,6 @@ class CwtchNotifier {
break; break;
case "ServerStateChange": case "ServerStateChange":
// Update the Server Cache // Update the Server Cache
EnvironmentConfig.debugLog("server state changes $data");
profileCN.getProfile(data["ProfileOnion"])?.updateServerStatusCache(data["GroupServer"], data["ConnectionState"]); profileCN.getProfile(data["ProfileOnion"])?.updateServerStatusCache(data["GroupServer"], data["ConnectionState"]);
profileCN.getProfile(data["ProfileOnion"])?.contactList.contacts.forEach((contact) { profileCN.getProfile(data["ProfileOnion"])?.contactList.contacts.forEach((contact) {
if (contact.isGroup == true && contact.server == data["GroupServer"]) { if (contact.isGroup == true && contact.server == data["GroupServer"]) {
@ -357,13 +339,17 @@ class CwtchNotifier {
} }
break; break;
case "NewRetValMessageFromPeer": case "NewRetValMessageFromPeer":
if (data["Path"] == "name" && data["Data"].toString().trim().length > 0) { if (data["Path"] == "profile.name") {
// Update locally on the UI... if (data["Data"].toString().trim().length > 0) {
if (profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["RemotePeer"]) != null) { // Update locally on the UI...
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["RemotePeer"])!.nickname = data["Data"]; if (profileCN.getProfile(data["ProfileOnion"])?.contactList.findContact(data["RemotePeer"]) != null) {
profileCN.getProfile(data["ProfileOnion"])?.contactList.findContact(data["RemotePeer"])!.nickname = data["Data"];
}
} }
} else if (data['Path'] == "profile.picture") {
// Not yet..
} else { } else {
EnvironmentConfig.debugLog("unhandled peer attribute event: ${data['Path']}"); EnvironmentConfig.debugLog("unhandled ret val event: ${data['Path']}");
} }
break; break;
case "ManifestSaved": case "ManifestSaved":
@ -380,6 +366,8 @@ class CwtchNotifier {
case "FileDownloaded": case "FileDownloaded":
profileCN.getProfile(data["ProfileOnion"])?.downloadMarkFinished(data["FileKey"], data["FilePath"]); profileCN.getProfile(data["ProfileOnion"])?.downloadMarkFinished(data["FileKey"], data["FilePath"]);
break; break;
case "ImportingProfileEvent":
break;
default: default:
EnvironmentConfig.debugLog("unhandled event: $type"); EnvironmentConfig.debugLog("unhandled event: $type");
} }

View File

@ -36,8 +36,9 @@ typedef VoidFromStringStringStringFn = void Function(Pointer<Utf8>, int, Pointer
typedef void_from_string_string_string_string_function = Void Function(Pointer<Utf8>, Int32, Pointer<Utf8>, Int32, Pointer<Utf8>, Int32, Pointer<Utf8>, Int32); typedef void_from_string_string_string_string_function = Void Function(Pointer<Utf8>, Int32, Pointer<Utf8>, Int32, Pointer<Utf8>, Int32, Pointer<Utf8>, Int32);
typedef VoidFromStringStringStringStringFn = void Function(Pointer<Utf8>, int, Pointer<Utf8>, int, Pointer<Utf8>, int, Pointer<Utf8>, int); typedef VoidFromStringStringStringStringFn = void Function(Pointer<Utf8>, int, Pointer<Utf8>, int, Pointer<Utf8>, int, Pointer<Utf8>, int);
typedef void_from_string_string_string_string_string_function = Void Function(Pointer<Utf8>, Int32, Pointer<Utf8>, Int32, Pointer<Utf8>, Int32, Pointer<Utf8>, Int32, Pointer<Utf8>, Int32); // DownloadFile
typedef VoidFromStringStringStringStringStringFn = void Function(Pointer<Utf8>, int, Pointer<Utf8>, int, Pointer<Utf8>, int, Pointer<Utf8>, int, Pointer<Utf8>, int); typedef void_from_string_int_string_string_string_function = Void Function(Pointer<Utf8>, Int32, Int32, Pointer<Utf8>, Int32, Pointer<Utf8>, Int32, Pointer<Utf8>, Int32);
typedef VoidFromStringIntStringStringStringFn = void Function(Pointer<Utf8>, int, int, Pointer<Utf8>, int, Pointer<Utf8>, int, Pointer<Utf8>, int);
typedef void_from_string_string_int_int_function = Void Function(Pointer<Utf8>, Int32, Pointer<Utf8>, Int32, Int64, Int64); typedef void_from_string_string_int_int_function = Void Function(Pointer<Utf8>, Int32, Pointer<Utf8>, Int32, Int64, Int64);
typedef VoidFromStringStringIntIntFn = void Function(Pointer<Utf8>, int, Pointer<Utf8>, int, int, int); typedef VoidFromStringStringIntIntFn = void Function(Pointer<Utf8>, int, Pointer<Utf8>, int, int, int);
@ -51,6 +52,9 @@ typedef StringFn = void Function(Pointer<Utf8> dir, int);
typedef string_string_to_void_function = Void Function(Pointer<Utf8> str, Int32 length, Pointer<Utf8> str2, Int32 length2); typedef string_string_to_void_function = Void Function(Pointer<Utf8> str, Int32 length, Pointer<Utf8> str2, Int32 length2);
typedef StringStringFn = void Function(Pointer<Utf8>, int, Pointer<Utf8>, int); typedef StringStringFn = void Function(Pointer<Utf8>, int, Pointer<Utf8>, int);
typedef string_int_to_void_function = Void Function(Pointer<Utf8> str, Int32 length, Int32 handle);
typedef VoidFromStringIntFn = void Function(Pointer<Utf8>, int, int);
typedef get_json_blob_string_function = Pointer<Utf8> Function(Pointer<Utf8> str, Int32 length); typedef get_json_blob_string_function = Pointer<Utf8> Function(Pointer<Utf8> str, Int32 length);
typedef GetJsonBlobStringFn = Pointer<Utf8> Function(Pointer<Utf8> str, int len); typedef GetJsonBlobStringFn = Pointer<Utf8> Function(Pointer<Utf8> str, int len);
@ -58,10 +62,34 @@ typedef GetJsonBlobStringFn = Pointer<Utf8> Function(Pointer<Utf8> str, int len)
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_int_function = Pointer<Utf8> Function(Pointer<Utf8>, Int32, Int32, Int32);
typedef GetJsonBlobFromStrIntIntFn = Pointer<Utf8> Function(Pointer<Utf8>, int, int, int);
typedef get_json_blob_from_str_int_string_function = Pointer<Utf8> Function(Pointer<Utf8>, Int32, Int32, Pointer<Utf8>, Int32);
typedef GetJsonBlobFromStrIntStringFn = Pointer<Utf8> Function(
Pointer<Utf8>,
int,
int,
Pointer<Utf8>,
int,
);
// func c_GetMessagesByContentHash(profile_ptr *C.char, profile_len C.int, handle_ptr *C.char, handle_len C.int, contenthash_ptr *C.char, contenthash_len C.int) *C.char // func c_GetMessagesByContentHash(profile_ptr *C.char, profile_len C.int, handle_ptr *C.char, handle_len C.int, contenthash_ptr *C.char, contenthash_len C.int) *C.char
typedef get_json_blob_from_str_str_str_function = Pointer<Utf8> Function(Pointer<Utf8>, Int32, Pointer<Utf8>, Int32, Pointer<Utf8>, Int32); typedef get_json_blob_from_str_str_str_function = Pointer<Utf8> Function(Pointer<Utf8>, Int32, Pointer<Utf8>, Int32, Pointer<Utf8>, Int32);
typedef GetJsonBlobFromStrStrStrFn = Pointer<Utf8> Function(Pointer<Utf8>, int, Pointer<Utf8>, int, Pointer<Utf8>, int); typedef GetJsonBlobFromStrStrStrFn = Pointer<Utf8> Function(Pointer<Utf8>, int, Pointer<Utf8>, int, Pointer<Utf8>, int);
typedef void_from_string_int_string_function = Void Function(Pointer<Utf8>, Int32, Int32, Pointer<Utf8>, Int32);
typedef VoidFromStringIntStringFn = void Function(Pointer<Utf8>, int, int, Pointer<Utf8>, int);
typedef void_from_string_int_string_string_function = Void Function(Pointer<Utf8>, Int32, Int32, Pointer<Utf8>, Int32, Pointer<Utf8>, Int32);
typedef VoidFromStringIntStringStringFn = void Function(Pointer<Utf8>, int, int, Pointer<Utf8>, int, Pointer<Utf8>, int);
typedef void_from_string_int_int_int_string_string_function = Void Function(Pointer<Utf8>, Int32, Int32, Int32, Int32, Pointer<Utf8>, Int32, Pointer<Utf8>, Int32);
typedef VoidFromStringIntIntIntStringStringFn = void Function(Pointer<Utf8>, int, int, int, int, Pointer<Utf8>, int, Pointer<Utf8>, int);
typedef void_from_string_int_int_function = Void Function(Pointer<Utf8>, Int32, Int32, Int32);
typedef VoidFromStringIntIntFn = void Function(Pointer<Utf8>, int, int, int);
typedef appbus_events_function = Pointer<Utf8> Function(); typedef appbus_events_function = Pointer<Utf8> Function();
typedef AppbusEventsFn = Pointer<Utf8> Function(); typedef AppbusEventsFn = Pointer<Utf8> Function();
@ -233,16 +261,6 @@ class CwtchFfi implements Cwtch {
} }
} }
// ignore: non_constant_identifier_names
void SelectProfile(String onion) async {
var selectProfileC = library.lookup<NativeFunction<get_json_blob_string_function>>("c_SelectProfile");
// ignore: non_constant_identifier_names
final SelectProfile = selectProfileC.asFunction<GetJsonBlobStringFn>();
final ut8Onion = onion.toNativeUtf8();
SelectProfile(ut8Onion, ut8Onion.length);
malloc.free(ut8Onion);
}
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void CreateProfile(String nick, String pass) { void CreateProfile(String nick, String pass) {
var createProfileC = library.lookup<NativeFunction<void_from_string_string_function>>("c_CreateProfile"); var createProfileC = library.lookup<NativeFunction<void_from_string_string_function>>("c_CreateProfile");
@ -266,17 +284,15 @@ class CwtchFfi implements Cwtch {
} }
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
Future<String> GetMessage(String profile, String handle, int index) async { Future<String> GetMessage(String profile, int handle, int index) async {
var getMessageC = library.lookup<NativeFunction<get_json_blob_from_str_str_int_function>>("c_GetMessage"); var getMessageC = library.lookup<NativeFunction<get_json_blob_from_str_int_int_function>>("c_GetMessage");
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
final GetMessage = getMessageC.asFunction<GetJsonBlobFromStrStrIntFn>(); final GetMessage = getMessageC.asFunction<GetJsonBlobFromStrIntIntFn>();
final utf8profile = profile.toNativeUtf8(); final utf8profile = profile.toNativeUtf8();
final utf8handle = handle.toNativeUtf8(); Pointer<Utf8> jsonMessageBytes = GetMessage(utf8profile, utf8profile.length, handle, index);
Pointer<Utf8> jsonMessageBytes = GetMessage(utf8profile, utf8profile.length, utf8handle, utf8handle.length, index);
String jsonMessage = jsonMessageBytes.toDartString(); String jsonMessage = jsonMessageBytes.toDartString();
_UnsafeFreePointerAnyUseOfThisFunctionMustBeDoubleApproved(jsonMessageBytes); _UnsafeFreePointerAnyUseOfThisFunctionMustBeDoubleApproved(jsonMessageBytes);
malloc.free(utf8profile); malloc.free(utf8profile);
malloc.free(utf8handle);
return jsonMessage; return jsonMessage;
} }
@ -306,89 +322,75 @@ class CwtchFfi implements Cwtch {
@override @override
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void AcceptContact(String profileOnion, String contactHandle) { void AcceptContact(String profileOnion, int contactHandle) {
var acceptContact = library.lookup<NativeFunction<string_string_to_void_function>>("c_AcceptContact"); var acceptContact = library.lookup<NativeFunction<string_int_to_void_function>>("c_AcceptConversation");
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
final AcceptContact = acceptContact.asFunction<VoidFromStringStringFn>(); final AcceptContact = acceptContact.asFunction<VoidFromStringIntFn>();
final u1 = profileOnion.toNativeUtf8(); final u1 = profileOnion.toNativeUtf8();
final u2 = contactHandle.toNativeUtf8(); AcceptContact(u1, u1.length, contactHandle);
AcceptContact(u1, u1.length, u2, u2.length);
malloc.free(u1); malloc.free(u1);
malloc.free(u2);
} }
@override @override
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void BlockContact(String profileOnion, String contactHandle) { void BlockContact(String profileOnion, int contactHandle) {
var blockContact = library.lookup<NativeFunction<string_string_to_void_function>>("c_BlockContact"); var blockContact = library.lookup<NativeFunction<string_int_to_void_function>>("c_BlockContact");
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
final BlockContact = blockContact.asFunction<VoidFromStringStringFn>(); final BlockContact = blockContact.asFunction<VoidFromStringIntFn>();
final u1 = profileOnion.toNativeUtf8(); final u1 = profileOnion.toNativeUtf8();
final u2 = contactHandle.toNativeUtf8(); BlockContact(u1, u1.length, contactHandle);
BlockContact(u1, u1.length, u2, u2.length);
malloc.free(u1); malloc.free(u1);
malloc.free(u2);
} }
@override @override
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void SendMessage(String profileOnion, String contactHandle, String message) { void SendMessage(String profileOnion, int contactHandle, String message) {
var sendMessage = library.lookup<NativeFunction<void_from_string_string_string_function>>("c_SendMessage"); var sendMessage = library.lookup<NativeFunction<void_from_string_int_string_function>>("c_SendMessage");
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
final SendMessage = sendMessage.asFunction<VoidFromStringStringStringFn>(); final SendMessage = sendMessage.asFunction<VoidFromStringIntStringFn>();
final u1 = profileOnion.toNativeUtf8(); final u1 = profileOnion.toNativeUtf8();
final u2 = contactHandle.toNativeUtf8();
final u3 = message.toNativeUtf8(); final u3 = message.toNativeUtf8();
SendMessage(u1, u1.length, u2, u2.length, u3, u3.length); SendMessage(u1, u1.length, contactHandle, u3, u3.length);
malloc.free(u1); malloc.free(u1);
malloc.free(u2);
malloc.free(u3); malloc.free(u3);
} }
@override @override
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void SendInvitation(String profileOnion, String contactHandle, String target) { void SendInvitation(String profileOnion, int contactHandle, int target) {
var sendInvitation = library.lookup<NativeFunction<void_from_string_string_string_function>>("c_SendInvitation"); var sendInvitation = library.lookup<NativeFunction<void_from_string_int_int_function>>("c_SendInvitation");
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
final SendInvitation = sendInvitation.asFunction<VoidFromStringStringStringFn>(); final SendInvitation = sendInvitation.asFunction<VoidFromStringIntIntFn>();
final u1 = profileOnion.toNativeUtf8(); final u1 = profileOnion.toNativeUtf8();
final u2 = contactHandle.toNativeUtf8(); SendInvitation(u1, u1.length, contactHandle, target);
final u3 = target.toNativeUtf8();
SendInvitation(u1, u1.length, u2, u2.length, u3, u3.length);
malloc.free(u1); malloc.free(u1);
malloc.free(u2);
malloc.free(u3);
} }
@override @override
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void ShareFile(String profileOnion, String contactHandle, String filepath) { void ShareFile(String profileOnion, int contactHandle, String filepath) {
var shareFile = library.lookup<NativeFunction<void_from_string_string_string_function>>("c_ShareFile"); var shareFile = library.lookup<NativeFunction<void_from_string_int_string_function>>("c_ShareFile");
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
final ShareFile = shareFile.asFunction<VoidFromStringStringStringFn>(); final ShareFile = shareFile.asFunction<VoidFromStringIntStringFn>();
final u1 = profileOnion.toNativeUtf8(); final u1 = profileOnion.toNativeUtf8();
final u2 = contactHandle.toNativeUtf8();
final u3 = filepath.toNativeUtf8(); final u3 = filepath.toNativeUtf8();
ShareFile(u1, u1.length, u2, u2.length, u3, u3.length); ShareFile(u1, u1.length, contactHandle, u3, u3.length);
malloc.free(u1); malloc.free(u1);
malloc.free(u2);
malloc.free(u3); malloc.free(u3);
} }
@override @override
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void DownloadFile(String profileOnion, String contactHandle, String filepath, String manifestpath, String filekey) { void DownloadFile(String profileOnion, int contactHandle, String filepath, String manifestpath, String filekey) {
var dlFile = library.lookup<NativeFunction<void_from_string_string_string_string_string_function>>("c_DownloadFile"); var dlFile = library.lookup<NativeFunction<void_from_string_int_string_string_string_function>>("c_DownloadFile");
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
final DownloadFile = dlFile.asFunction<VoidFromStringStringStringStringStringFn>(); final DownloadFile = dlFile.asFunction<VoidFromStringIntStringStringStringFn>();
final u1 = profileOnion.toNativeUtf8(); final u1 = profileOnion.toNativeUtf8();
final u2 = contactHandle.toNativeUtf8();
final u3 = filepath.toNativeUtf8(); final u3 = filepath.toNativeUtf8();
final u4 = manifestpath.toNativeUtf8(); final u4 = manifestpath.toNativeUtf8();
final u5 = filekey.toNativeUtf8(); final u5 = filekey.toNativeUtf8();
DownloadFile(u1, u1.length, u2, u2.length, u3, u3.length, u4, u4.length, u5, u5.length); DownloadFile(u1, u1.length, contactHandle, u3, u3.length, u4, u4.length, u5, u5.length);
malloc.free(u1); malloc.free(u1);
malloc.free(u2);
malloc.free(u3); malloc.free(u3);
malloc.free(u4); malloc.free(u4);
malloc.free(u5); malloc.free(u5);
@ -396,7 +398,7 @@ class CwtchFfi implements Cwtch {
@override @override
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void CreateDownloadableFile(String profileOnion, String contactHandle, String filenameSuggestion, String filekey) { void CreateDownloadableFile(String profileOnion, int contactHandle, String filenameSuggestion, String filekey) {
// android only - do nothing // android only - do nothing
} }
@ -415,20 +417,17 @@ class CwtchFfi implements Cwtch {
@override @override
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void VerifyOrResumeDownload(String profileOnion, String contactHandle, String filekey) { void VerifyOrResumeDownload(String profileOnion, int contactHandle, String filekey) {
var fn = library.lookup<NativeFunction<void_from_string_string_string_function>>("c_VerifyOrResumeDownload"); var fn = library.lookup<NativeFunction<void_from_string_int_string_function>>("c_VerifyOrResumeDownload");
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
final VerifyOrResumeDownload = fn.asFunction<VoidFromStringStringStringFn>(); final VerifyOrResumeDownload = fn.asFunction<VoidFromStringIntStringFn>();
final u1 = profileOnion.toNativeUtf8(); final u1 = profileOnion.toNativeUtf8();
final u2 = contactHandle.toNativeUtf8();
final u3 = filekey.toNativeUtf8(); final u3 = filekey.toNativeUtf8();
VerifyOrResumeDownload(u1, u1.length, u2, u2.length, u3, u3.length); VerifyOrResumeDownload(u1, u1.length, contactHandle, u3, u3.length);
malloc.free(u1); malloc.free(u1);
malloc.free(u2);
malloc.free(u3); malloc.free(u3);
} }
@override @override
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void ResetTor() { void ResetTor() {
@ -451,36 +450,6 @@ class CwtchFfi implements Cwtch {
malloc.free(u2); malloc.free(u2);
} }
@override
// ignore: non_constant_identifier_names
void SetGroupAttribute(String profileOnion, String groupHandle, String key, String value) {
var setGroupAttribute = library.lookup<NativeFunction<void_from_string_string_string_string_function>>("c_SetGroupAttribute");
// ignore: non_constant_identifier_names
final SetGroupAttribute = setGroupAttribute.asFunction<VoidFromStringStringStringStringFn>();
final u1 = profileOnion.toNativeUtf8();
final u2 = groupHandle.toNativeUtf8();
final u3 = key.toNativeUtf8();
final u4 = value.toNativeUtf8();
SetGroupAttribute(u1, u1.length, u2, u2.length, u3, u3.length, u4, u4.length);
malloc.free(u1);
malloc.free(u2);
malloc.free(u3);
malloc.free(u4);
}
@override
// ignore: non_constant_identifier_names
void RejectInvite(String profileOnion, String groupHandle) {
var rejectInvite = library.lookup<NativeFunction<string_string_to_void_function>>("c_RejectInvite");
// ignore: non_constant_identifier_names
final RejectInvite = rejectInvite.asFunction<VoidFromStringStringFn>();
final u1 = profileOnion.toNativeUtf8();
final u2 = groupHandle.toNativeUtf8();
RejectInvite(u1, u1.length, u2, u2.length);
malloc.free(u1);
malloc.free(u2);
}
@override @override
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void CreateGroup(String profileOnion, String server, String groupName) { void CreateGroup(String profileOnion, String server, String groupName) {
@ -499,41 +468,24 @@ class CwtchFfi implements Cwtch {
@override @override
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void ArchiveConversation(String profileOnion, String handle) { void ArchiveConversation(String profileOnion, int handle) {
var archiveConversation = library.lookup<NativeFunction<string_string_to_void_function>>("c_ArchiveConversation"); var archiveConversation = library.lookup<NativeFunction<string_int_to_void_function>>("c_ArchiveConversation");
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
final ArchiveConversation = archiveConversation.asFunction<VoidFromStringStringFn>(); final ArchiveConversation = archiveConversation.asFunction<VoidFromStringIntFn>();
final u1 = profileOnion.toNativeUtf8(); final u1 = profileOnion.toNativeUtf8();
final u2 = handle.toNativeUtf8(); ArchiveConversation(u1, u1.length, handle);
ArchiveConversation(u1, u1.length, u2, u2.length);
malloc.free(u1); malloc.free(u1);
malloc.free(u2);
} }
@override @override
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void DeleteContact(String profileOnion, String handle) { void DeleteContact(String profileOnion, int handle) {
var deleteContact = library.lookup<NativeFunction<string_string_to_void_function>>("c_DeleteContact"); var deleteContact = library.lookup<NativeFunction<string_int_to_void_function>>("c_DeleteContact");
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
final DeleteContact = deleteContact.asFunction<VoidFromStringStringFn>(); final DeleteContact = deleteContact.asFunction<VoidFromStringIntFn>();
final u1 = profileOnion.toNativeUtf8(); final u1 = profileOnion.toNativeUtf8();
final u2 = handle.toNativeUtf8(); DeleteContact(u1, u1.length, handle);
DeleteContact(u1, u1.length, u2, u2.length);
malloc.free(u1); malloc.free(u1);
malloc.free(u2);
}
@override
// ignore: non_constant_identifier_names
void UpdateMessageFlags(String profile, String handle, int index, int flags) {
var updateMessageFlagsC = library.lookup<NativeFunction<void_from_string_string_int_int_function>>("c_UpdateMessageFlags");
// ignore: non_constant_identifier_names
final updateMessageFlags = updateMessageFlagsC.asFunction<VoidFromStringStringIntIntFn>();
final utf8profile = profile.toNativeUtf8();
final utf8handle = handle.toNativeUtf8();
updateMessageFlags(utf8profile, utf8profile.length, utf8handle, utf8handle.length, index, flags);
malloc.free(utf8profile);
malloc.free(utf8handle);
} }
@override @override
@ -557,7 +509,7 @@ class CwtchFfi implements Cwtch {
final SetProfileAttribute = setProfileAttribute.asFunction<VoidFromStringStringStringFn>(); final SetProfileAttribute = setProfileAttribute.asFunction<VoidFromStringStringStringFn>();
final u1 = profile.toNativeUtf8(); final u1 = profile.toNativeUtf8();
final u2 = key.toNativeUtf8(); final u2 = key.toNativeUtf8();
final u3 = key.toNativeUtf8(); final u3 = val.toNativeUtf8();
SetProfileAttribute(u1, u1.length, u2, u2.length, u3, u3.length); SetProfileAttribute(u1, u1.length, u2, u2.length, u3, u3.length);
malloc.free(u1); malloc.free(u1);
malloc.free(u2); malloc.free(u2);
@ -566,17 +518,30 @@ class CwtchFfi implements Cwtch {
@override @override
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void SetContactAttribute(String profile, String contact, String key, String val) { void SetConversationAttribute(String profile, int contact, String key, String val) {
var setContactAttribute = library.lookup<NativeFunction<void_from_string_string_string_string_function>>("c_SetContactAttribute"); var setContactAttribute = library.lookup<NativeFunction<void_from_string_int_string_string_function>>("c_SetConversationAttribute");
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
final SetContactAttribute = setContactAttribute.asFunction<VoidFromStringStringStringStringFn>(); final SetContactAttribute = setContactAttribute.asFunction<VoidFromStringIntStringStringFn>();
final u1 = profile.toNativeUtf8(); final u1 = profile.toNativeUtf8();
final u2 = contact.toNativeUtf8();
final u3 = key.toNativeUtf8(); final u3 = key.toNativeUtf8();
final u4 = key.toNativeUtf8(); final u4 = val.toNativeUtf8();
SetContactAttribute(u1, u1.length, u2, u2.length, u3, u3.length, u4, u4.length); SetContactAttribute(u1, u1.length, contact, u3, u3.length, u4, u4.length);
malloc.free(u1);
malloc.free(u3);
malloc.free(u4);
}
@override
// ignore: non_constant_identifier_names
void SetMessageAttribute(String profile, int conversation, int channel, int message, String key, String val) {
var setMessageAttribute = library.lookup<NativeFunction<void_from_string_int_int_int_string_string_function>>("c_SetMessageAttribute");
// ignore: non_constant_identifier_names
final SetMessageAttribute = setMessageAttribute.asFunction<VoidFromStringIntIntIntStringStringFn>();
final u1 = profile.toNativeUtf8();
final u3 = key.toNativeUtf8();
final u4 = val.toNativeUtf8();
SetMessageAttribute(u1, u1.length, conversation, channel, message, u3, u3.length, u4, u4.length);
malloc.free(u1); malloc.free(u1);
malloc.free(u2);
malloc.free(u3); malloc.free(u3);
malloc.free(u4); malloc.free(u4);
} }
@ -703,19 +668,17 @@ class CwtchFfi implements Cwtch {
@override @override
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
Future GetMessageByContentHash(String profile, String handle, String contentHash) async { Future GetMessageByContentHash(String profile, int handle, String contentHash) async {
var getMessagesByContentHashC = library.lookup<NativeFunction<get_json_blob_from_str_str_str_function>>("c_GetMessagesByContentHash"); var getMessagesByContentHashC = library.lookup<NativeFunction<get_json_blob_from_str_int_string_function>>("c_GetMessagesByContentHash");
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
final GetMessagesByContentHash = getMessagesByContentHashC.asFunction<GetJsonBlobFromStrStrStrFn>(); final GetMessagesByContentHash = getMessagesByContentHashC.asFunction<GetJsonBlobFromStrIntStringFn>();
final utf8profile = profile.toNativeUtf8(); final utf8profile = profile.toNativeUtf8();
final utf8handle = handle.toNativeUtf8();
final utf8contentHash = contentHash.toNativeUtf8(); final utf8contentHash = contentHash.toNativeUtf8();
Pointer<Utf8> jsonMessageBytes = GetMessagesByContentHash(utf8profile, utf8profile.length, utf8handle, utf8handle.length, utf8contentHash, utf8contentHash.length); Pointer<Utf8> jsonMessageBytes = GetMessagesByContentHash(utf8profile, utf8profile.length, handle, utf8contentHash, utf8contentHash.length);
String jsonMessage = jsonMessageBytes.toDartString(); String jsonMessage = jsonMessageBytes.toDartString();
_UnsafeFreePointerAnyUseOfThisFunctionMustBeDoubleApproved(jsonMessageBytes); _UnsafeFreePointerAnyUseOfThisFunctionMustBeDoubleApproved(jsonMessageBytes);
malloc.free(utf8profile); malloc.free(utf8profile);
malloc.free(utf8handle);
malloc.free(utf8contentHash); malloc.free(utf8contentHash);
return jsonMessage; return jsonMessage;
} }
@ -728,4 +691,17 @@ class CwtchFfi implements Cwtch {
final Free = free.asFunction<FreeFn>(); final Free = free.asFunction<FreeFn>();
Free(ptr); Free(ptr);
} }
@override
Future<String> GetMessageByID(String profile, int handle, int index) async {
var getMessageC = library.lookup<NativeFunction<get_json_blob_from_str_int_int_function>>("c_GetMessageByID");
// ignore: non_constant_identifier_names
final GetMessage = getMessageC.asFunction<GetJsonBlobFromStrIntIntFn>();
final utf8profile = profile.toNativeUtf8();
Pointer<Utf8> jsonMessageBytes = GetMessage(utf8profile, utf8profile.length, handle, index);
String jsonMessage = jsonMessageBytes.toDartString();
_UnsafeFreePointerAnyUseOfThisFunctionMustBeDoubleApproved(jsonMessageBytes);
malloc.free(utf8profile);
return jsonMessage;
}
} }

View File

@ -66,11 +66,6 @@ class CwtchGomobile implements Cwtch {
cwtchNotifier.handleMessage(call.method, obj); cwtchNotifier.handleMessage(call.method, obj);
} }
// ignore: non_constant_identifier_names
void SelectProfile(String onion) {
cwtchPlatform.invokeMethod("SelectProfile", {"profile": onion});
}
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void CreateProfile(String nick, String pass) { void CreateProfile(String nick, String pass) {
cwtchPlatform.invokeMethod("CreateProfile", {"nick": nick, "pass": pass}); cwtchPlatform.invokeMethod("CreateProfile", {"nick": nick, "pass": pass});
@ -87,9 +82,13 @@ class CwtchGomobile implements Cwtch {
} }
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
Future<dynamic> GetMessage(String profile, String handle, int index) { Future<dynamic> GetMessage(String profile, int conversation, int index) {
print("gomobile.dart GetMessage " + index.toString()); return cwtchPlatform.invokeMethod("GetMessage", {"ProfileOnion": profile, "conversation": conversation, "index": index});
return cwtchPlatform.invokeMethod("GetMessage", {"profile": profile, "contact": handle, "index": index}); }
// ignore: non_constant_identifier_names
Future<dynamic> GetMessageByID(String profile, int conversation, int id) {
return cwtchPlatform.invokeMethod("GetMessageByID", {"ProfileOnion": profile, "conversation": conversation, "id": id});
} }
@override @override
@ -109,43 +108,43 @@ class CwtchGomobile implements Cwtch {
@override @override
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void AcceptContact(String profileOnion, String contactHandle) { void AcceptContact(String profileOnion, int conversation) {
cwtchPlatform.invokeMethod("AcceptContact", {"ProfileOnion": profileOnion, "handle": contactHandle}); cwtchPlatform.invokeMethod("AcceptContact", {"ProfileOnion": profileOnion, "conversation": conversation});
} }
@override @override
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void BlockContact(String profileOnion, String contactHandle) { void BlockContact(String profileOnion, int conversation) {
cwtchPlatform.invokeMethod("BlockContact", {"ProfileOnion": profileOnion, "handle": contactHandle}); cwtchPlatform.invokeMethod("BlockContact", {"ProfileOnion": profileOnion, "conversation": conversation});
} }
@override @override
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void SendMessage(String profileOnion, String contactHandle, String message) { void SendMessage(String profileOnion, int conversation, String message) {
cwtchPlatform.invokeMethod("SendMessage", {"ProfileOnion": profileOnion, "handle": contactHandle, "message": message}); cwtchPlatform.invokeMethod("SendMessage", {"ProfileOnion": profileOnion, "conversation": conversation, "message": message});
} }
@override @override
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void SendInvitation(String profileOnion, String contactHandle, String target) { void SendInvitation(String profileOnion, int conversation, int target) {
cwtchPlatform.invokeMethod("SendInvitation", {"ProfileOnion": profileOnion, "handle": contactHandle, "target": target}); cwtchPlatform.invokeMethod("SendInvitation", {"ProfileOnion": profileOnion, "conversation": conversation, "target": target});
} }
@override @override
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void ShareFile(String profileOnion, String contactHandle, String filepath) { void ShareFile(String profileOnion, int conversation, String filepath) {
cwtchPlatform.invokeMethod("ShareFile", {"ProfileOnion": profileOnion, "handle": contactHandle, "filepath": filepath}); cwtchPlatform.invokeMethod("ShareFile", {"ProfileOnion": profileOnion, "conversation": conversation, "filepath": filepath});
} }
@override @override
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void DownloadFile(String profileOnion, String contactHandle, String filepath, String manifestpath, String filekey) { void DownloadFile(String profileOnion, int conversation, String filepath, String manifestpath, String filekey) {
cwtchPlatform.invokeMethod("DownloadFile", {"ProfileOnion": profileOnion, "handle": contactHandle, "filepath": filepath, "manifestpath": manifestpath, "filekey": filekey}); cwtchPlatform.invokeMethod("DownloadFile", {"ProfileOnion": profileOnion, "conversation": conversation, "filepath": filepath, "manifestpath": manifestpath, "filekey": filekey});
} }
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void CreateDownloadableFile(String profileOnion, String contactHandle, String filenameSuggestion, String filekey) { void CreateDownloadableFile(String profileOnion, int conversation, String filenameSuggestion, String filekey) {
cwtchPlatform.invokeMethod("CreateDownloadableFile", {"ProfileOnion": profileOnion, "handle": contactHandle, "filename": filenameSuggestion, "filekey": filekey}); cwtchPlatform.invokeMethod("CreateDownloadableFile", {"ProfileOnion": profileOnion, "conversation": conversation, "filename": filenameSuggestion, "filekey": filekey});
} }
@override @override
@ -156,8 +155,8 @@ class CwtchGomobile implements Cwtch {
@override @override
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void VerifyOrResumeDownload(String profileOnion, String contactHandle, String filekey) { void VerifyOrResumeDownload(String profileOnion, int conversation, String filekey) {
cwtchPlatform.invokeMethod("VerifyOrResumeDownload", {"ProfileOnion": profileOnion, "handle": contactHandle, "filekey": filekey}); cwtchPlatform.invokeMethod("VerifyOrResumeDownload", {"ProfileOnion": profileOnion, "conversation": conversation, "filekey": filekey});
} }
@override @override
@ -172,18 +171,6 @@ class CwtchGomobile implements Cwtch {
cwtchPlatform.invokeMethod("ImportBundle", {"ProfileOnion": profileOnion, "bundle": bundle}); cwtchPlatform.invokeMethod("ImportBundle", {"ProfileOnion": profileOnion, "bundle": bundle});
} }
@override
// ignore: non_constant_identifier_names
void SetGroupAttribute(String profileOnion, String groupHandle, String key, String value) {
cwtchPlatform.invokeMethod("SetGroupAttribute", {"ProfileOnion": profileOnion, "groupHandle": groupHandle, "key": key, "value": value});
}
@override
// ignore: non_constant_identifier_names
void RejectInvite(String profileOnion, String groupHandle) {
cwtchPlatform.invokeMethod("RejectInvite", {"ProfileOnion": profileOnion, "groupHandle": groupHandle});
}
@override @override
void CreateGroup(String profileOnion, String server, String groupName) { void CreateGroup(String profileOnion, String server, String groupName) {
cwtchPlatform.invokeMethod("CreateGroup", {"ProfileOnion": profileOnion, "server": server, "groupName": groupName}); cwtchPlatform.invokeMethod("CreateGroup", {"ProfileOnion": profileOnion, "server": server, "groupName": groupName});
@ -191,20 +178,14 @@ class CwtchGomobile implements Cwtch {
@override @override
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void DeleteContact(String profileOnion, String handle) { void DeleteContact(String profileOnion, int conversation) {
cwtchPlatform.invokeMethod("DeleteContact", {"ProfileOnion": profileOnion, "handle": handle}); cwtchPlatform.invokeMethod("DeleteContact", {"ProfileOnion": profileOnion, "conversation": conversation});
} }
@override @override
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void ArchiveConversation(String profileOnion, String contactHandle) { void ArchiveConversation(String profileOnion, int conversation) {
cwtchPlatform.invokeMethod("ArchiveConversation", {"ProfileOnion": profileOnion, "handle": contactHandle}); cwtchPlatform.invokeMethod("ArchiveConversation", {"ProfileOnion": profileOnion, "conversation": conversation});
}
@override
void UpdateMessageFlags(String profile, String handle, int index, int flags) {
print("gomobile.dart UpdateMessageFlags " + index.toString());
cwtchPlatform.invokeMethod("UpdateMessageFlags", {"profile": profile, "contact": handle, "midx": index, "flags": flags});
} }
@override @override
@ -215,8 +196,8 @@ class CwtchGomobile implements Cwtch {
@override @override
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void SetContactAttribute(String profile, String contact, String key, String val) { void SetConversationAttribute(String profile, int conversation, String key, String val) {
cwtchPlatform.invokeMethod("SetContactAttribute", {"ProfileOnion": profile, "Contact": contact, "Key": key, "Val": val}); cwtchPlatform.invokeMethod("SetContactAttribute", {"ProfileOnion": profile, "conversation": conversation, "Key": key, "Val": val});
} }
@override @override
@ -273,14 +254,19 @@ class CwtchGomobile implements Cwtch {
cwtchPlatform.invokeMethod("SetServerAttribute", {"ServerOnion": serverOnion, "Key": key, "Val": val}); cwtchPlatform.invokeMethod("SetServerAttribute", {"ServerOnion": serverOnion, "Key": key, "Val": val});
} }
@override @override
Future<void> Shutdown() async { Future<void> Shutdown() async {
print("gomobile.dart Shutdown"); print("gomobile.dart Shutdown");
cwtchPlatform.invokeMethod("Shutdown", {}); cwtchPlatform.invokeMethod("Shutdown", {});
} }
@override @override
Future GetMessageByContentHash(String profile, String handle, String contentHash) { Future GetMessageByContentHash(String profile, int conversation, String contentHash) {
return cwtchPlatform.invokeMethod("GetMessageByContentHash", {"profile": profile, "contact": handle, "contentHash": contentHash}); return cwtchPlatform.invokeMethod("GetMessageByContentHash", {"ProfileOnion": profile, "conversation": conversation, "contentHash": contentHash});
}
@override
void SetMessageAttribute(String profile, int conversation, int channel, int message, String key, String val) {
cwtchPlatform.invokeMethod("SetMessageAttribute", {"ProfileOnion": profile, "conversation": conversation, "Channel": channel, "Message": message, "Key": key, "Val": val});
} }
} }

View File

@ -1,5 +1,7 @@
import 'dart:convert'; import 'dart:convert';
import 'package:cwtch/config.dart';
import 'package:cwtch/models/message.dart';
import 'package:cwtch/widgets/messagerow.dart'; import 'package:cwtch/widgets/messagerow.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:cwtch/models/profileservers.dart'; import 'package:cwtch/models/profileservers.dart';
@ -29,7 +31,7 @@ class AppState extends ChangeNotifier {
bool cwtchIsClosing = false; bool cwtchIsClosing = false;
String appError = ""; String appError = "";
String? _selectedProfile; String? _selectedProfile;
String? _selectedConversation; int? _selectedConversation;
int _initialScrollIndex = 0; int _initialScrollIndex = 0;
int _hoveredIndex = -1; int _hoveredIndex = -1;
int? _selectedIndex; int? _selectedIndex;
@ -51,8 +53,8 @@ class AppState extends ChangeNotifier {
notifyListeners(); notifyListeners();
} }
String? get selectedConversation => _selectedConversation; int? get selectedConversation => _selectedConversation;
set selectedConversation(String? newVal) { set selectedConversation(int? newVal) {
this._selectedConversation = newVal; this._selectedConversation = newVal;
notifyListeners(); notifyListeners();
} }
@ -172,8 +174,8 @@ class ContactListState extends ChangeNotifier {
//} </todo> //} </todo>
} }
void updateLastMessageTime(String forOnion, DateTime newMessageTime) { void updateLastMessageTime(int forIdentifier, DateTime newMessageTime) {
var contact = getContact(forOnion); var contact = getContact(forIdentifier);
if (contact == null) return; if (contact == null) return;
// Assert that the new time is after the current last message time AND that // Assert that the new time is after the current last message time AND that
@ -191,18 +193,23 @@ class ContactListState extends ChangeNotifier {
List<ContactInfoState> get contacts => _contacts.sublist(0); //todo: copy?? dont want caller able to bypass changenotifier List<ContactInfoState> get contacts => _contacts.sublist(0); //todo: copy?? dont want caller able to bypass changenotifier
ContactInfoState? getContact(String onion) { ContactInfoState? getContact(int identifier) {
int idx = _contacts.indexWhere((element) => element.onion == onion); int idx = _contacts.indexWhere((element) => element.identifier == identifier);
return idx >= 0 ? _contacts[idx] : null; return idx >= 0 ? _contacts[idx] : null;
} }
void removeContact(String onion) { void removeContact(int identifier) {
int idx = _contacts.indexWhere((element) => element.onion == onion); int idx = _contacts.indexWhere((element) => element.identifier == identifier);
if (idx >= 0) { if (idx >= 0) {
_contacts.removeAt(idx); _contacts.removeAt(idx);
notifyListeners(); notifyListeners();
} }
} }
ContactInfoState? findContact(String byHandle) {
int idx = _contacts.indexWhere((element) => element.onion == byHandle);
return idx >= 0 ? _contacts[idx] : null;
}
} }
class ProfileInfoState extends ChangeNotifier { class ProfileInfoState extends ChangeNotifier {
@ -238,7 +245,7 @@ class ProfileInfoState extends ChangeNotifier {
if (contactsJson != null && contactsJson != "" && contactsJson != "null") { if (contactsJson != null && contactsJson != "" && contactsJson != "null") {
List<dynamic> contacts = jsonDecode(contactsJson); List<dynamic> contacts = jsonDecode(contactsJson);
this._contacts.addAll(contacts.map((contact) { this._contacts.addAll(contacts.map((contact) {
return ContactInfoState(this.onion, contact["onion"], return ContactInfoState(this.onion, contact["identifier"], contact["onion"],
nickname: contact["name"], nickname: contact["name"],
status: contact["status"], status: contact["status"],
imagePath: contact["picture"], imagePath: contact["picture"],
@ -254,7 +261,7 @@ class ProfileInfoState extends ChangeNotifier {
// dummy set to invoke sort-on-load // dummy set to invoke sort-on-load
if (this._contacts.num > 0) { if (this._contacts.num > 0) {
this._contacts.updateLastMessageTime(this._contacts._contacts.first.onion, this._contacts._contacts.first.lastMessageTime); this._contacts.updateLastMessageTime(this._contacts._contacts.first.identifier, this._contacts._contacts.first.lastMessageTime);
} }
} }
@ -341,6 +348,7 @@ class ProfileInfoState extends ChangeNotifier {
} else { } else {
this._contacts.add(ContactInfoState( this._contacts.add(ContactInfoState(
this.onion, this.onion,
contact["identifier"],
contact["onion"], contact["onion"],
nickname: contact["name"], nickname: contact["name"],
status: contact["status"], status: contact["status"],
@ -494,8 +502,15 @@ ContactAuthorization stringToContactAuthorization(String authStr) {
} }
} }
class MessageCache {
final MessageMetadata metadata;
final String wrapper;
MessageCache(this.metadata, this.wrapper);
}
class ContactInfoState extends ChangeNotifier { class ContactInfoState extends ChangeNotifier {
final String profileOnion; final String profileOnion;
final int identifier;
final String onion; final String onion;
late String _nickname; late String _nickname;
@ -507,6 +522,7 @@ class ContactInfoState extends ChangeNotifier {
late int _totalMessages = 0; late int _totalMessages = 0;
late DateTime _lastMessageTime; late DateTime _lastMessageTime;
late Map<String, GlobalKey<MessageRowState>> keys; late Map<String, GlobalKey<MessageRowState>> keys;
late List<MessageCache?> messageCache;
int _newMarker = 0; int _newMarker = 0;
DateTime _newMarkerClearAt = DateTime.now(); DateTime _newMarkerClearAt = DateTime.now();
@ -515,7 +531,7 @@ class ContactInfoState extends ChangeNotifier {
String? _server; String? _server;
late bool _archived; late bool _archived;
ContactInfoState(this.profileOnion, this.onion, ContactInfoState(this.profileOnion, this.identifier, this.onion,
{nickname = "", {nickname = "",
isGroup = false, isGroup = false,
authorization = ContactAuthorization.unknown, authorization = ContactAuthorization.unknown,
@ -538,6 +554,7 @@ class ContactInfoState extends ChangeNotifier {
this._lastMessageTime = lastMessageTime == null ? DateTime.fromMillisecondsSinceEpoch(0) : lastMessageTime; this._lastMessageTime = lastMessageTime == null ? DateTime.fromMillisecondsSinceEpoch(0) : lastMessageTime;
this._server = server; this._server = server;
this._archived = archived; this._archived = archived;
this.messageCache = List.empty(growable: true);
keys = Map<String, GlobalKey<MessageRowState>>(); keys = Map<String, GlobalKey<MessageRowState>>();
} }
@ -593,7 +610,7 @@ class ContactInfoState extends ChangeNotifier {
if (newVal > 0) { if (newVal > 0) {
this._newMarker = newVal; this._newMarker = newVal;
} else { } else {
this._newMarkerClearAt = DateTime.now().add(const Duration(minutes:2)); this._newMarkerClearAt = DateTime.now().add(const Duration(minutes: 2));
} }
this._unreadMessages = newVal; this._unreadMessages = newVal;
notifyListeners(); notifyListeners();
@ -608,12 +625,13 @@ class ContactInfoState extends ChangeNotifier {
} }
return this._newMarker; return this._newMarker;
} }
// what's a getter that sometimes sets without a setter // what's a getter that sometimes sets without a setter
// that sometimes doesn't set // that sometimes doesn't set
set newMarker(int newVal) { set newMarker(int newVal) {
// only unreadMessages++ can set newMarker = 1; // only unreadMessages++ can set newMarker = 1;
// avoids drawing a marker when the convo is already open // avoids drawing a marker when the convo is already open
if (newVal > 1) { if (newVal >= 1) {
this._newMarker = newVal; this._newMarker = newVal;
notifyListeners(); notifyListeners();
} }
@ -649,11 +667,37 @@ class ContactInfoState extends ChangeNotifier {
} }
} }
GlobalKey<MessageRowState> getMessageKey(String index) { GlobalKey<MessageRowState> getMessageKey(int conversation, int message) {
String index = "c: " + conversation.toString() + " m:" + message.toString();
if (keys[index] == null) { if (keys[index] == null) {
keys[index] = GlobalKey<MessageRowState>(); keys[index] = GlobalKey<MessageRowState>();
} }
GlobalKey<MessageRowState> ret = keys[index]!; GlobalKey<MessageRowState> ret = keys[index]!;
return ret; return ret;
} }
GlobalKey<MessageRowState>? getMessageKeyOrFail(int conversation, int message) {
String index = "c: " + conversation.toString() + " m:" + message.toString();
if (keys[index] == null) {
return null;
}
GlobalKey<MessageRowState> ret = keys[index]!;
return ret;
}
void updateMessageCache(int conversation, int messageID, DateTime timestamp, String senderHandle, String senderImage, String data) {
this.messageCache.insert(0, MessageCache(MessageMetadata(profileOnion, conversation, messageID, timestamp, senderHandle, senderImage, "", {}, false, false), data));
this.totalMessages += 1;
}
void bumpMessageCache() {
this.messageCache.insert(0, null);
this.totalMessages += 1;
}
void ackCache(int messageID) {
this.messageCache.firstWhere((element) => element?.metadata.messageID == messageID)?.metadata.ackd = true;
notifyListeners();
}
} }

View File

@ -1,4 +1,5 @@
import 'dart:convert'; import 'dart:convert';
import 'package:cwtch/config.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -27,15 +28,54 @@ const GroupConversationHandleLength = 32;
abstract class Message { abstract class Message {
MessageMetadata getMetadata(); MessageMetadata getMetadata();
Widget getWidget(BuildContext context); Widget getWidget(BuildContext context, Key key);
Widget getPreviewWidget(BuildContext context); Widget getPreviewWidget(BuildContext context);
} }
Future<Message> messageHandler(BuildContext context, String profileOnion, String contactHandle, int index) { Message compileOverlay(MessageMetadata metadata, String messageData) {
try { try {
var rawMessageEnvelopeFuture = Provider.of<FlwtchState>(context, listen: false).cwtch.GetMessage(profileOnion, contactHandle, index); dynamic message = jsonDecode(messageData);
var content = message['d'] as dynamic;
var overlay = int.parse(message['o'].toString());
switch (overlay) {
case TextMessageOverlay:
return TextMessage(metadata, content);
case SuggestContactOverlay:
case InviteGroupOverlay:
return InviteMessage(overlay, metadata, content);
case QuotedMessageOverlay:
return QuotedMessage(metadata, content);
case FileShareOverlay:
return FileMessage(metadata, content);
default:
// Metadata is valid, content is not..
return MalformedMessage(metadata);
}
} catch (e) {
return MalformedMessage(metadata);
}
}
Future<Message> messageHandler(BuildContext context, String profileOnion, int conversationIdentifier, int index, {bool byID = false}) {
var cache = Provider.of<ProfileInfoState>(context).contactList.getContact(conversationIdentifier)?.messageCache;
if (cache != null && cache.length > index) {
if (cache[index] != null) {
return Future.value(compileOverlay(cache[index]!.metadata, cache[index]!.wrapper));
}
}
try {
Future<dynamic> rawMessageEnvelopeFuture;
if (byID) {
rawMessageEnvelopeFuture = Provider.of<FlwtchState>(context, listen: false).cwtch.GetMessageByID(profileOnion, conversationIdentifier, index);
} else {
rawMessageEnvelopeFuture = Provider.of<FlwtchState>(context, listen: false).cwtch.GetMessage(profileOnion, conversationIdentifier, index);
}
return rawMessageEnvelopeFuture.then((dynamic rawMessageEnvelope) { return rawMessageEnvelopeFuture.then((dynamic rawMessageEnvelope) {
var metadata = MessageMetadata(profileOnion, contactHandle, index, DateTime.now(), "", "", null, 0, false, true); var metadata = MessageMetadata(profileOnion, conversationIdentifier, index, DateTime.now(), "", "", "", <String, String>{}, false, true);
try { try {
dynamic messageWrapper = jsonDecode(rawMessageEnvelope); dynamic messageWrapper = jsonDecode(rawMessageEnvelope);
// There are 2 conditions in which this error condition can be met: // There are 2 conditions in which this error condition can be met:
@ -50,72 +90,48 @@ Future<Message> messageHandler(BuildContext context, String profileOnion, String
if (messageWrapper['Message'] == null || messageWrapper['Message'] == '' || messageWrapper['Message'] == '{}') { if (messageWrapper['Message'] == null || messageWrapper['Message'] == '' || messageWrapper['Message'] == '{}') {
return Future.delayed(Duration(seconds: 2), () { return Future.delayed(Duration(seconds: 2), () {
print("Tail recursive call to messageHandler called. This should be a rare event. If you see multiples of this log over a short period of time please log it as a bug."); print("Tail recursive call to messageHandler called. This should be a rare event. If you see multiples of this log over a short period of time please log it as a bug.");
return messageHandler(context, profileOnion, contactHandle, index).then((value) => value); return messageHandler(context, profileOnion, conversationIdentifier, -1, byID: byID).then((value) => value);
}); });
} }
// Construct the initial metadata // Construct the initial metadata
var messageID = messageWrapper['ID'];
var timestamp = DateTime.tryParse(messageWrapper['Timestamp'])!; var timestamp = DateTime.tryParse(messageWrapper['Timestamp'])!;
var senderHandle = messageWrapper['PeerID']; var senderHandle = messageWrapper['PeerID'];
var senderImage = messageWrapper['ContactImage']; var senderImage = messageWrapper['ContactImage'];
var flags = int.parse(messageWrapper['Flags'].toString()); var attributes = messageWrapper['Attributes'];
var ackd = messageWrapper['Acknowledged']; var ackd = messageWrapper['Acknowledged'];
var error = messageWrapper['Error'] != null; var error = messageWrapper['Error'] != null;
String? signature; var signature = messageWrapper['Signature'];
// If this is a group, store the signature metadata = MessageMetadata(profileOnion, conversationIdentifier, messageID, timestamp, senderHandle, senderImage, signature, attributes, ackd, error);
if (contactHandle.length == GroupConversationHandleLength) {
signature = messageWrapper['Signature'];
}
metadata = MessageMetadata(profileOnion, contactHandle, index, timestamp, senderHandle, senderImage, signature, flags, ackd, error);
dynamic message = jsonDecode(messageWrapper['Message']); return compileOverlay(metadata, messageWrapper['Message']);
var content = message['d'] as dynamic;
var overlay = int.parse(message['o'].toString());
switch (overlay) {
case TextMessageOverlay:
return TextMessage(metadata, content);
case SuggestContactOverlay:
case InviteGroupOverlay:
return InviteMessage(overlay, metadata, content);
case QuotedMessageOverlay:
return QuotedMessage(metadata, content);
case FileShareOverlay:
return FileMessage(metadata, content);
default:
// Metadata is valid, content is not..
return MalformedMessage(metadata);
}
} catch (e) { } catch (e) {
print("an error! " + e.toString()); EnvironmentConfig.debugLog("an error! " + e.toString());
return MalformedMessage(metadata); return MalformedMessage(metadata);
} }
}); });
} catch (e) { } catch (e) {
return Future.value(MalformedMessage(MessageMetadata(profileOnion, contactHandle, index, DateTime.now(), "", "", null, 0, false, true))); return Future.value(MalformedMessage(MessageMetadata(profileOnion, conversationIdentifier, -1, DateTime.now(), "", "", "", <String, String>{}, false, true)));
} }
} }
class MessageMetadata extends ChangeNotifier { class MessageMetadata extends ChangeNotifier {
// meta-metadata // meta-metadata
final String profileOnion; final String profileOnion;
final String contactHandle; final int conversationIdentifier;
final int messageIndex; final int messageID;
final DateTime timestamp; final DateTime timestamp;
final String senderHandle; final String senderHandle;
final String? senderImage; final String? senderImage;
int _flags; final dynamic _attributes;
bool _ackd; bool _ackd;
bool _error; bool _error;
final String? signature; final String? signature;
int get flags => this._flags; dynamic get attributes => this._attributes;
set flags(int newVal) {
this._flags = newVal;
notifyListeners();
}
bool get ackd => this._ackd; bool get ackd => this._ackd;
set ackd(bool newVal) { set ackd(bool newVal) {
@ -129,5 +145,5 @@ class MessageMetadata extends ChangeNotifier {
notifyListeners(); notifyListeners();
} }
MessageMetadata(this.profileOnion, this.contactHandle, this.messageIndex, this.timestamp, this.senderHandle, this.senderImage, this.signature, this._flags, this._ackd, this._error); MessageMetadata(this.profileOnion, this.conversationIdentifier, this.messageID, this.timestamp, this.senderHandle, this.senderImage, this.signature, this._attributes, this._ackd, this._error);
} }

View File

@ -17,11 +17,11 @@ class FileMessage extends Message {
FileMessage(this.metadata, this.content); FileMessage(this.metadata, this.content);
@override @override
Widget getWidget(BuildContext context) { Widget getWidget(BuildContext context, Key key) {
return ChangeNotifierProvider.value( return ChangeNotifierProvider.value(
key: key,
value: this.metadata, value: this.metadata,
builder: (bcontext, child) { builder: (bcontext, child) {
String idx = this.metadata.contactHandle + this.metadata.messageIndex.toString();
dynamic shareObj = jsonDecode(this.content); dynamic shareObj = jsonDecode(this.content);
if (shareObj == null) { if (shareObj == null) {
return MessageRow(MalformedBubble()); return MessageRow(MalformedBubble());
@ -35,7 +35,7 @@ class FileMessage extends Message {
return MessageRow(MalformedBubble()); return MessageRow(MalformedBubble());
} }
return MessageRow(FileBubble(nameSuggestion, rootHash, nonce, fileSize), key: Provider.of<ContactInfoState>(bcontext).getMessageKey(idx)); return MessageRow(FileBubble(nameSuggestion, rootHash, nonce, fileSize), key: key);
}); });
} }

View File

@ -17,18 +17,18 @@ class InviteMessage extends Message {
InviteMessage(this.overlay, this.metadata, this.content); InviteMessage(this.overlay, this.metadata, this.content);
@override @override
Widget getWidget(BuildContext context) { Widget getWidget(BuildContext context, Key key) {
return ChangeNotifierProvider.value( return ChangeNotifierProvider.value(
key: key,
value: this.metadata, value: this.metadata,
builder: (bcontext, child) { builder: (bcontext, child) {
String idx = this.metadata.contactHandle + this.metadata.messageIndex.toString();
String inviteTarget; String inviteTarget;
String inviteNick; String inviteNick;
String invite = this.content; String invite = this.content;
if (this.content.length == TorV3ContactHandleLength) { if (this.content.length == TorV3ContactHandleLength) {
inviteTarget = this.content; inviteTarget = this.content;
var targetContact = Provider.of<ProfileInfoState>(context).contactList.getContact(inviteTarget); var targetContact = Provider.of<ProfileInfoState>(context).contactList.findContact(inviteTarget);
inviteNick = targetContact == null ? this.content : targetContact.nickname; inviteNick = targetContact == null ? this.content : targetContact.nickname;
} else { } else {
var parts = this.content.toString().split("||"); var parts = this.content.toString().split("||");
@ -37,10 +37,10 @@ class InviteMessage extends Message {
inviteTarget = jsonObj['GroupID']; inviteTarget = jsonObj['GroupID'];
inviteNick = jsonObj['GroupName']; inviteNick = jsonObj['GroupName'];
} else { } else {
return MessageRow(MalformedBubble()); return MessageRow(MalformedBubble(), key: key);
} }
} }
return MessageRow(InvitationBubble(overlay, inviteTarget, inviteNick, invite), key: Provider.of<ContactInfoState>(bcontext).getMessageKey(idx)); return MessageRow(InvitationBubble(overlay, inviteTarget, inviteNick, invite), key: key);
}); });
} }
@ -54,7 +54,7 @@ class InviteMessage extends Message {
String invite = this.content; String invite = this.content;
if (this.content.length == TorV3ContactHandleLength) { if (this.content.length == TorV3ContactHandleLength) {
inviteTarget = this.content; inviteTarget = this.content;
var targetContact = Provider.of<ProfileInfoState>(context).contactList.getContact(inviteTarget); var targetContact = Provider.of<ProfileInfoState>(context).contactList.findContact(inviteTarget);
inviteNick = targetContact == null ? this.content : targetContact.nickname; inviteNick = targetContact == null ? this.content : targetContact.nickname;
} else { } else {
var parts = this.content.toString().split("||"); var parts = this.content.toString().split("||");

View File

@ -9,11 +9,11 @@ class MalformedMessage extends Message {
MalformedMessage(this.metadata); MalformedMessage(this.metadata);
@override @override
Widget getWidget(BuildContext context) { Widget getWidget(BuildContext context, Key key) {
return ChangeNotifierProvider.value( return ChangeNotifierProvider.value(
value: this.metadata, value: this.metadata,
builder: (context, child) { builder: (context, child) {
return MessageRow(MalformedBubble()); return MessageRow(MalformedBubble(), key: key);
}); });
} }

View File

@ -51,7 +51,7 @@ class QuotedMessage extends Message {
dynamic message = jsonDecode(this.content); dynamic message = jsonDecode(this.content);
return Text(message["body"]); return Text(message["body"]);
} catch (e) { } catch (e) {
return MalformedMessage(this.metadata).getWidget(context); return MalformedMessage(this.metadata).getWidget(context, Key("malformed"));
} }
}); });
} }
@ -62,16 +62,15 @@ class QuotedMessage extends Message {
} }
@override @override
Widget getWidget(BuildContext context) { Widget getWidget(BuildContext context, Key key) {
try { try {
dynamic message = jsonDecode(this.content); dynamic message = jsonDecode(this.content);
if (message["body"] == null || message["quotedHash"] == null) { if (message["body"] == null || message["quotedHash"] == null) {
return MalformedMessage(this.metadata).getWidget(context); return MalformedMessage(this.metadata).getWidget(context, key);
} }
var quotedMessagePotentials = Provider.of<FlwtchState>(context).cwtch.GetMessageByContentHash(metadata.profileOnion, metadata.contactHandle, message["quotedHash"]); var quotedMessagePotentials = Provider.of<FlwtchState>(context).cwtch.GetMessageByContentHash(metadata.profileOnion, metadata.conversationIdentifier, message["quotedHash"]);
int messageIndex = metadata.messageIndex;
Future<LocallyIndexedMessage?> quotedMessage = quotedMessagePotentials.then((matchingMessages) { Future<LocallyIndexedMessage?> quotedMessage = quotedMessagePotentials.then((matchingMessages) {
if (matchingMessages == "[]") { if (matchingMessages == "[]") {
return null; return null;
@ -81,9 +80,7 @@ class QuotedMessage extends Message {
// message // message
try { try {
var list = (jsonDecode(matchingMessages) as List<dynamic>).map((data) => LocallyIndexedMessage.fromJson(data)).toList(); var list = (jsonDecode(matchingMessages) as List<dynamic>).map((data) => LocallyIndexedMessage.fromJson(data)).toList();
LocallyIndexedMessage candidate = list.reversed.firstWhere((element) => messageIndex < element.index, orElse: () { LocallyIndexedMessage candidate = list.reversed.first;
return list.firstWhere((element) => messageIndex > element.index);
});
return candidate; return candidate;
} catch (e) { } catch (e) {
// Malformed Message will be returned... // Malformed Message will be returned...
@ -94,18 +91,17 @@ class QuotedMessage extends Message {
return ChangeNotifierProvider.value( return ChangeNotifierProvider.value(
value: this.metadata, value: this.metadata,
builder: (bcontext, child) { builder: (bcontext, child) {
String idx = this.metadata.contactHandle + this.metadata.messageIndex.toString();
return MessageRow( return MessageRow(
QuotedMessageBubble(message["body"], quotedMessage.then((LocallyIndexedMessage? localIndex) { QuotedMessageBubble(message["body"], quotedMessage.then((LocallyIndexedMessage? localIndex) {
if (localIndex != null) { if (localIndex != null) {
return messageHandler(context, metadata.profileOnion, metadata.contactHandle, localIndex.index); return messageHandler(context, metadata.profileOnion, metadata.conversationIdentifier, localIndex.index);
} }
return MalformedMessage(this.metadata); return MalformedMessage(this.metadata);
})), })),
key: Provider.of<ContactInfoState>(bcontext).getMessageKey(idx)); key: key);
}); });
} catch (e) { } catch (e) {
return MalformedMessage(this.metadata).getWidget(context); return MalformedMessage(this.metadata).getWidget(context, key);
} }
} }
} }

View File

@ -1,5 +1,8 @@
import 'package:cwtch/models/message.dart'; import 'package:cwtch/models/message.dart';
import 'package:cwtch/models/messages/malformedmessage.dart';
import 'package:cwtch/widgets/malformedbubble.dart';
import 'package:cwtch/widgets/messagebubble.dart'; import 'package:cwtch/widgets/messagebubble.dart';
import 'package:cwtch/widgets/messageloadingbubble.dart';
import 'package:cwtch/widgets/messagerow.dart'; import 'package:cwtch/widgets/messagerow.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
@ -28,12 +31,14 @@ class TextMessage extends Message {
} }
@override @override
Widget getWidget(BuildContext context) { Widget getWidget(BuildContext context, Key key) {
return ChangeNotifierProvider.value( return ChangeNotifierProvider.value(
value: this.metadata, value: this.metadata,
builder: (bcontext, child) { builder: (bcontext, child) {
String idx = this.metadata.contactHandle + this.metadata.messageIndex.toString(); return MessageRow(
return MessageRow(MessageBubble(this.content), key: Provider.of<ContactInfoState>(bcontext).getMessageKey(idx)); MessageBubble(this.content),
key: key,
);
}); });
} }
} }

View File

@ -24,12 +24,7 @@ class ServerListState extends ChangeNotifier {
if (idx >= 0) { if (idx >= 0) {
_servers[idx] = sis; _servers[idx] = sis;
} else { } else {
_servers.add(ServerInfoState(onion: onion, _servers.add(ServerInfoState(onion: onion, serverBundle: serverBundle, running: running, description: description, autoStart: autoStart, isEncrypted: isEncrypted));
serverBundle: serverBundle,
running: running,
description: description,
autoStart: autoStart,
isEncrypted: isEncrypted));
} }
notifyListeners(); notifyListeners();
} }
@ -37,7 +32,7 @@ class ServerListState extends ChangeNotifier {
void updateServer(String onion, String serverBundle, bool running, String description, bool autoStart, bool isEncrypted) { void updateServer(String onion, String serverBundle, bool running, String description, bool autoStart, bool isEncrypted) {
int idx = _servers.indexWhere((element) => element.onion == onion); int idx = _servers.indexWhere((element) => element.onion == onion);
if (idx >= 0) { if (idx >= 0) {
_servers[idx] = ServerInfoState(onion: onion, serverBundle: serverBundle, running: running, description: description, autoStart: autoStart, isEncrypted: isEncrypted); _servers[idx] = ServerInfoState(onion: onion, serverBundle: serverBundle, running: running, description: description, autoStart: autoStart, isEncrypted: isEncrypted);
} else { } else {
print("Tried to update server list without a starting state...this is probably an error"); print("Tried to update server list without a starting state...this is probably an error");
} }

View File

@ -11,6 +11,7 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
const TapirGroupsExperiment = "tapir-groups-experiment"; const TapirGroupsExperiment = "tapir-groups-experiment";
const ServerManagementExperiment = "servers-experiment"; const ServerManagementExperiment = "servers-experiment";
const FileSharingExperiment = "filesharing"; const FileSharingExperiment = "filesharing";
const ClickableLinksExperiment = "clickable-links";
enum DualpaneMode { enum DualpaneMode {
Single, Single,

View File

@ -296,11 +296,13 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
// Profile Editing // Profile Editing
if (ctrlrPass.value.text.isEmpty) { if (ctrlrPass.value.text.isEmpty) {
// Don't update password, only update name // Don't update password, only update name
Provider.of<ProfileInfoState>(context, listen: false).nickname = ctrlrNick.value.text;
Provider.of<FlwtchState>(context, listen: false).cwtch.SetProfileAttribute(Provider.of<ProfileInfoState>(context, listen: false).onion, "profile.name", ctrlrNick.value.text); Provider.of<FlwtchState>(context, listen: false).cwtch.SetProfileAttribute(Provider.of<ProfileInfoState>(context, listen: false).onion, "profile.name", ctrlrNick.value.text);
Navigator.of(context).pop(); Navigator.of(context).pop();
} else { } else {
// At this points passwords have been validated to be the same and not empty // At this points passwords have been validated to be the same and not empty
// Update both password and name, even if name hasn't been changed... // Update both password and name, even if name hasn't been changed...
Provider.of<ProfileInfoState>(context, listen: false).nickname = ctrlrNick.value.text;
Provider.of<FlwtchState>(context, listen: false).cwtch.SetProfileAttribute(Provider.of<ProfileInfoState>(context, listen: false).onion, "profile.name", ctrlrNick.value.text); Provider.of<FlwtchState>(context, listen: false).cwtch.SetProfileAttribute(Provider.of<ProfileInfoState>(context, listen: false).onion, "profile.name", ctrlrNick.value.text);
final updatePasswordEvent = { final updatePasswordEvent = {
"EventType": "ChangePassword", "EventType": "ChangePassword",

View File

@ -53,7 +53,6 @@ class _AddEditServerViewState extends State<AddEditServerView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: ctrlrOnion.text.isEmpty ? Text(AppLocalizations.of(context)!.addServerTitle) : Text(AppLocalizations.of(context)!.editServerTitle), title: ctrlrOnion.text.isEmpty ? Text(AppLocalizations.of(context)!.addServerTitle) : Text(AppLocalizations.of(context)!.editServerTitle),
@ -82,232 +81,222 @@ class _AddEditServerViewState extends State<AddEditServerView> {
child: Form( child: Form(
key: _formKey, key: _formKey,
child: Container( child: Container(
margin: EdgeInsets.fromLTRB(30, 0, 30, 10), margin: EdgeInsets.fromLTRB(30, 0, 30, 10),
padding: EdgeInsets.fromLTRB(20, 0 , 20, 10), padding: EdgeInsets.fromLTRB(20, 0, 20, 10),
child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.stretch, child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.stretch, children: [
children: [ // Onion
Visibility(
visible: serverInfoState.onion.isNotEmpty,
child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [
SizedBox(
height: 20,
),
CwtchLabel(label: AppLocalizations.of(context)!.serverAddress),
SizedBox(
height: 20,
),
SelectableText(serverInfoState.onion)
])),
// Onion // Description
Visibility( Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [
visible: serverInfoState.onion.isNotEmpty, SizedBox(
child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ height: 20,
SizedBox( ),
height: 20, CwtchLabel(label: AppLocalizations.of(context)!.serverDescriptionLabel),
), Text(AppLocalizations.of(context)!.serverDescriptionDescription),
CwtchLabel(label: AppLocalizations.of(context)!.serverAddress), SizedBox(
SizedBox( height: 20,
height: 20, ),
), CwtchTextField(
SelectableText( controller: ctrlrDesc,
serverInfoState.onion labelText: "Description",
) autofocus: false,
])), )
]),
// Description SizedBox(
Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ height: 20,
SizedBox( ),
height: 20,
),
CwtchLabel(label: AppLocalizations.of(context)!.serverDescriptionLabel),
Text(AppLocalizations.of(context)!.serverDescriptionDescription),
SizedBox(
height: 20,
),
CwtchTextField(
controller: ctrlrDesc,
labelText: "Description",
autofocus: false,
)
]),
SizedBox( // Enabled
height: 20, Visibility(
), visible: serverInfoState.onion.isNotEmpty,
child: SwitchListTile(
title: Text(AppLocalizations.of(context)!.serverEnabled, style: TextStyle(color: settings.current().mainTextColor())),
subtitle: Text(AppLocalizations.of(context)!.serverEnabledDescription),
value: serverInfoState.running,
onChanged: (bool value) {
serverInfoState.setRunning(value);
if (value) {
Provider.of<FlwtchState>(context, listen: false).cwtch.LaunchServer(serverInfoState.onion);
} else {
Provider.of<FlwtchState>(context, listen: false).cwtch.StopServer(serverInfoState.onion);
}
},
activeTrackColor: settings.theme.defaultButtonActiveColor(),
inactiveTrackColor: settings.theme.defaultButtonDisabledColor(),
secondary: Icon(CwtchIcons.negative_heart_24px, color: settings.current().mainTextColor()),
)),
// Enabled // Auto start
Visibility( SwitchListTile(
visible: serverInfoState.onion.isNotEmpty, title: Text(AppLocalizations.of(context)!.serverAutostartLabel, style: TextStyle(color: settings.current().mainTextColor())),
child: SwitchListTile( subtitle: Text(AppLocalizations.of(context)!.serverAutostartDescription),
title: Text(AppLocalizations.of(context)!.serverEnabled, style: TextStyle(color: settings.current().mainTextColor())), value: serverInfoState.autoStart,
subtitle: Text(AppLocalizations.of(context)!.serverEnabledDescription), onChanged: (bool value) {
value: serverInfoState.running, serverInfoState.setAutostart(value);
onChanged: (bool value) {
serverInfoState.setRunning(value);
if (value) {
Provider.of<FlwtchState>(context, listen: false).cwtch.LaunchServer(serverInfoState.onion);
} else {
Provider.of<FlwtchState>(context, listen: false).cwtch.StopServer(serverInfoState.onion);
}
},
activeTrackColor: settings.theme.defaultButtonActiveColor(),
inactiveTrackColor: settings.theme.defaultButtonDisabledColor(),
secondary: Icon(CwtchIcons.negative_heart_24px, color: settings.current().mainTextColor()),
)),
// Auto start if (!serverInfoState.onion.isEmpty) {
SwitchListTile( Provider.of<FlwtchState>(context, listen: false).cwtch.SetServerAttribute(serverInfoState.onion, "autostart", value ? "true" : "false");
title: Text(AppLocalizations.of(context)!.serverAutostartLabel, style: TextStyle(color: settings.current().mainTextColor())), }
subtitle: Text(AppLocalizations.of(context)!.serverAutostartDescription), },
value: serverInfoState.autoStart, activeTrackColor: settings.theme.defaultButtonActiveColor(),
onChanged: (bool value) { inactiveTrackColor: settings.theme.defaultButtonDisabledColor(),
serverInfoState.setAutostart(value); secondary: Icon(CwtchIcons.favorite_24dp, color: settings.current().mainTextColor()),
),
if (! serverInfoState.onion.isEmpty) { // ***** Password *****
Provider.of<FlwtchState>(context, listen: false).cwtch.SetServerAttribute(serverInfoState.onion, "autostart", value ? "true" : "false");
}
},
activeTrackColor: settings.theme.defaultButtonActiveColor(),
inactiveTrackColor: settings.theme.defaultButtonDisabledColor(),
secondary: Icon(CwtchIcons.favorite_24dp, color: settings.current().mainTextColor()),
),
// use password toggle
Visibility(
visible: serverInfoState.onion.isEmpty,
child: Column(mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[
SizedBox(
height: 20,
),
Checkbox(
value: usePassword,
fillColor: MaterialStateProperty.all(settings.current().defaultButtonColor()),
activeColor: settings.current().defaultButtonActiveColor(),
onChanged: _handleSwitchPassword,
),
Text(
AppLocalizations.of(context)!.radioUsePassword,
style: TextStyle(color: settings.current().mainTextColor()),
),
SizedBox(
height: 20,
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 24),
child: Text(
usePassword ? AppLocalizations.of(context)!.encryptedServerDescription : AppLocalizations.of(context)!.plainServerDescription,
textAlign: TextAlign.center,
)),
SizedBox(
height: 20,
),
])),
// ***** Password ***** // current password
Visibility(
visible: serverInfoState.onion.isNotEmpty && serverInfoState.isEncrypted,
child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[
CwtchLabel(label: AppLocalizations.of(context)!.currentPasswordLabel),
SizedBox(
height: 20,
),
CwtchPasswordField(
controller: ctrlrOldPass,
autoFillHints: [AutofillHints.newPassword],
validator: (value) {
// Password field can be empty when just updating the profile, not on creation
if (serverInfoState.isEncrypted && serverInfoState.onion.isEmpty && value.isEmpty && usePassword) {
return AppLocalizations.of(context)!.passwordErrorEmpty;
}
if (Provider.of<ErrorHandler>(context).deletedServerError == true) {
return AppLocalizations.of(context)!.enterCurrentPasswordForDeleteServer;
}
return null;
},
),
SizedBox(
height: 20,
),
])),
// use password toggle // new passwords 1 & 2
Visibility( Visibility(
visible: serverInfoState.onion.isEmpty, // Currently we don't support password change for servers so also gate this on Add server, when ready to support changing password remove the onion.isEmpty check
child: Column(mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ visible: serverInfoState.onion.isEmpty && usePassword,
SizedBox( child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [
height: 20, CwtchLabel(label: AppLocalizations.of(context)!.newPassword),
), SizedBox(
Checkbox( height: 20,
value: usePassword, ),
fillColor: MaterialStateProperty.all(settings.current().defaultButtonColor()), CwtchPasswordField(
activeColor: settings.current().defaultButtonActiveColor(), controller: ctrlrPass,
onChanged: _handleSwitchPassword, validator: (value) {
), // Password field can be empty when just updating the profile, not on creation
Text( if (serverInfoState.onion.isEmpty && value.isEmpty && usePassword) {
AppLocalizations.of(context)!.radioUsePassword, return AppLocalizations.of(context)!.passwordErrorEmpty;
style: TextStyle(color: settings.current().mainTextColor()), }
), if (value != ctrlrPass2.value.text) {
SizedBox( return AppLocalizations.of(context)!.passwordErrorMatch;
height: 20, }
), return null;
Padding( },
padding: EdgeInsets.symmetric(horizontal: 24), ),
child: Text( SizedBox(
usePassword ? AppLocalizations.of(context)!.encryptedServerDescription : AppLocalizations.of(context)!.plainServerDescription, height: 20,
textAlign: TextAlign.center, ),
)), CwtchLabel(label: AppLocalizations.of(context)!.password2Label),
SizedBox( SizedBox(
height: 20, height: 20,
), ),
])), CwtchPasswordField(
controller: ctrlrPass2,
validator: (value) {
// Password field can be empty when just updating the profile, not on creation
if (serverInfoState.onion.isEmpty && value.isEmpty && usePassword) {
return AppLocalizations.of(context)!.passwordErrorEmpty;
}
if (value != ctrlrPass.value.text) {
return AppLocalizations.of(context)!.passwordErrorMatch;
}
return null;
}),
]),
),
SizedBox(
height: 20,
),
// current password Row(
Visibility( mainAxisAlignment: MainAxisAlignment.center,
visible: serverInfoState.onion.isNotEmpty && serverInfoState.isEncrypted, children: [
child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ Expanded(
CwtchLabel(label: AppLocalizations.of(context)!.currentPasswordLabel), child: ElevatedButton(
SizedBox( onPressed: serverInfoState.onion.isEmpty ? _createPressed : _savePressed,
height: 20, child: Text(
), serverInfoState.onion.isEmpty ? AppLocalizations.of(context)!.addServerTitle : AppLocalizations.of(context)!.saveServerButton,
CwtchPasswordField( textAlign: TextAlign.center,
controller: ctrlrOldPass, ),
autoFillHints: [AutofillHints.newPassword], ),
validator: (value) { ),
// Password field can be empty when just updating the profile, not on creation ],
if (serverInfoState.isEncrypted && ),
serverInfoState.onion.isEmpty && Visibility(
value.isEmpty && visible: serverInfoState.onion.isNotEmpty,
usePassword) { child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.end, children: [
return AppLocalizations.of(context)!.passwordErrorEmpty; SizedBox(
} height: 20,
if (Provider.of<ErrorHandler>(context).deletedServerError == true) { ),
return AppLocalizations.of(context)!.enterCurrentPasswordForDeleteServer; Tooltip(
} message: AppLocalizations.of(context)!.enterCurrentPasswordForDeleteServer,
return null; child: ElevatedButton.icon(
}, onPressed: () {
), showAlertDialog(context);
SizedBox( },
height: 20, icon: Icon(Icons.delete_forever),
), label: Text(AppLocalizations.of(context)!.deleteBtn),
])), ))
]))
// new passwords 1 & 2 // ***** END Password *****
Visibility( ]))))));
// Currently we don't support password change for servers so also gate this on Add server, when ready to support changing password remove the onion.isEmpty check
visible: serverInfoState.onion.isEmpty && usePassword,
child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [
CwtchLabel(label: AppLocalizations.of(context)!.newPassword),
SizedBox(
height: 20,
),
CwtchPasswordField(
controller: ctrlrPass,
validator: (value) {
// Password field can be empty when just updating the profile, not on creation
if (serverInfoState.onion.isEmpty && value.isEmpty && usePassword) {
return AppLocalizations.of(context)!.passwordErrorEmpty;
}
if (value != ctrlrPass2.value.text) {
return AppLocalizations.of(context)!.passwordErrorMatch;
}
return null;
},
),
SizedBox(
height: 20,
),
CwtchLabel(label: AppLocalizations.of(context)!.password2Label),
SizedBox(
height: 20,
),
CwtchPasswordField(
controller: ctrlrPass2,
validator: (value) {
// Password field can be empty when just updating the profile, not on creation
if (serverInfoState.onion.isEmpty && value.isEmpty && usePassword) {
return AppLocalizations.of(context)!.passwordErrorEmpty;
}
if (value != ctrlrPass.value.text) {
return AppLocalizations.of(context)!.passwordErrorMatch;
}
return null;
}),
]),
),
SizedBox(
height: 20,
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded(
child: ElevatedButton(
onPressed: serverInfoState.onion.isEmpty ? _createPressed : _savePressed,
child: Text(
serverInfoState.onion.isEmpty ? AppLocalizations.of(context)!.addServerTitle : AppLocalizations.of(context)!.saveServerButton,
textAlign: TextAlign.center,
),
),
),
],
),
Visibility(
visible: serverInfoState.onion.isNotEmpty,
child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.end, children: [
SizedBox(
height: 20,
),
Tooltip(
message: AppLocalizations.of(context)!.enterCurrentPasswordForDeleteServer,
child: ElevatedButton.icon(
onPressed: () {
showAlertDialog(context);
},
icon: Icon(Icons.delete_forever),
label: Text(AppLocalizations.of(context)!.deleteBtn),
))
]))
// ***** END Password *****
]))))));
}); });
}); });
} }
@ -318,29 +307,20 @@ class _AddEditServerViewState extends State<AddEditServerView> {
// match (and are provided if the user has requested an encrypted profile). // match (and are provided if the user has requested an encrypted profile).
if (_formKey.currentState!.validate()) { if (_formKey.currentState!.validate()) {
if (usePassword) { if (usePassword) {
Provider Provider.of<FlwtchState>(context, listen: false).cwtch.CreateServer(ctrlrPass.value.text, ctrlrDesc.value.text, Provider.of<ServerInfoState>(context, listen: false).autoStart);
.of<FlwtchState>(context, listen: false)
.cwtch
.CreateServer(ctrlrPass.value.text, ctrlrDesc.value.text, Provider.of<ServerInfoState>(context, listen: false).autoStart);
} else { } else {
Provider Provider.of<FlwtchState>(context, listen: false).cwtch.CreateServer(DefaultPassword, ctrlrDesc.value.text, Provider.of<ServerInfoState>(context, listen: false).autoStart);
.of<FlwtchState>(context, listen: false)
.cwtch
.CreateServer(DefaultPassword, ctrlrDesc.value.text, Provider.of<ServerInfoState>(context, listen: false).autoStart);
} }
Navigator.of(context).pop(); Navigator.of(context).pop();
} }
} }
void _savePressed() { void _savePressed() {
var server = Provider.of<ServerInfoState>(context, listen: false); var server = Provider.of<ServerInfoState>(context, listen: false);
Provider.of<FlwtchState>(context, listen: false) Provider.of<FlwtchState>(context, listen: false).cwtch.SetServerAttribute(server.onion, "description", ctrlrDesc.text);
.cwtch.SetServerAttribute(server.onion, "description", ctrlrDesc.text);
server.setDescription(ctrlrDesc.text); server.setDescription(ctrlrDesc.text);
if (_formKey.currentState!.validate()) { if (_formKey.currentState!.validate()) {
// TODO support change password // TODO support change password
} }
@ -358,16 +338,11 @@ class _AddEditServerViewState extends State<AddEditServerView> {
Widget continueButton = ElevatedButton( Widget continueButton = ElevatedButton(
child: Text(AppLocalizations.of(context)!.deleteServerConfirmBtn), child: Text(AppLocalizations.of(context)!.deleteServerConfirmBtn),
onPressed: () { onPressed: () {
var onion = Provider var onion = Provider.of<ServerInfoState>(context, listen: false).onion;
.of<ServerInfoState>(context, listen: false) Provider.of<FlwtchState>(context, listen: false).cwtch.DeleteServer(onion, Provider.of<ServerInfoState>(context, listen: false).isEncrypted ? ctrlrOldPass.value.text : DefaultPassword);
.onion;
Provider
.of<FlwtchState>(context, listen: false)
.cwtch
.DeleteServer(onion, Provider.of<ServerInfoState>(context, listen: false).isEncrypted ? ctrlrOldPass.value.text : DefaultPassword);
Future.delayed( Future.delayed(
const Duration(milliseconds: 500), const Duration(milliseconds: 500),
() { () {
if (globalErrorHandler.deletedServerSuccess) { if (globalErrorHandler.deletedServerSuccess) {
final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.deleteServerSuccess + ":" + onion)); final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.deleteServerSuccess + ":" + onion));
ScaffoldMessenger.of(context).showSnackBar(snackBar); ScaffoldMessenger.of(context).showSnackBar(snackBar);
@ -395,4 +370,4 @@ class _AddEditServerViewState extends State<AddEditServerView> {
}, },
); );
} }
} }

View File

@ -22,7 +22,7 @@ class ContactsView extends StatefulWidget {
} }
// selectConversation can be called from anywhere to set the active conversation // selectConversation can be called from anywhere to set the active conversation
void selectConversation(BuildContext context, String handle) { void selectConversation(BuildContext context, int handle) {
// requery instead of using contactinfostate directly because sometimes listview gets confused about data that resorts // requery instead of using contactinfostate directly because sometimes listview gets confused about data that resorts
var initialIndex = Provider.of<ProfileInfoState>(context, listen: false).contactList.getContact(handle)!.unreadMessages; var initialIndex = Provider.of<ProfileInfoState>(context, listen: false).contactList.getContact(handle)!.unreadMessages;
Provider.of<ProfileInfoState>(context, listen: false).contactList.getContact(handle)!.unreadMessages = 0; Provider.of<ProfileInfoState>(context, listen: false).contactList.getContact(handle)!.unreadMessages = 0;
@ -36,7 +36,7 @@ void selectConversation(BuildContext context, String handle) {
if (Provider.of<Settings>(context, listen: false).uiColumns(isLandscape).length == 1) _pushMessageView(context, handle); if (Provider.of<Settings>(context, listen: false).uiColumns(isLandscape).length == 1) _pushMessageView(context, handle);
} }
void _pushMessageView(BuildContext context, String handle) { void _pushMessageView(BuildContext context, int handle) {
var profileOnion = Provider.of<ProfileInfoState>(context, listen: false).onion; var profileOnion = Provider.of<ProfileInfoState>(context, listen: false).onion;
Navigator.of(context).push( Navigator.of(context).push(
MaterialPageRoute<void>( MaterialPageRoute<void>(
@ -112,7 +112,6 @@ class _ContactsViewState extends State<ContactsView> {
Clipboard.setData(new ClipboardData(text: Provider.of<ProfileInfoState>(context, listen: false).onion)); Clipboard.setData(new ClipboardData(text: Provider.of<ProfileInfoState>(context, listen: false).onion));
})); }));
// TODO servers // TODO servers
// Search contacts // Search contacts

View File

@ -34,8 +34,8 @@ class _DoubleColumnViewState extends State<DoubleColumnView> {
MultiProvider(providers: [ MultiProvider(providers: [
ChangeNotifierProvider.value(value: Provider.of<ProfileInfoState>(context)), ChangeNotifierProvider.value(value: Provider.of<ProfileInfoState>(context)),
ChangeNotifierProvider.value( ChangeNotifierProvider.value(
value: flwtch.selectedConversation != null ? Provider.of<ProfileInfoState>(context).contactList.getContact(flwtch.selectedConversation!)! : ContactInfoState("", "")), value: flwtch.selectedConversation != null ? Provider.of<ProfileInfoState>(context).contactList.getContact(flwtch.selectedConversation!)! : ContactInfoState("", -1, "")),
], child: Container(key: Key(flwtch.selectedConversation??"never_this"), child: MessageView())), ], child: Container(key: Key(flwtch.selectedConversation!.toString()), child: MessageView())),
), ),
], ],
); );

View File

@ -191,9 +191,8 @@ class _GlobalSettingsViewState extends State<GlobalSettingsView> {
secondary: Icon(CwtchIcons.enable_groups, color: settings.current().mainTextColor()), secondary: Icon(CwtchIcons.enable_groups, color: settings.current().mainTextColor()),
), ),
Visibility( Visibility(
visible: !Platform.isAndroid && !Platform.isIOS, visible: !Platform.isAndroid && !Platform.isIOS,
child: child: SwitchListTile(
SwitchListTile(
title: Text(AppLocalizations.of(context)!.settingServers, style: TextStyle(color: settings.current().mainTextColor())), title: Text(AppLocalizations.of(context)!.settingServers, style: TextStyle(color: settings.current().mainTextColor())),
subtitle: Text(AppLocalizations.of(context)!.settingServersDescription), subtitle: Text(AppLocalizations.of(context)!.settingServersDescription),
value: settings.isExperimentEnabled(ServerManagementExperiment), value: settings.isExperimentEnabled(ServerManagementExperiment),
@ -227,6 +226,22 @@ class _GlobalSettingsViewState extends State<GlobalSettingsView> {
inactiveTrackColor: settings.theme.defaultButtonDisabledColor(), inactiveTrackColor: settings.theme.defaultButtonDisabledColor(),
secondary: Icon(Icons.attach_file, color: settings.current().mainTextColor()), secondary: Icon(Icons.attach_file, color: settings.current().mainTextColor()),
), ),
SwitchListTile(
title: Text("Enable Clickable Links", style: TextStyle(color: settings.current().mainTextColor())),
subtitle: Text("The clickable links experiment allows you to click on URLs shared in messages."),
value: settings.isExperimentEnabled(ClickableLinksExperiment),
onChanged: (bool value) {
if (value) {
settings.enableExperiment(ClickableLinksExperiment);
} else {
settings.disableExperiment(ClickableLinksExperiment);
}
saveSettings(context);
},
activeTrackColor: settings.theme.defaultButtonActiveColor(),
inactiveTrackColor: settings.theme.defaultButtonDisabledColor(),
secondary: Icon(Icons.link, color: settings.current().mainTextColor()),
),
], ],
)), )),
AboutListTile( AboutListTile(

View File

@ -78,9 +78,9 @@ class _GroupSettingsViewState extends State<GroupSettingsView> {
readonly: false, readonly: false,
onPressed: () { onPressed: () {
var profileOnion = Provider.of<ContactInfoState>(context, listen: false).profileOnion; var profileOnion = Provider.of<ContactInfoState>(context, listen: false).profileOnion;
var handle = Provider.of<ContactInfoState>(context, listen: false).onion; var handle = Provider.of<ContactInfoState>(context, listen: false).identifier;
Provider.of<ContactInfoState>(context, listen: false).nickname = ctrlrNick.text; Provider.of<ContactInfoState>(context, listen: false).nickname = ctrlrNick.text;
Provider.of<FlwtchState>(context, listen: false).cwtch.SetGroupAttribute(profileOnion, handle, "local.name", ctrlrNick.text); Provider.of<FlwtchState>(context, listen: false).cwtch.SetConversationAttribute(profileOnion, handle, "profile.name", ctrlrNick.text);
// todo translations // todo translations
final snackBar = SnackBar(content: Text("Group Nickname changed successfully")); final snackBar = SnackBar(content: Text("Group Nickname changed successfully"));
ScaffoldMessenger.of(context).showSnackBar(snackBar); ScaffoldMessenger.of(context).showSnackBar(snackBar);
@ -140,7 +140,7 @@ class _GroupSettingsViewState extends State<GroupSettingsView> {
child: ElevatedButton.icon( child: ElevatedButton.icon(
onPressed: () { onPressed: () {
var profileOnion = Provider.of<ContactInfoState>(context, listen: false).profileOnion; var profileOnion = Provider.of<ContactInfoState>(context, listen: false).profileOnion;
var handle = Provider.of<ContactInfoState>(context, listen: false).onion; var handle = Provider.of<ContactInfoState>(context, listen: false).identifier;
// locally update cache... // locally update cache...
Provider.of<ContactInfoState>(context, listen: false).isArchived = true; Provider.of<ContactInfoState>(context, listen: false).isArchived = true;
Provider.of<FlwtchState>(context, listen: false).cwtch.ArchiveConversation(profileOnion, handle); Provider.of<FlwtchState>(context, listen: false).cwtch.ArchiveConversation(profileOnion, handle);
@ -195,10 +195,11 @@ class _GroupSettingsViewState extends State<GroupSettingsView> {
child: Text(AppLocalizations.of(context)!.yesLeave), child: Text(AppLocalizations.of(context)!.yesLeave),
onPressed: () { onPressed: () {
var profileOnion = Provider.of<ContactInfoState>(context, listen: false).profileOnion; var profileOnion = Provider.of<ContactInfoState>(context, listen: false).profileOnion;
var handle = Provider.of<ContactInfoState>(context, listen: false).onion; var identifier = Provider.of<ContactInfoState>(context, listen: false).identifier;
// locally update cache... // locally update cache...
Provider.of<ContactInfoState>(context, listen: false).isArchived = true; Provider.of<ContactInfoState>(context, listen: false).isArchived = true;
Provider.of<FlwtchState>(context, listen: false).cwtch.DeleteContact(profileOnion, handle); Provider.of<ProfileInfoState>(context, listen: false).contactList.removeContact(identifier);
Provider.of<FlwtchState>(context, listen: false).cwtch.DeleteContact(profileOnion, identifier);
Future.delayed(Duration(milliseconds: 500), () { Future.delayed(Duration(milliseconds: 500), () {
Provider.of<AppState>(context, listen: false).selectedConversation = null; Provider.of<AppState>(context, listen: false).selectedConversation = null;
Navigator.of(context).popUntil((route) => route.settings.name == "conversations"); // dismiss dialog Navigator.of(context).popUntil((route) => route.settings.name == "conversations"); // dismiss dialog

View File

@ -1,6 +1,7 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:crypto/crypto.dart'; import 'package:crypto/crypto.dart';
import 'package:cwtch/config.dart';
import 'package:cwtch/cwtch_icons_icons.dart'; import 'package:cwtch/cwtch_icons_icons.dart';
import 'package:cwtch/models/message.dart'; import 'package:cwtch/models/message.dart';
import 'package:cwtch/models/messages/quotedmessage.dart'; import 'package:cwtch/models/messages/quotedmessage.dart';
@ -33,7 +34,7 @@ class MessageView extends StatefulWidget {
class _MessageViewState extends State<MessageView> { class _MessageViewState extends State<MessageView> {
final ctrlrCompose = TextEditingController(); final ctrlrCompose = TextEditingController();
final focusNode = FocusNode(); final focusNode = FocusNode();
String selectedContact = ""; int selectedContact = -1;
ItemPositionsListener scrollListener = ItemPositionsListener.create(); ItemPositionsListener scrollListener = ItemPositionsListener.create();
ItemScrollController scrollController = ItemScrollController(); ItemScrollController scrollController = ItemScrollController();
@ -41,10 +42,10 @@ class _MessageViewState extends State<MessageView> {
void initState() { void initState() {
scrollListener.itemPositions.addListener(() { scrollListener.itemPositions.addListener(() {
if (scrollListener.itemPositions.value.length != 0 && if (scrollListener.itemPositions.value.length != 0 &&
Provider.of<AppState>(context, listen: false).unreadMessagesBelow == true && Provider.of<AppState>(context, listen: false).unreadMessagesBelow == true &&
scrollListener.itemPositions.value.any((element) => element.index == 0)) { scrollListener.itemPositions.value.any((element) => element.index == 0)) {
Provider.of<AppState>(context, listen: false).initialScrollIndex = 0; Provider.of<AppState>(context, listen: false).initialScrollIndex = 0;
Provider.of<AppState>(context, listen: false).unreadMessagesBelow = false; Provider.of<AppState>(context, listen: false).unreadMessagesBelow = false;
} }
}); });
super.initState(); super.initState();
@ -168,7 +169,7 @@ class _MessageViewState extends State<MessageView> {
if (Provider.of<AppState>(context, listen: false).selectedConversation != null && Provider.of<AppState>(context, listen: false).selectedIndex != null) { if (Provider.of<AppState>(context, listen: false).selectedConversation != null && Provider.of<AppState>(context, listen: false).selectedIndex != null) {
Provider.of<FlwtchState>(context, listen: false) Provider.of<FlwtchState>(context, listen: false)
.cwtch .cwtch
.GetMessage(Provider.of<AppState>(context, listen: false).selectedProfile!, Provider.of<AppState>(context, listen: false).selectedConversation!, .GetMessageByID(Provider.of<AppState>(context, listen: false).selectedProfile!, Provider.of<AppState>(context, listen: false).selectedConversation!,
Provider.of<AppState>(context, listen: false).selectedIndex!) Provider.of<AppState>(context, listen: false).selectedIndex!)
.then((data) { .then((data) {
try { try {
@ -180,7 +181,7 @@ class _MessageViewState extends State<MessageView> {
ChatMessage cm = new ChatMessage(o: QuotedMessageOverlay, d: quotedMessage); ChatMessage cm = new ChatMessage(o: QuotedMessageOverlay, d: quotedMessage);
Provider.of<FlwtchState>(context, listen: false) Provider.of<FlwtchState>(context, listen: false)
.cwtch .cwtch
.SendMessage(Provider.of<ContactInfoState>(context, listen: false).profileOnion, Provider.of<ContactInfoState>(context, listen: false).onion, jsonEncode(cm)); .SendMessage(Provider.of<ContactInfoState>(context, listen: false).profileOnion, Provider.of<ContactInfoState>(context, listen: false).identifier, jsonEncode(cm));
} catch (e) {} } catch (e) {}
Provider.of<AppState>(context, listen: false).selectedIndex = null; Provider.of<AppState>(context, listen: false).selectedIndex = null;
_sendMessageHelper(); _sendMessageHelper();
@ -189,7 +190,7 @@ class _MessageViewState extends State<MessageView> {
ChatMessage cm = new ChatMessage(o: TextMessageOverlay, d: ctrlrCompose.value.text); ChatMessage cm = new ChatMessage(o: TextMessageOverlay, d: ctrlrCompose.value.text);
Provider.of<FlwtchState>(context, listen: false) Provider.of<FlwtchState>(context, listen: false)
.cwtch .cwtch
.SendMessage(Provider.of<ContactInfoState>(context, listen: false).profileOnion, Provider.of<ContactInfoState>(context, listen: false).onion, jsonEncode(cm)); .SendMessage(Provider.of<ContactInfoState>(context, listen: false).profileOnion, Provider.of<ContactInfoState>(context, listen: false).identifier, jsonEncode(cm));
_sendMessageHelper(); _sendMessageHelper();
} }
} }
@ -198,14 +199,14 @@ class _MessageViewState extends State<MessageView> {
void _sendInvitation([String? ignoredParam]) { void _sendInvitation([String? ignoredParam]) {
Provider.of<FlwtchState>(context, listen: false) Provider.of<FlwtchState>(context, listen: false)
.cwtch .cwtch
.SendInvitation(Provider.of<ContactInfoState>(context, listen: false).profileOnion, Provider.of<ContactInfoState>(context, listen: false).onion, this.selectedContact); .SendInvitation(Provider.of<ContactInfoState>(context, listen: false).profileOnion, Provider.of<ContactInfoState>(context, listen: false).identifier, this.selectedContact);
_sendMessageHelper(); _sendMessageHelper();
} }
void _sendFile(String filePath) { void _sendFile(String filePath) {
Provider.of<FlwtchState>(context, listen: false) Provider.of<FlwtchState>(context, listen: false)
.cwtch .cwtch
.ShareFile(Provider.of<ContactInfoState>(context, listen: false).profileOnion, Provider.of<ContactInfoState>(context, listen: false).onion, filePath); .ShareFile(Provider.of<ContactInfoState>(context, listen: false).profileOnion, Provider.of<ContactInfoState>(context, listen: false).identifier, filePath);
_sendMessageHelper(); _sendMessageHelper();
} }
@ -213,10 +214,10 @@ class _MessageViewState extends State<MessageView> {
ctrlrCompose.clear(); ctrlrCompose.clear();
focusNode.requestFocus(); focusNode.requestFocus();
Future.delayed(const Duration(milliseconds: 80), () { Future.delayed(const Duration(milliseconds: 80), () {
Provider.of<ContactInfoState>(context, listen: false).totalMessages++; Provider.of<ProfileInfoState>(context, listen: false).contactList.getContact(Provider.of<ContactInfoState>(context, listen: false).identifier)?.bumpMessageCache();
Provider.of<ContactInfoState>(context, listen: false).newMarker++; Provider.of<ContactInfoState>(context, listen: false).newMarker++;
// Resort the contact list... // Resort the contact list...
Provider.of<ProfileInfoState>(context, listen: false).contactList.updateLastMessageTime(Provider.of<ContactInfoState>(context, listen: false).onion, DateTime.now()); Provider.of<ProfileInfoState>(context, listen: false).contactList.updateLastMessageTime(Provider.of<ContactInfoState>(context, listen: false).identifier, DateTime.now());
}); });
} }
@ -268,7 +269,8 @@ class _MessageViewState extends State<MessageView> {
var children; var children;
if (Provider.of<AppState>(context).selectedConversation != null && Provider.of<AppState>(context).selectedIndex != null) { if (Provider.of<AppState>(context).selectedConversation != null && Provider.of<AppState>(context).selectedIndex != null) {
var quoted = FutureBuilder( var quoted = FutureBuilder(
future: messageHandler(context, Provider.of<AppState>(context).selectedProfile!, Provider.of<AppState>(context).selectedConversation!, Provider.of<AppState>(context).selectedIndex!), future:
messageHandler(context, Provider.of<AppState>(context).selectedProfile!, Provider.of<AppState>(context).selectedConversation!, Provider.of<AppState>(context).selectedIndex!, byID: true),
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.hasData) { if (snapshot.hasData) {
var message = snapshot.data! as Message; var message = snapshot.data! as Message;
@ -352,7 +354,7 @@ class _MessageViewState extends State<MessageView> {
return contact.onion != Provider.of<ContactInfoState>(context).onion; return contact.onion != Provider.of<ContactInfoState>(context).onion;
}, onChanged: (newVal) { }, onChanged: (newVal) {
setState(() { setState(() {
this.selectedContact = newVal; this.selectedContact = Provider.of<ProfileInfoState>(context).contactList.findContact(newVal)!.identifier;
}); });
})), })),
SizedBox( SizedBox(
@ -361,7 +363,7 @@ class _MessageViewState extends State<MessageView> {
ElevatedButton( ElevatedButton(
child: Text(AppLocalizations.of(bcontext)!.inviteBtn, semanticsLabel: AppLocalizations.of(bcontext)!.inviteBtn), child: Text(AppLocalizations.of(bcontext)!.inviteBtn, semanticsLabel: AppLocalizations.of(bcontext)!.inviteBtn),
onPressed: () { onPressed: () {
if (this.selectedContact != "") { if (this.selectedContact != -1) {
this._sendInvitation(); this._sendInvitation();
} }
Navigator.pop(bcontext); Navigator.pop(bcontext);

View File

@ -69,14 +69,9 @@ class _PeerSettingsViewState extends State<PeerSettingsView> {
readonly: false, readonly: false,
onPressed: () { onPressed: () {
var profileOnion = Provider.of<ContactInfoState>(context, listen: false).profileOnion; var profileOnion = Provider.of<ContactInfoState>(context, listen: false).profileOnion;
var onion = Provider.of<ContactInfoState>(context, listen: false).onion; var conversation = Provider.of<ContactInfoState>(context, listen: false).identifier;
Provider.of<ContactInfoState>(context, listen: false).nickname = ctrlrNick.text; Provider.of<ContactInfoState>(context, listen: false).nickname = ctrlrNick.text;
final setPeerAttribute = { Provider.of<FlwtchState>(context, listen: false).cwtch.SetConversationAttribute(profileOnion, conversation, "profile.name", ctrlrNick.text);
"EventType": "SetPeerAttribute",
"Data": {"RemotePeer": onion, "Key": "local.name", "Data": ctrlrNick.text},
};
final setPeerAttributeJson = jsonEncode(setPeerAttribute);
Provider.of<FlwtchState>(context, listen: false).cwtch.SendProfileEvent(profileOnion, setPeerAttributeJson);
final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.nickChangeSuccess)); final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.nickChangeSuccess));
ScaffoldMessenger.of(context).showSnackBar(snackBar); ScaffoldMessenger.of(context).showSnackBar(snackBar);
}, },
@ -200,7 +195,7 @@ class _PeerSettingsViewState extends State<PeerSettingsView> {
child: ElevatedButton.icon( child: ElevatedButton.icon(
onPressed: () { onPressed: () {
var profileOnion = Provider.of<ContactInfoState>(context, listen: false).profileOnion; var profileOnion = Provider.of<ContactInfoState>(context, listen: false).profileOnion;
var handle = Provider.of<ContactInfoState>(context, listen: false).onion; var handle = Provider.of<ContactInfoState>(context, listen: false).identifier;
// locally update cache... // locally update cache...
Provider.of<ContactInfoState>(context, listen: false).isArchived = true; Provider.of<ContactInfoState>(context, listen: false).isArchived = true;
Provider.of<FlwtchState>(context, listen: false).cwtch.ArchiveConversation(profileOnion, handle); Provider.of<FlwtchState>(context, listen: false).cwtch.ArchiveConversation(profileOnion, handle);
@ -239,7 +234,7 @@ class _PeerSettingsViewState extends State<PeerSettingsView> {
child: Text(AppLocalizations.of(context)!.yesLeave), child: Text(AppLocalizations.of(context)!.yesLeave),
onPressed: () { onPressed: () {
var profileOnion = Provider.of<ContactInfoState>(context, listen: false).profileOnion; var profileOnion = Provider.of<ContactInfoState>(context, listen: false).profileOnion;
var handle = Provider.of<ContactInfoState>(context, listen: false).onion; var handle = Provider.of<ContactInfoState>(context, listen: false).identifier;
// locally update cache... // locally update cache...
Provider.of<ContactInfoState>(context, listen: false).isArchived = true; Provider.of<ContactInfoState>(context, listen: false).isArchived = true;
Provider.of<FlwtchState>(context, listen: false).cwtch.DeleteContact(profileOnion, handle); Provider.of<FlwtchState>(context, listen: false).cwtch.DeleteContact(profileOnion, handle);

View File

@ -57,9 +57,9 @@ class _ProfileMgrViewState extends State<ProfileMgrView> {
SizedBox( SizedBox(
width: 10, width: 10,
), ),
Expanded(child: Text(MediaQuery.of(context).size.width > 600 ? Expanded(
AppLocalizations.of(context)!.titleManageProfiles : AppLocalizations.of(context)!.titleManageProfilesShort, child: Text(MediaQuery.of(context).size.width > 600 ? AppLocalizations.of(context)!.titleManageProfiles : AppLocalizations.of(context)!.titleManageProfilesShort,
style: TextStyle(color: settings.current().mainTextColor()))) style: TextStyle(color: settings.current().mainTextColor())))
]), ]),
actions: getActions(), actions: getActions(),
), ),
@ -238,7 +238,7 @@ class _ProfileMgrViewState extends State<ProfileMgrView> {
if (tiles.isEmpty) { if (tiles.isEmpty) {
return Center( return Center(
child: Text( child: Text(
AppLocalizations.of(context)!.unlockProfileTip, AppLocalizations.of(context)!.unlockProfileTip,
textAlign: TextAlign.center, textAlign: TextAlign.center,
)); ));
} }

View File

@ -30,44 +30,45 @@ class _ServersView extends State<ServersView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text( MediaQuery.of(context).size.width > 600 ? AppLocalizations.of(context)!.serversManagerTitleLong : AppLocalizations.of(context)!.serversManagerTitleShort), title: Text(MediaQuery.of(context).size.width > 600 ? AppLocalizations.of(context)!.serversManagerTitleLong : AppLocalizations.of(context)!.serversManagerTitleShort),
actions: getActions(), actions: getActions(),
),
floatingActionButton: FloatingActionButton(
onPressed: _pushAddServer,
tooltip: AppLocalizations.of(context)!.addServerTooltip,
child: Icon(
Icons.add,
semanticLabel: AppLocalizations.of(context)!.addServerTooltip,
), ),
), floatingActionButton: FloatingActionButton(
body: Consumer<ServerListState>( onPressed: _pushAddServer,
builder: (context, svrs, child) { tooltip: AppLocalizations.of(context)!.addServerTooltip,
final tiles = svrs.servers.map((ServerInfoState server) { child: Icon(
return ChangeNotifierProvider<ServerInfoState>.value( Icons.add,
value: server, semanticLabel: AppLocalizations.of(context)!.addServerTooltip,
builder: (context, child) => RepaintBoundary(child: ServerRow()), ),
),
body: Consumer<ServerListState>(
builder: (context, svrs, child) {
final tiles = svrs.servers.map(
(ServerInfoState server) {
return ChangeNotifierProvider<ServerInfoState>.value(
value: server,
builder: (context, child) => RepaintBoundary(child: ServerRow()),
);
},
); );
final divided = ListTile.divideTiles(
context: context,
tiles: tiles,
).toList();
if (tiles.isEmpty) {
return Center(
child: Text(
AppLocalizations.of(context)!.unlockServerTip,
textAlign: TextAlign.center,
));
}
return ListView(children: divided);
}, },
); ));
final divided = ListTile.divideTiles(
context: context,
tiles: tiles,
).toList();
if (tiles.isEmpty) {
return Center(
child: Text(
AppLocalizations.of(context)!.unlockServerTip,
textAlign: TextAlign.center,
));
}
return ListView(children: divided);
},
));
} }
List<Widget> getActions() { List<Widget> getActions() {
@ -93,41 +94,41 @@ class _ServersView extends State<ServersView> {
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: 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),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: <Widget>[ children: <Widget>[
Text(AppLocalizations.of(context)!.enterServerPassword), Text(AppLocalizations.of(context)!.enterServerPassword),
SizedBox( SizedBox(
height: 20, height: 20,
), ),
CwtchPasswordField( CwtchPasswordField(
autofocus: true, autofocus: true,
controller: ctrlrPassword, controller: ctrlrPassword,
action: unlock, action: unlock,
validator: (value) {}, validator: (value) {},
), ),
SizedBox( SizedBox(
height: 20, height: 20,
), ),
Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [
Spacer(), Spacer(),
Expanded( Expanded(
child: ElevatedButton( child: ElevatedButton(
child: Text(AppLocalizations.of(context)!.unlock, semanticsLabel: AppLocalizations.of(context)!.unlock), child: Text(AppLocalizations.of(context)!.unlock, semanticsLabel: AppLocalizations.of(context)!.unlock),
onPressed: () { onPressed: () {
unlock(ctrlrPassword.value.text); unlock(ctrlrPassword.value.text);
}, },
)), )),
Spacer() Spacer()
]), ]),
], ],
))), ))),
))); )));
}); });
} }
@ -141,9 +142,11 @@ class _ServersView extends State<ServersView> {
Navigator.of(context).push(MaterialPageRoute<void>( Navigator.of(context).push(MaterialPageRoute<void>(
builder: (BuildContext context) { builder: (BuildContext context) {
return MultiProvider( return MultiProvider(
providers: [ChangeNotifierProvider<ServerInfoState>( providers: [
create: (_) => ServerInfoState(onion: "", serverBundle: "", description: "", autoStart: true, running: false, isEncrypted: true), ChangeNotifierProvider<ServerInfoState>(
)], create: (_) => ServerInfoState(onion: "", serverBundle: "", description: "", autoStart: true, running: false, isEncrypted: true),
)
],
child: AddEditServerView(), child: AddEditServerView(),
); );
}, },

View File

@ -105,24 +105,26 @@ class _ContactRowState extends State<ContactRow> {
), ),
]), ]),
onTap: () { onTap: () {
selectConversation(context, contact.onion); selectConversation(context, contact.identifier);
}, },
)); ));
} }
void _btnApprove() { void _btnApprove() {
// Update the UI
Provider.of<ContactInfoState>(context, listen: false).authorization = ContactAuthorization.approved;
Provider.of<FlwtchState>(context, listen: false) Provider.of<FlwtchState>(context, listen: false)
.cwtch .cwtch
.AcceptContact(Provider.of<ContactInfoState>(context, listen: false).profileOnion, Provider.of<ContactInfoState>(context, listen: false).onion); .AcceptContact(Provider.of<ContactInfoState>(context, listen: false).profileOnion, Provider.of<ContactInfoState>(context, listen: false).identifier);
} }
void _btnReject() { void _btnReject() {
ContactInfoState contact = Provider.of<ContactInfoState>(context, listen: false); ContactInfoState contact = Provider.of<ContactInfoState>(context, listen: false);
if (contact.isGroup == true) { if (contact.isGroup == true) {
Provider.of<FlwtchState>(context, listen: false).cwtch.RejectInvite(Provider.of<ContactInfoState>(context, listen: false).profileOnion, contact.onion); // FIXME This flow is incrorect. Groups never just show up on the contact list anymore
Provider.of<ProfileInfoState>(context, listen: false).removeContact(contact.onion); Provider.of<ProfileInfoState>(context, listen: false).removeContact(contact.onion);
} else { } else {
Provider.of<FlwtchState>(context, listen: false).cwtch.BlockContact(Provider.of<ContactInfoState>(context, listen: false).profileOnion, contact.onion); Provider.of<FlwtchState>(context, listen: false).cwtch.BlockContact(Provider.of<ContactInfoState>(context, listen: false).profileOnion, contact.identifier);
} }
} }

View File

@ -1,5 +1,6 @@
import 'dart:io'; import 'dart:io';
import 'package:cwtch/config.dart';
import 'package:cwtch/models/message.dart'; import 'package:cwtch/models/message.dart';
import 'package:file_picker_desktop/file_picker_desktop.dart'; import 'package:file_picker_desktop/file_picker_desktop.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
@ -41,7 +42,7 @@ class FileBubbleState extends State<FileBubble> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var fromMe = Provider.of<MessageMetadata>(context).senderHandle == Provider.of<ProfileInfoState>(context).onion; var fromMe = Provider.of<MessageMetadata>(context).senderHandle == Provider.of<ProfileInfoState>(context).onion;
var flagStarted = Provider.of<MessageMetadata>(context).flags & 0x02 > 0; var flagStarted = Provider.of<MessageMetadata>(context).attributes["file-downloaded"] == "true";
var borderRadiousEh = 15.0; var borderRadiousEh = 15.0;
var showFileSharing = Provider.of<Settings>(context).isExperimentEnabled(FileSharingExperiment); var showFileSharing = Provider.of<Settings>(context).isExperimentEnabled(FileSharingExperiment);
var prettyDate = DateFormat.yMd(Platform.localeName).add_jm().format(Provider.of<MessageMetadata>(context).timestamp); var prettyDate = DateFormat.yMd(Platform.localeName).add_jm().format(Provider.of<MessageMetadata>(context).timestamp);
@ -49,7 +50,7 @@ class FileBubbleState extends State<FileBubble> {
// If the sender is not us, then we want to give them a nickname... // If the sender is not us, then we want to give them a nickname...
var senderDisplayStr = ""; var senderDisplayStr = "";
if (!fromMe) { if (!fromMe) {
ContactInfoState? contact = Provider.of<ProfileInfoState>(context).contactList.getContact(Provider.of<MessageMetadata>(context).senderHandle); ContactInfoState? contact = Provider.of<ProfileInfoState>(context).contactList.findContact(Provider.of<MessageMetadata>(context).senderHandle);
if (contact != null) { if (contact != null) {
senderDisplayStr = contact.nickname; senderDisplayStr = contact.nickname;
} else { } else {
@ -89,15 +90,15 @@ class FileBubbleState extends State<FileBubble> {
} else if (flagStarted) { } else if (flagStarted) {
// in this case, the download was done in a previous application launch, // in this case, the download was done in a previous application launch,
// so we probably have to request an info lookup // so we probably have to request an info lookup
if (!Provider.of<ProfileInfoState>(context).downloadInterrupted(widget.fileKey()) ) { if (!Provider.of<ProfileInfoState>(context).downloadInterrupted(widget.fileKey())) {
wdgDecorations = Text(AppLocalizations.of(context)!.fileCheckingStatus + '...' + '\u202F'); wdgDecorations = Text(AppLocalizations.of(context)!.fileCheckingStatus + '...' + '\u202F');
Provider.of<FlwtchState>(context, listen: false).cwtch.CheckDownloadStatus(Provider.of<ProfileInfoState>(context, listen: false).onion, widget.fileKey()); Provider.of<FlwtchState>(context, listen: false).cwtch.CheckDownloadStatus(Provider.of<ProfileInfoState>(context, listen: false).onion, widget.fileKey());
} else { } else {
var path = Provider.of<ProfileInfoState>(context).downloadFinalPath(widget.fileKey()) ?? ""; var path = Provider.of<ProfileInfoState>(context).downloadFinalPath(widget.fileKey()) ?? "";
wdgDecorations = Column( wdgDecorations = Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
crossAxisAlignment: CrossAxisAlignment.start, Text(AppLocalizations.of(context)!.fileInterrupted + ': ' + path + '\u202F'),
children:[Text(AppLocalizations.of(context)!.fileInterrupted + ': ' + path + '\u202F'),ElevatedButton(onPressed: _btnResume, child: Text(AppLocalizations.of(context)!.verfiyResumeButton))] ElevatedButton(onPressed: _btnResume, child: Text(AppLocalizations.of(context)!.verfiyResumeButton))
); ]);
} }
} else { } else {
wdgDecorations = Center( wdgDecorations = Center(
@ -146,15 +147,17 @@ class FileBubbleState extends State<FileBubble> {
String? selectedFileName; String? selectedFileName;
File? file; File? file;
var profileOnion = Provider.of<ProfileInfoState>(context, listen: false).onion; var profileOnion = Provider.of<ProfileInfoState>(context, listen: false).onion;
var handle = Provider.of<MessageMetadata>(context, listen: false).senderHandle; var conversation = Provider.of<ContactInfoState>(context, listen: false).identifier;
var contact = Provider.of<ContactInfoState>(context, listen: false).onion; var idx = Provider.of<MessageMetadata>(context, listen: false).messageID;
var idx = Provider.of<MessageMetadata>(context, listen: false).messageIndex;
if (Platform.isAndroid) { if (Platform.isAndroid) {
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.UpdateMessageFlags(profileOnion, contact, idx, Provider.of<MessageMetadata>(context, listen: false).flags | 0x02); Provider.of<FlwtchState>(context, listen: false).cwtch.SetMessageAttribute(profileOnion, conversation, 0, idx, "file-downloaded", "true");
Provider.of<MessageMetadata>(context, listen: false).flags |= 0x02; //Provider.of<MessageMetadata>(context, listen: false).attributes |= 0x02;
Provider.of<FlwtchState>(context, listen: false).cwtch.CreateDownloadableFile(profileOnion, handle, widget.nameSuggestion, widget.fileKey()); ContactInfoState? contact = Provider.of<ProfileInfoState>(context).contactList.findContact(Provider.of<MessageMetadata>(context).senderHandle);
if (contact != null) {
Provider.of<FlwtchState>(context, listen: false).cwtch.CreateDownloadableFile(profileOnion, contact.identifier, widget.nameSuggestion, widget.fileKey());
}
} else { } else {
try { try {
selectedFileName = await saveFile( selectedFileName = await saveFile(
@ -162,12 +165,15 @@ class FileBubbleState extends State<FileBubble> {
); );
if (selectedFileName != null) { if (selectedFileName != null) {
file = File(selectedFileName); file = File(selectedFileName);
print("saving to " + file.path); EnvironmentConfig.debugLog("saving to " + file.path);
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.UpdateMessageFlags(profileOnion, contact, idx, Provider.of<MessageMetadata>(context, listen: false).flags | 0x02); Provider.of<FlwtchState>(context, listen: false).cwtch.SetMessageAttribute(profileOnion, conversation, 0, idx, "file-downloaded", "true");
Provider.of<MessageMetadata>(context, listen: false).flags |= 0x02; //Provider.of<MessageMetadata>(context, listen: false).flags |= 0x02;
Provider.of<FlwtchState>(context, listen: false).cwtch.DownloadFile(profileOnion, handle, file.path, manifestPath, widget.fileKey()); ContactInfoState? contact = Provider.of<ProfileInfoState>(context).contactList.findContact(Provider.of<MessageMetadata>(context).senderHandle);
if (contact != null) {
Provider.of<FlwtchState>(context, listen: false).cwtch.DownloadFile(profileOnion, contact.identifier, file.path, manifestPath, widget.fileKey());
}
} }
} catch (e) { } catch (e) {
print(e); print(e);
@ -177,7 +183,7 @@ class FileBubbleState extends State<FileBubble> {
void _btnResume() async { void _btnResume() async {
var profileOnion = Provider.of<ProfileInfoState>(context, listen: false).onion; var profileOnion = Provider.of<ProfileInfoState>(context, listen: false).onion;
var handle = Provider.of<MessageMetadata>(context, listen: false).senderHandle; var handle = Provider.of<MessageMetadata>(context, listen: false).conversationIdentifier;
Provider.of<ProfileInfoState>(context, listen: false).downloadMarkResumed(widget.fileKey()); Provider.of<ProfileInfoState>(context, listen: false).downloadMarkResumed(widget.fileKey());
Provider.of<FlwtchState>(context, listen: false).cwtch.VerifyOrResumeDownload(profileOnion, handle, widget.fileKey()); Provider.of<FlwtchState>(context, listen: false).cwtch.VerifyOrResumeDownload(profileOnion, handle, widget.fileKey());
} }

View File

@ -36,16 +36,16 @@ class InvitationBubbleState extends State<InvitationBubble> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
var fromMe = Provider.of<MessageMetadata>(context).senderHandle == Provider.of<ProfileInfoState>(context).onion; var fromMe = Provider.of<MessageMetadata>(context).senderHandle == Provider.of<ProfileInfoState>(context).onion;
var isGroup = widget.overlay == InviteGroupOverlay; var isGroup = widget.overlay == InviteGroupOverlay;
isAccepted = Provider.of<ProfileInfoState>(context).contactList.getContact(widget.inviteTarget) != null; isAccepted = Provider.of<ProfileInfoState>(context).contactList.findContact(widget.inviteTarget) != null;
var borderRadiousEh = 15.0; var borderRadiousEh = 15.0;
var showGroupInvite = Provider.of<Settings>(context).isExperimentEnabled(TapirGroupsExperiment); var showGroupInvite = Provider.of<Settings>(context).isExperimentEnabled(TapirGroupsExperiment);
rejected = Provider.of<MessageMetadata>(context).flags & 0x01 == 0x01; rejected = Provider.of<MessageMetadata>(context).attributes["rejected-invite"] == "true";
var prettyDate = DateFormat.yMd(Platform.localeName).add_jm().format(Provider.of<MessageMetadata>(context).timestamp); var prettyDate = DateFormat.yMd(Platform.localeName).add_jm().format(Provider.of<MessageMetadata>(context).timestamp);
// If the sender is not us, then we want to give them a nickname... // If the sender is not us, then we want to give them a nickname...
var senderDisplayStr = ""; var senderDisplayStr = "";
if (!fromMe) { if (!fromMe) {
ContactInfoState? contact = Provider.of<ProfileInfoState>(context).contactList.getContact(Provider.of<MessageMetadata>(context).senderHandle); ContactInfoState? contact = Provider.of<ProfileInfoState>(context).contactList.findContact(Provider.of<MessageMetadata>(context).senderHandle);
if (contact != null) { if (contact != null) {
senderDisplayStr = contact.nickname; senderDisplayStr = contact.nickname;
} else { } else {
@ -69,7 +69,7 @@ class InvitationBubbleState extends State<InvitationBubble> {
? Text(AppLocalizations.of(context)!.groupInviteSettingsWarning) ? Text(AppLocalizations.of(context)!.groupInviteSettingsWarning)
: fromMe : fromMe
? senderInviteChrome( ? senderInviteChrome(
AppLocalizations.of(context)!.sendAnInvitation, isGroup ? Provider.of<ProfileInfoState>(context).contactList.getContact(widget.inviteTarget)!.nickname : widget.inviteTarget) AppLocalizations.of(context)!.sendAnInvitation, isGroup ? Provider.of<ProfileInfoState>(context).contactList.findContact(widget.inviteTarget)!.nickname : widget.inviteTarget)
: (inviteChrome(isGroup ? AppLocalizations.of(context)!.inviteToGroup : AppLocalizations.of(context)!.contactSuggestion, widget.inviteNick, widget.inviteTarget)); : (inviteChrome(isGroup ? AppLocalizations.of(context)!.inviteToGroup : AppLocalizations.of(context)!.contactSuggestion, widget.inviteNick, widget.inviteTarget));
Widget wdgDecorations; Widget wdgDecorations;
@ -128,10 +128,10 @@ class InvitationBubbleState extends State<InvitationBubble> {
void _btnReject() { void _btnReject() {
setState(() { setState(() {
var profileOnion = Provider.of<ProfileInfoState>(context, listen: false).onion; var profileOnion = Provider.of<ProfileInfoState>(context, listen: false).onion;
var contact = Provider.of<ContactInfoState>(context, listen: false).onion; var conversation = Provider.of<ContactInfoState>(context, listen: false).identifier;
var idx = Provider.of<MessageMetadata>(context, listen: false).messageIndex; var idx = Provider.of<MessageMetadata>(context, listen: false).messageID;
Provider.of<FlwtchState>(context, listen: false).cwtch.UpdateMessageFlags(profileOnion, contact, idx, Provider.of<MessageMetadata>(context, listen: false).flags | 0x01); Provider.of<FlwtchState>(context, listen: false).cwtch.SetMessageAttribute(profileOnion, conversation, 0, idx, "rejected-invite", "true");
Provider.of<MessageMetadata>(context, listen: false).flags |= 0x01; //Provider.of<MessageMetadata>(context, listen: false).flags |= 0x01;
}); });
} }

View File

@ -3,9 +3,13 @@ import 'dart:io';
import 'package:cwtch/models/message.dart'; import 'package:cwtch/models/message.dart';
import 'package:cwtch/widgets/malformedbubble.dart'; import 'package:cwtch/widgets/malformedbubble.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../model.dart'; import '../model.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:flutter_linkify/flutter_linkify.dart';
import 'package:url_launcher/url_launcher.dart';
import '../settings.dart'; import '../settings.dart';
import 'messagebubbledecorations.dart'; import 'messagebubbledecorations.dart';
@ -28,6 +32,7 @@ class MessageBubbleState extends State<MessageBubble> {
var prettyDate = ""; var prettyDate = "";
var borderRadiousEh = 15.0; var borderRadiousEh = 15.0;
// var myKey = Provider.of<MessageState>(context).profileOnion + "::" + Provider.of<MessageState>(context).contactHandle + "::" + Provider.of<MessageState>(context).messageIndex.toString(); // var myKey = Provider.of<MessageState>(context).profileOnion + "::" + Provider.of<MessageState>(context).contactHandle + "::" + Provider.of<MessageState>(context).messageIndex.toString();
var showClickableLinks = Provider.of<Settings>(context).isExperimentEnabled(ClickableLinksExperiment);
DateTime messageDate = Provider.of<MessageMetadata>(context).timestamp; DateTime messageDate = Provider.of<MessageMetadata>(context).timestamp;
prettyDate = DateFormat.yMd(Platform.localeName).add_jm().format(messageDate.toLocal()); prettyDate = DateFormat.yMd(Platform.localeName).add_jm().format(messageDate.toLocal());
@ -35,7 +40,7 @@ class MessageBubbleState extends State<MessageBubble> {
// If the sender is not us, then we want to give them a nickname... // If the sender is not us, then we want to give them a nickname...
var senderDisplayStr = ""; var senderDisplayStr = "";
if (!fromMe) { if (!fromMe) {
ContactInfoState? contact = Provider.of<ProfileInfoState>(context).contactList.getContact(Provider.of<MessageMetadata>(context).senderHandle); ContactInfoState? contact = Provider.of<ProfileInfoState>(context).contactList.findContact(Provider.of<MessageMetadata>(context).senderHandle);
if (contact != null) { if (contact != null) {
senderDisplayStr = contact.nickname; senderDisplayStr = contact.nickname;
} else { } else {
@ -45,16 +50,40 @@ class MessageBubbleState extends State<MessageBubble> {
var wdgSender = SelectableText(senderDisplayStr, var wdgSender = SelectableText(senderDisplayStr,
style: TextStyle(fontSize: 9.0, color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeTextColor() : Provider.of<Settings>(context).theme.messageFromOtherTextColor())); style: TextStyle(fontSize: 9.0, color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeTextColor() : Provider.of<Settings>(context).theme.messageFromOtherTextColor()));
var wdgMessage = SelectableText( var wdgMessage;
widget.content + '\u202F',
//key: Key(myKey), if (!showClickableLinks) {
focusNode: _focus, wdgMessage = SelectableText(
style: TextStyle( widget.content + '\u202F',
color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeTextColor() : Provider.of<Settings>(context).theme.messageFromOtherTextColor(), //key: Key(myKey),
), focusNode: _focus,
textAlign: TextAlign.left, style: TextStyle(
textWidthBasis: TextWidthBasis.longestLine, color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeTextColor() : Provider.of<Settings>(context).theme.messageFromOtherTextColor(),
); ),
textAlign: TextAlign.left,
textWidthBasis: TextWidthBasis.longestLine,
);
} else {
wdgMessage = SelectableLinkify(
text: widget.content + '\u202F',
// TODO: onOpen breaks the "selectable" functionality. Maybe something to do with gesture handler?
options: LinkifyOptions(humanize: false),
linkifiers: [UrlLinkifier()],
onOpen: (link) {
_modalOpenLink(context, link);
},
//key: Key(myKey),
focusNode: _focus,
style: TextStyle(
color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeTextColor() : Provider.of<Settings>(context).theme.messageFromOtherTextColor(),
),
linkStyle: TextStyle(
color: Provider.of<Settings>(context).current().mainTextColor(),
),
textAlign: TextAlign.left,
textWidthBasis: TextWidthBasis.longestLine,
);
}
var wdgDecorations = MessageBubbleDecoration(ackd: Provider.of<MessageMetadata>(context).ackd, errored: Provider.of<MessageMetadata>(context).error, fromMe: fromMe, prettyDate: prettyDate); var wdgDecorations = MessageBubbleDecoration(ackd: Provider.of<MessageMetadata>(context).ackd, errored: Provider.of<MessageMetadata>(context).error, fromMe: fromMe, prettyDate: prettyDate);
@ -90,4 +119,56 @@ class MessageBubbleState extends State<MessageBubble> {
children: fromMe ? [wdgMessage, wdgDecorations] : [wdgSender, wdgMessage, wdgDecorations]))))); children: fromMe ? [wdgMessage, wdgDecorations] : [wdgSender, wdgMessage, wdgDecorations])))));
}); });
} }
void _modalOpenLink(BuildContext ctx, LinkableElement link) {
showModalBottomSheet<void>(
context: ctx,
builder: (BuildContext bcontext) {
return Container(
height: 200, // bespoke value courtesy of the [TextField] docs
child: Center(
child: Padding(
padding: EdgeInsets.all(30.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(
"Opening this link will launch an application outside of Cwtch and may reveal metadata or otherwise compromise the security of Cwtch. Only open links from people you trust. Are you sure you want to continue?"),
Flex(direction: Axis.horizontal, mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[
Container(
margin: EdgeInsets.symmetric(vertical: 20, horizontal: 10),
child: ElevatedButton(
child: Text("Copy link", semanticsLabel: "Copy link"),
onPressed: () {
Clipboard.setData(new ClipboardData(text: link.url));
final snackBar = SnackBar(
content: Text(AppLocalizations.of(context)!.copiedClipboardNotification),
);
Navigator.pop(bcontext);
ScaffoldMessenger.of(context).showSnackBar(snackBar);
},
),
),
Container(
margin: EdgeInsets.symmetric(vertical: 20, horizontal: 10),
child: ElevatedButton(
child: Text("Open link", semanticsLabel: "Open link"),
onPressed: () async {
if (await canLaunch(link.url)) {
await launch(link.url);
} else {
throw 'Could not launch $link';
}
},
),
),
]),
],
)),
));
});
}
} }

View File

@ -76,18 +76,18 @@ class _MessageListState extends State<MessageList> {
reverse: true, // NOTE: There seems to be a bug in flutter that corrects the mouse wheel scroll, but not the drag direction... reverse: true, // NOTE: There seems to be a bug in flutter that corrects the mouse wheel scroll, but not the drag direction...
itemBuilder: (itemBuilderContext, index) { itemBuilder: (itemBuilderContext, index) {
var profileOnion = Provider.of<ProfileInfoState>(outerContext, listen: false).onion; var profileOnion = Provider.of<ProfileInfoState>(outerContext, listen: false).onion;
var contactHandle = Provider.of<ContactInfoState>(outerContext, listen: false).onion; var contactHandle = Provider.of<ContactInfoState>(outerContext, listen: false).identifier;
var messageIndex = Provider.of<ContactInfoState>(outerContext).totalMessages - index - 1; var messageIndex = index;
return FutureBuilder( return FutureBuilder(
future: messageHandler(outerContext, profileOnion, contactHandle, messageIndex), future: messageHandler(outerContext, profileOnion, contactHandle, messageIndex),
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.hasData) { if (snapshot.hasData) {
var message = snapshot.data as Message; var message = snapshot.data as Message;
// Already includes MessageRow,, var key = Provider.of<ContactInfoState>(outerContext, listen: false).getMessageKey(contactHandle, message.getMetadata().messageID);
return message.getWidget(context); return message.getWidget(context, key);
} else { } else {
return Text(''); //MessageLoadingBubble(); return MessageLoadingBubble();
} }
}, },
); );

View File

@ -1,9 +1,4 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../model.dart';
import 'package:intl/intl.dart';
import '../settings.dart';
class MessageLoadingBubble extends StatefulWidget { class MessageLoadingBubble extends StatefulWidget {
@override @override

View File

@ -15,6 +15,7 @@ import '../settings.dart';
class MessageRow extends StatefulWidget { class MessageRow extends StatefulWidget {
final Widget child; final Widget child;
MessageRow(this.child, {Key? key}) : super(key: key); MessageRow(this.child, {Key? key}) : super(key: key);
@override @override
@ -28,9 +29,12 @@ class MessageRowState extends State<MessageRow> with SingleTickerProviderStateMi
late Alignment _dragAlignment = Alignment.center; late Alignment _dragAlignment = Alignment.center;
Alignment _dragAffinity = Alignment.center; Alignment _dragAffinity = Alignment.center;
late int index;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
index = Provider.of<MessageMetadata>(context, listen: false).messageID;
_controller = AnimationController(vsync: this); _controller = AnimationController(vsync: this);
_controller.addListener(() { _controller.addListener(() {
setState(() { setState(() {
@ -41,15 +45,17 @@ class MessageRowState extends State<MessageRow> with SingleTickerProviderStateMi
@override @override
void dispose() { void dispose() {
_controller.dispose(); if (_controller != null) {
_controller.dispose();
}
super.dispose(); super.dispose();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var fromMe = Provider.of<MessageMetadata>(context).senderHandle == Provider.of<ProfileInfoState>(context).onion; var fromMe = Provider.of<MessageMetadata>(context).senderHandle == Provider.of<ProfileInfoState>(context).onion;
var isContact = Provider.of<ProfileInfoState>(context).contactList.getContact(Provider.of<MessageMetadata>(context).senderHandle) != null; var isContact = Provider.of<ProfileInfoState>(context).contactList.findContact(Provider.of<MessageMetadata>(context).senderHandle) != null;
var isBlocked = isContact ? Provider.of<ProfileInfoState>(context).contactList.getContact(Provider.of<MessageMetadata>(context).senderHandle)!.isBlocked : false; var isBlocked = isContact ? Provider.of<ProfileInfoState>(context).contactList.findContact(Provider.of<MessageMetadata>(context).senderHandle)!.isBlocked : false;
var actualMessage = Flexible(flex: 3, fit: FlexFit.loose, child: widget.child); var actualMessage = Flexible(flex: 3, fit: FlexFit.loose, child: widget.child);
_dragAffinity = fromMe ? Alignment.centerRight : Alignment.centerLeft; _dragAffinity = fromMe ? Alignment.centerRight : Alignment.centerLeft;
@ -60,7 +66,7 @@ class MessageRowState extends State<MessageRow> with SingleTickerProviderStateMi
var senderDisplayStr = ""; var senderDisplayStr = "";
if (!fromMe) { if (!fromMe) {
ContactInfoState? contact = Provider.of<ProfileInfoState>(context).contactList.getContact(Provider.of<MessageMetadata>(context).senderHandle); ContactInfoState? contact = Provider.of<ProfileInfoState>(context).contactList.findContact(Provider.of<MessageMetadata>(context).senderHandle);
if (contact != null) { if (contact != null) {
senderDisplayStr = contact.nickname; senderDisplayStr = contact.nickname;
} else { } else {
@ -69,7 +75,7 @@ class MessageRowState extends State<MessageRow> with SingleTickerProviderStateMi
} }
Widget wdgIcons = Visibility( Widget wdgIcons = Visibility(
visible: Provider.of<AppState>(context).hoveredIndex == Provider.of<MessageMetadata>(context).messageIndex, visible: Provider.of<AppState>(context).hoveredIndex == Provider.of<MessageMetadata>(context).messageID,
maintainSize: true, maintainSize: true,
maintainAnimation: true, maintainAnimation: true,
maintainState: true, maintainState: true,
@ -77,7 +83,7 @@ class MessageRowState extends State<MessageRow> with SingleTickerProviderStateMi
child: IconButton( child: IconButton(
tooltip: AppLocalizations.of(context)!.tooltipReplyToThisMessage, tooltip: AppLocalizations.of(context)!.tooltipReplyToThisMessage,
onPressed: () { onPressed: () {
Provider.of<AppState>(context, listen: false).selectedIndex = Provider.of<MessageMetadata>(context, listen: false).messageIndex; Provider.of<AppState>(context, listen: false).selectedIndex = Provider.of<MessageMetadata>(context, listen: false).messageID;
}, },
icon: Icon(Icons.reply, color: Provider.of<Settings>(context).theme.dropShadowColor()))); icon: Icon(Icons.reply, color: Provider.of<Settings>(context).theme.dropShadowColor())));
Widget wdgSpacer = Flexible(child: SizedBox(width: 60, height: 10)); Widget wdgSpacer = Flexible(child: SizedBox(width: 60, height: 10));
@ -163,7 +169,7 @@ class MessageRowState extends State<MessageRow> with SingleTickerProviderStateMi
// For desktop... // For desktop...
onHover: (event) { onHover: (event) {
setState(() { setState(() {
Provider.of<AppState>(context, listen: false).hoveredIndex = Provider.of<MessageMetadata>(context, listen: false).messageIndex; Provider.of<AppState>(context, listen: false).hoveredIndex = Provider.of<MessageMetadata>(context, listen: false).messageID;
}); });
}, },
onExit: (event) { onExit: (event) {
@ -185,7 +191,7 @@ class MessageRowState extends State<MessageRow> with SingleTickerProviderStateMi
}, },
onPanEnd: (details) { onPanEnd: (details) {
_runAnimation(details.velocity.pixelsPerSecond, size); _runAnimation(details.velocity.pixelsPerSecond, size);
Provider.of<AppState>(context, listen: false).selectedIndex = Provider.of<MessageMetadata>(context, listen: false).messageIndex; Provider.of<AppState>(context, listen: false).selectedIndex = Provider.of<MessageMetadata>(context, listen: false).messageID;
}, },
child: Padding( child: Padding(
padding: EdgeInsets.all(2), padding: EdgeInsets.all(2),
@ -198,8 +204,10 @@ class MessageRowState extends State<MessageRow> with SingleTickerProviderStateMi
children: widgetRow, children: widgetRow,
))))); )))));
var mark = Provider.of<ContactInfoState>(context).newMarker; var mark = Provider.of<ContactInfoState>(context).newMarker;
if (mark > 0 && mark == Provider.of<ContactInfoState>(context).totalMessages - Provider.of<MessageMetadata>(context).messageIndex) { if (mark > 0 &&
return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [Align(alignment:Alignment.center ,child:_bubbleNew()), mr]); Provider.of<ContactInfoState>(context).messageCache.length > mark &&
Provider.of<ContactInfoState>(context).messageCache[mark - 1]?.metadata.messageID == Provider.of<MessageMetadata>(context).messageID) {
return Column(crossAxisAlignment: fromMe ? CrossAxisAlignment.end : CrossAxisAlignment.start, children: [Align(alignment: Alignment.center, child: _bubbleNew()), mr]);
} else { } else {
return mr; return mr;
} }
@ -209,9 +217,7 @@ class MessageRowState extends State<MessageRow> with SingleTickerProviderStateMi
return Container( return Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Provider.of<Settings>(context).theme.messageFromMeBackgroundColor(), color: Provider.of<Settings>(context).theme.messageFromMeBackgroundColor(),
border: Border.all( border: Border.all(color: Provider.of<Settings>(context).theme.messageFromMeBackgroundColor(), width: 1),
color: Provider.of<Settings>(context).theme.messageFromMeBackgroundColor(),
width: 1),
borderRadius: BorderRadius.only( borderRadius: BorderRadius.only(
topLeft: Radius.circular(8), topLeft: Radius.circular(8),
topRight: Radius.circular(8), topRight: Radius.circular(8),
@ -219,9 +225,7 @@ class MessageRowState extends State<MessageRow> with SingleTickerProviderStateMi
bottomRight: Radius.circular(8), bottomRight: Radius.circular(8),
), ),
), ),
child: Padding( child: Padding(padding: EdgeInsets.all(9.0), child: Text(AppLocalizations.of(context)!.newMessagesLabel)));
padding: EdgeInsets.all(9.0),
child: Text(AppLocalizations.of(context)!.newMessagesLabel)));
} }
void _runAnimation(Offset pixelsPerSecond, Size size) { void _runAnimation(Offset pixelsPerSecond, Size size) {
@ -249,12 +253,17 @@ class MessageRowState extends State<MessageRow> with SingleTickerProviderStateMi
} }
void _btnGoto() { void _btnGoto() {
selectConversation(context, Provider.of<MessageMetadata>(context, listen: false).senderHandle); var id = Provider.of<ProfileInfoState>(context, listen: false).contactList.findContact(Provider.of<MessageMetadata>(context, listen: false).senderHandle)?.identifier;
if (id == null) {
// Can't happen
} else {
selectConversation(context, id);
}
} }
void _btnAdd() { void _btnAdd() {
var sender = Provider.of<MessageMetadata>(context, listen: false).senderHandle; var sender = Provider.of<MessageMetadata>(context, listen: false).senderHandle;
if (sender == null || sender == "") { if (sender == "") {
print("sender not yet loaded"); print("sender not yet loaded");
return; return;
} }

View File

@ -34,7 +34,7 @@ class QuotedMessageBubbleState extends State<QuotedMessageBubble> {
// If the sender is not us, then we want to give them a nickname... // If the sender is not us, then we want to give them a nickname...
var senderDisplayStr = ""; var senderDisplayStr = "";
if (!fromMe) { if (!fromMe) {
ContactInfoState? contact = Provider.of<ProfileInfoState>(context).contactList.getContact(Provider.of<MessageMetadata>(context).senderHandle); ContactInfoState? contact = Provider.of<ProfileInfoState>(context).contactList.findContact(Provider.of<MessageMetadata>(context).senderHandle);
if (contact != null) { if (contact != null) {
senderDisplayStr = contact.nickname; senderDisplayStr = contact.nickname;
} else { } else {

View File

@ -21,40 +21,38 @@ class _ServerRowState extends State<ServerRow> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var server = Provider.of<ServerInfoState>(context); var server = Provider.of<ServerInfoState>(context);
return Card(clipBehavior: Clip.antiAlias, return Card(
clipBehavior: Clip.antiAlias,
margin: EdgeInsets.all(0.0), margin: EdgeInsets.all(0.0),
child: InkWell( child: InkWell(
child: Row( child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Padding( Padding(
padding: const EdgeInsets.all(6.0), //border size padding: const EdgeInsets.all(6.0), //border size
child: Icon(CwtchIcons.dns_24px, child: Icon(CwtchIcons.dns_24px,
color: server.running ? Provider.of<Settings>(context).theme.portraitOnlineBorderColor() : Provider.of<Settings>(context).theme.portraitOfflineBorderColor(), color: server.running ? Provider.of<Settings>(context).theme.portraitOnlineBorderColor() : Provider.of<Settings>(context).theme.portraitOfflineBorderColor(), size: 64)),
size: 64)
),
Expanded( Expanded(
child: Column( child: Column(
children: [ children: [
Text( Text(
server.description, server.description,
semanticsLabel: server.description, semanticsLabel: server.description,
style: Provider.of<FlwtchState>(context).biggerFont.apply(color: server.running ? Provider.of<Settings>(context).theme.portraitOnlineBorderColor() : Provider.of<Settings>(context).theme.portraitOfflineBorderColor()), style: Provider.of<FlwtchState>(context)
.biggerFont
.apply(color: server.running ? Provider.of<Settings>(context).theme.portraitOnlineBorderColor() : Provider.of<Settings>(context).theme.portraitOfflineBorderColor()),
softWrap: true,
overflow: TextOverflow.ellipsis,
),
Visibility(
visible: !Provider.of<Settings>(context).streamerMode,
child: ExcludeSemantics(
child: Text(
server.onion,
softWrap: true, softWrap: true,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), style: TextStyle(color: server.running ? Provider.of<Settings>(context).theme.portraitOnlineBorderColor() : Provider.of<Settings>(context).theme.portraitOfflineBorderColor()),
Visibility( )))
visible: !Provider.of<Settings>(context).streamerMode, ],
child: ExcludeSemantics( )),
child: Text(
server.onion,
softWrap: true,
overflow: TextOverflow.ellipsis,
style: TextStyle(color: server.running ? Provider.of<Settings>(context).theme.portraitOnlineBorderColor() : Provider.of<Settings>(context).theme.portraitOfflineBorderColor()),
)))
],
)),
// Copy server button // Copy server button
IconButton( IconButton(
@ -75,23 +73,23 @@ class _ServerRowState extends State<ServerRow> {
_pushEditServer(server); _pushEditServer(server);
}, },
) )
]))); ])));
} }
void _pushEditServer(ServerInfoState server ) { void _pushEditServer(ServerInfoState server) {
Provider.of<ErrorHandler>(context).reset(); Provider.of<ErrorHandler>(context).reset();
Navigator.of(context).push(MaterialPageRoute<void>( Navigator.of(context).push(MaterialPageRoute<void>(
settings: RouteSettings(name: "serveraddedit"), settings: RouteSettings(name: "serveraddedit"),
builder: (BuildContext context) { builder: (BuildContext context) {
return MultiProvider( return MultiProvider(
providers: [ChangeNotifierProvider<ServerInfoState>( providers: [
create: (_) => server, ChangeNotifierProvider<ServerInfoState>(
)], create: (_) => server,
)
],
child: AddEditServerView(), child: AddEditServerView(),
); );
}, },
)); ));
} }
} }

View File

@ -6,6 +6,10 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <url_launcher_linux/url_launcher_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) { void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
} }

View File

@ -3,6 +3,7 @@
# #
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
url_launcher_linux
) )
set(PLUGIN_BUNDLED_LIBRARIES) set(PLUGIN_BUNDLED_LIBRARIES)

View File

@ -7,8 +7,10 @@ import Foundation
import package_info_plus_macos import package_info_plus_macos
import path_provider_macos import path_provider_macos
import url_launcher_macos
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin")) FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
} }

View File

@ -4,11 +4,14 @@ PODS:
- FlutterMacOS - FlutterMacOS
- path_provider_macos (0.0.1): - path_provider_macos (0.0.1):
- FlutterMacOS - FlutterMacOS
- url_launcher_macos (0.0.1):
- FlutterMacOS
DEPENDENCIES: DEPENDENCIES:
- FlutterMacOS (from `Flutter/ephemeral`) - FlutterMacOS (from `Flutter/ephemeral`)
- package_info_plus_macos (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus_macos/macos`) - package_info_plus_macos (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus_macos/macos`)
- path_provider_macos (from `Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos`) - path_provider_macos (from `Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos`)
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
EXTERNAL SOURCES: EXTERNAL SOURCES:
FlutterMacOS: FlutterMacOS:
@ -17,12 +20,15 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/package_info_plus_macos/macos :path: Flutter/ephemeral/.symlinks/plugins/package_info_plus_macos/macos
path_provider_macos: path_provider_macos:
:path: Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos :path: Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos
url_launcher_macos:
:path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
SPEC CHECKSUMS: SPEC CHECKSUMS:
FlutterMacOS: 57701585bf7de1b3fc2bb61f6378d73bbdea8424 FlutterMacOS: 57701585bf7de1b3fc2bb61f6378d73bbdea8424
package_info_plus_macos: f010621b07802a241d96d01876d6705f15e77c1c package_info_plus_macos: f010621b07802a241d96d01876d6705f15e77c1c
path_provider_macos: a0a3fd666cb7cd0448e936fb4abad4052961002b path_provider_macos: a0a3fd666cb7cd0448e936fb4abad4052961002b
url_launcher_macos: 45af3d61de06997666568a7149c1be98b41c95d4
PODFILE CHECKSUM: 6eac6b3292e5142cfc23bdeb71848a40ec51c14c PODFILE CHECKSUM: 6eac6b3292e5142cfc23bdeb71848a40ec51c14c
COCOAPODS: 1.9.3 COCOAPODS: 1.11.2

View File

@ -35,7 +35,7 @@ packages:
name: characters name: characters
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.1.0" version: "1.2.0"
charcode: charcode:
dependency: transitive dependency: transitive
description: description:
@ -125,6 +125,13 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
flutter_linkify:
dependency: "direct main"
description:
name: flutter_linkify
url: "https://pub.dartlang.org"
source: hosted
version: "5.0.2"
flutter_localizations: flutter_localizations:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter
@ -189,6 +196,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.6.3" version: "0.6.3"
linkify:
dependency: transitive
description:
name: linkify
url: "https://pub.dartlang.org"
source: hosted
version: "4.1.0"
matcher: matcher:
dependency: transitive dependency: transitive
description: description:
@ -403,7 +417,7 @@ packages:
name: test_api name: test_api
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.4.3" version: "0.4.7"
typed_data: typed_data:
dependency: transitive dependency: transitive
description: description:
@ -411,13 +425,55 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.3.0" version: "1.3.0"
url_launcher:
dependency: "direct main"
description:
name: url_launcher
url: "https://pub.dartlang.org"
source: hosted
version: "6.0.12"
url_launcher_linux:
dependency: transitive
description:
name: url_launcher_linux
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.2"
url_launcher_macos:
dependency: transitive
description:
name: url_launcher_macos
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.2"
url_launcher_platform_interface:
dependency: transitive
description:
name: url_launcher_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.4"
url_launcher_web:
dependency: transitive
description:
name: url_launcher_web
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.4"
url_launcher_windows:
dependency: transitive
description:
name: url_launcher_windows
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.2"
vector_math: vector_math:
dependency: transitive dependency: transitive
description: description:
name: vector_math name: vector_math
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.0" version: "2.1.1"
win32: win32:
dependency: transitive dependency: transitive
description: description:
@ -447,5 +503,5 @@ packages:
source: hosted source: hosted
version: "3.1.0" version: "3.1.0"
sdks: sdks:
dart: ">=2.13.0 <3.0.0" dart: ">=2.14.0 <3.0.0"
flutter: ">=2.0.0" flutter: ">=2.5.0"

View File

@ -43,6 +43,8 @@ dependencies:
scrollable_positioned_list: ^0.2.0-nullsafety.0 scrollable_positioned_list: ^0.2.0-nullsafety.0
file_picker: ^4.0.1 file_picker: ^4.0.1
file_picker_desktop: ^1.1.0 file_picker_desktop: ^1.1.0
flutter_linkify: ^5.0.2
url_launcher: ^6.0.12
dev_dependencies: dev_dependencies:
msix: ^2.1.3 msix: ^2.1.3

View File

@ -6,6 +6,9 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <url_launcher_windows/url_launcher_windows.h>
void RegisterPlugins(flutter::PluginRegistry* registry) { void RegisterPlugins(flutter::PluginRegistry* registry) {
UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
} }

View File

@ -3,6 +3,7 @@
# #
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
url_launcher_windows
) )
set(PLUGIN_BUNDLED_LIBRARIES) set(PLUGIN_BUNDLED_LIBRARIES)