sending an receiving works

This commit is contained in:
otsmr 2025-01-30 22:25:37 +01:00
parent e0d420b78d
commit 59146674b6
16 changed files with 217 additions and 83 deletions

View file

@ -7,6 +7,7 @@ import 'package:twonly/src/providers/api_provider.dart';
import 'package:twonly/src/providers/db_provider.dart'; import 'package:twonly/src/providers/db_provider.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:twonly/src/providers/download_change_provider.dart';
import 'package:twonly/src/providers/messages_change_provider.dart'; import 'package:twonly/src/providers/messages_change_provider.dart';
import 'package:twonly/src/providers/contacts_change_provider.dart'; import 'package:twonly/src/providers/contacts_change_provider.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
@ -61,6 +62,7 @@ void main() async {
MultiProvider( MultiProvider(
providers: [ providers: [
ChangeNotifierProvider(create: (_) => MessagesChangeProvider()), ChangeNotifierProvider(create: (_) => MessagesChangeProvider()),
ChangeNotifierProvider(create: (_) => DownloadChangeProvider()),
ChangeNotifierProvider(create: (_) => ContactChangeProvider()), ChangeNotifierProvider(create: (_) => ContactChangeProvider()),
], ],
child: MyApp(settingsController: settingsController), child: MyApp(settingsController: settingsController),

View file

@ -1,6 +1,7 @@
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:twonly/main.dart'; import 'package:twonly/main.dart';
import 'package:twonly/src/providers/contacts_change_provider.dart'; import 'package:twonly/src/providers/contacts_change_provider.dart';
import 'package:twonly/src/providers/download_change_provider.dart';
import 'package:twonly/src/providers/messages_change_provider.dart'; import 'package:twonly/src/providers/messages_change_provider.dart';
import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/utils/storage.dart';
import 'package:twonly/src/views/onboarding_view.dart'; import 'package:twonly/src/views/onboarding_view.dart';
@ -22,6 +23,7 @@ Function(bool) globalCallbackConnectionState = (a) {};
// these two callbacks are called on updated to the corresponding database // these two callbacks are called on updated to the corresponding database
Function globalCallBackOnContactChange = () {}; Function globalCallBackOnContactChange = () {};
Function(int) globalCallBackOnMessageChange = (a) {}; Function(int) globalCallBackOnMessageChange = (a) {};
Function(List<int>, bool) globalCallBackOnDownloadChange = (a, b) {};
/// The Widget that configures your application. /// The Widget that configures your application.
class MyApp extends StatefulWidget { class MyApp extends StatefulWidget {
@ -61,6 +63,10 @@ class _MyAppState extends State<MyApp> {
context.read<ContactChangeProvider>().update(); context.read<ContactChangeProvider>().update();
}; };
globalCallBackOnDownloadChange = (token, add) {
context.read<DownloadChangeProvider>().update(token, add);
};
globalCallBackOnMessageChange = (userId) { globalCallBackOnMessageChange = (userId) {
context.read<MessagesChangeProvider>().updateLastMessageFor(userId); context.read<MessagesChangeProvider>().updateLastMessageFor(userId);
}; };
@ -73,6 +79,7 @@ class _MyAppState extends State<MyApp> {
void dispose() { void dispose() {
// disable globalCallbacks to the flutter tree // disable globalCallbacks to the flutter tree
globalCallbackConnectionState = (a) {}; globalCallbackConnectionState = (a) {};
globalCallBackOnDownloadChange = (a, b) {};
globalCallBackOnContactChange = () {}; globalCallBackOnContactChange = () {};
globalCallBackOnMessageChange = (a) {}; globalCallBackOnMessageChange = (a) {};
super.dispose(); super.dispose();
@ -143,7 +150,8 @@ class _MyAppState extends State<MyApp> {
if (snapshot.hasData) { if (snapshot.hasData) {
return snapshot.data! return snapshot.data!
? HomeView( ? HomeView(
settingsController: widget.settingsController) settingsController: widget.settingsController,
)
: _showOnboarding : _showOnboarding
? OnboardingView( ? OnboardingView(
callbackOnSuccess: () { callbackOnSuccess: () {
@ -152,10 +160,12 @@ class _MyAppState extends State<MyApp> {
}); });
}, },
) )
: RegisterView(callbackOnSuccess: () { : RegisterView(
_isUserCreated = isUserCreated(); callbackOnSuccess: () {
setState(() {}); _isUserCreated = isUserCreated();
}); setState(() {});
},
);
} else { } else {
return Container(); return Container();
} }

View file

@ -1,6 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:provider/provider.dart';
import 'package:twonly/src/model/json/message.dart'; import 'package:twonly/src/model/json/message.dart';
import 'package:twonly/src/model/messages_model.dart';
import 'package:twonly/src/providers/download_change_provider.dart';
enum MessageSendState { enum MessageSendState {
received, received,
@ -12,21 +15,32 @@ enum MessageSendState {
} }
class MessageSendStateIcon extends StatelessWidget { class MessageSendStateIcon extends StatelessWidget {
final MessageSendState state; final DbMessage message;
final MessageKind kind; final MainAxisAlignment mainAxisAlignment;
final bool isDownloaded;
const MessageSendStateIcon(this.state, this.isDownloaded, this.kind, const MessageSendStateIcon(this.message,
{super.key}); {super.key, this.mainAxisAlignment = MainAxisAlignment.end});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Widget icon = Placeholder(); Widget icon = Placeholder();
String text = ""; String text = "";
Color color = kind.getColor(Theme.of(context).colorScheme.primary); Color color =
message.messageKind.getColor(Theme.of(context).colorScheme.primary);
switch (state) { Widget loaderIcon = Row(
children: [
SizedBox(
width: 10,
height: 10,
child: CircularProgressIndicator(strokeWidth: 1, color: color),
),
SizedBox(width: 2),
],
);
switch (message.getSendState()) {
case MessageSendState.receivedOpened: case MessageSendState.receivedOpened:
icon = Icon(Icons.crop_square, size: 14, color: color); icon = Icon(Icons.crop_square, size: 14, color: color);
text = "Received"; text = "Received";
@ -45,29 +59,38 @@ class MessageSendStateIcon extends StatelessWidget {
break; break;
case MessageSendState.sending: case MessageSendState.sending:
case MessageSendState.receiving: case MessageSendState.receiving:
icon = Row( icon = loaderIcon;
children: [
SizedBox(
width: 10,
height: 10,
child: CircularProgressIndicator(strokeWidth: 1, color: color),
),
SizedBox(width: 2),
],
);
text = "Sending"; text = "Sending";
break; break;
} }
if (!isDownloaded) { if (!message.isDownloaded) {
text = "Tap do load"; text = "Tap do load";
} }
bool isDownloading = false;
if (message.messageContent != null &&
message.messageContent!.downloadToken != null) {
isDownloading = context
.watch<DownloadChangeProvider>()
.currentlyDownloading
.contains(message.messageContent!.downloadToken!);
}
if (isDownloading) {
text = "Downloading";
icon = loaderIcon;
}
return Row( return Row(
mainAxisAlignment: mainAxisAlignment,
children: [ children: [
icon, icon,
const SizedBox(width: 3), const SizedBox(width: 3),
Text(text, style: TextStyle(fontSize: 12)), Text(
text,
style: TextStyle(fontSize: 12),
),
const SizedBox(width: 5), const SizedBox(width: 5),
], ],
); );

View file

@ -238,6 +238,17 @@ class DbMessages extends CvModelBase {
} }
} }
static Future _updateByOtherMessageId(
int fromUserId, int messageId, Map<String, dynamic> data) async {
await dbProvider.db!.update(
tableName,
data,
where: "$columnMessageOtherId = ?",
whereArgs: [messageId],
);
globalCallBackOnMessageChange(fromUserId);
}
// this ensures that the message id can be spoofed by another person // this ensures that the message id can be spoofed by another person
static Future _updateByMessageIdOther( static Future _updateByMessageIdOther(
int fromUserId, int messageId, Map<String, dynamic> data) async { int fromUserId, int messageId, Map<String, dynamic> data) async {
@ -250,14 +261,15 @@ class DbMessages extends CvModelBase {
globalCallBackOnMessageChange(fromUserId); globalCallBackOnMessageChange(fromUserId);
} }
static Future userOpenedMessage(int messageId) async { static Future userOpenedOtherMessage(
int otherMessageId, int fromUserId) async {
Map<String, dynamic> data = { Map<String, dynamic> data = {
columnMessageOpenedAt: DateTime.now().toIso8601String(), columnMessageOpenedAt: DateTime.now().toIso8601String(),
}; };
await _updateByMessageId(messageId, data); await _updateByOtherMessageId(fromUserId, otherMessageId, data);
} }
static Future userOpenedMessageOtherUser( static Future otherUserOpenedMyMessage(
int fromUserId, int messageId, DateTime openedAt) async { int fromUserId, int messageId, DateTime openedAt) async {
Map<String, dynamic> data = { Map<String, dynamic> data = {
columnMessageOpenedAt: openedAt.toIso8601String(), columnMessageOpenedAt: openedAt.toIso8601String(),

View file

@ -854,11 +854,15 @@ class ApplicationData_UploadData extends $pb.GeneratedMessage {
class ApplicationData_DownloadData extends $pb.GeneratedMessage { class ApplicationData_DownloadData extends $pb.GeneratedMessage {
factory ApplicationData_DownloadData({ factory ApplicationData_DownloadData({
$core.List<$core.int>? uploadToken, $core.List<$core.int>? uploadToken,
$core.int? offset,
}) { }) {
final $result = create(); final $result = create();
if (uploadToken != null) { if (uploadToken != null) {
$result.uploadToken = uploadToken; $result.uploadToken = uploadToken;
} }
if (offset != null) {
$result.offset = offset;
}
return $result; return $result;
} }
ApplicationData_DownloadData._() : super(); ApplicationData_DownloadData._() : super();
@ -867,6 +871,7 @@ class ApplicationData_DownloadData extends $pb.GeneratedMessage {
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'ApplicationData.DownloadData', package: const $pb.PackageName(_omitMessageNames ? '' : 'client_to_server'), createEmptyInstance: create) static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'ApplicationData.DownloadData', package: const $pb.PackageName(_omitMessageNames ? '' : 'client_to_server'), createEmptyInstance: create)
..a<$core.List<$core.int>>(1, _omitFieldNames ? '' : 'uploadToken', $pb.PbFieldType.OY) ..a<$core.List<$core.int>>(1, _omitFieldNames ? '' : 'uploadToken', $pb.PbFieldType.OY)
..a<$core.int>(2, _omitFieldNames ? '' : 'offset', $pb.PbFieldType.OU3)
..hasRequiredFields = false ..hasRequiredFields = false
; ;
@ -899,6 +904,15 @@ class ApplicationData_DownloadData extends $pb.GeneratedMessage {
$core.bool hasUploadToken() => $_has(0); $core.bool hasUploadToken() => $_has(0);
@$pb.TagNumber(1) @$pb.TagNumber(1)
void clearUploadToken() => clearField(1); void clearUploadToken() => clearField(1);
@$pb.TagNumber(2)
$core.int get offset => $_getIZ(1);
@$pb.TagNumber(2)
set offset($core.int v) { $_setUnsignedInt32(1, v); }
@$pb.TagNumber(2)
$core.bool hasOffset() => $_has(1);
@$pb.TagNumber(2)
void clearOffset() => clearField(2);
} }
enum ApplicationData_ApplicationData { enum ApplicationData_ApplicationData {

View file

@ -182,6 +182,7 @@ const ApplicationData_DownloadData$json = {
'1': 'DownloadData', '1': 'DownloadData',
'2': [ '2': [
{'1': 'upload_token', '3': 1, '4': 1, '5': 12, '10': 'uploadToken'}, {'1': 'upload_token', '3': 1, '4': 1, '5': 12, '10': 'uploadToken'},
{'1': 'offset', '3': 2, '4': 1, '5': 13, '10': 'offset'},
], ],
}; };
@ -204,8 +205,8 @@ final $typed_data.Uint8List applicationDataDescriptor = $convert.base64Decode(
'cl9pZBgBIAEoA1IGdXNlcklkGi0KEkdldFByZWtleXNCeVVzZXJJZBIXCgd1c2VyX2lkGAEgAS' 'cl9pZBgBIAEoA1IGdXNlcklkGi0KEkdldFByZWtleXNCeVVzZXJJZBIXCgd1c2VyX2lkGAEgAS'
'gDUgZ1c2VySWQaEAoOR2V0VXBsb2FkVG9rZW4aWwoKVXBsb2FkRGF0YRIhCgx1cGxvYWRfdG9r' 'gDUgZ1c2VySWQaEAoOR2V0VXBsb2FkVG9rZW4aWwoKVXBsb2FkRGF0YRIhCgx1cGxvYWRfdG9r'
'ZW4YASABKAxSC3VwbG9hZFRva2VuEhYKBm9mZnNldBgCIAEoDVIGb2Zmc2V0EhIKBGRhdGEYAy' 'ZW4YASABKAxSC3VwbG9hZFRva2VuEhYKBm9mZnNldBgCIAEoDVIGb2Zmc2V0EhIKBGRhdGEYAy'
'ABKAxSBGRhdGEaMQoMRG93bmxvYWREYXRhEiEKDHVwbG9hZF90b2tlbhgBIAEoDFILdXBsb2Fk' 'ABKAxSBGRhdGEaSQoMRG93bmxvYWREYXRhEiEKDHVwbG9hZF90b2tlbhgBIAEoDFILdXBsb2Fk'
'VG9rZW5CEQoPQXBwbGljYXRpb25EYXRh'); 'VG9rZW4SFgoGb2Zmc2V0GAIgASgNUgZvZmZzZXRCEQoPQXBwbGljYXRpb25EYXRh');
@$core.Deprecated('Use responseDescriptor instead') @$core.Deprecated('Use responseDescriptor instead')
const Response$json = { const Response$json = {

View file

@ -29,6 +29,7 @@ class ErrorCode extends $pb.ProtobufEnum {
static const ErrorCode OnlyOneSessionAllowed = ErrorCode._(1010, _omitEnumNames ? '' : 'OnlyOneSessionAllowed'); static const ErrorCode OnlyOneSessionAllowed = ErrorCode._(1010, _omitEnumNames ? '' : 'OnlyOneSessionAllowed');
static const ErrorCode UploadLimitReached = ErrorCode._(1011, _omitEnumNames ? '' : 'UploadLimitReached'); static const ErrorCode UploadLimitReached = ErrorCode._(1011, _omitEnumNames ? '' : 'UploadLimitReached');
static const ErrorCode InvalidUpdateToken = ErrorCode._(1012, _omitEnumNames ? '' : 'InvalidUpdateToken'); static const ErrorCode InvalidUpdateToken = ErrorCode._(1012, _omitEnumNames ? '' : 'InvalidUpdateToken');
static const ErrorCode InvalidOffset = ErrorCode._(1013, _omitEnumNames ? '' : 'InvalidOffset');
static const $core.List<ErrorCode> values = <ErrorCode> [ static const $core.List<ErrorCode> values = <ErrorCode> [
Unknown, Unknown,
@ -46,6 +47,7 @@ class ErrorCode extends $pb.ProtobufEnum {
OnlyOneSessionAllowed, OnlyOneSessionAllowed,
UploadLimitReached, UploadLimitReached,
InvalidUpdateToken, InvalidUpdateToken,
InvalidOffset,
]; ];
static final $core.Map<$core.int, ErrorCode> _byValue = $pb.ProtobufEnum.initByValue(values); static final $core.Map<$core.int, ErrorCode> _byValue = $pb.ProtobufEnum.initByValue(values);

View file

@ -32,6 +32,7 @@ const ErrorCode$json = {
{'1': 'OnlyOneSessionAllowed', '2': 1010}, {'1': 'OnlyOneSessionAllowed', '2': 1010},
{'1': 'UploadLimitReached', '2': 1011}, {'1': 'UploadLimitReached', '2': 1011},
{'1': 'InvalidUpdateToken', '2': 1012}, {'1': 'InvalidUpdateToken', '2': 1012},
{'1': 'InvalidOffset', '2': 1013},
], ],
}; };
@ -43,5 +44,6 @@ final $typed_data.Uint8List errorCodeDescriptor = $convert.base64Decode(
'VzZXJuYW1lTm90Rm91bmQQ7QcSFQoQVXNlcm5hbWVOb3RWYWxpZBDuBxIVChBJbnZhbGlkUHVi' 'VzZXJuYW1lTm90Rm91bmQQ7QcSFQoQVXNlcm5hbWVOb3RWYWxpZBDuBxIVChBJbnZhbGlkUHVi'
'bGljS2V5EO8HEiAKG1Nlc3Npb25BbHJlYWR5QXV0aGVudGljYXRlZBDwBxIcChdTZXNzaW9uTm' 'bGljS2V5EO8HEiAKG1Nlc3Npb25BbHJlYWR5QXV0aGVudGljYXRlZBDwBxIcChdTZXNzaW9uTm'
'90QXV0aGVudGljYXRlZBDxBxIaChVPbmx5T25lU2Vzc2lvbkFsbG93ZWQQ8gcSFwoSVXBsb2Fk' '90QXV0aGVudGljYXRlZBDxBxIaChVPbmx5T25lU2Vzc2lvbkFsbG93ZWQQ8gcSFwoSVXBsb2Fk'
'TGltaXRSZWFjaGVkEPMHEhcKEkludmFsaWRVcGRhdGVUb2tlbhD0Bw=='); 'TGltaXRSZWFjaGVkEPMHEhcKEkludmFsaWRVcGRhdGVUb2tlbhD0BxISCg1JbnZhbGlkT2Zmc2'
'V0EPUH');

View file

@ -7,6 +7,7 @@ import 'package:hive/hive.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:twonly/main.dart'; import 'package:twonly/main.dart';
import 'package:twonly/src/app.dart';
import 'package:twonly/src/model/json/message.dart'; import 'package:twonly/src/model/json/message.dart';
import 'package:twonly/src/model/messages_model.dart'; import 'package:twonly/src/model/messages_model.dart';
import 'package:twonly/src/proto/api/error.pb.dart'; import 'package:twonly/src/proto/api/error.pb.dart';
@ -56,7 +57,7 @@ Future sendTextMessage(Int64 target, String message) async {
timestamp: DateTime.now(), timestamp: DateTime.now(),
); );
await encryptAndSendMessage(target, msg); encryptAndSendMessage(target, msg);
} }
Future sendImageToSingleTarget(Int64 target, Uint8List imageBytes) async { Future sendImageToSingleTarget(Int64 target, Uint8List imageBytes) async {
@ -122,7 +123,7 @@ Future sendImage(List<Int64> userIds, String imagePath) async {
} }
} }
Future tryDownloadMedia(List<int> imageToken, {bool force = false}) async { Future tryDownloadMedia(List<int> mediaToken, {bool force = false}) async {
if (!force) { if (!force) {
// TODO: create option to enable download via mobile data // TODO: create option to enable download via mobile data
final List<ConnectivityResult> connectivityResult = final List<ConnectivityResult> connectivityResult =
@ -132,32 +133,39 @@ Future tryDownloadMedia(List<int> imageToken, {bool force = false}) async {
return; return;
} }
} }
Logger("tryDownloadMedia").info("Downloading: $imageToken"); Logger("tryDownloadMedia").info("Downloading: $mediaToken");
apiProvider.triggerDownload(imageToken); int offset = 0;
final box = await getMediaStorage();
Uint8List? media = box.get("$mediaToken");
if (media != null && media.isNotEmpty) {
offset = media.length;
}
globalCallBackOnDownloadChange(mediaToken, true);
apiProvider.triggerDownload(mediaToken, offset);
} }
Future userOpenedMessage(int fromUserId, int messageId) async { Future userOpenedOtherMessage(int fromUserId, int messageOtherId) async {
await DbMessages.userOpenedMessage(messageId); await DbMessages.userOpenedOtherMessage(messageOtherId, fromUserId);
encryptAndSendMessage( encryptAndSendMessage(
Int64(fromUserId), Int64(fromUserId),
Message( Message(
kind: MessageKind.opened, kind: MessageKind.opened,
messageId: messageId, messageId: messageOtherId,
timestamp: DateTime.now(), timestamp: DateTime.now(),
), ),
); );
} }
Future<Uint8List?> getDownloadedMedia( Future<Uint8List?> getDownloadedMedia(
List<int> mediaToken, int messageId) 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"); int fromUserId = box.get("${mediaToken}_fromUserId");
await userOpenedMessage(fromUserId, messageId); 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

@ -57,7 +57,8 @@ Future<client.Response> handleDownloadData(DownloadData data) async {
Uint8List downloadedBytes; Uint8List downloadedBytes;
if (buffered != null) { if (buffered != null) {
if (data.offset != buffered.length) { if (data.offset != buffered.length) {
return client.Response()..error = ErrorCode.BadRequest; // Logger("handleDownloadData").error(object)
return client.Response()..error = ErrorCode.InvalidOffset;
} }
var b = BytesBuilder(); var b = BytesBuilder();
b.add(buffered); b.add(buffered);
@ -70,16 +71,20 @@ Future<client.Response> handleDownloadData(DownloadData data) async {
if (data.fin) { if (data.fin) {
SignalHelper.getSignalStore(); SignalHelper.getSignalStore();
int fromUserId = box.get("${data.uploadToken}_fromUserId")!; int? fromUserId = box.get("${data.uploadToken}_fromUserId");
Uint8List? rawBytes = if (fromUserId != null) {
await SignalHelper.decryptBytes(downloadedBytes, Int64(fromUserId)); print(fromUserId);
Uint8List? rawBytes =
await SignalHelper.decryptBytes(downloadedBytes, Int64(fromUserId));
if (rawBytes != null) { if (rawBytes != null) {
box.put("${data.uploadToken}_downloaded", rawBytes); box.put("${data.uploadToken}_downloaded", rawBytes);
}
box.delete(boxId);
globalCallBackOnMessageChange(fromUserId);
globalCallBackOnDownloadChange(data.uploadToken, false);
} }
box.delete(boxId);
globalCallBackOnMessageChange(fromUserId);
} else { } else {
box.put(boxId, downloadedBytes); box.put(boxId, downloadedBytes);
} }
@ -102,7 +107,7 @@ Future<client.Response> handleNewMessage(
} }
break; break;
case MessageKind.opened: case MessageKind.opened:
await DbMessages.userOpenedMessageOtherUser( await DbMessages.otherUserOpenedMyMessage(
fromUserId.toInt(), fromUserId.toInt(),
message.messageId!, message.messageId!,
message.timestamp, message.timestamp,

View file

@ -255,8 +255,11 @@ class ApiProvider {
return await _sendRequestV0(req); return await _sendRequestV0(req);
} }
Future<Result> triggerDownload(List<int> token) async { Future<Result> triggerDownload(List<int> token, int offset) async {
var get = ApplicationData_DownloadData()..uploadToken = token; log.info("Offset: ${offset}");
var get = ApplicationData_DownloadData()
..uploadToken = token
..offset = offset;
var appData = ApplicationData()..downloaddata = get; var appData = ApplicationData()..downloaddata = get;
var req = createClientToServerFromApplicationData(appData); var req = createClientToServerFromApplicationData(appData);
return await _sendRequestV0(req); return await _sendRequestV0(req);

View file

@ -0,0 +1,16 @@
import 'dart:collection';
import 'package:flutter/foundation.dart';
class DownloadChangeProvider with ChangeNotifier, DiagnosticableTreeMixin {
final HashSet<List<int>> _currentlyDownloading = HashSet<List<int>>();
HashSet<List<int>> get currentlyDownloading => _currentlyDownloading;
void update(List<int> token, bool add) {
if (add) {
_currentlyDownloading.add(token);
} else {
_currentlyDownloading.remove(token);
}
}
}

View file

@ -6,7 +6,10 @@ import 'package:twonly/src/model/messages_model.dart';
/// for every contact. /// for every contact.
class MessagesChangeProvider with ChangeNotifier, DiagnosticableTreeMixin { class MessagesChangeProvider with ChangeNotifier, DiagnosticableTreeMixin {
final Map<int, DbMessage> _lastMessage = <int, DbMessage>{}; final Map<int, DbMessage> _lastMessage = <int, DbMessage>{};
final Map<int, int> _changeCounter = <int, int>{};
Map<int, DbMessage> get lastMessage => _lastMessage; Map<int, DbMessage> get lastMessage => _lastMessage;
Map<int, int> get changeCounter => _changeCounter;
void updateLastMessageFor(int targetUserId) async { void updateLastMessageFor(int targetUserId) async {
DbMessage? last = DbMessage? last =
@ -14,6 +17,10 @@ class MessagesChangeProvider with ChangeNotifier, DiagnosticableTreeMixin {
if (last != null) { if (last != null) {
_lastMessage[last.otherUserId] = last; _lastMessage[last.otherUserId] = last;
} }
if (!changeCounter.containsKey(targetUserId)) {
changeCounter[targetUserId] = 0;
}
changeCounter[targetUserId] = changeCounter[targetUserId]! + 1;
} }
void init() async { void init() async {

View file

@ -1,4 +1,5 @@
import 'package:cv/cv.dart'; import 'dart:collection';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -17,10 +18,10 @@ class ChatListEntry extends StatelessWidget {
final DbMessage message; final DbMessage message;
final Contact user; final Contact user;
final bool lastMessageFromSameUser; final bool lastMessageFromSameUser;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
bool right = message.messageOtherId == null; bool right = message.messageOtherId == null;
MessageSendState state = message.getSendState(); MessageSendState state = message.getSendState();
Widget child = Container(); Widget child = Container();
@ -29,15 +30,15 @@ class ChatListEntry extends StatelessWidget {
case MessageKind.textMessage: case MessageKind.textMessage:
child = Container( child = Container(
constraints: BoxConstraints( constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * maxWidth: MediaQuery.of(context).size.width * 0.8,
0.8, // Maximum 80% of the screen width
), ),
padding: EdgeInsets.symmetric( padding: EdgeInsets.symmetric(
vertical: 4, horizontal: 10), // Add some padding around the text vertical: 4, horizontal: 10), // Add some padding around the text
decoration: BoxDecoration( decoration: BoxDecoration(
color: right color: right
? Colors.deepPurpleAccent ? const Color.fromARGB(107, 124, 77, 255)
: Colors.blueAccent, // Set the background color : const Color.fromARGB(
83, 68, 137, 255), // Set the background color
borderRadius: BorderRadius.circular(12.0), // Set border radius borderRadius: BorderRadius.circular(12.0), // Set border radius
), ),
child: Text( child: Text(
@ -71,7 +72,7 @@ class ChatListEntry extends StatelessWidget {
}, },
child: Container( child: Container(
padding: EdgeInsets.all(10), padding: EdgeInsets.all(10),
width: 200, width: 150,
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border.all( border: Border.all(
color: color, // Set the background color color: color, // Set the background color
@ -79,10 +80,13 @@ class ChatListEntry extends StatelessWidget {
), ),
borderRadius: BorderRadius.circular(12.0), // Set border radius borderRadius: BorderRadius.circular(12.0), // Set border radius
), ),
child: MessageSendStateIcon( child: Align(
state, alignment: Alignment.centerRight,
message.isDownloaded, child: MessageSendStateIcon(
message.messageKind, message,
mainAxisAlignment:
right ? MainAxisAlignment.center : MainAxisAlignment.center,
),
), ),
), ),
); );
@ -94,7 +98,7 @@ class ChatListEntry extends StatelessWidget {
child: Padding( child: Padding(
padding: lastMessageFromSameUser padding: lastMessageFromSameUser
? EdgeInsets.only(top: 5, bottom: 0, right: 10, left: 10) ? EdgeInsets.only(top: 5, bottom: 0, right: 10, left: 10)
: EdgeInsets.all(10), : EdgeInsets.only(top: 5, bottom: 20, right: 10, left: 10),
child: child), child: child),
); );
} }
@ -112,34 +116,50 @@ class ChatItemDetailsView extends StatefulWidget {
class _ChatItemDetailsViewState extends State<ChatItemDetailsView> { class _ChatItemDetailsViewState extends State<ChatItemDetailsView> {
List<DbMessage> _messages = []; List<DbMessage> _messages = [];
int lastChangeCounter = 0;
final TextEditingController newMessageController = TextEditingController(); final TextEditingController newMessageController = TextEditingController();
HashSet<int> alreadyReportedOpened = HashSet<int>();
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_loadAsync(); _loadAsync(updateOpenStatus: true);
} }
Future _loadAsync() async { Future _loadAsync({bool updateOpenStatus = false}) async {
_messages = _messages =
await DbMessages.getAllMessagesForUser(widget.user.userId.toInt()); await DbMessages.getAllMessagesForUser(widget.user.userId.toInt());
setState(() {});
if (updateOpenStatus) {
_messages.where((x) => x.messageOpenedAt == null).forEach((message) {
if (message.messageOtherId != null &&
message.messageKind == MessageKind.textMessage) {
if (!alreadyReportedOpened.contains(message.messageOtherId!)) {
userOpenedOtherMessage(
message.otherUserId, message.messageOtherId!);
alreadyReportedOpened.add(message.messageOtherId!);
}
}
});
}
} }
Future _sendMessage() async { Future _sendMessage() async {
String text = newMessageController.text; String text = newMessageController.text;
if (text == "") return; if (text == "") return;
sendTextMessage(widget.user.userId, newMessageController.text); await sendTextMessage(widget.user.userId, newMessageController.text);
_loadAsync();
newMessageController.clear(); newMessageController.clear();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final messages = context.watch<MessagesChangeProvider>().lastMessage; final changeCounter = context.watch<MessagesChangeProvider>().changeCounter;
if (messages.containsKey(widget.user.userId.toInt()) && if (changeCounter.containsKey(widget.user.userId.toInt())) {
_messages.isNotEmpty) { if (changeCounter[widget.user.userId.toInt()] != lastChangeCounter) {
final lastMessage = messages[widget.user.userId.toInt()]; _loadAsync(updateOpenStatus: true);
if (lastMessage!.messageId != _messages[0].messageId) { lastChangeCounter = changeCounter[widget.user.userId.toInt()]!;
_loadAsync();
} }
} }
// messages = messages.reversed.toList(); // messages = messages.reversed.toList();
@ -163,7 +183,10 @@ class _ChatItemDetailsViewState extends State<ChatItemDetailsView> {
_messages[i].messageOtherId != null); _messages[i].messageOtherId != null);
} }
return ChatListEntry( return ChatListEntry(
_messages[i], widget.user, lastMessageFromSameUser); _messages[i],
widget.user,
lastMessageFromSameUser,
);
}, },
), ),
), ),

View file

@ -156,8 +156,7 @@ class _UserListItem extends State<UserListItem> {
title: Text(widget.user.displayName), title: Text(widget.user.displayName),
subtitle: Row( subtitle: Row(
children: [ children: [
MessageSendStateIcon(state, widget.lastMessage.isDownloaded, MessageSendStateIcon(widget.lastMessage),
widget.lastMessage.messageKind),
Text(""), Text(""),
const SizedBox(width: 5), const SizedBox(width: 5),
Text( Text(
@ -190,13 +189,19 @@ class _UserListItem extends State<UserListItem> {
tryDownloadMedia(token, force: true); tryDownloadMedia(token, force: true);
return; return;
} }
if (state == MessageSendState.received &&
widget.lastMessage.containsOtherMedia()) {
Navigator.push(
context,
MaterialPageRoute(builder: (context) {
return MediaViewerView(widget.user, widget.lastMessage);
}),
);
return;
}
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute(builder: (context) { MaterialPageRoute(builder: (context) {
if (state == MessageSendState.received &&
widget.lastMessage.containsOtherMedia()) {
return MediaViewerView(widget.user, widget.lastMessage);
}
return ChatItemDetailsView(user: widget.user); return ChatItemDetailsView(user: widget.user);
}), }),
); );

View file

@ -27,7 +27,8 @@ class _MediaViewerViewState extends State<MediaViewerView> {
Future _initAsync() async { Future _initAsync() async {
List<int> token = widget.message.messageContent!.downloadToken!; List<int> token = widget.message.messageContent!.downloadToken!;
_imageByte = await getDownloadedMedia(token, widget.message.messageId); _imageByte =
await getDownloadedMedia(token, widget.message.messageOtherId!);
setState(() {}); setState(() {});
} }