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.RECORD_AUDIO"/>
<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" />
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and

View file

@ -1,5 +1,5 @@
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>
<string>To create photos that can be shared.</string>
<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>
</plist>

View file

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

View file

@ -23,16 +23,6 @@ extension MessageKindExtension on MessageKind {
static MessageKind fromIndex(int 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
@ -72,6 +62,29 @@ class Message {
class 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) {
switch (json['type']) {
case 'MediaMessageContent':
@ -92,10 +105,12 @@ class MediaMessageContent extends MessageContent {
final List<int> downloadToken;
final int maxShowTime;
final bool isRealTwonly;
final bool isVideo;
MediaMessageContent({
required this.downloadToken,
required this.maxShowTime,
required this.isRealTwonly,
required this.isVideo,
});
static MediaMessageContent fromJson(Map json) {
@ -103,6 +118,7 @@ class MediaMessageContent extends MessageContent {
downloadToken: List<int>.from(json['downloadToken']),
maxShowTime: json['maxShowTime'],
isRealTwonly: json['isRealTwonly'],
isVideo: json['isVideo'] ?? false,
);
}

View file

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

View file

@ -152,10 +152,10 @@ Future uploadMediaFile(
kind: MessageKind.image,
messageId: messageId,
content: MediaMessageContent(
downloadToken: uploadToken,
maxShowTime: maxShowTime,
isRealTwonly: isRealTwonly,
),
downloadToken: uploadToken,
maxShowTime: maxShowTime,
isRealTwonly: isRealTwonly,
isVideo: false),
timestamp: DateTime.now(),
),
);
@ -171,10 +171,10 @@ Future encryptAndUploadMediaFile(
target.toInt(),
MessageKind.image,
MediaMessageContent(
downloadToken: [],
maxShowTime: maxShowTime,
isRealTwonly: isRealTwonly,
));
downloadToken: [],
maxShowTime: maxShowTime,
isRealTwonly: isRealTwonly,
isVideo: false));
// isRealTwonly,
if (messageId == null) return;
@ -252,11 +252,13 @@ Future<Uint8List?> getDownloadedMedia(
List<int> mediaToken, int messageOtherId) async {
final box = await getMediaStorage();
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.put("${mediaToken}_downloaded", "deleted");
// box.delete("${mediaToken}_fromUserId");
return media;
}

View file

@ -67,8 +67,8 @@ class ChatListEntry extends StatelessWidget {
}
break;
case MessageKind.image:
Color color =
message.messageKind.getColor(Theme.of(context).colorScheme.primary);
Color color = message.messageContent
.getColor(Theme.of(context).colorScheme.primary);
child = GestureDetector(
onTap: () {
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/services.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/model/contacts_model.dart';
import 'package:twonly/src/model/json/message.dart';
@ -18,75 +21,205 @@ class MediaViewerView extends StatefulWidget {
class _MediaViewerViewState extends State<MediaViewerView> {
Uint8List? _imageByte;
DateTime? canBeSeenUntil;
int maxShowTime = 999999;
bool isRealTwonly = false;
// DateTime opened;
@override
void initState() {
super.initState();
_initAsync();
}
Future _initAsync() async {
final content = widget.message.messageContent;
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;
_imageByte =
await getDownloadedMedia(token, widget.message.messageOtherId!);
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
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(
body: Stack(
fit: StackFit.expand,
children: [
MediaViewSizing(Image.memory(
_imageByte!,
fit: BoxFit.contain,
)),
Positioned(
left: 10,
top: 60,
child: Row(
// mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
icon: Icon(Icons.close, size: 30),
color: Colors.white,
onPressed: () async {
Navigator.pop(context);
body: SafeArea(
child: Stack(
fit: StackFit.expand,
children: [
if (_imageByte != null)
MediaViewSizing(
Image.memory(
_imageByte!,
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(
left: 10,
top: 10,
child: Row(
// mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
icon: Icon(Icons.close, size: 30),
color: Colors.white,
onPressed: () async {
Navigator.pop(context);
},
),
],
),
),
),
Positioned(
bottom: 70,
left: 0,
right: 0,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const SizedBox(width: 20),
FilledButton.icon(
icon: FaIcon(FontAwesomeIcons.solidPaperPlane),
onPressed: () async {},
style: ButtonStyle(
padding: WidgetStateProperty.all<EdgeInsets>(
EdgeInsets.symmetric(vertical: 10, horizontal: 30),
Positioned(
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,
right: 0,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// const SizedBox(width: 20),
FilledButton.icon(
icon: FaIcon(FontAwesomeIcons.solidPaperPlane),
onPressed: () async {},
style: ButtonStyle(
padding: WidgetStateProperty.all<EdgeInsets>(
EdgeInsets.symmetric(vertical: 10, horizontal: 30),
),
),
label: Text(
"Respond",
style: TextStyle(fontSize: 17),
),
),
),
label: Text(
"Respond",
style: TextStyle(fontSize: 17),
),
],
),
],
),
)
],
),
],
),
),
);
}

View file

@ -432,6 +432,14 @@ packages:
description: flutter
source: sdk
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:
dependency: "direct main"
description:
@ -674,6 +682,46 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: "direct main"
description:
@ -682,6 +730,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.3.0"
lottie:
dependency: "direct main"
description:
name: lottie
sha256: c5fa04a80a620066c15cf19cc44773e19e9b38e989ff23ea32e5903ef1015950
url: "https://pub.dev"
source: hosted
version: "3.3.1"
macros:
dependency: transitive
description:
@ -1217,4 +1273,4 @@ packages:
version: "3.1.3"
sdks:
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
json_annotation: ^4.9.0
libsignal_protocol_dart: ^0.7.1
local_auth: ^2.3.0
logging: ^1.3.0
lottie: ^3.3.1
path: ^1.9.0
path_provider: ^2.1.5
permission_handler: ^11.3.1
@ -72,6 +74,7 @@ flutter:
assets:
# Add assets from the images directory to the application.
- assets/images/
- assets/animations/present.lottie.json
- assets/icons/
- assets/icons/flame.png
- assets/images/onboarding/01.png