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_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = CN332ZUGRP;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.connect;
PRODUCT_BUNDLE_IDENTIFIER = eu.twonly;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
@ -664,13 +665,14 @@
CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = CN332ZUGRP;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.connect;
PRODUCT_BUNDLE_IDENTIFIER = eu.twonly;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@ -687,13 +689,14 @@
CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = CN332ZUGRP;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.connect;
PRODUCT_BUNDLE_IDENTIFIER = eu.twonly;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
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">
<plist version="1.0">
<dict>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
@ -24,6 +26,16 @@
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<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>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
@ -41,18 +53,5 @@
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</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>
</plist>

View file

@ -27,6 +27,19 @@ String beautifulZoomScale(double scale) {
}
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
Widget build(BuildContext context) {
var zoomButtonStyle = TextButton.styleFrom(
@ -47,34 +60,37 @@ class _CameraZoomButtonsState extends State<CameraZoomButtons> {
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextButton(
style: zoomButtonStyle.copyWith(
foregroundColor: WidgetStateProperty.all(
(widget.scaleFactor < 1) ? Colors.yellow : Colors.white,
if (showWideAngleZoom)
TextButton(
style: zoomButtonStyle.copyWith(
foregroundColor: WidgetStateProperty.all(
(widget.scaleFactor < 1) ? Colors.yellow : Colors.white,
),
),
onPressed: () async {
var level = await widget.controller.getMinZoomLevel();
widget.updateScaleFactor(level);
},
child: FutureBuilder(
future: widget.controller.getMinZoomLevel(),
builder: (context, snap) {
if (snap.hasData) {
var minLevel =
beautifulZoomScale(snap.data!.toDouble());
var currentLevel =
beautifulZoomScale(widget.scaleFactor);
return Text(
widget.scaleFactor < 1
? "${currentLevel}x"
: "${minLevel}x",
style: zoomTextStyle,
);
} else {
return Text("");
}
},
),
),
onPressed: () async {
var level = await widget.controller.getMinZoomLevel();
widget.updateScaleFactor(level);
},
child: FutureBuilder(
future: widget.controller.getMinZoomLevel(),
builder: (context, snap) {
if (snap.hasData) {
var minLevel = beautifulZoomScale(snap.data!.toDouble());
var currentLevel = beautifulZoomScale(widget.scaleFactor);
return Text(
widget.scaleFactor < 1
? "${currentLevel}x"
: "${minLevel}x",
style: zoomTextStyle,
);
} else {
return Text("");
}
},
),
),
TextButton(
style: zoomButtonStyle.copyWith(
foregroundColor: WidgetStateProperty.all(

View file

@ -15,42 +15,32 @@ import 'package:twonly/src/utils/signal.dart' as SignalHelper;
import 'package:twonly/src/utils/storage.dart';
Future tryTransmitMessages() async {
// List<Message> retransmit =
// await twonlyDatabase.getAllMessagesForRetransmitting();
List<Message> retransmit =
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();
// for (int i = 0; i < retransmit.length; i++) {
// int msgId = retransmit[i].messageId;
Box box = await getMediaStorage();
for (int i = 0; i < retransmit.length; i++) {
int msgId = retransmit[i].messageId;
// Uint8List? bytes = box.get("retransmit-$msgId-textmessage");
// if (bytes != null) {
// Result resp = await apiProvider.sendTextMessage(
// retransmit[i].contactId,
// bytes,
// );
// if (resp.isSuccess) {
// await twonlyDatabase.updateMessageByMessageId(
// msgId, MessagesCompanion(acknowledgeByServer: Value(true)));
// box.delete("retransmit-$msgId-textmessage");
// } 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);
// }
// }
Uint8List? bytes = box.get("retransmit-$msgId-textmessage");
if (bytes != null) {
Result resp = await apiProvider.sendTextMessage(
retransmit[i].contactId,
bytes,
);
if (resp.isSuccess) {
await twonlyDatabase.messagesDao.updateMessageByMessageId(
msgId, MessagesCompanion(acknowledgeByServer: Value(true)));
box.delete("retransmit-$msgId-textmessage");
} else {
// in case of error do nothing. As the message is not removed the app will try again when relaunched
}
}
}
}
// 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 'package:cryptography_plus/cryptography_plus.dart';
import 'package:drift/drift.dart';
import 'package:hive/hive.dart';
import 'package:logging/logging.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/app.dart';
@ -37,11 +38,40 @@ Future tryDownloadAllMediaFiles() async {
class Metadata {
late List<int> userIds;
late HashMap<int, int> messageIds;
late Uint8List imageBytes;
late Map<int, int> messageIds;
late bool isRealTwonly;
late int maxShowTime;
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 {
@ -50,11 +80,75 @@ class PrepareState {
late List<int> encryptionMac;
late List<int> encryptedBytes;
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 {
late List<int> uploadToken;
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 {
@ -208,10 +302,11 @@ Future sendImage(
metadata.userIds = userIds;
metadata.isRealTwonly = isRealTwonly;
metadata.maxShowTime = maxShowTime;
metadata.messageIds = HashMap();
metadata.messageIds = {};
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..
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 =
await ImageUploader.uploadState(prepareState, metadata.userIds.length);
if (uploadState == null) {
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 =
await ImageUploader.notifyState(prepareState, uploadState, metadata);

View file

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

View file

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

View file

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