multiple bug fixes

This commit is contained in:
otsmr 2025-03-16 01:11:40 +01:00
parent c6c625df59
commit c008174070
15 changed files with 202 additions and 867 deletions

View file

@ -1,258 +0,0 @@
import 'package:cv/cv.dart';
import 'package:fixnum/fixnum.dart';
import 'package:logging/logging.dart';
import 'package:sqflite_sqlcipher/sqflite.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/app.dart';
import 'package:twonly/src/utils/misc.dart';
class Contact {
Contact(
{required this.userId,
required this.displayName,
required this.accepted,
required this.blocked,
required this.verified,
required this.totalMediaCounter,
required this.requested});
final Int64 userId;
final String displayName;
final bool accepted;
final bool requested;
final bool blocked;
final bool verified;
final int totalMediaCounter;
}
class DbContacts extends CvModelBase {
static const tableName = "contacts";
static const columnUserId = "contact_user_id";
final userId = CvField<int>(columnUserId);
static const columnDisplayName = "display_name";
final displayName = CvField<String>(columnDisplayName);
static const columnAccepted = "accepted";
final accepted = CvField<int>(columnAccepted);
static const columnRequested = "requested";
final requested = CvField<int>(columnRequested);
static const columnBlocked = "blocked";
final blocked = CvField<int>(columnBlocked);
static const columnVerified = "verified";
final verified = CvField<int>(columnVerified);
static const columnTotalMediaCounter = "total_media_counter";
final totalMediaCounter = CvField<int>(columnTotalMediaCounter);
static const columnCreatedAt = "created_at";
final createdAt = CvField<DateTime>(columnCreatedAt);
static Future setupDatabaseTable(Database db) async {
String createTableString = """
CREATE TABLE IF NOT EXISTS $tableName (
$columnUserId INTEGER NOT NULL PRIMARY KEY,
$columnDisplayName TEXT,
$columnAccepted INT NOT NULL DEFAULT 0,
$columnRequested INT NOT NULL DEFAULT 0,
$columnBlocked INT NOT NULL DEFAULT 0,
$columnVerified INTEGER NOT NULL DEFAULT 0,
$columnTotalMediaCounter INT NOT NULL DEFAULT 0,
$columnCreatedAt DATETIME DEFAULT CURRENT_TIMESTAMP
)
""";
await db.execute(createTableString);
if (!await columnExists(db, tableName, columnVerified)) {
String alterTableString = """
ALTER TABLE $tableName
ADD COLUMN $columnVerified INTEGER NOT NULL DEFAULT 0
""";
await db.execute(alterTableString);
}
}
@override
List<CvField> get fields =>
[userId, displayName, accepted, requested, blocked, createdAt];
static Future<List<Contact>> getActiveUsers() async {
return (await _getAllUsers())
.where((u) => u.accepted && !u.blocked)
.toList();
}
static Future<List<Contact>> getBlockedUsers() async {
return (await _getAllUsers()).where((u) => u.blocked).toList();
}
static Future<List<Contact>> getUsers() async {
return (await _getAllUsers()).where((u) => !u.blocked).toList();
}
static Future<List<Contact>> getAllUsers() async {
return await _getAllUsers();
}
static Future updateTotalMediaCounter(
int userId,
) async {
List<Map<String, dynamic>> result = await dbProvider.db!.query(
tableName,
columns: [columnTotalMediaCounter],
where: '$columnUserId = ?',
whereArgs: [userId],
);
if (result.isNotEmpty) {
int totalMediaCounter = result.first.cast()[columnTotalMediaCounter];
_updateTotalMediaCounter(userId, totalMediaCounter + 1);
globalCallBackOnContactChange();
}
}
static List<Contact> _parseContacts(List<dynamic> users) {
List<Contact> parsedUsers = [];
for (int i = 0; i < users.length; i++) {
try {
int userId = users.cast()[i][columnUserId];
parsedUsers.add(
Contact(
userId: Int64(userId),
totalMediaCounter: users.cast()[i][columnTotalMediaCounter],
displayName: users.cast()[i][columnDisplayName],
accepted: users[i][columnAccepted] == 1,
blocked: users[i][columnBlocked] == 1,
verified: users[i][columnVerified] == 1,
requested: users[i][columnRequested] == 1,
),
);
} catch (e) {
Logger("contacts_model/parse_single_user").shout("$e");
return [];
}
}
return parsedUsers;
}
static Future<Contact?> getUserById(int userId) async {
try {
var user = await dbProvider.db!.query(tableName,
columns: [
columnUserId,
columnDisplayName,
columnAccepted,
columnRequested,
columnBlocked,
columnVerified,
columnTotalMediaCounter,
columnCreatedAt
],
where: "$columnUserId = ?",
whereArgs: [userId]);
if (user.isEmpty) return null;
return _parseContacts(user)[0];
} catch (e) {
Logger("contacts_model/getUserById").shout("$e");
return null;
}
}
static Future<List<Contact>> _getAllUsers() async {
try {
var users = await dbProvider.db!.query(tableName, columns: [
columnUserId,
columnDisplayName,
columnAccepted,
columnRequested,
columnBlocked,
columnVerified,
columnTotalMediaCounter,
columnCreatedAt
]);
if (users.isEmpty) return [];
return _parseContacts(users);
} catch (e) {
Logger("contacts_model/getUsers").shout("$e");
return [];
}
}
static Future _update(int userId, Map<String, dynamic> updates,
{bool notifyFlutter = true}) async {
await dbProvider.db!.update(
tableName,
updates,
where: "$columnUserId = ?",
whereArgs: [userId],
);
if (notifyFlutter) {
globalCallBackOnContactChange();
}
}
static Future changeDisplayName(int userId, String newDisplayName) async {
if (newDisplayName == "") return;
Map<String, dynamic> updates = {
columnDisplayName: newDisplayName,
};
await _update(userId, updates);
}
static Future _updateTotalMediaCounter(
int userId, int totalMediaCounter) async {
Map<String, dynamic> updates = {columnTotalMediaCounter: totalMediaCounter};
await _update(userId, updates, notifyFlutter: false);
}
static Future blockUser(int userId, {bool unblock = false}) async {
Map<String, dynamic> updates = {
columnBlocked: unblock ? 0 : 1,
};
await _update(userId, updates);
}
static Future acceptUser(int userId) async {
Map<String, dynamic> updates = {
columnAccepted: 1,
columnRequested: 0,
};
await _update(userId, updates);
}
static Future updateVerificationStatus(int userId, bool status) async {
Map<String, dynamic> updates = {
columnVerified: status ? 1 : 0,
};
await _update(userId, updates);
}
static Future deleteUser(int userId) async {
await dbProvider.db!.delete(
tableName,
where: "$columnUserId = ?",
whereArgs: [userId],
);
globalCallBackOnContactChange();
}
static Future<bool> insertNewContact(
String username, int userId, bool requested) async {
try {
int a = requested ? 1 : 0;
await dbProvider.db!.insert(DbContacts.tableName, {
DbContacts.columnDisplayName: username,
DbContacts.columnUserId: userId,
DbContacts.columnRequested: a
});
globalCallBackOnContactChange();
return true;
} catch (e) {
Logger("contacts_model/getUsers").shout("$e");
return false;
}
}
}

