mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-01-15 07:48:40 +00:00
download and preview of images
This commit is contained in:
parent
a9572e7890
commit
f866e4315e
12 changed files with 451 additions and 162 deletions
|
|
@ -5,6 +5,7 @@ import 'package:logging/logging.dart';
|
|||
import 'package:twonly/main.dart';
|
||||
import 'package:twonly/src/app.dart';
|
||||
import 'package:twonly/src/model/json/message.dart';
|
||||
import 'package:twonly/src/providers/api/api.dart';
|
||||
|
||||
class DbMessage {
|
||||
DbMessage({
|
||||
|
|
@ -15,6 +16,7 @@ class DbMessage {
|
|||
required this.messageContent,
|
||||
required this.messageOpenedAt,
|
||||
required this.messageAcknowledgeByUser,
|
||||
required this.isDownloaded,
|
||||
required this.messageAcknowledgeByServer,
|
||||
required this.sendOrReceivedAt,
|
||||
});
|
||||
|
|
@ -27,6 +29,7 @@ class DbMessage {
|
|||
MessageContent? messageContent;
|
||||
DateTime? messageOpenedAt;
|
||||
bool messageAcknowledgeByUser;
|
||||
bool isDownloaded;
|
||||
bool messageAcknowledgeByServer;
|
||||
DateTime sendOrReceivedAt;
|
||||
|
||||
|
|
@ -130,11 +133,11 @@ class DbMessages extends CvModelBase {
|
|||
}
|
||||
}
|
||||
|
||||
static Future insertOtherMessage(int userIdFrom, MessageKind kind,
|
||||
int messageId, String jsonContent) async {
|
||||
static Future<int?> insertOtherMessage(int userIdFrom, MessageKind kind,
|
||||
int messageOtherId, String jsonContent) async {
|
||||
try {
|
||||
await dbProvider.db!.insert(tableName, {
|
||||
columnMessageOtherId: messageId,
|
||||
int messageId = await dbProvider.db!.insert(tableName, {
|
||||
columnMessageOtherId: messageOtherId,
|
||||
columnMessageKind: kind.index,
|
||||
columnMessageContentJson: jsonContent,
|
||||
columnMessageAcknowledgeByServer: 1,
|
||||
|
|
@ -144,13 +147,26 @@ class DbMessages extends CvModelBase {
|
|||
columnSendOrReceivedAt: DateTime.now().toIso8601String()
|
||||
});
|
||||
globalCallBackOnMessageChange(userIdFrom);
|
||||
return true;
|
||||
return messageId;
|
||||
} catch (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(
|
||||
int otherUserId) async {
|
||||
var rows = await dbProvider.db!.query(
|
||||
|
|
@ -161,7 +177,7 @@ class DbMessages extends CvModelBase {
|
|||
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
|
||||
List<DbMessage> notAckByServer =
|
||||
|
|
@ -177,13 +193,11 @@ class DbMessages extends CvModelBase {
|
|||
return messages[0];
|
||||
}
|
||||
|
||||
static Future acknowledgeMessageByServer(int messageId) async {
|
||||
Map<String, dynamic> valuesToUpdate = {
|
||||
columnMessageAcknowledgeByServer: 1,
|
||||
};
|
||||
static Future _updateByMessageId(
|
||||
int messageId, Map<String, dynamic> data) async {
|
||||
await dbProvider.db!.update(
|
||||
tableName,
|
||||
valuesToUpdate,
|
||||
data,
|
||||
where: "$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
|
||||
static Future acknowledgeMessageByUser(int fromUserId, int messageId) async {
|
||||
Map<String, dynamic> valuesToUpdate = {
|
||||
|
|
@ -216,7 +244,8 @@ class DbMessages extends CvModelBase {
|
|||
sendOrReceivedAt
|
||||
];
|
||||
|
||||
static List<DbMessage> convertToDbMessage(List<dynamic> fromDb) {
|
||||
static Future<List<DbMessage>> convertToDbMessage(
|
||||
List<dynamic> fromDb) async {
|
||||
try {
|
||||
List<DbMessage> parsedUsers = [];
|
||||
for (int i = 0; i < fromDb.length; i++) {
|
||||
|
|
@ -229,6 +258,16 @@ class DbMessages extends CvModelBase {
|
|||
content = MessageContent.fromJson(
|
||||
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(
|
||||
DbMessage(
|
||||
sendOrReceivedAt:
|
||||
|
|
@ -236,9 +275,9 @@ class DbMessages extends CvModelBase {
|
|||
messageId: fromDb[i][columnMessageId],
|
||||
messageOtherId: fromDb[i][columnMessageOtherId],
|
||||
otherUserId: fromDb[i][columnOtherUserId],
|
||||
messageKind:
|
||||
MessageKindExtension.fromIndex(fromDb[i][columnMessageKind]),
|
||||
messageKind: messageKind,
|
||||
messageContent: content,
|
||||
isDownloaded: isDownloaded,
|
||||
messageOpenedAt: messageOpenedAt,
|
||||
messageAcknowledgeByUser:
|
||||
fromDb[i][columnMessageAcknowledgeByUser] == 1,
|
||||
|
|
|
|||
|
|
@ -281,6 +281,7 @@ class DownloadData extends $pb.GeneratedMessage {
|
|||
$core.List<$core.int>? uploadToken,
|
||||
$core.int? offset,
|
||||
$core.List<$core.int>? data,
|
||||
$core.bool? fin,
|
||||
}) {
|
||||
final $result = create();
|
||||
if (uploadToken != null) {
|
||||
|
|
@ -292,6 +293,9 @@ class DownloadData extends $pb.GeneratedMessage {
|
|||
if (data != null) {
|
||||
$result.data = data;
|
||||
}
|
||||
if (fin != null) {
|
||||
$result.fin = fin;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
DownloadData._() : super();
|
||||
|
|
@ -302,6 +306,7 @@ class DownloadData extends $pb.GeneratedMessage {
|
|||
..a<$core.List<$core.int>>(1, _omitFieldNames ? '' : 'uploadToken', $pb.PbFieldType.OY)
|
||||
..a<$core.int>(2, _omitFieldNames ? '' : 'offset', $pb.PbFieldType.OU3)
|
||||
..a<$core.List<$core.int>>(3, _omitFieldNames ? '' : 'data', $pb.PbFieldType.OY)
|
||||
..aOB(4, _omitFieldNames ? '' : 'fin')
|
||||
..hasRequiredFields = false
|
||||
;
|
||||
|
||||
|
|
@ -352,6 +357,15 @@ class DownloadData extends $pb.GeneratedMessage {
|
|||
$core.bool hasData() => $_has(2);
|
||||
@$pb.TagNumber(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 {
|
||||
|
|
|
|||
|
|
@ -73,13 +73,14 @@ const DownloadData$json = {
|
|||
{'1': 'upload_token', '3': 1, '4': 1, '5': 12, '10': 'uploadToken'},
|
||||
{'1': 'offset', '3': 2, '4': 1, '5': 13, '10': 'offset'},
|
||||
{'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`.
|
||||
final $typed_data.Uint8List downloadDataDescriptor = $convert.base64Decode(
|
||||
'CgxEb3dubG9hZERhdGESIQoMdXBsb2FkX3Rva2VuGAEgASgMUgt1cGxvYWRUb2tlbhIWCgZvZm'
|
||||
'ZzZXQYAiABKA1SBm9mZnNldBISCgRkYXRhGAMgASgMUgRkYXRh');
|
||||
'ZzZXQYAiABKA1SBm9mZnNldBISCgRkYXRhGAMgASgMUgRkYXRhEhAKA2ZpbhgEIAEoCFIDZmlu');
|
||||
|
||||
@$core.Deprecated('Use responseDescriptor instead')
|
||||
const Response$json = {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
import 'package:fixnum/fixnum.dart';
|
||||
import 'package:flutter/foundation.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 {
|
||||
print("check if free network connection");
|
||||
|
||||
print("Downloading: " + imageToken.toString());
|
||||
if (!force) {
|
||||
// TODO: create option to enable download via mobile data
|
||||
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();
|
||||
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]);
|
||||
|
||||
// box.put(imageToken.toString(), imageBytes);
|
||||
box.close();
|
||||
return media;
|
||||
}
|
||||
|
||||
Future<bool> isMediaDownloaded(List<int> mediaToken) async {
|
||||
final box = await getMediaStorage();
|
||||
|
||||
// box.put('secret', 'Hive is awesome');
|
||||
|
||||
return box.containsKey(mediaToken.toString());
|
||||
return box.containsKey("${mediaToken}_downloaded");
|
||||
}
|
||||
|
||||
Future initMediaStorage() async {
|
||||
|
|
|
|||
|
|
@ -1,15 +1,20 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
import 'package:fixnum/fixnum.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
|
||||
import 'package:logging/logging.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/json/message.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.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.pbserver.dart';
|
||||
import 'package:twonly/src/providers/api/api.dart';
|
||||
import 'package:twonly/src/providers/api/api_utils.dart';
|
||||
// ignore: library_prefixes
|
||||
|
|
@ -18,77 +23,22 @@ import 'package:twonly/src/utils/signal.dart' as SignalHelper;
|
|||
Future handleServerMessage(server.ServerToClient msg) async {
|
||||
client.Response? response;
|
||||
|
||||
if (msg.v0.hasRequestNewPreKeys()) {
|
||||
List<PreKeyRecord> localPreKeys = await SignalHelper.getPreKeys();
|
||||
|
||||
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());
|
||||
try {
|
||||
if (msg.v0.hasRequestNewPreKeys()) {
|
||||
response = await handleRequestNewPreKey();
|
||||
} else if (msg.v0.hasNewMessage()) {
|
||||
Uint8List body = Uint8List.fromList(msg.v0.newMessage.body);
|
||||
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;
|
||||
}
|
||||
var prekeys = client.Response_Prekeys(prekeys: prekeysList);
|
||||
var ok = client.Response_Ok()..prekeys = prekeys;
|
||||
response = client.Response()..ok = ok;
|
||||
} else if (msg.v0.hasNewMessage()) {
|
||||
Uint8List body = Uint8List.fromList(msg.v0.newMessage.body);
|
||||
Int64 fromUserId = msg.v0.newMessage.fromUserId;
|
||||
Message? message = await SignalHelper.getDecryptedText(fromUserId, body);
|
||||
if (message != null) {
|
||||
switch (message.kind) {
|
||||
case MessageKind.contactRequest:
|
||||
Result username = await apiProvider.getUsername(fromUserId);
|
||||
if (username.isSuccess) {
|
||||
Uint8List name = username.value.userdata.username;
|
||||
DbContacts.insertNewContact(
|
||||
utf8.decode(name), fromUserId.toInt(), true);
|
||||
}
|
||||
break;
|
||||
case MessageKind.rejectRequest:
|
||||
DbContacts.deleteUser(fromUserId.toInt());
|
||||
break;
|
||||
case MessageKind.acceptRequest:
|
||||
DbContacts.acceptUser(fromUserId.toInt());
|
||||
break;
|
||||
case MessageKind.ack:
|
||||
DbMessages.acknowledgeMessageByUser(
|
||||
fromUserId.toInt(), message.messageId!);
|
||||
break;
|
||||
default:
|
||||
if (message.kind != MessageKind.textMessage &&
|
||||
message.kind != MessageKind.video &&
|
||||
message.kind != MessageKind.image) {
|
||||
Logger("handleServerMessages")
|
||||
.shout("Got unknown MessageKind $message");
|
||||
} else {
|
||||
String content = jsonEncode(message.content!.toJson());
|
||||
await DbMessages.insertOtherMessage(
|
||||
fromUserId.toInt(), message.kind, message.messageId!, content);
|
||||
|
||||
encryptAndSendMessage(
|
||||
fromUserId,
|
||||
Message(
|
||||
kind: MessageKind.ack,
|
||||
messageId: message.messageId!,
|
||||
timestamp: DateTime.now(),
|
||||
),
|
||||
);
|
||||
|
||||
if (message.kind == MessageKind.video ||
|
||||
message.kind == MessageKind.image) {
|
||||
dynamic content = message.content!;
|
||||
List<int> downloadToken = content.downloadToken;
|
||||
tryDownloadMedia(downloadToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var ok = client.Response_Ok()..none = true;
|
||||
response = client.Response()..ok = ok;
|
||||
} 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()
|
||||
|
|
@ -97,3 +47,121 @@ Future handleServerMessage(server.ServerToClient msg) async {
|
|||
|
||||
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);
|
||||
if (message != null) {
|
||||
switch (message.kind) {
|
||||
case MessageKind.contactRequest:
|
||||
Result username = await apiProvider.getUsername(fromUserId);
|
||||
if (username.isSuccess) {
|
||||
Uint8List name = username.value.userdata.username;
|
||||
DbContacts.insertNewContact(
|
||||
utf8.decode(name), fromUserId.toInt(), true);
|
||||
}
|
||||
break;
|
||||
case MessageKind.rejectRequest:
|
||||
DbContacts.deleteUser(fromUserId.toInt());
|
||||
break;
|
||||
case MessageKind.acceptRequest:
|
||||
DbContacts.acceptUser(fromUserId.toInt());
|
||||
break;
|
||||
case MessageKind.ack:
|
||||
DbMessages.acknowledgeMessageByUser(
|
||||
fromUserId.toInt(), message.messageId!);
|
||||
break;
|
||||
default:
|
||||
if (message.kind != MessageKind.textMessage &&
|
||||
message.kind != MessageKind.video &&
|
||||
message.kind != MessageKind.image) {
|
||||
Logger("handleServerMessages")
|
||||
.shout("Got unknown MessageKind $message");
|
||||
} else {
|
||||
String content = jsonEncode(message.content!.toJson());
|
||||
int? messageId = await DbMessages.insertOtherMessage(
|
||||
fromUserId.toInt(), message.kind, message.messageId!, content);
|
||||
|
||||
if (messageId == null) {
|
||||
return client.Response()..error = ErrorCode.InternalError;
|
||||
}
|
||||
|
||||
encryptAndSendMessage(
|
||||
fromUserId,
|
||||
Message(
|
||||
kind: MessageKind.ack,
|
||||
messageId: message.messageId!,
|
||||
timestamp: DateTime.now(),
|
||||
),
|
||||
);
|
||||
|
||||
if (message.kind == MessageKind.video ||
|
||||
message.kind == MessageKind.image) {
|
||||
dynamic content = message.content!;
|
||||
List<int> downloadToken = content.downloadToken;
|
||||
|
||||
Box box = await getMediaStorage();
|
||||
box.put("${downloadToken}_fromUserId", fromUserId.toInt());
|
||||
|
||||
tryDownloadMedia(downloadToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var ok = client.Response_Ok()..none = true;
|
||||
return client.Response()..ok = ok;
|
||||
}
|
||||
|
||||
Future<client.Response> handleRequestNewPreKey() async {
|
||||
List<PreKeyRecord> localPreKeys = await SignalHelper.getPreKeys();
|
||||
|
||||
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;
|
||||
return client.Response()..ok = ok;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -255,6 +255,13 @@ class ApiProvider {
|
|||
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 {
|
||||
log.shout("fragmentate the data");
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
try {
|
||||
ConnectSignalProtocolStore signalStore = (await getSignalStore())!;
|
||||
|
|
|
|||
|
|
@ -1,18 +1,21 @@
|
|||
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/json/message.dart';
|
||||
import 'package:twonly/src/model/messages_model.dart';
|
||||
|
||||
class AlignedTextBox extends StatelessWidget {
|
||||
const AlignedTextBox({super.key, required this.text, required this.right});
|
||||
final String text;
|
||||
final bool right;
|
||||
class ChatListEntry extends StatelessWidget {
|
||||
const ChatListEntry(this.message, {super.key});
|
||||
final DbMessage message;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Align(
|
||||
alignment: right ? Alignment.centerRight : Alignment.centerLeft,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(10),
|
||||
child: Container(
|
||||
bool right = message.messageOtherId == null;
|
||||
|
||||
Widget child = Container();
|
||||
|
||||
switch (message.messageKind) {
|
||||
case MessageKind.textMessage:
|
||||
child = Container(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: MediaQuery.of(context).size.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
|
||||
),
|
||||
child: Text(
|
||||
text,
|
||||
message.messageContent!.text!,
|
||||
style: TextStyle(
|
||||
color: Colors.white, // Set text color for contrast
|
||||
fontSize: 17,
|
||||
),
|
||||
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.
|
||||
class ChatItemDetailsView extends StatelessWidget {
|
||||
class ChatItemDetailsView extends StatefulWidget {
|
||||
const ChatItemDetailsView({super.key, required this.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
|
||||
Widget build(BuildContext context) {
|
||||
List<List<dynamic>> messages = [
|
||||
["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();
|
||||
// messages = messages.reversed.toList();
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('Your Chat with ${user.displayName}'),
|
||||
title: Text('Your Chat with ${widget.user.displayName}'),
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: messages.length, // Number of items in the list
|
||||
itemCount: _messages.length, // Number of items in the list
|
||||
reverse: true,
|
||||
itemBuilder: (context, i) {
|
||||
return AlignedTextBox(
|
||||
text: messages[i][0], right: messages[i][1]);
|
||||
return ChatListEntry(_messages[i]);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -132,23 +132,10 @@ class UserListItem extends StatefulWidget {
|
|||
class _UserListItem extends State<UserListItem> {
|
||||
int flames = 0;
|
||||
int lastMessageInSeconds = 0;
|
||||
bool isDownloaded = true;
|
||||
|
||||
@override
|
||||
void 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
|
||||
|
|
@ -184,8 +171,8 @@ class _UserListItem extends State<UserListItem> {
|
|||
title: Text(widget.user.displayName),
|
||||
subtitle: Row(
|
||||
children: [
|
||||
MessageSendStateIcon(
|
||||
state, isDownloaded, widget.lastMessage.messageKind),
|
||||
MessageSendStateIcon(state, widget.lastMessage.isDownloaded,
|
||||
widget.lastMessage.messageKind),
|
||||
Text("•"),
|
||||
const SizedBox(width: 5),
|
||||
Text(
|
||||
|
|
@ -213,18 +200,17 @@ class _UserListItem extends State<UserListItem> {
|
|||
),
|
||||
leading: InitialsAvatar(displayName: widget.user.displayName),
|
||||
onTap: () {
|
||||
if (!widget.lastMessage.isDownloaded) {
|
||||
List<int> token = widget.lastMessage.messageContent!.downloadToken!;
|
||||
tryDownloadMedia(token, force: true);
|
||||
return;
|
||||
}
|
||||
Navigator.push(
|
||||
context,
|
||||
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 MediaViewerView(widget.user, widget.lastMessage);
|
||||
}
|
||||
return ChatItemDetailsView(user: widget.user);
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -1,19 +1,115 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
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/messages_model.dart';
|
||||
import 'package:twonly/src/providers/api/api.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
|
||||
class MediaViewerView extends StatefulWidget {
|
||||
final Contact otherUser;
|
||||
const MediaViewerView(this.otherUser, {super.key});
|
||||
final DbMessage message;
|
||||
const MediaViewerView(this.otherUser, this.message, {super.key});
|
||||
|
||||
@override
|
||||
State<MediaViewerView> createState() => _MediaViewerViewState();
|
||||
}
|
||||
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
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(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
32
pubspec.lock
32
pubspec.lock
|
|
@ -198,6 +198,22 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -238,6 +254,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -730,6 +754,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
nm:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: nm
|
||||
sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.0"
|
||||
optional:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ environment:
|
|||
dependencies:
|
||||
camerawesome: ^2.1.0
|
||||
collection: ^1.18.0
|
||||
connectivity_plus: ^6.1.2
|
||||
cv: ^1.1.3
|
||||
fixnum: ^1.1.1
|
||||
flutter:
|
||||
|
|
|
|||
Loading…
Reference in a new issue