fixing all compile errors

This commit is contained in:
otsmr 2025-10-25 01:08:59 +02:00
parent 84828cd820
commit 4260c63ce2
61 changed files with 3223 additions and 3606 deletions

View file

@ -43,18 +43,7 @@ class _AppState extends State<App> with WidgetsBindingObserver {
Future<void> setUserPlan() async {
final user = await getUser();
globalBestFriendUserId = -1;
if (user != null && mounted) {
if (user.myBestFriendContactId != null) {
final contact = await twonlyDB.contactsDao
.getContactByUserId(user.myBestFriendContactId!)
.getSingleOrNull();
if (contact != null) {
if (contact.alsoBestFriend) {
globalBestFriendUserId = user.myBestFriendContactId ?? 0;
}
}
}
if (mounted) {
await context
.read<CustomChangeProvider>()

View file

@ -27,4 +27,3 @@ void Function() globalCallbackNewDeviceRegistered = () {};
void Function(String planId) globalCallbackUpdatePlan = (String planId) {};
bool globalIsAppInBackground = true;
int globalBestFriendUserId = -1;

View file

@ -47,7 +47,6 @@ void main() async {
await initFileDownloader();
// await twonlyDB.messagesDao.resetPendingDownloadState();
// await twonlyDB.messagesDao.handleMediaFilesOlderThan30Days();
// await twonlyDB.messageRetransmissionDao.purgeOldRetransmissions();
// await twonlyDB.signalDao.purgeOutDatedPreKeys();

View file

@ -20,79 +20,6 @@ class ContactsDao extends DatabaseAccessor<TwonlyDB> with _$ContactsDaoMixin {
}
}
Future<int> incFlameCounter(
int contactId,
bool received,
DateTime timestamp,
) async {
final contact = (await (select(contacts)
..where((t) => t.userId.equals(contactId)))
.get())
.first;
final totalMediaCounter = contact.totalMediaCounter + 1;
var flameCounter = contact.flameCounter;
if (contact.lastMessageReceived != null &&
contact.lastMessageSend != null) {
final now = DateTime.now();
final startOfToday = DateTime(now.year, now.month, now.day);
final twoDaysAgo = startOfToday.subtract(const Duration(days: 2));
if (contact.lastMessageSend!.isBefore(twoDaysAgo) ||
contact.lastMessageReceived!.isBefore(twoDaysAgo)) {
flameCounter = 0;
}
}
var lastMessageSend = const Value<DateTime?>.absent();
var lastMessageReceived = const Value<DateTime?>.absent();
var lastFlameCounterChange = const Value<DateTime?>.absent();
if (contact.lastFlameCounterChange != null) {
final now = DateTime.now();
final startOfToday = DateTime(now.year, now.month, now.day);
if (contact.lastFlameCounterChange!.isBefore(startOfToday)) {
// last flame update was yesterday. check if it can be updated.
var updateFlame = false;
if (received) {
if (contact.lastMessageSend != null &&
contact.lastMessageSend!.isAfter(startOfToday)) {
// today a message was already send -> update flame
updateFlame = true;
}
} else if (contact.lastMessageReceived != null &&
contact.lastMessageReceived!.isAfter(startOfToday)) {
// today a message was already received -> update flame
updateFlame = true;
}
if (updateFlame) {
flameCounter += 1;
lastFlameCounterChange = Value(timestamp);
}
}
} else {
// There where no message until no...
lastFlameCounterChange = Value(timestamp);
}
if (received) {
lastMessageReceived = Value(timestamp);
} else {
lastMessageSend = Value(timestamp);
}
return (update(contacts)..where((t) => t.userId.equals(contactId))).write(
ContactsCompanion(
totalMediaCounter: Value(totalMediaCounter),
lastFlameCounterChange: lastFlameCounterChange,
lastMessageReceived: lastMessageReceived,
lastMessageSend: lastMessageSend,
flameCounter: Value(flameCounter),
),
);
}
SingleOrNullSelectable<Contact> getContactByUserId(int userId) {
return select(contacts)..where((t) => t.userId.equals(userId));
}
@ -135,37 +62,6 @@ class ContactsDao extends DatabaseAccessor<TwonlyDB> with _$ContactsDaoMixin {
.watchSingleOrNull();
}
// Stream<List<Contact>> watchContactsForShareView() {
// return (select(contacts)
// ..where(
// (t) =>
// t.accepted.equals(true) &
// t.blocked.equals(false) &
// t.deleted.equals(false),
// )
// ..orderBy([(t) => OrderingTerm.desc(t.lastMessageExchange)]))
// .watch();
// }
// Stream<List<Contact>> watchContactsForStartNewChat() {
// return (select(contacts)
// ..where((t) => t.accepted.equals(true) & t.blocked.equals(false))
// ..orderBy([(t) => OrderingTerm.desc(t.lastMessageExchange)]))
// .watch();
// }
// Stream<List<Contact>> watchContactsForChatList() {
// return (select(contacts)
// ..where(
// (t) =>
// t.accepted.equals(true) &
// t.blocked.equals(false) &
// t.archived.equals(false),
// )
// ..orderBy([(t) => OrderingTerm.desc(t.lastMessageExchange)]))
// .watch();
// }
Future<List<Contact>> getAllNotBlockedContacts() {
return (select(contacts)..where((t) => t.blocked.equals(false))).get();
}
@ -188,31 +84,15 @@ class ContactsDao extends DatabaseAccessor<TwonlyDB> with _$ContactsDaoMixin {
return query.map((row) => row.read(count)).watchSingle();
}
Stream<List<Contact>> watchAllAcceptedContacts() {
return (select(contacts)
..where((t) => t.blocked.equals(false) & t.accepted.equals(true)))
.watch();
}
Stream<List<Contact>> watchAllContacts() {
return select(contacts).watch();
}
Future<void> modifyFlameCounterForTesting() async {
await update(contacts).write(
ContactsCompanion(
lastFlameCounterChange: Value(DateTime.now()),
flameCounter: const Value(1337),
lastFlameSync: const Value(null),
),
);
}
Stream<int> watchFlameCounter(int userId) {
return (select(contacts)
..where(
(u) =>
u.userId.equals(userId) &
u.lastMessageReceived.isNotNull() &
u.lastMessageSend.isNotNull(),
))
.watchSingle()
.asyncMap(getFlameCounterFromContact);
}
}
String getContactDisplayName(Contact user) {
@ -234,18 +114,3 @@ String getContactDisplayName(Contact user) {
String applyStrikethrough(String text) {
return text.split('').map((char) => '$char\u0336').join();
}
int getFlameCounterFromContact(Contact contact) {
if (contact.lastMessageSend == null || contact.lastMessageReceived == null) {
return 0;
}
final now = DateTime.now();
final startOfToday = DateTime(now.year, now.month, now.day);
final twoDaysAgo = startOfToday.subtract(const Duration(days: 2));
if (contact.lastMessageSend!.isAfter(twoDaysAgo) &&
contact.lastMessageReceived!.isAfter(twoDaysAgo)) {
return contact.flameCounter + 1;
} else {
return 0;
}
}

View file

@ -32,7 +32,56 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
.get();
}
Future<List<Group>> getDirectChat(int userId) async {
Future<void> insertGroup(GroupsCompanion group) async {
await into(groups).insert(group);
}
Future<List<Contact>> getGroupContact(String groupId) async {
final query = select(contacts).join([
leftOuterJoin(
groupMembers,
groupMembers.contactId.equalsExp(contacts.userId) &
groupMembers.groupId.equals(groupId),
),
]);
return query.map((row) => row.readTable(contacts)).get();
}
Stream<List<Group>> watchGroups() {
return select(groups).watch();
}
Stream<Group?> watchGroup(String groupId) {
return (select(groups)..where((t) => t.groupId.equals(groupId)))
.watchSingleOrNull();
}
Stream<List<Group>> watchGroupsForChatList() {
return (select(groups)..where((t) => t.archived.equals(false))).watch();
}
Future<Group?> getGroup(String groupId) {
return (select(groups)..where((t) => t.groupId.equals(groupId)))
.getSingleOrNull();
}
Stream<int> watchFlameCounter(String groupId) {
return (select(groups)
..where(
(u) =>
u.groupId.equals(groupId) &
u.lastMessageReceived.isNotNull() &
u.lastMessageSend.isNotNull(),
))
.watchSingle()
.asyncMap(getFlameCounterFromGroup);
}
Future<List<Group>> getAllDirectChats() {
return (select(groups)..where((t) => t.isDirectChat.equals(true))).get();
}
Future<Group?> getDirectChat(int userId) async {
final query = (select(groups).join([
leftOuterJoin(
groupMembers,
@ -40,8 +89,94 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
groupMembers.contactId.equals(userId),
),
])
..where(groups.isGroupOfTwo.equals(true)));
..where(groups.isDirectChat.equals(true)));
return query.map((row) => row.readTable(groups)).get();
return query.map((row) => row.readTable(groups)).getSingleOrNull();
}
Future<void> incFlameCounter(
String groupId,
bool received,
DateTime timestamp,
) async {
final group = await (select(groups)
..where((t) => t.groupId.equals(groupId)))
.getSingle();
final totalMediaCounter = group.totalMediaCounter + 1;
var flameCounter = group.flameCounter;
if (group.lastMessageReceived != null && group.lastMessageSend != null) {
final now = DateTime.now();
final startOfToday = DateTime(now.year, now.month, now.day);
final twoDaysAgo = startOfToday.subtract(const Duration(days: 2));
if (group.lastMessageSend!.isBefore(twoDaysAgo) ||
group.lastMessageReceived!.isBefore(twoDaysAgo)) {
flameCounter = 0;
}
}
var lastMessageSend = const Value<DateTime?>.absent();
var lastMessageReceived = const Value<DateTime?>.absent();
var lastFlameCounterChange = const Value<DateTime?>.absent();
if (group.lastFlameCounterChange != null) {
final now = DateTime.now();
final startOfToday = DateTime(now.year, now.month, now.day);
if (group.lastFlameCounterChange!.isBefore(startOfToday)) {
// last flame update was yesterday. check if it can be updated.
var updateFlame = false;
if (received) {
if (group.lastMessageSend != null &&
group.lastMessageSend!.isAfter(startOfToday)) {
// today a message was already send -> update flame
updateFlame = true;
}
} else if (group.lastMessageReceived != null &&
group.lastMessageReceived!.isAfter(startOfToday)) {
// today a message was already received -> update flame
updateFlame = true;
}
if (updateFlame) {
flameCounter += 1;
lastFlameCounterChange = Value(timestamp);
}
}
} else {
// There where no message until no...
lastFlameCounterChange = Value(timestamp);
}
if (received) {
lastMessageReceived = Value(timestamp);
} else {
lastMessageSend = Value(timestamp);
}
await (update(groups)..where((t) => t.groupId.equals(groupId))).write(
GroupsCompanion(
totalMediaCounter: Value(totalMediaCounter),
lastFlameCounterChange: lastFlameCounterChange,
lastMessageReceived: lastMessageReceived,
lastMessageSend: lastMessageSend,
flameCounter: Value(flameCounter),
),
);
}
}
int getFlameCounterFromGroup(Group group) {
if (group.lastMessageSend == null || group.lastMessageReceived == null) {
return 0;
}
final now = DateTime.now();
final startOfToday = DateTime(now.year, now.month, now.day);
final twoDaysAgo = startOfToday.subtract(const Duration(days: 2));
if (group.lastMessageSend!.isAfter(twoDaysAgo) &&
group.lastMessageReceived!.isAfter(twoDaysAgo)) {
return group.flameCounter + 1;
} else {
return 0;
}
}

View file

@ -46,6 +46,11 @@ class MediaFilesDao extends DatabaseAccessor<TwonlyDB>
.getSingleOrNull();
}
Stream<MediaFile?> watchMedia(String mediaId) {
return (select(mediaFiles)..where((t) => t.mediaId.equals(mediaId)))
.watchSingleOrNull();
}
Future<void> resetPendingDownloadState() async {
await (update(mediaFiles)
..where(

View file

@ -4,6 +4,7 @@ import 'package:twonly/src/database/tables/contacts.table.dart';
import 'package:twonly/src/database/tables/groups.table.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/database/tables/messages.table.dart';
import 'package:twonly/src/database/tables/reactions.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
import 'package:twonly/src/utils/log.dart';
@ -15,7 +16,9 @@ part 'messages.dao.g.dart';
Messages,
Contacts,
MediaFiles,
Reactions,
MessageHistories,
GroupMembers,
MessageActions,
Groups,
],
@ -26,55 +29,39 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
// ignore: matching_super_parameters
MessagesDao(super.db);
// Stream<List<Message>> watchMessageNotOpened(int contactId) {
// return (select(messages)
// ..where(
// (t) =>
// t.openedAt.isNull() &
// t.contactId.equals(contactId) &
// t.errorWhileSending.equals(false),
// )
// ..orderBy([(t) => OrderingTerm.desc(t.sendAt)]))
// .watch();
// }
Stream<List<Message>> watchMessageNotOpened(String groupId) {
return (select(messages)
..where((t) => t.openedAt.isNull() & t.groupId.equals(groupId))
..orderBy([(t) => OrderingTerm.desc(t.createdAt)]))
.watch();
}
// Stream<List<Message>> watchMediaMessageNotOpened(int contactId) {
// return (select(messages)
// ..where(
// (t) =>
// t.openedAt.isNull() &
// t.contactId.equals(contactId) &
// t.errorWhileSending.equals(false) &
// t.messageOtherId.isNotNull() &
// t.kind.equals(MessageKind.media.name),
// )
// ..orderBy([(t) => OrderingTerm.asc(t.sendAt)]))
// .watch();
// }
Stream<List<Message>> watchMediaNotOpened(String groupId) {
return (select(messages)
..where(
(t) =>
t.openedAt.isNull() &
t.groupId.equals(groupId) &
t.senderId.isNotNull() &
t.type.equals(MessageType.media.name),
)
..orderBy([(t) => OrderingTerm.asc(t.createdAt)]))
.watch();
}
// Stream<List<Message>> watchLastMessage(int contactId) {
// return (select(messages)
// ..where((t) => t.contactId.equals(contactId))
// ..orderBy([(t) => OrderingTerm.desc(t.sendAt)])
// ..limit(1))
// .watch();
// }
Stream<List<Message>> watchLastMessage(String groupId) {
return (select(messages)
..where((t) => t.groupId.equals(groupId))
..orderBy([(t) => OrderingTerm.desc(t.createdAt)])
..limit(1))
.watch();
}
// Stream<List<Message>> watchAllMessagesFrom(int contactId) {
// return (select(messages)
// ..where(
// (t) =>
// t.contactId.equals(contactId) &
// t.contentJson.isNotNull() &
// (t.openedAt.isNull() |
// t.mediaStored.equals(true) |
// t.openedAt.isBiggerThanValue(
// DateTime.now().subtract(const Duration(days: 1)),
// )),
// )
// ..orderBy([(t) => OrderingTerm.asc(t.sendAt)]))
// .watch();
// }
Stream<List<Message>> watchByGroupId(String groupId) {
return ((select(messages)..where((t) => t.groupId.equals(groupId)))
..orderBy([(t) => OrderingTerm.asc(t.createdAt)]))
.watch();
}
// Future<void> removeOldMessages() {
// return (update(messages)
@ -92,22 +79,6 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
// .write(const MessagesCompanion(contentJson: Value(null)));
// }
// Future<void> handleMediaFilesOlderThan30Days() {
// /// media files will be deleted by the server after 30 days, so delete them here also
// return (update(messages)
// ..where(
// (t) => (t.kind.equals(MessageKind.media.name) &
// t.openedAt.isNull() &
// t.messageOtherId.isNull() &
// (t.sendAt.isSmallerThanValue(
// DateTime.now().subtract(
// const Duration(days: 30),
// ),
// ))),
// ))
// .write(const MessagesCompanion(errorWhileSending: Value(true)));
// }
// Future<List<Message>> getAllMessagesPendingDownloading() {
// return (select(messages)
// ..where(
@ -155,18 +126,18 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
// .get();
// }
// Future<void> openedAllNonMediaMessages(int contactId) {
// final updates = MessagesCompanion(openedAt: Value(DateTime.now()));
// return (update(messages)
// ..where(
// (t) =>
// t.contactId.equals(contactId) &
// t.messageOtherId.isNotNull() &
// t.openedAt.isNull() &
// t.kind.equals(MessageKind.media.name).not(),
// ))
// .write(updates);
// }
Future<void> openedAllTextMessages(String groupId) {
final updates = MessagesCompanion(openedAt: Value(DateTime.now()));
return (update(messages)
..where(
(t) =>
t.groupId.equals(groupId) &
t.senderId.isNotNull() &
t.openedAt.isNull() &
t.type.equals(MessageType.text.name),
))
.write(updates);
}
Future<void> handleMessageDeletion(
int contactId,
@ -259,6 +230,22 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
);
}
Future<bool> haveAllMembers(
String groupId,
String messageId,
MessageActionType action,
) async {
final members = await twonlyDB.groupsDao.getGroupMembers(groupId);
final actions = await (select(messageActions)
..where(
(t) => t.type.equals(action.name) & t.messageId.equals(messageId),
))
.get();
return members.length == actions.length;
}
// Future<void> updateMessageByOtherUser(
// int userId,
// int messageId,
@ -321,6 +308,29 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
}
}
Future<MessageAction?> getLastMessageAction(String messageId) async {
return (((select(messageActions)
..where(
(t) => t.messageId.equals(messageId),
))
..orderBy([(t) => OrderingTerm.desc(t.actionAt)]))
..limit(1))
.getSingleOrNull();
}
Future<void> reopenedMedia(String messageId) async {
await (delete(messageActions)
..where(
(t) =>
t.messageId.equals(messageId) &
t.contactId.isNull() &
t.type.equals(
MessageActionType.openedAt.name,
),
))
.go();
}
// Future<void> deleteMessagesByContactId(int contactId) {
// return (delete(messages)
// ..where(
@ -342,9 +352,9 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
// .go();
// }
// Future<void> deleteMessagesByMessageId(int messageId) {
// return (delete(messages)..where((t) => t.messageId.equals(messageId))).go();
// }
Future<void> deleteMessagesById(String messageId) {
return (delete(messages)..where((t) => t.messageId.equals(messageId))).go();
}
// Future<void> deleteAllMessagesByContactId(int contactId) {
// return (delete(messages)..where((t) => t.contactId.equals(contactId))).go();

View file

@ -8,7 +8,9 @@ mixin _$MessagesDaoMixin on DatabaseAccessor<TwonlyDB> {
$ContactsTable get contacts => attachedDatabase.contacts;
$MediaFilesTable get mediaFiles => attachedDatabase.mediaFiles;
$MessagesTable get messages => attachedDatabase.messages;
$ReactionsTable get reactions => attachedDatabase.reactions;
$MessageHistoriesTable get messageHistories =>
attachedDatabase.messageHistories;
$GroupMembersTable get groupMembers => attachedDatabase.groupMembers;
$MessageActionsTable get messageActions => attachedDatabase.messageActions;
}

View file

@ -43,4 +43,15 @@ class ReactionsDao extends DatabaseAccessor<TwonlyDB> with _$ReactionsDaoMixin {
Log.error(e);
}
}
Stream<List<Reaction>> watchReactions(String messageId) {
return (select(reactions)
..where((t) => t.messageId.equals(messageId))
..orderBy([(t) => OrderingTerm.desc(t.createdAt)]))
.watch();
}
Future<void> insertReaction(ReactionsCompanion reaction) async {
await into(reactions).insert(reaction);
}
}

View file

@ -13,28 +13,12 @@ class Contacts extends Table {
BoolColumn get accepted => boolean().withDefault(const Constant(false))();
BoolColumn get requested => boolean().withDefault(const Constant(false))();
BoolColumn get hidden => boolean().withDefault(const Constant(false))();
BoolColumn get blocked => boolean().withDefault(const Constant(false))();
BoolColumn get verified => boolean().withDefault(const Constant(false))();
BoolColumn get deleted => boolean().withDefault(const Constant(false))();
BoolColumn get alsoBestFriend =>
boolean().withDefault(const Constant(false))();
IntColumn get deleteMessagesAfterXMinutes =>
integer().withDefault(const Constant(60 * 24))();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
IntColumn get totalMediaCounter => integer().withDefault(const Constant(0))();
DateTimeColumn get lastMessageSend => dateTime().nullable()();
DateTimeColumn get lastMessageReceived => dateTime().nullable()();
DateTimeColumn get lastFlameCounterChange => dateTime().nullable()();
DateTimeColumn get lastFlameSync => dateTime().nullable()();
IntColumn get flameCounter => integer().withDefault(const Constant(0))();
@override
Set<Column> get primaryKey => {userId};
}

View file

