From 2ab0ad3daaba4b7dc3c1a6e8073957c70c50186c Mon Sep 17 00:00:00 2001 From: otsmr Date: Sun, 26 Jan 2025 01:25:59 +0100 Subject: [PATCH] uploads of files does work --- lib/src/model/json/message.dart | 2 +- lib/src/model/json/message.g.dart | 4 +- lib/src/providers/api_provider.dart | 39 +++++++++ lib/src/providers/notify_provider.dart | 18 ++++ lib/src/utils/api.dart | 47 +++++++++++ lib/src/utils/misc.dart | 13 +++ lib/src/utils/signal.dart | 21 +++++ lib/src/views/camera_preview_view.dart | 20 ++--- lib/src/views/chat_list_view.dart | 88 +++++++++++++------ lib/src/views/home_view.dart | 18 ++-- lib/src/views/share_image_view.dart | 112 ++++++++++++------------- pubspec.lock | 48 +++++++++++ pubspec.yaml | 1 + 13 files changed, 324 insertions(+), 107 deletions(-) diff --git a/lib/src/model/json/message.dart b/lib/src/model/json/message.dart index 6253d7d..7eac3e4 100644 --- a/lib/src/model/json/message.dart +++ b/lib/src/model/json/message.dart @@ -88,7 +88,7 @@ class TextContent extends MessageContent { @JsonSerializable() class ImageContent extends MessageContent { - final String imageToken; + final List imageToken; ImageContent(this.imageToken); diff --git a/lib/src/model/json/message.g.dart b/lib/src/model/json/message.g.dart index 0d45008..b6fb76a 100644 --- a/lib/src/model/json/message.g.dart +++ b/lib/src/model/json/message.g.dart @@ -47,7 +47,9 @@ Map _$TextContentToJson(TextContent instance) => }; ImageContent _$ImageContentFromJson(Map json) => ImageContent( - json['imageToken'] as String, + (json['imageToken'] as List) + .map((e) => (e as num).toInt()) + .toList(), ); Map _$ImageContentToJson(ImageContent instance) => diff --git a/lib/src/providers/api_provider.dart b/lib/src/providers/api_provider.dart index ffab719..77b4c66 100644 --- a/lib/src/providers/api_provider.dart +++ b/lib/src/providers/api_provider.dart @@ -178,6 +178,8 @@ class ApiProvider { DbContacts.acceptUser(fromUserId.toInt()); updateNotifier(); break; + case MessageKind.image: + log.info("Got image: ${message.content}"); default: log.shout("Got unknown MessageKind $message"); } @@ -360,6 +362,43 @@ class ApiProvider { return _asResult(resp); } + Future getUploadToken(int size) async { + var get = ApplicationData_GetUploadToken()..len = size; + 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); + } + + Future?> uploadData(Uint8List data) async { + Result res = await getUploadToken(data.length); + + if (res.isError || !res.value.hasUploadtoken()) { + Logger("api.dart").shout("Error getting upload token!"); + return null; + } + List uploadToken = res.value.uploadtoken; + log.info("Got token: $uploadToken"); + + log.shout("fragmentate the data"); + + var get = ApplicationData_UploadData() + ..uploadToken = uploadToken + ..data = data + ..offset = 0; + + 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; + } + Future getUserData(String username) async { var get = ApplicationData_GetUserByUsername()..username = username; var appData = ApplicationData()..getuserbyusername = get; diff --git a/lib/src/providers/notify_provider.dart b/lib/src/providers/notify_provider.dart index bbeb39f..26ea700 100644 --- a/lib/src/providers/notify_provider.dart +++ b/lib/src/providers/notify_provider.dart @@ -4,11 +4,29 @@ import 'package:twonly/src/model/contacts_model.dart'; /// Mix-in [DiagnosticableTreeMixin] to have access to [debugFillProperties] for the devtool // ignore: prefer_mixin class NotifyProvider with ChangeNotifier, DiagnosticableTreeMixin { + // The page index of the HomeView widget + int _activePageIdx = 0; + int _newContactRequests = 0; List _allContacts = []; + List _sendingCurrentlyTo = []; + int get newContactRequests => _newContactRequests; List get allContacts => _allContacts; + List get sendingCurrentlyTo => _sendingCurrentlyTo; + + int get activePageIdx => _activePageIdx; + + void setActivePageIdx(int idx) { + _activePageIdx = idx; + notifyListeners(); + } + + void addSendingTo(List users) { + _sendingCurrentlyTo.addAll(users); + notifyListeners(); + } void update() async { _allContacts = await DbContacts.getUsers(); diff --git a/lib/src/utils/api.dart b/lib/src/utils/api.dart index 0e5bf5d..fed91ca 100644 --- a/lib/src/utils/api.dart +++ b/lib/src/utils/api.dart @@ -1,12 +1,16 @@ 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/model/contacts_model.dart'; import 'package:twonly/src/model/json/message.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; @@ -55,6 +59,9 @@ Future encryptAndSendMessage(Int64 userId, Message msg) async { Result resp = await apiProvider.sendTextMessage(userId, bytes); + Logger("encryptAndSendMessage") + .shout("handle errors here and store them in the database"); + return resp; } @@ -86,3 +93,43 @@ Future createNewUser(String username, String inviteCode) async { return res; } + +Future sendImage( + BuildContext context, List users, String imagePath) async { + // 1. set notifier provider + + context.read().addSendingTo(users); + + 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; + } + + List? imageToken = await apiProvider.uploadData(encryptedImage); + if (imageToken == null) { + Logger("api.dart").shout("handle error uploading like saving..."); + continue; + } + + Message msg = Message( + kind: MessageKind.image, + content: ImageContent(imageToken), + timestamp: DateTime.timestamp(), + ); + + print("Send image to $target"); + encryptAndSendMessage(target, msg); + } +} diff --git a/lib/src/utils/misc.dart b/lib/src/utils/misc.dart index 35dd79c..f1587b4 100644 --- a/lib/src/utils/misc.dart +++ b/lib/src/utils/misc.dart @@ -1,8 +1,11 @@ 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'; @@ -118,3 +121,13 @@ InputDecoration getInputDecoration(context, hintText) { contentPadding: EdgeInsets.symmetric(vertical: 15.0, horizontal: 20.0), ); } + +Future getCompressedImage(File file) async { + var result = await FlutterImageCompress.compressWithFile( + file.absolute.path, + quality: 90, + ); + print(file.lengthSync()); + print(result!.length); + return result; +} diff --git a/lib/src/utils/signal.dart b/lib/src/utils/signal.dart index b883b56..018f3b8 100644 --- a/lib/src/utils/signal.dart +++ b/lib/src/utils/signal.dart @@ -190,6 +190,27 @@ List? removeLastFourBytes(Uint8List original) { return [newList, lastFourBytes]; } +Future encryptBytes(Uint8List bytes, Int64 target) async { + try { + ConnectSignalProtocolStore signalStore = (await getSignalStore())!; + + SessionCipher session = SessionCipher.fromStore( + signalStore, SignalProtocolAddress(target.toString(), defaultDeviceId)); + + final ciphertext = + await session.encrypt(Uint8List.fromList(gzip.encode(bytes))); + + var b = BytesBuilder(); + b.add(ciphertext.serialize()); + b.add(intToBytes(ciphertext.getType())); + + return b.takeBytes(); + } catch (e) { + Logger("utils/signal").shout(e.toString()); + return null; + } +} + Future encryptMessage(Message msg, Int64 target) async { try { ConnectSignalProtocolStore signalStore = (await getSignalStore())!; diff --git a/lib/src/views/camera_preview_view.dart b/lib/src/views/camera_preview_view.dart index 1a3a484..9531828 100644 --- a/lib/src/views/camera_preview_view.dart +++ b/lib/src/views/camera_preview_view.dart @@ -77,17 +77,17 @@ class _CameraPreviewViewState extends State { Navigator.push( context, PageRouteBuilder( - opaque: false, - pageBuilder: (context, a1, a2) => - ShareImageEditorView(image: path), - transitionsBuilder: - (context, animation, secondaryAnimation, child) { - return child; - }, - transitionDuration: Duration.zero, - reverseTransitionDuration: Duration.zero), + opaque: false, + pageBuilder: (context, a1, a2) => + ShareImageEditorView(image: path), + transitionsBuilder: + (context, animation, secondaryAnimation, child) { + return child; + }, + transitionDuration: Duration.zero, + reverseTransitionDuration: Duration.zero, + ), ); - debugPrint('Picture saved: ${path}'); }, multiple: (multiple) { multiple.fileBySensor.forEach((key, value) { diff --git a/lib/src/views/chat_list_view.dart b/lib/src/views/chat_list_view.dart index da135ec..65122ef 100644 --- a/lib/src/views/chat_list_view.dart +++ b/lib/src/views/chat_list_view.dart @@ -66,35 +66,69 @@ class _ChatListViewState extends State { @override Widget build(BuildContext context) { + List sendingCurrentlyTo = + context.watch().sendingCurrentlyTo; + 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: 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 + ), + ], + // ), ), - ); - }, - ), - ) - ], - ), - body: ListView.builder( - restorationId: 'chat_list_view', - itemCount: _activeUsers.length, - itemBuilder: (BuildContext context, int index) { - final user = _activeUsers[index]; - return UserListItem(user: user, secondsSinceOpen: _secondsSinceOpen); - }, - ), - ); + title: Text(sendingCurrentlyTo + .map((e) => e.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, secondsSinceOpen: _secondsSinceOpen); + }, + ), + ) + ], + )); } } diff --git a/lib/src/views/home_view.dart b/lib/src/views/home_view.dart index 740b064..b1b58cc 100644 --- a/lib/src/views/home_view.dart +++ b/lib/src/views/home_view.dart @@ -1,4 +1,6 @@ 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'; @@ -6,6 +8,8 @@ import 'profile_view.dart'; import '../settings/settings_controller.dart'; import 'package:flutter/material.dart'; +final PageController homeViewPageController = PageController(initialPage: 0); + class HomeView extends StatefulWidget { const HomeView({super.key, required this.settingsController}); final SettingsController settingsController; @@ -15,8 +19,6 @@ class HomeView extends StatefulWidget { } class HomeViewState extends State { - int _activePageIdx = 0; - final PageController _pageController = PageController(initialPage: 0); @override Widget build(BuildContext context) { return PieCanvas( @@ -43,11 +45,9 @@ class HomeViewState extends State { ), child: Scaffold( body: PageView( - controller: _pageController, + controller: homeViewPageController, onPageChanged: (index) { - setState(() { - _activePageIdx = index; - }); + context.read().setActivePageIdx(index); }, children: [ ChatListView(), @@ -69,14 +69,14 @@ class HomeViewState extends State { BottomNavigationBarItem(icon: Icon(Icons.verified_user), label: ""), ], onTap: (int index) { + context.read().setActivePageIdx(index); setState(() { - _activePageIdx = index; - _pageController.animateToPage(_activePageIdx, + homeViewPageController.animateToPage(index, duration: const Duration(milliseconds: 100), curve: Curves.bounceIn); }); }, - currentIndex: _activePageIdx, + currentIndex: context.watch().activePageIdx, ), ), ); diff --git a/lib/src/views/share_image_view.dart b/lib/src/views/share_image_view.dart index d6c4dfc..bcf336c 100644 --- a/lib/src/views/share_image_view.dart +++ b/lib/src/views/share_image_view.dart @@ -1,13 +1,16 @@ 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/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/utils/misc.dart'; +import 'package:twonly/src/views/home_view.dart'; class ShareImageView extends StatefulWidget { const ShareImageView({super.key, required this.image}); @@ -86,9 +89,14 @@ class _ShareImageView extends State { }, ), const SizedBox(height: 10), - HeadLineComponent(AppLocalizations.of(context)!.shareImageAllUsers), + if (_usersFiltered.isNotEmpty) + HeadLineComponent( + AppLocalizations.of(context)!.shareImageAllUsers), Expanded( - child: UserList(_usersFiltered), + child: UserList( + List.from(_usersFiltered), + selectedUserIds: _selectedUserIds, + ), ) ], ), @@ -103,13 +111,17 @@ class _ShareImageView extends State { FilledButton.icon( icon: Icon(Icons.send), onPressed: () async { - print(_selectedUserIds); - // Navigator.push( - // context, - // MaterialPageRoute( - // builder: (context) => - // ShareImageView(image: widget.image)), - // ); + sendImage( + context, + _users + .where((c) => _selectedUserIds.contains(c.userId)) + .toList(), + widget.image); + + Navigator.pop(context); + Navigator.pop(context); + context.read().setActivePageIdx(0); + homeViewPageController.jumpToPage(0); }, style: ButtonStyle( padding: WidgetStateProperty.all( @@ -130,62 +142,44 @@ class _ShareImageView extends State { } class UserList extends StatelessWidget { - const UserList(this._knownUsers, {super.key}); - final List _knownUsers; + const UserList(this.users, {super.key, required this.selectedUserIds}); + final List users; + final HashSet selectedUserIds; @override Widget build(BuildContext context) { // Step 1: Sort the users alphabetically - _knownUsers.sort((a, b) => a.displayName.compareTo(b.displayName)); - - // Step 2: Group users by their initials - Map> groupedUsers = {}; - for (var user in _knownUsers) { - String initial = user.displayName[0].toUpperCase(); - if (!groupedUsers.containsKey(initial)) { - groupedUsers[initial] = []; - } - groupedUsers[initial]!.add(user.displayName); - } - - // Step 3: Create a list of sections - List>> sections = - groupedUsers.entries.toList(); + users.sort((a, b) => a.displayName.compareTo(b.displayName)); return ListView.builder( restorationId: 'new_message_users_list', - itemCount: sections.length, - itemBuilder: (BuildContext context, int sectionIndex) { - final section = sections[sectionIndex]; - final initial = section.key; - final users = section.value; - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Header for the initial - // Padding( - // padding: - // const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0), - // child: Text( - // initial, - // style: TextStyle(fontWeight: FontWeight.normal, fontSize: 18), - // ), - // ), - // List of users under this initial - ...users.map((username) { - return ListTile( - title: Text(username), - leading: InitialsAvatar( - displayName: username, - fontSize: 15, - ), - onTap: () { - // Handle tap - }, - ); - }).toList(), - ], + itemCount: users.length, + itemBuilder: (BuildContext context, int i) { + Contact user = users[i]; + return ListTile( + title: Text(user.displayName), + leading: InitialsAvatar( + displayName: user.displayName, + fontSize: 15, + ), + trailing: Checkbox( + value: selectedUserIds.contains(user.userId), + onChanged: (bool? value) { + if (value == null) return; + if (value) { + selectedUserIds.add(user.userId); + } else { + selectedUserIds.remove(user.userId); + } + }, + ), + onTap: () { + if (!selectedUserIds.contains(user.userId)) { + selectedUserIds.add(user.userId); + } else { + selectedUserIds.remove(user.userId); + } + }, ); }, ); diff --git a/pubspec.lock b/pubspec.lock index b60710e..cbd1fae 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -315,6 +315,54 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_image_compress: + dependency: "direct main" + description: + name: flutter_image_compress + sha256: "51d23be39efc2185e72e290042a0da41aed70b14ef97db362a6b5368d0523b27" + url: "https://pub.dev" + source: hosted + version: "2.4.0" + flutter_image_compress_common: + dependency: transitive + description: + name: flutter_image_compress_common + sha256: "7f79bc6c8a363063620b4e372fa86bc691e1cb28e58048cd38e030692fbd99ee" + url: "https://pub.dev" + source: hosted + version: "1.0.5" + flutter_image_compress_macos: + dependency: transitive + description: + name: flutter_image_compress_macos + sha256: "20019719b71b743aba0ef874ed29c50747461e5e8438980dfa5c2031898f7337" + url: "https://pub.dev" + source: hosted + version: "1.0.3" + flutter_image_compress_ohos: + dependency: transitive + description: + name: flutter_image_compress_ohos + sha256: e76b92bbc830ee08f5b05962fc78a532011fcd2041f620b5400a593e96da3f51 + url: "https://pub.dev" + source: hosted + version: "0.0.3" + flutter_image_compress_platform_interface: + dependency: transitive + description: + name: flutter_image_compress_platform_interface + sha256: "579cb3947fd4309103afe6442a01ca01e1e6f93dc53bb4cbd090e8ce34a41889" + url: "https://pub.dev" + source: hosted + version: "1.0.5" + flutter_image_compress_web: + dependency: transitive + description: + name: flutter_image_compress_web + sha256: b9b141ac7c686a2ce7bb9a98176321e1182c9074650e47bb140741a44b6f5a96 + url: "https://pub.dev" + source: hosted + version: "0.1.5" flutter_keyboard_visibility: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index c6a73d9..a0cb68c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,6 +16,7 @@ dependencies: fixnum: ^1.1.1 flutter: sdk: flutter + flutter_image_compress: ^2.4.0 flutter_localizations: sdk: flutter flutter_secure_storage: ^9.2.2