uploads of files does work

This commit is contained in:
otsmr 2025-01-26 01:25:59 +01:00
parent e42b68e8ee
commit 2ab0ad3daa
13 changed files with 324 additions and 107 deletions

View file

@ -88,7 +88,7 @@ class TextContent extends MessageContent {
@JsonSerializable() @JsonSerializable()
class ImageContent extends MessageContent { class ImageContent extends MessageContent {
final String imageToken; final List<int> imageToken;
ImageContent(this.imageToken); ImageContent(this.imageToken);

View file

@ -47,7 +47,9 @@ Map<String, dynamic> _$TextContentToJson(TextContent instance) =>
}; };
ImageContent _$ImageContentFromJson(Map<String, dynamic> json) => ImageContent( ImageContent _$ImageContentFromJson(Map<String, dynamic> json) => ImageContent(
json['imageToken'] as String, (json['imageToken'] as List<dynamic>)
.map((e) => (e as num).toInt())
.toList(),
); );
Map<String, dynamic> _$ImageContentToJson(ImageContent instance) => Map<String, dynamic> _$ImageContentToJson(ImageContent instance) =>

View file

@ -178,6 +178,8 @@ class ApiProvider {
DbContacts.acceptUser(fromUserId.toInt()); DbContacts.acceptUser(fromUserId.toInt());
updateNotifier(); updateNotifier();
break; break;
case MessageKind.image:
log.info("Got image: ${message.content}");
default: default:
log.shout("Got unknown MessageKind $message"); log.shout("Got unknown MessageKind $message");
} }
@ -360,6 +362,43 @@ class ApiProvider {
return _asResult(resp); return _asResult(resp);
} }
Future<Result> 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<List<int>?> 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<int> 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<Result> getUserData(String username) async { Future<Result> getUserData(String username) async {
var get = ApplicationData_GetUserByUsername()..username = username; var get = ApplicationData_GetUserByUsername()..username = username;
var appData = ApplicationData()..getuserbyusername = get; var appData = ApplicationData()..getuserbyusername = get;

View file

@ -4,11 +4,29 @@ import 'package:twonly/src/model/contacts_model.dart';
/// Mix-in [DiagnosticableTreeMixin] to have access to [debugFillProperties] for the devtool /// Mix-in [DiagnosticableTreeMixin] to have access to [debugFillProperties] for the devtool
// ignore: prefer_mixin // ignore: prefer_mixin
class NotifyProvider with ChangeNotifier, DiagnosticableTreeMixin { class NotifyProvider with ChangeNotifier, DiagnosticableTreeMixin {
// The page index of the HomeView widget
int _activePageIdx = 0;
int _newContactRequests = 0; int _newContactRequests = 0;
List<Contact> _allContacts = []; List<Contact> _allContacts = [];
List<Contact> _sendingCurrentlyTo = [];
int get newContactRequests => _newContactRequests; int get newContactRequests => _newContactRequests;
List<Contact> get allContacts => _allContacts; List<Contact> get allContacts => _allContacts;
List<Contact> get sendingCurrentlyTo => _sendingCurrentlyTo;
int get activePageIdx => _activePageIdx;
void setActivePageIdx(int idx) {
_activePageIdx = idx;
notifyListeners();
}
void addSendingTo(List<Contact> users) {
_sendingCurrentlyTo.addAll(users);
notifyListeners();
}
void update() async { void update() async {
_allContacts = await DbContacts.getUsers(); _allContacts = await DbContacts.getUsers();

View file

@ -1,12 +1,16 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:io';
import 'package:fixnum/fixnum.dart'; import 'package:fixnum/fixnum.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:provider/provider.dart';
import 'package:twonly/main.dart'; import 'package:twonly/main.dart';
import 'package:twonly/src/model/contacts_model.dart'; import 'package:twonly/src/model/contacts_model.dart';
import 'package:twonly/src/model/json/message.dart'; import 'package:twonly/src/model/json/message.dart';
import 'package:twonly/src/proto/api/error.pb.dart'; import 'package:twonly/src/proto/api/error.pb.dart';
import 'package:twonly/src/providers/api_provider.dart'; import 'package:twonly/src/providers/api_provider.dart';
import 'package:twonly/src/providers/notify_provider.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
// ignore: library_prefixes // ignore: library_prefixes
import 'package:twonly/src/utils/signal.dart' as SignalHelper; import 'package:twonly/src/utils/signal.dart' as SignalHelper;
@ -55,6 +59,9 @@ Future<Result> encryptAndSendMessage(Int64 userId, Message msg) async {
Result resp = await apiProvider.sendTextMessage(userId, bytes); Result resp = await apiProvider.sendTextMessage(userId, bytes);
Logger("encryptAndSendMessage")
.shout("handle errors here and store them in the database");
return resp; return resp;
} }
@ -86,3 +93,43 @@ Future<Result> createNewUser(String username, String inviteCode) async {
return res; return res;
} }
Future sendImage(
BuildContext context, List<Contact> users, String imagePath) async {
// 1. set notifier provider
context.read<NotifyProvider>().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<int>? 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);
}
}

View file

@ -1,8 +1,11 @@
import 'dart:io'; import 'dart:io';
import 'dart:math'; import 'dart:math';
import 'dart:typed_data'; import 'dart:typed_data';
import 'dart:ui';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_image_compress/flutter_image_compress.dart';
import 'package:gal/gal.dart'; import 'package:gal/gal.dart';
import 'package:image/image.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.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), contentPadding: EdgeInsets.symmetric(vertical: 15.0, horizontal: 20.0),
); );
} }
Future<Uint8List?> getCompressedImage(File file) async {
var result = await FlutterImageCompress.compressWithFile(
file.absolute.path,
quality: 90,
);
print(file.lengthSync());
print(result!.length);
return result;
}

View file

@ -190,6 +190,27 @@ List<Uint8List>? removeLastFourBytes(Uint8List original) {
return [newList, lastFourBytes]; return [newList, lastFourBytes];
} }
Future<Uint8List?> 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<Uint8List?> encryptMessage(Message msg, Int64 target) async { Future<Uint8List?> encryptMessage(Message msg, Int64 target) async {
try { try {
ConnectSignalProtocolStore signalStore = (await getSignalStore())!; ConnectSignalProtocolStore signalStore = (await getSignalStore())!;

View file

@ -77,17 +77,17 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
Navigator.push( Navigator.push(
context, context,
PageRouteBuilder( PageRouteBuilder(
opaque: false, opaque: false,
pageBuilder: (context, a1, a2) => pageBuilder: (context, a1, a2) =>
ShareImageEditorView(image: path), ShareImageEditorView(image: path),
transitionsBuilder: transitionsBuilder:
(context, animation, secondaryAnimation, child) { (context, animation, secondaryAnimation, child) {
return child; return child;
}, },
transitionDuration: Duration.zero, transitionDuration: Duration.zero,
reverseTransitionDuration: Duration.zero), reverseTransitionDuration: Duration.zero,
),
); );
debugPrint('Picture saved: ${path}');
}, },
multiple: (multiple) { multiple: (multiple) {
multiple.fileBySensor.forEach((key, value) { multiple.fileBySensor.forEach((key, value) {

View file

@ -66,35 +66,69 @@ class _ChatListViewState extends State<ChatListView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
List<Contact> sendingCurrentlyTo =
context.watch<NotifyProvider>().sendingCurrentlyTo;
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(AppLocalizations.of(context)!.chatsTitle), title: Text(AppLocalizations.of(context)!.chatsTitle),
actions: [ actions: [
NotificationBadge( NotificationBadge(
count: context.watch<NotifyProvider>().newContactRequests, count: context.watch<NotifyProvider>().newContactRequests,
child: IconButton( child: IconButton(
icon: Icon(Icons.person_add), icon: Icon(Icons.person_add),
onPressed: () { onPressed: () {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => SearchUsernameView(), 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
),
],
// ),
), ),
); title: Text(sendingCurrentlyTo
}, .map((e) => e.displayName)
), .toList()
) .join(", ")),
], ),
), ),
body: ListView.builder( Expanded(
restorationId: 'chat_list_view', child: ListView.builder(
itemCount: _activeUsers.length, restorationId: 'chat_list_view',
itemBuilder: (BuildContext context, int index) { itemCount: _activeUsers.length,
final user = _activeUsers[index]; itemBuilder: (BuildContext context, int index) {
return UserListItem(user: user, secondsSinceOpen: _secondsSinceOpen); final user = _activeUsers[index];
}, return UserListItem(
), user: user, secondsSinceOpen: _secondsSinceOpen);
); },
),
)
],
));
} }
} }

