mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-01-15 09:28:41 +00:00
add receiver side for media
This commit is contained in:
parent
2e7b0edce3
commit
384bafe67b
13 changed files with 291 additions and 77 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
1
assets/animations/present.lottie.json
Normal file
1
assets/animations/present.lottie.json
Normal file
File diff suppressed because one or more lines are too long
BIN
assets/animations/present.lottie.zip
Normal file
BIN
assets/animations/present.lottie.zip
Normal file
Binary file not shown.
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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: [
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ class DbMessage {
|
|||
int? messageOtherId;
|
||||
int otherUserId;
|
||||
MessageKind messageKind;
|
||||
MessageContent? messageContent;
|
||||
MessageContent messageContent;
|
||||
DateTime? messageOpenedAt;
|
||||
bool messageAcknowledgeByUser;
|
||||
bool isDownloaded;
|
||||
|
|
|
|||
|
|
@ -155,7 +155,7 @@ Future uploadMediaFile(
|
|||
downloadToken: uploadToken,
|
||||
maxShowTime: maxShowTime,
|
||||
isRealTwonly: isRealTwonly,
|
||||
),
|
||||
isVideo: false),
|
||||
timestamp: DateTime.now(),
|
||||
),
|
||||
);
|
||||
|
|
@ -174,7 +174,7 @@ Future encryptAndUploadMediaFile(
|
|||
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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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,37 +21,149 @@ 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(
|
||||
body: SafeArea(
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
MediaViewSizing(Image.memory(
|
||||
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: 60,
|
||||
top: 10,
|
||||
child: Row(
|
||||
// mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
|
|
@ -63,13 +178,30 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
),
|
||||
),
|
||||
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,
|
||||
right: 0,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const SizedBox(width: 20),
|
||||
// const SizedBox(width: 20),
|
||||
FilledButton.icon(
|
||||
icon: FaIcon(FontAwesomeIcons.solidPaperPlane),
|
||||
onPressed: () async {},
|
||||
|
|
@ -85,9 +217,10 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
58
pubspec.lock
58
pubspec.lock
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue