start with chat view

This commit is contained in:
otsmr 2025-01-27 01:43:52 +01:00
parent 2ab0ad3daa
commit 2f3af42771
17 changed files with 440 additions and 226 deletions

View file

@ -1,3 +1,4 @@
import 'package:fixnum/fixnum.dart';
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/notify_provider.dart'; import 'package:twonly/src/providers/notify_provider.dart';
@ -11,6 +12,10 @@ import 'package:flutter_localizations/flutter_localizations.dart';
import 'dart:async'; import 'dart:async';
import 'settings/settings_controller.dart'; import 'settings/settings_controller.dart';
Function(Int64) addSendingTo = (a) {};
Function(Int64) removeSendingTo = (a) {};
Function() updateNotifyProvider = () {};
/// The Widget that configures your application. /// The Widget that configures your application.
class MyApp extends StatefulWidget { class MyApp extends StatefulWidget {
const MyApp({super.key, required this.settingsController}); const MyApp({super.key, required this.settingsController});
@ -43,6 +48,18 @@ class _MyAppState extends State<MyApp> {
context.read<NotifyProvider>().update(); context.read<NotifyProvider>().update();
}); });
addSendingTo = (a) {
context.read<NotifyProvider>().addSendingTo(a);
};
removeSendingTo = (a) {
context.read<NotifyProvider>().removeSendingTo(a);
};
updateNotifyProvider = () {
context.read<NotifyProvider>().update();
};
context.read<NotifyProvider>().update(); context.read<NotifyProvider>().update();
apiProvider.connect(); apiProvider.connect();
} }

View file

