mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-01-15 08:08:39 +00:00
retransmission works
This commit is contained in:
parent
444edf0230
commit
10dadcee7c
8 changed files with 255 additions and 90 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -74,6 +74,7 @@ class ApiProvider {
|
|||
|
||||
if (!globalIsAppInBackground) {
|
||||
tryTransmitMessages();
|
||||
retransmitMediaFiles();
|
||||
tryDownloadAllMediaFiles();
|
||||
notifyContactsAboutProfileChange();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
|
|
|||
Loading…
Reference in a new issue