download and preview of images

This commit is contained in:
otsmr 2025-01-29 00:11:00 +01:00
parent a9572e7890
commit f866e4315e
12 changed files with 451 additions and 162 deletions

View file

@ -5,6 +5,7 @@ import 'package:logging/logging.dart';
import 'package:twonly/main.dart'; import 'package:twonly/main.dart';
import 'package:twonly/src/app.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/providers/api/api.dart';
class DbMessage { class DbMessage {
DbMessage({ DbMessage({
@ -15,6 +16,7 @@ class DbMessage {
required this.messageContent, required this.messageContent,
required this.messageOpenedAt, required this.messageOpenedAt,
required this.messageAcknowledgeByUser, required this.messageAcknowledgeByUser,
required this.isDownloaded,
required this.messageAcknowledgeByServer, required this.messageAcknowledgeByServer,
required this.sendOrReceivedAt, required this.sendOrReceivedAt,
}); });
@ -27,6 +29,7 @@ class DbMessage {
MessageContent? messageContent; MessageContent? messageContent;
DateTime? messageOpenedAt; DateTime? messageOpenedAt;
bool messageAcknowledgeByUser; bool messageAcknowledgeByUser;
bool isDownloaded;
bool messageAcknowledgeByServer; bool messageAcknowledgeByServer;
DateTime sendOrReceivedAt; DateTime sendOrReceivedAt;
@ -130,11 +133,11 @@ class DbMessages extends CvModelBase {
} }
} }
static Future insertOtherMessage(int userIdFrom, MessageKind kind, static Future<int?> insertOtherMessage(int userIdFrom, MessageKind kind,
int messageId, String jsonContent) async { int messageOtherId, String jsonContent) async {
try { try {
await dbProvider.db!.insert(tableName, { int messageId = await dbProvider.db!.insert(tableName, {
columnMessageOtherId: messageId, columnMessageOtherId: messageOtherId,
columnMessageKind: kind.index, columnMessageKind: kind.index,
columnMessageContentJson: jsonContent, columnMessageContentJson: jsonContent,
columnMessageAcknowledgeByServer: 1, columnMessageAcknowledgeByServer: 1,
@ -144,13 +147,26 @@ class DbMessages extends CvModelBase {
columnSendOrReceivedAt: DateTime.now().toIso8601String() columnSendOrReceivedAt: DateTime.now().toIso8601String()
}); });
globalCallBackOnMessageChange(userIdFrom); globalCallBackOnMessageChange(userIdFrom);
return true; return messageId;
} catch (e) { } catch (e) {
Logger("contacts_model/getUsers").shout("$e"); Logger("contacts_model/getUsers").shout("$e");
return false; return null;
} }
} }
static Future<List<DbMessage>> getAllMessagesForUser(int otherUserId) async {
var rows = await dbProvider.db!.query(
tableName,
where: "$columnOtherUserId = ?",
whereArgs: [otherUserId],
orderBy: "$columnUpdatedAt DESC",
);
List<DbMessage> messages = await convertToDbMessage(rows);
return messages;
}
static Future<DbMessage?> getLastMessagesForPreviewForUser( static Future<DbMessage?> getLastMessagesForPreviewForUser(
int otherUserId) async { int otherUserId) async {
var rows = await dbProvider.db!.query( var rows = await dbProvider.db!.query(
@ -161,7 +177,7 @@ class DbMessages extends CvModelBase {
limit: 10, limit: 10,
); );
List<DbMessage> messages = convertToDbMessage(rows); List<DbMessage> messages = await convertToDbMessage(rows);
// check if there is a message which was not ack by the server // check if there is a message which was not ack by the server
List<DbMessage> notAckByServer = List<DbMessage> notAckByServer =
@ -177,13 +193,11 @@ class DbMessages extends CvModelBase {
return messages[0]; return messages[0];
} }
static Future acknowledgeMessageByServer(int messageId) async { static Future _updateByMessageId(
Map<String, dynamic> valuesToUpdate = { int messageId, Map<String, dynamic> data) async {
columnMessageAcknowledgeByServer: 1,
};
await dbProvider.db!.update( await dbProvider.db!.update(
tableName, tableName,
valuesToUpdate, data,
where: "$messageId = ?", where: "$messageId = ?",
whereArgs: [messageId], whereArgs: [messageId],
); );
@ -193,6 +207,20 @@ class DbMessages extends CvModelBase {
} }
} }
static Future userOpenedMessage(int messageId) async {
Map<String, dynamic> data = {
columnMessageOpenedAt: DateTime.now().toIso8601String(),
};
await _updateByMessageId(messageId, data);
}
static Future acknowledgeMessageByServer(int messageId) async {
Map<String, dynamic> data = {
columnMessageAcknowledgeByServer: 1,
};
await _updateByMessageId(messageId, data);
}
// check fromUserId to prevent spoofing // check fromUserId to prevent spoofing
static Future acknowledgeMessageByUser(int fromUserId, int messageId) async { static Future acknowledgeMessageByUser(int fromUserId, int messageId) async {
Map<String, dynamic> valuesToUpdate = { Map<String, dynamic> valuesToUpdate = {
@ -216,7 +244,8 @@ class DbMessages extends CvModelBase {
sendOrReceivedAt sendOrReceivedAt
]; ];
static List<DbMessage> convertToDbMessage(List<dynamic> fromDb) { static Future<List<DbMessage>> convertToDbMessage(
List<dynamic> fromDb) async {
try { try {
List<DbMessage> parsedUsers = []; List<DbMessage> parsedUsers = [];
for (int i = 0; i < fromDb.length; i++) { for (int i = 0; i < fromDb.length; i++) {
@ -229,6 +258,16 @@ class DbMessages extends CvModelBase {
content = MessageContent.fromJson( content = MessageContent.fromJson(
jsonDecode(fromDb[i][columnMessageContentJson])); jsonDecode(fromDb[i][columnMessageContentJson]));
} }
MessageKind messageKind =
MessageKindExtension.fromIndex(fromDb[i][columnMessageKind]);
bool isDownloaded = true;
if (messageKind == MessageKind.image ||
messageKind == MessageKind.video) {
// when the media was send from the user itself the content is null
if (content != null) {
isDownloaded = await isMediaDownloaded(content.downloadToken!);
}
}
parsedUsers.add( parsedUsers.add(
DbMessage( DbMessage(
sendOrReceivedAt: sendOrReceivedAt:
@ -236,9 +275,9 @@ class DbMessages extends CvModelBase {
messageId: fromDb[i][columnMessageId], messageId: fromDb[i][columnMessageId],
messageOtherId: fromDb[i][columnMessageOtherId], messageOtherId: fromDb[i][columnMessageOtherId],
otherUserId: fromDb[i][columnOtherUserId], otherUserId: fromDb[i][columnOtherUserId],
messageKind: messageKind: messageKind,
MessageKindExtension.fromIndex(fromDb[i][columnMessageKind]),
messageContent: content, messageContent: content,
isDownloaded: isDownloaded,
messageOpenedAt: messageOpenedAt, messageOpenedAt: messageOpenedAt,
messageAcknowledgeByUser: messageAcknowledgeByUser:
fromDb[i][columnMessageAcknowledgeByUser] == 1, fromDb[i][columnMessageAcknowledgeByUser] == 1,

View file

@ -281,6 +281,7 @@ class DownloadData extends $pb.GeneratedMessage {
$core.List<$core.int>? uploadToken, $core.List<$core.int>? uploadToken,
$core.int? offset, $core.int? offset,
$core.List<$core.int>? data, $core.List<$core.int>? data,
$core.bool? fin,
}) { }) {
final $result = create(); final $result = create();
if (uploadToken != null) { if (uploadToken != null) {
@ -292,6 +293,9 @@ class DownloadData extends $pb.GeneratedMessage {
if (data != null) { if (data != null) {
$result.data = data; $result.data = data;
} }
if (fin != null) {
$result.fin = fin;
}
return $result; return $result;
} }
DownloadData._() : super(); DownloadData._() : super();
@ -302,6 +306,7 @@ class DownloadData extends $pb.GeneratedMessage {
..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) ..a<$core.int>(2, _omitFieldNames ? '' : 'offset', $pb.PbFieldType.OU3)
..a<$core.List<$core.int>>(3, _omitFieldNames ? '' : 'data', $pb.PbFieldType.OY) ..a<$core.List<$core.int>>(3, _omitFieldNames ? '' : 'data', $pb.PbFieldType.OY)
..aOB(4, _omitFieldNames ? '' : 'fin')
..hasRequiredFields = false ..hasRequiredFields = false
; ;
@ -352,6 +357,15 @@ class DownloadData extends $pb.GeneratedMessage {
$core.bool hasData() => $_has(2); $core.bool hasData() => $_has(2);
@$pb.TagNumber(3) @$pb.TagNumber(3)
void clearData() => clearField(3); void clearData() => clearField(3);
@$pb.TagNumber(4)
$core.bool get fin => $_getBF(3);
@$pb.TagNumber(4)
set fin($core.bool v) { $_setBool(3, v); }
@$pb.TagNumber(4)
$core.bool hasFin() => $_has(3);
@$pb.TagNumber(4)
void clearFin() => clearField(4);
} }
class Response_PreKey extends $pb.GeneratedMessage { class Response_PreKey extends $pb.GeneratedMessage {

View file

@ -73,13 +73,14 @@ const DownloadData$json = {
{'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'}, {'1': 'offset', '3': 2, '4': 1, '5': 13, '10': 'offset'},
{'1': 'data', '3': 3, '4': 1, '5': 12, '10': 'data'}, {'1': 'data', '3': 3, '4': 1, '5': 12, '10': 'data'},
{'1': 'fin', '3': 4, '4': 1, '5': 8, '10': 'fin'},
], ],
}; };
/// Descriptor for `DownloadData`. Decode as a `google.protobuf.DescriptorProto`. /// Descriptor for `DownloadData`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List downloadDataDescriptor = $convert.base64Decode( final $typed_data.Uint8List downloadDataDescriptor = $convert.base64Decode(
'CgxEb3dubG9hZERhdGESIQoMdXBsb2FkX3Rva2VuGAEgASgMUgt1cGxvYWRUb2tlbhIWCgZvZm' 'CgxEb3dubG9hZERhdGESIQoMdXBsb2FkX3Rva2VuGAEgASgMUgt1cGxvYWRUb2tlbhIWCgZvZm'
'ZzZXQYAiABKA1SBm9mZnNldBISCgRkYXRhGAMgASgMUgRkYXRh'); 'ZzZXQYAiABKA1SBm9mZnNldBISCgRkYXRhGAMgASgMUgRkYXRhEhAKA2ZpbhgEIAEoCFIDZmlu');
@$core.Deprecated('Use responseDescriptor instead') @$core.Deprecated('Use responseDescriptor instead')
const Response$json = { const Response$json = {

View file

@ -1,5 +1,6 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:fixnum/fixnum.dart'; import 'package:fixnum/fixnum.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
@ -104,24 +105,34 @@ Future sendImage(List<Int64> userIds, String imagePath) async {
} }
Future tryDownloadMedia(List<int> imageToken, {bool force = false}) async { Future tryDownloadMedia(List<int> imageToken, {bool force = false}) async {
print("check if free network connection"); if (!force) {
// TODO: create option to enable download via mobile data
print("Downloading: " + imageToken.toString()); final List<ConnectivityResult> connectivityResult =
await (Connectivity().checkConnectivity());
if (connectivityResult.contains(ConnectivityResult.mobile)) {
Logger("tryDownloadMedia").info("abort download over mobile connection");
return;
}
}
Logger("tryDownloadMedia").info("Downloading: $imageToken");
apiProvider.triggerDownload(imageToken);
}
Future<Uint8List?> getDownloadedMedia(
List<int> mediaToken, int messageId) async {
final box = await getMediaStorage(); final box = await getMediaStorage();
Uint8List? media = box.get("${mediaToken}_downloaded");
// box.delete(mediaToken.toString());
// box.delete("${mediaToken}_downloaded");
// box.delete("${mediaToken}_fromUserId");
// await DbMessages.userOpenedMessage(messageId);
// Uint8List imageBytes = Uint8List.fromList([0]); return media;
// box.put(imageToken.toString(), imageBytes);
box.close();
} }
Future<bool> isMediaDownloaded(List<int> mediaToken) async { Future<bool> isMediaDownloaded(List<int> mediaToken) async {
final box = await getMediaStorage(); final box = await getMediaStorage();
return box.containsKey("${mediaToken}_downloaded");
// box.put('secret', 'Hive is awesome');
return box.containsKey(mediaToken.toString());
} }
Future initMediaStorage() async { Future initMediaStorage() async {

View file

@ -1,15 +1,20 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:typed_data';
import 'package:fixnum/fixnum.dart'; import 'package:fixnum/fixnum.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:hive/hive.dart';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:twonly/main.dart'; import 'package:twonly/main.dart';
import 'package:twonly/src/app.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';
import 'package:twonly/src/model/messages_model.dart'; import 'package:twonly/src/model/messages_model.dart';
import 'package:twonly/src/proto/api/client_to_server.pb.dart' as client; import 'package:twonly/src/proto/api/client_to_server.pb.dart' as client;
import 'package:twonly/src/proto/api/client_to_server.pbserver.dart'; import 'package:twonly/src/proto/api/client_to_server.pbserver.dart';
import 'package:twonly/src/proto/api/error.pb.dart';
import 'package:twonly/src/proto/api/server_to_client.pb.dart' as server; import 'package:twonly/src/proto/api/server_to_client.pb.dart' as server;
import 'package:twonly/src/proto/api/server_to_client.pbserver.dart';
import 'package:twonly/src/providers/api/api.dart'; import 'package:twonly/src/providers/api/api.dart';
import 'package:twonly/src/providers/api/api_utils.dart'; import 'package:twonly/src/providers/api/api_utils.dart';
// ignore: library_prefixes // ignore: library_prefixes
@ -18,21 +23,73 @@ import 'package:twonly/src/utils/signal.dart' as SignalHelper;
Future handleServerMessage(server.ServerToClient msg) async { Future handleServerMessage(server.ServerToClient msg) async {
client.Response? response; client.Response? response;
try {
if (msg.v0.hasRequestNewPreKeys()) { if (msg.v0.hasRequestNewPreKeys()) {
List<PreKeyRecord> localPreKeys = await SignalHelper.getPreKeys(); response = await handleRequestNewPreKey();
List<client.Response_PreKey> prekeysList = [];
for (int i = 0; i < localPreKeys.length; i++) {
prekeysList.add(client.Response_PreKey()
..id = Int64(localPreKeys[i].id)
..prekey = localPreKeys[i].getKeyPair().publicKey.serialize());
}
var prekeys = client.Response_Prekeys(prekeys: prekeysList);
var ok = client.Response_Ok()..prekeys = prekeys;
response = client.Response()..ok = ok;
} else if (msg.v0.hasNewMessage()) { } else if (msg.v0.hasNewMessage()) {
Uint8List body = Uint8List.fromList(msg.v0.newMessage.body); Uint8List body = Uint8List.fromList(msg.v0.newMessage.body);
Int64 fromUserId = msg.v0.newMessage.fromUserId; Int64 fromUserId = msg.v0.newMessage.fromUserId;
response = await handleNewMessage(fromUserId, body);
} else if (msg.v0.hasDownloaddata()) {
response = await handleDownloadData(msg.v0.downloaddata);
} else {
Logger("handleServerMessage")
.shout("Got a new message from the server: $msg");
return;
}
} catch (e) {
response = client.Response()..error = ErrorCode.InternalError;
}
var v0 = client.V0()
..seq = msg.v0.seq
..response = response;
apiProvider.sendResponse(ClientToServer()..v0 = v0);
}
Future<client.Response> handleDownloadData(DownloadData data) async {
debugPrint("Downloading: ${data.uploadToken} ${data.fin}");
final box = await getMediaStorage();
String boxId = data.uploadToken.toString();
Uint8List? buffered = box.get(boxId);
Uint8List downloadedBytes;
if (buffered != null) {
if (data.offset != buffered.length) {
return client.Response()..error = ErrorCode.BadRequest;
}
var b = BytesBuilder();
b.add(buffered);
b.add(data.data);
downloadedBytes = b.takeBytes();
} else {
downloadedBytes = Uint8List.fromList(data.data);
}
if (data.fin) {
SignalHelper.getSignalStore();
int fromUserId = box.get("${data.uploadToken}_fromUserId")!;
Uint8List? rawBytes =
await SignalHelper.decryptBytes(downloadedBytes, Int64(fromUserId));
if (rawBytes != null) {
box.put("${data.uploadToken}_downloaded", rawBytes);
}
box.delete(boxId);
globalCallBackOnMessageChange(fromUserId);
} else {
box.put(boxId, downloadedBytes);
}
var ok = client.Response_Ok()..none = true;
return client.Response()..ok = ok;
}
Future<client.Response> handleNewMessage(
Int64 fromUserId, Uint8List body) async {
Message? message = await SignalHelper.getDecryptedText(fromUserId, body); Message? message = await SignalHelper.getDecryptedText(fromUserId, body);
if (message != null) { if (message != null) {
switch (message.kind) { switch (message.kind) {
@ -62,9 +119,13 @@ Future handleServerMessage(server.ServerToClient msg) async {
.shout("Got unknown MessageKind $message"); .shout("Got unknown MessageKind $message");
} else { } else {
String content = jsonEncode(message.content!.toJson()); String content = jsonEncode(message.content!.toJson());
await DbMessages.insertOtherMessage( int? messageId = await DbMessages.insertOtherMessage(
fromUserId.toInt(), message.kind, message.messageId!, content); fromUserId.toInt(), message.kind, message.messageId!, content);
if (messageId == null) {
return client.Response()..error = ErrorCode.InternalError;
}
encryptAndSendMessage( encryptAndSendMessage(
fromUserId, fromUserId,
Message( Message(
@ -78,22 +139,29 @@ Future handleServerMessage(server.ServerToClient msg) async {
message.kind == MessageKind.image) { message.kind == MessageKind.image) {
dynamic content = message.content!; dynamic content = message.content!;
List<int> downloadToken = content.downloadToken; List<int> downloadToken = content.downloadToken;
Box box = await getMediaStorage();
box.put("${downloadToken}_fromUserId", fromUserId.toInt());
tryDownloadMedia(downloadToken); tryDownloadMedia(downloadToken);
} }
} }
} }
} }
var ok = client.Response_Ok()..none = true; var ok = client.Response_Ok()..none = true;
response = client.Response()..ok = ok; return client.Response()..ok = ok;
} else { }
Logger("handleServerMessage")
.shout("Got a new message from the server: $msg"); Future<client.Response> handleRequestNewPreKey() async {
return; List<PreKeyRecord> localPreKeys = await SignalHelper.getPreKeys();
}
List<client.Response_PreKey> prekeysList = [];
var v0 = client.V0() for (int i = 0; i < localPreKeys.length; i++) {
..seq = msg.v0.seq prekeysList.add(client.Response_PreKey()
..response = response; ..id = Int64(localPreKeys[i].id)
..prekey = localPreKeys[i].getKeyPair().publicKey.serialize());
apiProvider.sendResponse(ClientToServer()..v0 = v0); }
var prekeys = client.Response_Prekeys(prekeys: prekeysList);
var ok = client.Response_Ok()..prekeys = prekeys;
return client.Response()..ok = ok;
} }

View file

@ -255,6 +255,13 @@ class ApiProvider {
return await _sendRequestV0(req); return await _sendRequestV0(req);
} }
Future<Result> triggerDownload(List<int> token) async {
var get = ApplicationData_DownloadData()..uploadToken = token;
var appData = ApplicationData()..downloaddata = get;
var req = createClientToServerFromApplicationData(appData);
return await _sendRequestV0(req);
}
Future<List<int>?> uploadData(List<int> uploadToken, Uint8List data) async { Future<List<int>?> uploadData(List<int> uploadToken, Uint8List data) async {
log.shout("fragmentate the data"); log.shout("fragmentate the data");

View file

@ -211,6 +211,36 @@ Future<Uint8List?> encryptBytes(Uint8List bytes, Int64 target) async {
} }
} }
Future<Uint8List?> decryptBytes(Uint8List bytes, Int64 target) async {
try {
ConnectSignalProtocolStore signalStore = (await getSignalStore())!;
SessionCipher session = SessionCipher.fromStore(
signalStore, SignalProtocolAddress(target.toString(), defaultDeviceId));
List<Uint8List>? msgs = removeLastFourBytes(bytes);
if (msgs == null) return null;
Uint8List body = msgs[0];
int type = bytesToInt(msgs[1]);
Uint8List plaintext;
if (type == CiphertextMessage.prekeyType) {
PreKeySignalMessage pre = PreKeySignalMessage(body);
plaintext = await session.decrypt(pre);
} else if (type == CiphertextMessage.whisperType) {
SignalMessage signalMsg = SignalMessage.fromSerialized(body);
plaintext = await session.decryptFromSignal(signalMsg);
} else {
return null;
}
List<int>? plainBytes = gzip.decode(Uint8List.fromList(plaintext));
return Uint8List.fromList(plainBytes);
} catch (e) {
Logger("utils/signal").shout(e.toString());
return null;
}
}
Future<Uint8List?> encryptMessage(Message msg, Int64 target) async { Future<Uint8List?> encryptMessage(Message msg, Int64 target) async {
try { try {
ConnectSignalProtocolStore signalStore = (await getSignalStore())!; ConnectSignalProtocolStore signalStore = (await getSignalStore())!;

View file

@ -1,18 +1,21 @@
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: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/messages_model.dart';
class AlignedTextBox extends StatelessWidget { class ChatListEntry extends StatelessWidget {
const AlignedTextBox({super.key, required this.text, required this.right}); const ChatListEntry(this.message, {super.key});
final String text; final DbMessage message;
final bool right;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Align( bool right = message.messageOtherId == null;
alignment: right ? Alignment.centerRight : Alignment.centerLeft,
child: Padding( Widget child = Container();
padding: EdgeInsets.all(10),
child: Container( switch (message.messageKind) {
case MessageKind.textMessage:
child = Container(
constraints: BoxConstraints( constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * maxWidth: MediaQuery.of(context).size.width *
0.8, // Maximum 80% of the screen width 0.8, // Maximum 80% of the screen width
@ -26,64 +29,65 @@ class AlignedTextBox extends StatelessWidget {
borderRadius: BorderRadius.circular(12.0), // Set border radius borderRadius: BorderRadius.circular(12.0), // Set border radius
), ),
child: Text( child: Text(
text, message.messageContent!.text!,
style: TextStyle( style: TextStyle(
color: Colors.white, // Set text color for contrast color: Colors.white, // Set text color for contrast
fontSize: 17, fontSize: 17,
), ),
textAlign: TextAlign.left, // Center the text textAlign: TextAlign.left, // Center the text
), ),
), );
), break;
default:
return Container();
}
return Align(
alignment: right ? Alignment.centerRight : Alignment.centerLeft,
child: Padding(padding: EdgeInsets.all(10), child: child),
); );
} }
} }
/// Displays detailed information about a SampleItem. /// Displays detailed information about a SampleItem.
class ChatItemDetailsView extends StatelessWidget { class ChatItemDetailsView extends StatefulWidget {
const ChatItemDetailsView({super.key, required this.user}); const ChatItemDetailsView({super.key, required this.user});
final Contact user; final Contact user;
@override
State<ChatItemDetailsView> createState() => _ChatItemDetailsViewState();
}
class _ChatItemDetailsViewState extends State<ChatItemDetailsView> {
List<DbMessage> _messages = [];
@override
void initState() {
super.initState();
_loadAsync();
}
Future _loadAsync() async {
_messages =
await DbMessages.getAllMessagesForUser(widget.user.userId.toInt());
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
List<List<dynamic>> messages = [ // messages = messages.reversed.toList();
["Hallo", true],
["Wie geht's?", false],
["Das ist ein Test.", true],
["Flutter ist großartig!", false],
["Ich liebe Programmieren.", true],
["Das Wetter ist schön.", false],
["Hast du Pläne für heute?", true],
["Ich mag Pizza.", false],
["Lass uns einen Film schauen.", false],
["Das ist interessant.", false],
["Ich bin müde.", true],
["Was machst du gerade?", true],
["Ich habe ein neues Hobby.", true],
["Das ist eine lange Nachricht.", false],
["Ich freue mich auf das Wochenende.", true],
["Das ist eine zufällige Nachricht.", false],
["Ich lerne Dart.", true],
["Wie war dein Tag?", true],
["Ich genieße die Natur.", true],
["Das ist ein schöner Ort.", false],
["Meine letzte Nachricht.", false],
];
messages = messages.reversed.toList();
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text('Your Chat with ${user.displayName}'), title: Text('Your Chat with ${widget.user.displayName}'),
), ),
body: Column( body: Column(
children: [ children: [
Expanded( Expanded(
child: ListView.builder( child: ListView.builder(
itemCount: messages.length, // Number of items in the list itemCount: _messages.length, // Number of items in the list
reverse: true, reverse: true,
itemBuilder: (context, i) { itemBuilder: (context, i) {
return AlignedTextBox( return ChatListEntry(_messages[i]);
text: messages[i][0], right: messages[i][1]);
}, },
), ),
), ),

View file

@ -132,23 +132,10 @@ class UserListItem extends StatefulWidget {
class _UserListItem extends State<UserListItem> { class _UserListItem extends State<UserListItem> {
int flames = 0; int flames = 0;
int lastMessageInSeconds = 0; int lastMessageInSeconds = 0;
bool isDownloaded = true;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_loadAsync();
}
Future _loadAsync() async {
// flames = await widget.user.getFlames();
// setState(() {});
if (widget.lastMessage.containsOtherMedia()) {
isDownloaded = await isMediaDownloaded(
widget.lastMessage.messageContent!.downloadToken!);
setState(() {});
}
} }
@override @override
@ -184,8 +171,8 @@ class _UserListItem extends State<UserListItem> {
title: Text(widget.user.displayName), title: Text(widget.user.displayName),
subtitle: Row( subtitle: Row(
children: [ children: [
MessageSendStateIcon( MessageSendStateIcon(state, widget.lastMessage.isDownloaded,
state, isDownloaded, widget.lastMessage.messageKind), widget.lastMessage.messageKind),
Text(""), Text(""),
const SizedBox(width: 5), const SizedBox(width: 5),
Text( Text(
@ -213,18 +200,17 @@ class _UserListItem extends State<UserListItem> {
), ),
leading: InitialsAvatar(displayName: widget.user.displayName), leading: InitialsAvatar(displayName: widget.user.displayName),
onTap: () { onTap: () {
if (!widget.lastMessage.isDownloaded) {
List<int> token = widget.lastMessage.messageContent!.downloadToken!;
tryDownloadMedia(token, force: true);
return;
}
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute(builder: (context) { MaterialPageRoute(builder: (context) {
if (state == MessageSendState.received && if (state == MessageSendState.received &&
widget.lastMessage.containsOtherMedia()) { widget.lastMessage.containsOtherMedia()) {
List<int> token = return MediaViewerView(widget.user, widget.lastMessage);
widget.lastMessage.messageContent!.downloadToken!;
if (isDownloaded) {
return MediaViewerView(widget.user);
} else {
tryDownloadMedia(token);
}
} }
return ChatItemDetailsView(user: widget.user); return ChatItemDetailsView(user: widget.user);
}), }),

View file

@ -1,19 +1,115 @@
import 'dart:typed_data';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/src/model/contacts_model.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:flutter_gen/gen_l10n/app_localizations.dart';
class MediaViewerView extends StatefulWidget { class MediaViewerView extends StatefulWidget {
final Contact otherUser; final Contact otherUser;
const MediaViewerView(this.otherUser, {super.key}); final DbMessage message;
const MediaViewerView(this.otherUser, this.message, {super.key});
@override @override
State<MediaViewerView> createState() => _MediaViewerViewState(); State<MediaViewerView> createState() => _MediaViewerViewState();
} }
class _MediaViewerViewState extends State<MediaViewerView> { class _MediaViewerViewState extends State<MediaViewerView> {
Uint8List? _imageByte;
@override
void initState() {
super.initState();
_initAsync();
}
Future _initAsync() async {
List<int> token = widget.message.messageContent!.downloadToken!;
_imageByte = await getDownloadedMedia(token, widget.message.messageId);
print(_imageByte);
setState(() {});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
body: Text(widget.otherUser.displayName), body: Stack(
fit: StackFit.expand,
children: [
Positioned(
top: 0,
// bottom: 0,
left: 0,
right: 0,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 50),
child: ClipRRect(
borderRadius: BorderRadius.circular(22),
child: (_imageByte != null)
? Image.memory(
_imageByte!,
fit: BoxFit.contain,
)
: CircularProgressIndicator()),
),
),
_imageByte != null
? 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);
},
),
],
),
)
: Container(),
_imageByte != null
? 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 {
// Navigator.push(
// context,
// MaterialPageRoute(
// builder: (context) =>
// ShareImageView(image: widget.image)),
// );
},
style: ButtonStyle(
padding: WidgetStateProperty.all<EdgeInsets>(
EdgeInsets.symmetric(vertical: 10, horizontal: 30),
),
),
label: Text(
AppLocalizations.of(context)!
.shareImagedEditorShareWith,
style: TextStyle(fontSize: 17),
),
),
],
),
)
: Container(),
],
),
); );
} }
} }

View file

@ -198,6 +198,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.0.8" version: "0.0.8"
connectivity_plus:
dependency: "direct main"
description:
name: connectivity_plus
sha256: "8a68739d3ee113e51ad35583fdf9ab82c55d09d693d3c39da1aebab87c938412"
url: "https://pub.dev"
source: hosted
version: "6.1.2"
connectivity_plus_platform_interface:
dependency: transitive
description:
name: connectivity_plus_platform_interface
sha256: "42657c1715d48b167930d5f34d00222ac100475f73d10162ddf43e714932f204"
url: "https://pub.dev"
source: hosted
version: "2.0.1"
convert: convert:
dependency: transitive dependency: transitive
description: description:
@ -238,6 +254,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.1" version: "3.0.1"
dbus:
dependency: transitive
description:
name: dbus
sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c"
url: "https://pub.dev"
source: hosted
version: "0.7.11"
device_info_plus: device_info_plus:
dependency: transitive dependency: transitive
description: description:
@ -730,6 +754,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.0" version: "1.0.0"
nm:
dependency: transitive
description:
name: nm
sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254"
url: "https://pub.dev"
source: hosted
version: "0.5.0"
optional: optional:
dependency: transitive dependency: transitive
description: description:

View file

@ -12,6 +12,7 @@ environment:
dependencies: dependencies:
camerawesome: ^2.1.0 camerawesome: ^2.1.0
collection: ^1.18.0 collection: ^1.18.0
connectivity_plus: ^6.1.2
cv: ^1.1.3 cv: ^1.1.3
fixnum: ^1.1.1 fixnum: ^1.1.1
flutter: flutter: