start with downloading media

This commit is contained in:
otsmr 2025-01-27 23:29:16 +01:00
parent 5cea69c224
commit bae21c4738
15 changed files with 236 additions and 76 deletions

View file

@ -1,5 +1,8 @@
import 'package:flutter/foundation.dart';
import 'package:hive/hive.dart';
import 'package:path_provider/path_provider.dart';
import 'package:provider/provider.dart';
import 'package:twonly/src/providers/api/api.dart';
import 'package:twonly/src/providers/api_provider.dart';
import 'package:twonly/src/providers/db_provider.dart';
import 'package:flutter/material.dart';
@ -17,7 +20,7 @@ late ApiProvider apiProvider;
void main() async {
final settingsController = SettingsController(SettingsService());
// Load the user's peganreferred theme while the splash screen is displayed.
// Load the user's preferred theme while the splash screen is displayed.
// This prevents a sudden theme change when the app is first displayed.
await settingsController.loadSettings();
@ -33,6 +36,11 @@ void main() async {
}
});
final dir = await getApplicationDocumentsDirectory();
Hive.init(dir.path);
await initMediaStorage();
dbProvider = DbProvider();
// Database is just a file, so this will not block the loading of the app much
await dbProvider.ready;

View file

@ -25,18 +25,31 @@ class InitialsAvatar extends StatelessWidget {
Color avatarColor = _getColorFromUsername(
displayName, Theme.of(context).brightness == Brightness.dark);
return CircleAvatar(
backgroundColor: avatarColor,
radius: fontSize,
child: Text(
initials,
style: TextStyle(
color: _getTextColor(avatarColor),
fontWeight: FontWeight.normal,
fontSize: fontSize,
),
Widget child = Text(
initials,
style: TextStyle(
color: _getTextColor(avatarColor),
fontWeight: FontWeight.normal,
fontSize: fontSize,
),
);
bool isPro = initials[0] == "T";
double proSize = (fontSize == null) ? 40 : (fontSize! * 2);
return isPro
? ClipRRect(
borderRadius: BorderRadius.circular(12.0), //or 15.0
child: Container(
height: proSize,
width: proSize,
color: avatarColor,
child: Center(child: child),
),
)
: CircleAvatar(
backgroundColor: avatarColor, radius: fontSize, child: child);
}
Color _getTextColor(Color color) {

View file

@ -1,4 +1,6 @@
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/src/model/json/message.dart';
enum MessageSendState {
received,
@ -11,37 +13,39 @@ enum MessageSendState {
class MessageSendStateIcon extends StatelessWidget {
final MessageSendState state;
final MessageKind kind;
final bool isDownloaded;
const MessageSendStateIcon(this.state, {super.key});
const MessageSendStateIcon(this.state, this.isDownloaded, this.kind,
{super.key});
@override
Widget build(BuildContext context) {
Widget icon = Placeholder();
String text = "";
Color color = Theme.of(context).colorScheme.primary;
if (kind == MessageKind.textMessage) {
color = Colors.lightBlue;
} else if (kind == MessageKind.video) {
color = Colors.deepPurple;
}
switch (state) {
case MessageSendState.receivedOpened:
icon = Icon(Icons.crop_square, size: 14, color: color);
text = "Received";
break;
case MessageSendState.sendOpened:
icon = Icon(
Icons.crop_square,
size: 14,
color: Theme.of(context).colorScheme.primary,
);
icon = FaIcon(FontAwesomeIcons.paperPlane, size: 12, color: color);
text = "Opened";
break;
case MessageSendState.received:
icon = Icon(
Icons.square_rounded,
size: 14,
color: Theme.of(context).colorScheme.primary,
);
icon = Icon(Icons.square_rounded, size: 14, color: color);
text = "Received";
break;
case MessageSendState.send:
icon = Icon(
Icons.send,
size: 14,
);
icon = FaIcon(FontAwesomeIcons.solidPaperPlane, size: 12, color: color);
text = "Send";
break;
case MessageSendState.sending:
@ -51,9 +55,7 @@ class MessageSendStateIcon extends StatelessWidget {
SizedBox(
width: 10,
height: 10,
child: CircularProgressIndicator(
strokeWidth: 1,
),
child: CircularProgressIndicator(strokeWidth: 1, color: color),
),
SizedBox(width: 2),
],
@ -62,6 +64,10 @@ class MessageSendStateIcon extends StatelessWidget {
break;
}
if (!isDownloaded) {
text = "Tap do load";
}
return Row(
children: [
icon,

View file

@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:pie_menu/pie_menu.dart';
import 'package:twonly/src/model/contacts_model.dart';
@ -22,7 +23,6 @@ class _UserContextMenuState extends State<UserContextMenu> {
tooltip: const Text('Verify user'),
onSelect: () {
print('Verify user selected');
// Add your verification logic here
},
child: const Icon(Icons.gpp_maybe_rounded), // Can be any widget
),
@ -30,9 +30,8 @@ class _UserContextMenuState extends State<UserContextMenu> {
tooltip: const Text('Send image'),
onSelect: () {
print('Send image selected');
// Add your image sending logic here
},
child: const Icon(Icons.camera_alt_rounded), // Can be any widget
child: const FaIcon(FontAwesomeIcons.camera),
),
],
child: widget.child,

View file

@ -11,7 +11,7 @@ class DbMessage {
required this.messageId,
required this.messageOtherId,
required this.otherUserId,
required this.messageMessageKind,
required this.messageKind,
required this.messageContent,
required this.messageOpenedAt,
required this.messageAcknowledgeByUser,
@ -23,12 +23,17 @@ class DbMessage {
// is this null then the message was sent from the user itself
int? messageOtherId;
int otherUserId;
MessageKind messageMessageKind;
MessageKind messageKind;
MessageContent? messageContent;
DateTime? messageOpenedAt;
bool messageAcknowledgeByUser;
bool messageAcknowledgeByServer;
DateTime sendOrReceivedAt;
bool containsOtherMedia() {
if (messageOtherId == null) return false;
return messageKind == MessageKind.image || messageKind == MessageKind.video;
}
}
class DbMessages extends CvModelBase {
@ -44,7 +49,7 @@ class DbMessages extends CvModelBase {
final otherUserId = CvField<int>(columnOtherUserId);
static const columnMessageKind = "message_kind";
final messageMessageKind = CvField<int>(columnMessageKind);
final messageKind = CvField<int>(columnMessageKind);
static const columnMessageContentJson = "message_json";
final messageContentJson = CvField<String?>(columnMessageContentJson);
@ -135,7 +140,8 @@ class DbMessages extends CvModelBase {
columnMessageAcknowledgeByServer: 1,
columnMessageAcknowledgeByUser:
0, // ack in case of sending corresponds to the opened flag
columnOtherUserId: userIdFrom
columnOtherUserId: userIdFrom,
columnSendOrReceivedAt: DateTime.now().toIso8601String()
});
globalCallBackOnMessageChange(userIdFrom);
return true;
@ -204,7 +210,7 @@ class DbMessages extends CvModelBase {
@override
List<CvField> get fields => [
messageId,
messageMessageKind,
messageKind,
messageContentJson,
messageOpenedAt,
sendOrReceivedAt
@ -230,7 +236,7 @@ class DbMessages extends CvModelBase {
messageId: fromDb[i][columnMessageId],
messageOtherId: fromDb[i][columnMessageOtherId],
otherUserId: fromDb[i][columnOtherUserId],
messageMessageKind:
messageKind:
MessageKindExtension.fromIndex(fromDb[i][columnMessageKind]),
messageContent: content,
messageOpenedAt: messageOpenedAt,

View file

@ -1,6 +1,8 @@
import 'dart:convert';
import 'dart:io';
import 'package:fixnum/fixnum.dart';
import 'package:flutter/foundation.dart';
import 'package:hive/hive.dart';
import 'package:logging/logging.dart';
import 'package:twonly/main.dart';
import 'package:twonly/src/model/json/message.dart';
@ -104,4 +106,42 @@ Future tryDownloadMedia(List<int> imageToken, {bool force = false}) async {
print("check if free network connection");
print("Downloading: " + imageToken.toString());
final box = await getMediaStorage();
Uint8List imageBytes = Uint8List.fromList([0]);
box.put(imageToken.toString(), imageBytes);
}
Future<bool> isMediaDownloaded(List<int> mediaToken) async {
final box = await getMediaStorage();
// box.put('secret', 'Hive is awesome');
return box.containsKey(mediaToken.toString());
}
Future initMediaStorage() async {
final storage = getSecureStorage();
var containsEncryptionKey =
await storage.containsKey(key: 'hive_encryption_key');
if (!containsEncryptionKey) {
var key = Hive.generateSecureKey();
await storage.write(
key: 'hive_encryption_key',
value: base64UrlEncode(key),
);
}
}
Future<Box> getMediaStorage() async {
await initMediaStorage();
final storage = getSecureStorage();
var encryptionKey =
base64Url.decode((await storage.read(key: 'hive_encryption_key'))!);
return await Hive.openBox('media_storage',
encryptionCipher: HiveAesCipher(encryptionKey));
}

View file

@ -152,7 +152,9 @@ class ApiProvider {
Future<Result> _sendRequestV0(ClientToServer request) async {
if (_channel == null) {
return Result.error(ErrorCode.InternalError);
if (!await connect()) {
return Result.error(ErrorCode.InternalError);
}
}
var seq = Int64(Random().nextInt(4294967296));
while (messagesV0.containsKey(seq)) {

View file

@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/src/model/contacts_model.dart';
class AlignedTextBox extends StatelessWidget {
@ -39,8 +40,8 @@ class AlignedTextBox extends StatelessWidget {
}
/// Displays detailed information about a SampleItem.
class SampleItemDetailsView extends StatelessWidget {
const SampleItemDetailsView({super.key, required this.user});
class ChatItemDetailsView extends StatelessWidget {
const ChatItemDetailsView({super.key, required this.user});
final Contact user;
@ -86,24 +87,50 @@ class SampleItemDetailsView extends StatelessWidget {
},
),
),
const SizedBox(
height: 10,
),
Padding(
padding: const EdgeInsets.only(bottom: 40, left: 10, right: 10),
child: TextField(
decoration: InputDecoration(
// border: OutlineInputBorder(),
labelText: 'Enter your message',
suffixIcon: IconButton(
icon: Icon(Icons.send),
padding:
const EdgeInsets.only(bottom: 30, left: 20, right: 20, top: 10),
child: Row(
children: [
Expanded(
child: TextField(
// controller: _controller,
decoration: InputDecoration(
hintText: 'Type a message',
contentPadding: EdgeInsets.symmetric(horizontal: 10)
// border: OutlineInputBorder(),
),
),
),
SizedBox(width: 8),
IconButton(
icon: FaIcon(FontAwesomeIcons.solidPaperPlane),
onPressed: () {
// Handle send action
},
),
),
],
),
),
// Container(
// child: Row(children: [
// Padding(
// padding: const EdgeInsets.only(bottom: 40, left: 10, right: 10),
// child: TextField(
// decoration: InputDecoration(
// // border: OutlineInputBorder(),
// hintText: 'Enter your message',
// ),
// ),
// ),
// IconButton(
// icon: FaIcon(FontAwesomeIcons.solidPaperPlane),
// onPressed: () {
// // Handle send action
// },
// ),
// ]),
// ),
],
),
);

View file

@ -5,11 +5,13 @@ import 'package:twonly/src/components/notification_badge.dart';
import 'package:twonly/src/components/user_context_menu.dart';
import 'package:twonly/src/model/contacts_model.dart';
import 'package:twonly/src/model/messages_model.dart';
import 'package:twonly/src/providers/api/api.dart';
import 'package:twonly/src/providers/contacts_change_provider.dart';
import 'package:twonly/src/providers/messages_change_provider.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/chat_item_details_view.dart';
import 'package:twonly/src/views/home_view.dart';
import 'package:twonly/src/views/media_viewer_view.dart';
import 'package:twonly/src/views/search_username_view.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter/material.dart';
@ -47,6 +49,11 @@ class _ChatListViewState extends State<ChatListView> {
List<Contact> activeUsers = allUsers
.where((x) => lastMessages.containsKey(x.userId.toInt()))
.toList();
activeUsers.sort((b, a) {
return lastMessages[a.userId.toInt()]!
.sendOrReceivedAt
.compareTo(lastMessages[b.userId.toInt()]!.sendOrReceivedAt);
});
return Scaffold(
appBar: AppBar(
@ -125,24 +132,28 @@ class UserListItem extends StatefulWidget {
class _UserListItem extends State<UserListItem> {
int flames = 0;
int lastMessageInSeconds = 0;
bool isDownloaded = true;
@override
void initState() {
super.initState();
//_loadAsync();
_loadAsync();
}
// Future _loadAsync() async {
// flames = await widget.user.getFlames();
// lastMessageInSeconds = await widget.user.getLastMessageInSeconds();
// setState(() {});
// }
Future _loadAsync() async {
// flames = await widget.user.getFlames();
// setState(() {});
if (widget.lastMessage.containsOtherMedia()) {
isDownloaded = await isMediaDownloaded(
widget.lastMessage.messageContent!.downloadToken!);
setState(() {});
}
}
@override
Widget build(BuildContext context) {
MessageSendState state;
// int lastMessageInSeconds = widget.lastMessage.sendOrReceivedAt;
//print(widget.lastMessage.sendOrReceivedAt);
int lastMessageInSeconds = DateTime.now()
.difference(widget.lastMessage.sendOrReceivedAt)
.inSeconds;
@ -173,7 +184,8 @@ class _UserListItem extends State<UserListItem> {
title: Text(widget.user.displayName),
subtitle: Row(
children: [
MessageSendStateIcon(state),
MessageSendStateIcon(
state, isDownloaded, widget.lastMessage.messageKind),
Text(""),
const SizedBox(width: 5),
Text(
@ -203,11 +215,19 @@ class _UserListItem extends State<UserListItem> {
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SampleItemDetailsView(
user: widget.user,
),
),
MaterialPageRoute(builder: (context) {
if (state == MessageSendState.received &&
widget.lastMessage.containsOtherMedia()) {
List<int> token =
widget.lastMessage.messageContent!.downloadToken!;
if (isDownloaded) {
return MediaViewerView(widget.user);
} else {
tryDownloadMedia(token);
}
}
return ChatItemDetailsView(user: widget.user);
}),
);
},
),

View file

@ -1,3 +1,4 @@
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:pie_menu/pie_menu.dart';
import 'camera_preview_view.dart';
import 'chat_list_view.dart';
@ -79,12 +80,14 @@ class HomeViewState extends State<HomeView> {
selectedIconTheme:
IconThemeData(color: const Color.fromARGB(255, 255, 255, 255)),
items: [
BottomNavigationBarItem(icon: Icon(Icons.chat), label: ""),
BottomNavigationBarItem(
icon: Icon(Icons.camera_alt),
icon: FaIcon(FontAwesomeIcons.solidComments), label: ""),
BottomNavigationBarItem(
icon: FaIcon(FontAwesomeIcons.camera),
label: "",
),
BottomNavigationBarItem(icon: Icon(Icons.verified_user), label: ""),
BottomNavigationBarItem(
icon: FaIcon(FontAwesomeIcons.userShield), label: ""),
],
onTap: (int index) {
activePageIdx = index;

View file

@ -0,0 +1,19 @@
import 'package:flutter/material.dart';
import 'package:twonly/src/model/contacts_model.dart';
class MediaViewerView extends StatefulWidget {
final Contact otherUser;
const MediaViewerView(this.otherUser, {super.key});
@override
State<MediaViewerView> createState() => _MediaViewerViewState();
}
class _MediaViewerViewState extends State<MediaViewerView> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Text(widget.otherUser.displayName),
);
}
}

View file

@ -1,6 +1,7 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/share_image_view.dart';
@ -63,10 +64,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
// mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
icon: Icon(
Icons.close,
size: 30,
),
icon: Icon(Icons.close, size: 30),
color: Colors.white,
onPressed: () async {
Navigator.pop(context);
@ -87,7 +85,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
OutlinedButton.icon(
icon: _imageSaved
? Icon(Icons.check)
: Icon(Icons.save_rounded),
: FaIcon(FontAwesomeIcons.floppyDisk),
style: OutlinedButton.styleFrom(
iconColor: _imageSaved
? Theme.of(context).colorScheme.outline
@ -113,7 +111,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
),
const SizedBox(width: 20),
FilledButton.icon(
icon: Icon(Icons.send),
icon: FaIcon(FontAwesomeIcons.solidPaperPlane),
onPressed: () async {
Navigator.push(
context,

View file

@ -2,6 +2,7 @@ import 'dart:collection';
import 'package:fixnum/fixnum.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/src/components/best_friends_selector.dart';
import 'package:twonly/src/components/headline.dart';
import 'package:twonly/src/components/initialsavatar.dart';
@ -107,7 +108,7 @@ class _ShareImageView extends State<ShareImageView> {
mainAxisAlignment: MainAxisAlignment.end,
children: [
FilledButton.icon(
icon: Icon(Icons.send),
icon: FaIcon(FontAwesomeIcons.solidPaperPlane),
onPressed: () async {
sendImage(_selectedUserIds.toList(), widget.image);

View file

@ -490,6 +490,14 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
font_awesome_flutter:
dependency: "direct main"
description:
name: font_awesome_flutter
sha256: d3a89184101baec7f4600d58840a764d2ef760fe1c5a20ef9e6b0e9b24a07a3a
url: "https://pub.dev"
source: hosted
version: "10.8.0"
frontend_server_client:
dependency: transitive
description:
@ -530,6 +538,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.3.2"
hive:
dependency: "direct main"
description:
name: hive
sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941"
url: "https://pub.dev"
source: hosted
version: "2.2.3"
http:
dependency: transitive
description:

View file

@ -20,8 +20,10 @@ dependencies:
flutter_localizations:
sdk: flutter
flutter_secure_storage: ^9.2.2
font_awesome_flutter: ^10.8.0
gal: ^2.3.1
google_fonts: ^6.2.1
hive: ^2.2.3
image: ^4.3.0
intl: any
introduction_screen: ^3.1.14