View file

@ -1,4 +1,6 @@
import 'package:pie_menu/pie_menu.dart'; 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 'camera_preview_view.dart';
import 'chat_list_view.dart'; import 'chat_list_view.dart';
@ -6,6 +8,8 @@ import 'profile_view.dart';
import '../settings/settings_controller.dart'; import '../settings/settings_controller.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
final PageController homeViewPageController = PageController(initialPage: 0);
class HomeView extends StatefulWidget { class HomeView extends StatefulWidget {
const HomeView({super.key, required this.settingsController}); const HomeView({super.key, required this.settingsController});
final SettingsController settingsController; final SettingsController settingsController;
@ -15,8 +19,6 @@ class HomeView extends StatefulWidget {
} }
class HomeViewState extends State<HomeView> { class HomeViewState extends State<HomeView> {
int _activePageIdx = 0;
final PageController _pageController = PageController(initialPage: 0);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return PieCanvas( return PieCanvas(
@ -43,11 +45,9 @@ class HomeViewState extends State<HomeView> {
), ),
child: Scaffold( child: Scaffold(
body: PageView( body: PageView(
controller: _pageController, controller: homeViewPageController,
onPageChanged: (index) { onPageChanged: (index) {
setState(() { context.read<NotifyProvider>().setActivePageIdx(index);
_activePageIdx = index;
});
}, },
children: [ children: [
ChatListView(), ChatListView(),
@ -69,14 +69,14 @@ class HomeViewState extends State<HomeView> {
BottomNavigationBarItem(icon: Icon(Icons.verified_user), label: ""), BottomNavigationBarItem(icon: Icon(Icons.verified_user), label: ""),
], ],
onTap: (int index) { onTap: (int index) {
context.read<NotifyProvider>().setActivePageIdx(index);
setState(() { setState(() {
_activePageIdx = index; homeViewPageController.animateToPage(index,
_pageController.animateToPage(_activePageIdx,
duration: const Duration(milliseconds: 100), duration: const Duration(milliseconds: 100),
curve: Curves.bounceIn); curve: Curves.bounceIn);
}); });
}, },
currentIndex: _activePageIdx, currentIndex: context.watch<NotifyProvider>().activePageIdx,
), ),
), ),
); );

View file

@ -1,13 +1,16 @@
import 'dart:collection'; import 'dart:collection';
import 'package:fixnum/fixnum.dart'; import 'package:fixnum/fixnum.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:provider/provider.dart';
import 'package:twonly/src/components/best_friends_selector.dart'; import 'package:twonly/src/components/best_friends_selector.dart';
import 'package:twonly/src/components/headline.dart'; import 'package:twonly/src/components/headline.dart';
import 'package:twonly/src/components/initialsavatar.dart'; import 'package:twonly/src/components/initialsavatar.dart';
import 'package:twonly/src/model/contacts_model.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/utils/misc.dart';
import 'package:twonly/src/views/home_view.dart';
class ShareImageView extends StatefulWidget { class ShareImageView extends StatefulWidget {
const ShareImageView({super.key, required this.image}); const ShareImageView({super.key, required this.image});
@ -86,9 +89,14 @@ class _ShareImageView extends State<ShareImageView> {
}, },
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
HeadLineComponent(AppLocalizations.of(context)!.shareImageAllUsers), if (_usersFiltered.isNotEmpty)
HeadLineComponent(
AppLocalizations.of(context)!.shareImageAllUsers),
Expanded( Expanded(
child: UserList(_usersFiltered), child: UserList(
List.from(_usersFiltered),
selectedUserIds: _selectedUserIds,
),
) )
], ],
), ),
@ -103,13 +111,17 @@ class _ShareImageView extends State<ShareImageView> {
FilledButton.icon( FilledButton.icon(
icon: Icon(Icons.send), icon: Icon(Icons.send),
onPressed: () async { onPressed: () async {
print(_selectedUserIds); sendImage(
// Navigator.push( context,
// context, _users
// MaterialPageRoute( .where((c) => _selectedUserIds.contains(c.userId))
// builder: (context) => .toList(),
// ShareImageView(image: widget.image)), widget.image);
// );
Navigator.pop(context);
Navigator.pop(context);
context.read<NotifyProvider>().setActivePageIdx(0);
homeViewPageController.jumpToPage(0);
}, },
style: ButtonStyle( style: ButtonStyle(
padding: WidgetStateProperty.all<EdgeInsets>( padding: WidgetStateProperty.all<EdgeInsets>(
@ -130,62 +142,44 @@ class _ShareImageView extends State<ShareImageView> {
} }
class UserList extends StatelessWidget { class UserList extends StatelessWidget {
const UserList(this._knownUsers, {super.key}); const UserList(this.users, {super.key, required this.selectedUserIds});
final List<Contact> _knownUsers; final List<Contact> users;
final HashSet<Int64> selectedUserIds;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// Step 1: Sort the users alphabetically // Step 1: Sort the users alphabetically
_knownUsers.sort((a, b) => a.displayName.compareTo(b.displayName)); users.sort((a, b) => a.displayName.compareTo(b.displayName));
// Step 2: Group users by their initials
Map<String, List<String>> 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<MapEntry<String, List<String>>> sections =
groupedUsers.entries.toList();
return ListView.builder( return ListView.builder(
restorationId: 'new_message_users_list', restorationId: 'new_message_users_list',
itemCount: sections.length, itemCount: users.length,
itemBuilder: (BuildContext context, int sectionIndex) { itemBuilder: (BuildContext context, int i) {
final section = sections[sectionIndex]; Contact user = users[i];
final initial = section.key; return ListTile(
final users = section.value; title: Text(user.displayName),
leading: InitialsAvatar(
return Column( displayName: user.displayName,
crossAxisAlignment: CrossAxisAlignment.start, fontSize: 15,
children: [ ),
// Header for the initial trailing: Checkbox(
// Padding( value: selectedUserIds.contains(user.userId),
// padding: onChanged: (bool? value) {
// const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0), if (value == null) return;
// child: Text( if (value) {
// initial, selectedUserIds.add(user.userId);
// style: TextStyle(fontWeight: FontWeight.normal, fontSize: 18), } else {
// ), selectedUserIds.remove(user.userId);
// ), }
// List of users under this initial },
...users.map((username) { ),
return ListTile( onTap: () {
title: Text(username), if (!selectedUserIds.contains(user.userId)) {
leading: InitialsAvatar( selectedUserIds.add(user.userId);
displayName: username, } else {
fontSize: 15, selectedUserIds.remove(user.userId);
), }
onTap: () { },
// Handle tap
},
);
}).toList(),
],
); );
}, },
); );

View file

@ -315,6 +315,54 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" 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: flutter_keyboard_visibility:
dependency: transitive dependency: transitive
description: description:

View file

@ -16,6 +16,7 @@ dependencies:
fixnum: ^1.1.1 fixnum: ^1.1.1
flutter: flutter:
sdk: flutter sdk: flutter
flutter_image_compress: ^2.4.0
flutter_localizations: flutter_localizations:
sdk: flutter sdk: flutter
flutter_secure_storage: ^9.2.2 flutter_secure_storage: ^9.2.2