mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-01-15 14:48:41 +00:00
start with chat view
This commit is contained in:
parent
2ab0ad3daa
commit
2f3af42771
17 changed files with 440 additions and 226 deletions
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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);
|
|
||||||
// }
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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());
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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());
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue