From 5cea69c2242909a072221a70c2d989c1ff40aef4 Mon Sep 17 00:00:00 2001 From: otsmr Date: Mon, 27 Jan 2025 20:30:41 +0100 Subject: [PATCH] clean code --- lib/main.dart | 6 +- lib/src/app.dart | 52 ++-- lib/src/localization/app_en.arb | 2 + lib/src/model/contacts_model.dart | 5 + lib/src/model/messages_model.dart | 180 ++++++++---- lib/src/proto/api/client_to_server.pb.dart | 90 ++++-- .../proto/api/client_to_server.pbjson.dart | 46 +-- lib/src/proto/api/server_to_client.pb.dart | 98 ++++++- .../proto/api/server_to_client.pbjson.dart | 19 +- lib/src/providers/api/api.dart | 107 +++++++ lib/src/providers/api/api_utils.dart | 42 +++ lib/src/providers/api/server_messages.dart | 99 +++++++ lib/src/providers/api_provider.dart | 272 ++++-------------- ...der.dart => contacts_change_provider.dart} | 40 +-- .../providers/messages_change_provider.dart | 31 ++ lib/src/utils/api.dart | 169 ----------- lib/src/utils/misc.dart | 4 - lib/src/views/chat_list_view.dart | 148 +++++----- lib/src/views/home_view.dart | 40 ++- lib/src/views/register_view.dart | 58 ++-- lib/src/views/search_username_view.dart | 130 +++++---- lib/src/views/share_image_view.dart | 20 +- 22 files changed, 941 insertions(+), 717 deletions(-) create mode 100644 lib/src/providers/api/api.dart create mode 100644 lib/src/providers/api/api_utils.dart create mode 100644 lib/src/providers/api/server_messages.dart rename lib/src/providers/{notify_provider.dart => contacts_change_provider.dart} (53%) create mode 100644 lib/src/providers/messages_change_provider.dart delete mode 100644 lib/src/utils/api.dart diff --git a/lib/main.dart b/lib/main.dart index 11d9f6b..9921c17 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -4,7 +4,8 @@ import 'package:twonly/src/providers/api_provider.dart'; import 'package:twonly/src/providers/db_provider.dart'; import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; -import 'package:twonly/src/providers/notify_provider.dart'; +import 'package:twonly/src/providers/messages_change_provider.dart'; +import 'package:twonly/src/providers/contacts_change_provider.dart'; import 'package:twonly/src/utils/misc.dart'; import 'src/app.dart'; import 'src/settings/settings_controller.dart'; @@ -54,7 +55,8 @@ void main() async { runApp( MultiProvider( providers: [ - ChangeNotifierProvider(create: (_) => NotifyProvider()), + ChangeNotifierProvider(create: (_) => MessagesChangeProvider()), + ChangeNotifierProvider(create: (_) => ContactChangeProvider()), ], child: MyApp(settingsController: settingsController), ), diff --git a/lib/src/app.dart b/lib/src/app.dart index a080b86..9cd0949 100644 --- a/lib/src/app.dart +++ b/lib/src/app.dart @@ -1,7 +1,7 @@ -import 'package:fixnum/fixnum.dart'; import 'package:provider/provider.dart'; import 'package:twonly/main.dart'; -import 'package:twonly/src/providers/notify_provider.dart'; +import 'package:twonly/src/providers/contacts_change_provider.dart'; +import 'package:twonly/src/providers/messages_change_provider.dart'; import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/views/onboarding_view.dart'; import 'package:twonly/src/views/home_view.dart'; @@ -12,9 +12,16 @@ import 'package:flutter_localizations/flutter_localizations.dart'; import 'dart:async'; import 'settings/settings_controller.dart'; -Function(Int64) addSendingTo = (a) {}; -Function(Int64) removeSendingTo = (a) {}; -Function() updateNotifyProvider = () {}; +// these global function can be called from anywhere to update +// the ui when something changed. The callbacks will be set by +// MyApp widget. + +// this callback is called by the apiProvider +Function(bool) globalCallbackConnectionState = (a) {}; + +// these two callbacks are called on updated to the corresponding database +Function globalCallBackOnContactChange = () {}; +Function(int) globalCallBackOnMessageChange = (a) {}; /// The Widget that configures your application. class MyApp extends StatefulWidget { @@ -28,7 +35,7 @@ class MyApp extends StatefulWidget { class _MyAppState extends State { Future _isUserCreated = isUserCreated(); - bool _showOnboarding = true; + bool _showOnboarding = false; bool _isConnected = false; int redColorOpacity = 0; // Start with dark red bool redColorGoUp = true; @@ -39,31 +46,38 @@ class _MyAppState extends State { super.initState(); _startColorAnimation(); - apiProvider.setConnectionStateCallback((isConnected) { + // init change provider to load data from the database + context.read().update(); + context.read().init(); + + // register global callbacks to the widget tree + globalCallbackConnectionState = (isConnected) { setState(() { _isConnected = isConnected; }); - }); - apiProvider.setUpdatedContacts(() { - context.read().update(); - }); - - addSendingTo = (a) { - context.read().addSendingTo(a); }; - removeSendingTo = (a) { - context.read().removeSendingTo(a); + globalCallBackOnContactChange = () { + context.read().update(); }; - updateNotifyProvider = () { - context.read().update(); + globalCallBackOnMessageChange = (userId) { + context.read().updateLastMessageFor(userId); }; - context.read().update(); + // connect async to the backend api apiProvider.connect(); } + @override + void dispose() { + // disable globalCallbacks to the flutter tree + globalCallbackConnectionState = (a) {}; + globalCallBackOnContactChange = () {}; + globalCallBackOnMessageChange = (a) {}; + super.dispose(); + } + void _startColorAnimation() { // Change the color every second Future.delayed(Duration(milliseconds: 200), () { diff --git a/lib/src/localization/app_en.arb b/lib/src/localization/app_en.arb index e0ed7e3..c866ed3 100644 --- a/lib/src/localization/app_en.arb +++ b/lib/src/localization/app_en.arb @@ -20,6 +20,8 @@ "searchUsernameNotFound": "Username not found", "searchUsernameNewFollowerTitle": "Follow requests", "searchUsernameQrCodeBtn": "Scan QR code", + "chatListViewSearchUserNameBtn": "Add user", + "chatListViewSendFirstTwonly": "Send your first twonly!", "searchUsernameNotFoundLong": "\"{username}\" is not a twonly user. Please check the username and try again.", "errorUnknown": "An unexpected error has occurred. Please try again later.", "errorBadRequest": "The request could not be understood by the server due to malformed syntax. Please check your input and try again.", diff --git a/lib/src/model/contacts_model.dart b/lib/src/model/contacts_model.dart index 42bfe70..1e3be56 100644 --- a/lib/src/model/contacts_model.dart +++ b/lib/src/model/contacts_model.dart @@ -2,6 +2,7 @@ import 'package:cv/cv.dart'; import 'package:fixnum/fixnum.dart'; import 'package:logging/logging.dart'; import 'package:twonly/main.dart'; +import 'package:twonly/src/app.dart'; class Contact { Contact( @@ -98,6 +99,7 @@ class DbContacts extends CvModelBase { where: "$columnUserId = ?", whereArgs: [userId], ); + globalCallBackOnContactChange(); } static Future acceptUser(int userId) async { @@ -111,6 +113,7 @@ class DbContacts extends CvModelBase { where: "$columnUserId = ?", whereArgs: [userId], ); + globalCallBackOnContactChange(); } static Future deleteUser(int userId) async { @@ -119,6 +122,7 @@ class DbContacts extends CvModelBase { where: "$columnUserId = ?", whereArgs: [userId], ); + globalCallBackOnContactChange(); } static Future insertNewContact( @@ -130,6 +134,7 @@ class DbContacts extends CvModelBase { DbContacts.columnUserId: userId, DbContacts.columnRequested: a }); + globalCallBackOnContactChange(); return true; } catch (e) { Logger("contacts_model/getUsers").shout("$e"); diff --git a/lib/src/model/messages_model.dart b/lib/src/model/messages_model.dart index eb547c5..eef7ebc 100644 --- a/lib/src/model/messages_model.dart +++ b/lib/src/model/messages_model.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'package:cv/cv.dart'; import 'package:logging/logging.dart'; import 'package:twonly/main.dart'; +import 'package:twonly/src/app.dart'; import 'package:twonly/src/model/json/message.dart'; class DbMessage { @@ -13,7 +14,8 @@ class DbMessage { required this.messageMessageKind, required this.messageContent, required this.messageOpenedAt, - required this.messageAcknowledge, + required this.messageAcknowledgeByUser, + required this.messageAcknowledgeByServer, required this.sendOrReceivedAt, }); @@ -22,9 +24,10 @@ class DbMessage { int? messageOtherId; int otherUserId; MessageKind messageMessageKind; - MessageContent messageContent; + MessageContent? messageContent; DateTime? messageOpenedAt; - bool messageAcknowledge; + bool messageAcknowledgeByUser; + bool messageAcknowledgeByServer; DateTime sendOrReceivedAt; } @@ -38,19 +41,24 @@ class DbMessages extends CvModelBase { final messageOtherId = CvField(columnMessageOtherId); static const columnOtherUserId = "other_user_id"; - final otherUserId = CvField(columnOtherUserId); + final otherUserId = CvField(columnOtherUserId); static const columnMessageKind = "message_kind"; final messageMessageKind = CvField(columnMessageKind); static const columnMessageContentJson = "message_json"; - final messageContentJson = CvField(columnMessageContentJson); + final messageContentJson = CvField(columnMessageContentJson); static const columnMessageOpenedAt = "message_opened_at"; final messageOpenedAt = CvField(columnMessageOpenedAt); - static const columnMessageAcknowledge = "message_acknowledged"; - final messageAcknowledge = CvField(columnMessageAcknowledge); + static const columnMessageAcknowledgeByUser = "message_acknowledged_by_user"; + final messageAcknowledgeByUser = CvField(columnMessageAcknowledgeByUser); + + static const columnMessageAcknowledgeByServer = + "message_acknowledged_by_server"; + final messageAcknowledgeByServer = + CvField(columnMessageAcknowledgeByServer); static const columnSendOrReceivedAt = "message_send_or_received_at"; final sendOrReceivedAt = CvField(columnSendOrReceivedAt); @@ -63,10 +71,11 @@ class DbMessages extends CvModelBase { CREATE TABLE IF NOT EXISTS $tableName ( $columnMessageId INTEGER NOT NULL PRIMARY KEY, $columnMessageOtherId INTEGER DEFAULT NULL, - $columnOtherUserId INTEGER DEFAULT NULL, + $columnOtherUserId INTEGER NOT NULL, $columnMessageKind INTEGER NOT NULL, - $columnMessageAcknowledge INTEGER NOT NULL DEFAULT 0, - $columnMessageContentJson TEXT NOT NULL, + $columnMessageAcknowledgeByUser INTEGER NOT NULL DEFAULT 0, + $columnMessageAcknowledgeByServer INTEGER NOT NULL DEFAULT 0, + $columnMessageContentJson TEXT DEFAULT NULL, $columnMessageOpenedAt DATETIME DEFAULT NULL, $columnSendOrReceivedAt DATETIME DEFAULT CURRENT_TIMESTAMP, $columnUpdatedAt DATETIME DEFAULT CURRENT_TIMESTAMP @@ -74,8 +83,33 @@ class DbMessages extends CvModelBase { """; } - static Future insertMyMessage( - int userIdFrom, MessageKind kind, String jsonContent) async { + static Future deleteMessageById(int messageId) async { + await dbProvider.db!.delete( + tableName, + where: '$columnMessageId = ?', + whereArgs: [messageId], + ); + int? fromUserId = await getFromUserIdByMessageId(messageId); + if (fromUserId != null) { + globalCallBackOnMessageChange(fromUserId); + } + } + + static Future getFromUserIdByMessageId(int messageId) async { + List> result = await dbProvider.db!.query( + tableName, + columns: [columnOtherUserId], + where: '$columnMessageId = ?', + whereArgs: [messageId], + ); + if (result.isNotEmpty) { + return result.first[columnOtherUserId] as int?; + } + return null; + } + + static Future insertMyMessage(int userIdFrom, MessageKind kind, + {String? jsonContent}) async { try { int messageId = await dbProvider.db!.insert(tableName, { columnMessageKind: kind.index, @@ -83,6 +117,7 @@ class DbMessages extends CvModelBase { columnOtherUserId: userIdFrom, columnSendOrReceivedAt: DateTime.now().toIso8601String() }); + globalCallBackOnMessageChange(userIdFrom); return messageId; } catch (e) { Logger("contacts_model/getUsers").shout("$e"); @@ -97,8 +132,12 @@ class DbMessages extends CvModelBase { columnMessageOtherId: messageId, columnMessageKind: kind.index, columnMessageContentJson: jsonContent, + columnMessageAcknowledgeByServer: 1, + columnMessageAcknowledgeByUser: + 0, // ack in case of sending corresponds to the opened flag columnOtherUserId: userIdFrom }); + globalCallBackOnMessageChange(userIdFrom); return true; } catch (e) { Logger("contacts_model/getUsers").shout("$e"); @@ -106,56 +145,52 @@ class DbMessages extends CvModelBase { } } - static List convertToDbMessage(List fromDb) { - try { - List parsedUsers = []; - for (int i = 0; i < fromDb.length; i++) { - dynamic messageOpenedAt = fromDb[i][columnMessageOpenedAt]; - if (messageOpenedAt != null) { - messageOpenedAt = DateTime.tryParse(fromDb[i][columnMessageOpenedAt]); - } - print("Datetime: ${fromDb[i][columnSendOrReceivedAt]}"); - print( - "Datetime parsed: ${DateTime.tryParse(fromDb[i][columnSendOrReceivedAt])}"); - parsedUsers.add( - DbMessage( - sendOrReceivedAt: - DateTime.tryParse(fromDb[i][columnSendOrReceivedAt])!, - messageId: fromDb[i][columnMessageId], - messageOtherId: fromDb[i][columnMessageOtherId], - otherUserId: fromDb[i][columnOtherUserId], - messageMessageKind: - MessageKindExtension.fromIndex(fromDb[i][columnMessageKind]), - messageContent: MessageContent.fromJson( - jsonDecode(fromDb[i][columnMessageContentJson])), - messageOpenedAt: messageOpenedAt, - messageAcknowledge: fromDb[i][columnMessageAcknowledge] == 1, - ), - ); - } - return parsedUsers; - } catch (e) { - Logger("messages_model/convertToDbMessage").shout("$e"); - return []; - } - } - static Future getLastMessagesForPreviewForUser( int otherUserId) async { - var rows = await dbProvider.db!.query(tableName, - where: "$columnOtherUserId = ?", - whereArgs: [otherUserId], - orderBy: "$columnUpdatedAt DESC", - limit: 1); + var rows = await dbProvider.db!.query( + tableName, + where: "$columnOtherUserId = ?", + whereArgs: [otherUserId], + orderBy: "$columnUpdatedAt DESC", + limit: 10, + ); List messages = convertToDbMessage(rows); + + // check if there is a message which was not ack by the server + List notAckByServer = + messages.where((c) => !c.messageAcknowledgeByServer).toList(); + if (notAckByServer.isNotEmpty) return notAckByServer[0]; + + // check if there is a message which was not ack by the user + List notAckByUser = + messages.where((c) => !c.messageAcknowledgeByUser).toList(); + if (notAckByUser.isNotEmpty) return notAckByUser[0]; + if (messages.isEmpty) return null; return messages[0]; } - static Future acknowledgeMessage(int fromUserId, int messageId) async { + static Future acknowledgeMessageByServer(int messageId) async { Map valuesToUpdate = { - columnMessageAcknowledge: 1, + columnMessageAcknowledgeByServer: 1, + }; + await dbProvider.db!.update( + tableName, + valuesToUpdate, + where: "$messageId = ?", + whereArgs: [messageId], + ); + int? fromUserId = await getFromUserIdByMessageId(messageId); + if (fromUserId != null) { + globalCallBackOnMessageChange(fromUserId); + } + } + + // check fromUserId to prevent spoofing + static Future acknowledgeMessageByUser(int fromUserId, int messageId) async { + Map valuesToUpdate = { + columnMessageAcknowledgeByUser: 1, }; await dbProvider.db!.update( tableName, @@ -163,6 +198,7 @@ class DbMessages extends CvModelBase { where: "$messageId = ? AND $columnOtherUserId = ?", whereArgs: [messageId, fromUserId], ); + globalCallBackOnMessageChange(fromUserId); } @override @@ -173,4 +209,42 @@ class DbMessages extends CvModelBase { messageOpenedAt, sendOrReceivedAt ]; + + static List convertToDbMessage(List fromDb) { + try { + List parsedUsers = []; + for (int i = 0; i < fromDb.length; i++) { + dynamic messageOpenedAt = fromDb[i][columnMessageOpenedAt]; + if (messageOpenedAt != null) { + messageOpenedAt = DateTime.tryParse(fromDb[i][columnMessageOpenedAt]); + } + dynamic content = fromDb[i][columnMessageContentJson]; + if (content != null) { + content = MessageContent.fromJson( + jsonDecode(fromDb[i][columnMessageContentJson])); + } + parsedUsers.add( + DbMessage( + sendOrReceivedAt: + DateTime.tryParse(fromDb[i][columnSendOrReceivedAt])!, + messageId: fromDb[i][columnMessageId], + messageOtherId: fromDb[i][columnMessageOtherId], + otherUserId: fromDb[i][columnOtherUserId], + messageMessageKind: + MessageKindExtension.fromIndex(fromDb[i][columnMessageKind]), + messageContent: content, + messageOpenedAt: messageOpenedAt, + messageAcknowledgeByUser: + fromDb[i][columnMessageAcknowledgeByUser] == 1, + messageAcknowledgeByServer: + fromDb[i][columnMessageAcknowledgeByServer] == 1, + ), + ); + } + return parsedUsers; + } catch (e) { + Logger("messages_model/convertToDbMessage").shout("$e"); + return []; + } + } } diff --git a/lib/src/proto/api/client_to_server.pb.dart b/lib/src/proto/api/client_to_server.pb.dart index 851984e..0ff58a6 100644 --- a/lib/src/proto/api/client_to_server.pb.dart +++ b/lib/src/proto/api/client_to_server.pb.dart @@ -742,21 +742,12 @@ class ApplicationData_GetPrekeysByUserId extends $pb.GeneratedMessage { } class ApplicationData_GetUploadToken extends $pb.GeneratedMessage { - factory ApplicationData_GetUploadToken({ - $core.int? len, - }) { - final $result = create(); - if (len != null) { - $result.len = len; - } - return $result; - } + factory ApplicationData_GetUploadToken() => create(); ApplicationData_GetUploadToken._() : super(); factory ApplicationData_GetUploadToken.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); factory ApplicationData_GetUploadToken.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'ApplicationData.GetUploadToken', package: const $pb.PackageName(_omitMessageNames ? '' : 'client_to_server'), createEmptyInstance: create) - ..a<$core.int>(1, _omitFieldNames ? '' : 'len', $pb.PbFieldType.OU3) ..hasRequiredFields = false ; @@ -780,15 +771,6 @@ class ApplicationData_GetUploadToken extends $pb.GeneratedMessage { @$core.pragma('dart2js:noInline') static ApplicationData_GetUploadToken getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); static ApplicationData_GetUploadToken? _defaultInstance; - - @$pb.TagNumber(1) - $core.int get len => $_getIZ(0); - @$pb.TagNumber(1) - set len($core.int v) { $_setUnsignedInt32(0, v); } - @$pb.TagNumber(1) - $core.bool hasLen() => $_has(0); - @$pb.TagNumber(1) - void clearLen() => clearField(1); } class ApplicationData_UploadData extends $pb.GeneratedMessage { @@ -869,6 +851,56 @@ class ApplicationData_UploadData extends $pb.GeneratedMessage { void clearData() => clearField(3); } +class ApplicationData_DownloadData extends $pb.GeneratedMessage { + factory ApplicationData_DownloadData({ + $core.List<$core.int>? uploadToken, + }) { + final $result = create(); + if (uploadToken != null) { + $result.uploadToken = uploadToken; + } + return $result; + } + ApplicationData_DownloadData._() : super(); + factory ApplicationData_DownloadData.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory ApplicationData_DownloadData.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'ApplicationData.DownloadData', package: const $pb.PackageName(_omitMessageNames ? '' : 'client_to_server'), createEmptyInstance: create) + ..a<$core.List<$core.int>>(1, _omitFieldNames ? '' : 'uploadToken', $pb.PbFieldType.OY) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + ApplicationData_DownloadData clone() => ApplicationData_DownloadData()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + ApplicationData_DownloadData copyWith(void Function(ApplicationData_DownloadData) updates) => super.copyWith((message) => updates(message as ApplicationData_DownloadData)) as ApplicationData_DownloadData; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static ApplicationData_DownloadData create() => ApplicationData_DownloadData._(); + ApplicationData_DownloadData createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static ApplicationData_DownloadData getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static ApplicationData_DownloadData? _defaultInstance; + + @$pb.TagNumber(1) + $core.List<$core.int> get uploadToken => $_getN(0); + @$pb.TagNumber(1) + set uploadToken($core.List<$core.int> v) { $_setBytes(0, v); } + @$pb.TagNumber(1) + $core.bool hasUploadToken() => $_has(0); + @$pb.TagNumber(1) + void clearUploadToken() => clearField(1); +} + enum ApplicationData_ApplicationData { textmessage, getuserbyusername, @@ -876,6 +908,7 @@ enum ApplicationData_ApplicationData { getuploadtoken, uploaddata, getuserbyid, + downloaddata, notSet } @@ -887,6 +920,7 @@ class ApplicationData extends $pb.GeneratedMessage { ApplicationData_GetUploadToken? getuploadtoken, ApplicationData_UploadData? uploaddata, ApplicationData_GetUserById? getuserbyid, + ApplicationData_DownloadData? downloaddata, }) { final $result = create(); if (textmessage != null) { @@ -907,6 +941,9 @@ class ApplicationData extends $pb.GeneratedMessage { if (getuserbyid != null) { $result.getuserbyid = getuserbyid; } + if (downloaddata != null) { + $result.downloaddata = downloaddata; + } return $result; } ApplicationData._() : super(); @@ -920,16 +957,18 @@ class ApplicationData extends $pb.GeneratedMessage { 4 : ApplicationData_ApplicationData.getuploadtoken, 5 : ApplicationData_ApplicationData.uploaddata, 6 : ApplicationData_ApplicationData.getuserbyid, + 7 : ApplicationData_ApplicationData.downloaddata, 0 : ApplicationData_ApplicationData.notSet }; static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'ApplicationData', package: const $pb.PackageName(_omitMessageNames ? '' : 'client_to_server'), createEmptyInstance: create) - ..oo(0, [1, 2, 3, 4, 5, 6]) + ..oo(0, [1, 2, 3, 4, 5, 6, 7]) ..aOM(1, _omitFieldNames ? '' : 'textmessage', subBuilder: ApplicationData_TextMessage.create) ..aOM(2, _omitFieldNames ? '' : 'getuserbyusername', subBuilder: ApplicationData_GetUserByUsername.create) ..aOM(3, _omitFieldNames ? '' : 'getprekeysbyuserid', subBuilder: ApplicationData_GetPrekeysByUserId.create) ..aOM(4, _omitFieldNames ? '' : 'getuploadtoken', subBuilder: ApplicationData_GetUploadToken.create) ..aOM(5, _omitFieldNames ? '' : 'uploaddata', subBuilder: ApplicationData_UploadData.create) ..aOM(6, _omitFieldNames ? '' : 'getuserbyid', subBuilder: ApplicationData_GetUserById.create) + ..aOM(7, _omitFieldNames ? '' : 'downloaddata', subBuilder: ApplicationData_DownloadData.create) ..hasRequiredFields = false ; @@ -1022,6 +1061,17 @@ class ApplicationData extends $pb.GeneratedMessage { void clearGetuserbyid() => clearField(6); @$pb.TagNumber(6) ApplicationData_GetUserById ensureGetuserbyid() => $_ensure(5); + + @$pb.TagNumber(7) + ApplicationData_DownloadData get downloaddata => $_getN(6); + @$pb.TagNumber(7) + set downloaddata(ApplicationData_DownloadData v) { setField(7, v); } + @$pb.TagNumber(7) + $core.bool hasDownloaddata() => $_has(6); + @$pb.TagNumber(7) + void clearDownloaddata() => clearField(7); + @$pb.TagNumber(7) + ApplicationData_DownloadData ensureDownloaddata() => $_ensure(6); } class Response_PreKey extends $pb.GeneratedMessage { diff --git a/lib/src/proto/api/client_to_server.pbjson.dart b/lib/src/proto/api/client_to_server.pbjson.dart index 3c52508..d4ef740 100644 --- a/lib/src/proto/api/client_to_server.pbjson.dart +++ b/lib/src/proto/api/client_to_server.pbjson.dart @@ -117,12 +117,13 @@ const ApplicationData$json = { '2': [ {'1': 'textmessage', '3': 1, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.TextMessage', '9': 0, '10': 'textmessage'}, {'1': 'getuserbyusername', '3': 2, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.GetUserByUsername', '9': 0, '10': 'getuserbyusername'}, - {'1': 'getuserbyid', '3': 6, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.GetUserById', '9': 0, '10': 'getuserbyid'}, {'1': 'getprekeysbyuserid', '3': 3, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.GetPrekeysByUserId', '9': 0, '10': 'getprekeysbyuserid'}, {'1': 'getuploadtoken', '3': 4, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.GetUploadToken', '9': 0, '10': 'getuploadtoken'}, {'1': 'uploaddata', '3': 5, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.UploadData', '9': 0, '10': 'uploaddata'}, + {'1': 'getuserbyid', '3': 6, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.GetUserById', '9': 0, '10': 'getuserbyid'}, + {'1': 'downloaddata', '3': 7, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.DownloadData', '9': 0, '10': 'downloaddata'}, ], - '3': [ApplicationData_TextMessage$json, ApplicationData_GetUserByUsername$json, ApplicationData_GetUserById$json, ApplicationData_GetPrekeysByUserId$json, ApplicationData_GetUploadToken$json, ApplicationData_UploadData$json], + '3': [ApplicationData_TextMessage$json, ApplicationData_GetUserByUsername$json, ApplicationData_GetUserById$json, ApplicationData_GetPrekeysByUserId$json, ApplicationData_GetUploadToken$json, ApplicationData_UploadData$json, ApplicationData_DownloadData$json], '8': [ {'1': 'ApplicationData'}, ], @@ -164,9 +165,6 @@ const ApplicationData_GetPrekeysByUserId$json = { @$core.Deprecated('Use applicationDataDescriptor instead') const ApplicationData_GetUploadToken$json = { '1': 'GetUploadToken', - '2': [ - {'1': 'len', '3': 1, '4': 1, '5': 13, '10': 'len'}, - ], }; @$core.Deprecated('Use applicationDataDescriptor instead') @@ -179,25 +177,35 @@ const ApplicationData_UploadData$json = { ], }; +@$core.Deprecated('Use applicationDataDescriptor instead') +const ApplicationData_DownloadData$json = { + '1': 'DownloadData', + '2': [ + {'1': 'upload_token', '3': 1, '4': 1, '5': 12, '10': 'uploadToken'}, + ], +}; + /// Descriptor for `ApplicationData`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List applicationDataDescriptor = $convert.base64Decode( 'Cg9BcHBsaWNhdGlvbkRhdGESUQoLdGV4dG1lc3NhZ2UYASABKAsyLS5jbGllbnRfdG9fc2Vydm' 'VyLkFwcGxpY2F0aW9uRGF0YS5UZXh0TWVzc2FnZUgAUgt0ZXh0bWVzc2FnZRJjChFnZXR1c2Vy' 'Ynl1c2VybmFtZRgCIAEoCzIzLmNsaWVudF90b19zZXJ2ZXIuQXBwbGljYXRpb25EYXRhLkdldF' - 'VzZXJCeVVzZXJuYW1lSABSEWdldHVzZXJieXVzZXJuYW1lElEKC2dldHVzZXJieWlkGAYgASgL' - 'Mi0uY2xpZW50X3RvX3NlcnZlci5BcHBsaWNhdGlvbkRhdGEuR2V0VXNlckJ5SWRIAFILZ2V0dX' - 'NlcmJ5aWQSZgoSZ2V0cHJla2V5c2J5dXNlcmlkGAMgASgLMjQuY2xpZW50X3RvX3NlcnZlci5B' - 'cHBsaWNhdGlvbkRhdGEuR2V0UHJla2V5c0J5VXNlcklkSABSEmdldHByZWtleXNieXVzZXJpZB' - 'JaCg5nZXR1cGxvYWR0b2tlbhgEIAEoCzIwLmNsaWVudF90b19zZXJ2ZXIuQXBwbGljYXRpb25E' - 'YXRhLkdldFVwbG9hZFRva2VuSABSDmdldHVwbG9hZHRva2VuEk4KCnVwbG9hZGRhdGEYBSABKA' - 'syLC5jbGllbnRfdG9fc2VydmVyLkFwcGxpY2F0aW9uRGF0YS5VcGxvYWREYXRhSABSCnVwbG9h' - 'ZGRhdGEaOgoLVGV4dE1lc3NhZ2USFwoHdXNlcl9pZBgBIAEoA1IGdXNlcklkEhIKBGJvZHkYAy' - 'ABKAxSBGJvZHkaLwoRR2V0VXNlckJ5VXNlcm5hbWUSGgoIdXNlcm5hbWUYASABKAlSCHVzZXJu' - 'YW1lGiYKC0dldFVzZXJCeUlkEhcKB3VzZXJfaWQYASABKANSBnVzZXJJZBotChJHZXRQcmVrZX' - 'lzQnlVc2VySWQSFwoHdXNlcl9pZBgBIAEoA1IGdXNlcklkGiIKDkdldFVwbG9hZFRva2VuEhAK' - 'A2xlbhgBIAEoDVIDbGVuGlsKClVwbG9hZERhdGESIQoMdXBsb2FkX3Rva2VuGAEgASgMUgt1cG' - 'xvYWRUb2tlbhIWCgZvZmZzZXQYAiABKA1SBm9mZnNldBISCgRkYXRhGAMgASgMUgRkYXRhQhEK' - 'D0FwcGxpY2F0aW9uRGF0YQ=='); + 'VzZXJCeVVzZXJuYW1lSABSEWdldHVzZXJieXVzZXJuYW1lEmYKEmdldHByZWtleXNieXVzZXJp' + 'ZBgDIAEoCzI0LmNsaWVudF90b19zZXJ2ZXIuQXBwbGljYXRpb25EYXRhLkdldFByZWtleXNCeV' + 'VzZXJJZEgAUhJnZXRwcmVrZXlzYnl1c2VyaWQSWgoOZ2V0dXBsb2FkdG9rZW4YBCABKAsyMC5j' + 'bGllbnRfdG9fc2VydmVyLkFwcGxpY2F0aW9uRGF0YS5HZXRVcGxvYWRUb2tlbkgAUg5nZXR1cG' + 'xvYWR0b2tlbhJOCgp1cGxvYWRkYXRhGAUgASgLMiwuY2xpZW50X3RvX3NlcnZlci5BcHBsaWNh' + 'dGlvbkRhdGEuVXBsb2FkRGF0YUgAUgp1cGxvYWRkYXRhElEKC2dldHVzZXJieWlkGAYgASgLMi' + '0uY2xpZW50X3RvX3NlcnZlci5BcHBsaWNhdGlvbkRhdGEuR2V0VXNlckJ5SWRIAFILZ2V0dXNl' + 'cmJ5aWQSVAoMZG93bmxvYWRkYXRhGAcgASgLMi4uY2xpZW50X3RvX3NlcnZlci5BcHBsaWNhdG' + 'lvbkRhdGEuRG93bmxvYWREYXRhSABSDGRvd25sb2FkZGF0YRo6CgtUZXh0TWVzc2FnZRIXCgd1' + 'c2VyX2lkGAEgASgDUgZ1c2VySWQSEgoEYm9keRgDIAEoDFIEYm9keRovChFHZXRVc2VyQnlVc2' + 'VybmFtZRIaCgh1c2VybmFtZRgBIAEoCVIIdXNlcm5hbWUaJgoLR2V0VXNlckJ5SWQSFwoHdXNl' + 'cl9pZBgBIAEoA1IGdXNlcklkGi0KEkdldFByZWtleXNCeVVzZXJJZBIXCgd1c2VyX2lkGAEgAS' + 'gDUgZ1c2VySWQaEAoOR2V0VXBsb2FkVG9rZW4aWwoKVXBsb2FkRGF0YRIhCgx1cGxvYWRfdG9r' + 'ZW4YASABKAxSC3VwbG9hZFRva2VuEhYKBm9mZnNldBgCIAEoDVIGb2Zmc2V0EhIKBGRhdGEYAy' + 'ABKAxSBGRhdGEaMQoMRG93bmxvYWREYXRhEiEKDHVwbG9hZF90b2tlbhgBIAEoDFILdXBsb2Fk' + 'VG9rZW5CEQoPQXBwbGljYXRpb25EYXRh'); @$core.Deprecated('Use responseDescriptor instead') const Response$json = { diff --git a/lib/src/proto/api/server_to_client.pb.dart b/lib/src/proto/api/server_to_client.pb.dart index f5e0b43..df5a0f5 100644 --- a/lib/src/proto/api/server_to_client.pb.dart +++ b/lib/src/proto/api/server_to_client.pb.dart @@ -85,6 +85,7 @@ enum V0_Kind { response, newMessage, requestNewPreKeys, + downloaddata, notSet } @@ -94,6 +95,7 @@ class V0 extends $pb.GeneratedMessage { Response? response, NewMessage? newMessage, $core.bool? requestNewPreKeys, + DownloadData? downloaddata, }) { final $result = create(); if (seq != null) { @@ -108,6 +110,9 @@ class V0 extends $pb.GeneratedMessage { if (requestNewPreKeys != null) { $result.requestNewPreKeys = requestNewPreKeys; } + if (downloaddata != null) { + $result.downloaddata = downloaddata; + } return $result; } V0._() : super(); @@ -118,14 +123,16 @@ class V0 extends $pb.GeneratedMessage { 2 : V0_Kind.response, 3 : V0_Kind.newMessage, 4 : V0_Kind.requestNewPreKeys, + 5 : V0_Kind.downloaddata, 0 : V0_Kind.notSet }; static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'V0', package: const $pb.PackageName(_omitMessageNames ? '' : 'server_to_client'), createEmptyInstance: create) - ..oo(0, [2, 3, 4]) + ..oo(0, [2, 3, 4, 5]) ..a<$fixnum.Int64>(1, _omitFieldNames ? '' : 'seq', $pb.PbFieldType.OU6, defaultOrMaker: $fixnum.Int64.ZERO) ..aOM(2, _omitFieldNames ? '' : 'response', subBuilder: Response.create) ..aOM(3, _omitFieldNames ? '' : 'newMessage', protoName: 'newMessage', subBuilder: NewMessage.create) ..aOB(4, _omitFieldNames ? '' : 'RequestNewPreKeys', protoName: 'RequestNewPreKeys') + ..aOM(5, _omitFieldNames ? '' : 'downloaddata', subBuilder: DownloadData.create) ..hasRequiredFields = false ; @@ -192,6 +199,17 @@ class V0 extends $pb.GeneratedMessage { $core.bool hasRequestNewPreKeys() => $_has(3); @$pb.TagNumber(4) void clearRequestNewPreKeys() => clearField(4); + + @$pb.TagNumber(5) + DownloadData get downloaddata => $_getN(4); + @$pb.TagNumber(5) + set downloaddata(DownloadData v) { setField(5, v); } + @$pb.TagNumber(5) + $core.bool hasDownloaddata() => $_has(4); + @$pb.TagNumber(5) + void clearDownloaddata() => clearField(5); + @$pb.TagNumber(5) + DownloadData ensureDownloaddata() => $_ensure(4); } class NewMessage extends $pb.GeneratedMessage { @@ -258,6 +276,84 @@ class NewMessage extends $pb.GeneratedMessage { void clearFromUserId() => clearField(2); } +class DownloadData extends $pb.GeneratedMessage { + factory DownloadData({ + $core.List<$core.int>? uploadToken, + $core.int? offset, + $core.List<$core.int>? data, + }) { + final $result = create(); + if (uploadToken != null) { + $result.uploadToken = uploadToken; + } + if (offset != null) { + $result.offset = offset; + } + if (data != null) { + $result.data = data; + } + return $result; + } + DownloadData._() : super(); + factory DownloadData.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory DownloadData.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'DownloadData', package: const $pb.PackageName(_omitMessageNames ? '' : 'server_to_client'), createEmptyInstance: create) + ..a<$core.List<$core.int>>(1, _omitFieldNames ? '' : 'uploadToken', $pb.PbFieldType.OY) + ..a<$core.int>(2, _omitFieldNames ? '' : 'offset', $pb.PbFieldType.OU3) + ..a<$core.List<$core.int>>(3, _omitFieldNames ? '' : 'data', $pb.PbFieldType.OY) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + DownloadData clone() => DownloadData()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + DownloadData copyWith(void Function(DownloadData) updates) => super.copyWith((message) => updates(message as DownloadData)) as DownloadData; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static DownloadData create() => DownloadData._(); + DownloadData createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static DownloadData getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static DownloadData? _defaultInstance; + + @$pb.TagNumber(1) + $core.List<$core.int> get uploadToken => $_getN(0); + @$pb.TagNumber(1) + set uploadToken($core.List<$core.int> v) { $_setBytes(0, v); } + @$pb.TagNumber(1) + $core.bool hasUploadToken() => $_has(0); + @$pb.TagNumber(1) + void clearUploadToken() => clearField(1); + + @$pb.TagNumber(2) + $core.int get offset => $_getIZ(1); + @$pb.TagNumber(2) + set offset($core.int v) { $_setUnsignedInt32(1, v); } + @$pb.TagNumber(2) + $core.bool hasOffset() => $_has(1); + @$pb.TagNumber(2) + void clearOffset() => clearField(2); + + @$pb.TagNumber(3) + $core.List<$core.int> get data => $_getN(2); + @$pb.TagNumber(3) + set data($core.List<$core.int> v) { $_setBytes(2, v); } + @$pb.TagNumber(3) + $core.bool hasData() => $_has(2); + @$pb.TagNumber(3) + void clearData() => clearField(3); +} + class Response_PreKey extends $pb.GeneratedMessage { factory Response_PreKey({ $fixnum.Int64? id, diff --git a/lib/src/proto/api/server_to_client.pbjson.dart b/lib/src/proto/api/server_to_client.pbjson.dart index 63a8c74..acfbda6 100644 --- a/lib/src/proto/api/server_to_client.pbjson.dart +++ b/lib/src/proto/api/server_to_client.pbjson.dart @@ -37,6 +37,7 @@ const V0$json = { {'1': 'response', '3': 2, '4': 1, '5': 11, '6': '.server_to_client.Response', '9': 0, '10': 'response'}, {'1': 'newMessage', '3': 3, '4': 1, '5': 11, '6': '.server_to_client.NewMessage', '9': 0, '10': 'newMessage'}, {'1': 'RequestNewPreKeys', '3': 4, '4': 1, '5': 8, '9': 0, '10': 'RequestNewPreKeys'}, + {'1': 'downloaddata', '3': 5, '4': 1, '5': 11, '6': '.server_to_client.DownloadData', '9': 0, '10': 'downloaddata'}, ], '8': [ {'1': 'Kind'}, @@ -48,7 +49,8 @@ final $typed_data.Uint8List v0Descriptor = $convert.base64Decode( 'CgJWMBIQCgNzZXEYASABKARSA3NlcRI4CghyZXNwb25zZRgCIAEoCzIaLnNlcnZlcl90b19jbG' 'llbnQuUmVzcG9uc2VIAFIIcmVzcG9uc2USPgoKbmV3TWVzc2FnZRgDIAEoCzIcLnNlcnZlcl90' 'b19jbGllbnQuTmV3TWVzc2FnZUgAUgpuZXdNZXNzYWdlEi4KEVJlcXVlc3ROZXdQcmVLZXlzGA' - 'QgASgISABSEVJlcXVlc3ROZXdQcmVLZXlzQgYKBEtpbmQ='); + 'QgASgISABSEVJlcXVlc3ROZXdQcmVLZXlzEkQKDGRvd25sb2FkZGF0YRgFIAEoCzIeLnNlcnZl' + 'cl90b19jbGllbnQuRG93bmxvYWREYXRhSABSDGRvd25sb2FkZGF0YUIGCgRLaW5k'); @$core.Deprecated('Use newMessageDescriptor instead') const NewMessage$json = { @@ -64,6 +66,21 @@ final $typed_data.Uint8List newMessageDescriptor = $convert.base64Decode( 'CgpOZXdNZXNzYWdlEiAKDGZyb21fdXNlcl9pZBgCIAEoA1IKZnJvbVVzZXJJZBISCgRib2R5GA' 'EgASgMUgRib2R5'); +@$core.Deprecated('Use downloadDataDescriptor instead') +const DownloadData$json = { + '1': 'DownloadData', + '2': [ + {'1': 'upload_token', '3': 1, '4': 1, '5': 12, '10': 'uploadToken'}, + {'1': 'offset', '3': 2, '4': 1, '5': 13, '10': 'offset'}, + {'1': 'data', '3': 3, '4': 1, '5': 12, '10': 'data'}, + ], +}; + +/// Descriptor for `DownloadData`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List downloadDataDescriptor = $convert.base64Decode( + 'CgxEb3dubG9hZERhdGESIQoMdXBsb2FkX3Rva2VuGAEgASgMUgt1cGxvYWRUb2tlbhIWCgZvZm' + 'ZzZXQYAiABKA1SBm9mZnNldBISCgRkYXRhGAMgASgMUgRkYXRh'); + @$core.Deprecated('Use responseDescriptor instead') const Response$json = { '1': 'Response', diff --git a/lib/src/providers/api/api.dart b/lib/src/providers/api/api.dart new file mode 100644 index 0000000..3e2bf94 --- /dev/null +++ b/lib/src/providers/api/api.dart @@ -0,0 +1,107 @@ +import 'dart:io'; +import 'package:fixnum/fixnum.dart'; +import 'package:flutter/foundation.dart'; +import 'package:logging/logging.dart'; +import 'package:twonly/main.dart'; +import 'package:twonly/src/model/json/message.dart'; +import 'package:twonly/src/model/messages_model.dart'; +import 'package:twonly/src/proto/api/error.pb.dart'; +import 'package:twonly/src/providers/api/api_utils.dart'; +import 'package:twonly/src/utils/misc.dart'; +// ignore: library_prefixes +import 'package:twonly/src/utils/signal.dart' as SignalHelper; + +// this functions ensures that the message is received by the server and in case of errors will try again later +Future encryptAndSendMessage(Int64 userId, Message msg) async { + Uint8List? bytes = await SignalHelper.encryptMessage(msg, userId); + + if (bytes == null) { + Logger("api.dart").shout("Error encryption message!"); + return Result.error(ErrorCode.InternalError); + } + + Logger("api.dart").shout( + "TODO: store encrypted message and send later again. STORE: userId, bytes and messageId"); + + Result resp = await apiProvider.sendTextMessage(userId, bytes); + + if (resp.isSuccess) { + if (msg.messageId != null) { + DbMessages.acknowledgeMessageByServer(msg.messageId!); + } + // TODO: remove encrypted tmp file + } else { + // in case of error do nothing. As the message is not removed the app will try again when relaunched + } + + return resp; +} + +Future sendImageToSingleTarget(Int64 target, Uint8List imageBytes) async { + int? messageId = + await DbMessages.insertMyMessage(target.toInt(), MessageKind.image); + if (messageId == null) return; + + Result res = await apiProvider.getUploadToken(); + + if (res.isError || !res.value.hasUploadtoken()) { + Logger("api.dart").shout("Error getting upload token!"); + + // TODO store message for later and try again + + return null; + } + + List uploadToken = res.value.uploadtoken; + Logger("sendImageToSingleTarget").fine("Got token: $uploadToken"); + + MessageContent content = + MessageContent(text: null, downloadToken: uploadToken); + + Uint8List? encryptBytes = await SignalHelper.encryptBytes(imageBytes, target); + if (encryptBytes == null) { + await DbMessages.deleteMessageById(messageId); + Logger("api.dart").shout("Error encrypting image! Deleting image."); + return; + } + + List? imageToken = + await apiProvider.uploadData(uploadToken, encryptBytes); + if (imageToken == null) { + Logger("api.dart").shout("handle error uploading like saving..."); + return; + } + + print("TODO: insert into DB and then create this MESSAGE"); + + Message msg = Message( + kind: MessageKind.image, + messageId: messageId, + content: content, + timestamp: DateTime.now(), + ); + + await encryptAndSendMessage(target, msg); +} + +Future sendImage(List userIds, String imagePath) async { + // 1. set notifier provider + + File imageFile = File(imagePath); + + Uint8List? imageBytes = await getCompressedImage(imageFile); + if (imageBytes == null) { + Logger("api.dart").shout("Error compressing image!"); + return; + } + + for (int i = 0; i < userIds.length; i++) { + sendImageToSingleTarget(userIds[i], imageBytes); + } +} + +Future tryDownloadMedia(List imageToken, {bool force = false}) async { + print("check if free network connection"); + + print("Downloading: " + imageToken.toString()); +} diff --git a/lib/src/providers/api/api_utils.dart b/lib/src/providers/api/api_utils.dart new file mode 100644 index 0000000..49b9ead --- /dev/null +++ b/lib/src/providers/api/api_utils.dart @@ -0,0 +1,42 @@ +import 'package:fixnum/fixnum.dart'; +import 'package:twonly/src/proto/api/client_to_server.pb.dart' as client; +import 'package:twonly/src/proto/api/client_to_server.pbserver.dart'; +import 'package:twonly/src/proto/api/error.pb.dart'; +import 'package:twonly/src/proto/api/server_to_client.pb.dart' as server; + +class Result { + final T? value; + final E? error; + + bool get isSuccess => value != null; + bool get isError => error != null; + + Result.success(this.value) : error = null; + Result.error(this.error) : value = null; +} + +Result asResult(server.ServerToClient? msg) { + if (msg == null) { + return Result.error(ErrorCode.InternalError); + } + if (msg.v0.response.hasOk()) { + return Result.success(msg.v0.response.ok); + } else { + return Result.error(msg.v0.response.error); + } +} + +ClientToServer createClientToServerFromHandshake(Handshake handshake) { + var v0 = client.V0() + ..seq = Int64(0) + ..handshake = handshake; + return ClientToServer()..v0 = v0; +} + +ClientToServer createClientToServerFromApplicationData( + ApplicationData applicationData) { + var v0 = client.V0() + ..seq = Int64(0) + ..applicationdata = applicationData; + return ClientToServer()..v0 = v0; +} diff --git a/lib/src/providers/api/server_messages.dart b/lib/src/providers/api/server_messages.dart new file mode 100644 index 0000000..b50e1f1 --- /dev/null +++ b/lib/src/providers/api/server_messages.dart @@ -0,0 +1,99 @@ +import 'dart:convert'; +import 'package:fixnum/fixnum.dart'; +import 'package:flutter/foundation.dart'; +import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; +import 'package:logging/logging.dart'; +import 'package:twonly/main.dart'; +import 'package:twonly/src/model/contacts_model.dart'; +import 'package:twonly/src/model/json/message.dart'; +import 'package:twonly/src/model/messages_model.dart'; +import 'package:twonly/src/proto/api/client_to_server.pb.dart' as client; +import 'package:twonly/src/proto/api/client_to_server.pbserver.dart'; +import 'package:twonly/src/proto/api/server_to_client.pb.dart' as server; +import 'package:twonly/src/providers/api/api.dart'; +import 'package:twonly/src/providers/api/api_utils.dart'; +// ignore: library_prefixes +import 'package:twonly/src/utils/signal.dart' as SignalHelper; + +Future handleServerMessage(server.ServerToClient msg) async { + client.Response? response; + + if (msg.v0.hasRequestNewPreKeys()) { + List localPreKeys = await SignalHelper.getPreKeys(); + + List prekeysList = []; + for (int i = 0; i < localPreKeys.length; i++) { + prekeysList.add(client.Response_PreKey() + ..id = Int64(localPreKeys[i].id) + ..prekey = localPreKeys[i].getKeyPair().publicKey.serialize()); + } + var prekeys = client.Response_Prekeys(prekeys: prekeysList); + var ok = client.Response_Ok()..prekeys = prekeys; + response = client.Response()..ok = ok; + } else if (msg.v0.hasNewMessage()) { + Uint8List body = Uint8List.fromList(msg.v0.newMessage.body); + Int64 fromUserId = msg.v0.newMessage.fromUserId; + Message? message = await SignalHelper.getDecryptedText(fromUserId, body); + if (message != null) { + switch (message.kind) { + case MessageKind.contactRequest: + Result username = await apiProvider.getUsername(fromUserId); + if (username.isSuccess) { + Uint8List name = username.value.userdata.username; + DbContacts.insertNewContact( + utf8.decode(name), fromUserId.toInt(), true); + } + break; + case MessageKind.rejectRequest: + DbContacts.deleteUser(fromUserId.toInt()); + break; + case MessageKind.acceptRequest: + DbContacts.acceptUser(fromUserId.toInt()); + break; + case MessageKind.ack: + DbMessages.acknowledgeMessageByUser( + fromUserId.toInt(), message.messageId!); + break; + default: + if (message.kind != MessageKind.textMessage && + message.kind != MessageKind.video && + message.kind != MessageKind.image) { + Logger("handleServerMessages") + .shout("Got unknown MessageKind $message"); + } else { + String content = jsonEncode(message.content!.toJson()); + await DbMessages.insertOtherMessage( + fromUserId.toInt(), message.kind, message.messageId!, content); + + encryptAndSendMessage( + fromUserId, + Message( + kind: MessageKind.ack, + messageId: message.messageId!, + timestamp: DateTime.now(), + ), + ); + + if (message.kind == MessageKind.video || + message.kind == MessageKind.image) { + dynamic content = message.content!; + List downloadToken = content.downloadToken; + tryDownloadMedia(downloadToken); + } + } + } + } + var ok = client.Response_Ok()..none = true; + response = client.Response()..ok = ok; + } else { + Logger("handleServerMessage") + .shout("Got a new message from the server: $msg"); + return; + } + + var v0 = client.V0() + ..seq = msg.v0.seq + ..response = response; + + apiProvider.sendResponse(ClientToServer()..v0 = v0); +} diff --git a/lib/src/providers/api_provider.dart b/lib/src/providers/api_provider.dart index 65d310b..b7d61f3 100644 --- a/lib/src/providers/api_provider.dart +++ b/lib/src/providers/api_provider.dart @@ -1,18 +1,15 @@ +import 'dart:async'; import 'dart:collection'; -import 'dart:convert'; import 'dart:math'; import 'package:fixnum/fixnum.dart'; import 'package:flutter/foundation.dart'; -import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; import 'package:logging/logging.dart'; -import 'package:twonly/src/model/contacts_model.dart'; -import 'package:twonly/src/model/json/message.dart'; -import 'package:twonly/src/model/messages_model.dart'; -import 'package:twonly/src/proto/api/client_to_server.pb.dart' as client; +import 'package:twonly/src/app.dart'; import 'package:twonly/src/proto/api/client_to_server.pbserver.dart'; import 'package:twonly/src/proto/api/error.pb.dart'; import 'package:twonly/src/proto/api/server_to_client.pb.dart' as server; -import 'package:twonly/src/utils/api.dart'; +import 'package:twonly/src/providers/api/api_utils.dart'; +import 'package:twonly/src/providers/api/server_messages.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/storage.dart'; // ignore: library_prefixes @@ -22,30 +19,21 @@ import 'package:web_socket_channel/io.dart'; import 'package:libsignal_protocol_dart/src/ecc/ed25519.dart'; import 'package:web_socket_channel/web_socket_channel.dart'; -class Result { - final T? value; - final E? error; - - bool get isSuccess => value != null; - bool get isError => error != null; - - Result.success(this.value) : error = null; - Result.error(this.error) : value = null; -} - +/// The ApiProvider is responsible for communicating with the server. +/// It handles errors and does automatically tries to reconnect on +/// errors or network changes. class ApiProvider { - ApiProvider({required this.apiUrl, required this.backupApiUrl}); - final String apiUrl; final String? backupApiUrl; + ApiProvider({required this.apiUrl, required this.backupApiUrl}); + + final log = Logger("ApiProvider"); + + // reconnection params + Timer? reconnectionTimer; int _reconnectionDelay = 5; - bool _tryingToConnect = false; - final log = Logger("api_provider"); - Function(bool)? _connectionStateCallback; - Function? _updatedContacts; final HashMap messagesV0 = HashMap(); - IOWebSocketChannel? _channel; Future _connectTo(String apiUrl) async { @@ -68,19 +56,23 @@ class ApiProvider { if (_channel != null && _channel!.closeCode != null) { return true; } + // ensure that the connect function is not called again by the timer. + if (reconnectionTimer != null) { + reconnectionTimer!.cancel(); + } log.info("Trying to connect to the backend $apiUrl!"); if (await _connectTo(apiUrl)) { await authenticate(); - if (_connectionStateCallback != null) _connectionStateCallback!(true); + globalCallbackConnectionState(true); _reconnectionDelay = 5; return true; } if (backupApiUrl != null) { log.info("Trying to connect to the backup backend $backupApiUrl!"); if (await _connectTo(backupApiUrl!)) { + globalCallbackConnectionState(true); await authenticate(); - if (_connectionStateCallback != null) _connectionStateCallback!(true); _reconnectionDelay = 5; return true; } @@ -91,42 +83,35 @@ class ApiProvider { bool get isConnected => _channel != null && _channel!.closeCode != null; void _onDone() { - if (_connectionStateCallback != null) { - _connectionStateCallback!(false); - } + globalCallbackConnectionState(false); _channel = null; tryToReconnect(); } void _onError(dynamic e) { - if (_connectionStateCallback != null) { - _connectionStateCallback!(false); - } + globalCallbackConnectionState(false); _channel = null; tryToReconnect(); } - void setConnectionStateCallback(Function(bool) callBack) { - _connectionStateCallback = callBack; - } - - void setUpdatedContacts(Function callBack) { - _updatedContacts = callBack; - } - void tryToReconnect() { - if (_tryingToConnect) return; - _tryingToConnect = true; - Future.delayed(Duration(seconds: _reconnectionDelay)).then( - (value) async { - _tryingToConnect = false; - _reconnectionDelay = _reconnectionDelay + 2; - if (_reconnectionDelay > 20) { - _reconnectionDelay = 20; - } - await connect(); - }, - ); + if (reconnectionTimer != null) { + reconnectionTimer!.cancel(); + } + + final int randomDelay = Random().nextInt(20); + final int delay = _reconnectionDelay + randomDelay; + + debugPrint("Delay reconnection $delay"); + + reconnectionTimer = Timer(Duration(seconds: delay), () async { + // increase delay but set a maximum of 60 seconds (including the random delay) + _reconnectionDelay = _reconnectionDelay * 2; + if (_reconnectionDelay > 40) { + _reconnectionDelay = 40; + } + await connect(); + }); } void _onData(dynamic msgBuffer) { @@ -135,103 +120,13 @@ class ApiProvider { if (msg.v0.hasResponse()) { messagesV0[msg.v0.seq] = msg; } else { - _handleServerMessage(msg); + handleServerMessage(msg); } } catch (e) { log.shout("Error parsing the servers message: $e"); } } - Future _handleServerMessage(server.ServerToClient msg) async { - client.Response? response; - - if (msg.v0.hasRequestNewPreKeys()) { - List localPreKeys = await SignalHelper.getPreKeys(); - - List prekeysList = []; - for (int i = 0; i < localPreKeys.length; i++) { - prekeysList.add(client.Response_PreKey() - ..id = Int64(localPreKeys[i].id) - ..prekey = localPreKeys[i].getKeyPair().publicKey.serialize()); - } - var prekeys = client.Response_Prekeys(prekeys: prekeysList); - var ok = client.Response_Ok()..prekeys = prekeys; - response = client.Response()..ok = ok; - } else if (msg.v0.hasNewMessage()) { - Uint8List body = Uint8List.fromList(msg.v0.newMessage.body); - Int64 fromUserId = msg.v0.newMessage.fromUserId; - Message? message = await SignalHelper.getDecryptedText(fromUserId, body); - if (message != null) { - switch (message.kind) { - case MessageKind.contactRequest: - Result username = await getUsername(fromUserId); - if (username.isSuccess) { - Uint8List name = username.value.userdata.username; - DbContacts.insertNewContact( - utf8.decode(name), fromUserId.toInt(), true); - } - break; - case MessageKind.rejectRequest: - DbContacts.deleteUser(fromUserId.toInt()); - break; - case MessageKind.acceptRequest: - DbContacts.acceptUser(fromUserId.toInt()); - break; - case MessageKind.ack: - DbMessages.acknowledgeMessage( - fromUserId.toInt(), message.messageId!); - break; - default: - if (message.kind != MessageKind.textMessage && - message.kind != MessageKind.video && - message.kind != MessageKind.image) { - log.shout("Got unknown MessageKind $message"); - } else { - String content = jsonEncode(message.content!.toJson()); - await DbMessages.insertOtherMessage(fromUserId.toInt(), - message.kind, message.messageId!, content); - - encryptAndSendMessage( - fromUserId, - Message( - kind: MessageKind.ack, - messageId: message.messageId!, - timestamp: DateTime.now(), - ), - ); - - if (message.kind == MessageKind.video || - message.kind == MessageKind.image) { - dynamic content = message.content!; - List downloadToken = content.downloadToken; - tryDownloadMedia(downloadToken); - } - } - } - } - updateNotifier(); - var ok = client.Response_Ok()..none = true; - response = client.Response()..ok = ok; - } else { - log.shout("Got a new message from the server: $msg"); - return; - } - - var v0 = client.V0() - ..seq = msg.v0.seq - ..response = response; - var res = ClientToServer()..v0 = v0; - - final resBytes = res.writeToBuffer(); - _channel!.sink.add(resBytes); - } - - Future updateNotifier() async { - if (_updatedContacts != null) { - _updatedContacts!(); - } - } - Future _waitForResponse(Int64 seq) async { final startTime = DateTime.now(); @@ -251,9 +146,13 @@ class ApiProvider { } } - Future _sendRequestV0(ClientToServer request) async { + Future sendResponse(ClientToServer response) async { + _channel!.sink.add(response.writeToBuffer()); + } + + Future _sendRequestV0(ClientToServer request) async { if (_channel == null) { - return null; + return Result.error(ErrorCode.InternalError); } var seq = Int64(Random().nextInt(4294967296)); while (messagesV0.containsKey(seq)) { @@ -265,30 +164,7 @@ class ApiProvider { _channel!.sink.add(requestBytes); - return await _waitForResponse(seq); - } - - ClientToServer createClientToServerFromHandshake(Handshake handshake) { - var v0 = client.V0() - ..seq = Int64(0) - ..handshake = handshake; - return ClientToServer()..v0 = v0; - } - - ClientToServer createClientToServerFromApplicationData( - ApplicationData applicationData) { - var v0 = client.V0() - ..seq = Int64(0) - ..applicationdata = applicationData; - return ClientToServer()..v0 = v0; - } - - Result _asResult(server.ServerToClient msg) { - if (msg.v0.response.hasOk()) { - return Result.success(msg.v0.response.ok); - } else { - return Result.error(msg.v0.response.error); - } + return asResult(await _waitForResponse(seq)); } Future authenticate() async { @@ -299,12 +175,7 @@ class ApiProvider { var handshake = Handshake()..getchallenge = Handshake_GetChallenge(); var req = createClientToServerFromHandshake(handshake); - final resp = await _sendRequestV0(req); - if (resp == null) { - log.shout("Server is not reachable!"); - return; - } - final result = _asResult(resp); + final result = await _sendRequestV0(req); if (result.isError) { log.shout(result); return; @@ -328,12 +199,7 @@ class ApiProvider { var req2 = createClientToServerFromHandshake(opensession); - final resp2 = await _sendRequestV0(req2); - if (resp2 == null) { - log.shout("Server is not reachable!"); - return; - } - final result2 = _asResult(resp2); + final result2 = await _sendRequestV0(req2); if (result2.isError) { log.shout(result2); return; @@ -370,33 +236,21 @@ class ApiProvider { var handshake = Handshake()..register = register; var req = createClientToServerFromHandshake(handshake); - final resp = await _sendRequestV0(req); - if (resp == null) { - return Result.error(ErrorCode.InternalError); - } - return _asResult(resp); + return await _sendRequestV0(req); } Future getUsername(Int64 userId) async { var get = ApplicationData_GetUserById()..userId = userId; var appData = ApplicationData()..getuserbyid = get; var req = createClientToServerFromApplicationData(appData); - final resp = await _sendRequestV0(req); - if (resp == null) { - return Result.error(ErrorCode.InternalError); - } - return _asResult(resp); + return await _sendRequestV0(req); } - Future getUploadToken(int size) async { - var get = ApplicationData_GetUploadToken()..len = size; + Future getUploadToken() async { + var get = ApplicationData_GetUploadToken(); var appData = ApplicationData()..getuploadtoken = get; var req = createClientToServerFromApplicationData(appData); - final resp = await _sendRequestV0(req); - if (resp == null) { - return Result.error(ErrorCode.InternalError); - } - return _asResult(resp); + return await _sendRequestV0(req); } Future?> uploadData(List uploadToken, Uint8List data) async { @@ -409,23 +263,15 @@ class ApiProvider { var appData = ApplicationData()..uploaddata = get; var req = createClientToServerFromApplicationData(appData); - final resp = await _sendRequestV0(req); - if (resp == null) { - return null; - } - return _asResult(resp).isSuccess ? uploadToken : null; + final result = await _sendRequestV0(req); + return result.isSuccess ? uploadToken : null; } Future getUserData(String username) async { var get = ApplicationData_GetUserByUsername()..username = username; var appData = ApplicationData()..getuserbyusername = get; var req = createClientToServerFromApplicationData(appData); - - final resp = await _sendRequestV0(req); - if (resp == null) { - return Result.error(ErrorCode.InternalError); - } - return _asResult(resp); + return await _sendRequestV0(req); } Future sendTextMessage(Int64 target, Uint8List msg) async { @@ -436,10 +282,6 @@ class ApiProvider { var appData = ApplicationData()..textmessage = testMessage; var req = createClientToServerFromApplicationData(appData); - final resp = await _sendRequestV0(req); - if (resp == null) { - return Result.error(ErrorCode.InternalError); - } - return _asResult(resp); + return await _sendRequestV0(req); } } diff --git a/lib/src/providers/notify_provider.dart b/lib/src/providers/contacts_change_provider.dart similarity index 53% rename from lib/src/providers/notify_provider.dart rename to lib/src/providers/contacts_change_provider.dart index bf9d570..9904176 100644 --- a/lib/src/providers/notify_provider.dart +++ b/lib/src/providers/contacts_change_provider.dart @@ -1,50 +1,26 @@ -import 'dart:collection'; - -import 'package:fixnum/fixnum.dart'; import 'package:flutter/foundation.dart'; import 'package:twonly/src/model/contacts_model.dart'; import 'package:twonly/src/model/messages_model.dart'; -/// Mix-in [DiagnosticableTreeMixin] to have access to [debugFillProperties] for the devtool -// ignore: prefer_mixin -class NotifyProvider with ChangeNotifier, DiagnosticableTreeMixin { +// This provider will update the UI on changes in the contact list +class ContactChangeProvider with ChangeNotifier, DiagnosticableTreeMixin { // The page index of the HomeView widget - int _activePageIdx = 0; + // int _activePageIdx = 0; List _allContacts = []; final Map _lastMessagesGroupedByUser = {}; - final List _sendingCurrentlyTo = []; - int get newContactRequests => _allContacts .where((contact) => !contact.accepted && contact.requested) .length; List get allContacts => _allContacts; - Map get lastMessagesGroupedByUser => - _lastMessagesGroupedByUser; - HashSet get sendingCurrentlyTo => - HashSet.from(_sendingCurrentlyTo); - int get activePageIdx => _activePageIdx; + // int get activePageIdx => _activePageIdx; - void setActivePageIdx(int idx) { - _activePageIdx = idx; - notifyListeners(); - } - - void addSendingTo(Int64 user) { - _sendingCurrentlyTo.add(user); - notifyListeners(); - } - - // removes the first occurrence of the user - void removeSendingTo(Int64 user) { - int index = _sendingCurrentlyTo.indexOf(user); - if (index != -1) { - _sendingCurrentlyTo.removeAt(index); - } - notifyListeners(); - } + // void setActivePageIdx(int idx) { + // _activePageIdx = idx; + // notifyListeners(); + // } void update() async { _allContacts = await DbContacts.getUsers(); diff --git a/lib/src/providers/messages_change_provider.dart b/lib/src/providers/messages_change_provider.dart new file mode 100644 index 0000000..65abdfa --- /dev/null +++ b/lib/src/providers/messages_change_provider.dart @@ -0,0 +1,31 @@ +import 'package:flutter/foundation.dart'; +import 'package:twonly/src/model/contacts_model.dart'; +import 'package:twonly/src/model/messages_model.dart'; + +/// This provider does always contains the latest messages send or received +/// for every contact. +class MessagesChangeProvider with ChangeNotifier, DiagnosticableTreeMixin { + final Map _lastMessage = {}; + + Map get lastMessage => _lastMessage; + + void updateLastMessageFor(int targetUserId) async { + DbMessage? last = + await DbMessages.getLastMessagesForPreviewForUser(targetUserId); + if (last != null) { + _lastMessage[last.otherUserId] = last; + } + } + + void init() async { + // load everything from the database + List allContacts = await DbContacts.getUsers(); + for (Contact contact in allContacts) { + DbMessage? last = await DbMessages.getLastMessagesForPreviewForUser( + contact.userId.toInt()); + if (last != null) { + _lastMessage[last.otherUserId] = last; + } + } + } +} diff --git a/lib/src/utils/api.dart b/lib/src/utils/api.dart deleted file mode 100644 index 0a1ef22..0000000 --- a/lib/src/utils/api.dart +++ /dev/null @@ -1,169 +0,0 @@ -import 'dart:convert'; -import 'dart:io'; -import 'package:fixnum/fixnum.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:logging/logging.dart'; -import 'package:provider/provider.dart'; -import 'package:twonly/main.dart'; -import 'package:twonly/src/app.dart'; -import 'package:twonly/src/model/contacts_model.dart'; -import 'package:twonly/src/model/json/message.dart'; -import 'package:twonly/src/model/messages_model.dart'; -import 'package:twonly/src/proto/api/error.pb.dart'; -import 'package:twonly/src/providers/api_provider.dart'; -import 'package:twonly/src/providers/notify_provider.dart'; -import 'package:twonly/src/utils/misc.dart'; -// ignore: library_prefixes -import 'package:twonly/src/utils/signal.dart' as SignalHelper; -import 'package:twonly/src/model/json/user_data.dart'; - -Future addNewContact(String username) async { - final res = await apiProvider.getUserData(username); - - if (res.isSuccess) { - bool added = await DbContacts.insertNewContact( - username, res.value.userdata.userId.toInt(), false); - - if (!added) { - print("RETURN FALSE HIER!!!"); - return false; - } - - if (await SignalHelper.addNewContact(res.value.userdata)) { - Message msg = - Message(kind: MessageKind.contactRequest, timestamp: DateTime.now()); - - Uint8List? bytes = - await SignalHelper.encryptMessage(msg, res.value.userdata.userId); - - if (bytes == null) { - Logger("utils/api").shout("Error encryption message!"); - return res.error(ErrorCode.InternalError); - } - - Result resp = - await apiProvider.sendTextMessage(res.value.userdata.userId, bytes); - - return resp.isSuccess; - } - } - return res.isSuccess; -} - -Future encryptAndSendMessage(Int64 userId, Message msg) async { - Uint8List? bytes = await SignalHelper.encryptMessage(msg, userId); - - if (bytes == null) { - Logger("utils/api").shout("Error encryption message!"); - return Result.error(ErrorCode.InternalError); - } - - Result resp = await apiProvider.sendTextMessage(userId, bytes); - - if (resp.isError) { - // TODO: - Logger("encryptAndSendMessage") - .shout("handle errors here and store them in the database"); - } - - return resp; -} - -Future rejectUserRequest(Int64 userId) async { - Message msg = - Message(kind: MessageKind.rejectRequest, timestamp: DateTime.now()); - return encryptAndSendMessage(userId, msg); -} - -Future acceptUserRequest(Int64 userId) async { - Message msg = - Message(kind: MessageKind.acceptRequest, timestamp: DateTime.now()); - return encryptAndSendMessage(userId, msg); -} - -Future createNewUser(String username, String inviteCode) async { - final storage = getSecureStorage(); - - await SignalHelper.createIfNotExistsSignalIdentity(); - - final res = await apiProvider.register(username, inviteCode); - - if (res.isSuccess) { - Logger("create_new_user").info("Got user_id ${res.value} from server"); - final userData = UserData( - userId: res.value.userid, username: username, displayName: username); - storage.write(key: "user_data", value: jsonEncode(userData)); - } - - return res; -} - -Future sendImageToSingleTarget( - BuildContext context, Int64 target, Uint8List encryptBytes) async { - Result res = await apiProvider.getUploadToken(encryptBytes.length); - - if (res.isError || !res.value.hasUploadtoken()) { - Logger("api.dart").shout("Error getting upload token!"); - return null; - } - List uploadToken = res.value.uploadtoken; - Logger("sendImageToSingleTarget").info("Got token: $uploadToken"); - - MessageContent content = - MessageContent(text: null, downloadToken: uploadToken); - int? messageId = await DbMessages.insertMyMessage( - target.toInt(), MessageKind.image, jsonEncode(content.toJson())); - if (messageId == null) return; - - updateNotifyProvider(); - - List? imageToken = - await apiProvider.uploadData(uploadToken, encryptBytes); - if (imageToken == null) { - Logger("api.dart").shout("handle error uploading like saving..."); - return; - } - - print("TODO: insert into DB and then create this MESSAGE"); - - Message msg = Message( - kind: MessageKind.image, - messageId: messageId, - content: content, - timestamp: DateTime.now(), - ); - - await encryptAndSendMessage(target, msg); - removeSendingTo(target); -} - -Future sendImage( - BuildContext context, List users, String imagePath) async { - // 1. set notifier provider - - File imageFile = File(imagePath); - - Uint8List? imageBytes = await getCompressedImage(imageFile); - if (imageBytes == null) { - Logger("api.dart").shout("Error compressing image!"); - return; - } - - for (int i = 0; i < users.length; i++) { - Int64 target = users[i].userId; - Uint8List? encryptedImage = - await SignalHelper.encryptBytes(imageBytes, target); - if (encryptedImage == null) { - Logger("api.dart").shout("Error encrypting image!"); - continue; - } - sendImageToSingleTarget(context, target, encryptedImage); - } -} - -Future tryDownloadMedia(List imageToken, {bool force = false}) async { - print("check if free network connection"); - - print("Downloading: " + imageToken.toString()); -} diff --git a/lib/src/utils/misc.dart b/lib/src/utils/misc.dart index f1587b4..28858d8 100644 --- a/lib/src/utils/misc.dart +++ b/lib/src/utils/misc.dart @@ -1,11 +1,9 @@ import 'dart:io'; import 'dart:math'; import 'dart:typed_data'; -import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter_image_compress/flutter_image_compress.dart'; import 'package:gal/gal.dart'; -import 'package:image/image.dart'; import 'package:logging/logging.dart'; import 'package:path_provider/path_provider.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; @@ -127,7 +125,5 @@ Future getCompressedImage(File file) async { file.absolute.path, quality: 90, ); - print(file.lengthSync()); - print(result!.length); return result; } diff --git a/lib/src/views/chat_list_view.dart b/lib/src/views/chat_list_view.dart index 05fdf47..4026161 100644 --- a/lib/src/views/chat_list_view.dart +++ b/lib/src/views/chat_list_view.dart @@ -1,6 +1,3 @@ -import 'dart:collection'; - -import 'package:fixnum/fixnum.dart'; import 'package:provider/provider.dart'; import 'package:twonly/src/components/initialsavatar.dart'; import 'package:twonly/src/components/message_send_state_icon.dart'; @@ -8,13 +5,14 @@ import 'package:twonly/src/components/notification_badge.dart'; import 'package:twonly/src/components/user_context_menu.dart'; import 'package:twonly/src/model/contacts_model.dart'; import 'package:twonly/src/model/messages_model.dart'; -import 'package:twonly/src/providers/notify_provider.dart'; +import 'package:twonly/src/providers/contacts_change_provider.dart'; +import 'package:twonly/src/providers/messages_change_provider.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/chat_item_details_view.dart'; +import 'package:twonly/src/views/home_view.dart'; import 'package:twonly/src/views/search_username_view.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter/material.dart'; -import 'dart:async'; class ChatItem { const ChatItem( @@ -42,93 +40,81 @@ class _ChatListViewState extends State { @override Widget build(BuildContext context) { Map lastMessages = - context.watch().lastMessagesGroupedByUser; + context.watch().lastMessage; - HashSet sendingCurrentlyTo = - context.watch().sendingCurrentlyTo; + List allUsers = context.read().allContacts; - List activeUsers = context - .read() - .allContacts + List activeUsers = allUsers .where((x) => lastMessages.containsKey(x.userId.toInt())) .toList(); + return Scaffold( - appBar: AppBar( - title: Text(AppLocalizations.of(context)!.chatsTitle), - actions: [ - NotificationBadge( - count: context.watch().newContactRequests, - child: IconButton( - icon: Icon(Icons.person_add), - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => SearchUsernameView(), - ), - ); - }, + appBar: AppBar( + title: Text(AppLocalizations.of(context)!.chatsTitle), + actions: [ + NotificationBadge( + count: context.watch().newContactRequests, + child: IconButton( + icon: Icon(Icons.person_add), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => SearchUsernameView(), + ), + ); + }, + ), + ) + ], + ), + body: (activeUsers.isEmpty) + ? Center( + child: Padding( + padding: const EdgeInsets.all(10), + child: OutlinedButton.icon( + icon: Icon((allUsers.isEmpty) + ? Icons.person_add + : Icons.camera_alt), + onPressed: () { + (allUsers.isEmpty) + ? Navigator.push( + context, + MaterialPageRoute( + builder: (context) => SearchUsernameView(), + ), + ) + : globalUpdateOfHomeViewPageIndex(1); + }, + label: Text((allUsers.isEmpty) + ? AppLocalizations.of(context)! + .chatListViewSearchUserNameBtn + : AppLocalizations.of(context)! + .chatListViewSendFirstTwonly)), ), ) - ], - ), - body: Column( - children: [ - // if (sendingCurrentlyTo.isNotEmpty) - // Container( - // padding: EdgeInsets.symmetric(vertical: 10, horizontal: 10), - // child: ListTile( - // leading: Stack( - // // child: Stack( - // alignment: Alignment.center, - // children: [ - // CircularProgressIndicator( - // strokeWidth: 1, - // ), - // Icon( - // Icons.send, // Replace with your desired icon - // color: Theme.of(context).colorScheme.primary, - // size: 20, // Adjust the size as needed - // ), - // ], - // // ), - // ), - // title: Text(sendingCurrentlyTo - // .map((e) => _activeUsers - // .where((c) => c.userId == e) - // .map((c) => c.displayName)) - // .toList() - // .join(", ")), - // ), - // ), // - Expanded( - child: ListView.builder( - restorationId: 'chat_list_view', - itemCount: activeUsers.length, - itemBuilder: (BuildContext context, int index) { - final user = activeUsers[index]; - return UserListItem( - user: user, - lastMessage: lastMessages[user.userId.toInt()]!, - isSending: sendingCurrentlyTo.contains(user.userId), - ); - }, - ), - ) - ], - )); + : ListView.builder( + restorationId: 'chat_list_view', + itemCount: activeUsers.length, + itemBuilder: (BuildContext context, int index) { + final user = activeUsers[index]; + return UserListItem( + user: user, + lastMessage: lastMessages[user.userId.toInt()]!, + ); + }, + ), + ); } } class UserListItem extends StatefulWidget { final Contact user; final DbMessage lastMessage; - final bool isSending; const UserListItem({ super.key, required this.user, - required this.isSending, required this.lastMessage, }); @@ -146,11 +132,11 @@ class _UserListItem extends State { //_loadAsync(); } - Future _loadAsync() async { - // flames = await widget.user.getFlames(); - // lastMessageInSeconds = await widget.user.getLastMessageInSeconds(); - setState(() {}); - } + // Future _loadAsync() async { + // flames = await widget.user.getFlames(); + // lastMessageInSeconds = await widget.user.getLastMessageInSeconds(); + // setState(() {}); + // } @override Widget build(BuildContext context) { @@ -161,7 +147,7 @@ class _UserListItem extends State { .difference(widget.lastMessage.sendOrReceivedAt) .inSeconds; - if (widget.isSending) { + if (!widget.lastMessage.messageAcknowledgeByServer) { state = MessageSendState.sending; } else { if (widget.lastMessage.messageOtherId == null) { diff --git a/lib/src/views/home_view.dart b/lib/src/views/home_view.dart index b1b58cc..70d5053 100644 --- a/lib/src/views/home_view.dart +++ b/lib/src/views/home_view.dart @@ -1,14 +1,11 @@ import 'package:pie_menu/pie_menu.dart'; -import 'package:provider/provider.dart'; -import 'package:twonly/src/providers/notify_provider.dart'; - import 'camera_preview_view.dart'; import 'chat_list_view.dart'; import 'profile_view.dart'; import '../settings/settings_controller.dart'; import 'package:flutter/material.dart'; -final PageController homeViewPageController = PageController(initialPage: 0); +Function(int) globalUpdateOfHomeViewPageIndex = (a) {}; class HomeView extends StatefulWidget { const HomeView({super.key, required this.settingsController}); @@ -19,6 +16,27 @@ class HomeView extends StatefulWidget { } class HomeViewState extends State { + int activePageIdx = 0; + final PageController homeViewPageController = PageController(initialPage: 0); + + @override + void initState() { + super.initState(); + globalUpdateOfHomeViewPageIndex = (index) { + homeViewPageController.jumpToPage(index); + setState(() { + activePageIdx = index; + }); + }; + } + + @override + void dispose() { + // disable globalCallbacks to the flutter tree + globalUpdateOfHomeViewPageIndex = (a) {}; + super.dispose(); + } + @override Widget build(BuildContext context) { return PieCanvas( @@ -47,7 +65,7 @@ class HomeViewState extends State { body: PageView( controller: homeViewPageController, onPageChanged: (index) { - context.read().setActivePageIdx(index); + activePageIdx = index; }, children: [ ChatListView(), @@ -69,14 +87,16 @@ class HomeViewState extends State { BottomNavigationBarItem(icon: Icon(Icons.verified_user), label: ""), ], onTap: (int index) { - context.read().setActivePageIdx(index); + activePageIdx = index; setState(() { - homeViewPageController.animateToPage(index, - duration: const Duration(milliseconds: 100), - curve: Curves.bounceIn); + homeViewPageController.animateToPage( + index, + duration: const Duration(milliseconds: 100), + curve: Curves.bounceIn, + ); }); }, - currentIndex: context.watch().activePageIdx, + currentIndex: activePageIdx, ), ), ); diff --git a/lib/src/views/register_view.dart b/lib/src/views/register_view.dart index 2dac9b3..54556e6 100644 --- a/lib/src/views/register_view.dart +++ b/lib/src/views/register_view.dart @@ -1,9 +1,12 @@ +import 'dart:convert'; +import 'package:logging/logging.dart'; import 'package:twonly/main.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:twonly/src/utils/api.dart'; +import 'package:twonly/src/model/json/user_data.dart'; import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/utils/signal.dart'; class RegisterView extends StatefulWidget { const RegisterView({super.key, required this.callbackOnSuccess}); @@ -19,6 +22,41 @@ class _RegisterViewState extends State { bool _isTryingToRegister = false; + Future createNewUser() async { + String username = usernameController.text; + String inviteCode = inviteCodeController.text; + setState(() { + _isTryingToRegister = true; + }); + + final storage = getSecureStorage(); + + await createIfNotExistsSignalIdentity(); + + final res = await apiProvider.register(username, inviteCode); + + if (res.isSuccess) { + Logger("create_new_user").info("Got user_id ${res.value} from server"); + final userData = UserData( + userId: res.value.userid, username: username, displayName: username); + storage.write(key: "user_data", value: jsonEncode(userData)); + } + + setState(() { + _isTryingToRegister = false; + }); + + if (res.isSuccess) { + apiProvider.authenticate(); + widget.callbackOnSuccess(); + return; + } + + if (context.mounted) { + showAlertDialog(context, "Oh no!", errorCodeToText(context, res.error)); + } + } + @override Widget build(BuildContext context) { InputDecoration getInputDecoration(hintText) { @@ -117,23 +155,7 @@ class _RegisterViewState extends State { ) : Icon(Icons.group), onPressed: () async { - setState(() { - _isTryingToRegister = true; - }); - final res = await createNewUser( - usernameController.text, inviteCodeController.text); - setState(() { - _isTryingToRegister = false; - apiProvider.authenticate(); - }); - if (res.isSuccess) { - widget.callbackOnSuccess(); - return; - } - if (context.mounted) { - final errMsg = errorCodeToText(context, res.error); - showAlertDialog(context, "Oh no!", errMsg); - } + createNewUser(); }, style: ButtonStyle( padding: WidgetStateProperty.all( diff --git a/lib/src/views/search_username_view.dart b/lib/src/views/search_username_view.dart index 8dbd3dd..3dddb0d 100644 --- a/lib/src/views/search_username_view.dart +++ b/lib/src/views/search_username_view.dart @@ -2,12 +2,16 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:provider/provider.dart'; +import 'package:twonly/main.dart'; import 'package:twonly/src/components/headline.dart'; import 'package:twonly/src/components/initialsavatar.dart'; import 'package:twonly/src/model/contacts_model.dart'; -import 'package:twonly/src/providers/notify_provider.dart'; -import 'package:twonly/src/utils/api.dart'; +import 'package:twonly/src/model/json/message.dart'; +import 'package:twonly/src/providers/contacts_change_provider.dart'; +import 'package:twonly/src/providers/api/api.dart'; import 'package:twonly/src/views/register_view.dart'; +// ignore: library_prefixes +import 'package:twonly/src/utils/signal.dart' as SignalHelper; class SearchUsernameView extends StatefulWidget { const SearchUsernameView({super.key}); @@ -25,24 +29,30 @@ class _SearchUsernameView extends State { _isLoading = true; }); - final status = await addNewContact(searchUserName.text); + final res = await apiProvider.getUserData(searchUserName.text); + if (res.isSuccess) { + bool added = await DbContacts.insertNewContact( + searchUserName.text, + res.value.userdata.userId.toInt(), + false, + ); + + if (added) { + if (await SignalHelper.addNewContact(res.value.userdata)) { + encryptAndSendMessage( + res.value.userdata.userId, + Message( + kind: MessageKind.contactRequest, + timestamp: DateTime.now(), + ), + ); + } + } + } setState(() { _isLoading = false; }); - - if (context.mounted) { - if (status) { - context.read().update(); - } else if (context.mounted) { - showAlertDialog( - context, - AppLocalizations.of(context)!.searchUsernameNotFound, - AppLocalizations.of(context)! - .searchUsernameNotFoundLong(searchUserName.text)); - } - } - return status; } @override @@ -94,7 +104,7 @@ class _SearchUsernameView extends State { ), SizedBox(height: 30), if (context - .read() + .read() .allContacts .where((contact) => !contact.accepted) .isNotEmpty) @@ -122,6 +132,8 @@ class _SearchUsernameView extends State { } class ContactsListView extends StatefulWidget { + const ContactsListView({super.key}); + @override State createState() => _ContactsListViewState(); } @@ -130,7 +142,7 @@ class _ContactsListViewState extends State { @override Widget build(BuildContext context) { List contacts = context - .read() + .read() .allContacts .where((contact) => !contact.accepted) .toList(); @@ -143,46 +155,50 @@ class _ContactsListViewState extends State { leading: InitialsAvatar(displayName: contact.displayName), trailing: Row( mainAxisSize: MainAxisSize.min, - children: (!contact.requested) - ? [Text('Pending')] - : [ - Tooltip( - message: "Block the user without informing.", - child: IconButton( - icon: Icon(Icons.person_off_rounded, - color: const Color.fromARGB(164, 244, 67, 54)), - onPressed: () async { - await DbContacts.blockUser(contact.userId.toInt()); - if (context.mounted) { - context.read().update(); - } - }, + children: [ + if (!contact.requested) Text('Pending'), + if (contact.requested) ...[ + Tooltip( + message: "Block the user without informing.", + child: IconButton( + icon: Icon(Icons.person_off_rounded, + color: const Color.fromARGB(164, 244, 67, 54)), + onPressed: () async { + await DbContacts.blockUser(contact.userId.toInt()); + }, + ), + ), + Tooltip( + message: "Reject the request and let the requester know.", + child: IconButton( + icon: Icon(Icons.close, color: Colors.red), + onPressed: () async { + await DbContacts.deleteUser(contact.userId.toInt()); + encryptAndSendMessage( + contact.userId, + Message( + kind: MessageKind.rejectRequest, + timestamp: DateTime.now(), + ), + ); + }, + ), + ), + IconButton( + icon: Icon(Icons.check, color: Colors.green), + onPressed: () async { + await DbContacts.acceptUser(contact.userId.toInt()); + encryptAndSendMessage( + contact.userId, + Message( + kind: MessageKind.acceptRequest, + timestamp: DateTime.now(), ), - ), - Tooltip( - message: "Reject the request and let the requester know.", - child: IconButton( - icon: Icon(Icons.close, color: Colors.red), - onPressed: () async { - await DbContacts.deleteUser(contact.userId.toInt()); - if (context.mounted) { - context.read().update(); - } - rejectUserRequest(contact.userId); - }, - ), - ), - IconButton( - icon: Icon(Icons.check, color: Colors.green), - onPressed: () async { - await DbContacts.acceptUser(contact.userId.toInt()); - if (context.mounted) { - context.read().update(); - } - acceptUserRequest(contact.userId); - }, - ), - ], + ); + }, + ), + ], + ], ), ); }, diff --git a/lib/src/views/share_image_view.dart b/lib/src/views/share_image_view.dart index cf2c57c..48bcffe 100644 --- a/lib/src/views/share_image_view.dart +++ b/lib/src/views/share_image_view.dart @@ -2,14 +2,11 @@ import 'dart:collection'; import 'package:fixnum/fixnum.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:provider/provider.dart'; -import 'package:twonly/src/app.dart'; import 'package:twonly/src/components/best_friends_selector.dart'; import 'package:twonly/src/components/headline.dart'; import 'package:twonly/src/components/initialsavatar.dart'; import 'package:twonly/src/model/contacts_model.dart'; -import 'package:twonly/src/providers/notify_provider.dart'; -import 'package:twonly/src/utils/api.dart'; +import 'package:twonly/src/providers/api/api.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/home_view.dart'; @@ -112,21 +109,12 @@ class _ShareImageView extends State { FilledButton.icon( icon: Icon(Icons.send), onPressed: () async { - for (Int64 a in _selectedUserIds) { - addSendingTo(a); - } - - sendImage( - context, - _users - .where((c) => _selectedUserIds.contains(c.userId)) - .toList(), - widget.image); + sendImage(_selectedUserIds.toList(), widget.image); + // TODO: pop back to the HomeView page popUntil did not work. check later how to improve in case of pushing more then 2 Navigator.pop(context); Navigator.pop(context); - context.read().setActivePageIdx(0); - homeViewPageController.jumpToPage(0); + globalUpdateOfHomeViewPageIndex(0); }, style: ButtonStyle( padding: WidgetStateProperty.all(