@ -7,15 +7,31 @@ class Groups extends Table {
TextColumn get groupId => text().clientDefault(() => uuid.v4())();
BoolColumn get isGroupAdmin => boolean()();
BoolColumn get isGroupOfTwo => boolean()();
BoolColumn get isDirectChat => boolean()();
BoolColumn get pinned => boolean().withDefault(const Constant(false))();
BoolColumn get archived => boolean().withDefault(const Constant(false))();
TextColumn get groupName => text()();
IntColumn get totalMediaCounter => integer().withDefault(const Constant(0))();
BoolColumn get alsoBestFriend =>
boolean().withDefault(const Constant(false))();
IntColumn get deleteMessagesAfterMilliseconds =>
integer().withDefault(const Constant(1000 * 60 * 24))();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
DateTimeColumn get lastMessageSend => dateTime().nullable()();
DateTimeColumn get lastMessageReceived => dateTime().nullable()();
DateTimeColumn get lastFlameCounterChange => dateTime().nullable()();
DateTimeColumn get lastFlameSync => dateTime().nullable()();
IntColumn get flameCounter => integer().withDefault(const Constant(0))();
DateTimeColumn get lastMessageExchange =>
dateTime().withDefault(currentDateAndTime)();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
@override
Set<Column> get primaryKey => {groupId};

View file

@ -33,9 +33,9 @@ class Messages extends Table {
BoolColumn get isDeletedFromSender =>
boolean().withDefault(const Constant(false))();
BoolColumn get isEdited => boolean().withDefault(const Constant(false))();
DateTimeColumn get openedAt => dateTime().nullable()();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
DateTimeColumn get modifiedAt => dateTime().nullable()();
@override
Set<Column> get primaryKey => {messageId};
@ -43,15 +43,12 @@ class Messages extends Table {
enum MessageActionType {
openedAt,
modifiedAt,
ackByUserAt,
ackByServerAt,
}
@DataClassName('MessageAction')
class MessageActions extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get messageId =>
text().references(Messages, #messageId, onDelete: KeyAction.cascade)();
@ -59,10 +56,11 @@ class MessageActions extends Table {
integer().references(Contacts, #contactId, onDelete: KeyAction.cascade)();
TextColumn get type => textEnum<MessageActionType>()();
DateTimeColumn get actionAt => dateTime().withDefault(currentDateAndTime)();
@override
Set<Column> get primaryKey => {id};
Set<Column> get primaryKey => {messageId, contactId, type};
}
@DataClassName('MessageHistory')
@ -72,8 +70,9 @@ class MessageHistories extends Table {
TextColumn get messageId =>
text().references(Messages, #messageId, onDelete: KeyAction.cascade)();
IntColumn get contactId =>
integer().references(Contacts, #contactId, onDelete: KeyAction.cascade)();
IntColumn get contactId => integer()
.nullable()
.references(Contacts, #contactId, onDelete: KeyAction.cascade)();
TextColumn get content => text().nullable()();

View file

@ -42,7 +42,7 @@ part 'twonly.db.g.dart';
SignalSessionStores,
SignalContactPreKeys,
SignalContactSignedPreKeys,
MessageActions
MessageActions,
],
daos: [
MessagesDao,

File diff suppressed because it is too large Load diff

View file

@ -1,331 +0,0 @@
// ignore_for_file: strict_raw_type, prefer_constructors_over_static_methods
import 'package:flutter/material.dart';
import 'package:twonly/src/database/tables_old/messages_table.dart';
import 'package:twonly/src/utils/misc.dart';
Color getMessageColorFromType(MessageContent content, BuildContext context) {
Color color;
if (content is TextMessageContent) {
color = Colors.blueAccent;
} else {
if (content is MediaMessageContent) {
if (content.isRealTwonly) {
color = context.color.primary;
} else {
if (content.isVideo) {
color = const Color.fromARGB(255, 243, 33, 208);
} else {
color = Colors.redAccent;
}
}
} else {
return (isDarkMode(context)) ? Colors.white : Colors.black;
}
}
return color;
}
extension MessageKindExtension on MessageKind {
String get name => toString().split('.').last;
static MessageKind fromString(String name) {
return MessageKind.values.firstWhere((e) => e.name == name);
}
}
class MessageJson {
MessageJson({
required this.kind,
required this.content,
required this.timestamp,
this.messageReceiverId,
this.messageSenderId,
this.retransId,
});
final MessageKind kind;
final MessageContent? content;
final int? messageReceiverId;
final int? messageSenderId;
int? retransId;
DateTime timestamp;
@override
String toString() {
return 'Message(kind: $kind, content: $content, timestamp: $timestamp)';
}
static MessageJson fromJson(Map<String, dynamic> json) {
final kind = MessageKindExtension.fromString(json['kind'] as String);
return MessageJson(
kind: kind,
messageReceiverId: (json['messageReceiverId'] as num?)?.toInt(),
messageSenderId: (json['messageSenderId'] as num?)?.toInt(),
retransId: (json['retransId'] as num?)?.toInt(),
content: MessageContent.fromJson(
kind,
json['content'] as Map<String, dynamic>,
),
timestamp: DateTime.fromMillisecondsSinceEpoch(json['timestamp'] as int),
);
}
Map<String, dynamic> toJson() => <String, dynamic>{
'kind': kind.name,
'content': content?.toJson(),
'messageReceiverId': messageReceiverId,
'messageSenderId': messageSenderId,
'retransId': retransId,
'timestamp': timestamp.toUtc().millisecondsSinceEpoch,
};
}
class MessageContent {
MessageContent();
static MessageContent? fromJson(MessageKind kind, Map json) {
switch (kind) {
case MessageKind.media:
return MediaMessageContent.fromJson(json);
case MessageKind.textMessage:
return TextMessageContent.fromJson(json);
case MessageKind.profileChange:
return ProfileContent.fromJson(json);
case MessageKind.pushKey:
return PushKeyContent.fromJson(json);
case MessageKind.reopenedMedia:
return ReopenedMediaFileContent.fromJson(json);
case MessageKind.flameSync:
return FlameSyncContent.fromJson(json);
case MessageKind.ack:
return AckContent.fromJson(json);
case MessageKind.signalDecryptError:
return SignalDecryptErrorContent.fromJson(json);
case MessageKind.storedMediaFile:
case MessageKind.contactRequest:
case MessageKind.rejectRequest:
case MessageKind.acceptRequest:
case MessageKind.opened:
case MessageKind.requestPushKey:
case MessageKind.receiveMediaError:
}
return null;
}
Map toJson() {
return {};
}
}
class MediaMessageContent extends MessageContent {
MediaMessageContent({
required this.maxShowTime,
required this.isRealTwonly,
required this.isVideo,
required this.mirrorVideo,
this.downloadToken,
this.encryptionKey,
this.encryptionMac,
this.encryptionNonce,
});
final int maxShowTime;
final bool isRealTwonly;
final bool isVideo;
final bool mirrorVideo;
final List<int>? downloadToken;
final List<int>? encryptionKey;
final List<int>? encryptionMac;
final List<int>? encryptionNonce;
static MediaMessageContent fromJson(Map json) {
return MediaMessageContent(
downloadToken: json['downloadToken'] == null
? null
: List<int>.from(json['downloadToken'] as List),
encryptionKey: json['encryptionKey'] == null
? null
: List<int>.from(json['encryptionKey'] as List),
encryptionMac: json['encryptionMac'] == null
? null
: List<int>.from(json['encryptionMac'] as List),
encryptionNonce: json['encryptionNonce'] == null
? null
: List<int>.from(json['encryptionNonce'] as List),
maxShowTime: json['maxShowTime'] as int,
isRealTwonly: json['isRealTwonly'] as bool,
isVideo: json['isVideo'] as bool? ?? false,
mirrorVideo: json['mirrorVideo'] as bool? ?? false,
);
}
@override
Map toJson() {
return {
'downloadToken': downloadToken,
'encryptionKey': encryptionKey,
'encryptionMac': encryptionMac,
'encryptionNonce': encryptionNonce,
'isRealTwonly': isRealTwonly,
'maxShowTime': maxShowTime,
'isVideo': isVideo,
'mirrorVideo': mirrorVideo,
};
}
}
class TextMessageContent extends MessageContent {
TextMessageContent({
required this.text,
this.responseToMessageId,
this.responseToOtherMessageId,
});
String text;
int? responseToMessageId;
int? responseToOtherMessageId;
static TextMessageContent fromJson(Map json) {
return TextMessageContent(
text: json['text'] as String,
responseToOtherMessageId: json.containsKey('responseToOtherMessageId')
? json['responseToOtherMessageId'] as int?
: null,
responseToMessageId: json.containsKey('responseToMessageId')
? json['responseToMessageId'] as int?
: null,
);
}
@override
Map toJson() {
return {
'text': text,
'responseToMessageId': responseToMessageId,
'responseToOtherMessageId': responseToOtherMessageId,
};
}
}
class ReopenedMediaFileContent extends MessageContent {
ReopenedMediaFileContent({required this.messageId});
int messageId;
static ReopenedMediaFileContent fromJson(Map json) {
return ReopenedMediaFileContent(messageId: json['messageId'] as int);
}
@override
Map toJson() {
return {'messageId': messageId};
}
}
class SignalDecryptErrorContent extends MessageContent {
SignalDecryptErrorContent({required this.encryptedHash});
List<int> encryptedHash;
static SignalDecryptErrorContent fromJson(Map json) {
return SignalDecryptErrorContent(
encryptedHash: List<int>.from(json['encryptedHash'] as List),
);
}
@override
Map toJson() {
return {
'encryptedHash': encryptedHash,
};
}
}
class AckContent extends MessageContent {
AckContent({required this.messageIdToAck, required this.retransIdToAck});
int? messageIdToAck;
int retransIdToAck;
static AckContent fromJson(Map json) {
return AckContent(
messageIdToAck: json['messageIdToAck'] as int?,
retransIdToAck: json['retransIdToAck'] as int,
);
}
@override
Map toJson() {
return {
'messageIdToAck': messageIdToAck,
'retransIdToAck': retransIdToAck,
};
}
}
class ProfileContent extends MessageContent {
ProfileContent({required this.avatarSvg, required this.displayName});
String avatarSvg;
String displayName;
static ProfileContent fromJson(Map json) {
return ProfileContent(
avatarSvg: json['avatarSvg'] as String,
displayName: json['displayName'] as String,
);
}
@override
Map toJson() {
return {'avatarSvg': avatarSvg, 'displayName': displayName};
}
}
class PushKeyContent extends MessageContent {
PushKeyContent({required this.keyId, required this.key});
int keyId;
List<int> key;
static PushKeyContent fromJson(Map json) {
return PushKeyContent(
keyId: json['keyId'] as int,
key: List<int>.from(json['key'] as List),
);
}
@override
Map toJson() {
return {
'keyId': keyId,
'key': key,
};
}
}
class FlameSyncContent extends MessageContent {
FlameSyncContent({
required this.flameCounter,
required this.bestFriend,
required this.lastFlameCounterChange,
});
int flameCounter;
DateTime lastFlameCounterChange;
bool bestFriend;
static FlameSyncContent fromJson(Map json) {
return FlameSyncContent(
flameCounter: json['flameCounter'] as int,
bestFriend: json['bestFriend'] as bool,
lastFlameCounterChange: DateTime.fromMillisecondsSinceEpoch(
json['lastFlameCounterChange'] as int,
),
);
}
@override
Map toJson() {
return {
'flameCounter': flameCounter,
'bestFriend': bestFriend,
'lastFlameCounterChange':
lastFlameCounterChange.toUtc().millisecondsSinceEpoch,
};
}
}

View file

@ -72,7 +72,7 @@ class UserData {
List<String>? tutorialDisplayed;
int? myBestFriendContactId;
String? myBestFriendGroupId;
DateTime? signalLastSignedPreKeyUpdated;

View file

@ -48,7 +48,7 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) => UserData(
..tutorialDisplayed = (json['tutorialDisplayed'] as List<dynamic>?)
?.map((e) => e as String)
.toList()
..myBestFriendContactId = (json['myBestFriendContactId'] as num?)?.toInt()
..myBestFriendGroupId = json['myBestFriendGroupId'] as String?
..signalLastSignedPreKeyUpdated =
json['signalLastSignedPreKeyUpdated'] == null
? null
@ -97,7 +97,7 @@ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
'lastPlanBallance': instance.lastPlanBallance,
'additionalUserInvites': instance.additionalUserInvites,
'tutorialDisplayed': instance.tutorialDisplayed,
'myBestFriendContactId': instance.myBestFriendContactId,
'myBestFriendGroupId': instance.myBestFriendGroupId,
'signalLastSignedPreKeyUpdated':
instance.signalLastSignedPreKeyUpdated?.toIso8601String(),
'currentPreKeyIndexStart': instance.currentPreKeyIndexStart,

View file

@ -663,14 +663,14 @@ class EncryptedContent_Media extends $pb.GeneratedMessage {
class EncryptedContent_MediaUpdate extends $pb.GeneratedMessage {
factory EncryptedContent_MediaUpdate({
EncryptedContent_MediaUpdate_Type? type,
$core.String? targetMessageId,
$core.String? targetMediaId,
}) {
final $result = create();
if (type != null) {
$result.type = type;
}
if (targetMessageId != null) {
$result.targetMessageId = targetMessageId;
if (targetMediaId != null) {
$result.targetMediaId = targetMediaId;
}
return $result;
}
@ -680,7 +680,7 @@ class EncryptedContent_MediaUpdate extends $pb.GeneratedMessage {
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'EncryptedContent.MediaUpdate', createEmptyInstance: create)
..e<EncryptedContent_MediaUpdate_Type>(1, _omitFieldNames ? '' : 'type', $pb.PbFieldType.OE, defaultOrMaker: EncryptedContent_MediaUpdate_Type.REOPENED, valueOf: EncryptedContent_MediaUpdate_Type.valueOf, enumValues: EncryptedContent_MediaUpdate_Type.values)
..aOS(2, _omitFieldNames ? '' : 'targetMessageId', protoName: 'targetMessageId')
..aOS(2, _omitFieldNames ? '' : 'targetMediaId', protoName: 'targetMediaId')
..hasRequiredFields = false
;
@ -715,13 +715,13 @@ class EncryptedContent_MediaUpdate extends $pb.GeneratedMessage {
void clearType() => clearField(1);
@$pb.TagNumber(2)
$core.String get targetMessageId => $_getSZ(1);
$core.String get targetMediaId => $_getSZ(1);
@$pb.TagNumber(2)
set targetMessageId($core.String v) { $_setString(1, v); }
set targetMediaId($core.String v) { $_setString(1, v); }
@$pb.TagNumber(2)
$core.bool hasTargetMessageId() => $_has(1);
$core.bool hasTargetMediaId() => $_has(1);
@$pb.TagNumber(2)
void clearTargetMessageId() => clearField(2);
void clearTargetMediaId() => clearField(2);
}
class EncryptedContent_ContactRequest extends $pb.GeneratedMessage {
@ -1025,8 +1025,8 @@ class EncryptedContent_FlameSync extends $pb.GeneratedMessage {
class EncryptedContent extends $pb.GeneratedMessage {
factory EncryptedContent({
$core.String? groupId,
$core.bool? isDirectChat,
$fixnum.Int64? senderProfileCounter,
EncryptedContent_TextMessage? textMessage,
EncryptedContent_MessageUpdate? messageUpdate,
EncryptedContent_Media? media,
EncryptedContent_MediaUpdate? mediaUpdate,
@ -1035,17 +1035,18 @@ class EncryptedContent extends $pb.GeneratedMessage {
EncryptedContent_FlameSync? flameSync,
EncryptedContent_PushKeys? pushKeys,
EncryptedContent_Reaction? reaction,
EncryptedContent_TextMessage? textMessage,
}) {
final $result = create();
if (groupId != null) {
$result.groupId = groupId;
}
if (isDirectChat != null) {
$result.isDirectChat = isDirectChat;
}
if (senderProfileCounter != null) {
$result.senderProfileCounter = senderProfileCounter;
}
if (textMessage != null) {
$result.textMessage = textMessage;
}
if (messageUpdate != null) {
$result.messageUpdate = messageUpdate;
}
@ -1070,6 +1071,9 @@ class EncryptedContent extends $pb.GeneratedMessage {
if (reaction != null) {
$result.reaction = reaction;
}
if (textMessage != null) {
$result.textMessage = textMessage;
}
return $result;
}
EncryptedContent._() : super();
@ -1078,8 +1082,8 @@ class EncryptedContent extends $pb.GeneratedMessage {
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'EncryptedContent', createEmptyInstance: create)
..aOS(2, _omitFieldNames ? '' : 'groupId', protoName: 'groupId')
..aInt64(3, _omitFieldNames ? '' : 'senderProfileCounter', protoName: 'senderProfileCounter')
..aOM<EncryptedContent_TextMessage>(4, _omitFieldNames ? '' : 'textMessage', protoName: 'textMessage', subBuilder: EncryptedContent_TextMessage.create)
..aOB(3, _omitFieldNames ? '' : 'isDirectChat', protoName: 'isDirectChat')
..aInt64(4, _omitFieldNames ? '' : 'senderProfileCounter', protoName: 'senderProfileCounter')
..aOM<EncryptedContent_MessageUpdate>(5, _omitFieldNames ? '' : 'messageUpdate', protoName: 'messageUpdate', subBuilder: EncryptedContent_MessageUpdate.create)
..aOM<EncryptedContent_Media>(6, _omitFieldNames ? '' : 'media', subBuilder: EncryptedContent_Media.create)
..aOM<EncryptedContent_MediaUpdate>(7, _omitFieldNames ? '' : 'mediaUpdate', protoName: 'mediaUpdate', subBuilder: EncryptedContent_MediaUpdate.create)
@ -1088,6 +1092,7 @@ class EncryptedContent extends $pb.GeneratedMessage {
..aOM<EncryptedContent_FlameSync>(10, _omitFieldNames ? '' : 'flameSync', protoName: 'flameSync', subBuilder: EncryptedContent_FlameSync.create)
..aOM<EncryptedContent_PushKeys>(11, _omitFieldNames ? '' : 'pushKeys', protoName: 'pushKeys', subBuilder: EncryptedContent_PushKeys.create)
..aOM<EncryptedContent_Reaction>(12, _omitFieldNames ? '' : 'reaction', subBuilder: EncryptedContent_Reaction.create)
..aOM<EncryptedContent_TextMessage>(13, _omitFieldNames ? '' : 'textMessage', protoName: 'textMessage', subBuilder: EncryptedContent_TextMessage.create)
..hasRequiredFields = false
;
@ -1121,26 +1126,24 @@ class EncryptedContent extends $pb.GeneratedMessage {
@$pb.TagNumber(2)
void clearGroupId() => clearField(2);
/// / This can be added, so the receiver can check weather he is up to date with the current profile
@$pb.TagNumber(3)
$fixnum.Int64 get senderProfileCounter => $_getI64(1);
$core.bool get isDirectChat => $_getBF(1);
@$pb.TagNumber(3)
set senderProfileCounter($fixnum.Int64 v) { $_setInt64(1, v); }
set isDirectChat($core.bool v) { $_setBool(1, v); }
@$pb.TagNumber(3)
$core.bool hasSenderProfileCounter() => $_has(1);
$core.bool hasIsDirectChat() => $_has(1);
@$pb.TagNumber(3)
void clearSenderProfileCounter() => clearField(3);
void clearIsDirectChat() => clearField(3);
/// / This can be added, so the receiver can check weather he is up to date with the current profile
@$pb.TagNumber(4)
EncryptedContent_TextMessage get textMessage => $_getN(2);
$fixnum.Int64 get senderProfileCounter => $_getI64(2);
@$pb.TagNumber(4)
set textMessage(EncryptedContent_TextMessage v) { setField(4, v); }
set senderProfileCounter($fixnum.Int64 v) { $_setInt64(2, v); }
@$pb.TagNumber(4)
$core.bool hasTextMessage() => $_has(2);
$core.bool hasSenderProfileCounter() => $_has(2);
@$pb.TagNumber(4)
void clearTextMessage() => clearField(4);
@$pb.TagNumber(4)
EncryptedContent_TextMessage ensureTextMessage() => $_ensure(2);
void clearSenderProfileCounter() => clearField(4);
@$pb.TagNumber(5)
EncryptedContent_MessageUpdate get messageUpdate => $_getN(3);
@ -1229,6 +1232,17 @@ class EncryptedContent extends $pb.GeneratedMessage {
void clearReaction() => clearField(12);
@$pb.TagNumber(12)
EncryptedContent_Reaction ensureReaction() => $_ensure(10);
@$pb.TagNumber(13)
EncryptedContent_TextMessage get textMessage => $_getN(11);
@$pb.TagNumber(13)
set textMessage(EncryptedContent_TextMessage v) { setField(13, v); }
@$pb.TagNumber(13)
$core.bool hasTextMessage() => $_has(11);
@$pb.TagNumber(13)
void clearTextMessage() => clearField(13);
@$pb.TagNumber(13)
EncryptedContent_TextMessage ensureTextMessage() => $_ensure(11);
}

View file

@ -95,8 +95,8 @@ const EncryptedContent$json = {
'1': 'EncryptedContent',
'2': [
{'1': 'groupId', '3': 2, '4': 1, '5': 9, '9': 0, '10': 'groupId', '17': true},
{'1': 'senderProfileCounter', '3': 3, '4': 1, '5': 3, '9': 1, '10': 'senderProfileCounter', '17': true},
{'1': 'textMessage', '3': 4, '4': 1, '5': 11, '6': '.EncryptedContent.TextMessage', '9': 2, '10': 'textMessage', '17': true},
{'1': 'isDirectChat', '3': 3, '4': 1, '5': 8, '9': 1, '10': 'isDirectChat', '17': true},
{'1': 'senderProfileCounter', '3': 4, '4': 1, '5': 3, '9': 2, '10': 'senderProfileCounter', '17': true},
{'1': 'messageUpdate', '3': 5, '4': 1, '5': 11, '6': '.EncryptedContent.MessageUpdate', '9': 3, '10': 'messageUpdate', '17': true},
{'1': 'media', '3': 6, '4': 1, '5': 11, '6': '.EncryptedContent.Media', '9': 4, '10': 'media', '17': true},
{'1': 'mediaUpdate', '3': 7, '4': 1, '5': 11, '6': '.EncryptedContent.MediaUpdate', '9': 5, '10': 'mediaUpdate', '17': true},
@ -105,12 +105,13 @@ const EncryptedContent$json = {
{'1': 'flameSync', '3': 10, '4': 1, '5': 11, '6': '.EncryptedContent.FlameSync', '9': 8, '10': 'flameSync', '17': true},
{'1': 'pushKeys', '3': 11, '4': 1, '5': 11, '6': '.EncryptedContent.PushKeys', '9': 9, '10': 'pushKeys', '17': true},
{'1': 'reaction', '3': 12, '4': 1, '5': 11, '6': '.EncryptedContent.Reaction', '9': 10, '10': 'reaction', '17': true},
{'1': 'textMessage', '3': 13, '4': 1, '5': 11, '6': '.EncryptedContent.TextMessage', '9': 11, '10': 'textMessage', '17': true},
],
'3': [EncryptedContent_TextMessage$json, EncryptedContent_Reaction$json, EncryptedContent_MessageUpdate$json, EncryptedContent_Media$json, EncryptedContent_MediaUpdate$json, EncryptedContent_ContactRequest$json, EncryptedContent_ContactUpdate$json, EncryptedContent_PushKeys$json, EncryptedContent_FlameSync$json],
'8': [
{'1': '_groupId'},
{'1': '_isDirectChat'},
{'1': '_senderProfileCounter'},
{'1': '_textMessage'},
{'1': '_messageUpdate'},
{'1': '_media'},
{'1': '_mediaUpdate'},
@ -119,6 +120,7 @@ const EncryptedContent$json = {
{'1': '_flameSync'},
{'1': '_pushKeys'},
{'1': '_reaction'},
{'1': '_textMessage'},
],
};
@ -219,7 +221,7 @@ const EncryptedContent_MediaUpdate$json = {
'1': 'MediaUpdate',
'2': [
{'1': 'type', '3': 1, '4': 1, '5': 14, '6': '.EncryptedContent.MediaUpdate.Type', '10': 'type'},
{'1': 'targetMessageId', '3': 2, '4': 1, '5': 9, '10': 'targetMessageId'},
{'1': 'targetMediaId', '3': 2, '4': 1, '5': 9, '10': 'targetMediaId'},
],
'4': [EncryptedContent_MediaUpdate_Type$json],
};
@ -315,59 +317,60 @@ const EncryptedContent_FlameSync$json = {
/// Descriptor for `EncryptedContent`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List encryptedContentDescriptor = $convert.base64Decode(
'ChBFbmNyeXB0ZWRDb250ZW50Eh0KB2dyb3VwSWQYAiABKAlIAFIHZ3JvdXBJZIgBARI3ChRzZW'
'5kZXJQcm9maWxlQ291bnRlchgDIAEoA0gBUhRzZW5kZXJQcm9maWxlQ291bnRlcogBARJECgt0'
'ZXh0TWVzc2FnZRgEIAEoCzIdLkVuY3J5cHRlZENvbnRlbnQuVGV4dE1lc3NhZ2VIAlILdGV4dE'
'1lc3NhZ2WIAQESSgoNbWVzc2FnZVVwZGF0ZRgFIAEoCzIfLkVuY3J5cHRlZENvbnRlbnQuTWVz'
'c2FnZVVwZGF0ZUgDUg1tZXNzYWdlVXBkYXRliAEBEjIKBW1lZGlhGAYgASgLMhcuRW5jcnlwdG'
'VkQ29udGVudC5NZWRpYUgEUgVtZWRpYYgBARJECgttZWRpYVVwZGF0ZRgHIAEoCzIdLkVuY3J5'
'cHRlZENvbnRlbnQuTWVkaWFVcGRhdGVIBVILbWVkaWFVcGRhdGWIAQESSgoNY29udGFjdFVwZG'
'F0ZRgIIAEoCzIfLkVuY3J5cHRlZENvbnRlbnQuQ29udGFjdFVwZGF0ZUgGUg1jb250YWN0VXBk'
'YXRliAEBEk0KDmNvbnRhY3RSZXF1ZXN0GAkgASgLMiAuRW5jcnlwdGVkQ29udGVudC5Db250YW'
'N0UmVxdWVzdEgHUg5jb250YWN0UmVxdWVzdIgBARI+CglmbGFtZVN5bmMYCiABKAsyGy5FbmNy'
'eXB0ZWRDb250ZW50LkZsYW1lU3luY0gIUglmbGFtZVN5bmOIAQESOwoIcHVzaEtleXMYCyABKA'
'syGi5FbmNyeXB0ZWRDb250ZW50LlB1c2hLZXlzSAlSCHB1c2hLZXlziAEBEjsKCHJlYWN0aW9u'
'GAwgASgLMhouRW5jcnlwdGVkQ29udGVudC5SZWFjdGlvbkgKUghyZWFjdGlvbogBARqpAQoLVG'
'V4dE1lc3NhZ2USKAoPc2VuZGVyTWVzc2FnZUlkGAEgASgJUg9zZW5kZXJNZXNzYWdlSWQSEgoE'
'dGV4dBgCIAEoCVIEdGV4dBIcCgl0aW1lc3RhbXAYAyABKANSCXRpbWVzdGFtcBIrCg5xdW90ZU'
'1lc3NhZ2VJZBgEIAEoCUgAUg5xdW90ZU1lc3NhZ2VJZIgBAUIRCg9fcXVvdGVNZXNzYWdlSWQa'
'gQEKCFJlYWN0aW9uEigKD3RhcmdldE1lc3NhZ2VJZBgBIAEoCVIPdGFyZ2V0TWVzc2FnZUlkEh'
'kKBWVtb2ppGAIgASgJSABSBWVtb2ppiAEBEhsKBnJlbW92ZRgDIAEoCEgBUgZyZW1vdmWIAQFC'
'CAoGX2Vtb2ppQgkKB19yZW1vdmUatwIKDU1lc3NhZ2VVcGRhdGUSOAoEdHlwZRgBIAEoDjIkLk'
'VuY3J5cHRlZENvbnRlbnQuTWVzc2FnZVVwZGF0ZS5UeXBlUgR0eXBlEi0KD3NlbmRlck1lc3Nh'
'Z2VJZBgCIAEoCUgAUg9zZW5kZXJNZXNzYWdlSWSIAQESOgoYbXVsdGlwbGVTZW5kZXJNZXNzYW'
'dlSWRzGAMgAygJUhhtdWx0aXBsZVNlbmRlck1lc3NhZ2VJZHMSFwoEdGV4dBgEIAEoCUgBUgR0'
'ZXh0iAEBEhwKCXRpbWVzdGFtcBgFIAEoA1IJdGltZXN0YW1wIi0KBFR5cGUSCgoGREVMRVRFEA'
'ASDQoJRURJVF9URVhUEAESCgoGT1BFTkVEEAJCEgoQX3NlbmRlck1lc3NhZ2VJZEIHCgVfdGV4'
'dBqMBQoFTWVkaWESKAoPc2VuZGVyTWVzc2FnZUlkGAEgASgJUg9zZW5kZXJNZXNzYWdlSWQSMA'
'oEdHlwZRgCIAEoDjIcLkVuY3J5cHRlZENvbnRlbnQuTWVkaWEuVHlwZVIEdHlwZRJDChpkaXNw'
'bGF5TGltaXRJbk1pbGxpc2Vjb25kcxgDIAEoA0gAUhpkaXNwbGF5TGltaXRJbk1pbGxpc2Vjb2'
'5kc4gBARI2ChZyZXF1aXJlc0F1dGhlbnRpY2F0aW9uGAQgASgIUhZyZXF1aXJlc0F1dGhlbnRp'
'Y2F0aW9uEhwKCXRpbWVzdGFtcBgFIAEoA1IJdGltZXN0YW1wEisKDnF1b3RlTWVzc2FnZUlkGA'
'YgASgJSAFSDnF1b3RlTWVzc2FnZUlkiAEBEikKDWRvd25sb2FkVG9rZW4YByABKAxIAlINZG93'
'bmxvYWRUb2tlbogBARIpCg1lbmNyeXB0aW9uS2V5GAggASgMSANSDWVuY3J5cHRpb25LZXmIAQ'
'ESKQoNZW5jcnlwdGlvbk1hYxgJIAEoDEgEUg1lbmNyeXB0aW9uTWFjiAEBEi0KD2VuY3J5cHRp'
'b25Ob25jZRgKIAEoDEgFUg9lbmNyeXB0aW9uTm9uY2WIAQEiMwoEVHlwZRIMCghSRVVQTE9BRB'
'AAEgkKBUlNQUdFEAESCQoFVklERU8QAhIHCgNHSUYQA0IdChtfZGlzcGxheUxpbWl0SW5NaWxs'
'aXNlY29uZHNCEQoPX3F1b3RlTWVzc2FnZUlkQhAKDl9kb3dubG9hZFRva2VuQhAKDl9lbmNyeX'
'B0aW9uS2V5QhAKDl9lbmNyeXB0aW9uTWFjQhIKEF9lbmNyeXB0aW9uTm9uY2UapwEKC01lZGlh'
'VXBkYXRlEjYKBHR5cGUYASABKA4yIi5FbmNyeXB0ZWRDb250ZW50Lk1lZGlhVXBkYXRlLlR5cG'
'VSBHR5cGUSKAoPdGFyZ2V0TWVzc2FnZUlkGAIgASgJUg90YXJnZXRNZXNzYWdlSWQiNgoEVHlw'
'ZRIMCghSRU9QRU5FRBAAEgoKBlNUT1JFRBABEhQKEERFQ1JZUFRJT05fRVJST1IQAhp4Cg5Db2'
'50YWN0UmVxdWVzdBI5CgR0eXBlGAEgASgOMiUuRW5jcnlwdGVkQ29udGVudC5Db250YWN0UmVx'
'dWVzdC5UeXBlUgR0eXBlIisKBFR5cGUSCwoHUkVRVUVTVBAAEgoKBlJFSkVDVBABEgoKBkFDQ0'
'VQVBACGtIBCg1Db250YWN0VXBkYXRlEjgKBHR5cGUYASABKA4yJC5FbmNyeXB0ZWRDb250ZW50'
'LkNvbnRhY3RVcGRhdGUuVHlwZVIEdHlwZRIhCglhdmF0YXJTdmcYAiABKAlIAFIJYXZhdGFyU3'
'ZniAEBEiUKC2Rpc3BsYXlOYW1lGAMgASgJSAFSC2Rpc3BsYXlOYW1liAEBIh8KBFR5cGUSCwoH'
'UkVRVUVTVBAAEgoKBlVQREFURRABQgwKCl9hdmF0YXJTdmdCDgoMX2Rpc3BsYXlOYW1lGtUBCg'
'hQdXNoS2V5cxIzCgR0eXBlGAEgASgOMh8uRW5jcnlwdGVkQ29udGVudC5QdXNoS2V5cy5UeXBl'
'UgR0eXBlEhkKBWtleUlkGAIgASgDSABSBWtleUlkiAEBEhUKA2tleRgDIAEoDEgBUgNrZXmIAQ'
'ESIQoJY3JlYXRlZEF0GAQgASgDSAJSCWNyZWF0ZWRBdIgBASIfCgRUeXBlEgsKB1JFUVVFU1QQ'
'ABIKCgZVUERBVEUQAUIICgZfa2V5SWRCBgoEX2tleUIMCgpfY3JlYXRlZEF0GocBCglGbGFtZV'
'N5bmMSIgoMZmxhbWVDb3VudGVyGAEgASgDUgxmbGFtZUNvdW50ZXISNgoWbGFzdEZsYW1lQ291'
'bnRlckNoYW5nZRgCIAEoA1IWbGFzdEZsYW1lQ291bnRlckNoYW5nZRIeCgpiZXN0RnJpZW5kGA'
'MgASgIUgpiZXN0RnJpZW5kQgoKCF9ncm91cElkQhcKFV9zZW5kZXJQcm9maWxlQ291bnRlckIO'
'CgxfdGV4dE1lc3NhZ2VCEAoOX21lc3NhZ2VVcGRhdGVCCAoGX21lZGlhQg4KDF9tZWRpYVVwZG'
'F0ZUIQCg5fY29udGFjdFVwZGF0ZUIRCg9fY29udGFjdFJlcXVlc3RCDAoKX2ZsYW1lU3luY0IL'
'CglfcHVzaEtleXNCCwoJX3JlYWN0aW9u');
'ChBFbmNyeXB0ZWRDb250ZW50Eh0KB2dyb3VwSWQYAiABKAlIAFIHZ3JvdXBJZIgBARInCgxpc0'
'RpcmVjdENoYXQYAyABKAhIAVIMaXNEaXJlY3RDaGF0iAEBEjcKFHNlbmRlclByb2ZpbGVDb3Vu'
'dGVyGAQgASgDSAJSFHNlbmRlclByb2ZpbGVDb3VudGVyiAEBEkoKDW1lc3NhZ2VVcGRhdGUYBS'
'ABKAsyHy5FbmNyeXB0ZWRDb250ZW50Lk1lc3NhZ2VVcGRhdGVIA1INbWVzc2FnZVVwZGF0ZYgB'
'ARIyCgVtZWRpYRgGIAEoCzIXLkVuY3J5cHRlZENvbnRlbnQuTWVkaWFIBFIFbWVkaWGIAQESRA'
'oLbWVkaWFVcGRhdGUYByABKAsyHS5FbmNyeXB0ZWRDb250ZW50Lk1lZGlhVXBkYXRlSAVSC21l'
'ZGlhVXBkYXRliAEBEkoKDWNvbnRhY3RVcGRhdGUYCCABKAsyHy5FbmNyeXB0ZWRDb250ZW50Lk'
'NvbnRhY3RVcGRhdGVIBlINY29udGFjdFVwZGF0ZYgBARJNCg5jb250YWN0UmVxdWVzdBgJIAEo'
'CzIgLkVuY3J5cHRlZENvbnRlbnQuQ29udGFjdFJlcXVlc3RIB1IOY29udGFjdFJlcXVlc3SIAQ'
'ESPgoJZmxhbWVTeW5jGAogASgLMhsuRW5jcnlwdGVkQ29udGVudC5GbGFtZVN5bmNICFIJZmxh'
'bWVTeW5jiAEBEjsKCHB1c2hLZXlzGAsgASgLMhouRW5jcnlwdGVkQ29udGVudC5QdXNoS2V5c0'
'gJUghwdXNoS2V5c4gBARI7CghyZWFjdGlvbhgMIAEoCzIaLkVuY3J5cHRlZENvbnRlbnQuUmVh'
'Y3Rpb25IClIIcmVhY3Rpb26IAQESRAoLdGV4dE1lc3NhZ2UYDSABKAsyHS5FbmNyeXB0ZWRDb2'
'50ZW50LlRleHRNZXNzYWdlSAtSC3RleHRNZXNzYWdliAEBGqkBCgtUZXh0TWVzc2FnZRIoCg9z'
'ZW5kZXJNZXNzYWdlSWQYASABKAlSD3NlbmRlck1lc3NhZ2VJZBISCgR0ZXh0GAIgASgJUgR0ZX'
'h0EhwKCXRpbWVzdGFtcBgDIAEoA1IJdGltZXN0YW1wEisKDnF1b3RlTWVzc2FnZUlkGAQgASgJ'
'SABSDnF1b3RlTWVzc2FnZUlkiAEBQhEKD19xdW90ZU1lc3NhZ2VJZBqBAQoIUmVhY3Rpb24SKA'
'oPdGFyZ2V0TWVzc2FnZUlkGAEgASgJUg90YXJnZXRNZXNzYWdlSWQSGQoFZW1vamkYAiABKAlI'
'AFIFZW1vammIAQESGwoGcmVtb3ZlGAMgASgISAFSBnJlbW92ZYgBAUIICgZfZW1vamlCCQoHX3'
'JlbW92ZRq3AgoNTWVzc2FnZVVwZGF0ZRI4CgR0eXBlGAEgASgOMiQuRW5jcnlwdGVkQ29udGVu'
'dC5NZXNzYWdlVXBkYXRlLlR5cGVSBHR5cGUSLQoPc2VuZGVyTWVzc2FnZUlkGAIgASgJSABSD3'
'NlbmRlck1lc3NhZ2VJZIgBARI6ChhtdWx0aXBsZVNlbmRlck1lc3NhZ2VJZHMYAyADKAlSGG11'
'bHRpcGxlU2VuZGVyTWVzc2FnZUlkcxIXCgR0ZXh0GAQgASgJSAFSBHRleHSIAQESHAoJdGltZX'
'N0YW1wGAUgASgDUgl0aW1lc3RhbXAiLQoEVHlwZRIKCgZERUxFVEUQABINCglFRElUX1RFWFQQ'
'ARIKCgZPUEVORUQQAkISChBfc2VuZGVyTWVzc2FnZUlkQgcKBV90ZXh0GowFCgVNZWRpYRIoCg'
'9zZW5kZXJNZXNzYWdlSWQYASABKAlSD3NlbmRlck1lc3NhZ2VJZBIwCgR0eXBlGAIgASgOMhwu'
'RW5jcnlwdGVkQ29udGVudC5NZWRpYS5UeXBlUgR0eXBlEkMKGmRpc3BsYXlMaW1pdEluTWlsbG'
'lzZWNvbmRzGAMgASgDSABSGmRpc3BsYXlMaW1pdEluTWlsbGlzZWNvbmRziAEBEjYKFnJlcXVp'
'cmVzQXV0aGVudGljYXRpb24YBCABKAhSFnJlcXVpcmVzQXV0aGVudGljYXRpb24SHAoJdGltZX'
'N0YW1wGAUgASgDUgl0aW1lc3RhbXASKwoOcXVvdGVNZXNzYWdlSWQYBiABKAlIAVIOcXVvdGVN'
'ZXNzYWdlSWSIAQESKQoNZG93bmxvYWRUb2tlbhgHIAEoDEgCUg1kb3dubG9hZFRva2VuiAEBEi'
'kKDWVuY3J5cHRpb25LZXkYCCABKAxIA1INZW5jcnlwdGlvbktleYgBARIpCg1lbmNyeXB0aW9u'
'TWFjGAkgASgMSARSDWVuY3J5cHRpb25NYWOIAQESLQoPZW5jcnlwdGlvbk5vbmNlGAogASgMSA'
'VSD2VuY3J5cHRpb25Ob25jZYgBASIzCgRUeXBlEgwKCFJFVVBMT0FEEAASCQoFSU1BR0UQARIJ'
'CgVWSURFTxACEgcKA0dJRhADQh0KG19kaXNwbGF5TGltaXRJbk1pbGxpc2Vjb25kc0IRCg9fcX'
'VvdGVNZXNzYWdlSWRCEAoOX2Rvd25sb2FkVG9rZW5CEAoOX2VuY3J5cHRpb25LZXlCEAoOX2Vu'
'Y3J5cHRpb25NYWNCEgoQX2VuY3J5cHRpb25Ob25jZRqjAQoLTWVkaWFVcGRhdGUSNgoEdHlwZR'
'gBIAEoDjIiLkVuY3J5cHRlZENvbnRlbnQuTWVkaWFVcGRhdGUuVHlwZVIEdHlwZRIkCg10YXJn'
'ZXRNZWRpYUlkGAIgASgJUg10YXJnZXRNZWRpYUlkIjYKBFR5cGUSDAoIUkVPUEVORUQQABIKCg'
'ZTVE9SRUQQARIUChBERUNSWVBUSU9OX0VSUk9SEAIaeAoOQ29udGFjdFJlcXVlc3QSOQoEdHlw'
'ZRgBIAEoDjIlLkVuY3J5cHRlZENvbnRlbnQuQ29udGFjdFJlcXVlc3QuVHlwZVIEdHlwZSIrCg'
'RUeXBlEgsKB1JFUVVFU1QQABIKCgZSRUpFQ1QQARIKCgZBQ0NFUFQQAhrSAQoNQ29udGFjdFVw'
'ZGF0ZRI4CgR0eXBlGAEgASgOMiQuRW5jcnlwdGVkQ29udGVudC5Db250YWN0VXBkYXRlLlR5cG'
'VSBHR5cGUSIQoJYXZhdGFyU3ZnGAIgASgJSABSCWF2YXRhclN2Z4gBARIlCgtkaXNwbGF5TmFt'
'ZRgDIAEoCUgBUgtkaXNwbGF5TmFtZYgBASIfCgRUeXBlEgsKB1JFUVVFU1QQABIKCgZVUERBVE'
'UQAUIMCgpfYXZhdGFyU3ZnQg4KDF9kaXNwbGF5TmFtZRrVAQoIUHVzaEtleXMSMwoEdHlwZRgB'
'IAEoDjIfLkVuY3J5cHRlZENvbnRlbnQuUHVzaEtleXMuVHlwZVIEdHlwZRIZCgVrZXlJZBgCIA'
'EoA0gAUgVrZXlJZIgBARIVCgNrZXkYAyABKAxIAVIDa2V5iAEBEiEKCWNyZWF0ZWRBdBgEIAEo'
'A0gCUgljcmVhdGVkQXSIAQEiHwoEVHlwZRILCgdSRVFVRVNUEAASCgoGVVBEQVRFEAFCCAoGX2'
'tleUlkQgYKBF9rZXlCDAoKX2NyZWF0ZWRBdBqHAQoJRmxhbWVTeW5jEiIKDGZsYW1lQ291bnRl'
'chgBIAEoA1IMZmxhbWVDb3VudGVyEjYKFmxhc3RGbGFtZUNvdW50ZXJDaGFuZ2UYAiABKANSFm'
'xhc3RGbGFtZUNvdW50ZXJDaGFuZ2USHgoKYmVzdEZyaWVuZBgDIAEoCFIKYmVzdEZyaWVuZEIK'
'CghfZ3JvdXBJZEIPCg1faXNEaXJlY3RDaGF0QhcKFV9zZW5kZXJQcm9maWxlQ291bnRlckIQCg'
'5fbWVzc2FnZVVwZGF0ZUIICgZfbWVkaWFCDgoMX21lZGlhVXBkYXRlQhAKDl9jb250YWN0VXBk'
'YXRlQhEKD19jb250YWN0UmVxdWVzdEIMCgpfZmxhbWVTeW5jQgsKCV9wdXNoS2V5c0ILCglfcm'
'VhY3Rpb25CDgoMX3RleHRNZXNzYWdl');

View file

@ -30,11 +30,11 @@ message PlaintextContent {
message EncryptedContent {
optional string groupId = 2;
optional bool isDirectChat = 3;
/// This can be added, so the receiver can check weather he is up to date with the current profile
optional int64 senderProfileCounter = 3;
optional int64 senderProfileCounter = 4;
optional TextMessage textMessage = 4;
optional MessageUpdate messageUpdate = 5;
optional Media media = 6;
optional MediaUpdate mediaUpdate = 7;
@ -43,6 +43,7 @@ message EncryptedContent {
optional FlameSync flameSync = 10;
optional PushKeys pushKeys = 11;
optional Reaction reaction = 12;
optional TextMessage textMessage = 13;
message TextMessage {
string senderMessageId = 1;
@ -98,7 +99,7 @@ message EncryptedContent {
DECRYPTION_ERROR = 2;
}
Type type = 1;
string targetMessageId = 2;
string targetMediaId = 2;
}
message ContactRequest {

View file

@ -207,7 +207,8 @@ Future<void> requestMediaReupload(String mediaId) async {
final messages = await twonlyDB.messagesDao.getMessagesByMediaId(mediaId);
if (messages.length != 1 || messages.first.senderId == null) {
Log.error(
'Media file has none or more than one sender. That is not possible');
'Media file has none or more than one sender. That is not possible',
);
return;
}
@ -216,7 +217,7 @@ Future<void> requestMediaReupload(String mediaId) async {
EncryptedContent(
mediaUpdate: EncryptedContent_MediaUpdate(
type: EncryptedContent_MediaUpdate_Type.DECRYPTION_ERROR,
targetMessageId: mediaId,
targetMediaId: mediaId,
),
),
);

View file

@ -119,6 +119,15 @@ Future<void> _createUploadRequest(MediaFileService media) async {
for (final message in messages) {
final groupMembers =
await twonlyDB.groupsDao.getGroupMembers(message.groupId);
if (media.mediaFile.reuploadRequestedBy == null) {
await twonlyDB.groupsDao.incFlameCounter(
message.groupId,
false,
message.createdAt,
);
}
for (final groupMember in groupMembers) {
/// only send the upload to the users
if (media.mediaFile.reuploadRequestedBy != null) {
@ -128,12 +137,6 @@ Future<void> _createUploadRequest(MediaFileService media) async {
}
}
await twonlyDB.contactsDao.incFlameCounter(
groupMember.contactId,
false,
message.createdAt,
);
final downloadToken = getRandomUint8List(32);
var type = EncryptedContent_Media_Type.IMAGE;
@ -169,7 +172,8 @@ Future<void> _createUploadRequest(MediaFileService media) async {
if (cipherText == null) {
Log.error(
'Could not generate ciphertext message for ${groupMember.contactId}');
'Could not generate ciphertext message for ${groupMember.contactId}',
);
}
final messageOnSuccess = TextMessage()

View file

@ -161,11 +161,13 @@ Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({
Future<void> insertAndSendTextMessage(
String groupId,
String textMessage,
String? quotesMessageId,
) async {
final message = await twonlyDB.messagesDao.insertMessage(
MessagesCompanion(
groupId: Value(groupId),
content: Value(textMessage),
quotesMessageId: Value(quotesMessageId),
),
);
if (message == null) {
@ -173,19 +175,40 @@ Future<void> insertAndSendTextMessage(
return;
}
final encryptedContent = pb.EncryptedContent(
textMessage: pb.EncryptedContent_TextMessage(
senderMessageId: message.messageId,
text: textMessage,
timestamp: Int64(message.createdAt.millisecondsSinceEpoch),
),
);
if (quotesMessageId != null) {
encryptedContent.textMessage.quoteMessageId = quotesMessageId;
}
await sendCipherTextToGroup(groupId, encryptedContent);
}
Future<void> sendCipherTextToGroup(
String groupId,
pb.EncryptedContent encryptedContent,
) async {
final groupMembers = await twonlyDB.groupsDao.getGroupMembers(groupId);
final group = await twonlyDB.groupsDao.getGroup(groupId);
if (group == null) return;
encryptedContent
..groupId = groupId
..isDirectChat = group.isDirectChat;
for (final groupMember in groupMembers) {
unawaited(sendCipherText(
groupMember.contactId,
pb.EncryptedContent(
textMessage: pb.EncryptedContent_TextMessage(
senderMessageId: message.messageId,
text: textMessage,
timestamp: Int64(message.createdAt.millisecondsSinceEpoch),
),
unawaited(
sendCipherText(
groupMember.contactId,
encryptedContent,
),
));
);
}
}

View file

@ -82,24 +82,22 @@ Future<void> handleFlameSync(
EncryptedContent_FlameSync flameSync,
) async {
Log.info('Got a flameSync from $contactId');
final contact = await twonlyDB.contactsDao
.getContactByUserId(contactId)
.getSingleOrNull();
if (contact == null || contact.lastFlameCounterChange != null) return;
final group = await twonlyDB.groupsDao.getDirectChat(contactId);
if (group == null || group.lastFlameCounterChange != null) return;
var updates = ContactsCompanion(
var updates = GroupsCompanion(
alsoBestFriend: Value(flameSync.bestFriend),
);
if (isToday(contact.lastFlameCounterChange!) &&
if (isToday(group.lastFlameCounterChange!) &&
isToday(fromTimestamp(flameSync.lastFlameCounterChange))) {
if (flameSync.flameCounter > contact.flameCounter) {
updates = ContactsCompanion(
if (flameSync.flameCounter > group.flameCounter) {
updates = GroupsCompanion(
flameCounter: Value(flameSync.flameCounter.toInt()),
);
}
}
await twonlyDB.contactsDao.updateContact(contactId, updates);
await twonlyDB.groupsDao.updateGroup(group.groupId, updates);
}
Future<int?> checkForProfileUpdate(

View file

@ -100,8 +100,8 @@ Future<void> handleMedia(
);
if (message != null) {
Log.info('Inserted a new media message with ID: ${message.messageId}');
await twonlyDB.contactsDao.incFlameCounter(
fromUserId,
await twonlyDB.groupsDao.incFlameCounter(
message.groupId,
true,
fromTimestamp(media.timestamp),
);
@ -115,15 +115,17 @@ Future<void> handleMediaUpdate(
String groupId,
EncryptedContent_MediaUpdate mediaUpdate,
) async {
final message = await twonlyDB.messagesDao
.getMessageById(mediaUpdate.targetMessageId)
.getSingleOrNull();
if (message == null || message.mediaId == null) return;
final messages = await twonlyDB.messagesDao
.getMessagesByMediaId(mediaUpdate.targetMediaId);
if (messages.length != 1) return;
final message = messages.first;
if (message.senderId != fromUserId) return;
final mediaFile =
await twonlyDB.mediaFilesDao.getMediaFileById(message.mediaId!);
if (mediaFile == null) {
Log.info(
'Got media file update, but media file was not found ${message.mediaId}');
'Got media file update, but media file was not found ${message.mediaId}',
);
return;
}

View file

@ -10,7 +10,8 @@ Future<void> handleMessageUpdate(
switch (messageUpdate.type) {
case EncryptedContent_MessageUpdate_Type.OPENED:
Log.info(
'Opened message ${messageUpdate.multipleSenderMessageIds.length}');
'Opened message ${messageUpdate.multipleSenderMessageIds.length}',
);
for (final senderMessageId in messageUpdate.multipleSenderMessageIds) {
await twonlyDB.messagesDao.handleMessageOpened(
contactId,

View file

@ -88,7 +88,7 @@ Future<void> handleMediaError(MediaFile media) async {
EncryptedContent(
mediaUpdate: EncryptedContent_MediaUpdate(
type: EncryptedContent_MediaUpdate_Type.DECRYPTION_ERROR,
targetMessageId: message.messageId,
targetMediaId: message.mediaId,
),
),
);

View file

@ -2,7 +2,7 @@ import 'package:collection/collection.dart';
import 'package:drift/drift.dart';
import 'package:fixnum/fixnum.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/database/daos/groups.dao.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
import 'package:twonly/src/services/api/messages.dart';
@ -10,49 +10,52 @@ import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart';
Future<void> syncFlameCounters() async {
final user = await getUser();
if (user == null) return;
final contacts = await twonlyDB.contactsDao.getAllNotBlockedContacts();
if (contacts.isEmpty) return;
final maxMessageCounter = contacts.map((x) => x.totalMediaCounter).max;
final groups = await twonlyDB.groupsDao.getAllDirectChats();
if (groups.isEmpty) return;
final maxMessageCounter = groups.map((x) => x.totalMediaCounter).max;
final bestFriend =
contacts.firstWhere((x) => x.totalMediaCounter == maxMessageCounter);
groups.firstWhere((x) => x.totalMediaCounter == maxMessageCounter);
if (user.myBestFriendContactId != bestFriend.userId) {
if (gUser.myBestFriendGroupId != bestFriend.groupId) {
await updateUserdata((user) {
user.myBestFriendContactId = bestFriend.userId;
user.myBestFriendGroupId = bestFriend.groupId;
return user;
});
}
for (final contact in contacts) {
if (contact.lastFlameCounterChange == null || contact.deleted) continue;
if (!isToday(contact.lastFlameCounterChange!)) continue;
if (contact.lastFlameSync != null) {
if (isToday(contact.lastFlameSync!)) continue;
for (final group in groups) {
if (group.lastFlameCounterChange == null) continue;
if (!isToday(group.lastFlameCounterChange!)) continue;
if (group.lastFlameSync != null) {
if (isToday(group.lastFlameSync!)) continue;
}
final flameCounter = getFlameCounterFromContact(contact) - 1;
final flameCounter = getFlameCounterFromGroup(group) - 1;
// only sync when flame counter is higher than three days
if (flameCounter < 1 && bestFriend.userId != contact.userId) continue;
// only sync when flame counter is higher than three days or when they are bestFriends
if (flameCounter < 1 && bestFriend.groupId != group.groupId) continue;
final groupMembers =
await twonlyDB.groupsDao.getGroupMembers(group.groupId);
if (groupMembers.length != 1) {
continue; // flame sync is only done for groups of two
}
await sendCipherText(
contact.userId,
groupMembers.first.contactId,
EncryptedContent(
flameSync: EncryptedContent_FlameSync(
flameCounter: Int64(flameCounter),
lastFlameCounterChange:
Int64(contact.lastFlameCounterChange!.millisecondsSinceEpoch),
bestFriend: contact.userId == bestFriend.userId,
Int64(group.lastFlameCounterChange!.millisecondsSinceEpoch),
bestFriend: group.groupId == bestFriend.groupId,
),
),
);
await twonlyDB.contactsDao.updateContact(
contact.userId,
ContactsCompanion(
await twonlyDB.groupsDao.updateGroup(
group.groupId,
GroupsCompanion(
lastFlameSync: Value(DateTime.now()),
),
);

View file

@ -119,7 +119,7 @@ class MediaFileService {
originalPath,
storedPath,
thumbnailPath,
uploadRequestPath
uploadRequestPath,
];
for (final path in pathsToRemove) {
@ -146,11 +146,13 @@ class MediaFileService {
String namePrefix = '',
String extensionParam = '',
}) {
final mediaBaseDir = Directory(join(
applicationSupportDirectory.path,
'mediafiles',
directory,
));
final mediaBaseDir = Directory(
join(
applicationSupportDirectory.path,
'mediafiles',
directory,
),
);
if (!mediaBaseDir.existsSync()) {
mediaBaseDir.createSync(recursive: true);
}

View file

@ -1,5 +1,4 @@
import 'dart:math';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_image_compress/flutter_image_compress.dart';
@ -8,11 +7,14 @@ import 'package:intl/intl.dart';
import 'package:local_auth/local_auth.dart';
import 'package:pie_menu/pie_menu.dart';
import 'package:provider/provider.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/database/tables/messages.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/localization/generated/app_localizations.dart';
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
import 'package:twonly/src/providers/settings.provider.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart';
extension ShortCutsExtension on BuildContext {
AppLocalizations get lang => AppLocalizations.of(this)!;
@ -284,3 +286,28 @@ PieTheme getPieCanvasTheme(BuildContext context) {
),
);
}
Color getMessageColorFromType(
Message message,
MediaFile? mediaFile,
BuildContext context,
) {
Color color;
if (message.type == MessageType.text) {
color = Colors.blueAccent;
} else if (mediaFile != null) {
if (mediaFile.requiresAuthentication) {
color = context.color.primary;
} else {
if (mediaFile.type == MediaType.video) {
color = const Color.fromARGB(255, 243, 33, 208);
} else {
color = Colors.redAccent;
}
}
} else {
return (isDarkMode(context)) ? Colors.white : Colors.black;
}
return color;
}

View file

@ -1,37 +1,31 @@
// ignore_for_file: strict_raw_type
import 'dart:collection';
import 'package:flutter/material.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/components/avatar_icon.component.dart';
import 'package:twonly/src/views/components/flame.dart';
import 'package:twonly/src/views/components/headline.dart';
import 'package:twonly/src/views/components/initialsavatar.dart';
class BestFriendsSelector extends StatelessWidget {
const BestFriendsSelector({
required this.users,
required this.isRealTwonly,
required this.updateStatus,
required this.selectedUserIds,
required this.groups,
required this.selectedGroupIds,
required this.updateSelectedGroupIds,
required this.title,
required this.showSelectAll,
super.key,
});
final List<Contact> users;
final void Function(int, bool) updateStatus;
final HashSet<int> selectedUserIds;
final bool isRealTwonly;
final List<Group> groups;
final HashSet<String> selectedGroupIds;
final void Function(String, bool) updateSelectedGroupIds;
final String title;
final bool showSelectAll;
@override
Widget build(BuildContext context) {
if (users.isEmpty) {
if (groups.isEmpty) {
return Container();
}
return Column(
children: [
Row(
@ -39,11 +33,11 @@ class BestFriendsSelector extends StatelessWidget {
Expanded(
child: HeadLineComponent(title),
),
if (!isRealTwonly)
if (showSelectAll)
GestureDetector(
onTap: () {
for (final user in users) {
updateStatus(user.userId, true);
for (final group in groups) {
updateSelectedGroupIds(group.groupId, true);
}
},
child: Container(
@ -70,7 +64,7 @@ class BestFriendsSelector extends StatelessWidget {
Column(
spacing: 8,
children: List.generate(
(users.length + 1) ~/ 2,
(groups.length + 1) ~/ 2,
(rowIndex) {
final firstUserIndex = rowIndex * 2;
final secondUserIndex = firstUserIndex + 1;
@ -79,21 +73,19 @@ class BestFriendsSelector extends StatelessWidget {
children: [
Expanded(
child: UserCheckbox(
isChecked: selectedUserIds
.contains(users[firstUserIndex].userId),
user: users[firstUserIndex],
onChanged: updateStatus,
isRealTwonly: isRealTwonly,
isChecked: selectedGroupIds
.contains(groups[firstUserIndex].groupId),
group: groups[firstUserIndex],
onChanged: updateSelectedGroupIds,
),
),
if (secondUserIndex < users.length)
if (secondUserIndex < groups.length)
Expanded(
child: UserCheckbox(
isChecked: selectedUserIds
.contains(users[secondUserIndex].userId),
user: users[secondUserIndex],
onChanged: updateStatus,
isRealTwonly: isRealTwonly,
isChecked: selectedGroupIds
.contains(groups[secondUserIndex].groupId),
group: groups[secondUserIndex],
onChanged: updateSelectedGroupIds,
),
)
else
@ -112,28 +104,24 @@ class BestFriendsSelector extends StatelessWidget {
class UserCheckbox extends StatelessWidget {
const UserCheckbox({
required this.user,
required this.group,
required this.onChanged,
required this.isRealTwonly,
required this.isChecked,
super.key,
});
final Contact user;
final void Function(int, bool) onChanged;
final Group group;
final void Function(String, bool) onChanged;
final bool isChecked;
final bool isRealTwonly;
@override
Widget build(BuildContext context) {
final displayName = getContactDisplayName(user);
return Container(
padding: const EdgeInsets.symmetric(
horizontal: 3,
), // Padding inside the container
child: GestureDetector(
onTap: () {
onChanged(user.userId, !isChecked);
onChanged(group.groupId, !isChecked);
},
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10),
@ -149,8 +137,8 @@ class UserCheckbox extends StatelessWidget {
),
child: Row(
children: [
ContactAvatar(
contact: user,
AvatarIcon(
group: group,
fontSize: 12,
),
const SizedBox(width: 8),
@ -160,28 +148,21 @@ class UserCheckbox extends StatelessWidget {
Row(
children: [
Text(
displayName.length > 8
? '${displayName.substring(0, 8)}...'
: displayName,
group.groupName.length > 12
? '${group.groupName.substring(0, 9)}...'
: group.groupName,
overflow: TextOverflow.ellipsis,
),
],
),
StreamBuilder(
stream: twonlyDB.contactsDao.watchFlameCounter(user.userId),
builder: (context, snapshot) {
if (!snapshot.hasData || snapshot.data! == 0) {
return Container();
}
return FlameCounterWidget(user, snapshot.data!);
},
),
FlameCounterWidget(groupId: group.groupId),
],
),
Expanded(child: Container()),
Checkbox(
value: isChecked,
side: WidgetStateBorderSide.resolveWith(
// ignore: strict_raw_type
(Set states) {
if (states.contains(WidgetState.selected)) {
return const BorderSide(width: 0);
@ -192,7 +173,7 @@ class UserCheckbox extends StatelessWidget {
},
),
onChanged: (bool? value) {
onChanged(user.userId, value ?? false);
onChanged(group.groupId, value ?? false);
},
),
],

View file

@ -186,7 +186,8 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
onPressed: () async {
if (media.type != MediaType.video) {
await mediaService.setDisplayLimit(
(media.displayLimitInMilliseconds == null) ? 0 : null);
(media.displayLimitInMilliseconds == null) ? 0 : null,
);
if (!mounted) return;
setState(() {});
return;
@ -465,8 +466,9 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
: const FaIcon(FontAwesomeIcons.solidPaperPlane),
onPressed: () async {
if (sendingOrLoadingImage) return;
if (widget.sendToGroup == null)
if (widget.sendToGroup == null) {
return pushShareImageView();
}
await sendImageToSinglePerson();
},
style: ButtonStyle(

View file

@ -2,23 +2,19 @@
import 'dart:async';
import 'dart:collection';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/database/daos/groups.dao.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/services/api/mediafiles/upload.service.dart';
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/camera/share_image_components/best_friends_selector.dart';
import 'package:twonly/src/views/components/avatar_icon.component.dart';
import 'package:twonly/src/views/components/flame.dart';
import 'package:twonly/src/views/components/headline.dart';
import 'package:twonly/src/views/components/initialsavatar.dart';
import 'package:twonly/src/views/components/verified_shield.dart';
import 'package:twonly/src/views/settings/subscription/subscription.view.dart';
class ShareImageView extends StatefulWidget {
const ShareImageView({
@ -38,28 +34,27 @@ class ShareImageView extends StatefulWidget {
}
class _ShareImageView extends State<ShareImageView> {
List<Contact> contacts = [];
List<Contact> _otherUsers = [];
List<Contact> _bestFriends = [];
List<Contact> _pinnedContacts = [];
Uint8List? imageBytes;
List<Group> contacts = [];
List<Group> _otherUsers = [];
List<Group> _bestFriends = [];
List<Group> _pinnedContacts = [];
bool sendingImage = false;
bool mediaStoreFutureReady = false;
bool hideArchivedUsers = true;
final TextEditingController searchUserName = TextEditingController();
late StreamSubscription<List<Contact>> contactSub;
late StreamSubscription<List<Group>> allGroupSub;
String lastQuery = '';
@override
void initState() {
super.initState();
final allContacts = twonlyDB.contactsDao.watchContactsForShareView();
contactSub = allContacts.listen((allContacts) async {
allGroupSub = twonlyDB.groupsDao.watchGroups().listen((allGroups) async {
setState(() {
contacts = allContacts;
contacts = allGroups;
});
await updateUsers(allContacts.where((x) => !x.archived).toList());
await updateGroups(allGroups.where((x) => !x.archived).toList());
});
unawaited(initAsync());
@ -69,6 +64,7 @@ class _ShareImageView extends State<ShareImageView> {
if (widget.mediaStoreFuture != null) {
await widget.mediaStoreFuture;
}
mediaStoreFutureReady = true;
await widget.mediaFileService.setUploadState(UploadState.preprocessing);
unawaited(startBackgroundMediaUpload(widget.mediaFileService));
if (!mounted) return;
@ -77,16 +73,17 @@ class _ShareImageView extends State<ShareImageView> {
@override
void dispose() {
unawaited(contactSub.cancel());
unawaited(allGroupSub.cancel());
super.dispose();
}
Future<void> updateUsers(List<Contact> users) async {
Future<void> updateGroups(List<Group> groups) async {
// Sort contacts by flameCounter and then by totalMediaCounter
users.sort((a, b) {
groups.sort((a, b) {
// First, compare by flameCounter
final flameComparison = getFlameCounterFromContact(b)
.compareTo(getFlameCounterFromContact(a));
final flameComparison =
getFlameCounterFromGroup(b).compareTo(getFlameCounterFromGroup(a));
if (flameComparison != 0) {
return flameComparison; // Sort by flameCounter in descending order
}
@ -97,18 +94,18 @@ class _ShareImageView extends State<ShareImageView> {
});
// Separate best friends and other users
final bestFriends = <Contact>[];
final otherUsers = <Contact>[];
final pinnedContacts = users.where((c) => c.pinned).toList();
final bestFriends = <Group>[];
final otherUsers = <Group>[];
final pinnedContacts = groups.where((c) => c.pinned).toList();
for (final contact in users) {
if (contact.pinned) continue;
if (!contact.archived &&
(getFlameCounterFromContact(contact)) > 0 &&
for (final group in groups) {
if (group.pinned) continue;
if (!group.archived &&
(getFlameCounterFromGroup(group)) > 0 &&
bestFriends.length < 6) {
bestFriends.add(contact);
bestFriends.add(group);
} else {
otherUsers.add(contact);
otherUsers.add(group);
}
}
@ -122,13 +119,13 @@ class _ShareImageView extends State<ShareImageView> {
Future<void> _filterUsers(String query) async {
lastQuery = query;
if (query.isEmpty) {
await updateUsers(
await updateGroups(
contacts
.where(
(x) =>
!x.archived ||
!hideArchivedUsers ||
widget.selectedUserIds.contains(x.userId),
widget.selectedGroupIds.contains(x.groupId),
)
.toList(),
);
@ -136,16 +133,14 @@ class _ShareImageView extends State<ShareImageView> {
}
final usersFiltered = contacts
.where(
(user) => getContactDisplayName(user)
.toLowerCase()
.contains(query.toLowerCase()),
(user) => user.groupName.toLowerCase().contains(query.toLowerCase()),
)
.toList();
await updateUsers(usersFiltered);
await updateGroups(usersFiltered);
}
void updateStatus(int userId, bool checked) {
widget.updateStatus(userId, checked);
void updateSelectedGroupIds(String groupId, bool checked) {
widget.updateSelectedGroupIds(groupId, checked);
setState(() {});
}
@ -173,19 +168,21 @@ class _ShareImageView extends State<ShareImageView> {
),
if (_pinnedContacts.isNotEmpty) const SizedBox(height: 10),
BestFriendsSelector(
users: _pinnedContacts,
selectedUserIds: widget.selectedUserIds,
isRealTwonly: widget.isRealTwonly,
updateStatus: updateStatus,
groups: _pinnedContacts,
selectedGroupIds: widget.selectedGroupIds,
updateSelectedGroupIds: updateSelectedGroupIds,
title: context.lang.shareImagePinnedContacts,
showSelectAll:
!widget.mediaFileService.mediaFile.requiresAuthentication,
),
const SizedBox(height: 10),
BestFriendsSelector(
users: _bestFriends,
selectedUserIds: widget.selectedUserIds,
isRealTwonly: widget.isRealTwonly,
updateStatus: updateStatus,
groups: _bestFriends,
selectedGroupIds: widget.selectedGroupIds,
updateSelectedGroupIds: updateSelectedGroupIds,
title: context.lang.shareImageBestFriends,
showSelectAll:
!widget.mediaFileService.mediaFile.requiresAuthentication,
),
const SizedBox(height: 10),
if (_otherUsers.isNotEmpty)
@ -229,9 +226,8 @@ class _ShareImageView extends State<ShareImageView> {
Expanded(
child: UserList(
List.from(_otherUsers),
selectedUserIds: widget.selectedUserIds,
isRealTwonly: widget.isRealTwonly,
updateStatus: updateStatus,
selectedGroupIds: widget.selectedGroupIds,
updateSelectedGroupIds: updateSelectedGroupIds,
),
),
],
@ -246,7 +242,7 @@ class _ShareImageView extends State<ShareImageView> {
mainAxisAlignment: MainAxisAlignment.end,
children: [
FilledButton.icon(
icon: imageBytes == null || sendingImage
icon: !mediaStoreFutureReady || sendingImage
? SizedBox(
height: 12,
width: 12,
@ -257,50 +253,28 @@ class _ShareImageView extends State<ShareImageView> {
)
: const FaIcon(FontAwesomeIcons.solidPaperPlane),
onPressed: () async {
if (imageBytes == null || widget.selectedUserIds.isEmpty) {
if (!mediaStoreFutureReady ||
widget.selectedGroupIds.isEmpty) {
return;
}
final err = await isAllowedToSend();
if (!context.mounted) return;
setState(() {
sendingImage = true;
});
if (err != null) {
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return SubscriptionView(
redirectError: err,
);
},
),
);
} else {
setState(() {
sendingImage = true;
});
await insertMediaFileInMessagesTable(
widget.mediaFileService,
widget.selectedGroupIds.toList(),
);
await finalizeUpload(
widget.mediaUploadId,
widget.selectedUserIds.toList(),
widget.isRealTwonly,
widget.videoUploadHandler != null,
widget.mirrorVideo,
widget.maxShowTime,
);
/// trigger the upload of the media file.
unawaited(handleNextMediaUploadSteps(widget.mediaUploadId));
if (context.mounted) {
Navigator.pop(context, true);
// if (widget.preselectedUser != null) {
// Navigator.pop(context, true);
// } else {
// Navigator.popUntil(context, (route) => route.isFirst, true);
// globalUpdateOfHomeViewPageIndex(1);
// }
}
if (context.mounted) {
Navigator.pop(context, true);
// if (widget.preselectedUser != null) {
// Navigator.pop(context, true);
// } else {
// Navigator.popUntil(context, (route) => route.isFirst, true);
// globalUpdateOfHomeViewPageIndex(1);
// }
}
},
style: ButtonStyle(
@ -308,7 +282,7 @@ class _ShareImageView extends State<ShareImageView> {
const EdgeInsets.symmetric(vertical: 10, horizontal: 30),
),
backgroundColor: WidgetStateProperty.all<Color>(
imageBytes == null || widget.selectedUserIds.isEmpty
mediaStoreFutureReady || widget.selectedGroupIds.isEmpty
? Theme.of(context).colorScheme.secondary
: Theme.of(context).colorScheme.primary,
),
@ -328,52 +302,42 @@ class _ShareImageView extends State<ShareImageView> {
class UserList extends StatelessWidget {
const UserList(
this.users, {
required this.selectedUserIds,
required this.updateStatus,
required this.isRealTwonly,
this.groups, {
required this.selectedGroupIds,
required this.updateSelectedGroupIds,
super.key,
});
final void Function(int, bool) updateStatus;
final List<Contact> users;
final bool isRealTwonly;
final HashSet<int> selectedUserIds;
final void Function(String, bool) updateSelectedGroupIds;
final List<Group> groups;
final HashSet<String> selectedGroupIds;
@override
Widget build(BuildContext context) {
// Step 1: Sort the users alphabetically
users
groups
.sort((a, b) => b.lastMessageExchange.compareTo(a.lastMessageExchange));
return ListView.builder(
restorationId: 'new_message_users_list',
itemCount: users.length,
itemCount: groups.length,
itemBuilder: (BuildContext context, int i) {
final user = users[i];
final flameCounter = getFlameCounterFromContact(user);
final group = groups[i];
return ListTile(
title: Row(
children: [
if (isRealTwonly)
Padding(
padding: const EdgeInsets.only(right: 1),
child: VerifiedShield(user),
),
Text(getContactDisplayName(user)),
if (flameCounter >= 1)
FlameCounterWidget(
user,
flameCounter,
prefix: true,
),
Text(group.groupName),
FlameCounterWidget(
groupId: group.groupId,
prefix: true,
),
],
),
leading: ContactAvatar(
contact: user,
leading: AvatarIcon(
group: group,
fontSize: 15,
),
trailing: Checkbox(
value: selectedUserIds.contains(user.userId),
value: selectedGroupIds.contains(group.groupId),
side: WidgetStateBorderSide.resolveWith(
(Set states) {
if (states.contains(WidgetState.selected)) {
@ -384,11 +348,14 @@ class UserList extends StatelessWidget {
),
onChanged: (bool? value) {
if (value == null) return;
updateStatus(user.userId, value);
updateSelectedGroupIds(group.groupId, value);
},
),
onTap: () {
updateStatus(user.userId, !selectedUserIds.contains(user.userId));
updateSelectedGroupIds(
group.groupId,
!selectedGroupIds.contains(group.groupId),
);
},
);
},

View file

@ -1,15 +1,12 @@
import 'dart:async';
import 'package:drift/drift.dart' hide Column;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/database/tables/messages_table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/json/message_old.dart';
import 'package:twonly/src/model/protobuf/push_notification/push_notification.pbserver.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
import 'package:twonly/src/services/api/messages.dart';
import 'package:twonly/src/services/api/utils.dart';
import 'package:twonly/src/services/notifications/pushkeys.notifications.dart';
@ -17,8 +14,8 @@ import 'package:twonly/src/services/signal/session.signal.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart';
import 'package:twonly/src/views/components/alert_dialog.dart';
import 'package:twonly/src/views/components/avatar_icon.component.dart';
import 'package:twonly/src/views/components/headline.dart';
import 'package:twonly/src/views/components/initialsavatar.dart';
class AddNewUserView extends StatefulWidget {
const AddNewUserView({super.key});
@ -97,19 +94,18 @@ class _SearchUsernameView extends State<AddNewUserView> {
if (added > 0) {
if (await createNewSignalSession(userdata)) {
// before notifying the other party, add
// 1. Setup notifications keys with the other user
await setupNotificationWithUsers(
forceContact: userdata.userId.toInt(),
);
await encryptAndSendMessageAsync(
null,
// 2. Then send user request
await sendCipherText(
userdata.userId.toInt(),
MessageJson(
kind: MessageKind.contactRequest,
timestamp: DateTime.now(),
content: MessageContent(),
EncryptedContent(
contactRequest: EncryptedContent_ContactRequest(
type: EncryptedContent_ContactRequest_Type.REQUEST,
),
),
pushNotification: PushNotification(kind: PushKind.contactRequest),
);
}
}
@ -198,7 +194,7 @@ class ContactsListView extends StatelessWidget {
child: IconButton(
icon: const FaIcon(FontAwesomeIcons.boxArchive, size: 15),
onPressed: () async {
const update = ContactsCompanion(archived: Value(true));
const update = ContactsCompanion(requested: Value(false));
await twonlyDB.contactsDao.updateContact(contact.userId, update);
},
),
@ -234,17 +230,18 @@ class ContactsListView extends StatelessWidget {
IconButton(
icon: const Icon(Icons.check, color: Colors.green),
onPressed: () async {
const update = ContactsCompanion(accepted: Value(true));
const update = ContactsCompanion(
accepted: Value(true),
requested: Value(false),
);
await twonlyDB.contactsDao.updateContact(contact.userId, update);
await encryptAndSendMessageAsync(
null,
await sendCipherText(
contact.userId,
MessageJson(
kind: MessageKind.acceptRequest,
timestamp: DateTime.now(),
content: MessageContent(),
EncryptedContent(
contactRequest: EncryptedContent_ContactRequest(
type: EncryptedContent_ContactRequest_Type.ACCEPT,
),
),
pushNotification: PushNotification(kind: PushKind.acceptRequest),
);
await notifyContactsAboutProfileChange();
},
@ -261,7 +258,7 @@ class ContactsListView extends StatelessWidget {
final displayName = getContactDisplayName(contact);
return ListTile(
title: Text(displayName),
leading: ContactAvatar(contact: contact),
leading: AvatarIcon(contact: contact),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: contact.requested

View file

@ -1,5 +1,4 @@
import 'dart:async';
import 'package:cryptography_plus/cryptography_plus.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
@ -7,27 +6,18 @@ import 'package:flutter/services.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:provider/provider.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/json/userdata.dart';
import 'package:twonly/src/providers/connection.provider.dart';
import 'package:twonly/src/services/api/mediafiles/download.service.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart';
import 'package:twonly/src/views/camera/camera_send_to_view.dart';
import 'package:twonly/src/views/chats/add_new_user.view.dart';
import 'package:twonly/src/views/chats/chat_list_components/backup_notice.card.dart';
import 'package:twonly/src/views/chats/chat_list_components/connection_info.comp.dart';
import 'package:twonly/src/views/chats/chat_list_components/feedback_btn.dart';
import 'package:twonly/src/views/chats/chat_list_components/last_message_time.dart';
import 'package:twonly/src/views/chats/chat_messages.view.dart';
import 'package:twonly/src/views/chats/chat_messages_components/message_send_state_icon.dart';
import 'package:twonly/src/views/chats/media_viewer.view.dart';
import 'package:twonly/src/views/chats/chat_list_components/group_list_item.dart';
import 'package:twonly/src/views/chats/start_new_chat.view.dart';
import 'package:twonly/src/views/components/flame.dart';
import 'package:twonly/src/views/components/initialsavatar.dart';
import 'package:twonly/src/views/components/avatar_icon.component.dart';
import 'package:twonly/src/views/components/notification_badge.dart';
import 'package:twonly/src/views/components/user_context_menu.component.dart';
import 'package:twonly/src/views/settings/help/changelog.view.dart';
import 'package:twonly/src/views/settings/profile/profile.view.dart';
import 'package:twonly/src/views/settings/settings_main.view.dart';
@ -41,10 +31,9 @@ class ChatListView extends StatefulWidget {
}
class _ChatListViewState extends State<ChatListView> {
late StreamSubscription<List<Contact>> _contactsSub;
List<Contact> _contacts = [];
List<Contact> _pinnedContacts = [];
UserData? _user;
late StreamSubscription<List<Group>> _contactsSub;
List<Group> _groupsNotPinned = [];
List<Group> _groupsPinned = [];
GlobalKey firstUserListItemKey = GlobalKey();
GlobalKey searchForOtherUsers = GlobalKey();
@ -58,11 +47,11 @@ class _ChatListViewState extends State<ChatListView> {
}
Future<void> initAsync() async {
final stream = twonlyDB.contactsDao.watchContactsForChatList();
_contactsSub = stream.listen((contacts) {
final stream = twonlyDB.groupsDao.watchGroups();
_contactsSub = stream.listen((groups) {
setState(() {
_contacts = contacts.where((x) => !x.pinned).toList();
_pinnedContacts = contacts.where((x) => x.pinned).toList();
_groupsNotPinned = groups.where((x) => !x.pinned).toList();
_groupsPinned = groups.where((x) => x.pinned).toList();
});
});
@ -71,21 +60,16 @@ class _ChatListViewState extends State<ChatListView> {
if (!mounted) return;
await showChatListTutorialSearchOtherUsers(context, searchForOtherUsers);
if (!mounted) return;
if (_contacts.isNotEmpty) {
if (_groupsNotPinned.isNotEmpty) {
await showChatListTutorialContextMenu(context, firstUserListItemKey);
}
});
final user = await getUser();
if (user == null) return;
setState(() {
_user = user;
});
final changeLog = await rootBundle.loadString('CHANGELOG.md');
final changeLogHash =
(await compute(Sha256().hash, changeLog.codeUnits)).bytes;
if (!user.hideChangeLog &&
user.lastChangeLogHash.toString() != changeLogHash.toString()) {
if (!gUser.hideChangeLog &&
gUser.lastChangeLogHash.toString() != changeLogHash.toString()) {
await updateUserdata((u) {
u.lastChangeLogHash = changeLogHash;
return u;
@ -93,7 +77,7 @@ class _ChatListViewState extends State<ChatListView> {
if (!mounted) return;
// only show changelog to people who already have contacts
// this prevents that this is shown directly after the user registered
if (_contacts.isNotEmpty) {
if (_groupsNotPinned.isNotEmpty) {
await Navigator.push(
context,
MaterialPageRoute(
@ -133,12 +117,11 @@ class _ChatListViewState extends State<ChatListView> {
},
),
);
_user = await getUser();
if (!mounted) return;
setState(() {});
setState(() {}); // gUser has updated
},
child: ContactAvatar(
userData: _user,
child: AvatarIcon(
userData: gUser,
fontSize: 14,
color: context.color.onSurface.withAlpha(20),
),
@ -210,9 +193,8 @@ class _ChatListViewState extends State<ChatListView> {
builder: (context) => const SettingsMainView(),
),
);
_user = await getUser();
if (!mounted) return;
setState(() {});
setState(() {}); // gUser may has changed...
},
icon: const FaIcon(FontAwesomeIcons.gear, size: 19),
),
@ -227,7 +209,7 @@ class _ChatListViewState extends State<ChatListView> {
child: isConnected ? Container() : const ConnectionInfo(),
),
Positioned.fill(
child: (_contacts.isEmpty && _pinnedContacts.isEmpty)
child: (_groupsNotPinned.isEmpty && _groupsPinned.isEmpty)
? Center(
child: Padding(
padding: const EdgeInsets.all(10),
@ -252,9 +234,9 @@ class _ChatListViewState extends State<ChatListView> {
await Future.delayed(const Duration(seconds: 1));
},
child: ListView.builder(
itemCount: _pinnedContacts.length +
(_pinnedContacts.isNotEmpty ? 1 : 0) +
_contacts.length +
itemCount: _groupsPinned.length +
(_groupsPinned.isNotEmpty ? 1 : 0) +
_groupsNotPinned.length +
1,
itemBuilder: (context, index) {
if (index == 0) {
@ -262,11 +244,11 @@ class _ChatListViewState extends State<ChatListView> {
}
index -= 1;
// Check if the index is for the pinned users
if (index < _pinnedContacts.length) {
final contact = _pinnedContacts[index];
return UserListItem(
key: ValueKey(contact.userId),
user: contact,
if (index < _groupsPinned.length) {
final group = _groupsPinned[index];
return GroupListItem(
key: ValueKey(group.groupId),
group: group,
firstUserListItemKey: (index == 0 || index == 1)
? firstUserListItemKey
: null,
@ -274,21 +256,21 @@ class _ChatListViewState extends State<ChatListView> {
}
// If there are pinned users, account for the Divider
var adjustedIndex = index - _pinnedContacts.length;
if (_pinnedContacts.isNotEmpty && adjustedIndex == 0) {
var adjustedIndex = index - _groupsPinned.length;
if (_groupsPinned.isNotEmpty && adjustedIndex == 0) {
return const Divider();
}
// Adjust the index for the contacts list
adjustedIndex -= (_pinnedContacts.isNotEmpty ? 1 : 0);
adjustedIndex -= (_groupsPinned.isNotEmpty ? 1 : 0);
// Get the contacts that are not pinned
final contact = _contacts.elementAt(
final group = _groupsNotPinned.elementAt(
adjustedIndex,
);
return UserListItem(
key: ValueKey(contact.userId),
user: contact,
return GroupListItem(
key: ValueKey(group.groupId),
group: group,
firstUserListItemKey:
(index == 0) ? firstUserListItemKey : null,
);
@ -317,219 +299,3 @@ class _ChatListViewState extends State<ChatListView> {
);
}
}
class UserListItem extends StatefulWidget {
const UserListItem({
required this.user,
required this.firstUserListItemKey,
super.key,
});
final Contact user;
final GlobalKey? firstUserListItemKey;
@override
State<UserListItem> createState() => _UserListItem();
}
class _UserListItem extends State<UserListItem> {
MessageSendState state = MessageSendState.send;
Message? currentMessage;
List<Message> messagesNotOpened = [];
late StreamSubscription<List<Message>> messagesNotOpenedStream;
List<Message> lastMessages = [];
late StreamSubscription<List<Message>> lastMessageStream;
List<Message> previewMessages = [];
bool hasNonOpenedMediaFile = false;
@override
void initState() {
super.initState();
initStreams();
}
@override
void dispose() {
messagesNotOpenedStream.cancel();
lastMessageStream.cancel();
super.dispose();
}
void initStreams() {
lastMessageStream = twonlyDB.messagesDao
.watchLastMessage(widget.user.userId)
.listen((update) {
updateState(update, messagesNotOpened);
});
messagesNotOpenedStream = twonlyDB.messagesDao
.watchMessageNotOpened(widget.user.userId)
.listen((update) {
updateState(lastMessages, update);
});
}
void updateState(
List<Message> newLastMessages,
List<Message> newMessagesNotOpened,
) {
if (newLastMessages.isEmpty) {
// there are no messages at all
currentMessage = null;
previewMessages = [];
} else if (newMessagesNotOpened.isEmpty) {
// there are no not opened messages show just the last message in the table
currentMessage = newLastMessages.last;
previewMessages = newLastMessages;
} else {
// filter first for received messages
final receivedMessages =
newMessagesNotOpened.where((x) => x.messageOtherId != null).toList();
if (receivedMessages.isNotEmpty) {
previewMessages = receivedMessages;
currentMessage = receivedMessages.first;
} else {
previewMessages = newMessagesNotOpened;
currentMessage = newMessagesNotOpened.first;
}
}
final msgs =
previewMessages.where((x) => x.kind == MessageKind.media).toList();
if (msgs.isNotEmpty &&
msgs.first.kind == MessageKind.media &&
msgs.first.messageOtherId != null &&
msgs.first.openedAt == null) {
hasNonOpenedMediaFile = true;
} else {
hasNonOpenedMediaFile = false;
}
lastMessages = newLastMessages;
messagesNotOpened = newMessagesNotOpened;
setState(() {
// sets lastMessages, messagesNotOpened and currentMessage
});
}
Future<void> onTap() async {
if (currentMessage == null) {
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return CameraSendToView(widget.user);
},
),
);
return;
}
if (hasNonOpenedMediaFile) {
final msgs =
previewMessages.where((x) => x.kind == MessageKind.media).toList();
switch (msgs.first.downloadState) {
case DownloadState.pending:
await startDownloadMedia(msgs.first, true);
return;
case DownloadState.downloaded:
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return MediaViewerView(widget.user);
},
),
);
return;
case DownloadState.downloading:
return;
}
}
if (!mounted) return;
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return ChatMessagesView(widget.user);
},
),
);
}
@override
Widget build(BuildContext context) {
final flameCounter = getFlameCounterFromContact(widget.user);
return Stack(
children: [
Positioned(
top: 0,
bottom: 0,
left: 50,
child: SizedBox(
key: widget.firstUserListItemKey,
height: 20,
width: 20,
),
),
UserContextMenu(
contact: widget.user,
child: ListTile(
title: Text(
getContactDisplayName(widget.user),
),
subtitle: (widget.user.deleted)
? Text(context.lang.userDeletedAccount)
: (currentMessage == null)
? Text(context.lang.chatsTapToSend)
: Row(
children: [
MessageSendStateIcon(previewMessages),
const Text(''),
const SizedBox(width: 5),
if (currentMessage != null)
LastMessageTime(message: currentMessage!),
if (flameCounter > 0)
FlameCounterWidget(
widget.user,
flameCounter,
prefix: true,
),
],
),
leading: ContactAvatar(contact: widget.user),
trailing: (widget.user.deleted)
? null
: IconButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
if (hasNonOpenedMediaFile) {
return ChatMessagesView(widget.user);
} else {
return CameraSendToView(widget.user);
}
},
),
);
},
icon: FaIcon(
hasNonOpenedMediaFile
? FontAwesomeIcons.solidComments
: FontAwesomeIcons.camera,
color: context.color.outline.withAlpha(150),
),
),
onTap: onTap,
),
),
],
);
}
}

View file

@ -0,0 +1,227 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/database/tables/messages.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/services/api/mediafiles/download.service.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/camera/camera_send_to_view.dart';
import 'package:twonly/src/views/chats/chat_list_components/last_message_time.dart';
import 'package:twonly/src/views/chats/chat_messages.view.dart';
import 'package:twonly/src/views/chats/chat_messages_components/message_send_state_icon.dart';
import 'package:twonly/src/views/chats/media_viewer.view.dart';
import 'package:twonly/src/views/components/avatar_icon.component.dart';
import 'package:twonly/src/views/components/flame.dart';
import 'package:twonly/src/views/components/group_context_menu.component.dart';
class GroupListItem extends StatefulWidget {
const GroupListItem({
required this.group,
required this.firstUserListItemKey,
super.key,
});
final Group group;
final GlobalKey? firstUserListItemKey;
@override
State<GroupListItem> createState() => _UserListItem();
}
class _UserListItem extends State<GroupListItem> {
MessageSendState state = MessageSendState.send;
Message? currentMessage;
List<Message> messagesNotOpened = [];
late StreamSubscription<List<Message>> messagesNotOpenedStream;
List<Message> lastMessages = [];
late StreamSubscription<List<Message>> lastMessageStream;
List<Message> previewMessages = [];
bool hasNonOpenedMediaFile = false;
@override
void initState() {
super.initState();
initStreams();
}
@override
void dispose() {
messagesNotOpenedStream.cancel();
lastMessageStream.cancel();
super.dispose();
}
void initStreams() {
lastMessageStream = twonlyDB.messagesDao
.watchLastMessage(widget.group.groupId)
.listen((update) {
updateState(update, messagesNotOpened);
});
messagesNotOpenedStream = twonlyDB.messagesDao
.watchMessageNotOpened(widget.group.groupId)
.listen((update) {
updateState(lastMessages, update);
});
}
void updateState(
List<Message> newLastMessages,
List<Message> newMessagesNotOpened,
) {
if (newLastMessages.isEmpty) {
// there are no messages at all
currentMessage = null;
previewMessages = [];
} else if (newMessagesNotOpened.isEmpty) {
// there are no not opened messages show just the last message in the table
currentMessage = newLastMessages.last;
previewMessages = newLastMessages;
} else {
// filter first for received messages
final receivedMessages =
newMessagesNotOpened.where((x) => x.senderId != null).toList();
if (receivedMessages.isNotEmpty) {
previewMessages = receivedMessages;
currentMessage = receivedMessages.first;
} else {
previewMessages = newMessagesNotOpened;
currentMessage = newMessagesNotOpened.first;
}
}
final msgs =
previewMessages.where((x) => x.type == MessageType.media).toList();
if (msgs.isNotEmpty &&
msgs.first.type == MessageType.media &&
msgs.first.senderId != null &&
msgs.first.openedAt == null) {
hasNonOpenedMediaFile = true;
} else {
hasNonOpenedMediaFile = false;
}
lastMessages = newLastMessages;
messagesNotOpened = newMessagesNotOpened;
setState(() {
// sets lastMessages, messagesNotOpened and currentMessage
});
}
Future<void> onTap() async {
if (currentMessage == null) {
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return CameraSendToView(widget.group);
},
),
);
return;
}
if (hasNonOpenedMediaFile) {
final msgs =
previewMessages.where((x) => x.type == MessageType.media).toList();
final mediaFile =
await twonlyDB.mediaFilesDao.getMediaFileById(msgs.first.mediaId!);
if (mediaFile?.downloadState == null) return;
if (mediaFile!.downloadState! == DownloadState.pending) {
await startDownloadMedia(mediaFile, true);
return;
}
if (mediaFile.downloadState! == DownloadState.downloaded) {
if (!mounted) return;
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return MediaViewerView(widget.group);
},
),
);
return;
}
}
if (!mounted) return;
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return ChatMessagesView(widget.group);
},
),
);
}
@override
Widget build(BuildContext context) {
return Stack(
children: [
Positioned(
top: 0,
bottom: 0,
left: 50,
child: SizedBox(
key: widget.firstUserListItemKey,
height: 20,
width: 20,
),
),
GroupContextMenu(
group: widget.group,
child: ListTile(
title: Text(
widget.group.groupName,
),
subtitle: (currentMessage == null)
? Text(context.lang.chatsTapToSend)
: Row(
children: [
MessageSendStateIcon(previewMessages),
const Text(''),
const SizedBox(width: 5),
if (currentMessage != null)
LastMessageTime(message: currentMessage!),
FlameCounterWidget(
groupId: widget.group.groupId,
prefix: true,
),
],
),
leading: AvatarIcon(group: widget.group),
trailing: IconButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
if (hasNonOpenedMediaFile) {
return ChatMessagesView(widget.group);
} else {
return CameraSendToView(widget.group);
}
},
),
);
},
icon: FaIcon(
hasNonOpenedMediaFile
? FontAwesomeIcons.solidComments
: FontAwesomeIcons.camera,
color: context.color.outline.withAlpha(150),
),
),
onTap: onTap,
),
),
],
);
}
}

View file

@ -1,6 +1,7 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/utils/misc.dart';
@ -21,10 +22,13 @@ class _LastMessageTimeState extends State<LastMessageTime> {
void initState() {
super.initState();
// Change the color every 200 milliseconds
updateTime = Timer.periodic(const Duration(milliseconds: 500), (timer) {
updateTime =
Timer.periodic(const Duration(milliseconds: 500), (timer) async {
final lastAction = await twonlyDB.messagesDao
.getLastMessageAction(widget.message.messageId);
setState(() {
lastMessageInSeconds = DateTime.now()
.difference(widget.message.openedAt ?? widget.message.sendAt)
.difference(lastAction?.actionAt ?? widget.message.createdAt)
.inSeconds;
if (lastMessageInSeconds < 0) {
lastMessageInSeconds = 0;

View file

@ -1,19 +1,13 @@
import 'dart:async';
import 'dart:collection';
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:pie_menu/pie_menu.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/database/tables/messages_table.dart';
import 'package:twonly/src/database/tables/messages.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/json/message_old.dart';
import 'package:twonly/src/model/memory_item.model.dart';
import 'package:twonly/src/model/protobuf/push_notification/push_notification.pb.dart';
import 'package:twonly/src/services/api/messages.dart';
import 'package:twonly/src/services/notifications/background.notifications.dart';
import 'package:twonly/src/utils/misc.dart';
@ -21,25 +15,17 @@ import 'package:twonly/src/views/camera/camera_send_to_view.dart';
import 'package:twonly/src/views/chats/chat_messages_components/chat_date_chip.dart';
import 'package:twonly/src/views/chats/chat_messages_components/chat_list_entry.dart';
import 'package:twonly/src/views/chats/chat_messages_components/response_container.dart';
import 'package:twonly/src/views/components/animate_icon.dart';
import 'package:twonly/src/views/components/initialsavatar.dart';
import 'package:twonly/src/views/components/user_context_menu.component.dart';
import 'package:twonly/src/views/components/verified_shield.dart';
import 'package:twonly/src/views/components/avatar_icon.component.dart';
import 'package:twonly/src/views/contact/contact.view.dart';
import 'package:twonly/src/views/groups/group.view.dart';
import 'package:twonly/src/views/tutorial/tutorials.dart';
Color getMessageColor(Message message) {
return (message.messageOtherId == null)
return (message.senderId == null)
? const Color.fromARGB(255, 58, 136, 102)
: const Color.fromARGB(233, 68, 137, 255);
}
class ChatMessage {
ChatMessage({required this.message, required this.responseTo});
final Message message;
final Message? responseTo;
}
class ChatItem {
const ChatItem._({this.message, this.date, this.time});
factory ChatItem.date(DateTime date) {
@ -48,10 +34,10 @@ class ChatItem {
factory ChatItem.time(DateTime time) {
return ChatItem._(time: time);
}
factory ChatItem.message(ChatMessage message) {
factory ChatItem.message(Message message) {
return ChatItem._(message: message);
}
final ChatMessage? message;
final Message? message;
final DateTime? date;
final DateTime? time;
bool get isMessage => message != null;
@ -72,14 +58,13 @@ class ChatMessagesView extends StatefulWidget {
class _ChatMessagesViewState extends State<ChatMessagesView> {
TextEditingController newMessageController = TextEditingController();
HashSet<int> alreadyReportedOpened = HashSet<int>();
late Contact user;
late Group group;
String currentInputText = '';
late StreamSubscription<Contact?> userSub;
late StreamSubscription<Group?> userSub;
late StreamSubscription<List<Message>> messageSub;
List<ChatItem> messages = [];
List<MemoryItem> galleryItems = [];
Map<int, List<Message>> emojiReactionsToMessageId = {};
Message? responseToMessage;
Message? quotesMessage;
GlobalKey verifyShieldKey = GlobalKey();
late FocusNode textFieldFocus;
Timer? tutorial;
@ -89,7 +74,7 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
@override
void initState() {
super.initState();
user = widget.contact;
group = widget.group;
textFieldFocus = FocusNode();
initStreams();
@ -110,118 +95,59 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
}
Future<void> initStreams() async {
await twonlyDB.messagesDao.removeOldMessages();
final contact = twonlyDB.contactsDao.watchContact(widget.contact.userId);
userSub = contact.listen((contact) {
if (contact == null) return;
final groupStream = twonlyDB.groupsDao.watchGroup(group.groupId);
userSub = groupStream.listen((newGroup) {
if (newGroup == null) return;
setState(() {
user = contact;
group = newGroup;
});
});
final msgStream =
twonlyDB.messagesDao.watchAllMessagesFrom(widget.contact.userId);
final msgStream = twonlyDB.messagesDao.watchByGroupId(group.groupId);
messageSub = msgStream.listen((newMessages) async {
// if (!context.mounted) return;
if (Platform.isAndroid) {
await flutterLocalNotificationsPlugin.cancel(widget.contact.userId);
} else {
await flutterLocalNotificationsPlugin.cancelAll();
}
await flutterLocalNotificationsPlugin.cancelAll();
final chatItems = <ChatItem>[];
final storedMediaFiles = <Message>[];
DateTime? lastDate;
final tmpEmojiReactionsToMessageId = <int, List<Message>>{};
// only send openedMessage to one text message, as receiver will then set all as read...
List<int> openedTextMessageOtherIds;
final messageOtherMessageIdToMyMessageId = <int, int>{};
final messageIdToMessage = <int, Message>{};
/// there is probably a better way...
for (final msg in newMessages) {
if (msg.messageOtherId != null) {
messageOtherMessageIdToMyMessageId[msg.messageOtherId!] =
msg.messageId;
}
messageIdToMessage[msg.messageId] = msg;
}
final openedMessages = <int, List<String>>{};
for (final msg in newMessages) {
if (msg.kind == MessageKind.textMessage &&
msg.messageOtherId != null &&
msg.openedAt == null &&
(openedTextMessageOtherIds == null ||
openedTextMessageOtherIds < msg.messageOtherId!)) {
openedTextMessageOtherIds.add(msg.messageOtherId);
if (msg.type == MessageType.text &&
msg.senderId != null &&
msg.openedAt == null) {
openedMessages[msg.senderId!]!.add(msg.messageId);
}
Message? responseTo;
if (msg.kind == MessageKind.media && msg.mediaStored) {
if (msg.type == MessageType.media && msg.mediaStored) {
storedMediaFiles.add(msg);
}
final responseId = msg.responseToMessageId ??
messageOtherMessageIdToMyMessageId[msg.responseToOtherMessageId];
var isReaction = false;
if (responseId != null) {
responseTo = messageIdToMessage[responseId];
final content = MessageContent.fromJson(
msg.kind,
jsonDecode(msg.contentJson!) as Map,
);
if (content is TextMessageContent) {
if (isEmoji(content.text)) {
isReaction = true;
tmpEmojiReactionsToMessageId
.putIfAbsent(responseId, () => [])
.add(msg);
}
}
if (msg.kind == MessageKind.reopenedMedia) {
isReaction = true;
tmpEmojiReactionsToMessageId
.putIfAbsent(responseId, () => [])
.add(msg);
}
}
if (!isReaction) {
if (lastDate == null ||
msg.sendAt.day != lastDate.day ||
msg.sendAt.month != lastDate.month ||
msg.sendAt.year != lastDate.year) {
chatItems.add(ChatItem.date(msg.sendAt));
lastDate = msg.sendAt;
} else if (msg.sendAt.difference(lastDate).inMinutes >= 20) {
chatItems.add(ChatItem.time(msg.sendAt));
lastDate = msg.sendAt;
}
chatItems.add(
ChatItem.message(
ChatMessage(
message: msg,
responseTo: responseTo,
),
),
);
if (lastDate == null ||
msg.createdAt.day != lastDate.day ||
msg.createdAt.month != lastDate.month ||
msg.createdAt.year != lastDate.year) {
chatItems.add(ChatItem.date(msg.createdAt));
lastDate = msg.createdAt;
} else if (msg.createdAt.difference(lastDate).inMinutes >= 20) {
chatItems.add(ChatItem.time(msg.createdAt));
lastDate = msg.createdAt;
}
chatItems.add(ChatItem.message(msg));
}
if (openedTextMessageOtherIds.isNotEmpty) {
for (final contactId in openedMessages.keys) {
await notifyContactAboutOpeningMessage(
widget.contact.userId,
openedTextMessageOtherIds,
contactId,
openedMessages[contactId]!,
);
}
await twonlyDB.messagesDao
.openedAllNonMediaMessages(widget.contact.userId);
await twonlyDB.messagesDao.openedAllTextMessages(widget.group.groupId);
setState(() {
emojiReactionsToMessageId = tmpEmojiReactionsToMessageId;
messages = chatItems.reversed.toList();
});
@ -234,33 +160,21 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
Future<void> _sendMessage() async {
if (newMessageController.text == '') return;
await sendTextMessage(
user.userId,
TextMessageContent(
text: newMessageController.text,
responseToMessageId: responseToMessage?.messageOtherId,
responseToOtherMessageId: responseToMessage?.messageId,
),
PushNotification(
kind: (responseToMessage == null)
? PushKind.text
: (isEmoji(newMessageController.text))
? PushKind.reaction
: PushKind.response,
reactionContent: (isEmoji(newMessageController.text))
? newMessageController.text
: null,
),
await insertAndSendTextMessage(
group.groupId,
newMessageController.text,
quotesMessage?.messageId,
);
newMessageController.clear();
currentInputText = '';
responseToMessage = null;
quotesMessage = null;
setState(() {});
}
Future<void> scrollToMessage(int messageId) async {
Future<void> scrollToMessage(String messageId) async {
final index = messages.indexWhere(
(x) => x.isMessage && x.message!.message.messageId == messageId,
(x) => x.isMessage && x.message!.messageId == messageId,
);
if (index == -1) return;
setState(() {
@ -286,20 +200,34 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
child: Scaffold(
appBar: AppBar(
title: GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return ContactView(widget.contact.userId);
},
),
);
onTap: () async {
if (widget.group.isDirectChat) {
final member = await twonlyDB.groupsDao
.getGroupMembers(widget.group.groupId);
if (!context.mounted) return;
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return ContactView(member.first.contactId);
},
),
);
} else {
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return GroupView(widget.group);
},
),
);
}
},
child: Row(
children: [
ContactAvatar(
contact: user,
AvatarIcon(
group: group,
fontSize: 19,
),
const SizedBox(width: 10),
@ -308,10 +236,10 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
color: Colors.transparent,
child: Row(
children: [
Text(getContactDisplayName(user)),
Text(group.groupName),
const SizedBox(width: 10),
if (user.verified)
VerifiedShield(key: verifyShieldKey, user),
// if (group.verified)
// VerifiedShield(key: verifyShieldKey, group),
],
),
),
@ -345,7 +273,7 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
return Transform.translate(
offset: Offset(
(focusedScrollItem == i)
? (chatMessage.message.messageOtherId == null)
? (chatMessage.quotesMessageId == null)
? -8
: 8
: 0,
@ -354,19 +282,15 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
child: Transform.scale(
scale: (focusedScrollItem == i) ? 1.05 : 1,
child: ChatListEntry(
key:
Key(chatMessage.message.messageId.toString()),
key: Key(chatMessage.messageId),
chatMessage,
user,
group,
galleryItems,
isLastMessageFromSameUser(messages, i),
emojiReactionsToMessageId[
chatMessage.message.messageId] ??
[],
scrollToMessage: scrollToMessage,
onResponseTriggered: () {
setState(() {
responseToMessage = chatMessage.message;
quotesMessage = chatMessage;
});
textFieldFocus.requestFocus();
},
@ -377,7 +301,7 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
},
),
),
if (responseToMessage != null && !user.deleted)
if (quotesMessage != null)
Container(
padding: const EdgeInsets.only(
left: 20,
@ -388,15 +312,15 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
children: [
Expanded(
child: ResponsePreview(
message: responseToMessage!,
message: quotesMessage,
showBorder: true,
contact: user,
group: group,
),
),
IconButton(
onPressed: () {
setState(() {
responseToMessage = null;
quotesMessage = null;
});
},
icon: const FaIcon(
@ -415,50 +339,48 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
top: 10,
),
child: Row(
children: (user.deleted)
? []
: [
Expanded(
child: TextField(
controller: newMessageController,
focusNode: textFieldFocus,
keyboardType: TextInputType.multiline,
maxLines: 4,
minLines: 1,
onChanged: (value) {
currentInputText = value;
setState(() {});
},
onSubmitted: (_) {
_sendMessage();
},
decoration: inputTextMessageDeco(context),
),
),
if (currentInputText != '')
IconButton(
padding: const EdgeInsets.all(15),
icon: const FaIcon(
FontAwesomeIcons.solidPaperPlane,
),
onPressed: _sendMessage,
)
else
IconButton(
icon: const FaIcon(FontAwesomeIcons.camera),
padding: const EdgeInsets.all(15),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return CameraSendToView(widget.contact);
},
),
);
children: [
Expanded(
child: TextField(
controller: newMessageController,
focusNode: textFieldFocus,
keyboardType: TextInputType.multiline,
maxLines: 4,
minLines: 1,
onChanged: (value) {
currentInputText = value;
setState(() {});
},
onSubmitted: (_) {
_sendMessage();
},
decoration: inputTextMessageDeco(context),
),
),
if (currentInputText != '')
IconButton(
padding: const EdgeInsets.all(15),
icon: const FaIcon(
FontAwesomeIcons.solidPaperPlane,
),
onPressed: _sendMessage,
)
else
IconButton(
icon: const FaIcon(FontAwesomeIcons.camera),
padding: const EdgeInsets.all(15),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return CameraSendToView(widget.group);
},
),
],
);
},
),
],
),
),
],
@ -479,11 +401,11 @@ bool isLastMessageFromSameUser(List<ChatItem> messages, int index) {
final currentMessage = messages[index];
if (lastMessage.isMessage && currentMessage.isMessage) {
// Check if both messages have the same messageOtherId (or both are null)
return (lastMessage.message!.message.messageOtherId == null &&
currentMessage.message!.message.messageOtherId == null) ||
(lastMessage.message!.message.messageOtherId != null &&
currentMessage.message!.message.messageOtherId != null);
// Check if both messages have the same quotesMessageId (or both are null)
return (lastMessage.message!.quotesMessageId == null &&
currentMessage.message!.quotesMessageId == null) ||
(lastMessage.message!.quotesMessageId != null &&
currentMessage.message!.quotesMessageId != null);
}
return false;
}

View file

@ -1,10 +1,9 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:twonly/src/database/tables/messages.table.dart'
hide MessageActions;
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/json/message_old.dart';
import 'package:twonly/src/model/memory_item.model.dart';
import 'package:twonly/src/views/chats/chat_messages.view.dart';
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
import 'package:twonly/src/views/chats/chat_messages_components/chat_media_entry.dart';
import 'package:twonly/src/views/chats/chat_messages_components/chat_reaction_row.dart';
import 'package:twonly/src/views/chats/chat_messages_components/chat_text_entry.dart';
@ -14,21 +13,19 @@ import 'package:twonly/src/views/chats/chat_messages_components/response_contain
class ChatListEntry extends StatefulWidget {
const ChatListEntry(
this.msg,
this.contact,
this.message,
this.group,
this.galleryItems,
this.lastMessageFromSameUser,
this.otherReactions, {
this.lastMessageFromSameUser, {
required this.onResponseTriggered,
required this.scrollToMessage,
super.key,
});
final ChatMessage msg;
final Contact contact;
final Message message;
final Group group;
final bool lastMessageFromSameUser;
final List<Message> otherReactions;
final List<MemoryItem> galleryItems;
final void Function(int) scrollToMessage;
final void Function(String) scrollToMessage;
final void Function() onResponseTriggered;
@override
@ -36,26 +33,22 @@ class ChatListEntry extends StatefulWidget {
}
class _ChatListEntryState extends State<ChatListEntry> {
MessageContent? content;
String? textMessage;
MediaFileService? mediaService;
@override
void initState() {
initAsync();
super.initState();
final msgContent = MessageContent.fromJson(
widget.msg.message.kind,
jsonDecode(widget.msg.message.contentJson!) as Map,
);
if (msgContent is TextMessageContent) {
textMessage = msgContent.text;
}
content = msgContent;
}
Future<void> initAsync() async {
mediaService = await MediaFileService.fromMediaId(widget.message.messageId);
setState(() {});
}
@override
Widget build(BuildContext context) {
if (content == null) return Container();
final right = widget.msg.message.messageOtherId == null;
final right = widget.message.senderId == null;
return Align(
alignment: right ? Alignment.centerRight : Alignment.centerLeft,
@ -64,7 +57,7 @@ class _ChatListEntryState extends State<ChatListEntry> {
? const EdgeInsets.only(top: 5, right: 10, left: 10)
: const EdgeInsets.only(top: 5, bottom: 20, right: 10, left: 10),
child: MessageContextMenu(
message: widget.msg.message,
message: widget.message,
onResponseTriggered: widget.onResponseTriggered,
child: Column(
mainAxisAlignment:
@ -73,36 +66,36 @@ class _ChatListEntryState extends State<ChatListEntry> {
right ? CrossAxisAlignment.end : CrossAxisAlignment.start,
children: [
MessageActions(
message: widget.msg.message,
message: widget.message,
onResponseTriggered: widget.onResponseTriggered,
child: Stack(
alignment:
right ? Alignment.centerRight : Alignment.centerLeft,
children: [
ResponseContainer(
msg: widget.msg,
contact: widget.contact,
msg: widget.message,
group: widget.group,
mediaService: mediaService,
scrollToMessage: widget.scrollToMessage,
child: (textMessage != null)
child: (widget.message.type == MessageType.text)
? ChatTextEntry(
message: widget.msg,
text: textMessage!,
hasReaction: widget.otherReactions.isNotEmpty,
message: widget.message,
)
: ChatMediaEntry(
message: widget.msg.message,
contact: widget.contact,
galleryItems: widget.galleryItems,
content: content!,
),
: (mediaService == null)
? null
: ChatMediaEntry(
message: widget.message,
group: widget.group,
mediaService: mediaService!,
galleryItems: widget.galleryItems,
),
),
Positioned(
bottom: 5,
left: 5,
right: 5,
child: ReactionRow(
otherReactions: widget.otherReactions,
message: widget.msg.message,
message: widget.message,
),
),
],

View file

@ -1,35 +1,33 @@
import 'dart:async';
import 'package:drift/drift.dart' show Value;
import 'package:flutter/material.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/tables/messages_table.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/database/tables/messages.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/json/message_old.dart';
import 'package:twonly/src/model/memory_item.model.dart';
import 'package:twonly/src/model/protobuf/push_notification/push_notification.pbserver.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'
hide Message;
import 'package:twonly/src/services/api/mediafiles/download.service.dart'
as received;
import 'package:twonly/src/services/api/messages.dart';
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/camera/share_image_editor_view.dart';
import 'package:twonly/src/views/chats/chat_messages_components/in_chat_media_viewer.dart';
import 'package:twonly/src/views/chats/media_viewer.view.dart';
import 'package:twonly/src/views/tutorial/tutorials.dart';
class ChatMediaEntry extends StatefulWidget {
const ChatMediaEntry({
required this.message,
required this.contact,
required this.content,
required this.group,
required this.galleryItems,
required this.mediaService,
super.key,
});
final Message message;
final Contact contact;
final MessageContent content;
final Group group;
final List<MemoryItem> galleryItems;
final MediaFileService mediaService;
@override
State<ChatMediaEntry> createState() => _ChatMediaEntryState();
@ -39,97 +37,58 @@ class _ChatMediaEntryState extends State<ChatMediaEntry> {
GlobalKey reopenMediaFile = GlobalKey();
bool canBeReopened = false;
@override
void initState() {
super.initState();
unawaited(checkIfTutorialCanBeShown());
}
Future<void> checkIfTutorialCanBeShown() async {
if (widget.message.openedAt == null &&
widget.message.messageOtherId != null ||
widget.message.mediaStored) {
return;
}
final content = getMediaContent(widget.message);
if (content == null ||
content.isRealTwonly ||
content.maxShowTime != gMediaShowInfinite) {
return;
}
if (await received.existsMediaFile(widget.message.messageId, 'png')) {
if (mounted) {
setState(() {
canBeReopened = true;
});
}
Future.delayed(const Duration(seconds: 1), () async {
if (!mounted) return;
await showReopenMediaFilesTutorial(context, reopenMediaFile);
});
}
}
Future<void> onDoubleTap() async {
if (widget.message.openedAt == null &&
widget.message.messageOtherId != null ||
widget.message.mediaStored) {
if (widget.message.openedAt == null || widget.message.mediaStored) {
return;
}
if (await received.existsMediaFile(widget.message.messageId, 'png')) {
await encryptAndSendMessageAsync(
null,
widget.contact.userId,
MessageJson(
kind: MessageKind.reopenedMedia,
messageSenderId: widget.message.messageId,
content: ReopenedMediaFileContent(
messageId: widget.message.messageOtherId!,
if (widget.mediaService.tempPath.existsSync()) {
await sendCipherTextToGroup(
widget.message.groupId,
EncryptedContent(
mediaUpdate: EncryptedContent_MediaUpdate(
type: EncryptedContent_MediaUpdate_Type.REOPENED,
targetMediaId: widget.message.mediaId,
),
timestamp: DateTime.now(),
),
pushNotification: PushNotification(
kind: PushKind.reopenedMedia,
),
);
await twonlyDB.messagesDao.updateMessageByMessageId(
widget.message.messageId,
const MessagesCompanion(openedAt: Value(null)),
);
await twonlyDB.messagesDao.reopenedMedia(widget.message.messageId);
}
}
Future<void> onTap() async {
if (widget.message.downloadState == DownloadState.downloaded &&
if (widget.mediaService.mediaFile.downloadState ==
DownloadState.downloaded &&
widget.message.openedAt == null) {
if (!mounted) return;
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return MediaViewerView(
widget.contact,
widget.group,
initialMessage: widget.message,
);
},
),
);
await checkIfTutorialCanBeShown();
} else if (widget.message.downloadState == DownloadState.pending) {
await received.startDownloadMedia(widget.message, true);
} else if (widget.mediaService.mediaFile.downloadState ==
DownloadState.pending) {
await received.startDownloadMedia(widget.mediaService.mediaFile, true);
}
}
@override
Widget build(BuildContext context) {
final color = getMessageColorFromType(
widget.content,
widget.message,
widget.mediaService.mediaFile,
context,
);
return GestureDetector(
key: reopenMediaFile,
onDoubleTap: onDoubleTap,
onTap: widget.message.kind == MessageKind.media ? onTap : null,
onTap: (widget.message.type == MessageType.media) ? onTap : null,
child: SizedBox(
width: 150,
height: widget.message.mediaStored ? 271 : null,
@ -139,7 +98,8 @@ class _ChatMediaEntryState extends State<ChatMediaEntry> {
borderRadius: BorderRadius.circular(12),
child: InChatMediaViewer(
message: widget.message,
contact: widget.contact,
group: widget.group,
mediaService: widget.mediaService,
color: color,
galleryItems: widget.galleryItems,
canBeReopened: canBeReopened,

View file

@ -1,20 +1,15 @@
import 'dart:convert';
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/json/message_old.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/components/animate_icon.dart';
class ReactionRow extends StatefulWidget {
const ReactionRow({
required this.otherReactions,
required this.message,
super.key,
});
final List<Message> otherReactions;
final Message message;
@override
@ -22,65 +17,79 @@ class ReactionRow extends StatefulWidget {
}
class _ReactionRowState extends State<ReactionRow> {
List<Reaction> reactions = [];
StreamSubscription<List<Reaction>>? reactionsSub;
@override
void initState() {
initAsync();
super.initState();
}
@override
void dispose() {
reactionsSub?.cancel();
super.dispose();
}
Future<void> initAsync() async {
final stream =
twonlyDB.reactionsDao.watchReactions(widget.message.messageId);
reactionsSub = stream.listen((update) {
setState(() {
reactions = update;
});
});
}
@override
Widget build(BuildContext context) {
final children = <Widget>[];
var hasOneTextReaction = false;
var hasOneReopened = false;
for (final reaction in widget.otherReactions.reversed) {
final content = MessageContent.fromJson(
reaction.kind,
jsonDecode(reaction.contentJson!) as Map,
);
if (content is ReopenedMediaFileContent) {
if (hasOneReopened) continue;
hasOneReopened = true;
children.add(
Expanded(
child: Align(
alignment: Alignment.bottomRight,
child: Padding(
padding: const EdgeInsets.only(right: 3),
child: FaIcon(
FontAwesomeIcons.repeat,
size: 12,
color: isDarkMode(context) ? Colors.white : Colors.black,
),
),
),
),
);
}
for (final reaction in reactions) {
// if (content is ReopenedMediaFileContent) {
// if (hasOneReopened) continue;
// hasOneReopened = true;
// children.add(
// Expanded(
// child: Align(
// alignment: Alignment.bottomRight,
// child: Padding(
// padding: const EdgeInsets.only(right: 3),
// child: FaIcon(
// FontAwesomeIcons.repeat,
// size: 12,
// color: isDarkMode(context) ? Colors.white : Colors.black,
// ),
// ),
// ),
// ),
// );
// }
// only show one reaction
if (hasOneTextReaction) continue;
if (content is TextMessageContent) {
hasOneTextReaction = true;
if (!isEmoji(content.text)) continue;
late Widget child;
if (EmojiAnimation.animatedIcons.containsKey(content.text)) {
child = SizedBox(
height: 18,
child: EmojiAnimation(emoji: content.text),
);
} else {
child = Text(content.text, style: const TextStyle(fontSize: 14));
}
children.insert(
0,
Padding(
padding: const EdgeInsets.only(left: 3),
child: child,
),
late Widget child;
if (EmojiAnimation.animatedIcons.containsKey(reaction.emoji)) {
child = SizedBox(
height: 18,
child: EmojiAnimation(emoji: reaction.emoji),
);
} else {
child = Text(reaction.emoji, style: const TextStyle(fontSize: 14));
}
children.insert(
0,
Padding(
padding: const EdgeInsets.only(left: 3),
child: child,
),
);
}
if (children.isEmpty) return Container();
return Row(
mainAxisAlignment: widget.message.messageOtherId == null
mainAxisAlignment: widget.message.senderId == null
? MainAxisAlignment.end
: MainAxisAlignment.end,
children: children,

View file

@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/views/chats/chat_messages.view.dart';
import 'package:twonly/src/views/components/animate_icon.dart';
import 'package:twonly/src/views/components/better_text.dart';
@ -6,17 +7,14 @@ import 'package:twonly/src/views/components/better_text.dart';
class ChatTextEntry extends StatelessWidget {
const ChatTextEntry({
required this.message,
required this.text,
required this.hasReaction,
super.key,
});
final String text;
final ChatMessage message;
final bool hasReaction;
final Message message;
@override
Widget build(BuildContext context) {
final text = message.content ?? '';
if (EmojiAnimation.supported(text)) {
return Container(
constraints: const BoxConstraints(
@ -33,16 +31,10 @@ class ChatTextEntry extends StatelessWidget {
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.8,
),
padding: EdgeInsets.only(
left: 10,
top: 4,
bottom: 4,
right: hasReaction ? 30 : 10,
),
padding: const EdgeInsets.only(left: 10, top: 4, bottom: 4),
decoration: BoxDecoration(
color: message.responseTo == null
? getMessageColor(message.message)
: null,
color:
message.quotesMessageId == null ? getMessageColor(message) : null,
borderRadius: BorderRadius.circular(12),
),
child: BetterText(text: text),

View file

@ -1,9 +1,9 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/memory_item.model.dart';
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
import 'package:twonly/src/views/chats/chat_messages_components/message_send_state_icon.dart';
import 'package:twonly/src/views/memories/memories_item_thumbnail.dart';
import 'package:twonly/src/views/memories/memories_photo_slider.view.dart';
@ -11,7 +11,8 @@ import 'package:twonly/src/views/memories/memories_photo_slider.view.dart';
class InChatMediaViewer extends StatefulWidget {
const InChatMediaViewer({
required this.message,
required this.contact,
required this.group,
required this.mediaService,
required this.color,
required this.galleryItems,
required this.canBeReopened,
@ -19,7 +20,8 @@ class InChatMediaViewer extends StatefulWidget {
});
final Message message;
final Contact contact;
final Group group;
final MediaFileService mediaService;
final List<MemoryItem> galleryItems;
final Color color;
final bool canBeReopened;
@ -56,8 +58,7 @@ class _InChatMediaViewerState extends State<InChatMediaViewer> {
bool loadIndex() {
if (widget.message.mediaStored) {
final index = widget.galleryItems.indexWhere(
(x) =>
x.id == (widget.message.mediaUploadId ?? widget.message.messageId),
(x) => x.mediaService.mediaFile.mediaId == (widget.message.messageId),
);
if (index != -1) {
galleryItemIndex = index;
@ -83,7 +84,7 @@ class _InChatMediaViewerState extends State<InChatMediaViewer> {
if (widget.message.mediaStored) return;
final stream = twonlyDB.messagesDao
.getMessageByMessageId(widget.message.messageId)
.getMessageById(widget.message.messageId)
.watchSingleOrNull();
messageStream = stream.listen((updated) async {
if (updated != null) {

View file

@ -1,14 +1,14 @@
// ignore_for_file: inference_failure_on_function_invocation
import 'package:drift/drift.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:pie_menu/pie_menu.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/tables/messages_table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/json/message_old.dart';
import 'package:twonly/src/model/protobuf/push_notification/push_notification.pbserver.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pbserver.dart'
as pb;
import 'package:twonly/src/services/api/messages.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/camera/image_editor/data/layer.dart';
@ -48,24 +48,22 @@ class MessageContextMenu extends StatelessWidget {
) as EmojiLayerData?;
if (layer == null) return;
await sendTextMessage(
message.contactId,
TextMessageContent(
text: layer.text,
responseToMessageId: message.messageOtherId,
responseToOtherMessageId:
(message.messageOtherId == null) ? message.messageId : null,
await twonlyDB.reactionsDao.insertReaction(
ReactionsCompanion(
messageId: Value(message.messageId),
emoji: Value(layer.text),
),
);
await sendCipherTextToGroup(
message.groupId,
pb.EncryptedContent(
reaction: pb.EncryptedContent_Reaction(
targetMessageId: message.messageId,
emoji: layer.text,
remove: false,
),
),
(message.messageOtherId != null)
? PushNotification(
kind: (message.kind == MessageKind.textMessage)
? PushKind.reactionToText
: (getMediaContent(message)!.isVideo)
? PushKind.reactionToVideo
: PushKind.reactionToImage,
reactionContent: layer.text,
)
: null,
);
},
child: const FaIcon(FontAwesomeIcons.faceLaugh),
@ -75,15 +73,15 @@ class MessageContextMenu extends StatelessWidget {
onSelect: onResponseTriggered,
child: const FaIcon(FontAwesomeIcons.reply),
),
PieAction(
tooltip: Text(context.lang.copy),
onSelect: () async {
final text = getMessageText(message);
await Clipboard.setData(ClipboardData(text: text));
await HapticFeedback.heavyImpact();
},
child: const FaIcon(FontAwesomeIcons.solidCopy),
),
if (message.content != null)
PieAction(
tooltip: Text(context.lang.copy),
onSelect: () async {
await Clipboard.setData(ClipboardData(text: message.content!));
await HapticFeedback.heavyImpact();
},
child: const FaIcon(FontAwesomeIcons.solidCopy),
),
PieAction(
tooltip: Text(context.lang.delete),
onSelect: () async {
@ -94,8 +92,7 @@ class MessageContextMenu extends StatelessWidget {
customOk: context.lang.deleteOkBtn,
);
if (delete) {
await twonlyDB.messagesDao
.deleteMessagesByMessageId(message.messageId);
await twonlyDB.messagesDao.deleteMessagesById(message.messageId);
}
},
child: const FaIcon(FontAwesomeIcons.trash),

View file

@ -1,11 +1,11 @@
import 'dart:collection';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/src/database/tables/messages_table.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/database/tables/messages.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/json/message_old.dart';
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/components/animate_icon.dart';
@ -18,17 +18,23 @@ enum MessageSendState {
sending,
}
MessageSendState messageSendStateFromMessage(Message msg) {
Future<MessageSendState> messageSendStateFromMessage(Message msg) async {
MessageSendState state;
if (!msg.acknowledgeByServer) {
if (msg.messageOtherId == null) {
final ackByServer = await twonlyDB.messagesDao.haveAllMembers(
msg.groupId,
msg.messageId,
MessageActionType.ackByServerAt,
);
if (!ackByServer) {
if (msg.senderId == null) {
state = MessageSendState.sending;
} else {
state = MessageSendState.receiving;
}
} else {
if (msg.messageOtherId == null) {
if (msg.senderId == null) {
// message send
if (msg.openedAt == null) {
state = MessageSendState.send;
@ -63,9 +69,113 @@ class MessageSendStateIcon extends StatefulWidget {
}
class _MessageSendStateIconState extends State<MessageSendStateIcon> {
List<Widget> icons = <Widget>[];
String text = '';
Widget? textWidget;
@override
void initState() {
super.initState();
initAsync();
}
Future<void> initAsync() async {
final kindsAlreadyShown = HashSet<MessageType>();
for (final message in widget.messages) {
if (icons.length == 2) break;
if (kindsAlreadyShown.contains(message.type)) continue;
kindsAlreadyShown.add(message.type);
final state = await messageSendStateFromMessage(message);
final mediaFile = message.mediaId == null
? null
: await MediaFileService.fromMediaId(message.mediaId!);
if (!mounted) return;
final color =
getMessageColorFromType(message, mediaFile?.mediaFile, context);
Widget icon = const Placeholder();
textWidget = null;
switch (state) {
case MessageSendState.receivedOpened:
icon = Icon(Icons.crop_square, size: 14, color: color);
if (message.content != null) {
if (isEmoji(message.content!)) {
icon = Text(
message.content!,
style: const TextStyle(fontSize: 12),
);
}
}
text = context.lang.messageSendState_Received;
if (widget.canBeReopened) {
textWidget = Text(
context.lang.doubleClickToReopen,
style: const TextStyle(fontSize: 9),
);
}
case MessageSendState.sendOpened:
icon = FaIcon(FontAwesomeIcons.paperPlane, size: 12, color: color);
text = context.lang.messageSendState_Opened;
case MessageSendState.received:
icon = Icon(Icons.square_rounded, size: 14, color: color);
text = context.lang.messageSendState_Received;
if (message.type == MessageType.media) {
if (mediaFile!.mediaFile.downloadState == DownloadState.pending) {
text = context.lang.messageSendState_TapToLoad;
}
if (mediaFile.mediaFile.downloadState ==
DownloadState.downloading) {
text = context.lang.messageSendState_Loading;
icon = getLoaderIcon(color);
}
}
case MessageSendState.send:
icon =
FaIcon(FontAwesomeIcons.solidPaperPlane, size: 12, color: color);
text = context.lang.messageSendState_Send;
case MessageSendState.sending:
icon = getLoaderIcon(color);
text = context.lang.messageSendState_Sending;
case MessageSendState.receiving:
icon = getLoaderIcon(color);
text = context.lang.messageSendState_Received;
}
if (message.mediaStored) {
icon = FaIcon(FontAwesomeIcons.floppyDisk, size: 12, color: color);
text = context.lang.messageStoredInGallery;
}
if (mediaFile != null) {
if (mediaFile.mediaFile.stored) {
icon = FaIcon(FontAwesomeIcons.repeat, size: 12, color: color);
text = context.lang.messageReopened;
}
if (mediaFile.mediaFile.reuploadRequestedBy != null) {
icon =
FaIcon(FontAwesomeIcons.clockRotateLeft, size: 12, color: color);
textWidget = Text(
context.lang.retransmissionRequested,
style: const TextStyle(fontSize: 9),
);
}
}
if (message.type == MessageType.media) {
icons.insert(0, icon);
} else {
icons.add(icon);
}
}
setState(() {});
}
Widget getLoaderIcon(Color color) {
@ -83,108 +193,6 @@ class _MessageSendStateIconState extends State<MessageSendStateIcon> {
@override
Widget build(BuildContext context) {
final icons = <Widget>[];
var text = '';
final kindsAlreadyShown = HashSet<MessageKind>();
Widget? textWidget;
for (final message in widget.messages) {
if (icons.length == 2) break;
if (kindsAlreadyShown.contains(message.kind)) continue;
kindsAlreadyShown.add(message.kind);
final state = messageSendStateFromMessage(message);
late Color color;
MessageContent? content;
if (message.contentJson == null) {
color = getMessageColorFromType(TextMessageContent(text: ''), context);
} else {
content = MessageContent.fromJson(
message.kind,
jsonDecode(message.contentJson!) as Map,
);
if (content == null) continue;
color = getMessageColorFromType(content, context);
}
Widget icon = const Placeholder();
textWidget = null;
switch (state) {
case MessageSendState.receivedOpened:
icon = Icon(Icons.crop_square, size: 14, color: color);
if (content is TextMessageContent) {
if (isEmoji(content.text)) {
icon = Text(content.text, style: const TextStyle(fontSize: 12));
}
}
text = context.lang.messageSendState_Received;
if (widget.canBeReopened) {
textWidget = Text(
context.lang.doubleClickToReopen,
style: const TextStyle(fontSize: 9),
);
}
case MessageSendState.sendOpened:
icon = FaIcon(FontAwesomeIcons.paperPlane, size: 12, color: color);
text = context.lang.messageSendState_Opened;
case MessageSendState.received:
icon = Icon(Icons.square_rounded, size: 14, color: color);
text = context.lang.messageSendState_Received;
if (message.kind == MessageKind.media) {
if (message.downloadState == DownloadState.pending) {
text = context.lang.messageSendState_TapToLoad;
}
if (message.downloadState == DownloadState.downloading) {
text = context.lang.messageSendState_Loading;
icon = getLoaderIcon(color);
}
}
case MessageSendState.send:
icon =
FaIcon(FontAwesomeIcons.solidPaperPlane, size: 12, color: color);
text = context.lang.messageSendState_Send;
case MessageSendState.sending:
icon = getLoaderIcon(color);
text = context.lang.messageSendState_Sending;
case MessageSendState.receiving:
icon = getLoaderIcon(color);
text = context.lang.messageSendState_Received;
}
if (message.kind == MessageKind.storedMediaFile) {
icon = FaIcon(FontAwesomeIcons.floppyDisk, size: 12, color: color);
text = context.lang.messageStoredInGallery;
}
if (message.kind == MessageKind.reopenedMedia) {
icon = FaIcon(FontAwesomeIcons.repeat, size: 12, color: color);
text = context.lang.messageReopened;
}
if (message.errorWhileSending) {
icon =
FaIcon(FontAwesomeIcons.circleExclamation, size: 12, color: color);
text = 'Error';
}
if (message.mediaRetransmissionState == MediaRetransmitting.requested) {
icon = FaIcon(FontAwesomeIcons.clockRotateLeft, size: 12, color: color);
textWidget = Text(
context.lang.retransmissionRequested,
style: const TextStyle(fontSize: 9),
);
}
if (message.kind == MessageKind.media) {
icons.insert(0, icon);
} else {
icons.add(icon);
}
}
if (icons.isEmpty) return Container();
var icon = icons[0];
@ -215,7 +223,7 @@ class _MessageSendStateIconState extends State<MessageSendStateIcon> {
icon,
const SizedBox(width: 3),
if (textWidget != null)
textWidget
textWidget!
else
Text(
text,

View file

@ -1,29 +1,27 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/database/tables/messages_table.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/database/tables/messages.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/json/message_old.dart';
import 'package:twonly/src/model/memory_item.model.dart';
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/chats/chat_messages.view.dart';
class ResponseContainer extends StatefulWidget {
const ResponseContainer({
required this.msg,
required this.contact,
required this.group,
required this.child,
required this.scrollToMessage,
required this.mediaService,
super.key,
});
final ChatMessage msg;
final Widget child;
final Contact contact;
final void Function(int) scrollToMessage;
final Message msg;
final Widget? child;
final Group group;
final MediaFileService? mediaService;
final void Function(String) scrollToMessage;
@override
State<ResponseContainer> createState() => _ResponseContainerState();
@ -57,17 +55,20 @@ class _ResponseContainerState extends State<ResponseContainer> {
@override
Widget build(BuildContext context) {
if (widget.msg.responseTo == null) {
return widget.child;
if (widget.msg.quotesMessageId == null) {
if (widget.child == null) {
return Container();
}
return widget.child!;
}
return GestureDetector(
onTap: () => widget.scrollToMessage(widget.msg.responseTo!.messageId),
onTap: () => widget.scrollToMessage(widget.msg.quotesMessageId!),
child: Container(
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.8,
),
decoration: BoxDecoration(
color: getMessageColor(widget.msg.message),
color: getMessageColor(widget.msg),
borderRadius: BorderRadius.circular(12),
),
child: Column(
@ -88,8 +89,8 @@ class _ResponseContainerState extends State<ResponseContainer> {
),
),
child: ResponsePreview(
contact: widget.contact,
message: widget.msg.responseTo!,
group: widget.group,
messageId: widget.msg.quotesMessageId,
showBorder: false,
),
),
@ -108,14 +109,16 @@ class _ResponseContainerState extends State<ResponseContainer> {
class ResponsePreview extends StatefulWidget {
const ResponsePreview({
required this.message,
required this.contact,
required this.group,
required this.showBorder,
this.message,
this.messageId,
super.key,
});
final Message message;
final Contact contact;
final Message? message;
final String? messageId;
final Group group;
final bool showBorder;
@override
@ -123,56 +126,49 @@ class ResponsePreview extends StatefulWidget {
}
class _ResponsePreviewState extends State<ResponsePreview> {
File? thumbnailPath;
Message? message;
MediaFileService? mediaService;
@override
void initState() {
message = widget.message;
initAsync();
super.initState();
unawaited(initAsync());
}
Future<void> initAsync() async {
final items = await MemoryItem.convertFromMessages([widget.message]);
if (items.length == 1 && mounted) {
setState(() {
thumbnailPath = items.values.first.thumbnailPath;
});
message ??= await twonlyDB.messagesDao
.getMessageById(widget.messageId!)
.getSingleOrNull();
if (message?.mediaId != null) {
mediaService = await MediaFileService.fromMediaId(message!.mediaId!);
}
if (mounted) setState(() {});
}
@override
Widget build(BuildContext context) {
if (message == null) return Container();
String? subtitle;
if (widget.message.kind == MessageKind.textMessage) {
if (widget.message.contentJson != null) {
final content = MessageContent.fromJson(
MessageKind.textMessage,
jsonDecode(widget.message.contentJson!) as Map,
);
if (content is TextMessageContent) {
subtitle = truncateString(content.text);
}
if (message!.type == MessageType.text) {
if (message!.content != null) {
subtitle = truncateString(message!.content!);
}
}
if (widget.message.kind == MessageKind.media) {
final content = MessageContent.fromJson(
MessageKind.media,
jsonDecode(widget.message.contentJson!) as Map,
);
if (content is MediaMessageContent) {
subtitle = content.isVideo ? 'Video' : 'Image';
}
if (message!.type == MessageType.media && mediaService != null) {
subtitle =
mediaService!.mediaFile.type == MediaType.video ? 'Video' : 'Image';
}
var username = 'You';
if (widget.message.messageOtherId != null) {
username = getContactDisplayName(widget.contact);
if (message!.senderId != null) {
username = message!.senderId.toString();
}
final color = getMessageColor(widget.message);
final color = getMessageColor(message!);
if (!widget.message.mediaStored) {
if (!message!.mediaStored) {
return Container(
padding: widget.showBorder
? const EdgeInsets.only(left: 10, right: 10)
@ -225,10 +221,10 @@ class _ResponsePreviewState extends State<ResponsePreview> {
],
),
),
if (thumbnailPath != null)
if (mediaService != null)
SizedBox(
height: widget.showBorder ? 100 : 210,
child: Image.file(thumbnailPath!),
child: Image.file(mediaService!.thumbnailPath),
),
],
),

View file

@ -1,29 +1,24 @@
// ignore_for_file: avoid_dynamic_calls
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:drift/drift.dart' hide Column;
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:lottie/lottie.dart';
import 'package:no_screenshot/no_screenshot.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/database/tables/messages_table.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart'
show DownloadState, MediaType;
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/json/message_old.dart';
import 'package:twonly/src/model/protobuf/push_notification/push_notification.pb.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'
as pb;
import 'package:twonly/src/services/api/mediafiles/download.service.dart';
import 'package:twonly/src/services/api/messages.dart';
import 'package:twonly/src/services/api/utils.dart';
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
import 'package:twonly/src/services/notifications/background.notifications.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart';
import 'package:twonly/src/views/camera/camera_send_to_view.dart';
import 'package:twonly/src/views/camera/share_image_editor_view.dart';
import 'package:twonly/src/views/chats/media_viewer_components/reaction_buttons.component.dart';
import 'package:twonly/src/views/components/animate_icon.dart';
import 'package:twonly/src/views/components/media_view_sizing.dart';
import 'package:video_player/video_player.dart';
@ -31,8 +26,8 @@ import 'package:video_player/video_player.dart';
final NoScreenshot _noScreenshot = NoScreenshot.instance;
class MediaViewerView extends StatefulWidget {
const MediaViewerView(this.contact, {super.key, this.initialMessage});
final Contact contact;
const MediaViewerView(this.group, {super.key, this.initialMessage});
final Group group;
final Message? initialMessage;
@ -48,23 +43,21 @@ class _MediaViewerViewState extends State<MediaViewerView> {
double mediaViewerDistanceFromBottom = 0;
// current image related
Uint8List? imageBytes;
String? videoPath;
VideoPlayerController? videoController;
MediaFileService? currentMedia;
Message? currentMessage;
DateTime? canBeSeenUntil;
int maxShowTime = gMediaShowInfinite;
double progress = 0;
bool isRealTwonly = false;
bool mirrorVideo = false;
bool isDownloading = false;
bool showSendTextMessageInput = false;
final GlobalKey mediaWidgetKey = GlobalKey();
bool imageSaved = false;
bool imageSaving = false;
bool displayTwonlyPresent = true;
StreamSubscription<Message?>? downloadStateListener;
StreamSubscription<MediaFile?>? downloadStateListener;
List<Message> allMediaFiles = [];
late StreamSubscription<List<Message>> _subscription;
@ -94,20 +87,21 @@ class _MediaViewerViewState extends State<MediaViewerView> {
Future<void> asyncLoadNextMedia(bool firstRun) async {
final messages =
twonlyDB.messagesDao.watchMediaMessageNotOpened(widget.contact.userId);
twonlyDB.messagesDao.watchMediaNotOpened(widget.group.groupId);
_subscription = messages.listen((messages) {
_subscription = messages.listen((messages) async {
for (final msg in messages) {
// if (!allMediaFiles.any((m) => m.messageId == msg.messageId)) {
// allMediaFiles.add(msg);
// }
// Find the index of the existing message with the same messageId
if (msg.mediaId == currentMedia?.mediaFile.mediaId) {
// The update of the current Media in case of a download is done in loadCurrentMediaFile
continue;
}
/// If the messages was already there just replace it and go to the next...
final index =
allMediaFiles.indexWhere((m) => m.messageId == msg.messageId);
if (index >= 1) {
// to not modify the first message
// If the message exists, replace it
allMediaFiles[index] = msg;
} else if (index == -1) {
// If the message does not exist, add it
@ -116,101 +110,90 @@ class _MediaViewerViewState extends State<MediaViewerView> {
}
setState(() {});
if (firstRun) {
loadCurrentMediaFile();
// ignore: parameter_assignments
firstRun = false;
await loadCurrentMediaFile();
}
});
}
Future<void> nextMediaOrExit() async {
if (!mounted) return;
await videoController?.dispose();
nextMediaTimer?.cancel();
progressTimer?.cancel();
if (allMediaFiles.isNotEmpty) {
try {
if (!imageSaved && maxShowTime != gMediaShowInfinite) {
await deleteMediaFile(allMediaFiles.first.messageId, 'mp4');
await deleteMediaFile(allMediaFiles.first.messageId, 'png');
}
} catch (e) {
Log.error('$e');
/// Remove the current media file in case it is not set to unlimited
if (currentMedia != null) {
if (!imageSaved &&
currentMedia!.mediaFile.displayLimitInMilliseconds != null) {
currentMedia!.fullMediaRemoval();
}
}
if (allMediaFiles.isEmpty || allMediaFiles.length == 1) {
if (mounted) {
Navigator.pop(context);
}
await videoController?.dispose();
if (!mounted) return;
nextMediaTimer?.cancel();
progressTimer?.cancel();
if (allMediaFiles.isEmpty) {
Navigator.pop(context);
} else {
allMediaFiles.removeAt(0);
await loadCurrentMediaFile();
}
}
Future<void> loadCurrentMediaFile({bool showTwonly = false}) async {
if (!mounted) return;
if (!context.mounted || allMediaFiles.isEmpty) return nextMediaOrExit();
if (!mounted || !context.mounted) return;
if (allMediaFiles.isEmpty) return nextMediaOrExit();
await _noScreenshot.screenshotOff();
setState(() {
videoController = null;
imageBytes = null;
currentMedia = null;
currentMessage = null;
canBeSeenUntil = null;
maxShowTime = gMediaShowInfinite;
imageSaving = false;
imageSaved = false;
mirrorVideo = false;
progress = 0;
videoPath = null;
isDownloading = false;
isRealTwonly = false;
showSendTextMessageInput = false;
});
if (Platform.isAndroid) {
await flutterLocalNotificationsPlugin
.cancel(allMediaFiles.first.contactId);
} else {
await flutterLocalNotificationsPlugin.cancelAll();
}
// if (Platform.isAndroid) {
// await flutterLocalNotificationsPlugin
// .cancel(allMediaFiles.first.contactId);
// } else {
await flutterLocalNotificationsPlugin.cancelAll();
// }
if (allMediaFiles.first.downloadState != DownloadState.downloaded) {
setState(() {
isDownloading = true;
});
await startDownloadMedia(allMediaFiles.first, true);
final stream =
twonlyDB.mediaFilesDao.watchMedia(currentMedia!.mediaFile.mediaId);
final stream = twonlyDB.messagesDao
.getMessageByMessageId(allMediaFiles.first.messageId)
.watchSingleOrNull();
await downloadStateListener?.cancel();
downloadStateListener = stream.listen((updated) async {
if (updated != null) {
if (updated.downloadState == DownloadState.downloaded) {
await downloadStateListener?.cancel();
await handleNextDownloadedMedia(updated, showTwonly);
// start downloading all the other possible missing media files.
await tryDownloadAllMediaFiles(force: true);
}
var downloadTriggered = false;
await downloadStateListener?.cancel();
downloadStateListener = stream.listen((updated) async {
if (updated == null) return;
if (updated.downloadState != DownloadState.downloaded) {
if (!downloadTriggered) {
downloadTriggered = true;
await startDownloadMedia(currentMedia!.mediaFile, true);
unawaited(tryDownloadAllMediaFiles(force: true));
}
});
} else {
await handleNextDownloadedMedia(allMediaFiles.first, showTwonly);
}
return;
}
await downloadStateListener?.cancel();
await handleNextDownloadedMedia(showTwonly);
// start downloading all the other possible missing media files.
});
}
Future<void> handleNextDownloadedMedia(
Message current,
bool showTwonly,
) async {
final content =
MediaMessageContent.fromJson(jsonDecode(current.contentJson!) as Map);
currentMessage = allMediaFiles.removeAt(0);
final currentMediaLocal =
await MediaFileService.fromMediaId(currentMessage!.mediaId!);
if (currentMediaLocal == null || !mounted) return;
if (content.isRealTwonly) {
setState(() {
isRealTwonly = true;
});
if (currentMediaLocal.mediaFile.requiresAuthentication) {
if (!showTwonly) return;
final isAuth = await authenticateUser(
@ -224,66 +207,56 @@ class _MediaViewerViewState extends State<MediaViewerView> {
}
await notifyContactAboutOpeningMessage(
current.contactId,
[current.messageOtherId!],
currentMessage!.senderId!,
[currentMessage!.messageId],
);
await twonlyDB.messagesDao.updateMessageByMessageId(
current.messageId,
await twonlyDB.messagesDao.updateMessageId(
currentMessage!.messageId,
MessagesCompanion(openedAt: Value(DateTime.now())),
);
if (content.isVideo) {
final videoPathTmp = await getVideoPath(current.messageId);
if (videoPathTmp != null) {
videoController = VideoPlayerController.file(File(videoPathTmp.path));
await videoController
?.setLooping(content.maxShowTime == gMediaShowInfinite);
await videoController?.initialize().then((_) {
videoController!.play();
videoController?.addListener(() {
setState(() {
progress = 1 -
videoController!.value.position.inSeconds /
videoController!.value.duration.inSeconds;
});
if (content.maxShowTime != gMediaShowInfinite) {
if (videoController?.value.position ==
videoController?.value.duration) {
nextMediaOrExit();
}
}
});
setState(() {
videoPath = videoPathTmp.path;
});
// ignore: invalid_return_type_for_catch_error, argument_type_not_assignable_to_error_handler
}).catchError(Log.error);
}
}
imageBytes = await getImageBytes(current.messageId);
if ((imageBytes == null && !content.isVideo) ||
(content.isVideo && videoController == null)) {
Log.error('media files are not found...');
// When the message should be downloaded but imageBytes are null then a error happened
await handleMediaError(current);
if (!currentMediaLocal.tempPath.existsSync()) {
Log.error('Temp media file not found...');
await handleMediaError(currentMediaLocal.mediaFile);
return nextMediaOrExit();
}
if (!content.isVideo) {
if (content.maxShowTime != gMediaShowInfinite) {
if (currentMediaLocal.mediaFile.type == MediaType.video) {
videoController = VideoPlayerController.file(currentMediaLocal.tempPath);
await videoController?.setLooping(
currentMediaLocal.mediaFile.displayLimitInMilliseconds == null,
);
await videoController?.initialize().then((_) {
videoController!.play();
videoController?.addListener(() {
setState(() {
progress = 1 -
videoController!.value.position.inSeconds /
videoController!.value.duration.inSeconds;
});
if (currentMediaLocal.mediaFile.displayLimitInMilliseconds != null) {
if (videoController?.value.position ==
videoController?.value.duration) {
nextMediaOrExit();
}
}
});
// ignore: invalid_return_type_for_catch_error, argument_type_not_assignable_to_error_handler
}).catchError(Log.error);
} else {
if (currentMediaLocal.mediaFile.displayLimitInMilliseconds != null) {
canBeSeenUntil = DateTime.now().add(
Duration(seconds: content.maxShowTime),
Duration(
milliseconds:
currentMediaLocal.mediaFile.displayLimitInMilliseconds!,
),
);
startTimer();
}
}
setState(() {
maxShowTime = content.maxShowTime;
isDownloading = false;
mirrorVideo = content.mirrorVideo;
currentMedia = currentMediaLocal;
});
}
@ -299,44 +272,37 @@ class _MediaViewerViewState extends State<MediaViewerView> {
if (canBeSeenUntil != null) {
final difference = canBeSeenUntil!.difference(DateTime.now());
// Calculate the progress as a value between 0.0 and 1.0
progress = difference.inMilliseconds / (maxShowTime * 1000);
progress = difference.inMilliseconds /
(currentMedia!.mediaFile.displayLimitInMilliseconds!);
setState(() {});
}
});
}
Future<void> onPressedSaveToGallery() async {
if (allMediaFiles.first.messageOtherId == null) {
return; // should not be possible
}
setState(() {
imageSaving = true;
});
await twonlyDB.messagesDao.updateMessageByMessageId(
allMediaFiles.first.messageId,
const MessagesCompanion(mediaStored: Value(true)),
);
await encryptAndSendMessageAsync(
null,
widget.contact.userId,
MessageJson(
kind: MessageKind.storedMediaFile,
messageSenderId: allMediaFiles.first.messageId,
messageReceiverId: allMediaFiles.first.messageOtherId,
content: MessageContent(),
timestamp: DateTime.now(),
await currentMedia!.storeMediaFile();
await sendCipherTextToGroup(
widget.group.groupId,
pb.EncryptedContent(
mediaUpdate: pb.EncryptedContent_MediaUpdate(
type: pb.EncryptedContent_MediaUpdate_Type.STORED,
targetMediaId: currentMedia!.mediaFile.mediaId,
),
),
pushNotification: PushNotification(kind: PushKind.storedMediaFile),
);
setState(() {
imageSaved = true;
});
final user = await getUser();
if (user != null && (user.storeMediaFilesInGallery)) {
if (videoPath != null) {
await saveVideoToGallery(videoPath!);
} else {
await saveImageToGallery(imageBytes!);
if (gUser.storeMediaFilesInGallery) {
if (currentMedia!.mediaFile.type == MediaType.video) {
await saveVideoToGallery(currentMedia!.storedPath.path);
} else if (currentMedia!.mediaFile.type == MediaType.image) {
final imageBytes = await currentMedia!.storedPath.readAsBytes();
await saveImageToGallery(imageBytes);
}
}
setState(() {
@ -358,7 +324,8 @@ class _MediaViewerViewState extends State<MediaViewerView> {
key: mediaWidgetKey,
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (maxShowTime == gMediaShowInfinite)
if (currentMedia != null &&
currentMedia!.mediaFile.displayLimitInMilliseconds == null)
OutlinedButton(
style: OutlinedButton.styleFrom(
iconColor: imageSaved
@ -368,7 +335,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
? Theme.of(context).colorScheme.outline
: Theme.of(context).colorScheme.primary,
),
onPressed: onPressedSaveToGallery,
onPressed: (currentMedia == null) ? null : onPressedSaveToGallery,
child: Row(
children: [
if (imageSaving)
@ -450,11 +417,12 @@ class _MediaViewerViewState extends State<MediaViewerView> {
context,
MaterialPageRoute(
builder: (context) {
return CameraSendToView(widget.contact);
return CameraSendToView(widget.group);
},
),
);
if (mounted && maxShowTime != gMediaShowInfinite) {
if (mounted &&
currentMedia!.mediaFile.displayLimitInMilliseconds != null) {
await nextMediaOrExit();
} else {
await videoController?.play();
@ -477,7 +445,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
child: Stack(
fit: StackFit.expand,
children: [
if ((imageBytes != null || videoController != null) &&
if ((currentMedia != null || videoController != null) &&
(canBeSeenUntil == null || progress >= 0))
GestureDetector(
onTap: () {
@ -497,15 +465,12 @@ class _MediaViewerViewState extends State<MediaViewerView> {
children: [
if (videoController != null)
Positioned.fill(
child: Transform.flip(
flipX: mirrorVideo,
child: VideoPlayer(videoController!),
),
child: VideoPlayer(videoController!),
),
if (imageBytes != null)
if (currentMedia!.mediaFile.type == MediaType.image)
Positioned.fill(
child: Image.memory(
imageBytes!,
child: Image.file(
currentMedia!.tempPath,
fit: BoxFit.contain,
frameBuilder: (
context,
@ -534,7 +499,9 @@ class _MediaViewerViewState extends State<MediaViewerView> {
),
),
),
if (isRealTwonly && imageBytes == null)
if (currentMedia != null &&
currentMedia!.mediaFile.requiresAuthentication &&
displayTwonlyPresent)
Positioned.fill(
child: GestureDetector(
onTap: () {
@ -570,7 +537,8 @@ class _MediaViewerViewState extends State<MediaViewerView> {
],
),
),
if (isDownloading)
if (currentMedia?.mediaFile.downloadState !=
DownloadState.downloaded)
const Positioned.fill(
child: Center(
child: SizedBox(
@ -602,7 +570,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
left: showSendTextMessageInput ? 0 : null,
right: showSendTextMessageInput ? 0 : 15,
child: Text(
getContactDisplayName(widget.contact),
widget.group.groupName,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: showSendTextMessageInput ? 24 : 14,
@ -658,18 +626,12 @@ class _MediaViewerViewState extends State<MediaViewerView> {
),
IconButton(
icon: const FaIcon(FontAwesomeIcons.solidPaperPlane),
onPressed: () {
onPressed: () async {
if (textMessageController.text.isNotEmpty) {
sendTextMessage(
widget.contact.userId,
TextMessageContent(
text: textMessageController.text,
responseToMessageId:
allMediaFiles.first.messageOtherId,
),
PushNotification(
kind: PushKind.response,
),
await insertAndSendTextMessage(
widget.group.groupId,
textMessageController.text,
currentMessage!.messageId,
);
textMessageController.clear();
}
@ -683,14 +645,13 @@ class _MediaViewerViewState extends State<MediaViewerView> {
),
),
),
if (allMediaFiles.isNotEmpty)
if (currentMedia != null)
ReactionButtons(
show: showShortReactions,
textInputFocused: showSendTextMessageInput,
mediaViewerDistanceFromBottom: mediaViewerDistanceFromBottom,
userId: widget.contact.userId,
responseToMessageId: allMediaFiles.first.messageOtherId!,
isVideo: videoController != null,
groupId: widget.group.groupId,
messageId: currentMessage!.messageId,
hide: () {
setState(() {
showShortReactions = false;
@ -704,195 +665,3 @@ class _MediaViewerViewState extends State<MediaViewerView> {
);
}
}
class ReactionButtons extends StatefulWidget {
const ReactionButtons({
required this.show,
required this.textInputFocused,
required this.userId,
required this.mediaViewerDistanceFromBottom,
required this.responseToMessageId,
required this.isVideo,
required this.hide,
super.key,
});
final double mediaViewerDistanceFromBottom;
final bool show;
final bool isVideo;
final bool textInputFocused;
final int userId;
final int responseToMessageId;
final void Function() hide;
@override
State<ReactionButtons> createState() => _ReactionButtonsState();
}
class _ReactionButtonsState extends State<ReactionButtons> {
int selectedShortReaction = -1;
List<String> selectedEmojis =
EmojiAnimation.animatedIcons.keys.toList().sublist(0, 6);
@override
void initState() {
super.initState();
initAsync();
}
Future<void> initAsync() async {
final user = await getUser();
if (user != null && user.preSelectedEmojies != null) {
selectedEmojis = user.preSelectedEmojies!;
}
setState(() {});
}
@override
Widget build(BuildContext context) {
final firstRowEmojis = selectedEmojis.take(6).toList();
final secondRowEmojis =
selectedEmojis.length > 6 ? selectedEmojis.skip(6).toList() : [];
return AnimatedPositioned(
duration: const Duration(milliseconds: 200), // Animation duration
bottom: widget.show
? (widget.textInputFocused
? 50
: widget.mediaViewerDistanceFromBottom)
: widget.mediaViewerDistanceFromBottom - 20,
left: widget.show ? 0 : MediaQuery.sizeOf(context).width / 2,
right: widget.show ? 0 : MediaQuery.sizeOf(context).width / 2,
curve: Curves.linearToEaseOut,
child: AnimatedOpacity(
opacity: widget.show ? 1.0 : 0.0, // Fade in/out
duration: const Duration(milliseconds: 150),
child: Container(
color: widget.show ? Colors.black.withAlpha(0) : Colors.transparent,
padding:
widget.show ? const EdgeInsets.symmetric(vertical: 32) : null,
child: Column(
children: [
if (secondRowEmojis.isNotEmpty)
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.end,
children: secondRowEmojis
.map(
(emoji) => EmojiReactionWidget(
userId: widget.userId,
responseToMessageId: widget.responseToMessageId,
hide: widget.hide,
show: widget.show,
isVideo: widget.isVideo,
emoji: emoji as String,
),
)
.toList(),
),
if (secondRowEmojis.isNotEmpty) const SizedBox(height: 15),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.end,
children: firstRowEmojis
.map(
(emoji) => EmojiReactionWidget(
userId: widget.userId,
responseToMessageId: widget.responseToMessageId,
hide: widget.hide,
show: widget.show,
isVideo: widget.isVideo,
emoji: emoji,
),
)
.toList(),
),
],
),
),
),
);
}
}
class EmojiReactionWidget extends StatefulWidget {
const EmojiReactionWidget({
required this.userId,
required this.responseToMessageId,
required this.hide,
required this.isVideo,
required this.show,
required this.emoji,
super.key,
});
final int userId;
final int responseToMessageId;
final Function hide;
final bool show;
final bool isVideo;
final String emoji;
@override
State<EmojiReactionWidget> createState() => _EmojiReactionWidgetState();
}
class _EmojiReactionWidgetState extends State<EmojiReactionWidget> {
int selectedShortReaction = -1;
@override
Widget build(BuildContext context) {
return AnimatedSize(
duration: const Duration(milliseconds: 200),
curve: Curves.linearToEaseOut,
child: GestureDetector(
onTap: () {
sendTextMessage(
widget.userId,
TextMessageContent(
text: widget.emoji,
responseToMessageId: widget.responseToMessageId,
),
PushNotification(
kind: widget.isVideo
? PushKind.reactionToVideo
: PushKind.reactionToImage,
reactionContent: widget.emoji,
),
);
setState(() {
selectedShortReaction = 0; // Assuming index is 0 for this example
});
Future.delayed(const Duration(milliseconds: 300), () {
if (mounted) {
setState(() {
widget.hide();
selectedShortReaction = -1;
});
}
});
},
child: (selectedShortReaction ==
0) // Assuming index is 0 for this example
? EmojiAnimationFlying(
emoji: widget.emoji,
duration: const Duration(milliseconds: 300),
startPosition: 0,
size: (widget.show) ? 40 : 10,
)
: AnimatedOpacity(
opacity: (selectedShortReaction == -1) ? 1 : 0, // Fade in/out
duration: const Duration(milliseconds: 150),
child: SizedBox(
width: widget.show ? 40 : 10,
child: Center(
child: EmojiAnimation(
emoji: widget.emoji,
),
),
),
),
),
);
}
}

View file

@ -0,0 +1,92 @@
// ignore_for_file: avoid_dynamic_calls
import 'package:drift/drift.dart' show Value;
import 'package:flutter/material.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
import 'package:twonly/src/services/api/messages.dart';
import 'package:twonly/src/views/components/animate_icon.dart';
class EmojiReactionWidget extends StatefulWidget {
const EmojiReactionWidget({
required this.messageId,
required this.groupId,
required this.hide,
required this.show,
required this.emoji,
super.key,
});
final String messageId;
final String groupId;
final Function hide;
final bool show;
final String emoji;
@override
State<EmojiReactionWidget> createState() => _EmojiReactionWidgetState();
}
class _EmojiReactionWidgetState extends State<EmojiReactionWidget> {
int selectedShortReaction = -1;
@override
Widget build(BuildContext context) {
return AnimatedSize(
duration: const Duration(milliseconds: 200),
curve: Curves.linearToEaseOut,
child: GestureDetector(
onTap: () async {
await twonlyDB.reactionsDao.insertReaction(
ReactionsCompanion(
messageId: Value(widget.messageId),
emoji: Value(widget.emoji),
),
);
await sendCipherTextToGroup(
widget.groupId,
EncryptedContent(
reaction: EncryptedContent_Reaction(
targetMessageId: widget.messageId,
emoji: widget.emoji,
),
),
);
setState(() {
selectedShortReaction = 0; // Assuming index is 0 for this example
});
Future.delayed(const Duration(milliseconds: 300), () {
if (mounted) {
setState(() {
widget.hide();
selectedShortReaction = -1;
});
}
});
},
child: (selectedShortReaction ==
0) // Assuming index is 0 for this example
? EmojiAnimationFlying(
emoji: widget.emoji,
duration: const Duration(milliseconds: 300),
startPosition: 0,
size: (widget.show) ? 40 : 10,
)
: AnimatedOpacity(
opacity: (selectedShortReaction == -1) ? 1 : 0, // Fade in/out
duration: const Duration(milliseconds: 150),
child: SizedBox(
width: widget.show ? 40 : 10,
child: Center(
child: EmojiAnimation(
emoji: widget.emoji,
),
),
),
),
),
);
}
}

View file

@ -0,0 +1,112 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:twonly/src/utils/storage.dart';
import 'package:twonly/src/views/chats/media_viewer_components/emoji_reactions_row.component.dart';
import 'package:twonly/src/views/components/animate_icon.dart';
class ReactionButtons extends StatefulWidget {
const ReactionButtons({
required this.show,
required this.textInputFocused,
required this.mediaViewerDistanceFromBottom,
required this.messageId,
required this.groupId,
required this.hide,
super.key,
});
final double mediaViewerDistanceFromBottom;
final bool show;
final bool textInputFocused;
final String messageId;
final String groupId;
final void Function() hide;
@override
State<ReactionButtons> createState() => _ReactionButtonsState();
}
class _ReactionButtonsState extends State<ReactionButtons> {
int selectedShortReaction = -1;
List<String> selectedEmojis =
EmojiAnimation.animatedIcons.keys.toList().sublist(0, 6);
@override
void initState() {
super.initState();
initAsync();
}
Future<void> initAsync() async {
final user = await getUser();
if (user != null && user.preSelectedEmojies != null) {
selectedEmojis = user.preSelectedEmojies!;
}
setState(() {});
}
@override
Widget build(BuildContext context) {
final firstRowEmojis = selectedEmojis.take(6).toList();
final secondRowEmojis =
selectedEmojis.length > 6 ? selectedEmojis.skip(6).toList() : [];
return AnimatedPositioned(
duration: const Duration(milliseconds: 200), // Animation duration
bottom: widget.show
? (widget.textInputFocused
? 50
: widget.mediaViewerDistanceFromBottom)
: widget.mediaViewerDistanceFromBottom - 20,
left: widget.show ? 0 : MediaQuery.sizeOf(context).width / 2,
right: widget.show ? 0 : MediaQuery.sizeOf(context).width / 2,
curve: Curves.linearToEaseOut,
child: AnimatedOpacity(
opacity: widget.show ? 1.0 : 0.0, // Fade in/out
duration: const Duration(milliseconds: 150),
child: Container(
color: widget.show ? Colors.black.withAlpha(0) : Colors.transparent,
padding:
widget.show ? const EdgeInsets.symmetric(vertical: 32) : null,
child: Column(
children: [
if (secondRowEmojis.isNotEmpty)
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.end,
children: secondRowEmojis
.map(
(emoji) => EmojiReactionWidget(
messageId: widget.messageId,
groupId: widget.groupId,
hide: widget.hide,
show: widget.show,
emoji: emoji as String,
),
)
.toList(),
),
if (secondRowEmojis.isNotEmpty) const SizedBox(height: 15),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.end,
children: firstRowEmojis
.map(
(emoji) => EmojiReactionWidget(
messageId: widget.messageId,
groupId: widget.groupId,
hide: widget.hide,
show: widget.show,
emoji: emoji,
),
)
.toList(),
),
],
),
),
),
);
}
}

View file

@ -1,5 +1,4 @@
import 'dart:async';
import 'package:drift/drift.dart' hide Column;
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
@ -10,8 +9,8 @@ import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/chats/add_new_user.view.dart';
import 'package:twonly/src/views/chats/chat_messages.view.dart';
import 'package:twonly/src/views/components/avatar_icon.component.dart';
import 'package:twonly/src/views/components/flame.dart';
import 'package:twonly/src/views/components/initialsavatar.dart';
import 'package:twonly/src/views/components/user_context_menu.component.dart';
class StartNewChatView extends StatefulWidget {
@ -30,7 +29,7 @@ class _StartNewChatView extends State<StartNewChatView> {
void initState() {
super.initState();
final stream = twonlyDB.contactsDao.watchContactsForStartNewChat();
final stream = twonlyDB.contactsDao.watchAllAcceptedContacts();
contactSub = stream.listen((update) async {
update.sort(
@ -147,7 +146,6 @@ class UserList extends StatelessWidget {
return const Divider();
}
final user = users[i - 2];
final flameCounter = getFlameCounterFromContact(user);
return UserContextMenu(
key: Key(user.userId.toString()),
contact: user,
@ -155,40 +153,37 @@ class UserList extends StatelessWidget {
title: Row(
children: [
Text(getContactDisplayName(user)),
if (flameCounter >= 1)
FlameCounterWidget(
user,
flameCounter,
prefix: true,
),
const Spacer(),
IconButton(
icon: FaIcon(
FontAwesomeIcons.boxOpen,
size: 13,
color: user.archived ? null : Colors.transparent,
),
onPressed: user.archived
? () async {
const update =
ContactsCompanion(archived: Value(false));
await twonlyDB.contactsDao
.updateContact(user.userId, update);
}
: null,
FlameCounterWidget(
contactId: user.userId,
prefix: true,
),
],
),
leading: ContactAvatar(
leading: AvatarIcon(
contact: user,
fontSize: 13,
),
onTap: () async {
var directChat =
await twonlyDB.groupsDao.getDirectChat(user.userId);
if (directChat == null) {
await twonlyDB.groupsDao.insertGroup(
GroupsCompanion(
isDirectChat: const Value(true),
groupName: Value(
getContactDisplayName(user),
),
),
);
directChat =
await twonlyDB.groupsDao.getDirectChat(user.userId);
}
if (!context.mounted) return;
await Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) {
return ChatMessagesView(user);
return ChatMessagesView(directChat!);
},
),
);

View file

@ -0,0 +1,94 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/json/userdata.dart';
import 'package:twonly/src/utils/log.dart';
class AvatarIcon extends StatefulWidget {
const AvatarIcon({
super.key,
this.group,
this.contact,
this.userData,
this.fontSize = 20,
this.color,
});
final Group? group;
final Contact? contact;
final UserData? userData;
final double? fontSize;
final Color? color;
@override
State<AvatarIcon> createState() => _AvatarIconState();
}
class _AvatarIconState extends State<AvatarIcon> {
List<String> avatarSVGs = [];
@override
void initState() {
initAsync();
super.initState();
}
Future<void> initAsync() async {
if (widget.group != null) {
final contacts =
await twonlyDB.groupsDao.getGroupContact(widget.group!.groupId);
if (contacts.length == 1) {
if (contacts.first.avatarSvg != null) {
avatarSVGs.add(contacts.first.avatarSvg!);
}
} else {
for (final contact in contacts) {
if (contact.avatarSvg != null) {
avatarSVGs.add(contact.avatarSvg!);
}
}
}
// avatarSvg = group!.avatarSvg;
} else if (widget.userData?.avatarSvg != null) {
avatarSVGs.add(widget.userData!.avatarSvg!);
} else if (widget.contact?.avatarSvg != null) {
avatarSVGs.add(widget.contact!.avatarSvg!);
}
if (mounted) setState(() {});
}
@override
Widget build(BuildContext context) {
final proSize = (widget.fontSize == null) ? 40 : (widget.fontSize! * 2);
return Container(
constraints: BoxConstraints(
minHeight: 2 * (widget.fontSize ?? 20),
minWidth: 2 * (widget.fontSize ?? 20),
maxWidth: 2 * (widget.fontSize ?? 20),
maxHeight: 2 * (widget.fontSize ?? 20),
),
child: Center(
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Container(
height: proSize as double,
width: proSize,
color: widget.color,
child: Center(
child: avatarSVGs.isEmpty
? SvgPicture.asset('assets/images/default_avatar.svg')
: SvgPicture.string(
avatarSVGs.first,
errorBuilder: (context, error, stackTrace) {
Log.error('$error');
return Container();
},
),
),
),
),
),
);
}
}

View file

@ -1,26 +1,65 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/views/components/animate_icon.dart';
class FlameCounterWidget extends StatelessWidget {
const FlameCounterWidget(
this.user,
this.flameCounter, {
class FlameCounterWidget extends StatefulWidget {
const FlameCounterWidget({
this.groupId,
this.contactId,
this.prefix = false,
super.key,
});
final Contact user;
final int flameCounter;
final String? groupId;
final int? contactId;
final bool prefix;
@override
State<FlameCounterWidget> createState() => _FlameCounterWidgetState();
}
class _FlameCounterWidgetState extends State<FlameCounterWidget> {
int flameCounter = 0;
bool isBestFriend = false;
StreamSubscription<int>? flameCounterSub;
@override
void initState() {
initAsync();
super.initState();
}
@override
void dispose() {
flameCounterSub?.cancel();
super.dispose();
}
Future<void> initAsync() async {
var groupId = widget.groupId;
if (widget.groupId == null && widget.contactId != null) {
final group = await twonlyDB.groupsDao.getDirectChat(widget.contactId!);
groupId = group?.groupId;
}
if (groupId != null) {
isBestFriend = gUser.myBestFriendGroupId == groupId;
final stream = twonlyDB.groupsDao.watchFlameCounter(groupId);
flameCounterSub = stream.listen((counter) {
setState(() {
flameCounter = counter;
});
});
}
}
@override
Widget build(BuildContext context) {
return Row(
children: [
if (prefix) const SizedBox(width: 5),
if (prefix) const Text(''),
if (prefix) const SizedBox(width: 5),
if (widget.prefix) const SizedBox(width: 5),
if (widget.prefix) const Text(''),
if (widget.prefix) const SizedBox(width: 5),
Text(
flameCounter.toString(),
style: const TextStyle(fontSize: 13),
@ -28,7 +67,7 @@ class FlameCounterWidget extends StatelessWidget {
SizedBox(
height: 15,
child: EmojiAnimation(
emoji: (globalBestFriendUserId == user.userId) ? '❤️‍🔥' : '🔥',
emoji: isBestFriend ? '❤️‍🔥' : '🔥',
),
),
],

View file

@ -1,64 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/json/userdata.dart';
import 'package:twonly/src/utils/log.dart';
class ContactAvatar extends StatelessWidget {
const ContactAvatar({
super.key,
this.contact,
this.userData,
this.fontSize = 20,
this.color,
});
final Contact? contact;
final UserData? userData;
final double? fontSize;
final Color? color;
@override
Widget build(BuildContext context) {
String? avatarSvg;
if (contact != null) {
avatarSvg = contact!.avatarSvg;
} else if (userData != null) {
avatarSvg = userData!.avatarSvg;
} else {
return Container();
}
final proSize = (fontSize == null) ? 40 : (fontSize! * 2);
return Container(
constraints: BoxConstraints(
minHeight: 2 * (fontSize ?? 20),
minWidth: 2 * (fontSize ?? 20),
maxWidth: 2 * (fontSize ?? 20),
maxHeight: 2 * (fontSize ?? 20),
),
child: Center(
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Container(
height: proSize as double,
width: proSize,
color: color,
child: Center(
child: avatarSvg == null
? SvgPicture.asset('assets/images/default_avatar.svg')
: SvgPicture.string(
avatarSvg,
errorBuilder: (context, error, stackTrace) {
Log.error('$error');
return Container();
},
),
),
),
),
),
);
}
}

View file

@ -5,8 +5,8 @@ import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/contact/contact.view.dart';
class UserContextMenuBlocked extends StatefulWidget {
const UserContextMenuBlocked({
class UserContextMenu extends StatefulWidget {
const UserContextMenu({
required this.contact,
required this.child,
super.key,
@ -15,10 +15,10 @@ class UserContextMenuBlocked extends StatefulWidget {
final Contact contact;
@override
State<UserContextMenuBlocked> createState() => _UserContextMenuBlocked();
State<UserContextMenu> createState() => _UserContextMenuBlocked();
}
class _UserContextMenuBlocked extends State<UserContextMenuBlocked> {
class _UserContextMenuBlocked extends State<UserContextMenu> {
@override
Widget build(BuildContext context) {
return PieMenu(

View file

@ -7,9 +7,8 @@ import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/services/api/utils.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/components/alert_dialog.dart';
import 'package:twonly/src/views/components/avatar_icon.component.dart';
import 'package:twonly/src/views/components/better_list_title.dart';
import 'package:twonly/src/views/components/flame.dart';
import 'package:twonly/src/views/components/initialsavatar.dart';
import 'package:twonly/src/views/components/verified_shield.dart';
import 'package:twonly/src/views/contact/contact_verify.view.dart';
@ -105,12 +104,12 @@ class _ContactViewState extends State<ContactView> {
return Container();
}
final contact = snapshot.data!;
final flameCounter = getFlameCounterFromContact(contact);
// final flameCounter = getFlameCounterFromContact(contact);
return ListView(
children: [
Padding(
padding: const EdgeInsets.all(10),
child: ContactAvatar(contact: contact, fontSize: 30),
child: AvatarIcon(contact: contact, fontSize: 30),
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
@ -123,12 +122,12 @@ class _ContactViewState extends State<ContactView> {
getContactDisplayName(contact),
style: const TextStyle(fontSize: 20),
),
if (flameCounter > 0)
FlameCounterWidget(
contact,
flameCounter,
prefix: true,
),
// if (flameCounter > 0)
// FlameCounterWidget(
// contact,
// flameCounter,
// prefix: true,
// ),
],
),
if (getContactDisplayName(contact) != contact.username)

View file

@ -0,0 +1,18 @@
import 'package:flutter/material.dart';
import 'package:twonly/src/database/twonly.db.dart';
class GroupView extends StatefulWidget {
const GroupView(this.group, {super.key});
final Group group;
@override
State<GroupView> createState() => _GroupViewState();
}
class _GroupViewState extends State<GroupView> {
@override
Widget build(BuildContext context) {
return const Placeholder();
}
}

View file

@ -208,7 +208,8 @@ class _MemoriesPhotoSliderViewState extends State<MemoriesPhotoSliderView> {
minScale: PhotoViewComputedScale.contained,
maxScale: PhotoViewComputedScale.covered * 4.1,
heroAttributes: PhotoViewHeroAttributes(
tag: item.mediaService.mediaFile.mediaId),
tag: item.mediaService.mediaFile.mediaId,
),
)
: PhotoViewGalleryPageOptions(
imageProvider: FileImage(item.mediaService.storedPath),
@ -216,7 +217,8 @@ class _MemoriesPhotoSliderViewState extends State<MemoriesPhotoSliderView> {
minScale: PhotoViewComputedScale.contained,
maxScale: PhotoViewComputedScale.covered * 4.1,
heroAttributes: PhotoViewHeroAttributes(
tag: item.mediaService.mediaFile.mediaId),
tag: item.mediaService.mediaFile.mediaId,
),
);
}
}

View file

@ -43,20 +43,18 @@ class _AutomatedTestingViewState extends State<AutomatedTestingView> {
await twonlyDB.contactsDao.getContactsByUsername(username);
for (final contact in contacts) {
final groups =
final group =
await twonlyDB.groupsDao.getDirectChat(contact.userId);
for (final group in groups) {
for (var i = 0; i < 200; i++) {
setState(() {
lotsOfMessagesStatus =
'At message $i to ${contact.username}.';
});
await insertAndSendTextMessage(
group.groupId,
'Message $i.',
);
}
for (var i = 0; i < 200; i++) {
setState(() {
lotsOfMessagesStatus =
'At message $i to ${contact.username}.';
});
await insertAndSendTextMessage(
group!.groupId,
'Message $i.',
null,
);
}
}
},

View file

@ -1,9 +1,6 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/services/flame.service.dart';
import 'package:twonly/src/utils/storage.dart';
import 'package:twonly/src/views/settings/developer/automated_testing.view.dart';
import 'package:twonly/src/views/settings/developer/retransmission_data.view.dart';
@ -69,14 +66,14 @@ class _DeveloperSettingsViewState extends State<DeveloperSettingsView> {
);
},
),
if (kDebugMode)
ListTile(
title: const Text('FlameSync Test'),
onTap: () async {
await twonlyDB.contactsDao.modifyFlameCounterForTesting();
await syncFlameCounters();
},
),
// if (kDebugMode)
// ListTile(
// title: const Text('FlameSync Test'),
// onTap: () async {
// await twonlyDB.contactsDao.modifyFlameCounterForTesting();
// await syncFlameCounters();
// },
// ),
if (kDebugMode)
ListTile(
title: const Text('Automated Testing'),

View file

@ -5,7 +5,7 @@ import 'package:twonly/globals.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/components/initialsavatar.dart';
import 'package:twonly/src/views/components/avatar_icon.component.dart';
import 'package:twonly/src/views/components/user_context_menu.component.dart';
class PrivacyViewBlockUsers extends StatefulWidget {
@ -108,7 +108,7 @@ class UserList extends StatelessWidget {
itemCount: users.length,
itemBuilder: (BuildContext context, int i) {
final user = users[i];
return UserContextMenuBlocked(
return UserContextMenu(
contact: user,
child: ListTile(
title: Row(
@ -116,7 +116,7 @@ class UserList extends StatelessWidget {
Text(getContactDisplayName(user)),
],
),
leading: ContactAvatar(contact: user, fontSize: 15),
leading: AvatarIcon(contact: user, fontSize: 15),
trailing: Checkbox(
value: user.blocked,
onChanged: (bool? value) async {

View file

@ -5,8 +5,8 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/src/model/json/userdata.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart';
import 'package:twonly/src/views/components/avatar_icon.component.dart';
import 'package:twonly/src/views/components/better_list_title.dart';
import 'package:twonly/src/views/components/initialsavatar.dart';
import 'package:twonly/src/views/settings/account.view.dart';
import 'package:twonly/src/views/settings/appearance.view.dart';
import 'package:twonly/src/views/settings/backup/backup.view.dart';
@ -72,7 +72,7 @@ class _SettingsMainViewState extends State<SettingsMainView> {
color: context.color.surface.withAlpha(0),
child: Row(
children: [
ContactAvatar(
AvatarIcon(
userData: userData,
fontSize: 30,
),