add receiver side for media

This commit is contained in:
otsmr 2025-02-05 00:43:41 +01:00
parent 2e7b0edce3
commit 384bafe67b
13 changed files with 291 additions and 77 deletions

View file

@ -34,6 +34,7 @@
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.RECORD_AUDIO"/> <uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.CAMERA"/> <uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.USE_BIOMETRIC"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="29" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="29" />
<!-- Required to query activities that can process text, see: <!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and https://developer.android.com/training/package-visibility and

View file

@ -1,5 +1,5 @@
package com.example.connect package com.example.connect
import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.android.FlutterFragmentActivity
class MainActivity: FlutterActivity() class MainActivity: FlutterFragmentActivity()

File diff suppressed because one or more lines are too long

Binary file not shown.

View file

@ -48,6 +48,8 @@
<key>NSCameraUsageDescription</key> <key>NSCameraUsageDescription</key>
<string>To create photos that can be shared.</string> <string>To create photos that can be shared.</string>
<key>NSMicrophoneUsageDescription</key> <key>NSMicrophoneUsageDescription</key>
<string>Explanation on why the microphone access is needed.</string> <string>To create videos that can be securely shared.</string>
<key>NSFaceIDUsageDescription</key>
<string>To protect others twonlies!</string>
</dict> </dict>
</plist> </plist>

View file

@ -28,7 +28,7 @@ class MessageSendStateIcon extends StatelessWidget {
String text = ""; String text = "";
Color color = Color color =
message.messageKind.getColor(Theme.of(context).colorScheme.primary); message.messageContent.getColor(Theme.of(context).colorScheme.primary);
Widget loaderIcon = Row( Widget loaderIcon = Row(
children: [ children: [

View file

@ -23,16 +23,6 @@ extension MessageKindExtension on MessageKind {
static MessageKind fromIndex(int index) { static MessageKind fromIndex(int index) {
return MessageKind.values[index]; return MessageKind.values[index];
} }
Color getColor(Color primary) {
Color color = primary;
if (this == MessageKind.textMessage) {
color = Colors.lightBlue;
} else if (this == MessageKind.video) {
color = Colors.deepPurple;
}
return color;
}
} }
// TODO: use message as base class, remove kind and flatten content // TODO: use message as base class, remove kind and flatten content
@ -72,6 +62,29 @@ class Message {
class MessageContent { class MessageContent {
MessageContent(); MessageContent();
Color getColor(Color primary) {
Color color;
if (this is TextMessageContent) {
color = Colors.lightBlue;
} else {
final content = this;
if (content is MediaMessageContent) {
if (content.isRealTwonly) {
color = primary;
} else {
if (content.isVideo) {
color = Colors.deepPurple;
} else {
color = const Color.fromARGB(255, 214, 47, 47);
}
}
} else {
return Colors.black; // this should not happen
}
}
return color;
}
static MessageContent fromJson(Map json) { static MessageContent fromJson(Map json) {
switch (json['type']) { switch (json['type']) {
case 'MediaMessageContent': case 'MediaMessageContent':
@ -92,10 +105,12 @@ class MediaMessageContent extends MessageContent {
final List<int> downloadToken; final List<int> downloadToken;
final int maxShowTime; final int maxShowTime;
final bool isRealTwonly; final bool isRealTwonly;
final bool isVideo;
MediaMessageContent({ MediaMessageContent({
required this.downloadToken, required this.downloadToken,
required this.maxShowTime, required this.maxShowTime,
required this.isRealTwonly, required this.isRealTwonly,
required this.isVideo,
}); });
static MediaMessageContent fromJson(Map json) { static MediaMessageContent fromJson(Map json) {
@ -103,6 +118,7 @@ class MediaMessageContent extends MessageContent {
downloadToken: List<int>.from(json['downloadToken']), downloadToken: List<int>.from(json['downloadToken']),
maxShowTime: json['maxShowTime'], maxShowTime: json['maxShowTime'],
isRealTwonly: json['isRealTwonly'], isRealTwonly: json['isRealTwonly'],
isVideo: json['isVideo'] ?? false,
); );
} }

View file

@ -27,7 +27,7 @@ class DbMessage {
int? messageOtherId; int? messageOtherId;
int otherUserId; int otherUserId;
MessageKind messageKind; MessageKind messageKind;
MessageContent? messageContent; MessageContent messageContent;
DateTime? messageOpenedAt; DateTime? messageOpenedAt;
bool messageAcknowledgeByUser; bool messageAcknowledgeByUser;
bool isDownloaded; bool isDownloaded;

View file

@ -155,7 +155,7 @@ Future uploadMediaFile(
downloadToken: uploadToken, downloadToken: uploadToken,
maxShowTime: maxShowTime, maxShowTime: maxShowTime,
isRealTwonly: isRealTwonly, isRealTwonly: isRealTwonly,
), isVideo: false),
timestamp: DateTime.now(), timestamp: DateTime.now(),
), ),
); );
@ -174,7 +174,7 @@ Future encryptAndUploadMediaFile(
downloadToken: [], downloadToken: [],
maxShowTime: maxShowTime, maxShowTime: maxShowTime,
isRealTwonly: isRealTwonly, isRealTwonly: isRealTwonly,
)); isVideo: false));
// isRealTwonly, // isRealTwonly,
if (messageId == null) return; if (messageId == null) return;
@ -252,11 +252,13 @@ Future<Uint8List?> getDownloadedMedia(
List<int> mediaToken, int messageOtherId) async { List<int> mediaToken, int messageOtherId) async {
final box = await getMediaStorage(); final box = await getMediaStorage();
Uint8List? media = box.get("${mediaToken}_downloaded"); Uint8List? media = box.get("${mediaToken}_downloaded");
int fromUserId = box.get("${mediaToken}_fromUserId");
await userOpenedOtherMessage(fromUserId, messageOtherId); // int fromUserId = box.get("${mediaToken}_fromUserId");
// await userOpenedOtherMessage(fromUserId, messageOtherId);
// box.delete(mediaToken.toString()); // box.delete(mediaToken.toString());
// box.put("${mediaToken}_downloaded", "deleted"); // box.put("${mediaToken}_downloaded", "deleted");
// box.delete("${mediaToken}_fromUserId"); // box.delete("${mediaToken}_fromUserId");
return media; return media;
} }

View file

@ -67,8 +67,8 @@ class ChatListEntry extends StatelessWidget {
} }
break; break;
case MessageKind.image: case MessageKind.image:
Color color = Color color = message.messageContent
message.messageKind.getColor(Theme.of(context).colorScheme.primary); .getColor(Theme.of(context).colorScheme.primary);
child = GestureDetector( child = GestureDetector(
onTap: () { onTap: () {
if (state == MessageSendState.received && !isDownloading) { if (state == MessageSendState.received && !isDownloading) {

View file

@ -1,6 +1,9 @@
import 'dart:typed_data'; import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:local_auth/local_auth.dart';
import 'package:lottie/lottie.dart';
import 'package:twonly/src/components/media_view_sizing.dart'; import 'package:twonly/src/components/media_view_sizing.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';
@ -18,37 +21,149 @@ class MediaViewerView extends StatefulWidget {
class _MediaViewerViewState extends State<MediaViewerView> { class _MediaViewerViewState extends State<MediaViewerView> {
Uint8List? _imageByte; Uint8List? _imageByte;
DateTime? canBeSeenUntil;
int maxShowTime = 999999;
bool isRealTwonly = false;
// DateTime opened;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_initAsync();
}
Future _initAsync() async {
final content = widget.message.messageContent; final content = widget.message.messageContent;
if (content is MediaMessageContent) { if (content is MediaMessageContent) {
if (content.isRealTwonly) {
isRealTwonly = true;
}
}
loadMedia();
}
Future loadMedia({bool force = false}) async {
final content = widget.message.messageContent;
if (content is MediaMessageContent) {
if (content.isRealTwonly) {
if (!force) {
return;
}
try {
final LocalAuthentication auth = LocalAuthentication();
bool didAuthenticate = await auth.authenticate(
localizedReason: 'Please authenticate to see this twonly!',
options: const AuthenticationOptions(useErrorDialogs: false));
if (!didAuthenticate) {
if (context.mounted) {
Navigator.pop(context);
}
return;
}
} on PlatformException catch (e) {
debugPrint(e.toString());
// these errors because of hardware not available or bio is not enrolled
// as this is just a nice gimig, do not interrupt the user experience
}
}
List<int> token = content.downloadToken; List<int> token = content.downloadToken;
_imageByte = _imageByte =
await getDownloadedMedia(token, widget.message.messageOtherId!); await getDownloadedMedia(token, widget.message.messageOtherId!);
setState(() {}); setState(() {});
} }
} }
startTimer() {
Future.delayed(canBeSeenUntil!.difference(DateTime.now()), () {
if (context.mounted) {
Navigator.pop(context);
}
});
}
mediaOpened() {
if (canBeSeenUntil != null) return;
final content = widget.message.messageContent;
if (content is MediaMessageContent) {
if (content.maxShowTime != 999999) {
canBeSeenUntil = DateTime.now().add(
Duration(seconds: content.maxShowTime),
);
maxShowTime = content.maxShowTime;
startTimer();
}
}
}
@override
void dispose() {
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (_imageByte == null) return Container(); double progress = 0;
if (canBeSeenUntil != null) {
Duration difference = canBeSeenUntil!.difference(DateTime.now());
print(difference.inMilliseconds);
// Calculate the progress as a value between 0.0 and 1.0
progress = (difference.inMilliseconds / (maxShowTime * 1000));
if (progress <= 0) {
return Scaffold();
}
}
// progress = 0.8;
return Scaffold( return Scaffold(
body: Stack( body: SafeArea(
child: Stack(
fit: StackFit.expand, fit: StackFit.expand,
children: [ children: [
MediaViewSizing(Image.memory( if (_imageByte != null)
MediaViewSizing(
Image.memory(
_imageByte!, _imageByte!,
fit: BoxFit.contain, fit: BoxFit.contain,
)), frameBuilder:
((context, child, frame, wasSynchronouslyLoaded) {
if (frame != null || wasSynchronouslyLoaded) {
mediaOpened();
}
if (wasSynchronouslyLoaded) return child;
return AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
child: frame != null
? child
: SizedBox(
height: 60,
width: 60,
child: CircularProgressIndicator(strokeWidth: 6),
),
);
}),
),
),
if (isRealTwonly && _imageByte == null)
Positioned.fill(
child: GestureDetector(
onTap: () {
loadMedia(force: true);
},
child: Column(
children: [
Expanded(
child: Lottie.asset(
'assets/animations/present.lottie.json'),
),
Container(
padding: EdgeInsets.only(bottom: 200),
child: Text("Tap to open your twonly!"),
),
],
),
),
),
Positioned( Positioned(
left: 10, left: 10,
top: 60, top: 10,
child: Row( child: Row(
// mainAxisAlignment: MainAxisAlignment.center, // mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
@ -63,13 +178,30 @@ class _MediaViewerViewState extends State<MediaViewerView> {
), ),
), ),
Positioned( Positioned(
bottom: 70, right: 20,
top: 27,
child: Row(
children: [
if (canBeSeenUntil != null)
SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
value: progress,
strokeWidth: 2.0,
)),
],
),
),
if (_imageByte != null)
Positioned(
bottom: 30,
left: 0, left: 0,
right: 0, right: 0,
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
const SizedBox(width: 20), // const SizedBox(width: 20),
FilledButton.icon( FilledButton.icon(
icon: FaIcon(FontAwesomeIcons.solidPaperPlane), icon: FaIcon(FontAwesomeIcons.solidPaperPlane),
onPressed: () async {}, onPressed: () async {},
@ -85,9 +217,10 @@ class _MediaViewerViewState extends State<MediaViewerView> {
), ),
], ],
), ),
) ),
], ],
), ),
),
); );
} }
} }

View file

@ -432,6 +432,14 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
name: flutter_plugin_android_lifecycle
sha256: "615a505aef59b151b46bbeef55b36ce2b6ed299d160c51d84281946f0aa0ce0e"
url: "https://pub.dev"
source: hosted
version: "2.0.24"
flutter_secure_storage: flutter_secure_storage:
dependency: "direct main" dependency: "direct main"
description: description:
@ -674,6 +682,46 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.1.1" version: "5.1.1"
local_auth:
dependency: "direct main"
description:
name: local_auth
sha256: "434d854cf478f17f12ab29a76a02b3067f86a63a6d6c4eb8fbfdcfe4879c1b7b"
url: "https://pub.dev"
source: hosted
version: "2.3.0"
local_auth_android:
dependency: transitive
description:
name: local_auth_android
sha256: "6763aaf8965f21822624cb2fd3c03d2a8b3791037b5efb0fe4b13e110f5afc92"
url: "https://pub.dev"
source: hosted
version: "1.0.46"
local_auth_darwin:
dependency: transitive
description:
name: local_auth_darwin
sha256: "630996cd7b7f28f5ab92432c4b35d055dd03a747bc319e5ffbb3c4806a3e50d2"
url: "https://pub.dev"
source: hosted
version: "1.4.3"
local_auth_platform_interface:
dependency: transitive
description:
name: local_auth_platform_interface
sha256: "1b842ff177a7068442eae093b64abe3592f816afd2a533c0ebcdbe40f9d2075a"
url: "https://pub.dev"
source: hosted
version: "1.0.10"
local_auth_windows:
dependency: transitive
description:
name: local_auth_windows
sha256: bc4e66a29b0fdf751aafbec923b5bed7ad6ed3614875d8151afe2578520b2ab5
url: "https://pub.dev"
source: hosted
version: "1.0.11"
logging: logging:
dependency: "direct main" dependency: "direct main"
description: description:
@ -682,6 +730,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.0" version: "1.3.0"
lottie:
dependency: "direct main"
description:
name: lottie
sha256: c5fa04a80a620066c15cf19cc44773e19e9b38e989ff23ea32e5903ef1015950
url: "https://pub.dev"
source: hosted
version: "3.3.1"
macros: macros:
dependency: transitive dependency: transitive
description: description:
@ -1217,4 +1273,4 @@ packages:
version: "3.1.3" version: "3.1.3"
sdks: sdks:
dart: ">=3.6.0 <4.0.0" dart: ">=3.6.0 <4.0.0"
flutter: ">=3.24.0" flutter: ">=3.27.0"

View file

@ -33,7 +33,9 @@ dependencies:
introduction_screen: ^3.1.14 introduction_screen: ^3.1.14
json_annotation: ^4.9.0 json_annotation: ^4.9.0
libsignal_protocol_dart: ^0.7.1 libsignal_protocol_dart: ^0.7.1
local_auth: ^2.3.0
logging: ^1.3.0 logging: ^1.3.0
lottie: ^3.3.1
path: ^1.9.0 path: ^1.9.0
path_provider: ^2.1.5 path_provider: ^2.1.5
permission_handler: ^11.3.1 permission_handler: ^11.3.1
@ -72,6 +74,7 @@ flutter:
assets: assets:
# Add assets from the images directory to the application. # Add assets from the images directory to the application.
- assets/images/ - assets/images/
- assets/animations/present.lottie.json
- assets/icons/ - assets/icons/
- assets/icons/flame.png - assets/icons/flame.png
- assets/images/onboarding/01.png - assets/images/onboarding/01.png