@ -1,16 +1,18 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
enum MessageSendState { enum MessageSendState {
opened,
received, received,
receivedOpened,
receiving,
send, send,
sendOpened,
sending, sending,
} }
class MessageSendStateIcon extends StatelessWidget { class MessageSendStateIcon extends StatelessWidget {
final MessageSendState state; final MessageSendState state;
const MessageSendStateIcon({super.key, required this.state}); const MessageSendStateIcon(this.state, {super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -18,7 +20,8 @@ class MessageSendStateIcon extends StatelessWidget {
String text = ""; String text = "";
switch (state) { switch (state) {
case MessageSendState.opened: case MessageSendState.receivedOpened:
case MessageSendState.sendOpened:
icon = Icon( icon = Icon(
Icons.crop_square, Icons.crop_square,
size: 14, size: 14,
@ -42,6 +45,7 @@ class MessageSendStateIcon extends StatelessWidget {
text = "Send"; text = "Send";
break; break;
case MessageSendState.sending: case MessageSendState.sending:
case MessageSendState.receiving:
icon = Row( icon = Row(
children: [ children: [
SizedBox( SizedBox(

View file

@ -38,7 +38,7 @@ class DbContacts extends CvModelBase {
static String getCreateTableString() { static String getCreateTableString() {
return """ return """
CREATE TABLE $tableName ( CREATE TABLE IF NOT EXISTS $tableName (
$columnUserId INTEGER NOT NULL PRIMARY KEY, $columnUserId INTEGER NOT NULL PRIMARY KEY,
$columnDisplayName TEXT, $columnDisplayName TEXT,
$columnAccepted INT NOT NULL DEFAULT 0, $columnAccepted INT NOT NULL DEFAULT 0,

View file

@ -19,7 +19,7 @@ class DbSignalIdentityKeyStore extends CvModelBase {
static String getCreateTableString() { static String getCreateTableString() {
return """ return """
CREATE TABLE $tableName ( CREATE TABLE IF NOT EXISTS $tableName (
$columnDeviceId INTEGER NOT NULL, $columnDeviceId INTEGER NOT NULL,
$columnName TEXT NOT NULL, $columnName TEXT NOT NULL,
$columnIdentityKey BLOB NOT NULL, $columnIdentityKey BLOB NOT NULL,

View file

@ -1,4 +1,3 @@
import 'dart:convert';
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import 'package:twonly/src/utils/json.dart'; import 'package:twonly/src/utils/json.dart';
part 'message.g.dart'; part 'message.g.dart';
@ -9,7 +8,22 @@ enum MessageKind {
video, video,
contactRequest, contactRequest,
rejectRequest, rejectRequest,
acceptRequest acceptRequest,
ack
}
extension MessageKindExtension on MessageKind {
String get name => toString().split('.').last;
static MessageKind fromString(String name) {
return MessageKind.values.firstWhere((e) => e.name == name);
}
int get index => this.index;
static MessageKind fromIndex(int index) {
return MessageKind.values[index];
}
} }
// so _$MessageKindEnumMap gets generated // so _$MessageKindEnumMap gets generated
@ -23,84 +37,33 @@ class Message {
@Int64Converter() @Int64Converter()
final MessageKind kind; final MessageKind kind;
final MessageContent? content; final MessageContent? content;
final int? messageId;
DateTime timestamp; DateTime timestamp;
Message({required this.kind, this.content, required this.timestamp}); Message(
{required this.kind,
this.messageId,
this.content,
required this.timestamp});
@override @override
String toString() { String toString() {
return 'Message(kind: $kind, content: $content, timestamp: $timestamp)'; return 'Message(kind: $kind, content: $content, timestamp: $timestamp)';
} }
static Message fromJson(String jsonString) { factory Message.fromJson(Map<String, dynamic> json) =>
Map<String, dynamic> json = jsonDecode(jsonString); _$MessageFromJson(json);
dynamic content; Map<String, dynamic> toJson() => _$MessageToJson(this);
MessageKind kind = $enumDecode(_$MessageKindEnumMap, json['kind']);
switch (kind) {
case MessageKind.textMessage:
content = TextContent.fromJson(json["content"]);
break;
case MessageKind.image:
content = ImageContent.fromJson(json["content"]);
break;
default:
}
return Message(
kind: kind,
timestamp: DateTime.parse(json['timestamp'] as String),
content: content,
);
}
String toJson() {
var json = <String, dynamic>{
'kind': _$MessageKindEnumMap[kind]!,
'timestamp': timestamp.toIso8601String(),
'content': content
};
return jsonEncode(json);
}
}
abstract class MessageContent {
MessageContent();
factory MessageContent.fromJson(Map<String, dynamic> json) {
return TextContent("");
}
Map<String, dynamic> toJson();
}
// factory MessageContent.fromJson(Map<String, dynamic> json) =>
// _$MessageContentFromJson(json);
@JsonSerializable()
class TextContent extends MessageContent {
final String text;
TextContent(this.text);
factory TextContent.fromJson(Map<String, dynamic> json) =>
_$TextContentFromJson(json);
@override
Map<String, dynamic> toJson() => _$TextContentToJson(this);
} }
@JsonSerializable() @JsonSerializable()
class ImageContent extends MessageContent { class MessageContent {
final List<int> imageToken; final String? text;
final List<int>? downloadToken;
ImageContent(this.imageToken); MessageContent({required this.text, required this.downloadToken});
factory ImageContent.fromJson(Map<String, dynamic> json) => factory MessageContent.fromJson(Map<String, dynamic> json) =>
_$ImageContentFromJson(json); _$MessageContentFromJson(json);
@override Map<String, dynamic> toJson() => _$MessageContentToJson(this);
Map<String, dynamic> toJson() => _$ImageContentToJson(this);
} }
// @JsonSerializable()
// class VideoContent extends MessageContent {
// final String videoUrl;
// VideoContent(this.videoUrl);
// }

View file

@ -21,10 +21,12 @@ const _$MessageKindEnumMap = {
MessageKind.contactRequest: 'contactRequest', MessageKind.contactRequest: 'contactRequest',
MessageKind.rejectRequest: 'rejectRequest', MessageKind.rejectRequest: 'rejectRequest',
MessageKind.acceptRequest: 'acceptRequest', MessageKind.acceptRequest: 'acceptRequest',
MessageKind.ack: 'ack',
}; };
Message _$MessageFromJson(Map<String, dynamic> json) => Message( Message _$MessageFromJson(Map<String, dynamic> json) => Message(
kind: $enumDecode(_$MessageKindEnumMap, json['kind']), kind: $enumDecode(_$MessageKindEnumMap, json['kind']),
messageId: (json['messageId'] as num?)?.toInt(),
content: json['content'] == null content: json['content'] == null
? null ? null
: MessageContent.fromJson(json['content'] as Map<String, dynamic>), : MessageContent.fromJson(json['content'] as Map<String, dynamic>),
@ -34,25 +36,20 @@ Message _$MessageFromJson(Map<String, dynamic> json) => Message(
Map<String, dynamic> _$MessageToJson(Message instance) => <String, dynamic>{ Map<String, dynamic> _$MessageToJson(Message instance) => <String, dynamic>{
'kind': _$MessageKindEnumMap[instance.kind]!, 'kind': _$MessageKindEnumMap[instance.kind]!,
'content': instance.content, 'content': instance.content,
'messageId': instance.messageId,
'timestamp': instance.timestamp.toIso8601String(), 'timestamp': instance.timestamp.toIso8601String(),
}; };
TextContent _$TextContentFromJson(Map<String, dynamic> json) => TextContent( MessageContent _$MessageContentFromJson(Map<String, dynamic> json) =>
json['text'] as String, MessageContent(
); text: json['text'] as String?,
downloadToken: (json['downloadToken'] as List<dynamic>?)
Map<String, dynamic> _$TextContentToJson(TextContent instance) => ?.map((e) => (e as num).toInt())
<String, dynamic>{
'text': instance.text,
};
ImageContent _$ImageContentFromJson(Map<String, dynamic> json) => ImageContent(
(json['imageToken'] as List<dynamic>)
.map((e) => (e as num).toInt())
.toList(), .toList(),
); );
Map<String, dynamic> _$ImageContentToJson(ImageContent instance) => Map<String, dynamic> _$MessageContentToJson(MessageContent instance) =>
<String, dynamic>{ <String, dynamic>{
'imageToken': instance.imageToken, 'text': instance.text,
'downloadToken': instance.downloadToken,
}; };

View file

@ -1,27 +1,176 @@
import 'dart:convert';
import 'package:cv/cv.dart'; import 'package:cv/cv.dart';
import 'package:logging/logging.dart';
import 'package:twonly/main.dart';
import 'package:twonly/src/model/json/message.dart';
class DbMessage {
DbMessage({
required this.messageId,
required this.messageOtherId,
required this.otherUserId,
required this.messageMessageKind,
required this.messageContent,
required this.messageOpenedAt,
required this.messageAcknowledge,
required this.sendOrReceivedAt,
});
int messageId;
// is this null then the message was sent from the user itself
int? messageOtherId;
int otherUserId;
MessageKind messageMessageKind;
MessageContent messageContent;
DateTime? messageOpenedAt;
bool messageAcknowledge;
DateTime sendOrReceivedAt;
}
class DbMessages extends CvModelBase { class DbMessages extends CvModelBase {
static const tableName = "messages"; static const tableName = "messages";
static const columnMessageId = "messageId"; static const columnMessageId = "id";
final messageId = CvField<int>(columnMessageId); final messageId = CvField<int>(columnMessageId);
static const columnBody = "body"; static const columnMessageOtherId = "message_other_id";
final messageBody = CvField<int>(columnBody); final messageOtherId = CvField<int?>(columnMessageOtherId);
static const columnCreatedAt = "created_at"; static const columnOtherUserId = "other_user_id";
final createdAt = CvField<DateTime>(columnCreatedAt); final otherUserId = CvField<int?>(columnOtherUserId);
static const columnMessageKind = "message_kind";
final messageMessageKind = CvField<int>(columnMessageKind);
static const columnMessageContentJson = "message_json";
final messageContentJson = CvField<String>(columnMessageContentJson);
static const columnMessageOpenedAt = "message_opened_at";
final messageOpenedAt = CvField<DateTime?>(columnMessageOpenedAt);
static const columnMessageAcknowledge = "message_acknowledged";
final messageAcknowledge = CvField<int>(columnMessageAcknowledge);
static const columnSendOrReceivedAt = "message_send_or_received_at";
final sendOrReceivedAt = CvField<DateTime>(columnSendOrReceivedAt);
static const columnUpdatedAt = "updated_at";
final updatedAt = CvField<DateTime>(columnUpdatedAt);
static String getCreateTableString() { static String getCreateTableString() {
return """ return """
CREATE TABLE $tableName ( CREATE TABLE IF NOT EXISTS $tableName (
$columnMessageId INTEGER NOT NULL, $columnMessageId INTEGER NOT NULL PRIMARY KEY,
$columnCreatedAt DATETIME DEFAULT CURRENT_TIMESTAMP, $columnMessageOtherId INTEGER DEFAULT NULL,
PRIMARY KEY ($columnMessageId) $columnOtherUserId INTEGER DEFAULT NULL,
$columnMessageKind INTEGER NOT NULL,
$columnMessageAcknowledge INTEGER NOT NULL DEFAULT 0,
$columnMessageContentJson TEXT NOT NULL,
$columnMessageOpenedAt DATETIME DEFAULT NULL,
$columnSendOrReceivedAt DATETIME DEFAULT CURRENT_TIMESTAMP,
$columnUpdatedAt DATETIME DEFAULT CURRENT_TIMESTAMP
) )
"""; """;
} }
static Future<int?> insertMyMessage(
int userIdFrom, MessageKind kind, String jsonContent) async {
try {
int messageId = await dbProvider.db!.insert(tableName, {
columnMessageKind: kind.index,
columnMessageContentJson: jsonContent,
columnOtherUserId: userIdFrom,
columnSendOrReceivedAt: DateTime.now().toIso8601String()
});
return messageId;
} catch (e) {
Logger("contacts_model/getUsers").shout("$e");
return null;
}
}
static Future insertOtherMessage(int userIdFrom, MessageKind kind,
int messageId, String jsonContent) async {
try {
await dbProvider.db!.insert(tableName, {
columnMessageOtherId: messageId,
columnMessageKind: kind.index,
columnMessageContentJson: jsonContent,
columnOtherUserId: userIdFrom
});
return true;
} catch (e) {
Logger("contacts_model/getUsers").shout("$e");
return false;
}
}
static List<DbMessage> convertToDbMessage(List<dynamic> fromDb) {
try {
List<DbMessage> parsedUsers = [];
for (int i = 0; i < fromDb.length; i++) {
dynamic messageOpenedAt = fromDb[i][columnMessageOpenedAt];
if (messageOpenedAt != null) {
messageOpenedAt = DateTime.tryParse(fromDb[i][columnMessageOpenedAt]);
}
print("Datetime: ${fromDb[i][columnSendOrReceivedAt]}");
print(
"Datetime parsed: ${DateTime.tryParse(fromDb[i][columnSendOrReceivedAt])}");
parsedUsers.add(
DbMessage(
sendOrReceivedAt:
DateTime.tryParse(fromDb[i][columnSendOrReceivedAt])!,
messageId: fromDb[i][columnMessageId],
messageOtherId: fromDb[i][columnMessageOtherId],
otherUserId: fromDb[i][columnOtherUserId],
messageMessageKind:
MessageKindExtension.fromIndex(fromDb[i][columnMessageKind]),
messageContent: MessageContent.fromJson(
jsonDecode(fromDb[i][columnMessageContentJson])),
messageOpenedAt: messageOpenedAt,
messageAcknowledge: fromDb[i][columnMessageAcknowledge] == 1,
),
);
}
return parsedUsers;
} catch (e) {
Logger("messages_model/convertToDbMessage").shout("$e");
return [];
}
}
static Future<DbMessage?> getLastMessagesForPreviewForUser(
int otherUserId) async {
var rows = await dbProvider.db!.query(tableName,
where: "$columnOtherUserId = ?",
whereArgs: [otherUserId],
orderBy: "$columnUpdatedAt DESC",
limit: 1);
List<DbMessage> messages = convertToDbMessage(rows);
if (messages.isEmpty) return null;
return messages[0];
}
static Future acknowledgeMessage(int fromUserId, int messageId) async {
Map<String, dynamic> valuesToUpdate = {
columnMessageAcknowledge: 1,
};
await dbProvider.db!.update(
tableName,
valuesToUpdate,
where: "$messageId = ? AND $columnOtherUserId = ?",
whereArgs: [messageId, fromUserId],
);
}
@override @override
List<CvField> get fields => [messageId, createdAt]; List<CvField> get fields => [
messageId,
messageMessageKind,
messageContentJson,
messageOpenedAt,
sendOrReceivedAt
];
} }

View file

@ -15,7 +15,7 @@ class DbSignalPreKeyStore extends CvModelBase {
static String getCreateTableString() { static String getCreateTableString() {
return """ return """
CREATE TABLE $tableName ( CREATE TABLE IF NOT EXISTS $tableName (
$columnPreKeyId INTEGER NOT NULL, $columnPreKeyId INTEGER NOT NULL,
$columnPreKey BLOB NOT NULL, $columnPreKey BLOB NOT NULL,
$columnCreatedAt DATETIME DEFAULT CURRENT_TIMESTAMP, $columnCreatedAt DATETIME DEFAULT CURRENT_TIMESTAMP,

View file

@ -15,7 +15,7 @@ class DbSignalSenderKeyStore extends CvModelBase {
static String getCreateTableString() { static String getCreateTableString() {
return """ return """
CREATE TABLE $tableName ( CREATE TABLE IF NOT EXISTS $tableName (
$columnSenderKeyName TEXT NOT NULL, $columnSenderKeyName TEXT NOT NULL,
$columnSenderKey BLOB NOT NULL, $columnSenderKey BLOB NOT NULL,
$columnCreatedAt DATETIME DEFAULT CURRENT_TIMESTAMP, $columnCreatedAt DATETIME DEFAULT CURRENT_TIMESTAMP,

View file

@ -18,7 +18,7 @@ class DbSignalSessionStore extends CvModelBase {
static String getCreateTableString() { static String getCreateTableString() {
return """ return """
CREATE TABLE $tableName ( CREATE TABLE IF NOT EXISTS $tableName (
$columnDeviceId INTEGER NOT NULL, $columnDeviceId INTEGER NOT NULL,
$columnName TEXT NOT NULL, $columnName TEXT NOT NULL,
$columnSessionRecord BLOB NOT NULL, $columnSessionRecord BLOB NOT NULL,

View file

@ -7,10 +7,12 @@ import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.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/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/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/utils/api.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/utils/storage.dart';
// ignore: library_prefixes // ignore: library_prefixes
@ -167,23 +169,47 @@ class ApiProvider {
Uint8List name = username.value.userdata.username; Uint8List name = username.value.userdata.username;
DbContacts.insertNewContact( DbContacts.insertNewContact(
utf8.decode(name), fromUserId.toInt(), true); utf8.decode(name), fromUserId.toInt(), true);
updateNotifier();
} }
break; break;
case MessageKind.rejectRequest: case MessageKind.rejectRequest:
DbContacts.deleteUser(fromUserId.toInt()); DbContacts.deleteUser(fromUserId.toInt());
updateNotifier();
break; break;
case MessageKind.acceptRequest: case MessageKind.acceptRequest:
DbContacts.acceptUser(fromUserId.toInt()); DbContacts.acceptUser(fromUserId.toInt());
updateNotifier();
break; break;
case MessageKind.image: case MessageKind.ack:
log.info("Got image: ${message.content}"); DbMessages.acknowledgeMessage(
fromUserId.toInt(), message.messageId!);
break;
default: default:
if (message.kind != MessageKind.textMessage &&
message.kind != MessageKind.video &&
message.kind != MessageKind.image) {
log.shout("Got unknown MessageKind $message"); log.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);
} }
} }
}
}
updateNotifier();
var ok = client.Response_Ok()..none = true; var ok = client.Response_Ok()..none = true;
response = client.Response()..ok = ok; response = client.Response()..ok = ok;
} else { } else {
@ -373,16 +399,7 @@ class ApiProvider {
return _asResult(resp); return _asResult(resp);
} }
Future<List<int>?> uploadData(Uint8List data) async { Future<List<int>?> uploadData(List<int> uploadToken, Uint8List data) async {
Result res = await getUploadToken(data.length);
if (res.isError || !res.value.hasUploadtoken()) {
Logger("api.dart").shout("Error getting upload token!");
return null;
}
List<int> uploadToken = res.value.uploadtoken;
log.info("Got token: $uploadToken");
log.shout("fragmentate the data"); log.shout("fragmentate the data");
var get = ApplicationData_UploadData() var get = ApplicationData_UploadData()

View file

@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:math'; import 'dart:math';
import 'package:twonly/src/model/contacts_model.dart'; import 'package:twonly/src/model/contacts_model.dart';
import 'package:twonly/src/model/identity_key_store_model.dart'; import 'package:twonly/src/model/identity_key_store_model.dart';
import 'package:twonly/src/model/messages_model.dart';
import 'package:twonly/src/model/model_constants.dart'; import 'package:twonly/src/model/model_constants.dart';
import 'package:twonly/src/model/pre_key_model.dart'; import 'package:twonly/src/model/pre_key_model.dart';
import 'package:twonly/src/model/sender_key_store_model.dart'; import 'package:twonly/src/model/sender_key_store_model.dart';
@ -53,21 +54,12 @@ class DbProvider {
} }
Future _createDb(Database db) async { Future _createDb(Database db) async {
await db.execute('DROP TABLE If EXISTS ${DbSignalSessionStore.tableName}');
await db.execute(DbSignalSessionStore.getCreateTableString()); await db.execute(DbSignalSessionStore.getCreateTableString());
await db.execute('DROP TABLE If EXISTS ${DbSignalPreKeyStore.tableName}');
await db.execute(DbSignalPreKeyStore.getCreateTableString()); await db.execute(DbSignalPreKeyStore.getCreateTableString());
await db
.execute('DROP TABLE If EXISTS ${DbSignalSenderKeyStore.tableName}');
await db.execute(DbSignalSenderKeyStore.getCreateTableString()); await db.execute(DbSignalSenderKeyStore.getCreateTableString());
await db
.execute('DROP TABLE If EXISTS ${DbSignalIdentityKeyStore.tableName}');
await db.execute(DbSignalIdentityKeyStore.getCreateTableString()); await db.execute(DbSignalIdentityKeyStore.getCreateTableString());
await db.execute('DROP TABLE If EXISTS ${DbContacts.tableName}');
await db.execute(DbContacts.getCreateTableString()); await db.execute(DbContacts.getCreateTableString());
await db.execute(DbMessages.getCreateTableString());
} }
Future open() async { Future open() async {

View file

@ -1,5 +1,9 @@
import 'dart:collection';
import 'package:fixnum/fixnum.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.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';
/// Mix-in [DiagnosticableTreeMixin] to have access to [debugFillProperties] for the devtool /// Mix-in [DiagnosticableTreeMixin] to have access to [debugFillProperties] for the devtool
// ignore: prefer_mixin // ignore: prefer_mixin
@ -7,14 +11,19 @@ class NotifyProvider with ChangeNotifier, DiagnosticableTreeMixin {
// The page index of the HomeView widget // The page index of the HomeView widget
int _activePageIdx = 0; int _activePageIdx = 0;
int _newContactRequests = 0;
List<Contact> _allContacts = []; List<Contact> _allContacts = [];
final Map<int, DbMessage> _lastMessagesGroupedByUser = <int, DbMessage>{};
List<Contact> _sendingCurrentlyTo = []; final List<Int64> _sendingCurrentlyTo = [];
int get newContactRequests => _newContactRequests; int get newContactRequests => _allContacts
.where((contact) => !contact.accepted && contact.requested)
.length;
List<Contact> get allContacts => _allContacts; List<Contact> get allContacts => _allContacts;
List<Contact> get sendingCurrentlyTo => _sendingCurrentlyTo; Map<int, DbMessage> get lastMessagesGroupedByUser =>
_lastMessagesGroupedByUser;
HashSet<Int64> get sendingCurrentlyTo =>
HashSet<Int64>.from(_sendingCurrentlyTo);
int get activePageIdx => _activePageIdx; int get activePageIdx => _activePageIdx;
@ -23,18 +32,29 @@ class NotifyProvider with ChangeNotifier, DiagnosticableTreeMixin {
notifyListeners(); notifyListeners();
} }
void addSendingTo(List<Contact> users) { void addSendingTo(Int64 user) {
_sendingCurrentlyTo.addAll(users); _sendingCurrentlyTo.add(user);
notifyListeners();
}
// removes the first occurrence of the user
void removeSendingTo(Int64 user) {
int index = _sendingCurrentlyTo.indexOf(user);
if (index != -1) {
_sendingCurrentlyTo.removeAt(index);
}
notifyListeners(); notifyListeners();
} }
void update() async { void update() async {
_allContacts = await DbContacts.getUsers(); _allContacts = await DbContacts.getUsers();
for (Contact contact in _allContacts) {
_newContactRequests = _allContacts DbMessage? last = await DbMessages.getLastMessagesForPreviewForUser(
.where((contact) => !contact.accepted && contact.requested) contact.userId.toInt());
.length; if (last != null) {
print(_newContactRequests); _lastMessagesGroupedByUser[last.otherUserId] = last;
}
}
notifyListeners(); notifyListeners();
} }

View file

@ -6,8 +6,10 @@ import 'package:flutter/material.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.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/proto/api/error.pb.dart'; import 'package:twonly/src/proto/api/error.pb.dart';
import 'package:twonly/src/providers/api_provider.dart'; import 'package:twonly/src/providers/api_provider.dart';
import 'package:twonly/src/providers/notify_provider.dart'; import 'package:twonly/src/providers/notify_provider.dart';
@ -25,7 +27,7 @@ Future<bool> addNewContact(String username) async {
if (!added) { if (!added) {
print("RETURN FALSE HIER!!!"); print("RETURN FALSE HIER!!!");
// return false; return false;
} }
if (await SignalHelper.addNewContact(res.value.userdata)) { if (await SignalHelper.addNewContact(res.value.userdata)) {
@ -59,8 +61,11 @@ Future<Result> encryptAndSendMessage(Int64 userId, Message msg) async {
Result resp = await apiProvider.sendTextMessage(userId, bytes); Result resp = await apiProvider.sendTextMessage(userId, bytes);
if (resp.isError) {
// TODO:
Logger("encryptAndSendMessage") Logger("encryptAndSendMessage")
.shout("handle errors here and store them in the database"); .shout("handle errors here and store them in the database");
}
return resp; return resp;
} }
@ -94,12 +99,49 @@ Future<Result> createNewUser(String username, String inviteCode) async {
return res; return res;
} }
Future sendImageToSingleTarget(
BuildContext context, Int64 target, Uint8List encryptBytes) async {
Result res = await apiProvider.getUploadToken(encryptBytes.length);
if (res.isError || !res.value.hasUploadtoken()) {
Logger("api.dart").shout("Error getting upload token!");
return null;
}
List<int> uploadToken = res.value.uploadtoken;
Logger("sendImageToSingleTarget").info("Got token: $uploadToken");
MessageContent content =
MessageContent(text: null, downloadToken: uploadToken);
int? messageId = await DbMessages.insertMyMessage(
target.toInt(), MessageKind.image, jsonEncode(content.toJson()));
if (messageId == null) return;
updateNotifyProvider();
List<int>? imageToken =
await apiProvider.uploadData(uploadToken, encryptBytes);
if (imageToken == null) {
Logger("api.dart").shout("handle error uploading like saving...");
return;
}
print("TODO: insert into DB and then create this MESSAGE");
Message msg = Message(
kind: MessageKind.image,
messageId: messageId,
content: content,
timestamp: DateTime.now(),
);
await encryptAndSendMessage(target, msg);
removeSendingTo(target);
}
Future sendImage( Future sendImage(
BuildContext context, List<Contact> users, String imagePath) async { BuildContext context, List<Contact> users, String imagePath) async {
// 1. set notifier provider // 1. set notifier provider
context.read<NotifyProvider>().addSendingTo(users);
File imageFile = File(imagePath); File imageFile = File(imagePath);
Uint8List? imageBytes = await getCompressedImage(imageFile); Uint8List? imageBytes = await getCompressedImage(imageFile);
@ -116,20 +158,12 @@ Future sendImage(
Logger("api.dart").shout("Error encrypting image!"); Logger("api.dart").shout("Error encrypting image!");
continue; continue;
} }
sendImageToSingleTarget(context, target, encryptedImage);
List<int>? imageToken = await apiProvider.uploadData(encryptedImage);
if (imageToken == null) {
Logger("api.dart").shout("handle error uploading like saving...");
continue;
}
Message msg = Message(
kind: MessageKind.image,
content: ImageContent(imageToken),
timestamp: DateTime.timestamp(),
);
print("Send image to $target");
encryptAndSendMessage(target, msg);
} }
} }
Future tryDownloadMedia(List<int> imageToken, {bool force = false}) async {
print("check if free network connection");
print("Downloading: " + imageToken.toString());
}

View file

@ -218,8 +218,8 @@ Future<Uint8List?> encryptMessage(Message msg, Int64 target) async {
SessionCipher session = SessionCipher.fromStore( SessionCipher session = SessionCipher.fromStore(
signalStore, SignalProtocolAddress(target.toString(), defaultDeviceId)); signalStore, SignalProtocolAddress(target.toString(), defaultDeviceId));
final ciphertext = await session final ciphertext = await session.encrypt(
.encrypt(Uint8List.fromList(gzip.encode(utf8.encode(msg.toJson())))); Uint8List.fromList(gzip.encode(utf8.encode(jsonEncode(msg.toJson())))));
var b = BytesBuilder(); var b = BytesBuilder();
b.add(ciphertext.serialize()); b.add(ciphertext.serialize());
@ -256,7 +256,8 @@ Future<Message?> getDecryptedText(Int64 source, Uint8List msg) async {
} else { } else {
return null; return null;
} }
Message dectext = Message.fromJson(utf8.decode(gzip.decode(plaintext))); Message dectext =
Message.fromJson(jsonDecode(utf8.decode(gzip.decode(plaintext))));
return dectext; return dectext;
} catch (e) { } catch (e) {
Logger("utils/signal").shout(e.toString()); Logger("utils/signal").shout(e.toString());

View file

@ -1,9 +1,13 @@
import 'dart:collection';
import 'package:fixnum/fixnum.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:twonly/src/components/initialsavatar.dart'; import 'package:twonly/src/components/initialsavatar.dart';
import 'package:twonly/src/components/message_send_state_icon.dart'; import 'package:twonly/src/components/message_send_state_icon.dart';
import 'package:twonly/src/components/notification_badge.dart'; import 'package:twonly/src/components/notification_badge.dart';
import 'package:twonly/src/components/user_context_menu.dart'; import 'package:twonly/src/components/user_context_menu.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/notify_provider.dart'; import 'package:twonly/src/providers/notify_provider.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/chat_item_details_view.dart'; import 'package:twonly/src/views/chat_item_details_view.dart';
@ -35,40 +39,19 @@ class ChatListView extends StatefulWidget {
} }
class _ChatListViewState extends State<ChatListView> { class _ChatListViewState extends State<ChatListView> {
int _secondsSinceOpen = 0;
late Timer _timer;
List<Contact> _activeUsers = [];
@override
void initState() {
super.initState();
_startTimer();
_loadActiveUsers();
}
Future _loadActiveUsers() async {
_activeUsers = context.read<NotifyProvider>().allContacts;
}
void _startTimer() {
_timer = Timer.periodic(Duration(seconds: 1), (timer) {
setState(() {
_secondsSinceOpen++;
});
});
}
@override
void dispose() {
_timer.cancel(); // Cancel the timer when the widget is disposed
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
List<Contact> sendingCurrentlyTo = Map<int, DbMessage> lastMessages =
context.watch<NotifyProvider>().lastMessagesGroupedByUser;
HashSet<Int64> sendingCurrentlyTo =
context.watch<NotifyProvider>().sendingCurrentlyTo; context.watch<NotifyProvider>().sendingCurrentlyTo;
List<Contact> activeUsers = context
.read<NotifyProvider>()
.allContacts
.where((x) => lastMessages.containsKey(x.userId.toInt()))
.toList();
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(AppLocalizations.of(context)!.chatsTitle), title: Text(AppLocalizations.of(context)!.chatsTitle),
@ -91,39 +74,44 @@ class _ChatListViewState extends State<ChatListView> {
), ),
body: Column( body: Column(
children: [ children: [
if (sendingCurrentlyTo.isNotEmpty) // if (sendingCurrentlyTo.isNotEmpty)
Container( // Container(
padding: EdgeInsets.symmetric(vertical: 10, horizontal: 10), // padding: EdgeInsets.symmetric(vertical: 10, horizontal: 10),
child: ListTile( // child: ListTile(
leading: Stack( // leading: Stack(
// child: Stack( // // child: Stack(
alignment: Alignment.center, // alignment: Alignment.center,
children: [ // children: [
CircularProgressIndicator( // CircularProgressIndicator(
strokeWidth: 1, // strokeWidth: 1,
),
Icon(
Icons.send, // Replace with your desired icon
color: Theme.of(context).colorScheme.primary,
size: 20, // Adjust the size as needed
),
],
// ), // ),
), // Icon(
title: Text(sendingCurrentlyTo // Icons.send, // Replace with your desired icon
.map((e) => e.displayName) // color: Theme.of(context).colorScheme.primary,
.toList() // size: 20, // Adjust the size as needed
.join(", ")), // ),
), // ],
), // // ),
// ),
// title: Text(sendingCurrentlyTo
// .map((e) => _activeUsers
// .where((c) => c.userId == e)
// .map((c) => c.displayName))
// .toList()
// .join(", ")),
// ),
// ), //
Expanded( Expanded(
child: ListView.builder( child: ListView.builder(
restorationId: 'chat_list_view', restorationId: 'chat_list_view',
itemCount: _activeUsers.length, itemCount: activeUsers.length,
itemBuilder: (BuildContext context, int index) { itemBuilder: (BuildContext context, int index) {
final user = _activeUsers[index]; final user = activeUsers[index];
return UserListItem( return UserListItem(
user: user, secondsSinceOpen: _secondsSinceOpen); user: user,
lastMessage: lastMessages[user.userId.toInt()]!,
isSending: sendingCurrentlyTo.contains(user.userId),
);
}, },
), ),
) )
@ -134,12 +122,14 @@ class _ChatListViewState extends State<ChatListView> {
class UserListItem extends StatefulWidget { class UserListItem extends StatefulWidget {
final Contact user; final Contact user;
final int secondsSinceOpen; final DbMessage lastMessage;
final bool isSending;
const UserListItem({ const UserListItem({
super.key, super.key,
required this.user, required this.user,
required this.secondsSinceOpen, required this.isSending,
required this.lastMessage,
}); });
@override @override
@ -153,7 +143,7 @@ class _UserListItem extends State<UserListItem> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_loadAsync(); //_loadAsync();
} }
Future _loadAsync() async { Future _loadAsync() async {
@ -164,19 +154,44 @@ class _UserListItem extends State<UserListItem> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
MessageSendState state;
// int lastMessageInSeconds = widget.lastMessage.sendOrReceivedAt;
//print(widget.lastMessage.sendOrReceivedAt);
int lastMessageInSeconds = DateTime.now()
.difference(widget.lastMessage.sendOrReceivedAt)
.inSeconds;
if (widget.isSending) {
state = MessageSendState.sending;
} else {
if (widget.lastMessage.messageOtherId == null) {
// message send
if (widget.lastMessage.messageOpenedAt == null) {
state = MessageSendState.send;
} else {
state = MessageSendState.sendOpened;
}
} else {
// message received
if (widget.lastMessage.messageOpenedAt == null) {
state = MessageSendState.received;
} else {
state = MessageSendState.receivedOpened;
}
}
}
return UserContextMenu( return UserContextMenu(
user: widget.user, user: widget.user,
child: ListTile( child: ListTile(
title: Text(widget.user.displayName), title: Text(widget.user.displayName),
subtitle: Row( subtitle: Row(
children: [ children: [
// MessageSendStateIcon( MessageSendStateIcon(state),
// state: widget.user.state,
// ),
Text(""), Text(""),
const SizedBox(width: 5), const SizedBox(width: 5),
Text( Text(
formatDuration(lastMessageInSeconds + widget.secondsSinceOpen), formatDuration(lastMessageInSeconds),
style: TextStyle(fontSize: 12), style: TextStyle(fontSize: 12),
), ),
if (flames > 0) if (flames > 0)

View file

@ -3,6 +3,7 @@ import 'package:fixnum/fixnum.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:twonly/src/app.dart';
import 'package:twonly/src/components/best_friends_selector.dart'; import 'package:twonly/src/components/best_friends_selector.dart';
import 'package:twonly/src/components/headline.dart'; import 'package:twonly/src/components/headline.dart';
import 'package:twonly/src/components/initialsavatar.dart'; import 'package:twonly/src/components/initialsavatar.dart';
@ -111,6 +112,10 @@ class _ShareImageView extends State<ShareImageView> {
FilledButton.icon( FilledButton.icon(
icon: Icon(Icons.send), icon: Icon(Icons.send),
onPressed: () async { onPressed: () async {
for (Int64 a in _selectedUserIds) {
addSendingTo(a);
}
sendImage( sendImage(
context, context,
_users _users