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 { Future<void> setUserPlan() async {
final user = await getUser(); final user = await getUser();
globalBestFriendUserId = -1;
if (user != null && mounted) { 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) { if (mounted) {
await context await context
.read<CustomChangeProvider>() .read<CustomChangeProvider>()

View file

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

View file

@ -47,7 +47,6 @@ void main() async {
await initFileDownloader(); await initFileDownloader();
// await twonlyDB.messagesDao.resetPendingDownloadState(); // await twonlyDB.messagesDao.resetPendingDownloadState();
// await twonlyDB.messagesDao.handleMediaFilesOlderThan30Days();
// await twonlyDB.messageRetransmissionDao.purgeOldRetransmissions(); // await twonlyDB.messageRetransmissionDao.purgeOldRetransmissions();
// await twonlyDB.signalDao.purgeOutDatedPreKeys(); // 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) { SingleOrNullSelectable<Contact> getContactByUserId(int userId) {
return select(contacts)..where((t) => t.userId.equals(userId)); return select(contacts)..where((t) => t.userId.equals(userId));
} }
@ -135,37 +62,6 @@ class ContactsDao extends DatabaseAccessor<TwonlyDB> with _$ContactsDaoMixin {
.watchSingleOrNull(); .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() { Future<List<Contact>> getAllNotBlockedContacts() {
return (select(contacts)..where((t) => t.blocked.equals(false))).get(); 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(); 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() { Stream<List<Contact>> watchAllContacts() {
return select(contacts).watch(); 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) { String getContactDisplayName(Contact user) {
@ -234,18 +114,3 @@ String getContactDisplayName(Contact user) {
String applyStrikethrough(String text) { String applyStrikethrough(String text) {
return text.split('').map((char) => '$char\u0336').join(); 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(); .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([ final query = (select(groups).join([
leftOuterJoin( leftOuterJoin(
groupMembers, groupMembers,
@ -40,8 +89,94 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
groupMembers.contactId.equals(userId), 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(); .getSingleOrNull();
} }
Stream<MediaFile?> watchMedia(String mediaId) {
return (select(mediaFiles)..where((t) => t.mediaId.equals(mediaId)))
.watchSingleOrNull();
}
Future<void> resetPendingDownloadState() async { Future<void> resetPendingDownloadState() async {
await (update(mediaFiles) await (update(mediaFiles)
..where( ..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/groups.table.dart';
import 'package:twonly/src/database/tables/mediafiles.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/messages.table.dart';
import 'package:twonly/src/database/tables/reactions.table.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
@ -15,7 +16,9 @@ part 'messages.dao.g.dart';
Messages, Messages,
Contacts, Contacts,
MediaFiles, MediaFiles,
Reactions,
MessageHistories, MessageHistories,
GroupMembers,
MessageActions, MessageActions,
Groups, Groups,
], ],
@ -26,55 +29,39 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
// ignore: matching_super_parameters // ignore: matching_super_parameters
MessagesDao(super.db); MessagesDao(super.db);
// Stream<List<Message>> watchMessageNotOpened(int contactId) { Stream<List<Message>> watchMessageNotOpened(String groupId) {
// return (select(messages) return (select(messages)
// ..where( ..where((t) => t.openedAt.isNull() & t.groupId.equals(groupId))
// (t) => ..orderBy([(t) => OrderingTerm.desc(t.createdAt)]))
// t.openedAt.isNull() & .watch();
// t.contactId.equals(contactId) & }
// t.errorWhileSending.equals(false),
// )
// ..orderBy([(t) => OrderingTerm.desc(t.sendAt)]))
// .watch();
// }
// Stream<List<Message>> watchMediaMessageNotOpened(int contactId) { Stream<List<Message>> watchMediaNotOpened(String groupId) {
// return (select(messages) return (select(messages)
// ..where( ..where(
// (t) => (t) =>
// t.openedAt.isNull() & t.openedAt.isNull() &
// t.contactId.equals(contactId) & t.groupId.equals(groupId) &
// t.errorWhileSending.equals(false) & t.senderId.isNotNull() &
// t.messageOtherId.isNotNull() & t.type.equals(MessageType.media.name),
// t.kind.equals(MessageKind.media.name), )
// ) ..orderBy([(t) => OrderingTerm.asc(t.createdAt)]))
// ..orderBy([(t) => OrderingTerm.asc(t.sendAt)])) .watch();
// .watch(); }
// }
// Stream<List<Message>> watchLastMessage(int contactId) { Stream<List<Message>> watchLastMessage(String groupId) {
// return (select(messages) return (select(messages)
// ..where((t) => t.contactId.equals(contactId)) ..where((t) => t.groupId.equals(groupId))
// ..orderBy([(t) => OrderingTerm.desc(t.sendAt)]) ..orderBy([(t) => OrderingTerm.desc(t.createdAt)])
// ..limit(1)) ..limit(1))
// .watch(); .watch();
// } }
// Stream<List<Message>> watchAllMessagesFrom(int contactId) { Stream<List<Message>> watchByGroupId(String groupId) {
// return (select(messages) return ((select(messages)..where((t) => t.groupId.equals(groupId)))
// ..where( ..orderBy([(t) => OrderingTerm.asc(t.createdAt)]))
// (t) => .watch();
// 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();
// }
// Future<void> removeOldMessages() { // Future<void> removeOldMessages() {
// return (update(messages) // return (update(messages)
@ -92,22 +79,6 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
// .write(const MessagesCompanion(contentJson: Value(null))); // .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() { // Future<List<Message>> getAllMessagesPendingDownloading() {
// return (select(messages) // return (select(messages)
// ..where( // ..where(
@ -155,18 +126,18 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
// .get(); // .get();
// } // }
// Future<void> openedAllNonMediaMessages(int contactId) { Future<void> openedAllTextMessages(String groupId) {
// final updates = MessagesCompanion(openedAt: Value(DateTime.now())); final updates = MessagesCompanion(openedAt: Value(DateTime.now()));
// return (update(messages) return (update(messages)
// ..where( ..where(
// (t) => (t) =>
// t.contactId.equals(contactId) & t.groupId.equals(groupId) &
// t.messageOtherId.isNotNull() & t.senderId.isNotNull() &
// t.openedAt.isNull() & t.openedAt.isNull() &
// t.kind.equals(MessageKind.media.name).not(), t.type.equals(MessageType.text.name),
// )) ))
// .write(updates); .write(updates);
// } }
Future<void> handleMessageDeletion( Future<void> handleMessageDeletion(
int contactId, 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( // Future<void> updateMessageByOtherUser(
// int userId, // int userId,
// int messageId, // 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) { // Future<void> deleteMessagesByContactId(int contactId) {
// return (delete(messages) // return (delete(messages)
// ..where( // ..where(
@ -342,9 +352,9 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
// .go(); // .go();
// } // }
// Future<void> deleteMessagesByMessageId(int messageId) { Future<void> deleteMessagesById(String messageId) {
// return (delete(messages)..where((t) => t.messageId.equals(messageId))).go(); return (delete(messages)..where((t) => t.messageId.equals(messageId))).go();
// } }
// Future<void> deleteAllMessagesByContactId(int contactId) { // Future<void> deleteAllMessagesByContactId(int contactId) {
// return (delete(messages)..where((t) => t.contactId.equals(contactId))).go(); // 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; $ContactsTable get contacts => attachedDatabase.contacts;
$MediaFilesTable get mediaFiles => attachedDatabase.mediaFiles; $MediaFilesTable get mediaFiles => attachedDatabase.mediaFiles;
$MessagesTable get messages => attachedDatabase.messages; $MessagesTable get messages => attachedDatabase.messages;
$ReactionsTable get reactions => attachedDatabase.reactions;
$MessageHistoriesTable get messageHistories => $MessageHistoriesTable get messageHistories =>
attachedDatabase.messageHistories; attachedDatabase.messageHistories;
$GroupMembersTable get groupMembers => attachedDatabase.groupMembers;
$MessageActionsTable get messageActions => attachedDatabase.messageActions; $MessageActionsTable get messageActions => attachedDatabase.messageActions;
} }

View file

@ -43,4 +43,15 @@ class ReactionsDao extends DatabaseAccessor<TwonlyDB> with _$ReactionsDaoMixin {
Log.error(e); 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 accepted => boolean().withDefault(const Constant(false))();
BoolColumn get requested => 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 blocked => boolean().withDefault(const Constant(false))();
BoolColumn get verified => boolean().withDefault(const Constant(false))(); BoolColumn get verified => boolean().withDefault(const Constant(false))();
BoolColumn get deleted => 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)(); 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 @override
Set<Column> get primaryKey => {userId}; Set<Column> get primaryKey => {userId};
} }

View file

@ -7,15 +7,31 @@ class Groups extends Table {
TextColumn get groupId => text().clientDefault(() => uuid.v4())(); TextColumn get groupId => text().clientDefault(() => uuid.v4())();
BoolColumn get isGroupAdmin => boolean()(); BoolColumn get isGroupAdmin => boolean()();
BoolColumn get isGroupOfTwo => boolean()(); BoolColumn get isDirectChat => boolean()();
BoolColumn get pinned => boolean().withDefault(const Constant(false))(); BoolColumn get pinned => boolean().withDefault(const Constant(false))();
BoolColumn get archived => boolean().withDefault(const Constant(false))(); BoolColumn get archived => boolean().withDefault(const Constant(false))();
TextColumn get groupName => text()(); 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 => DateTimeColumn get lastMessageExchange =>
dateTime().withDefault(currentDateAndTime)(); dateTime().withDefault(currentDateAndTime)();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
@override @override
Set<Column> get primaryKey => {groupId}; Set<Column> get primaryKey => {groupId};

View file

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

View file

@ -42,7 +42,7 @@ part 'twonly.db.g.dart';
SignalSessionStores, SignalSessionStores,
SignalContactPreKeys, SignalContactPreKeys,
SignalContactSignedPreKeys, SignalContactSignedPreKeys,
MessageActions MessageActions,
], ],
daos: [ daos: [
MessagesDao, 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; List<String>? tutorialDisplayed;
int? myBestFriendContactId; String? myBestFriendGroupId;
DateTime? signalLastSignedPreKeyUpdated; DateTime? signalLastSignedPreKeyUpdated;

View file

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

View file

@ -663,14 +663,14 @@ class EncryptedContent_Media extends $pb.GeneratedMessage {
class EncryptedContent_MediaUpdate extends $pb.GeneratedMessage { class EncryptedContent_MediaUpdate extends $pb.GeneratedMessage {
factory EncryptedContent_MediaUpdate({ factory EncryptedContent_MediaUpdate({
EncryptedContent_MediaUpdate_Type? type, EncryptedContent_MediaUpdate_Type? type,
$core.String? targetMessageId, $core.String? targetMediaId,
}) { }) {
final $result = create(); final $result = create();
if (type != null) { if (type != null) {
$result.type = type; $result.type = type;
} }
if (targetMessageId != null) { if (targetMediaId != null) {
$result.targetMessageId = targetMessageId; $result.targetMediaId = targetMediaId;
} }
return $result; return $result;
} }
@ -680,7 +680,7 @@ class EncryptedContent_MediaUpdate extends $pb.GeneratedMessage {
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'EncryptedContent.MediaUpdate', createEmptyInstance: create) 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) ..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 ..hasRequiredFields = false
; ;
@ -715,13 +715,13 @@ class EncryptedContent_MediaUpdate extends $pb.GeneratedMessage {
void clearType() => clearField(1); void clearType() => clearField(1);
@$pb.TagNumber(2) @$pb.TagNumber(2)
$core.String get targetMessageId => $_getSZ(1); $core.String get targetMediaId => $_getSZ(1);
@$pb.TagNumber(2) @$pb.TagNumber(2)
set targetMessageId($core.String v) { $_setString(1, v); } set targetMediaId($core.String v) { $_setString(1, v); }
@$pb.TagNumber(2) @$pb.TagNumber(2)
$core.bool hasTargetMessageId() => $_has(1); $core.bool hasTargetMediaId() => $_has(1);
@$pb.TagNumber(2) @$pb.TagNumber(2)
void clearTargetMessageId() => clearField(2); void clearTargetMediaId() => clearField(2);
} }
class EncryptedContent_ContactRequest extends $pb.GeneratedMessage { class EncryptedContent_ContactRequest extends $pb.GeneratedMessage {
@ -1025,8 +1025,8 @@ class EncryptedContent_FlameSync extends $pb.GeneratedMessage {
class EncryptedContent extends $pb.GeneratedMessage { class EncryptedContent extends $pb.GeneratedMessage {
factory EncryptedContent({ factory EncryptedContent({
$core.String? groupId, $core.String? groupId,
$core.bool? isDirectChat,
$fixnum.Int64? senderProfileCounter, $fixnum.Int64? senderProfileCounter,
EncryptedContent_TextMessage? textMessage,
EncryptedContent_MessageUpdate? messageUpdate, EncryptedContent_MessageUpdate? messageUpdate,
EncryptedContent_Media? media, EncryptedContent_Media? media,
EncryptedContent_MediaUpdate? mediaUpdate, EncryptedContent_MediaUpdate? mediaUpdate,
@ -1035,17 +1035,18 @@ class EncryptedContent extends $pb.GeneratedMessage {
EncryptedContent_FlameSync? flameSync, EncryptedContent_FlameSync? flameSync,
EncryptedContent_PushKeys? pushKeys, EncryptedContent_PushKeys? pushKeys,
EncryptedContent_Reaction? reaction, EncryptedContent_Reaction? reaction,
EncryptedContent_TextMessage? textMessage,
}) { }) {
final $result = create(); final $result = create();
if (groupId != null) { if (groupId != null) {
$result.groupId = groupId; $result.groupId = groupId;
} }
if (isDirectChat != null) {
$result.isDirectChat = isDirectChat;
}
if (senderProfileCounter != null) { if (senderProfileCounter != null) {
$result.senderProfileCounter = senderProfileCounter; $result.senderProfileCounter = senderProfileCounter;
} }
if (textMessage != null) {
$result.textMessage = textMessage;
}
if (messageUpdate != null) { if (messageUpdate != null) {
$result.messageUpdate = messageUpdate; $result.messageUpdate = messageUpdate;
} }
@ -1070,6 +1071,9 @@ class EncryptedContent extends $pb.GeneratedMessage {
if (reaction != null) { if (reaction != null) {
$result.reaction = reaction; $result.reaction = reaction;
} }
if (textMessage != null) {
$result.textMessage = textMessage;
}
return $result; return $result;
} }
EncryptedContent._() : super(); EncryptedContent._() : super();
@ -1078,8 +1082,8 @@ class EncryptedContent extends $pb.GeneratedMessage {
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'EncryptedContent', createEmptyInstance: create) static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'EncryptedContent', createEmptyInstance: create)
..aOS(2, _omitFieldNames ? '' : 'groupId', protoName: 'groupId') ..aOS(2, _omitFieldNames ? '' : 'groupId', protoName: 'groupId')
..aInt64(3, _omitFieldNames ? '' : 'senderProfileCounter', protoName: 'senderProfileCounter') ..aOB(3, _omitFieldNames ? '' : 'isDirectChat', protoName: 'isDirectChat')
..aOM<EncryptedContent_TextMessage>(4, _omitFieldNames ? '' : 'textMessage', protoName: 'textMessage', subBuilder: EncryptedContent_TextMessage.create) ..aInt64(4, _omitFieldNames ? '' : 'senderProfileCounter', protoName: 'senderProfileCounter')
..aOM<EncryptedContent_MessageUpdate>(5, _omitFieldNames ? '' : 'messageUpdate', protoName: 'messageUpdate', subBuilder: EncryptedContent_MessageUpdate.create) ..aOM<EncryptedContent_MessageUpdate>(5, _omitFieldNames ? '' : 'messageUpdate', protoName: 'messageUpdate', subBuilder: EncryptedContent_MessageUpdate.create)
..aOM<EncryptedContent_Media>(6, _omitFieldNames ? '' : 'media', subBuilder: EncryptedContent_Media.create) ..aOM<EncryptedContent_Media>(6, _omitFieldNames ? '' : 'media', subBuilder: EncryptedContent_Media.create)
..aOM<EncryptedContent_MediaUpdate>(7, _omitFieldNames ? '' : 'mediaUpdate', protoName: 'mediaUpdate', subBuilder: EncryptedContent_MediaUpdate.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_FlameSync>(10, _omitFieldNames ? '' : 'flameSync', protoName: 'flameSync', subBuilder: EncryptedContent_FlameSync.create)
..aOM<EncryptedContent_PushKeys>(11, _omitFieldNames ? '' : 'pushKeys', protoName: 'pushKeys', subBuilder: EncryptedContent_PushKeys.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_Reaction>(12, _omitFieldNames ? '' : 'reaction', subBuilder: EncryptedContent_Reaction.create)
..aOM<EncryptedContent_TextMessage>(13, _omitFieldNames ? '' : 'textMessage', protoName: 'textMessage', subBuilder: EncryptedContent_TextMessage.create)
..hasRequiredFields = false ..hasRequiredFields = false
; ;
@ -1121,26 +1126,24 @@ class EncryptedContent extends $pb.GeneratedMessage {
@$pb.TagNumber(2) @$pb.TagNumber(2)
void clearGroupId() => clearField(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) @$pb.TagNumber(3)
$fixnum.Int64 get senderProfileCounter => $_getI64(1); $core.bool get isDirectChat => $_getBF(1);
@$pb.TagNumber(3) @$pb.TagNumber(3)
set senderProfileCounter($fixnum.Int64 v) { $_setInt64(1, v); } set isDirectChat($core.bool v) { $_setBool(1, v); }
@$pb.TagNumber(3) @$pb.TagNumber(3)
$core.bool hasSenderProfileCounter() => $_has(1); $core.bool hasIsDirectChat() => $_has(1);
@$pb.TagNumber(3) @$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) @$pb.TagNumber(4)
EncryptedContent_TextMessage get textMessage => $_getN(2); $fixnum.Int64 get senderProfileCounter => $_getI64(2);
@$pb.TagNumber(4) @$pb.TagNumber(4)
set textMessage(EncryptedContent_TextMessage v) { setField(4, v); } set senderProfileCounter($fixnum.Int64 v) { $_setInt64(2, v); }
@$pb.TagNumber(4) @$pb.TagNumber(4)
$core.bool hasTextMessage() => $_has(2); $core.bool hasSenderProfileCounter() => $_has(2);
@$pb.TagNumber(4) @$pb.TagNumber(4)
void clearTextMessage() => clearField(4); void clearSenderProfileCounter() => clearField(4);
@$pb.TagNumber(4)
EncryptedContent_TextMessage ensureTextMessage() => $_ensure(2);
@$pb.TagNumber(5) @$pb.TagNumber(5)
EncryptedContent_MessageUpdate get messageUpdate => $_getN(3); EncryptedContent_MessageUpdate get messageUpdate => $_getN(3);
@ -1229,6 +1232,17 @@ class EncryptedContent extends $pb.GeneratedMessage {
void clearReaction() => clearField(12); void clearReaction() => clearField(12);
@$pb.TagNumber(12) @$pb.TagNumber(12)
EncryptedContent_Reaction ensureReaction() => $_ensure(10); 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', '1': 'EncryptedContent',
'2': [ '2': [
{'1': 'groupId', '3': 2, '4': 1, '5': 9, '9': 0, '10': 'groupId', '17': true}, {'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': 'isDirectChat', '3': 3, '4': 1, '5': 8, '9': 1, '10': 'isDirectChat', '17': true},
{'1': 'textMessage', '3': 4, '4': 1, '5': 11, '6': '.EncryptedContent.TextMessage', '9': 2, '10': 'textMessage', '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': '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': '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}, {'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': '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': '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': '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], '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': [ '8': [
{'1': '_groupId'}, {'1': '_groupId'},
{'1': '_isDirectChat'},
{'1': '_senderProfileCounter'}, {'1': '_senderProfileCounter'},
{'1': '_textMessage'},
{'1': '_messageUpdate'}, {'1': '_messageUpdate'},
{'1': '_media'}, {'1': '_media'},
{'1': '_mediaUpdate'}, {'1': '_mediaUpdate'},
@ -119,6 +120,7 @@ const EncryptedContent$json = {
{'1': '_flameSync'}, {'1': '_flameSync'},
{'1': '_pushKeys'}, {'1': '_pushKeys'},
{'1': '_reaction'}, {'1': '_reaction'},
{'1': '_textMessage'},
], ],
}; };
@ -219,7 +221,7 @@ const EncryptedContent_MediaUpdate$json = {
'1': 'MediaUpdate', '1': 'MediaUpdate',
'2': [ '2': [
{'1': 'type', '3': 1, '4': 1, '5': 14, '6': '.EncryptedContent.MediaUpdate.Type', '10': 'type'}, {'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], '4': [EncryptedContent_MediaUpdate_Type$json],
}; };
@ -315,59 +317,60 @@ const EncryptedContent_FlameSync$json = {
/// Descriptor for `EncryptedContent`. Decode as a `google.protobuf.DescriptorProto`. /// Descriptor for `EncryptedContent`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List encryptedContentDescriptor = $convert.base64Decode( final $typed_data.Uint8List encryptedContentDescriptor = $convert.base64Decode(
'ChBFbmNyeXB0ZWRDb250ZW50Eh0KB2dyb3VwSWQYAiABKAlIAFIHZ3JvdXBJZIgBARI3ChRzZW' 'ChBFbmNyeXB0ZWRDb250ZW50Eh0KB2dyb3VwSWQYAiABKAlIAFIHZ3JvdXBJZIgBARInCgxpc0'
'5kZXJQcm9maWxlQ291bnRlchgDIAEoA0gBUhRzZW5kZXJQcm9maWxlQ291bnRlcogBARJECgt0' 'RpcmVjdENoYXQYAyABKAhIAVIMaXNEaXJlY3RDaGF0iAEBEjcKFHNlbmRlclByb2ZpbGVDb3Vu'
'ZXh0TWVzc2FnZRgEIAEoCzIdLkVuY3J5cHRlZENvbnRlbnQuVGV4dE1lc3NhZ2VIAlILdGV4dE' 'dGVyGAQgASgDSAJSFHNlbmRlclByb2ZpbGVDb3VudGVyiAEBEkoKDW1lc3NhZ2VVcGRhdGUYBS'
'1lc3NhZ2WIAQESSgoNbWVzc2FnZVVwZGF0ZRgFIAEoCzIfLkVuY3J5cHRlZENvbnRlbnQuTWVz' 'ABKAsyHy5FbmNyeXB0ZWRDb250ZW50Lk1lc3NhZ2VVcGRhdGVIA1INbWVzc2FnZVVwZGF0ZYgB'
'c2FnZVVwZGF0ZUgDUg1tZXNzYWdlVXBkYXRliAEBEjIKBW1lZGlhGAYgASgLMhcuRW5jcnlwdG' 'ARIyCgVtZWRpYRgGIAEoCzIXLkVuY3J5cHRlZENvbnRlbnQuTWVkaWFIBFIFbWVkaWGIAQESRA'
'VkQ29udGVudC5NZWRpYUgEUgVtZWRpYYgBARJECgttZWRpYVVwZGF0ZRgHIAEoCzIdLkVuY3J5' 'oLbWVkaWFVcGRhdGUYByABKAsyHS5FbmNyeXB0ZWRDb250ZW50Lk1lZGlhVXBkYXRlSAVSC21l'
'cHRlZENvbnRlbnQuTWVkaWFVcGRhdGVIBVILbWVkaWFVcGRhdGWIAQESSgoNY29udGFjdFVwZG' 'ZGlhVXBkYXRliAEBEkoKDWNvbnRhY3RVcGRhdGUYCCABKAsyHy5FbmNyeXB0ZWRDb250ZW50Lk'
'F0ZRgIIAEoCzIfLkVuY3J5cHRlZENvbnRlbnQuQ29udGFjdFVwZGF0ZUgGUg1jb250YWN0VXBk' 'NvbnRhY3RVcGRhdGVIBlINY29udGFjdFVwZGF0ZYgBARJNCg5jb250YWN0UmVxdWVzdBgJIAEo'
'YXRliAEBEk0KDmNvbnRhY3RSZXF1ZXN0GAkgASgLMiAuRW5jcnlwdGVkQ29udGVudC5Db250YW' 'CzIgLkVuY3J5cHRlZENvbnRlbnQuQ29udGFjdFJlcXVlc3RIB1IOY29udGFjdFJlcXVlc3SIAQ'
'N0UmVxdWVzdEgHUg5jb250YWN0UmVxdWVzdIgBARI+CglmbGFtZVN5bmMYCiABKAsyGy5FbmNy' 'ESPgoJZmxhbWVTeW5jGAogASgLMhsuRW5jcnlwdGVkQ29udGVudC5GbGFtZVN5bmNICFIJZmxh'
'eXB0ZWRDb250ZW50LkZsYW1lU3luY0gIUglmbGFtZVN5bmOIAQESOwoIcHVzaEtleXMYCyABKA' 'bWVTeW5jiAEBEjsKCHB1c2hLZXlzGAsgASgLMhouRW5jcnlwdGVkQ29udGVudC5QdXNoS2V5c0'
'syGi5FbmNyeXB0ZWRDb250ZW50LlB1c2hLZXlzSAlSCHB1c2hLZXlziAEBEjsKCHJlYWN0aW9u' 'gJUghwdXNoS2V5c4gBARI7CghyZWFjdGlvbhgMIAEoCzIaLkVuY3J5cHRlZENvbnRlbnQuUmVh'
'GAwgASgLMhouRW5jcnlwdGVkQ29udGVudC5SZWFjdGlvbkgKUghyZWFjdGlvbogBARqpAQoLVG' 'Y3Rpb25IClIIcmVhY3Rpb26IAQESRAoLdGV4dE1lc3NhZ2UYDSABKAsyHS5FbmNyeXB0ZWRDb2'
'V4dE1lc3NhZ2USKAoPc2VuZGVyTWVzc2FnZUlkGAEgASgJUg9zZW5kZXJNZXNzYWdlSWQSEgoE' '50ZW50LlRleHRNZXNzYWdlSAtSC3RleHRNZXNzYWdliAEBGqkBCgtUZXh0TWVzc2FnZRIoCg9z'
'dGV4dBgCIAEoCVIEdGV4dBIcCgl0aW1lc3RhbXAYAyABKANSCXRpbWVzdGFtcBIrCg5xdW90ZU' 'ZW5kZXJNZXNzYWdlSWQYASABKAlSD3NlbmRlck1lc3NhZ2VJZBISCgR0ZXh0GAIgASgJUgR0ZX'
'1lc3NhZ2VJZBgEIAEoCUgAUg5xdW90ZU1lc3NhZ2VJZIgBAUIRCg9fcXVvdGVNZXNzYWdlSWQa' 'h0EhwKCXRpbWVzdGFtcBgDIAEoA1IJdGltZXN0YW1wEisKDnF1b3RlTWVzc2FnZUlkGAQgASgJ'
'gQEKCFJlYWN0aW9uEigKD3RhcmdldE1lc3NhZ2VJZBgBIAEoCVIPdGFyZ2V0TWVzc2FnZUlkEh' 'SABSDnF1b3RlTWVzc2FnZUlkiAEBQhEKD19xdW90ZU1lc3NhZ2VJZBqBAQoIUmVhY3Rpb24SKA'
'kKBWVtb2ppGAIgASgJSABSBWVtb2ppiAEBEhsKBnJlbW92ZRgDIAEoCEgBUgZyZW1vdmWIAQFC' 'oPdGFyZ2V0TWVzc2FnZUlkGAEgASgJUg90YXJnZXRNZXNzYWdlSWQSGQoFZW1vamkYAiABKAlI'
'CAoGX2Vtb2ppQgkKB19yZW1vdmUatwIKDU1lc3NhZ2VVcGRhdGUSOAoEdHlwZRgBIAEoDjIkLk' 'AFIFZW1vammIAQESGwoGcmVtb3ZlGAMgASgISAFSBnJlbW92ZYgBAUIICgZfZW1vamlCCQoHX3'
'VuY3J5cHRlZENvbnRlbnQuTWVzc2FnZVVwZGF0ZS5UeXBlUgR0eXBlEi0KD3NlbmRlck1lc3Nh' 'JlbW92ZRq3AgoNTWVzc2FnZVVwZGF0ZRI4CgR0eXBlGAEgASgOMiQuRW5jcnlwdGVkQ29udGVu'
'Z2VJZBgCIAEoCUgAUg9zZW5kZXJNZXNzYWdlSWSIAQESOgoYbXVsdGlwbGVTZW5kZXJNZXNzYW' 'dC5NZXNzYWdlVXBkYXRlLlR5cGVSBHR5cGUSLQoPc2VuZGVyTWVzc2FnZUlkGAIgASgJSABSD3'
'dlSWRzGAMgAygJUhhtdWx0aXBsZVNlbmRlck1lc3NhZ2VJZHMSFwoEdGV4dBgEIAEoCUgBUgR0' 'NlbmRlck1lc3NhZ2VJZIgBARI6ChhtdWx0aXBsZVNlbmRlck1lc3NhZ2VJZHMYAyADKAlSGG11'
'ZXh0iAEBEhwKCXRpbWVzdGFtcBgFIAEoA1IJdGltZXN0YW1wIi0KBFR5cGUSCgoGREVMRVRFEA' 'bHRpcGxlU2VuZGVyTWVzc2FnZUlkcxIXCgR0ZXh0GAQgASgJSAFSBHRleHSIAQESHAoJdGltZX'
'ASDQoJRURJVF9URVhUEAESCgoGT1BFTkVEEAJCEgoQX3NlbmRlck1lc3NhZ2VJZEIHCgVfdGV4' 'N0YW1wGAUgASgDUgl0aW1lc3RhbXAiLQoEVHlwZRIKCgZERUxFVEUQABINCglFRElUX1RFWFQQ'
'dBqMBQoFTWVkaWESKAoPc2VuZGVyTWVzc2FnZUlkGAEgASgJUg9zZW5kZXJNZXNzYWdlSWQSMA' 'ARIKCgZPUEVORUQQAkISChBfc2VuZGVyTWVzc2FnZUlkQgcKBV90ZXh0GowFCgVNZWRpYRIoCg'
'oEdHlwZRgCIAEoDjIcLkVuY3J5cHRlZENvbnRlbnQuTWVkaWEuVHlwZVIEdHlwZRJDChpkaXNw' '9zZW5kZXJNZXNzYWdlSWQYASABKAlSD3NlbmRlck1lc3NhZ2VJZBIwCgR0eXBlGAIgASgOMhwu'
'bGF5TGltaXRJbk1pbGxpc2Vjb25kcxgDIAEoA0gAUhpkaXNwbGF5TGltaXRJbk1pbGxpc2Vjb2' 'RW5jcnlwdGVkQ29udGVudC5NZWRpYS5UeXBlUgR0eXBlEkMKGmRpc3BsYXlMaW1pdEluTWlsbG'
'5kc4gBARI2ChZyZXF1aXJlc0F1dGhlbnRpY2F0aW9uGAQgASgIUhZyZXF1aXJlc0F1dGhlbnRp' 'lzZWNvbmRzGAMgASgDSABSGmRpc3BsYXlMaW1pdEluTWlsbGlzZWNvbmRziAEBEjYKFnJlcXVp'
'Y2F0aW9uEhwKCXRpbWVzdGFtcBgFIAEoA1IJdGltZXN0YW1wEisKDnF1b3RlTWVzc2FnZUlkGA' 'cmVzQXV0aGVudGljYXRpb24YBCABKAhSFnJlcXVpcmVzQXV0aGVudGljYXRpb24SHAoJdGltZX'
'YgASgJSAFSDnF1b3RlTWVzc2FnZUlkiAEBEikKDWRvd25sb2FkVG9rZW4YByABKAxIAlINZG93' 'N0YW1wGAUgASgDUgl0aW1lc3RhbXASKwoOcXVvdGVNZXNzYWdlSWQYBiABKAlIAVIOcXVvdGVN'
'bmxvYWRUb2tlbogBARIpCg1lbmNyeXB0aW9uS2V5GAggASgMSANSDWVuY3J5cHRpb25LZXmIAQ' 'ZXNzYWdlSWSIAQESKQoNZG93bmxvYWRUb2tlbhgHIAEoDEgCUg1kb3dubG9hZFRva2VuiAEBEi'
'ESKQoNZW5jcnlwdGlvbk1hYxgJIAEoDEgEUg1lbmNyeXB0aW9uTWFjiAEBEi0KD2VuY3J5cHRp' 'kKDWVuY3J5cHRpb25LZXkYCCABKAxIA1INZW5jcnlwdGlvbktleYgBARIpCg1lbmNyeXB0aW9u'
'b25Ob25jZRgKIAEoDEgFUg9lbmNyeXB0aW9uTm9uY2WIAQEiMwoEVHlwZRIMCghSRVVQTE9BRB' 'TWFjGAkgASgMSARSDWVuY3J5cHRpb25NYWOIAQESLQoPZW5jcnlwdGlvbk5vbmNlGAogASgMSA'
'AAEgkKBUlNQUdFEAESCQoFVklERU8QAhIHCgNHSUYQA0IdChtfZGlzcGxheUxpbWl0SW5NaWxs' 'VSD2VuY3J5cHRpb25Ob25jZYgBASIzCgRUeXBlEgwKCFJFVVBMT0FEEAASCQoFSU1BR0UQARIJ'
'aXNlY29uZHNCEQoPX3F1b3RlTWVzc2FnZUlkQhAKDl9kb3dubG9hZFRva2VuQhAKDl9lbmNyeX' 'CgVWSURFTxACEgcKA0dJRhADQh0KG19kaXNwbGF5TGltaXRJbk1pbGxpc2Vjb25kc0IRCg9fcX'
'B0aW9uS2V5QhAKDl9lbmNyeXB0aW9uTWFjQhIKEF9lbmNyeXB0aW9uTm9uY2UapwEKC01lZGlh' 'VvdGVNZXNzYWdlSWRCEAoOX2Rvd25sb2FkVG9rZW5CEAoOX2VuY3J5cHRpb25LZXlCEAoOX2Vu'
'VXBkYXRlEjYKBHR5cGUYASABKA4yIi5FbmNyeXB0ZWRDb250ZW50Lk1lZGlhVXBkYXRlLlR5cG' 'Y3J5cHRpb25NYWNCEgoQX2VuY3J5cHRpb25Ob25jZRqjAQoLTWVkaWFVcGRhdGUSNgoEdHlwZR'
'VSBHR5cGUSKAoPdGFyZ2V0TWVzc2FnZUlkGAIgASgJUg90YXJnZXRNZXNzYWdlSWQiNgoEVHlw' 'gBIAEoDjIiLkVuY3J5cHRlZENvbnRlbnQuTWVkaWFVcGRhdGUuVHlwZVIEdHlwZRIkCg10YXJn'
'ZRIMCghSRU9QRU5FRBAAEgoKBlNUT1JFRBABEhQKEERFQ1JZUFRJT05fRVJST1IQAhp4Cg5Db2' 'ZXRNZWRpYUlkGAIgASgJUg10YXJnZXRNZWRpYUlkIjYKBFR5cGUSDAoIUkVPUEVORUQQABIKCg'
'50YWN0UmVxdWVzdBI5CgR0eXBlGAEgASgOMiUuRW5jcnlwdGVkQ29udGVudC5Db250YWN0UmVx' 'ZTVE9SRUQQARIUChBERUNSWVBUSU9OX0VSUk9SEAIaeAoOQ29udGFjdFJlcXVlc3QSOQoEdHlw'
'dWVzdC5UeXBlUgR0eXBlIisKBFR5cGUSCwoHUkVRVUVTVBAAEgoKBlJFSkVDVBABEgoKBkFDQ0' 'ZRgBIAEoDjIlLkVuY3J5cHRlZENvbnRlbnQuQ29udGFjdFJlcXVlc3QuVHlwZVIEdHlwZSIrCg'
'VQVBACGtIBCg1Db250YWN0VXBkYXRlEjgKBHR5cGUYASABKA4yJC5FbmNyeXB0ZWRDb250ZW50' 'RUeXBlEgsKB1JFUVVFU1QQABIKCgZSRUpFQ1QQARIKCgZBQ0NFUFQQAhrSAQoNQ29udGFjdFVw'
'LkNvbnRhY3RVcGRhdGUuVHlwZVIEdHlwZRIhCglhdmF0YXJTdmcYAiABKAlIAFIJYXZhdGFyU3' 'ZGF0ZRI4CgR0eXBlGAEgASgOMiQuRW5jcnlwdGVkQ29udGVudC5Db250YWN0VXBkYXRlLlR5cG'
'ZniAEBEiUKC2Rpc3BsYXlOYW1lGAMgASgJSAFSC2Rpc3BsYXlOYW1liAEBIh8KBFR5cGUSCwoH' 'VSBHR5cGUSIQoJYXZhdGFyU3ZnGAIgASgJSABSCWF2YXRhclN2Z4gBARIlCgtkaXNwbGF5TmFt'
'UkVRVUVTVBAAEgoKBlVQREFURRABQgwKCl9hdmF0YXJTdmdCDgoMX2Rpc3BsYXlOYW1lGtUBCg' 'ZRgDIAEoCUgBUgtkaXNwbGF5TmFtZYgBASIfCgRUeXBlEgsKB1JFUVVFU1QQABIKCgZVUERBVE'
'hQdXNoS2V5cxIzCgR0eXBlGAEgASgOMh8uRW5jcnlwdGVkQ29udGVudC5QdXNoS2V5cy5UeXBl' 'UQAUIMCgpfYXZhdGFyU3ZnQg4KDF9kaXNwbGF5TmFtZRrVAQoIUHVzaEtleXMSMwoEdHlwZRgB'
'UgR0eXBlEhkKBWtleUlkGAIgASgDSABSBWtleUlkiAEBEhUKA2tleRgDIAEoDEgBUgNrZXmIAQ' 'IAEoDjIfLkVuY3J5cHRlZENvbnRlbnQuUHVzaEtleXMuVHlwZVIEdHlwZRIZCgVrZXlJZBgCIA'
'ESIQoJY3JlYXRlZEF0GAQgASgDSAJSCWNyZWF0ZWRBdIgBASIfCgRUeXBlEgsKB1JFUVVFU1QQ' 'EoA0gAUgVrZXlJZIgBARIVCgNrZXkYAyABKAxIAVIDa2V5iAEBEiEKCWNyZWF0ZWRBdBgEIAEo'
'ABIKCgZVUERBVEUQAUIICgZfa2V5SWRCBgoEX2tleUIMCgpfY3JlYXRlZEF0GocBCglGbGFtZV' 'A0gCUgljcmVhdGVkQXSIAQEiHwoEVHlwZRILCgdSRVFVRVNUEAASCgoGVVBEQVRFEAFCCAoGX2'
'N5bmMSIgoMZmxhbWVDb3VudGVyGAEgASgDUgxmbGFtZUNvdW50ZXISNgoWbGFzdEZsYW1lQ291' 'tleUlkQgYKBF9rZXlCDAoKX2NyZWF0ZWRBdBqHAQoJRmxhbWVTeW5jEiIKDGZsYW1lQ291bnRl'
'bnRlckNoYW5nZRgCIAEoA1IWbGFzdEZsYW1lQ291bnRlckNoYW5nZRIeCgpiZXN0RnJpZW5kGA' 'chgBIAEoA1IMZmxhbWVDb3VudGVyEjYKFmxhc3RGbGFtZUNvdW50ZXJDaGFuZ2UYAiABKANSFm'
'MgASgIUgpiZXN0RnJpZW5kQgoKCF9ncm91cElkQhcKFV9zZW5kZXJQcm9maWxlQ291bnRlckIO' 'xhc3RGbGFtZUNvdW50ZXJDaGFuZ2USHgoKYmVzdEZyaWVuZBgDIAEoCFIKYmVzdEZyaWVuZEIK'
'CgxfdGV4dE1lc3NhZ2VCEAoOX21lc3NhZ2VVcGRhdGVCCAoGX21lZGlhQg4KDF9tZWRpYVVwZG' 'CghfZ3JvdXBJZEIPCg1faXNEaXJlY3RDaGF0QhcKFV9zZW5kZXJQcm9maWxlQ291bnRlckIQCg'
'F0ZUIQCg5fY29udGFjdFVwZGF0ZUIRCg9fY29udGFjdFJlcXVlc3RCDAoKX2ZsYW1lU3luY0IL' '5fbWVzc2FnZVVwZGF0ZUIICgZfbWVkaWFCDgoMX21lZGlhVXBkYXRlQhAKDl9jb250YWN0VXBk'
'CglfcHVzaEtleXNCCwoJX3JlYWN0aW9u'); 'YXRlQhEKD19jb250YWN0UmVxdWVzdEIMCgpfZmxhbWVTeW5jQgsKCV9wdXNoS2V5c0ILCglfcm'
'VhY3Rpb25CDgoMX3RleHRNZXNzYWdl');

View file

@ -30,11 +30,11 @@ message PlaintextContent {
message EncryptedContent { message EncryptedContent {
optional string groupId = 2; 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 /// 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 MessageUpdate messageUpdate = 5;
optional Media media = 6; optional Media media = 6;
optional MediaUpdate mediaUpdate = 7; optional MediaUpdate mediaUpdate = 7;
@ -43,6 +43,7 @@ message EncryptedContent {
optional FlameSync flameSync = 10; optional FlameSync flameSync = 10;
optional PushKeys pushKeys = 11; optional PushKeys pushKeys = 11;
optional Reaction reaction = 12; optional Reaction reaction = 12;
optional TextMessage textMessage = 13;
message TextMessage { message TextMessage {
string senderMessageId = 1; string senderMessageId = 1;
@ -98,7 +99,7 @@ message EncryptedContent {
DECRYPTION_ERROR = 2; DECRYPTION_ERROR = 2;
} }
Type type = 1; Type type = 1;
string targetMessageId = 2; string targetMediaId = 2;
} }
message ContactRequest { message ContactRequest {

View file

@ -207,7 +207,8 @@ Future<void> requestMediaReupload(String mediaId) async {
final messages = await twonlyDB.messagesDao.getMessagesByMediaId(mediaId); final messages = await twonlyDB.messagesDao.getMessagesByMediaId(mediaId);
if (messages.length != 1 || messages.first.senderId == null) { if (messages.length != 1 || messages.first.senderId == null) {
Log.error( 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; return;
} }
@ -216,7 +217,7 @@ Future<void> requestMediaReupload(String mediaId) async {
EncryptedContent( EncryptedContent(
mediaUpdate: EncryptedContent_MediaUpdate( mediaUpdate: EncryptedContent_MediaUpdate(
type: EncryptedContent_MediaUpdate_Type.DECRYPTION_ERROR, 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) { for (final message in messages) {
final groupMembers = final groupMembers =
await twonlyDB.groupsDao.getGroupMembers(message.groupId); 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) { for (final groupMember in groupMembers) {
/// only send the upload to the users /// only send the upload to the users
if (media.mediaFile.reuploadRequestedBy != null) { 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); final downloadToken = getRandomUint8List(32);
var type = EncryptedContent_Media_Type.IMAGE; var type = EncryptedContent_Media_Type.IMAGE;
@ -169,7 +172,8 @@ Future<void> _createUploadRequest(MediaFileService media) async {
if (cipherText == null) { if (cipherText == null) {
Log.error( Log.error(
'Could not generate ciphertext message for ${groupMember.contactId}'); 'Could not generate ciphertext message for ${groupMember.contactId}',
);
} }
final messageOnSuccess = TextMessage() final messageOnSuccess = TextMessage()

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,5 +1,4 @@
import 'dart:math'; import 'dart:math';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_image_compress/flutter_image_compress.dart'; import 'package:flutter_image_compress/flutter_image_compress.dart';
@ -8,11 +7,14 @@ import 'package:intl/intl.dart';
import 'package:local_auth/local_auth.dart'; import 'package:local_auth/local_auth.dart';
import 'package:pie_menu/pie_menu.dart'; import 'package:pie_menu/pie_menu.dart';
import 'package:provider/provider.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/database/twonly.db.dart';
import 'package:twonly/src/localization/generated/app_localizations.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/model/protobuf/api/websocket/error.pb.dart';
import 'package:twonly/src/providers/settings.provider.dart'; import 'package:twonly/src/providers/settings.provider.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart';
extension ShortCutsExtension on BuildContext { extension ShortCutsExtension on BuildContext {
AppLocalizations get lang => AppLocalizations.of(this)!; 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 'dart:collection';
import 'package:flutter/material.dart'; 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/database/twonly.db.dart';
import 'package:twonly/src/utils/misc.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/flame.dart';
import 'package:twonly/src/views/components/headline.dart'; import 'package:twonly/src/views/components/headline.dart';
import 'package:twonly/src/views/components/initialsavatar.dart';
class BestFriendsSelector extends StatelessWidget { class BestFriendsSelector extends StatelessWidget {
const BestFriendsSelector({ const BestFriendsSelector({
required this.users, required this.groups,
required this.isRealTwonly, required this.selectedGroupIds,
required this.updateStatus, required this.updateSelectedGroupIds,
required this.selectedUserIds,
required this.title, required this.title,
required this.showSelectAll,
super.key, super.key,
}); });
final List<Contact> users; final List<Group> groups;
final void Function(int, bool) updateStatus; final HashSet<String> selectedGroupIds;
final HashSet<int> selectedUserIds; final void Function(String, bool) updateSelectedGroupIds;
final bool isRealTwonly;
final String title; final String title;
final bool showSelectAll;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (users.isEmpty) { if (groups.isEmpty) {
return Container(); return Container();
} }
return Column( return Column(
children: [ children: [
Row( Row(
@ -39,11 +33,11 @@ class BestFriendsSelector extends StatelessWidget {
Expanded( Expanded(
child: HeadLineComponent(title), child: HeadLineComponent(title),
), ),
if (!isRealTwonly) if (showSelectAll)
GestureDetector( GestureDetector(
onTap: () { onTap: () {
for (final user in users) { for (final group in groups) {
updateStatus(user.userId, true); updateSelectedGroupIds(group.groupId, true);
} }
}, },
child: Container( child: Container(
@ -70,7 +64,7 @@ class BestFriendsSelector extends StatelessWidget {
Column( Column(
spacing: 8, spacing: 8,
children: List.generate( children: List.generate(
(users.length + 1) ~/ 2, (groups.length + 1) ~/ 2,
(rowIndex) { (rowIndex) {
final firstUserIndex = rowIndex * 2; final firstUserIndex = rowIndex * 2;
final secondUserIndex = firstUserIndex + 1; final secondUserIndex = firstUserIndex + 1;
@ -79,21 +73,19 @@ class BestFriendsSelector extends StatelessWidget {
children: [ children: [
Expanded( Expanded(
child: UserCheckbox( child: UserCheckbox(
isChecked: selectedUserIds isChecked: selectedGroupIds
.contains(users[firstUserIndex].userId), .contains(groups[firstUserIndex].groupId),
user: users[firstUserIndex], group: groups[firstUserIndex],
onChanged: updateStatus, onChanged: updateSelectedGroupIds,
isRealTwonly: isRealTwonly,
), ),
), ),
if (secondUserIndex < users.length) if (secondUserIndex < groups.length)
Expanded( Expanded(
child: UserCheckbox( child: UserCheckbox(
isChecked: selectedUserIds isChecked: selectedGroupIds
.contains(users[secondUserIndex].userId), .contains(groups[secondUserIndex].groupId),
user: users[secondUserIndex], group: groups[secondUserIndex],
onChanged: updateStatus, onChanged: updateSelectedGroupIds,
isRealTwonly: isRealTwonly,
), ),
) )
else else
@ -112,28 +104,24 @@ class BestFriendsSelector extends StatelessWidget {
class UserCheckbox extends StatelessWidget { class UserCheckbox extends StatelessWidget {
const UserCheckbox({ const UserCheckbox({
required this.user, required this.group,
required this.onChanged, required this.onChanged,
required this.isRealTwonly,
required this.isChecked, required this.isChecked,
super.key, super.key,
}); });
final Contact user; final Group group;
final void Function(int, bool) onChanged; final void Function(String, bool) onChanged;
final bool isChecked; final bool isChecked;
final bool isRealTwonly;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final displayName = getContactDisplayName(user);
return Container( return Container(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 3, horizontal: 3,
), // Padding inside the container ), // Padding inside the container
child: GestureDetector( child: GestureDetector(
onTap: () { onTap: () {
onChanged(user.userId, !isChecked); onChanged(group.groupId, !isChecked);
}, },
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10), padding: const EdgeInsets.symmetric(horizontal: 10),
@ -149,8 +137,8 @@ class UserCheckbox extends StatelessWidget {
), ),
child: Row( child: Row(
children: [ children: [
ContactAvatar( AvatarIcon(
contact: user, group: group,
fontSize: 12, fontSize: 12,
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
@ -160,28 +148,21 @@ class UserCheckbox extends StatelessWidget {
Row( Row(
children: [ children: [
Text( Text(
displayName.length > 8 group.groupName.length > 12
? '${displayName.substring(0, 8)}...' ? '${group.groupName.substring(0, 9)}...'
: displayName, : group.groupName,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
], ],
), ),
StreamBuilder( FlameCounterWidget(groupId: group.groupId),
stream: twonlyDB.contactsDao.watchFlameCounter(user.userId),
builder: (context, snapshot) {
if (!snapshot.hasData || snapshot.data! == 0) {
return Container();
}
return FlameCounterWidget(user, snapshot.data!);
},
),
], ],
), ),
Expanded(child: Container()), Expanded(child: Container()),
Checkbox( Checkbox(
value: isChecked, value: isChecked,
side: WidgetStateBorderSide.resolveWith( side: WidgetStateBorderSide.resolveWith(
// ignore: strict_raw_type
(Set states) { (Set states) {
if (states.contains(WidgetState.selected)) { if (states.contains(WidgetState.selected)) {
return const BorderSide(width: 0); return const BorderSide(width: 0);
@ -192,7 +173,7 @@ class UserCheckbox extends StatelessWidget {
}, },
), ),
onChanged: (bool? value) { 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 { onPressed: () async {
if (media.type != MediaType.video) { if (media.type != MediaType.video) {
await mediaService.setDisplayLimit( await mediaService.setDisplayLimit(
(media.displayLimitInMilliseconds == null) ? 0 : null); (media.displayLimitInMilliseconds == null) ? 0 : null,
);
if (!mounted) return; if (!mounted) return;
setState(() {}); setState(() {});
return; return;
@ -465,8 +466,9 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
: const FaIcon(FontAwesomeIcons.solidPaperPlane), : const FaIcon(FontAwesomeIcons.solidPaperPlane),
onPressed: () async { onPressed: () async {
if (sendingOrLoadingImage) return; if (sendingOrLoadingImage) return;
if (widget.sendToGroup == null) if (widget.sendToGroup == null) {
return pushShareImageView(); return pushShareImageView();
}
await sendImageToSinglePerson(); await sendImageToSinglePerson();
}, },
style: ButtonStyle( style: ButtonStyle(

View file

@ -2,23 +2,19 @@
import 'dart:async'; import 'dart:async';
import 'dart:collection'; import 'dart:collection';
import 'dart:typed_data';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/globals.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/tables/mediafiles.table.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/services/api/mediafiles/upload.service.dart'; import 'package:twonly/src/services/api/mediafiles/upload.service.dart';
import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
import 'package:twonly/src/utils/misc.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/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/flame.dart';
import 'package:twonly/src/views/components/headline.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 { class ShareImageView extends StatefulWidget {
const ShareImageView({ const ShareImageView({
@ -38,28 +34,27 @@ class ShareImageView extends StatefulWidget {
} }
class _ShareImageView extends State<ShareImageView> { class _ShareImageView extends State<ShareImageView> {
List<Contact> contacts = []; List<Group> contacts = [];
List<Contact> _otherUsers = []; List<Group> _otherUsers = [];
List<Contact> _bestFriends = []; List<Group> _bestFriends = [];
List<Contact> _pinnedContacts = []; List<Group> _pinnedContacts = [];
Uint8List? imageBytes;
bool sendingImage = false; bool sendingImage = false;
bool mediaStoreFutureReady = false;
bool hideArchivedUsers = true; bool hideArchivedUsers = true;
final TextEditingController searchUserName = TextEditingController(); final TextEditingController searchUserName = TextEditingController();
late StreamSubscription<List<Contact>> contactSub; late StreamSubscription<List<Group>> allGroupSub;
String lastQuery = ''; String lastQuery = '';
@override @override
void initState() { void initState() {
super.initState(); super.initState();
final allContacts = twonlyDB.contactsDao.watchContactsForShareView(); allGroupSub = twonlyDB.groupsDao.watchGroups().listen((allGroups) async {
contactSub = allContacts.listen((allContacts) async {
setState(() { setState(() {
contacts = allContacts; contacts = allGroups;
}); });
await updateUsers(allContacts.where((x) => !x.archived).toList()); await updateGroups(allGroups.where((x) => !x.archived).toList());
}); });
unawaited(initAsync()); unawaited(initAsync());
@ -69,6 +64,7 @@ class _ShareImageView extends State<ShareImageView> {
if (widget.mediaStoreFuture != null) { if (widget.mediaStoreFuture != null) {
await widget.mediaStoreFuture; await widget.mediaStoreFuture;
} }
mediaStoreFutureReady = true;
await widget.mediaFileService.setUploadState(UploadState.preprocessing); await widget.mediaFileService.setUploadState(UploadState.preprocessing);
unawaited(startBackgroundMediaUpload(widget.mediaFileService)); unawaited(startBackgroundMediaUpload(widget.mediaFileService));
if (!mounted) return; if (!mounted) return;
@ -77,16 +73,17 @@ class _ShareImageView extends State<ShareImageView> {
@override @override
void dispose() { void dispose() {
unawaited(contactSub.cancel()); unawaited(allGroupSub.cancel());
super.dispose(); super.dispose();
} }
Future<void> updateUsers(List<Contact> users) async { Future<void> updateGroups(List<Group> groups) async {
// Sort contacts by flameCounter and then by totalMediaCounter // Sort contacts by flameCounter and then by totalMediaCounter
users.sort((a, b) { groups.sort((a, b) {
// First, compare by flameCounter // First, compare by flameCounter
final flameComparison = getFlameCounterFromContact(b)
.compareTo(getFlameCounterFromContact(a)); final flameComparison =
getFlameCounterFromGroup(b).compareTo(getFlameCounterFromGroup(a));
if (flameComparison != 0) { if (flameComparison != 0) {
return flameComparison; // Sort by flameCounter in descending order return flameComparison; // Sort by flameCounter in descending order
} }
@ -97,18 +94,18 @@ class _ShareImageView extends State<ShareImageView> {
}); });
// Separate best friends and other users // Separate best friends and other users
final bestFriends = <Contact>[]; final bestFriends = <Group>[];
final otherUsers = <Contact>[]; final otherUsers = <Group>[];
final pinnedContacts = users.where((c) => c.pinned).toList(); final pinnedContacts = groups.where((c) => c.pinned).toList();
for (final contact in users) { for (final group in groups) {
if (contact.pinned) continue; if (group.pinned) continue;
if (!contact.archived && if (!group.archived &&
(getFlameCounterFromContact(contact)) > 0 && (getFlameCounterFromGroup(group)) > 0 &&
bestFriends.length < 6) { bestFriends.length < 6) {
bestFriends.add(contact); bestFriends.add(group);
} else { } else {
otherUsers.add(contact); otherUsers.add(group);
} }
} }
@ -122,13 +119,13 @@ class _ShareImageView extends State<ShareImageView> {
Future<void> _filterUsers(String query) async { Future<void> _filterUsers(String query) async {
lastQuery = query; lastQuery = query;
if (query.isEmpty) { if (query.isEmpty) {
await updateUsers( await updateGroups(
contacts contacts
.where( .where(
(x) => (x) =>
!x.archived || !x.archived ||
!hideArchivedUsers || !hideArchivedUsers ||
widget.selectedUserIds.contains(x.userId), widget.selectedGroupIds.contains(x.groupId),
) )
.toList(), .toList(),
); );
@ -136,16 +133,14 @@ class _ShareImageView extends State<ShareImageView> {
} }
final usersFiltered = contacts final usersFiltered = contacts
.where( .where(
(user) => getContactDisplayName(user) (user) => user.groupName.toLowerCase().contains(query.toLowerCase()),
.toLowerCase()
.contains(query.toLowerCase()),
) )
.toList(); .toList();
await updateUsers(usersFiltered); await updateGroups(usersFiltered);
} }
void updateStatus(int userId, bool checked) { void updateSelectedGroupIds(String groupId, bool checked) {
widget.updateStatus(userId, checked); widget.updateSelectedGroupIds(groupId, checked);
setState(() {}); setState(() {});
} }
@ -173,19 +168,21 @@ class _ShareImageView extends State<ShareImageView> {
), ),
if (_pinnedContacts.isNotEmpty) const SizedBox(height: 10), if (_pinnedContacts.isNotEmpty) const SizedBox(height: 10),
BestFriendsSelector( BestFriendsSelector(
users: _pinnedContacts, groups: _pinnedContacts,
selectedUserIds: widget.selectedUserIds, selectedGroupIds: widget.selectedGroupIds,
isRealTwonly: widget.isRealTwonly, updateSelectedGroupIds: updateSelectedGroupIds,
updateStatus: updateStatus,
title: context.lang.shareImagePinnedContacts, title: context.lang.shareImagePinnedContacts,
showSelectAll:
!widget.mediaFileService.mediaFile.requiresAuthentication,
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
BestFriendsSelector( BestFriendsSelector(
users: _bestFriends, groups: _bestFriends,
selectedUserIds: widget.selectedUserIds, selectedGroupIds: widget.selectedGroupIds,
isRealTwonly: widget.isRealTwonly, updateSelectedGroupIds: updateSelectedGroupIds,
updateStatus: updateStatus,
title: context.lang.shareImageBestFriends, title: context.lang.shareImageBestFriends,
showSelectAll:
!widget.mediaFileService.mediaFile.requiresAuthentication,
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
if (_otherUsers.isNotEmpty) if (_otherUsers.isNotEmpty)
@ -229,9 +226,8 @@ class _ShareImageView extends State<ShareImageView> {
Expanded( Expanded(
child: UserList( child: UserList(
List.from(_otherUsers), List.from(_otherUsers),
selectedUserIds: widget.selectedUserIds, selectedGroupIds: widget.selectedGroupIds,
isRealTwonly: widget.isRealTwonly, updateSelectedGroupIds: updateSelectedGroupIds,
updateStatus: updateStatus,
), ),
), ),
], ],
@ -246,7 +242,7 @@ class _ShareImageView extends State<ShareImageView> {
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [
FilledButton.icon( FilledButton.icon(
icon: imageBytes == null || sendingImage icon: !mediaStoreFutureReady || sendingImage
? SizedBox( ? SizedBox(
height: 12, height: 12,
width: 12, width: 12,
@ -257,41 +253,20 @@ class _ShareImageView extends State<ShareImageView> {
) )
: const FaIcon(FontAwesomeIcons.solidPaperPlane), : const FaIcon(FontAwesomeIcons.solidPaperPlane),
onPressed: () async { onPressed: () async {
if (imageBytes == null || widget.selectedUserIds.isEmpty) { if (!mediaStoreFutureReady ||
widget.selectedGroupIds.isEmpty) {
return; return;
} }
final err = await isAllowedToSend();
if (!context.mounted) return;
if (err != null) {
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return SubscriptionView(
redirectError: err,
);
},
),
);
} else {
setState(() { setState(() {
sendingImage = true; sendingImage = true;
}); });
await finalizeUpload( await insertMediaFileInMessagesTable(
widget.mediaUploadId, widget.mediaFileService,
widget.selectedUserIds.toList(), widget.selectedGroupIds.toList(),
widget.isRealTwonly,
widget.videoUploadHandler != null,
widget.mirrorVideo,
widget.maxShowTime,
); );
/// trigger the upload of the media file.
unawaited(handleNextMediaUploadSteps(widget.mediaUploadId));
if (context.mounted) { if (context.mounted) {
Navigator.pop(context, true); Navigator.pop(context, true);
// if (widget.preselectedUser != null) { // if (widget.preselectedUser != null) {
@ -301,14 +276,13 @@ class _ShareImageView extends State<ShareImageView> {
// globalUpdateOfHomeViewPageIndex(1); // globalUpdateOfHomeViewPageIndex(1);
// } // }
} }
}
}, },
style: ButtonStyle( style: ButtonStyle(
padding: WidgetStateProperty.all<EdgeInsets>( padding: WidgetStateProperty.all<EdgeInsets>(
const EdgeInsets.symmetric(vertical: 10, horizontal: 30), const EdgeInsets.symmetric(vertical: 10, horizontal: 30),
), ),
backgroundColor: WidgetStateProperty.all<Color>( backgroundColor: WidgetStateProperty.all<Color>(
imageBytes == null || widget.selectedUserIds.isEmpty mediaStoreFutureReady || widget.selectedGroupIds.isEmpty
? Theme.of(context).colorScheme.secondary ? Theme.of(context).colorScheme.secondary
: Theme.of(context).colorScheme.primary, : Theme.of(context).colorScheme.primary,
), ),
@ -328,52 +302,42 @@ class _ShareImageView extends State<ShareImageView> {
class UserList extends StatelessWidget { class UserList extends StatelessWidget {
const UserList( const UserList(
this.users, { this.groups, {
required this.selectedUserIds, required this.selectedGroupIds,
required this.updateStatus, required this.updateSelectedGroupIds,
required this.isRealTwonly,
super.key, super.key,
}); });
final void Function(int, bool) updateStatus; final void Function(String, bool) updateSelectedGroupIds;
final List<Contact> users; final List<Group> groups;
final bool isRealTwonly; final HashSet<String> selectedGroupIds;
final HashSet<int> selectedUserIds;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// Step 1: Sort the users alphabetically // Step 1: Sort the users alphabetically
users groups
.sort((a, b) => b.lastMessageExchange.compareTo(a.lastMessageExchange)); .sort((a, b) => b.lastMessageExchange.compareTo(a.lastMessageExchange));
return ListView.builder( return ListView.builder(
restorationId: 'new_message_users_list', restorationId: 'new_message_users_list',
itemCount: users.length, itemCount: groups.length,
itemBuilder: (BuildContext context, int i) { itemBuilder: (BuildContext context, int i) {
final user = users[i]; final group = groups[i];
final flameCounter = getFlameCounterFromContact(user);
return ListTile( return ListTile(
title: Row( title: Row(
children: [ children: [
if (isRealTwonly) Text(group.groupName),
Padding(
padding: const EdgeInsets.only(right: 1),
child: VerifiedShield(user),
),
Text(getContactDisplayName(user)),
if (flameCounter >= 1)
FlameCounterWidget( FlameCounterWidget(
user, groupId: group.groupId,
flameCounter,
prefix: true, prefix: true,
), ),
], ],
), ),
leading: ContactAvatar( leading: AvatarIcon(
contact: user, group: group,
fontSize: 15, fontSize: 15,
), ),
trailing: Checkbox( trailing: Checkbox(
value: selectedUserIds.contains(user.userId), value: selectedGroupIds.contains(group.groupId),
side: WidgetStateBorderSide.resolveWith( side: WidgetStateBorderSide.resolveWith(
(Set states) { (Set states) {
if (states.contains(WidgetState.selected)) { if (states.contains(WidgetState.selected)) {
@ -384,11 +348,14 @@ class UserList extends StatelessWidget {
), ),
onChanged: (bool? value) { onChanged: (bool? value) {
if (value == null) return; if (value == null) return;
updateStatus(user.userId, value); updateSelectedGroupIds(group.groupId, value);
}, },
), ),
onTap: () { 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 'dart:async';
import 'package:drift/drift.dart' hide Column; import 'package:drift/drift.dart' hide Column;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/database/daos/contacts.dao.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/database/twonly.db.dart';
import 'package:twonly/src/model/json/message_old.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
import 'package:twonly/src/model/protobuf/push_notification/push_notification.pbserver.dart';
import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/services/api/messages.dart';
import 'package:twonly/src/services/api/utils.dart'; import 'package:twonly/src/services/api/utils.dart';
import 'package:twonly/src/services/notifications/pushkeys.notifications.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/misc.dart';
import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/utils/storage.dart';
import 'package:twonly/src/views/components/alert_dialog.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/headline.dart';
import 'package:twonly/src/views/components/initialsavatar.dart';
class AddNewUserView extends StatefulWidget { class AddNewUserView extends StatefulWidget {
const AddNewUserView({super.key}); const AddNewUserView({super.key});
@ -97,19 +94,18 @@ class _SearchUsernameView extends State<AddNewUserView> {
if (added > 0) { if (added > 0) {
if (await createNewSignalSession(userdata)) { if (await createNewSignalSession(userdata)) {
// before notifying the other party, add // 1. Setup notifications keys with the other user
await setupNotificationWithUsers( await setupNotificationWithUsers(
forceContact: userdata.userId.toInt(), forceContact: userdata.userId.toInt(),
); );
await encryptAndSendMessageAsync( // 2. Then send user request
null, await sendCipherText(
userdata.userId.toInt(), userdata.userId.toInt(),
MessageJson( EncryptedContent(
kind: MessageKind.contactRequest, contactRequest: EncryptedContent_ContactRequest(
timestamp: DateTime.now(), type: EncryptedContent_ContactRequest_Type.REQUEST,
content: MessageContent(), ),
), ),
pushNotification: PushNotification(kind: PushKind.contactRequest),
); );
} }
} }
@ -198,7 +194,7 @@ class ContactsListView extends StatelessWidget {
child: IconButton( child: IconButton(
icon: const FaIcon(FontAwesomeIcons.boxArchive, size: 15), icon: const FaIcon(FontAwesomeIcons.boxArchive, size: 15),
onPressed: () async { onPressed: () async {
const update = ContactsCompanion(archived: Value(true)); const update = ContactsCompanion(requested: Value(false));
await twonlyDB.contactsDao.updateContact(contact.userId, update); await twonlyDB.contactsDao.updateContact(contact.userId, update);
}, },
), ),
@ -234,17 +230,18 @@ class ContactsListView extends StatelessWidget {
IconButton( IconButton(
icon: const Icon(Icons.check, color: Colors.green), icon: const Icon(Icons.check, color: Colors.green),
onPressed: () async { 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 twonlyDB.contactsDao.updateContact(contact.userId, update);
await encryptAndSendMessageAsync( await sendCipherText(
null,
contact.userId, contact.userId,
MessageJson( EncryptedContent(
kind: MessageKind.acceptRequest, contactRequest: EncryptedContent_ContactRequest(
timestamp: DateTime.now(), type: EncryptedContent_ContactRequest_Type.ACCEPT,
content: MessageContent(), ),
), ),
pushNotification: PushNotification(kind: PushKind.acceptRequest),
); );
await notifyContactsAboutProfileChange(); await notifyContactsAboutProfileChange();
}, },
@ -261,7 +258,7 @@ class ContactsListView extends StatelessWidget {
final displayName = getContactDisplayName(contact); final displayName = getContactDisplayName(contact);
return ListTile( return ListTile(
title: Text(displayName), title: Text(displayName),
leading: ContactAvatar(contact: contact), leading: AvatarIcon(contact: contact),
trailing: Row( trailing: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: contact.requested children: contact.requested

View file

@ -1,5 +1,4 @@
import 'dart:async'; import 'dart:async';
import 'package:cryptography_plus/cryptography_plus.dart'; import 'package:cryptography_plus/cryptography_plus.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.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:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:twonly/globals.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/database/twonly.db.dart';
import 'package:twonly/src/model/json/userdata.dart';
import 'package:twonly/src/providers/connection.provider.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/misc.dart';
import 'package:twonly/src/utils/storage.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/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/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/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/feedback_btn.dart';
import 'package:twonly/src/views/chats/chat_list_components/last_message_time.dart'; import 'package:twonly/src/views/chats/chat_list_components/group_list_item.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/start_new_chat.view.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/avatar_icon.component.dart';
import 'package:twonly/src/views/components/initialsavatar.dart';
import 'package:twonly/src/views/components/notification_badge.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/help/changelog.view.dart';
import 'package:twonly/src/views/settings/profile/profile.view.dart'; import 'package:twonly/src/views/settings/profile/profile.view.dart';
import 'package:twonly/src/views/settings/settings_main.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> { class _ChatListViewState extends State<ChatListView> {
late StreamSubscription<List<Contact>> _contactsSub; late StreamSubscription<List<Group>> _contactsSub;
List<Contact> _contacts = []; List<Group> _groupsNotPinned = [];
List<Contact> _pinnedContacts = []; List<Group> _groupsPinned = [];
UserData? _user;
GlobalKey firstUserListItemKey = GlobalKey(); GlobalKey firstUserListItemKey = GlobalKey();
GlobalKey searchForOtherUsers = GlobalKey(); GlobalKey searchForOtherUsers = GlobalKey();
@ -58,11 +47,11 @@ class _ChatListViewState extends State<ChatListView> {
} }
Future<void> initAsync() async { Future<void> initAsync() async {
final stream = twonlyDB.contactsDao.watchContactsForChatList(); final stream = twonlyDB.groupsDao.watchGroups();
_contactsSub = stream.listen((contacts) { _contactsSub = stream.listen((groups) {
setState(() { setState(() {
_contacts = contacts.where((x) => !x.pinned).toList(); _groupsNotPinned = groups.where((x) => !x.pinned).toList();
_pinnedContacts = contacts.where((x) => x.pinned).toList(); _groupsPinned = groups.where((x) => x.pinned).toList();
}); });
}); });
@ -71,21 +60,16 @@ class _ChatListViewState extends State<ChatListView> {
if (!mounted) return; if (!mounted) return;
await showChatListTutorialSearchOtherUsers(context, searchForOtherUsers); await showChatListTutorialSearchOtherUsers(context, searchForOtherUsers);
if (!mounted) return; if (!mounted) return;
if (_contacts.isNotEmpty) { if (_groupsNotPinned.isNotEmpty) {
await showChatListTutorialContextMenu(context, firstUserListItemKey); await showChatListTutorialContextMenu(context, firstUserListItemKey);
} }
}); });
final user = await getUser();
if (user == null) return;
setState(() {
_user = user;
});
final changeLog = await rootBundle.loadString('CHANGELOG.md'); final changeLog = await rootBundle.loadString('CHANGELOG.md');
final changeLogHash = final changeLogHash =
(await compute(Sha256().hash, changeLog.codeUnits)).bytes; (await compute(Sha256().hash, changeLog.codeUnits)).bytes;
if (!user.hideChangeLog && if (!gUser.hideChangeLog &&
user.lastChangeLogHash.toString() != changeLogHash.toString()) { gUser.lastChangeLogHash.toString() != changeLogHash.toString()) {
await updateUserdata((u) { await updateUserdata((u) {
u.lastChangeLogHash = changeLogHash; u.lastChangeLogHash = changeLogHash;
return u; return u;
@ -93,7 +77,7 @@ class _ChatListViewState extends State<ChatListView> {
if (!mounted) return; if (!mounted) return;
// only show changelog to people who already have contacts // only show changelog to people who already have contacts
// this prevents that this is shown directly after the user registered // this prevents that this is shown directly after the user registered
if (_contacts.isNotEmpty) { if (_groupsNotPinned.isNotEmpty) {
await Navigator.push( await Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
@ -133,12 +117,11 @@ class _ChatListViewState extends State<ChatListView> {
}, },
), ),
); );
_user = await getUser();
if (!mounted) return; if (!mounted) return;
setState(() {}); setState(() {}); // gUser has updated
}, },
child: ContactAvatar( child: AvatarIcon(
userData: _user, userData: gUser,
fontSize: 14, fontSize: 14,
color: context.color.onSurface.withAlpha(20), color: context.color.onSurface.withAlpha(20),
), ),
@ -210,9 +193,8 @@ class _ChatListViewState extends State<ChatListView> {
builder: (context) => const SettingsMainView(), builder: (context) => const SettingsMainView(),
), ),
); );
_user = await getUser();
if (!mounted) return; if (!mounted) return;
setState(() {}); setState(() {}); // gUser may has changed...
}, },
icon: const FaIcon(FontAwesomeIcons.gear, size: 19), icon: const FaIcon(FontAwesomeIcons.gear, size: 19),
), ),
@ -227,7 +209,7 @@ class _ChatListViewState extends State<ChatListView> {
child: isConnected ? Container() : const ConnectionInfo(), child: isConnected ? Container() : const ConnectionInfo(),
), ),
Positioned.fill( Positioned.fill(
child: (_contacts.isEmpty && _pinnedContacts.isEmpty) child: (_groupsNotPinned.isEmpty && _groupsPinned.isEmpty)
? Center( ? Center(
child: Padding( child: Padding(
padding: const EdgeInsets.all(10), padding: const EdgeInsets.all(10),
@ -252,9 +234,9 @@ class _ChatListViewState extends State<ChatListView> {
await Future.delayed(const Duration(seconds: 1)); await Future.delayed(const Duration(seconds: 1));
}, },
child: ListView.builder( child: ListView.builder(
itemCount: _pinnedContacts.length + itemCount: _groupsPinned.length +
(_pinnedContacts.isNotEmpty ? 1 : 0) + (_groupsPinned.isNotEmpty ? 1 : 0) +
_contacts.length + _groupsNotPinned.length +
1, 1,
itemBuilder: (context, index) { itemBuilder: (context, index) {
if (index == 0) { if (index == 0) {
@ -262,11 +244,11 @@ class _ChatListViewState extends State<ChatListView> {
} }
index -= 1; index -= 1;
// Check if the index is for the pinned users // Check if the index is for the pinned users
if (index < _pinnedContacts.length) { if (index < _groupsPinned.length) {
final contact = _pinnedContacts[index]; final group = _groupsPinned[index];
return UserListItem( return GroupListItem(
key: ValueKey(contact.userId), key: ValueKey(group.groupId),
user: contact, group: group,
firstUserListItemKey: (index == 0 || index == 1) firstUserListItemKey: (index == 0 || index == 1)
? firstUserListItemKey ? firstUserListItemKey
: null, : null,
@ -274,21 +256,21 @@ class _ChatListViewState extends State<ChatListView> {
} }
// If there are pinned users, account for the Divider // If there are pinned users, account for the Divider
var adjustedIndex = index - _pinnedContacts.length; var adjustedIndex = index - _groupsPinned.length;
if (_pinnedContacts.isNotEmpty && adjustedIndex == 0) { if (_groupsPinned.isNotEmpty && adjustedIndex == 0) {
return const Divider(); return const Divider();
} }
// Adjust the index for the contacts list // Adjust the index for the contacts list
adjustedIndex -= (_pinnedContacts.isNotEmpty ? 1 : 0); adjustedIndex -= (_groupsPinned.isNotEmpty ? 1 : 0);
// Get the contacts that are not pinned // Get the contacts that are not pinned
final contact = _contacts.elementAt( final group = _groupsNotPinned.elementAt(
adjustedIndex, adjustedIndex,
); );
return UserListItem( return GroupListItem(
key: ValueKey(contact.userId), key: ValueKey(group.groupId),
user: contact, group: group,
firstUserListItemKey: firstUserListItemKey:
(index == 0) ? firstUserListItemKey : null, (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 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
@ -21,10 +22,13 @@ class _LastMessageTimeState extends State<LastMessageTime> {
void initState() { void initState() {
super.initState(); super.initState();
// Change the color every 200 milliseconds // 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(() { setState(() {
lastMessageInSeconds = DateTime.now() lastMessageInSeconds = DateTime.now()
.difference(widget.message.openedAt ?? widget.message.sendAt) .difference(lastAction?.actionAt ?? widget.message.createdAt)
.inSeconds; .inSeconds;
if (lastMessageInSeconds < 0) { if (lastMessageInSeconds < 0) {
lastMessageInSeconds = 0; lastMessageInSeconds = 0;

View file

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

View file

@ -1,10 +1,9 @@
import 'dart:convert';
import 'package:flutter/material.dart'; 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/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/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_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_reaction_row.dart';
import 'package:twonly/src/views/chats/chat_messages_components/chat_text_entry.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 { class ChatListEntry extends StatefulWidget {
const ChatListEntry( const ChatListEntry(
this.msg, this.message,
this.contact, this.group,
this.galleryItems, this.galleryItems,
this.lastMessageFromSameUser, this.lastMessageFromSameUser, {
this.otherReactions, {
required this.onResponseTriggered, required this.onResponseTriggered,
required this.scrollToMessage, required this.scrollToMessage,
super.key, super.key,
}); });
final ChatMessage msg; final Message message;
final Contact contact; final Group group;
final bool lastMessageFromSameUser; final bool lastMessageFromSameUser;
final List<Message> otherReactions;
final List<MemoryItem> galleryItems; final List<MemoryItem> galleryItems;
final void Function(int) scrollToMessage; final void Function(String) scrollToMessage;
final void Function() onResponseTriggered; final void Function() onResponseTriggered;
@override @override
@ -36,26 +33,22 @@ class ChatListEntry extends StatefulWidget {
} }
class _ChatListEntryState extends State<ChatListEntry> { class _ChatListEntryState extends State<ChatListEntry> {
MessageContent? content; MediaFileService? mediaService;
String? textMessage;
@override @override
void initState() { void initState() {
initAsync();
super.initState(); 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (content == null) return Container(); final right = widget.message.senderId == null;
final right = widget.msg.message.messageOtherId == null;
return Align( return Align(
alignment: right ? Alignment.centerRight : Alignment.centerLeft, 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, right: 10, left: 10)
: const EdgeInsets.only(top: 5, bottom: 20, right: 10, left: 10), : const EdgeInsets.only(top: 5, bottom: 20, right: 10, left: 10),
child: MessageContextMenu( child: MessageContextMenu(
message: widget.msg.message, message: widget.message,
onResponseTriggered: widget.onResponseTriggered, onResponseTriggered: widget.onResponseTriggered,
child: Column( child: Column(
mainAxisAlignment: mainAxisAlignment:
@ -73,27 +66,28 @@ class _ChatListEntryState extends State<ChatListEntry> {
right ? CrossAxisAlignment.end : CrossAxisAlignment.start, right ? CrossAxisAlignment.end : CrossAxisAlignment.start,
children: [ children: [
MessageActions( MessageActions(
message: widget.msg.message, message: widget.message,
onResponseTriggered: widget.onResponseTriggered, onResponseTriggered: widget.onResponseTriggered,
child: Stack( child: Stack(
alignment: alignment:
right ? Alignment.centerRight : Alignment.centerLeft, right ? Alignment.centerRight : Alignment.centerLeft,
children: [ children: [
ResponseContainer( ResponseContainer(
msg: widget.msg, msg: widget.message,
contact: widget.contact, group: widget.group,
mediaService: mediaService,
scrollToMessage: widget.scrollToMessage, scrollToMessage: widget.scrollToMessage,
child: (textMessage != null) child: (widget.message.type == MessageType.text)
? ChatTextEntry( ? ChatTextEntry(
message: widget.msg, message: widget.message,
text: textMessage!,
hasReaction: widget.otherReactions.isNotEmpty,
) )
: (mediaService == null)
? null
: ChatMediaEntry( : ChatMediaEntry(
message: widget.msg.message, message: widget.message,
contact: widget.contact, group: widget.group,
mediaService: mediaService!,
galleryItems: widget.galleryItems, galleryItems: widget.galleryItems,
content: content!,
), ),
), ),
Positioned( Positioned(
@ -101,8 +95,7 @@ class _ChatListEntryState extends State<ChatListEntry> {
left: 5, left: 5,
right: 5, right: 5,
child: ReactionRow( child: ReactionRow(
otherReactions: widget.otherReactions, message: widget.message,
message: widget.msg.message,
), ),
), ),
], ],

View file

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

View file

@ -1,20 +1,15 @@
import 'dart:convert'; import 'dart:async';
import 'package:flutter/material.dart'; 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/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'; import 'package:twonly/src/views/components/animate_icon.dart';
class ReactionRow extends StatefulWidget { class ReactionRow extends StatefulWidget {
const ReactionRow({ const ReactionRow({
required this.otherReactions,
required this.message, required this.message,
super.key, super.key,
}); });
final List<Message> otherReactions;
final Message message; final Message message;
@override @override
@ -22,50 +17,65 @@ class ReactionRow extends StatefulWidget {
} }
class _ReactionRowState extends State<ReactionRow> { 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final children = <Widget>[]; final children = <Widget>[];
var hasOneTextReaction = false; for (final reaction in reactions) {
var hasOneReopened = false; // if (content is ReopenedMediaFileContent) {
for (final reaction in widget.otherReactions.reversed) { // if (hasOneReopened) continue;
final content = MessageContent.fromJson( // hasOneReopened = true;
reaction.kind, // children.add(
jsonDecode(reaction.contentJson!) as Map, // Expanded(
); // child: Align(
// alignment: Alignment.bottomRight,
if (content is ReopenedMediaFileContent) { // child: Padding(
if (hasOneReopened) continue; // padding: const EdgeInsets.only(right: 3),
hasOneReopened = true; // child: FaIcon(
children.add( // FontAwesomeIcons.repeat,
Expanded( // size: 12,
child: Align( // color: isDarkMode(context) ? Colors.white : Colors.black,
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 // only show one reaction
if (hasOneTextReaction) continue;
if (content is TextMessageContent) {
hasOneTextReaction = true;
if (!isEmoji(content.text)) continue;
late Widget child; late Widget child;
if (EmojiAnimation.animatedIcons.containsKey(content.text)) { if (EmojiAnimation.animatedIcons.containsKey(reaction.emoji)) {
child = SizedBox( child = SizedBox(
height: 18, height: 18,
child: EmojiAnimation(emoji: content.text), child: EmojiAnimation(emoji: reaction.emoji),
); );
} else { } else {
child = Text(content.text, style: const TextStyle(fontSize: 14)); child = Text(reaction.emoji, style: const TextStyle(fontSize: 14));
} }
children.insert( children.insert(
0, 0,
@ -75,12 +85,11 @@ class _ReactionRowState extends State<ReactionRow> {
), ),
); );
} }
}
if (children.isEmpty) return Container(); if (children.isEmpty) return Container();
return Row( return Row(
mainAxisAlignment: widget.message.messageOtherId == null mainAxisAlignment: widget.message.senderId == null
? MainAxisAlignment.end ? MainAxisAlignment.end
: MainAxisAlignment.end, : MainAxisAlignment.end,
children: children, children: children,

View file

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

View file

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

View file

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

View file

@ -1,11 +1,11 @@
import 'dart:collection'; import 'dart:collection';
import 'dart:convert';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.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/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/utils/misc.dart';
import 'package:twonly/src/views/components/animate_icon.dart'; import 'package:twonly/src/views/components/animate_icon.dart';
@ -18,17 +18,23 @@ enum MessageSendState {
sending, sending,
} }
MessageSendState messageSendStateFromMessage(Message msg) { Future<MessageSendState> messageSendStateFromMessage(Message msg) async {
MessageSendState state; MessageSendState state;
if (!msg.acknowledgeByServer) { final ackByServer = await twonlyDB.messagesDao.haveAllMembers(
if (msg.messageOtherId == null) { msg.groupId,
msg.messageId,
MessageActionType.ackByServerAt,
);
if (!ackByServer) {
if (msg.senderId == null) {
state = MessageSendState.sending; state = MessageSendState.sending;
} else { } else {
state = MessageSendState.receiving; state = MessageSendState.receiving;
} }
} else { } else {
if (msg.messageOtherId == null) { if (msg.senderId == null) {
// message send // message send
if (msg.openedAt == null) { if (msg.openedAt == null) {
state = MessageSendState.send; state = MessageSendState.send;
@ -63,9 +69,113 @@ class MessageSendStateIcon extends StatefulWidget {
} }
class _MessageSendStateIconState extends State<MessageSendStateIcon> { class _MessageSendStateIconState extends State<MessageSendStateIcon> {
List<Widget> icons = <Widget>[];
String text = '';
Widget? textWidget;
@override @override
void initState() { void initState() {
super.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) { Widget getLoaderIcon(Color color) {
@ -83,108 +193,6 @@ class _MessageSendStateIconState extends State<MessageSendStateIcon> {
@override @override
Widget build(BuildContext context) { 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(); if (icons.isEmpty) return Container();
var icon = icons[0]; var icon = icons[0];
@ -215,7 +223,7 @@ class _MessageSendStateIconState extends State<MessageSendStateIcon> {
icon, icon,
const SizedBox(width: 3), const SizedBox(width: 3),
if (textWidget != null) if (textWidget != null)
textWidget textWidget!
else else
Text( Text(
text, text,

View file

@ -1,29 +1,27 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:twonly/src/database/daos/contacts.dao.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/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/model/memory_item.model.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/chats/chat_messages.view.dart'; import 'package:twonly/src/views/chats/chat_messages.view.dart';
class ResponseContainer extends StatefulWidget { class ResponseContainer extends StatefulWidget {
const ResponseContainer({ const ResponseContainer({
required this.msg, required this.msg,
required this.contact, required this.group,
required this.child, required this.child,
required this.scrollToMessage, required this.scrollToMessage,
required this.mediaService,
super.key, super.key,
}); });
final ChatMessage msg; final Message msg;
final Widget child; final Widget? child;
final Contact contact; final Group group;
final void Function(int) scrollToMessage; final MediaFileService? mediaService;
final void Function(String) scrollToMessage;
@override @override
State<ResponseContainer> createState() => _ResponseContainerState(); State<ResponseContainer> createState() => _ResponseContainerState();
@ -57,17 +55,20 @@ class _ResponseContainerState extends State<ResponseContainer> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (widget.msg.responseTo == null) { if (widget.msg.quotesMessageId == null) {
return widget.child; if (widget.child == null) {
return Container();
}
return widget.child!;
} }
return GestureDetector( return GestureDetector(
onTap: () => widget.scrollToMessage(widget.msg.responseTo!.messageId), onTap: () => widget.scrollToMessage(widget.msg.quotesMessageId!),
child: Container( child: Container(
constraints: BoxConstraints( constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.8, maxWidth: MediaQuery.of(context).size.width * 0.8,
), ),
decoration: BoxDecoration( decoration: BoxDecoration(
color: getMessageColor(widget.msg.message), color: getMessageColor(widget.msg),
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
child: Column( child: Column(
@ -88,8 +89,8 @@ class _ResponseContainerState extends State<ResponseContainer> {
), ),
), ),
child: ResponsePreview( child: ResponsePreview(
contact: widget.contact, group: widget.group,
message: widget.msg.responseTo!, messageId: widget.msg.quotesMessageId,
showBorder: false, showBorder: false,
), ),
), ),
@ -108,14 +109,16 @@ class _ResponseContainerState extends State<ResponseContainer> {
class ResponsePreview extends StatefulWidget { class ResponsePreview extends StatefulWidget {
const ResponsePreview({ const ResponsePreview({
required this.message, required this.group,
required this.contact,
required this.showBorder, required this.showBorder,
this.message,
this.messageId,
super.key, super.key,
}); });
final Message message; final Message? message;
final Contact contact; final String? messageId;
final Group group;
final bool showBorder; final bool showBorder;
@override @override
@ -123,56 +126,49 @@ class ResponsePreview extends StatefulWidget {
} }
class _ResponsePreviewState extends State<ResponsePreview> { class _ResponsePreviewState extends State<ResponsePreview> {
File? thumbnailPath; Message? message;
MediaFileService? mediaService;
@override @override
void initState() { void initState() {
message = widget.message;
initAsync();
super.initState(); super.initState();
unawaited(initAsync());
} }
Future<void> initAsync() async { Future<void> initAsync() async {
final items = await MemoryItem.convertFromMessages([widget.message]); message ??= await twonlyDB.messagesDao
if (items.length == 1 && mounted) { .getMessageById(widget.messageId!)
setState(() { .getSingleOrNull();
thumbnailPath = items.values.first.thumbnailPath; if (message?.mediaId != null) {
}); mediaService = await MediaFileService.fromMediaId(message!.mediaId!);
} }
if (mounted) setState(() {});
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (message == null) return Container();
String? subtitle; String? subtitle;
if (widget.message.kind == MessageKind.textMessage) { if (message!.type == MessageType.text) {
if (widget.message.contentJson != null) { if (message!.content != null) {
final content = MessageContent.fromJson( subtitle = truncateString(message!.content!);
MessageKind.textMessage,
jsonDecode(widget.message.contentJson!) as Map,
);
if (content is TextMessageContent) {
subtitle = truncateString(content.text);
} }
} }
} if (message!.type == MessageType.media && mediaService != null) {
if (widget.message.kind == MessageKind.media) { subtitle =
final content = MessageContent.fromJson( mediaService!.mediaFile.type == MediaType.video ? 'Video' : 'Image';
MessageKind.media,
jsonDecode(widget.message.contentJson!) as Map,
);
if (content is MediaMessageContent) {
subtitle = content.isVideo ? 'Video' : 'Image';
}
} }
var username = 'You'; var username = 'You';
if (widget.message.messageOtherId != null) { if (message!.senderId != null) {
username = getContactDisplayName(widget.contact); username = message!.senderId.toString();
} }
final color = getMessageColor(widget.message); final color = getMessageColor(message!);
if (!widget.message.mediaStored) { if (!message!.mediaStored) {
return Container( return Container(
padding: widget.showBorder padding: widget.showBorder
? const EdgeInsets.only(left: 10, right: 10) ? const EdgeInsets.only(left: 10, right: 10)
@ -225,10 +221,10 @@ class _ResponsePreviewState extends State<ResponsePreview> {
], ],
), ),
), ),
if (thumbnailPath != null) if (mediaService != null)
SizedBox( SizedBox(
height: widget.showBorder ? 100 : 210, 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:async';
import 'dart:convert';
import 'dart:io';
import 'package:drift/drift.dart' hide Column; import 'package:drift/drift.dart' hide Column;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:lottie/lottie.dart'; import 'package:lottie/lottie.dart';
import 'package:no_screenshot/no_screenshot.dart'; import 'package:no_screenshot/no_screenshot.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart'
import 'package:twonly/src/database/tables/messages_table.dart'; show DownloadState, MediaType;
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/json/message_old.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'
import 'package:twonly/src/model/protobuf/push_notification/push_notification.pb.dart'; as pb;
import 'package:twonly/src/services/api/mediafiles/download.service.dart'; import 'package:twonly/src/services/api/mediafiles/download.service.dart';
import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/services/api/messages.dart';
import 'package:twonly/src/services/api/utils.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/services/notifications/background.notifications.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart';
import 'package:twonly/src/views/camera/camera_send_to_view.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/animate_icon.dart';
import 'package:twonly/src/views/components/media_view_sizing.dart'; import 'package:twonly/src/views/components/media_view_sizing.dart';
import 'package:video_player/video_player.dart'; import 'package:video_player/video_player.dart';
@ -31,8 +26,8 @@ import 'package:video_player/video_player.dart';
final NoScreenshot _noScreenshot = NoScreenshot.instance; final NoScreenshot _noScreenshot = NoScreenshot.instance;
class MediaViewerView extends StatefulWidget { class MediaViewerView extends StatefulWidget {
const MediaViewerView(this.contact, {super.key, this.initialMessage}); const MediaViewerView(this.group, {super.key, this.initialMessage});
final Contact contact; final Group group;
final Message? initialMessage; final Message? initialMessage;
@ -48,23 +43,21 @@ class _MediaViewerViewState extends State<MediaViewerView> {
double mediaViewerDistanceFromBottom = 0; double mediaViewerDistanceFromBottom = 0;
// current image related // current image related
Uint8List? imageBytes;
String? videoPath;
VideoPlayerController? videoController; VideoPlayerController? videoController;
MediaFileService? currentMedia;
Message? currentMessage;
DateTime? canBeSeenUntil; DateTime? canBeSeenUntil;
int maxShowTime = gMediaShowInfinite;
double progress = 0; double progress = 0;
bool isRealTwonly = false;
bool mirrorVideo = false;
bool isDownloading = false;
bool showSendTextMessageInput = false; bool showSendTextMessageInput = false;
final GlobalKey mediaWidgetKey = GlobalKey(); final GlobalKey mediaWidgetKey = GlobalKey();
bool imageSaved = false; bool imageSaved = false;
bool imageSaving = false; bool imageSaving = false;
bool displayTwonlyPresent = true;
StreamSubscription<Message?>? downloadStateListener; StreamSubscription<MediaFile?>? downloadStateListener;
List<Message> allMediaFiles = []; List<Message> allMediaFiles = [];
late StreamSubscription<List<Message>> _subscription; late StreamSubscription<List<Message>> _subscription;
@ -94,20 +87,21 @@ class _MediaViewerViewState extends State<MediaViewerView> {
Future<void> asyncLoadNextMedia(bool firstRun) async { Future<void> asyncLoadNextMedia(bool firstRun) async {
final messages = 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) { for (final msg in messages) {
// if (!allMediaFiles.any((m) => m.messageId == msg.messageId)) { if (msg.mediaId == currentMedia?.mediaFile.mediaId) {
// allMediaFiles.add(msg); // The update of the current Media in case of a download is done in loadCurrentMediaFile
// } continue;
// Find the index of the existing message with the same messageId }
/// If the messages was already there just replace it and go to the next...
final index = final index =
allMediaFiles.indexWhere((m) => m.messageId == msg.messageId); allMediaFiles.indexWhere((m) => m.messageId == msg.messageId);
if (index >= 1) { if (index >= 1) {
// to not modify the first message
// If the message exists, replace it
allMediaFiles[index] = msg; allMediaFiles[index] = msg;
} else if (index == -1) { } else if (index == -1) {
// If the message does not exist, add it // If the message does not exist, add it
@ -116,101 +110,90 @@ class _MediaViewerViewState extends State<MediaViewerView> {
} }
setState(() {}); setState(() {});
if (firstRun) { if (firstRun) {
loadCurrentMediaFile();
// ignore: parameter_assignments // ignore: parameter_assignments
firstRun = false; firstRun = false;
await loadCurrentMediaFile();
} }
}); });
} }
Future<void> nextMediaOrExit() async { Future<void> nextMediaOrExit() async {
if (!mounted) return; /// Remove the current media file in case it is not set to unlimited
if (currentMedia != null) {
if (!imageSaved &&
currentMedia!.mediaFile.displayLimitInMilliseconds != null) {
currentMedia!.fullMediaRemoval();
}
}
await videoController?.dispose(); await videoController?.dispose();
if (!mounted) return;
nextMediaTimer?.cancel(); nextMediaTimer?.cancel();
progressTimer?.cancel(); progressTimer?.cancel();
if (allMediaFiles.isNotEmpty) {
try { if (allMediaFiles.isEmpty) {
if (!imageSaved && maxShowTime != gMediaShowInfinite) {
await deleteMediaFile(allMediaFiles.first.messageId, 'mp4');
await deleteMediaFile(allMediaFiles.first.messageId, 'png');
}
} catch (e) {
Log.error('$e');
}
}
if (allMediaFiles.isEmpty || allMediaFiles.length == 1) {
if (mounted) {
Navigator.pop(context); Navigator.pop(context);
}
} else { } else {
allMediaFiles.removeAt(0);
await loadCurrentMediaFile(); await loadCurrentMediaFile();
} }
} }
Future<void> loadCurrentMediaFile({bool showTwonly = false}) async { Future<void> loadCurrentMediaFile({bool showTwonly = false}) async {
if (!mounted) return; if (!mounted || !context.mounted) return;
if (!context.mounted || allMediaFiles.isEmpty) return nextMediaOrExit(); if (allMediaFiles.isEmpty) return nextMediaOrExit();
await _noScreenshot.screenshotOff(); await _noScreenshot.screenshotOff();
setState(() { setState(() {
videoController = null; videoController = null;
imageBytes = null; currentMedia = null;
currentMessage = null;
canBeSeenUntil = null; canBeSeenUntil = null;
maxShowTime = gMediaShowInfinite;
imageSaving = false; imageSaving = false;
imageSaved = false; imageSaved = false;
mirrorVideo = false;
progress = 0; progress = 0;
videoPath = null;
isDownloading = false;
isRealTwonly = false;
showSendTextMessageInput = false; showSendTextMessageInput = false;
}); });
if (Platform.isAndroid) { // if (Platform.isAndroid) {
await flutterLocalNotificationsPlugin // await flutterLocalNotificationsPlugin
.cancel(allMediaFiles.first.contactId); // .cancel(allMediaFiles.first.contactId);
} else { // } else {
await flutterLocalNotificationsPlugin.cancelAll(); await flutterLocalNotificationsPlugin.cancelAll();
} // }
if (allMediaFiles.first.downloadState != DownloadState.downloaded) { final stream =
setState(() { twonlyDB.mediaFilesDao.watchMedia(currentMedia!.mediaFile.mediaId);
isDownloading = true;
}); var downloadTriggered = false;
await startDownloadMedia(allMediaFiles.first, true);
final stream = twonlyDB.messagesDao
.getMessageByMessageId(allMediaFiles.first.messageId)
.watchSingleOrNull();
await downloadStateListener?.cancel(); await downloadStateListener?.cancel();
downloadStateListener = stream.listen((updated) async { downloadStateListener = stream.listen((updated) async {
if (updated != null) { if (updated == null) return;
if (updated.downloadState == DownloadState.downloaded) { if (updated.downloadState != DownloadState.downloaded) {
if (!downloadTriggered) {
downloadTriggered = true;
await startDownloadMedia(currentMedia!.mediaFile, true);
unawaited(tryDownloadAllMediaFiles(force: true));
}
return;
}
await downloadStateListener?.cancel(); await downloadStateListener?.cancel();
await handleNextDownloadedMedia(updated, showTwonly); await handleNextDownloadedMedia(showTwonly);
// start downloading all the other possible missing media files. // start downloading all the other possible missing media files.
await tryDownloadAllMediaFiles(force: true);
}
}
}); });
} else {
await handleNextDownloadedMedia(allMediaFiles.first, showTwonly);
}
} }
Future<void> handleNextDownloadedMedia( Future<void> handleNextDownloadedMedia(
Message current,
bool showTwonly, bool showTwonly,
) async { ) async {
final content = currentMessage = allMediaFiles.removeAt(0);
MediaMessageContent.fromJson(jsonDecode(current.contentJson!) as Map); final currentMediaLocal =
await MediaFileService.fromMediaId(currentMessage!.mediaId!);
if (currentMediaLocal == null || !mounted) return;
if (content.isRealTwonly) { if (currentMediaLocal.mediaFile.requiresAuthentication) {
setState(() {
isRealTwonly = true;
});
if (!showTwonly) return; if (!showTwonly) return;
final isAuth = await authenticateUser( final isAuth = await authenticateUser(
@ -224,21 +207,26 @@ class _MediaViewerViewState extends State<MediaViewerView> {
} }
await notifyContactAboutOpeningMessage( await notifyContactAboutOpeningMessage(
current.contactId, currentMessage!.senderId!,
[current.messageOtherId!], [currentMessage!.messageId],
); );
await twonlyDB.messagesDao.updateMessageByMessageId( await twonlyDB.messagesDao.updateMessageId(
current.messageId, currentMessage!.messageId,
MessagesCompanion(openedAt: Value(DateTime.now())), MessagesCompanion(openedAt: Value(DateTime.now())),
); );
if (content.isVideo) { if (!currentMediaLocal.tempPath.existsSync()) {
final videoPathTmp = await getVideoPath(current.messageId); Log.error('Temp media file not found...');
if (videoPathTmp != null) { await handleMediaError(currentMediaLocal.mediaFile);
videoController = VideoPlayerController.file(File(videoPathTmp.path)); return nextMediaOrExit();
await videoController }
?.setLooping(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((_) { await videoController?.initialize().then((_) {
videoController!.play(); videoController!.play();
videoController?.addListener(() { videoController?.addListener(() {
@ -247,43 +235,28 @@ class _MediaViewerViewState extends State<MediaViewerView> {
videoController!.value.position.inSeconds / videoController!.value.position.inSeconds /
videoController!.value.duration.inSeconds; videoController!.value.duration.inSeconds;
}); });
if (content.maxShowTime != gMediaShowInfinite) { if (currentMediaLocal.mediaFile.displayLimitInMilliseconds != null) {
if (videoController?.value.position == if (videoController?.value.position ==
videoController?.value.duration) { videoController?.value.duration) {
nextMediaOrExit(); nextMediaOrExit();
} }
} }
}); });
setState(() {
videoPath = videoPathTmp.path;
});
// ignore: invalid_return_type_for_catch_error, argument_type_not_assignable_to_error_handler // ignore: invalid_return_type_for_catch_error, argument_type_not_assignable_to_error_handler
}).catchError(Log.error); }).catchError(Log.error);
} } else {
} if (currentMediaLocal.mediaFile.displayLimitInMilliseconds != null) {
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);
return nextMediaOrExit();
}
if (!content.isVideo) {
if (content.maxShowTime != gMediaShowInfinite) {
canBeSeenUntil = DateTime.now().add( canBeSeenUntil = DateTime.now().add(
Duration(seconds: content.maxShowTime), Duration(
milliseconds:
currentMediaLocal.mediaFile.displayLimitInMilliseconds!,
),
); );
startTimer(); startTimer();
} }
} }
setState(() { setState(() {
maxShowTime = content.maxShowTime; currentMedia = currentMediaLocal;
isDownloading = false;
mirrorVideo = content.mirrorVideo;
}); });
} }
@ -299,44 +272,37 @@ class _MediaViewerViewState extends State<MediaViewerView> {
if (canBeSeenUntil != null) { if (canBeSeenUntil != null) {
final difference = canBeSeenUntil!.difference(DateTime.now()); final difference = canBeSeenUntil!.difference(DateTime.now());
// Calculate the progress as a value between 0.0 and 1.0 // Calculate the progress as a value between 0.0 and 1.0
progress = difference.inMilliseconds / (maxShowTime * 1000); progress = difference.inMilliseconds /
(currentMedia!.mediaFile.displayLimitInMilliseconds!);
setState(() {}); setState(() {});
} }
}); });
} }
Future<void> onPressedSaveToGallery() async { Future<void> onPressedSaveToGallery() async {
if (allMediaFiles.first.messageOtherId == null) {
return; // should not be possible
}
setState(() { setState(() {
imageSaving = true; imageSaving = true;
}); });
await twonlyDB.messagesDao.updateMessageByMessageId( await currentMedia!.storeMediaFile();
allMediaFiles.first.messageId, await sendCipherTextToGroup(
const MessagesCompanion(mediaStored: Value(true)), widget.group.groupId,
); pb.EncryptedContent(
await encryptAndSendMessageAsync( mediaUpdate: pb.EncryptedContent_MediaUpdate(
null, type: pb.EncryptedContent_MediaUpdate_Type.STORED,
widget.contact.userId, targetMediaId: currentMedia!.mediaFile.mediaId,
MessageJson( ),
kind: MessageKind.storedMediaFile,
messageSenderId: allMediaFiles.first.messageId,
messageReceiverId: allMediaFiles.first.messageOtherId,
content: MessageContent(),
timestamp: DateTime.now(),
), ),
pushNotification: PushNotification(kind: PushKind.storedMediaFile),
); );
setState(() { setState(() {
imageSaved = true; imageSaved = true;
}); });
final user = await getUser();
if (user != null && (user.storeMediaFilesInGallery)) { if (gUser.storeMediaFilesInGallery) {
if (videoPath != null) { if (currentMedia!.mediaFile.type == MediaType.video) {
await saveVideoToGallery(videoPath!); await saveVideoToGallery(currentMedia!.storedPath.path);
} else { } else if (currentMedia!.mediaFile.type == MediaType.image) {
await saveImageToGallery(imageBytes!); final imageBytes = await currentMedia!.storedPath.readAsBytes();
await saveImageToGallery(imageBytes);
} }
} }
setState(() { setState(() {
@ -358,7 +324,8 @@ class _MediaViewerViewState extends State<MediaViewerView> {
key: mediaWidgetKey, key: mediaWidgetKey,
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
if (maxShowTime == gMediaShowInfinite) if (currentMedia != null &&
currentMedia!.mediaFile.displayLimitInMilliseconds == null)
OutlinedButton( OutlinedButton(
style: OutlinedButton.styleFrom( style: OutlinedButton.styleFrom(
iconColor: imageSaved iconColor: imageSaved
@ -368,7 +335,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
? Theme.of(context).colorScheme.outline ? Theme.of(context).colorScheme.outline
: Theme.of(context).colorScheme.primary, : Theme.of(context).colorScheme.primary,
), ),
onPressed: onPressedSaveToGallery, onPressed: (currentMedia == null) ? null : onPressedSaveToGallery,
child: Row( child: Row(
children: [ children: [
if (imageSaving) if (imageSaving)
@ -450,11 +417,12 @@ class _MediaViewerViewState extends State<MediaViewerView> {
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) { builder: (context) {
return CameraSendToView(widget.contact); return CameraSendToView(widget.group);
}, },
), ),
); );
if (mounted && maxShowTime != gMediaShowInfinite) { if (mounted &&
currentMedia!.mediaFile.displayLimitInMilliseconds != null) {
await nextMediaOrExit(); await nextMediaOrExit();
} else { } else {
await videoController?.play(); await videoController?.play();
@ -477,7 +445,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
child: Stack( child: Stack(
fit: StackFit.expand, fit: StackFit.expand,
children: [ children: [
if ((imageBytes != null || videoController != null) && if ((currentMedia != null || videoController != null) &&
(canBeSeenUntil == null || progress >= 0)) (canBeSeenUntil == null || progress >= 0))
GestureDetector( GestureDetector(
onTap: () { onTap: () {
@ -497,15 +465,12 @@ class _MediaViewerViewState extends State<MediaViewerView> {
children: [ children: [
if (videoController != null) if (videoController != null)
Positioned.fill( Positioned.fill(
child: Transform.flip(
flipX: mirrorVideo,
child: VideoPlayer(videoController!), child: VideoPlayer(videoController!),
), ),
), if (currentMedia!.mediaFile.type == MediaType.image)
if (imageBytes != null)
Positioned.fill( Positioned.fill(
child: Image.memory( child: Image.file(
imageBytes!, currentMedia!.tempPath,
fit: BoxFit.contain, fit: BoxFit.contain,
frameBuilder: ( frameBuilder: (
context, context,
@ -534,7 +499,9 @@ class _MediaViewerViewState extends State<MediaViewerView> {
), ),
), ),
), ),
if (isRealTwonly && imageBytes == null) if (currentMedia != null &&
currentMedia!.mediaFile.requiresAuthentication &&
displayTwonlyPresent)
Positioned.fill( Positioned.fill(
child: GestureDetector( child: GestureDetector(
onTap: () { onTap: () {
@ -570,7 +537,8 @@ class _MediaViewerViewState extends State<MediaViewerView> {
], ],
), ),
), ),
if (isDownloading) if (currentMedia?.mediaFile.downloadState !=
DownloadState.downloaded)
const Positioned.fill( const Positioned.fill(
child: Center( child: Center(
child: SizedBox( child: SizedBox(
@ -602,7 +570,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
left: showSendTextMessageInput ? 0 : null, left: showSendTextMessageInput ? 0 : null,
right: showSendTextMessageInput ? 0 : 15, right: showSendTextMessageInput ? 0 : 15,
child: Text( child: Text(
getContactDisplayName(widget.contact), widget.group.groupName,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
fontSize: showSendTextMessageInput ? 24 : 14, fontSize: showSendTextMessageInput ? 24 : 14,
@ -658,18 +626,12 @@ class _MediaViewerViewState extends State<MediaViewerView> {
), ),
IconButton( IconButton(
icon: const FaIcon(FontAwesomeIcons.solidPaperPlane), icon: const FaIcon(FontAwesomeIcons.solidPaperPlane),
onPressed: () { onPressed: () async {
if (textMessageController.text.isNotEmpty) { if (textMessageController.text.isNotEmpty) {
sendTextMessage( await insertAndSendTextMessage(
widget.contact.userId, widget.group.groupId,
TextMessageContent( textMessageController.text,
text: textMessageController.text, currentMessage!.messageId,
responseToMessageId:
allMediaFiles.first.messageOtherId,
),
PushNotification(
kind: PushKind.response,
),
); );
textMessageController.clear(); textMessageController.clear();
} }
@ -683,14 +645,13 @@ class _MediaViewerViewState extends State<MediaViewerView> {
), ),
), ),
), ),
if (allMediaFiles.isNotEmpty) if (currentMedia != null)
ReactionButtons( ReactionButtons(
show: showShortReactions, show: showShortReactions,
textInputFocused: showSendTextMessageInput, textInputFocused: showSendTextMessageInput,
mediaViewerDistanceFromBottom: mediaViewerDistanceFromBottom, mediaViewerDistanceFromBottom: mediaViewerDistanceFromBottom,
userId: widget.contact.userId, groupId: widget.group.groupId,
responseToMessageId: allMediaFiles.first.messageOtherId!, messageId: currentMessage!.messageId,
isVideo: videoController != null,
hide: () { hide: () {
setState(() { setState(() {
showShortReactions = false; 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 'dart:async';
import 'package:drift/drift.dart' hide Column; import 'package:drift/drift.dart' hide Column;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.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/utils/misc.dart';
import 'package:twonly/src/views/chats/add_new_user.view.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/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/flame.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/user_context_menu.component.dart';
class StartNewChatView extends StatefulWidget { class StartNewChatView extends StatefulWidget {
@ -30,7 +29,7 @@ class _StartNewChatView extends State<StartNewChatView> {
void initState() { void initState() {
super.initState(); super.initState();
final stream = twonlyDB.contactsDao.watchContactsForStartNewChat(); final stream = twonlyDB.contactsDao.watchAllAcceptedContacts();
contactSub = stream.listen((update) async { contactSub = stream.listen((update) async {
update.sort( update.sort(
@ -147,7 +146,6 @@ class UserList extends StatelessWidget {
return const Divider(); return const Divider();
} }
final user = users[i - 2]; final user = users[i - 2];
final flameCounter = getFlameCounterFromContact(user);
return UserContextMenu( return UserContextMenu(
key: Key(user.userId.toString()), key: Key(user.userId.toString()),
contact: user, contact: user,
@ -155,40 +153,37 @@ class UserList extends StatelessWidget {
title: Row( title: Row(
children: [ children: [
Text(getContactDisplayName(user)), Text(getContactDisplayName(user)),
if (flameCounter >= 1)
FlameCounterWidget( FlameCounterWidget(
user, contactId: user.userId,
flameCounter,
prefix: true, 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,
),
], ],
), ),
leading: ContactAvatar( leading: AvatarIcon(
contact: user, contact: user,
fontSize: 13, fontSize: 13,
), ),
onTap: () async { 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( await Navigator.pushReplacement(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) { 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:flutter/material.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/views/components/animate_icon.dart'; import 'package:twonly/src/views/components/animate_icon.dart';
class FlameCounterWidget extends StatelessWidget { class FlameCounterWidget extends StatefulWidget {
const FlameCounterWidget( const FlameCounterWidget({
this.user, this.groupId,
this.flameCounter, { this.contactId,
this.prefix = false, this.prefix = false,
super.key, super.key,
}); });
final Contact user; final String? groupId;
final int flameCounter; final int? contactId;
final bool prefix; 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Row( return Row(
children: [ children: [
if (prefix) const SizedBox(width: 5), if (widget.prefix) const SizedBox(width: 5),
if (prefix) const Text(''), if (widget.prefix) const Text(''),
if (prefix) const SizedBox(width: 5), if (widget.prefix) const SizedBox(width: 5),
Text( Text(
flameCounter.toString(), flameCounter.toString(),
style: const TextStyle(fontSize: 13), style: const TextStyle(fontSize: 13),
@ -28,7 +67,7 @@ class FlameCounterWidget extends StatelessWidget {
SizedBox( SizedBox(
height: 15, height: 15,
child: EmojiAnimation( 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/utils/misc.dart';
import 'package:twonly/src/views/contact/contact.view.dart'; import 'package:twonly/src/views/contact/contact.view.dart';
class UserContextMenuBlocked extends StatefulWidget { class UserContextMenu extends StatefulWidget {
const UserContextMenuBlocked({ const UserContextMenu({
required this.contact, required this.contact,
required this.child, required this.child,
super.key, super.key,
@ -15,10 +15,10 @@ class UserContextMenuBlocked extends StatefulWidget {
final Contact contact; final Contact contact;
@override @override
State<UserContextMenuBlocked> createState() => _UserContextMenuBlocked(); State<UserContextMenu> createState() => _UserContextMenuBlocked();
} }
class _UserContextMenuBlocked extends State<UserContextMenuBlocked> { class _UserContextMenuBlocked extends State<UserContextMenu> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return PieMenu( 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/services/api/utils.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/components/alert_dialog.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/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/components/verified_shield.dart';
import 'package:twonly/src/views/contact/contact_verify.view.dart'; import 'package:twonly/src/views/contact/contact_verify.view.dart';
@ -105,12 +104,12 @@ class _ContactViewState extends State<ContactView> {
return Container(); return Container();
} }
final contact = snapshot.data!; final contact = snapshot.data!;
final flameCounter = getFlameCounterFromContact(contact); // final flameCounter = getFlameCounterFromContact(contact);
return ListView( return ListView(
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.all(10), padding: const EdgeInsets.all(10),
child: ContactAvatar(contact: contact, fontSize: 30), child: AvatarIcon(contact: contact, fontSize: 30),
), ),
Row( Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
@ -123,12 +122,12 @@ class _ContactViewState extends State<ContactView> {
getContactDisplayName(contact), getContactDisplayName(contact),
style: const TextStyle(fontSize: 20), style: const TextStyle(fontSize: 20),
), ),
if (flameCounter > 0) // if (flameCounter > 0)
FlameCounterWidget( // FlameCounterWidget(
contact, // contact,
flameCounter, // flameCounter,
prefix: true, // prefix: true,
), // ),
], ],
), ),
if (getContactDisplayName(contact) != contact.username) 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, minScale: PhotoViewComputedScale.contained,
maxScale: PhotoViewComputedScale.covered * 4.1, maxScale: PhotoViewComputedScale.covered * 4.1,
heroAttributes: PhotoViewHeroAttributes( heroAttributes: PhotoViewHeroAttributes(
tag: item.mediaService.mediaFile.mediaId), tag: item.mediaService.mediaFile.mediaId,
),
) )
: PhotoViewGalleryPageOptions( : PhotoViewGalleryPageOptions(
imageProvider: FileImage(item.mediaService.storedPath), imageProvider: FileImage(item.mediaService.storedPath),
@ -216,7 +217,8 @@ class _MemoriesPhotoSliderViewState extends State<MemoriesPhotoSliderView> {
minScale: PhotoViewComputedScale.contained, minScale: PhotoViewComputedScale.contained,
maxScale: PhotoViewComputedScale.covered * 4.1, maxScale: PhotoViewComputedScale.covered * 4.1,
heroAttributes: PhotoViewHeroAttributes( heroAttributes: PhotoViewHeroAttributes(
tag: item.mediaService.mediaFile.mediaId), tag: item.mediaService.mediaFile.mediaId,
),
); );
} }
} }

View file

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

View file

@ -1,9 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.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/utils/storage.dart';
import 'package:twonly/src/views/settings/developer/automated_testing.view.dart'; import 'package:twonly/src/views/settings/developer/automated_testing.view.dart';
import 'package:twonly/src/views/settings/developer/retransmission_data.view.dart'; import 'package:twonly/src/views/settings/developer/retransmission_data.view.dart';
@ -69,14 +66,14 @@ class _DeveloperSettingsViewState extends State<DeveloperSettingsView> {
); );
}, },
), ),
if (kDebugMode) // if (kDebugMode)
ListTile( // ListTile(
title: const Text('FlameSync Test'), // title: const Text('FlameSync Test'),
onTap: () async { // onTap: () async {
await twonlyDB.contactsDao.modifyFlameCounterForTesting(); // await twonlyDB.contactsDao.modifyFlameCounterForTesting();
await syncFlameCounters(); // await syncFlameCounters();
}, // },
), // ),
if (kDebugMode) if (kDebugMode)
ListTile( ListTile(
title: const Text('Automated Testing'), 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/daos/contacts.dao.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/utils/misc.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'; import 'package:twonly/src/views/components/user_context_menu.component.dart';
class PrivacyViewBlockUsers extends StatefulWidget { class PrivacyViewBlockUsers extends StatefulWidget {
@ -108,7 +108,7 @@ class UserList extends StatelessWidget {
itemCount: users.length, itemCount: users.length,
itemBuilder: (BuildContext context, int i) { itemBuilder: (BuildContext context, int i) {
final user = users[i]; final user = users[i];
return UserContextMenuBlocked( return UserContextMenu(
contact: user, contact: user,
child: ListTile( child: ListTile(
title: Row( title: Row(
@ -116,7 +116,7 @@ class UserList extends StatelessWidget {
Text(getContactDisplayName(user)), Text(getContactDisplayName(user)),
], ],
), ),
leading: ContactAvatar(contact: user, fontSize: 15), leading: AvatarIcon(contact: user, fontSize: 15),
trailing: Checkbox( trailing: Checkbox(
value: user.blocked, value: user.blocked,
onChanged: (bool? value) async { 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/model/json/userdata.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/utils/storage.dart';
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/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/account.view.dart';
import 'package:twonly/src/views/settings/appearance.view.dart'; import 'package:twonly/src/views/settings/appearance.view.dart';
import 'package:twonly/src/views/settings/backup/backup.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), color: context.color.surface.withAlpha(0),
child: Row( child: Row(
children: [ children: [
ContactAvatar( AvatarIcon(
userData: userData, userData: userData,
fontSize: 30, fontSize: 30,
), ),