retransmission works

This commit is contained in:
otsmr 2025-03-24 21:45:29 +01:00
parent 444edf0230
commit 10dadcee7c
8 changed files with 255 additions and 90 deletions

View file

@ -477,13 +477,14 @@
CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES; CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = CN332ZUGRP;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
PRODUCT_BUNDLE_IDENTIFIER = com.example.connect; PRODUCT_BUNDLE_IDENTIFIER = eu.twonly;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
@ -664,13 +665,14 @@
CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES; CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = CN332ZUGRP;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
PRODUCT_BUNDLE_IDENTIFIER = com.example.connect; PRODUCT_BUNDLE_IDENTIFIER = eu.twonly;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@ -687,13 +689,14 @@
CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES; CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = CN332ZUGRP;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
PRODUCT_BUNDLE_IDENTIFIER = com.example.connect; PRODUCT_BUNDLE_IDENTIFIER = eu.twonly;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;

View file

@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string> <string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key> <key>CFBundleDisplayName</key>
@ -24,6 +26,16 @@
<string>$(FLUTTER_BUILD_NUMBER)</string> <string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>
<true/> <true/>
<key>NSCameraUsageDescription</key>
<string>To create photos that can be shared.</string>
<key>NSFaceIDUsageDescription</key>
<string>To protect others twonlies!</string>
<key>NSMicrophoneUsageDescription</key>
<string>To create videos that can be securely shared.</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>Store photos in the gallery.</string>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UILaunchStoryboardName</key> <key>UILaunchStoryboardName</key>
<string>LaunchScreen</string> <string>LaunchScreen</string>
<key>UIMainStoryboardFile</key> <key>UIMainStoryboardFile</key>
@ -41,18 +53,5 @@
<string>UIInterfaceOrientationLandscapeLeft</string> <string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string> <string>UIInterfaceOrientationLandscapeRight</string>
</array> </array>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>NSCameraUsageDescription</key>
<string>To create photos that can be shared.</string>
<key>NSMicrophoneUsageDescription</key>
<string>To create videos that can be securely shared.</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>Store photos in the gallery.</string>
<key>NSFaceIDUsageDescription</key>
<string>To protect others twonlies!</string>
</dict> </dict>
</plist> </plist>

View file

@ -27,6 +27,19 @@ String beautifulZoomScale(double scale) {
} }
class _CameraZoomButtonsState extends State<CameraZoomButtons> { class _CameraZoomButtonsState extends State<CameraZoomButtons> {
bool showWideAngleZoom = false;
@override
void initState() {
super.initState();
initAsync();
}
Future initAsync() async {
showWideAngleZoom = (await widget.controller.getMinZoomLevel()) < 1;
setState(() {});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var zoomButtonStyle = TextButton.styleFrom( var zoomButtonStyle = TextButton.styleFrom(
@ -47,6 +60,7 @@ class _CameraZoomButtonsState extends State<CameraZoomButtons> {
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
if (showWideAngleZoom)
TextButton( TextButton(
style: zoomButtonStyle.copyWith( style: zoomButtonStyle.copyWith(
foregroundColor: WidgetStateProperty.all( foregroundColor: WidgetStateProperty.all(
@ -61,8 +75,10 @@ class _CameraZoomButtonsState extends State<CameraZoomButtons> {
future: widget.controller.getMinZoomLevel(), future: widget.controller.getMinZoomLevel(),
builder: (context, snap) { builder: (context, snap) {
if (snap.hasData) { if (snap.hasData) {
var minLevel = beautifulZoomScale(snap.data!.toDouble()); var minLevel =
var currentLevel = beautifulZoomScale(widget.scaleFactor); beautifulZoomScale(snap.data!.toDouble());
var currentLevel =
beautifulZoomScale(widget.scaleFactor);
return Text( return Text(
widget.scaleFactor < 1 widget.scaleFactor < 1
? "${currentLevel}x" ? "${currentLevel}x"

View file

@ -15,42 +15,32 @@ import 'package:twonly/src/utils/signal.dart' as SignalHelper;
import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/utils/storage.dart';
Future tryTransmitMessages() async { Future tryTransmitMessages() async {
// List<Message> retransmit = List<Message> retransmit =
// await twonlyDatabase.getAllMessagesForRetransmitting(); await twonlyDatabase.messagesDao.getAllMessagesForRetransmitting();
// if (retransmit.isEmpty) return; if (retransmit.isEmpty) return;
// Logger("api.dart").info("try sending messages: ${retransmit.length}"); Logger("api.dart").info("try sending messages: ${retransmit.length}");
// Box box = await getMediaStorage(); Box box = await getMediaStorage();
// for (int i = 0; i < retransmit.length; i++) { for (int i = 0; i < retransmit.length; i++) {
// int msgId = retransmit[i].messageId; int msgId = retransmit[i].messageId;
// Uint8List? bytes = box.get("retransmit-$msgId-textmessage"); Uint8List? bytes = box.get("retransmit-$msgId-textmessage");
// if (bytes != null) { if (bytes != null) {
// Result resp = await apiProvider.sendTextMessage( Result resp = await apiProvider.sendTextMessage(
// retransmit[i].contactId, retransmit[i].contactId,
// bytes, bytes,
// ); );
if (resp.isSuccess) {
// if (resp.isSuccess) { await twonlyDatabase.messagesDao.updateMessageByMessageId(
// await twonlyDatabase.updateMessageByMessageId( msgId, MessagesCompanion(acknowledgeByServer: Value(true)));
// msgId, MessagesCompanion(acknowledgeByServer: Value(true))); box.delete("retransmit-$msgId-textmessage");
} else {
// box.delete("retransmit-$msgId-textmessage"); // in case of error do nothing. As the message is not removed the app will try again when relaunched
// } else { }
// // in case of error do nothing. As the message is not removed the app will try again when relaunched }
// } }
// }
// Uint8List? encryptedMedia = await box.get("retransmit-$msgId-media");
// if (encryptedMedia != null) {
// MediaMessageContent content =
// MediaMessageContent.fromJson(jsonDecode(retransmit[i].contentJson!));
// uploadMediaFile(msgId, retransmit[i].contactId, encryptedMedia,
// content.isRealTwonly, content.maxShowTime, retransmit[i].sendAt);
// }
// }
} }
// this functions ensures that the message is received by the server and in case of errors will try again later // this functions ensures that the message is received by the server and in case of errors will try again later

View file

@ -2,6 +2,7 @@ import 'dart:collection';
import 'dart:convert'; import 'dart:convert';
import 'package:cryptography_plus/cryptography_plus.dart'; import 'package:cryptography_plus/cryptography_plus.dart';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:hive/hive.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/app.dart'; import 'package:twonly/src/app.dart';
@ -37,11 +38,40 @@ Future tryDownloadAllMediaFiles() async {
class Metadata { class Metadata {
late List<int> userIds; late List<int> userIds;
late HashMap<int, int> messageIds; late Map<int, int> messageIds;
late Uint8List imageBytes;
late bool isRealTwonly; late bool isRealTwonly;
late int maxShowTime; late int maxShowTime;
late DateTime messageSendAt; late DateTime messageSendAt;
Metadata();
Map<String, dynamic> toJson() {
// Convert Map<int, int> to Map<String, int> for JSON encoding
Map<String, int> stringKeyMessageIds =
messageIds.map((key, value) => MapEntry(key.toString(), value));
return {
'userIds': userIds,
'messageIds': stringKeyMessageIds,
'isRealTwonly': isRealTwonly,
'maxShowTime': maxShowTime,
'messageSendAt': messageSendAt.toIso8601String(),
};
}
factory Metadata.fromJson(Map<String, dynamic> json) {
Metadata state = Metadata();
state.userIds = List<int>.from(json['userIds']);
// Convert Map<String, dynamic> to Map<int, int>
state.messageIds = (json['messageIds'] as Map<String, dynamic>)
.map((key, value) => MapEntry(int.parse(key), value as int));
state.isRealTwonly = json['isRealTwonly'];
state.maxShowTime = json['maxShowTime'];
state.messageSendAt = DateTime.parse(json['messageSendAt']);
return state;
}
} }
class PrepareState { class PrepareState {
@ -50,11 +80,75 @@ class PrepareState {
late List<int> encryptionMac; late List<int> encryptionMac;
late List<int> encryptedBytes; late List<int> encryptedBytes;
late List<int> encryptionNonce; late List<int> encryptionNonce;
PrepareState();
Map<String, dynamic> toJson() {
return {
'sha2Hash': sha2Hash,
'encryptionKey': encryptionKey,
'encryptionMac': encryptionMac,
'encryptedBytes': encryptedBytes,
'encryptionNonce': encryptionNonce,
};
}
factory PrepareState.fromJson(Map<String, dynamic> json) {
PrepareState state = PrepareState();
state.sha2Hash = List<int>.from(json['sha2Hash']);
state.encryptionKey = List<int>.from(json['encryptionKey']);
state.encryptionMac = List<int>.from(json['encryptionMac']);
state.encryptedBytes = List<int>.from(json['encryptedBytes']);
state.encryptionNonce = List<int>.from(json['encryptionNonce']);
return state;
}
} }
class UploadState { class UploadState {
late List<int> uploadToken; late List<int> uploadToken;
late List<List<int>> downloadTokens; late List<List<int>> downloadTokens;
UploadState();
Map<String, dynamic> toJson() {
return {
'uploadToken': uploadToken,
'downloadTokens': downloadTokens,
};
}
factory UploadState.fromJson(Map<String, dynamic> json) {
UploadState state = UploadState();
state.uploadToken = List<int>.from(json['uploadToken']);
state.downloadTokens = List<List<int>>.from(
json['downloadTokens'].map((token) => List<int>.from(token)),
);
return state;
}
}
class States {
late Metadata metadata;
late PrepareState prepareState;
States({
required this.metadata,
required this.prepareState,
});
Map<String, dynamic> toJson() {
return {
'metadata': metadata.toJson(),
'prepareState': prepareState.toJson(),
};
}
factory States.fromJson(Map<String, dynamic> json) {
return States(
metadata: Metadata.fromJson(json['metadata']),
prepareState: PrepareState.fromJson(json['prepareState']),
);
}
} }
class ImageUploader { class ImageUploader {
@ -208,10 +302,11 @@ Future sendImage(
metadata.userIds = userIds; metadata.userIds = userIds;
metadata.isRealTwonly = isRealTwonly; metadata.isRealTwonly = isRealTwonly;
metadata.maxShowTime = maxShowTime; metadata.maxShowTime = maxShowTime;
metadata.messageIds = HashMap(); metadata.messageIds = {};
metadata.messageSendAt = DateTime.now(); metadata.messageSendAt = DateTime.now();
// store prepareState and metadata... String stateId = prepareState.sha2Hash.toString();
States states = States(metadata: metadata, prepareState: prepareState);
// at this point it is safe inform the user about the process of sending the image.. // at this point it is safe inform the user about the process of sending the image..
for (final userId in metadata.userIds) { for (final userId in metadata.userIds) {
@ -240,13 +335,66 @@ Future sendImage(
} }
} }
{
Box storage = await getMediaStorage();
String? mediaFilesJson = storage.get("mediaUploads");
Map<String, dynamic> allMediaFiles = {};
if (mediaFilesJson != null) {
// allMediaFiles = jsonDecode(mediaFilesJson);
}
allMediaFiles[stateId] = jsonEncode(states.toJson());
storage.put("mediaUploads", jsonEncode(allMediaFiles));
}
uploadMediaState(stateId, prepareState, metadata);
}
Future retransmitMediaFiles() async {
Box storage = await getMediaStorage();
String? mediaFilesJson = storage.get("mediaUploads");
if (mediaFilesJson == null) {
return;
}
Map<String, dynamic> allMediaFiles = jsonDecode(mediaFilesJson);
for (final entry in allMediaFiles.entries) {
try {
String stateId = entry.key;
States states = States.fromJson(jsonDecode(entry.value));
// upload one by one
await uploadMediaState(stateId, states.prepareState, states.metadata);
} catch (e) {
Logger("media.dart").shout(e);
}
}
}
// if the upload failes this function is called again from the retransmitMediaFiles function which is
// called when the WebSocket is reconnected again.
Future uploadMediaState(
String stateId, PrepareState prepareState, Metadata metadata) async {
final uploadState = final uploadState =
await ImageUploader.uploadState(prepareState, metadata.userIds.length); await ImageUploader.uploadState(prepareState, metadata.userIds.length);
if (uploadState == null) { if (uploadState == null) {
return; return;
} }
// delete prepareState and store uploadState... {
Box storage = await getMediaStorage();
String? mediaFilesJson = storage.get("mediaUploads");
if (mediaFilesJson != null) {
Map<String, dynamic> allMediaFiles = jsonDecode(mediaFilesJson);
allMediaFiles.remove(stateId);
storage.put("mediaUploads", jsonEncode(allMediaFiles));
}
}
final notifyState = final notifyState =
await ImageUploader.notifyState(prepareState, uploadState, metadata); await ImageUploader.notifyState(prepareState, uploadState, metadata);

View file

@ -74,6 +74,7 @@ class ApiProvider {
if (!globalIsAppInBackground) { if (!globalIsAppInBackground) {
tryTransmitMessages(); tryTransmitMessages();
retransmitMediaFiles();
tryDownloadAllMediaFiles(); tryDownloadAllMediaFiles();
notifyContactsAboutProfileChange(); notifyContactsAboutProfileChange();
} }

View file

@ -25,6 +25,8 @@ Future<Box> getMediaStorage() async {
var encryptionKey = var encryptionKey =
base64Url.decode((await storage.read(key: 'hive_encryption_key'))!); base64Url.decode((await storage.read(key: 'hive_encryption_key'))!);
return await Hive.openBox('media_storage', return await Hive.openBox(
encryptionCipher: HiveAesCipher(encryptionKey)); 'media_storage',
encryptionCipher: HiveAesCipher(encryptionKey),
);
} }

View file

@ -119,7 +119,12 @@ class _ChatListViewState extends State<ChatListView> {
.reduce((a, b) => a > b ? a : b); .reduce((a, b) => a > b ? a : b);
} }
return ListView.builder( return RefreshIndicator(
onRefresh: () async {
await apiProvider.connect();
await Future.delayed(Duration(seconds: 1));
},
child: ListView.builder(
restorationId: 'chat_list_view', restorationId: 'chat_list_view',
itemCount: contacts.length, itemCount: contacts.length,
itemBuilder: (BuildContext context, int index) { itemBuilder: (BuildContext context, int index) {
@ -129,6 +134,7 @@ class _ChatListViewState extends State<ChatListView> {
maxTotalMediaCounter: maxTotalMediaCounter, maxTotalMediaCounter: maxTotalMediaCounter,
); );
}, },
),
); );
}, },
), ),