View file

@ -1,444 +0,0 @@
import 'dart:convert';
import 'package:cv/cv.dart';
import 'package:logging/logging.dart';
import 'package:sqflite_sqlcipher/sqflite.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/app.dart';
import 'package:twonly/src/components/message_send_state_icon.dart';
import 'package:twonly/src/model/json/message.dart';
import 'package:twonly/src/providers/api/api.dart';
import 'package:twonly/src/utils/misc.dart';
class DbMessage {
DbMessage({
required this.messageId,
required this.messageOtherId,
required this.otherUserId,
required this.messageContent,
required this.messageOpenedAt,
required this.messageAcknowledgeByUser,
required this.isDownloaded,
required this.messageAcknowledgeByServer,
required this.sendAt,
});
int messageId;
// is this null then the message was sent from the user itself
int? messageOtherId;
int otherUserId;
MessageContent messageContent;
DateTime? messageOpenedAt;
bool messageAcknowledgeByUser;
bool isDownloaded;
bool messageAcknowledgeByServer;
DateTime sendAt;
bool containsOtherMedia() {
if (messageOtherId == null) return false;
return isMedia();
}
bool get messageReceived => messageOtherId != null;
bool isRealTwonly() {
final content = messageContent;
if (content is MediaMessageContent) {
if (content.isRealTwonly) {
return true;
}
}
return false;
}
bool isMedia() {
return messageContent is MediaMessageContent;
}
MessageSendState getSendState() {
MessageSendState state;
if (!messageAcknowledgeByServer) {
state = MessageSendState.sending;
} else {
if (messageOtherId == null) {
// message send
if (messageOpenedAt == null) {
state = MessageSendState.send;
} else {
state = MessageSendState.sendOpened;
}
} else {
// message received
if (messageOpenedAt == null) {
state = MessageSendState.received;
} else {
state = MessageSendState.receivedOpened;
}
}
}
return state;
}
}
class DbMessages extends CvModelBase {
static const tableName = "messages";
static const columnMessageId = "id";
final messageId = CvField<int>(columnMessageId);
static const columnMessageOtherId = "message_other_id";
final messageOtherId = CvField<int?>(columnMessageOtherId);
static const columnOtherUserId = "other_user_id";
final otherUserId = CvField<int>(columnOtherUserId);
static const columnMessageKind = "message_kind";
final messageKind = 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 columnMessageAcknowledgeByUser = "message_acknowledged_by_user";
final messageAcknowledgeByUser = CvField<int>(columnMessageAcknowledgeByUser);
static const columnMessageAcknowledgeByServer =
"message_acknowledged_by_server";
final messageAcknowledgeByServer =
CvField<int>(columnMessageAcknowledgeByServer);
static const columnSendAt = "message_send_or_received_at";
final sendAt = CvField<DateTime>(columnSendAt);
static const columnUpdatedAt = "updated_at";
final updatedAt = CvField<DateTime>(columnUpdatedAt);
static Future setupDatabaseTable(Database db) async {
String createTableString = """
CREATE TABLE IF NOT EXISTS $tableName (
$columnMessageId INTEGER NOT NULL PRIMARY KEY,
$columnMessageOtherId INTEGER DEFAULT NULL,
$columnOtherUserId INTEGER NOT NULL,
$columnMessageKind INTEGER NOT NULL,
$columnMessageAcknowledgeByUser INTEGER NOT NULL DEFAULT 0,
$columnMessageAcknowledgeByServer INTEGER NOT NULL DEFAULT 0,
$columnMessageContentJson TEXT NOT NULL,
$columnMessageOpenedAt DATETIME DEFAULT NULL,
$columnSendAt DATETIME DEFAULT CURRENT_TIMESTAMP,
$columnUpdatedAt DATETIME DEFAULT CURRENT_TIMESTAMP
)
""";
await db.execute(createTableString);
}
static Future<List<(DateTime, int?)>> getMessageDates(int otherUserId) async {
final List<Map<String, dynamic>> maps = await dbProvider.db!.rawQuery('''
SELECT $columnSendAt, $columnMessageOtherId
FROM $tableName
WHERE $columnOtherUserId = ? AND ($columnMessageKind = ? OR $columnMessageKind = ?)
ORDER BY $columnSendAt DESC;
''', [otherUserId, MessageKind.image.index, MessageKind.video.index]);
try {
return List.generate(maps.length, (i) {
return (
DateTime.tryParse(maps[i][columnSendAt])!,
maps[i][columnMessageOtherId]
);
});
} catch (e) {
Logger("error parsing datetime: $e");
return [];
}
}
static Future<int?> deleteMessageById(int messageId) async {
await dbProvider.db!.delete(
tableName,
where: '$columnMessageId = ?',
whereArgs: [messageId],
);
int? fromUserId = await getFromUserIdByMessageId(messageId);
if (fromUserId != null) {
globalCallBackOnMessageChange(fromUserId, messageId);
}
return fromUserId;
}
static Future<int?> getFromUserIdByMessageId(int messageId) async {
List<Map<String, dynamic>> result = await dbProvider.db!.query(
tableName,
columns: [columnOtherUserId],
where: '$columnMessageId = ?',
whereArgs: [messageId],
);
if (result.isNotEmpty) {
return result.first[columnOtherUserId] as int?;
}
return null;
}
static Future<int?> insertMyMessage(int userIdFrom, MessageKind kind,
MessageContent content, DateTime messageSendAt) async {
try {
int messageId = await dbProvider.db!.insert(tableName, {
columnMessageKind: kind.index,
columnMessageContentJson: jsonEncode(content.toJson()),
columnOtherUserId: userIdFrom,
columnSendAt: messageSendAt.toIso8601String()
});
globalCallBackOnMessageChange(userIdFrom, messageId);
return messageId;
} catch (e) {
Logger("messsage_model/insertMyMessage").shout("$e");
return null;
}
}
static Future<int?> insertOtherMessage(int userIdFrom, MessageKind kind,
int messageOtherId, String jsonContent, DateTime messageSendAt) async {
try {
int messageId = await dbProvider.db!.insert(tableName, {
columnMessageOtherId: messageOtherId,
columnMessageKind: kind.index,
columnMessageContentJson: jsonContent,
columnMessageAcknowledgeByServer: 1,
columnMessageAcknowledgeByUser:
0, // ack in case of sending corresponds to the opened flag
columnOtherUserId: userIdFrom,
columnSendAt: messageSendAt.toIso8601String()
});
globalCallBackOnMessageChange(userIdFrom, messageId);
return messageId;
} catch (e) {
Logger("messsage_model/insertOtherMessage").shout("$e");
return null;
}
}
static Future<List<DbMessage>> getAllMessagesForUserWithHigherMessageId(
int otherUserId, int lastMessageId) async {
var rows = await dbProvider.db!.query(
tableName,
where: "$columnOtherUserId = ? AND $columnMessageId > ?",
whereArgs: [otherUserId, lastMessageId],
orderBy: "$columnUpdatedAt DESC",
);
List<DbMessage> messages = await convertToDbMessage(rows);
return messages;
}
static Future<List<DbMessage>> getAllMessagesForUser(int otherUserId) async {
var rows = await dbProvider.db!.query(
tableName,
where: "$columnOtherUserId = ?",
whereArgs: [otherUserId],
orderBy: "$columnSendAt DESC",
);
List<DbMessage> messages = await convertToDbMessage(rows);
return messages;
}
static Future<DbMessage?> getMessageById(int messageId) async {
var rows = await dbProvider.db!.query(tableName,
where: "$columnMessageId = ?", whereArgs: [messageId]);
List<DbMessage> messages = await convertToDbMessage(rows);
return messages.firstOrNull;
}
static Future<List<DbMessage>> getAllMessagesForRetransmitting() async {
var rows = await dbProvider.db!.query(
tableName,
where: "$columnMessageAcknowledgeByServer = 0",
);
List<DbMessage> messages = await convertToDbMessage(rows);
return messages;
}
static Future<DbMessage?> getLastMessagesForPreviewForUser(
int otherUserId) async {
var rows = await dbProvider.db!.query(
tableName,
where: "$columnOtherUserId = ?",
whereArgs: [otherUserId],
orderBy: "$columnUpdatedAt DESC",
limit: 10,
);
List<DbMessage> messages = await convertToDbMessage(rows);
// check if you received a message which the user has not already opened
List<DbMessage> receivedByOther = messages
.where((c) => c.messageOtherId != null && c.messageOpenedAt == null)
.toList();
if (receivedByOther.isNotEmpty) {
return receivedByOther[receivedByOther.length - 1];
}
// check if there is a message which was not ack by the server
List<DbMessage> notAckByServer =
messages.where((c) => !c.messageAcknowledgeByServer).toList();
if (notAckByServer.isNotEmpty) return notAckByServer[0];
// check if there is a message which was not ack by the user
List<DbMessage> notAckByUser =
messages.where((c) => !c.messageAcknowledgeByUser).toList();
if (notAckByUser.isNotEmpty) return notAckByUser[0];
if (messages.isEmpty) return null;
return messages[0];
}
static Future _updateByMessageId(int messageId, Map<String, dynamic> data,
{bool notifyFlutterState = true}) async {
await dbProvider.db!.update(
tableName,
data,
where: "$columnMessageId = ?",
whereArgs: [messageId],
);
if (notifyFlutterState) {
int? fromUserId = await getFromUserIdByMessageId(messageId);
if (fromUserId != null) {
globalCallBackOnMessageChange(fromUserId, messageId);
}
}
}
static Future _updateByOtherMessageId(
int fromUserId, int messageId, Map<String, dynamic> data) async {
await dbProvider.db!.update(
tableName,
data,
where: "$columnMessageOtherId = ?",
whereArgs: [messageId],
);
globalCallBackOnMessageChange(fromUserId, messageId);
}
// this ensures that the message id can be spoofed by another person
static Future _updateByMessageIdOther(
int fromUserId, int messageId, Map<String, dynamic> data) async {
await dbProvider.db!.update(
tableName,
data,
where: "$columnMessageId = ? AND $columnOtherUserId = ?",
whereArgs: [messageId, fromUserId],
);
globalCallBackOnMessageChange(fromUserId, messageId);
}
static Future userOpenedOtherMessage(
int fromUserId, int otherMessageId) async {
Map<String, dynamic> data = {
columnMessageOpenedAt: DateTime.now().toIso8601String(),
};
await _updateByOtherMessageId(fromUserId, otherMessageId, data);
}
static Future otherUserOpenedMyMessage(
int fromUserId, int messageId, DateTime openedAt) async {
Map<String, dynamic> data = {
columnMessageOpenedAt: openedAt.toIso8601String(),
};
await _updateByMessageIdOther(fromUserId, 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 = {
columnMessageAcknowledgeByUser: 1,
};
await dbProvider.db!.update(
tableName,
valuesToUpdate,
where: "$messageId = ? AND $columnOtherUserId = ?",
whereArgs: [messageId, fromUserId],
);
globalCallBackOnMessageChange(fromUserId, messageId);
}
@override
List<CvField> get fields =>
[messageId, messageKind, messageContentJson, messageOpenedAt, sendAt];
// TODO: The message meta is needed to maintain the flame. Delete if not.
// This function should calculate if this message is needed for the flame calculation and delete the message complete and not only
// the message content.
static Future deleteTextContent(
int messageId, TextMessageContent oldMessage) async {
oldMessage.text = "";
Map<String, dynamic> data = {
columnMessageContentJson: jsonEncode(oldMessage.toJson()),
};
await _updateByMessageId(messageId, data, notifyFlutterState: false);
}
static Future<List<DbMessage>> convertToDbMessage(
List<dynamic> fromDb) async {
try {
List<DbMessage> parsedUsers = [];
final box = await getMediaStorage();
for (int i = 0; i < fromDb.length; i++) {
dynamic messageOpenedAt = fromDb[i][columnMessageOpenedAt];
MessageContent content = MessageContent.fromJson(
jsonDecode(fromDb[i][columnMessageContentJson]));
var tmp = content;
if (messageOpenedAt != null) {
messageOpenedAt = DateTime.tryParse(fromDb[i][columnMessageOpenedAt]);
if (tmp is TextMessageContent && messageOpenedAt != null) {
if (calculateTimeDifference(DateTime.now(), messageOpenedAt)
.inHours >=
24) {
deleteTextContent(fromDb[i][columnMessageId], tmp);
}
}
}
int? messageOtherId = fromDb[i][columnMessageOtherId];
bool isDownloaded = true;
if (messageOtherId != null) {
if (content is MediaMessageContent) {
// when the media was send from the user itself the content is null
isDownloaded =
box.containsKey("${content.downloadToken}_downloaded");
}
}
parsedUsers.add(
DbMessage(
sendAt: DateTime.tryParse(fromDb[i][columnSendAt])!,
messageId: fromDb[i][columnMessageId],
messageOtherId: messageOtherId,
otherUserId: fromDb[i][columnOtherUserId],
messageContent: content,
isDownloaded: isDownloaded,
messageOpenedAt: messageOpenedAt,
messageAcknowledgeByUser:
fromDb[i][columnMessageAcknowledgeByUser] == 1,
messageAcknowledgeByServer:
fromDb[i][columnMessageAcknowledgeByServer] == 1,
),
);
}
return parsedUsers;
} catch (e) {
Logger("messages_model/convertToDbMessage").shout("$e");
return [];
}
}
}

View file

@ -20,7 +20,11 @@ MessageSendState messageSendStateFromMessage(Message msg) {
MessageSendState state; MessageSendState state;
if (!msg.acknowledgeByServer) { if (!msg.acknowledgeByServer) {
state = MessageSendState.sending; if (msg.messageOtherId == null) {
state = MessageSendState.sending;
} else {
state = MessageSendState.receiving;
}
} else { } else {
if (msg.messageOtherId == null) { if (msg.messageOtherId == null) {
// message send // message send
@ -66,15 +70,12 @@ class _MessageSendStateIconState extends State<MessageSendStateIcon> {
textMsg = msg; textMsg = msg;
} }
if (msg.kind == MessageKind.media) { if (msg.kind == MessageKind.media) {
MessageJson message = MediaMessageContent content =
MessageJson.fromJson(jsonDecode(msg.contentJson!)); MediaMessageContent.fromJson(jsonDecode(msg.contentJson!));
final content = message.content; if (content.isVideo) {
if (content is MediaMessageContent) { videoMsg = msg;
if (content.isVideo) { } else {
videoMsg = msg; imageMsg = msg;
} else {
imageMsg = msg;
}
} }
} }
} }
@ -110,9 +111,10 @@ class _MessageSendStateIconState extends State<MessageSendStateIcon> {
Widget icon = Placeholder(); Widget icon = Placeholder();
MessageSendState state = messageSendStateFromMessage(message); MessageSendState state = messageSendStateFromMessage(message);
MessageJson msg = MessageJson.fromJson(jsonDecode(message.contentJson!)); MessageContent? content = MessageContent.fromJson(
if (msg.content == null) continue; message.kind, jsonDecode(message.contentJson!));
Color color = getMessageColorFromType(msg.content!, twonlyColor); if (content == null) continue;
Color color = getMessageColorFromType(content, twonlyColor);
switch (state) { switch (state) {
case MessageSendState.receivedOpened: case MessageSendState.receivedOpened:
@ -139,16 +141,20 @@ class _MessageSendStateIconState extends State<MessageSendStateIcon> {
break; break;
} }
if (message.downloadState == DownloadState.pending) { if (message.kind == MessageKind.media) {
text = context.lang.messageSendState_TapToLoad; if (message.downloadState == DownloadState.pending) {
} text = context.lang.messageSendState_TapToLoad;
if (message.downloadState == DownloadState.downloaded) { }
text = context.lang.messageSendState_Loading; if (message.downloadState == DownloadState.downloaded) {
icon = getLoaderIcon(color); text = context.lang.messageSendState_Loading;
icon = getLoaderIcon(color);
}
} }
icons.add(icon); icons.add(icon);
} }
if (icons.isEmpty) return Container();
Widget icon = icons[0]; Widget icon = icons[0];
if (icons.length == 2) { if (icons.length == 2) {

View file

@ -29,23 +29,34 @@ class TwonlyDatabase extends _$TwonlyDatabase {
Stream<List<Message>> watchMessageNotOpened(int contactId) { Stream<List<Message>> watchMessageNotOpened(int contactId) {
return (select(messages) return (select(messages)
..where((t) => t.openedAt.isNull() & t.contactId.equals(contactId))) ..where((t) => t.openedAt.isNull() & t.contactId.equals(contactId))
..orderBy([(t) => OrderingTerm.desc(t.sendAt)]))
.watch(); .watch();
} }
Stream<Message?> watchLastMessage(int contactId) { Stream<List<Message>> watchLastMessage(int contactId) {
return (select(messages) return (select(messages)
..where((t) => t.contactId.equals(contactId)) ..where((t) => t.contactId.equals(contactId))
..orderBy([(t) => OrderingTerm.desc(t.sendAt)]) ..orderBy([(t) => OrderingTerm.desc(t.sendAt)])
..limit(1)) ..limit(1))
.watchSingleOrNull(); .watch();
} }
Stream<List<Message>> watchAllMessagesFrom(int contactId) { Stream<List<Message>> watchAllMessagesFrom(int contactId) {
return (select(messages)..where((t) => t.contactId.equals(contactId))) return (select(messages)
..where((t) => t.contactId.equals(contactId))
..orderBy([(t) => OrderingTerm.desc(t.sendAt)]))
.watch(); .watch();
} }
Future<List<Message>> getAllMessagesPendingDownloading() {
return (select(messages)
..where((t) =>
t.downloadState.equals(DownloadState.downloaded.index).not() &
t.kind.equals(MessageKind.media.name)))
.get();
}
Future<List<Message>> getAllMessagesForRetransmitting() { Future<List<Message>> getAllMessagesForRetransmitting() {
return (select(messages)..where((t) => t.acknowledgeByServer.equals(false))) return (select(messages)..where((t) => t.acknowledgeByServer.equals(false)))
.get(); .get();
@ -168,7 +179,9 @@ class TwonlyDatabase extends _$TwonlyDatabase {
Stream<int?> watchContactsRequested() { Stream<int?> watchContactsRequested() {
final count = contacts.requested.count(distinct: true); final count = contacts.requested.count(distinct: true);
final query = selectOnly(contacts)..where(contacts.requested.equals(true)); final query = selectOnly(contacts)
..where(contacts.requested.equals(true) &
contacts.accepted.equals(true).not());
query.addColumns([count]); query.addColumns([count]);
return query.map((row) => row.read(count)).watchSingle(); return query.map((row) => row.read(count)).watchSingle();
} }

View file

@ -756,7 +756,7 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> {
downloadState = GeneratedColumn<int>('download_state', aliasedName, false, downloadState = GeneratedColumn<int>('download_state', aliasedName, false,
type: DriftSqlType.int, type: DriftSqlType.int,
requiredDuringInsert: false, requiredDuringInsert: false,
defaultValue: Constant(DownloadState.pending.index)) defaultValue: Constant(DownloadState.downloaded.index))
.withConverter<DownloadState>($MessagesTable.$converterdownloadState); .withConverter<DownloadState>($MessagesTable.$converterdownloadState);
static const VerificationMeta _acknowledgeByServerMeta = static const VerificationMeta _acknowledgeByServerMeta =
const VerificationMeta('acknowledgeByServer'); const VerificationMeta('acknowledgeByServer');

View file

@ -19,7 +19,7 @@ class Messages extends Table {
BoolColumn get acknowledgeByUser => boolean().withDefault(Constant(false))(); BoolColumn get acknowledgeByUser => boolean().withDefault(Constant(false))();
IntColumn get downloadState => intEnum<DownloadState>() IntColumn get downloadState => intEnum<DownloadState>()
.withDefault(Constant(DownloadState.pending.index))(); .withDefault(Constant(DownloadState.downloaded.index))();
BoolColumn get acknowledgeByServer => BoolColumn get acknowledgeByServer =>
boolean().withDefault(Constant(false))(); boolean().withDefault(Constant(false))();

View file

@ -15,6 +15,25 @@ import 'package:twonly/src/utils/misc.dart';
// ignore: library_prefixes // ignore: library_prefixes
import 'package:twonly/src/utils/signal.dart' as SignalHelper; import 'package:twonly/src/utils/signal.dart' as SignalHelper;
Future tryDownloadAllMediaFiles() async {
if (!await isAllowedToDownload()) {
return;
}
List<Message> messages =
await twonlyDatabase.getAllMessagesPendingDownloading();
for (Message message in messages) {
MessageContent? content = MessageContent.fromJson(message.kind, jsonDecode(message.contentJson!));
if (content is MediaMessageContent) {
tryDownloadMedia(message.messageId, message.contactId, content.downloadToken);
}
}
}
Future tryTransmitMessages() async { Future tryTransmitMessages() async {
List<Message> retransmit = List<Message> retransmit =
await twonlyDatabase.getAllMessagesForRetransmitting(); await twonlyDatabase.getAllMessagesForRetransmitting();
@ -36,10 +55,10 @@ Future tryTransmitMessages() async {
); );
if (resp.isSuccess) { if (resp.isSuccess) {
await twonlyDatabase.updateMessageByMessageId( await twonlyDatabase.updateMessageByMessageId(
msgId, msgId,
MessagesCompanion(acknowledgeByServer: Value(true)) MessagesCompanion(acknowledgeByServer: Value(true))
); );
box.delete("retransmit-$msgId-textmessage"); box.delete("retransmit-$msgId-textmessage");
} else { } else {
@ -49,11 +68,9 @@ Future tryTransmitMessages() async {
Uint8List? encryptedMedia = await box.get("retransmit-$msgId-media"); Uint8List? encryptedMedia = await box.get("retransmit-$msgId-media");
if (encryptedMedia != null) { if (encryptedMedia != null) {
final content = MessageJson.fromJson(jsonDecode(retransmit[i].contentJson!)).content; MediaMessageContent content = MediaMessageContent.fromJson(jsonDecode(retransmit[i].contentJson!));
if (content is MediaMessageContent) {
uploadMediaFile(msgId, retransmit[i].contactId, encryptedMedia, uploadMediaFile(msgId, retransmit[i].contactId, encryptedMedia,
content.isRealTwonly, content.maxShowTime, retransmit[i].sendAt); content.isRealTwonly, content.maxShowTime, retransmit[i].sendAt);
}
} }
} }
} }

View file

@ -185,6 +185,7 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
kind: Value(message.kind), kind: Value(message.kind),
messageOtherId: Value(message.messageId), messageOtherId: Value(message.messageId),
contentJson: Value(content), contentJson: Value(content),
downloadState: Value(DownloadState.downloaded),
sendAt: Value(message.timestamp), sendAt: Value(message.timestamp),
); );

View file

@ -70,10 +70,10 @@ class ApiProvider {
Future onConnected() async { Future onConnected() async {
await authenticate(); await authenticate();
globalCallbackConnectionState(true); globalCallbackConnectionState(true);
// _reconnectionDelay = 5;
if (!globalIsAppInBackground) { if (!globalIsAppInBackground) {
tryTransmitMessages(); tryTransmitMessages();
tryDownloadAllMediaFiles();
} }
} }

View file

@ -1,5 +1,6 @@
import 'dart:io'; import 'dart:io';
import 'dart:math'; import 'dart:math';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_image_compress/flutter_image_compress.dart'; import 'package:flutter_image_compress/flutter_image_compress.dart';
@ -235,3 +236,13 @@ Future<bool> authenticateUser(String localizedReason,
} }
return false; return false;
} }
Future<bool> isAllowedToDownload() async {
final List<ConnectivityResult> connectivityResult =
await (Connectivity().checkConnectivity());
if (connectivityResult.contains(ConnectivityResult.mobile)) {
Logger("tryDownloadMedia").info("abort download over mobile connection");
return false;
}
return true;
}

View file

@ -1,5 +1,6 @@
import 'dart:convert'; import 'dart:convert';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:path_provider/path_provider.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/model/json/user_data.dart'; import 'package:twonly/src/model/json/user_data.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
@ -32,6 +33,12 @@ Future<bool> deleteLocalUserData() async {
final storage = getSecureStorage(); final storage = getSecureStorage();
var password = await storage.read(key: "sqflite_database_password"); var password = await storage.read(key: "sqflite_database_password");
await dbProvider.remove(); await dbProvider.remove();
final appDir = await getApplicationSupportDirectory();
if (appDir.existsSync()) {
appDir.deleteSync(recursive: true);
}
await storage.write(key: "sqflite_database_password", value: password); await storage.write(key: "sqflite_database_password", value: password);
await storage.deleteAll(); await storage.deleteAll();
return true; return true;

View file

@ -36,8 +36,9 @@ class ChatListEntry extends StatelessWidget {
bool isDownloading = false; bool isDownloading = false;
List<int> token = []; List<int> token = [];
final messageJson = MessageJson.fromJson(jsonDecode(message.contentJson!)); MessageContent? content =
final content = messageJson.content; MessageContent.fromJson(message.kind, jsonDecode(message.contentJson!));
if (message.messageOtherId != null && content is MediaMessageContent) { if (message.messageOtherId != null && content is MediaMessageContent) {
token = content.downloadToken; token = content.downloadToken;
isDownloading = message.downloadState == DownloadState.downloading; isDownloading = message.downloadState == DownloadState.downloading;

View file

@ -36,9 +36,45 @@ class _ChatListViewState extends State<ChatListView> {
Stream<List<Contact>> contacts = twonlyDatabase.watchContactsForChatList(); Stream<List<Contact>> contacts = twonlyDatabase.watchContactsForChatList();
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: GestureDetector( title: GestureDetector(
onTap: () { onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ProfileView(),
),
);
},
child: Text("twonly"),
),
// title:
actions: [
StreamBuilder(
stream: twonlyDatabase.watchContactsRequested(),
builder: (context, snapshot) {
var count = 0;
if (snapshot.hasData && snapshot.data != null) {
count = snapshot.data!;
}
return NotificationBadge(
count: count.toString(),
child: IconButton(
icon: FaIcon(FontAwesomeIcons.userPlus, size: 18),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SearchUsernameView(),
),
);
},
),
);
},
),
IconButton(
onPressed: () {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
@ -46,21 +82,24 @@ class _ChatListViewState extends State<ChatListView> {
), ),
); );
}, },
child: Text("twonly"), icon: FaIcon(FontAwesomeIcons.gear, size: 19),
), )
// title: ],
actions: [ ),
StreamBuilder( body: StreamBuilder(
stream: twonlyDatabase.watchContactsRequested(), stream: contacts,
builder: (context, snapshot) { builder: (context, snapshot) {
var count = 0; if (!snapshot.hasData || snapshot.data == null) {
if (snapshot.hasData && snapshot.data != null) { return Container();
count = snapshot.data!; }
}
return NotificationBadge( final contacts = snapshot.data!;
count: count.toString(), if (contacts.isEmpty) {
child: IconButton( return Center(
icon: FaIcon(FontAwesomeIcons.userPlus, size: 18), child: Padding(
padding: const EdgeInsets.all(10),
child: OutlinedButton.icon(
icon: Icon(Icons.person_add),
onPressed: () { onPressed: () {
Navigator.push( Navigator.push(
context, context,
@ -69,68 +108,32 @@ class _ChatListViewState extends State<ChatListView> {
), ),
); );
}, },
), label: Text(context.lang.chatListViewSearchUserNameBtn)),
); ),
},
),
IconButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ProfileView(),
),
);
},
icon: FaIcon(FontAwesomeIcons.gear, size: 19),
)
],
),
body: StreamBuilder(
stream: contacts,
builder: (context, snapshot) {
if (!snapshot.hasData || snapshot.data == null) {
return Container();
}
final contacts = snapshot.data!;
if (contacts.isEmpty) {
return Center(
child: Padding(
padding: const EdgeInsets.all(10),
child: OutlinedButton.icon(
icon: Icon(Icons.person_add),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SearchUsernameView()));
},
label: Text(context.lang.chatListViewSearchUserNameBtn)),
),
);
}
int maxTotalMediaCounter = 0;
if (contacts.isNotEmpty) {
maxTotalMediaCounter = contacts
.map((x) => x.totalMediaCounter)
.reduce((a, b) => a > b ? a : b);
}
return ListView.builder(
restorationId: 'chat_list_view',
itemCount: contacts.length,
itemBuilder: (BuildContext context, int index) {
final user = contacts[index];
return UserListItem(
user: user,
maxTotalMediaCounter: maxTotalMediaCounter,
);
},
); );
}, }
));
int maxTotalMediaCounter = 0;
if (contacts.isNotEmpty) {
maxTotalMediaCounter = contacts
.map((x) => x.totalMediaCounter)
.reduce((a, b) => a > b ? a : b);
}
return ListView.builder(
restorationId: 'chat_list_view',
itemCount: contacts.length,
itemBuilder: (BuildContext context, int index) {
final user = contacts[index];
return UserListItem(
user: user,
maxTotalMediaCounter: maxTotalMediaCounter,
);
},
);
},
),
);
} }
} }
@ -156,22 +159,9 @@ class _UserListItem extends State<UserListItem> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
// initAsync();
lastUpdateTime(); lastUpdateTime();
} }
// Future initAsync() async {
// if (currentMessage != null) {
// if (currentMessage!.downloadState != DownloadState.downloading) {
// final content = widget.lastMessage!.messageContent;
// if (content is MediaMessageContent) {
// tryDownloadMedia(widget.lastMessage!.messageId,
// widget.lastMessage!.otherUserId, content.downloadToken);
// }
// }
// }
// }
void lastUpdateTime() { void lastUpdateTime() {
// Change the color every 200 milliseconds // Change the color every 200 milliseconds
updateTime = Timer.periodic(Duration(milliseconds: 200), (timer) { updateTime = Timer.periodic(Duration(milliseconds: 200), (timer) {
@ -193,25 +183,6 @@ class _UserListItem extends State<UserListItem> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final notOpenedMessages =
twonlyDatabase.watchMessageNotOpened(widget.user.userId);
final lastMessage = twonlyDatabase.watchLastMessage(widget.user.userId);
// if (widget.lastMessage != null) {
// state = widget.lastMessage!.getSendState();
// final content = widget.lastMessage!.messageContent;
// if (widget.lastMessage!.messageReceived &&
// content is MediaMessageContent) {
// token = content.downloadToken;
// isDownloading = context
// .watch<DownloadChangeProvider>()
// .currentlyDownloading
// .contains(token.toString());
// }
// }
int flameCounter = getFlameCounterFromContact(widget.user); int flameCounter = getFlameCounterFromContact(widget.user);
return UserContextMenu( return UserContextMenu(
@ -219,25 +190,35 @@ class _UserListItem extends State<UserListItem> {
child: ListTile( child: ListTile(
title: Text(getContactDisplayName(widget.user)), title: Text(getContactDisplayName(widget.user)),
subtitle: StreamBuilder( subtitle: StreamBuilder(
stream: lastMessage, stream: twonlyDatabase.watchLastMessage(widget.user.userId),
builder: (context, lastMessageSnapshot) { builder: (context, lastMessageSnapshot) {
if (!lastMessageSnapshot.hasData) { if (!lastMessageSnapshot.hasData) {
return Container(); return Container();
} }
if (lastMessageSnapshot.data == null) { if (lastMessageSnapshot.data!.isEmpty) {
return Text(context.lang.chatsTapToSend); return Text(context.lang.chatsTapToSend);
} }
final lastMessage = lastMessageSnapshot.data!; final lastMessage = lastMessageSnapshot.data!.first;
return StreamBuilder( return StreamBuilder(
stream: notOpenedMessages, stream: twonlyDatabase.watchMessageNotOpened(widget.user.userId),
builder: (context, notOpenedMessagesSnapshot) { builder: (context, notOpenedMessagesSnapshot) {
if (!lastMessageSnapshot.hasData) { if (!lastMessageSnapshot.hasData) {
return Container(); return Container();
} }
var lastMessages = [lastMessage]; var lastMessages = [lastMessage];
if (notOpenedMessagesSnapshot.data != null) { if (notOpenedMessagesSnapshot.data != null &&
notOpenedMessagesSnapshot.data!.isNotEmpty) {
lastMessages = notOpenedMessagesSnapshot.data!; lastMessages = notOpenedMessagesSnapshot.data!;
var media =
lastMessages.where((x) => x.kind == MessageKind.media);
if (media.isNotEmpty) {
currentMessage = media.first;
} else {
currentMessage = lastMessages.first;
}
} else {
currentMessage = lastMessage;
} }
return Row( return Row(

View file

@ -86,10 +86,9 @@ class _MediaViewerViewState extends State<MediaViewerView> {
await _noScreenshot.screenshotOff(); await _noScreenshot.screenshotOff();
if (!context.mounted || allMediaFiles.isEmpty) return; if (!context.mounted || allMediaFiles.isEmpty) return;
final Message current = allMediaFiles.first; final current = allMediaFiles.first;
final MessageJson messageJson = final MediaMessageContent content =
MessageJson.fromJson(jsonDecode(current.contentJson!)); MediaMessageContent.fromJson(jsonDecode(current.contentJson!));
final MessageContent? content = messageJson.content;
setState(() { setState(() {
// reset current image values // reset current image values
@ -101,17 +100,16 @@ class _MediaViewerViewState extends State<MediaViewerView> {
isRealTwonly = false; isRealTwonly = false;
}); });
if (content is MediaMessageContent) { if (content.isRealTwonly) {
if (content.isRealTwonly) { setState(() {
setState(() { isRealTwonly = true;
isRealTwonly = true; });
}); if (!showTwonly) {
if (!showTwonly) { return;
return;
}
} }
if (isRealTwonly) { if (isRealTwonly) {
if (!context.mounted) return;
bool isAuth = await authenticateUser(context.lang.mediaViewerAuthReason, bool isAuth = await authenticateUser(context.lang.mediaViewerAuthReason,
force: false); force: false);
if (!isAuth) { if (!isAuth) {

View file

@ -67,6 +67,8 @@ class _ContactViewState extends State<ContactView> {
), ),
], ],
), ),
if (getContactDisplayName(contact) != contact.username)
Center(child: Text("(${contact.username})")),
SizedBox(height: 50), SizedBox(height: 50),
BetterListTile( BetterListTile(
icon: FontAwesomeIcons.pencil, icon: FontAwesomeIcons.pencil,
@ -127,7 +129,7 @@ class _ContactViewState extends State<ContactView> {
Future<String?> showNicknameChangeDialog( Future<String?> showNicknameChangeDialog(
BuildContext context, Contact contact) { BuildContext context, Contact contact) {
final TextEditingController controller = final TextEditingController controller =
TextEditingController(text: contact.displayName); TextEditingController(text: getContactDisplayName(contact));
return showDialog<String>( return showDialog<String>(
context: context, context: context,