diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj
index 30d564f..2dfafeb 100644
--- a/ios/Runner.xcodeproj/project.pbxproj
+++ b/ios/Runner.xcodeproj/project.pbxproj
@@ -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;
diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist
index afc890d..292b5ca 100644
--- a/ios/Runner/Info.plist
+++ b/ios/Runner/Info.plist
@@ -2,6 +2,8 @@
+ CADisableMinimumFrameDurationOnPhone
+
CFBundleDevelopmentRegion
$(DEVELOPMENT_LANGUAGE)
CFBundleDisplayName
@@ -24,6 +26,16 @@
$(FLUTTER_BUILD_NUMBER)
LSRequiresIPhoneOS
+ NSCameraUsageDescription
+ To create photos that can be shared.
+ NSFaceIDUsageDescription
+ To protect others twonlies!
+ NSMicrophoneUsageDescription
+ To create videos that can be securely shared.
+ NSPhotoLibraryAddUsageDescription
+ Store photos in the gallery.
+ UIApplicationSupportsIndirectInputEvents
+
UILaunchStoryboardName
LaunchScreen
UIMainStoryboardFile
@@ -41,18 +53,5 @@
UIInterfaceOrientationLandscapeLeft
UIInterfaceOrientationLandscapeRight
- CADisableMinimumFrameDurationOnPhone
-
- UIApplicationSupportsIndirectInputEvents
-
-
- NSCameraUsageDescription
- To create photos that can be shared.
- NSMicrophoneUsageDescription
- To create videos that can be securely shared.
- NSPhotoLibraryAddUsageDescription
- Store photos in the gallery.
- NSFaceIDUsageDescription
- To protect others twonlies!
diff --git a/lib/src/components/zoom_selector.dart b/lib/src/components/zoom_selector.dart
index efc5ff1..c3567fb 100644
--- a/lib/src/components/zoom_selector.dart
+++ b/lib/src/components/zoom_selector.dart
@@ -27,6 +27,19 @@ String beautifulZoomScale(double scale) {
}
class _CameraZoomButtonsState extends State {
+ 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 {
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(
diff --git a/lib/src/providers/api/api.dart b/lib/src/providers/api/api.dart
index 31d20b1..db6f93f 100644
--- a/lib/src/providers/api/api.dart
+++ b/lib/src/providers/api/api.dart
@@ -15,42 +15,32 @@ import 'package:twonly/src/utils/signal.dart' as SignalHelper;
import 'package:twonly/src/utils/storage.dart';
Future tryTransmitMessages() async {
- // List retransmit =
- // await twonlyDatabase.getAllMessagesForRetransmitting();
+ List 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
diff --git a/lib/src/providers/api/media.dart b/lib/src/providers/api/media.dart
index a909246..69d0f91 100644
--- a/lib/src/providers/api/media.dart
+++ b/lib/src/providers/api/media.dart
@@ -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 userIds;
- late HashMap messageIds;
- late Uint8List imageBytes;
+ late Map messageIds;
late bool isRealTwonly;
late int maxShowTime;
late DateTime messageSendAt;
+
+ Metadata();
+
+ Map toJson() {
+ // Convert Map to Map for JSON encoding
+ Map 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 json) {
+ Metadata state = Metadata();
+ state.userIds = List.from(json['userIds']);
+
+ // Convert Map to Map
+ state.messageIds = (json['messageIds'] as Map)
+ .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 encryptionMac;
late List encryptedBytes;
late List encryptionNonce;
+
+ PrepareState();
+
+ Map toJson() {
+ return {
+ 'sha2Hash': sha2Hash,
+ 'encryptionKey': encryptionKey,
+ 'encryptionMac': encryptionMac,
+ 'encryptedBytes': encryptedBytes,
+ 'encryptionNonce': encryptionNonce,
+ };
+ }
+
+ factory PrepareState.fromJson(Map json) {
+ PrepareState state = PrepareState();
+ state.sha2Hash = List.from(json['sha2Hash']);
+ state.encryptionKey = List.from(json['encryptionKey']);
+ state.encryptionMac = List.from(json['encryptionMac']);
+ state.encryptedBytes = List.from(json['encryptedBytes']);
+ state.encryptionNonce = List.from(json['encryptionNonce']);
+ return state;
+ }
}
class UploadState {
late List uploadToken;
late List> downloadTokens;
+
+ UploadState();
+
+ Map toJson() {
+ return {
+ 'uploadToken': uploadToken,
+ 'downloadTokens': downloadTokens,
+ };
+ }
+
+ factory UploadState.fromJson(Map json) {
+ UploadState state = UploadState();
+ state.uploadToken = List.from(json['uploadToken']);
+ state.downloadTokens = List>.from(
+ json['downloadTokens'].map((token) => List.from(token)),
+ );
+ return state;
+ }
+}
+
+class States {
+ late Metadata metadata;
+ late PrepareState prepareState;
+
+ States({
+ required this.metadata,
+ required this.prepareState,
+ });
+
+ Map toJson() {
+ return {
+ 'metadata': metadata.toJson(),
+ 'prepareState': prepareState.toJson(),
+ };
+ }
+
+ factory States.fromJson(Map 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 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 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 allMediaFiles = jsonDecode(mediaFilesJson);
+ allMediaFiles.remove(stateId);
+ storage.put("mediaUploads", jsonEncode(allMediaFiles));
+ }
+ }
final notifyState =
await ImageUploader.notifyState(prepareState, uploadState, metadata);
diff --git a/lib/src/providers/api_provider.dart b/lib/src/providers/api_provider.dart
index 8817c65..64ff91f 100644
--- a/lib/src/providers/api_provider.dart
+++ b/lib/src/providers/api_provider.dart
@@ -74,6 +74,7 @@ class ApiProvider {
if (!globalIsAppInBackground) {
tryTransmitMessages();
+ retransmitMediaFiles();
tryDownloadAllMediaFiles();
notifyContactsAboutProfileChange();
}
diff --git a/lib/src/providers/hive.dart b/lib/src/providers/hive.dart
index f0effa9..e0fcc00 100644
--- a/lib/src/providers/hive.dart
+++ b/lib/src/providers/hive.dart
@@ -25,6 +25,8 @@ Future 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),
+ );
}
diff --git a/lib/src/views/chats/chat_list_view.dart b/lib/src/views/chats/chat_list_view.dart
index df0ad87..0ab9637 100644
--- a/lib/src/views/chats/chat_list_view.dart
+++ b/lib/src/views/chats/chat_list_view.dart
@@ -119,16 +119,22 @@ class _ChatListViewState extends State {
.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,
+ );
+ },
+ ),
);
},
),