mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-01-15 07:48:40 +00:00
fixing all compile errors
This commit is contained in:
parent
84828cd820
commit
4260c63ce2
61 changed files with 3223 additions and 3606 deletions
11
lib/app.dart
11
lib/app.dart
|
|
@ -43,18 +43,7 @@ class _AppState extends State<App> with WidgetsBindingObserver {
|
|||
|
||||
Future<void> setUserPlan() async {
|
||||
final user = await getUser();
|
||||
globalBestFriendUserId = -1;
|
||||
if (user != null && mounted) {
|
||||
if (user.myBestFriendContactId != null) {
|
||||
final contact = await twonlyDB.contactsDao
|
||||
.getContactByUserId(user.myBestFriendContactId!)
|
||||
.getSingleOrNull();
|
||||
if (contact != null) {
|
||||
if (contact.alsoBestFriend) {
|
||||
globalBestFriendUserId = user.myBestFriendContactId ?? 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (mounted) {
|
||||
await context
|
||||
.read<CustomChangeProvider>()
|
||||
|
|
|
|||
|
|
@ -27,4 +27,3 @@ void Function() globalCallbackNewDeviceRegistered = () {};
|
|||
void Function(String planId) globalCallbackUpdatePlan = (String planId) {};
|
||||
|
||||
bool globalIsAppInBackground = true;
|
||||
int globalBestFriendUserId = -1;
|
||||
|
|
|
|||
|
|
@ -47,7 +47,6 @@ void main() async {
|
|||
await initFileDownloader();
|
||||
|
||||
// await twonlyDB.messagesDao.resetPendingDownloadState();
|
||||
// await twonlyDB.messagesDao.handleMediaFilesOlderThan30Days();
|
||||
// await twonlyDB.messageRetransmissionDao.purgeOldRetransmissions();
|
||||
// await twonlyDB.signalDao.purgeOutDatedPreKeys();
|
||||
|
||||
|
|
|
|||
|
|
@ -20,79 +20,6 @@ class ContactsDao extends DatabaseAccessor<TwonlyDB> with _$ContactsDaoMixin {
|
|||
}
|
||||
}
|
||||
|
||||
Future<int> incFlameCounter(
|
||||
int contactId,
|
||||
bool received,
|
||||
DateTime timestamp,
|
||||
) async {
|
||||
final contact = (await (select(contacts)
|
||||
..where((t) => t.userId.equals(contactId)))
|
||||
.get())
|
||||
.first;
|
||||
|
||||
final totalMediaCounter = contact.totalMediaCounter + 1;
|
||||
var flameCounter = contact.flameCounter;
|
||||
|
||||
if (contact.lastMessageReceived != null &&
|
||||
contact.lastMessageSend != null) {
|
||||
final now = DateTime.now();
|
||||
final startOfToday = DateTime(now.year, now.month, now.day);
|
||||
final twoDaysAgo = startOfToday.subtract(const Duration(days: 2));
|
||||
if (contact.lastMessageSend!.isBefore(twoDaysAgo) ||
|
||||
contact.lastMessageReceived!.isBefore(twoDaysAgo)) {
|
||||
flameCounter = 0;
|
||||
}
|
||||
}
|
||||
|
||||
var lastMessageSend = const Value<DateTime?>.absent();
|
||||
var lastMessageReceived = const Value<DateTime?>.absent();
|
||||
var lastFlameCounterChange = const Value<DateTime?>.absent();
|
||||
|
||||
if (contact.lastFlameCounterChange != null) {
|
||||
final now = DateTime.now();
|
||||
final startOfToday = DateTime(now.year, now.month, now.day);
|
||||
|
||||
if (contact.lastFlameCounterChange!.isBefore(startOfToday)) {
|
||||
// last flame update was yesterday. check if it can be updated.
|
||||
var updateFlame = false;
|
||||
if (received) {
|
||||
if (contact.lastMessageSend != null &&
|
||||
contact.lastMessageSend!.isAfter(startOfToday)) {
|
||||
// today a message was already send -> update flame
|
||||
updateFlame = true;
|
||||
}
|
||||
} else if (contact.lastMessageReceived != null &&
|
||||
contact.lastMessageReceived!.isAfter(startOfToday)) {
|
||||
// today a message was already received -> update flame
|
||||
updateFlame = true;
|
||||
}
|
||||
if (updateFlame) {
|
||||
flameCounter += 1;
|
||||
lastFlameCounterChange = Value(timestamp);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// There where no message until no...
|
||||
lastFlameCounterChange = Value(timestamp);
|
||||
}
|
||||
|
||||
if (received) {
|
||||
lastMessageReceived = Value(timestamp);
|
||||
} else {
|
||||
lastMessageSend = Value(timestamp);
|
||||
}
|
||||
|
||||
return (update(contacts)..where((t) => t.userId.equals(contactId))).write(
|
||||
ContactsCompanion(
|
||||
totalMediaCounter: Value(totalMediaCounter),
|
||||
lastFlameCounterChange: lastFlameCounterChange,
|
||||
lastMessageReceived: lastMessageReceived,
|
||||
lastMessageSend: lastMessageSend,
|
||||
flameCounter: Value(flameCounter),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
SingleOrNullSelectable<Contact> getContactByUserId(int userId) {
|
||||
return select(contacts)..where((t) => t.userId.equals(userId));
|
||||
}
|
||||
|
|
@ -135,37 +62,6 @@ class ContactsDao extends DatabaseAccessor<TwonlyDB> with _$ContactsDaoMixin {
|
|||
.watchSingleOrNull();
|
||||
}
|
||||
|
||||
// Stream<List<Contact>> watchContactsForShareView() {
|
||||
// return (select(contacts)
|
||||
// ..where(
|
||||
// (t) =>
|
||||
// t.accepted.equals(true) &
|
||||
// t.blocked.equals(false) &
|
||||
// t.deleted.equals(false),
|
||||
// )
|
||||
// ..orderBy([(t) => OrderingTerm.desc(t.lastMessageExchange)]))
|
||||
// .watch();
|
||||
// }
|
||||
|
||||
// Stream<List<Contact>> watchContactsForStartNewChat() {
|
||||
// return (select(contacts)
|
||||
// ..where((t) => t.accepted.equals(true) & t.blocked.equals(false))
|
||||
// ..orderBy([(t) => OrderingTerm.desc(t.lastMessageExchange)]))
|
||||
// .watch();
|
||||
// }
|
||||
|
||||
// Stream<List<Contact>> watchContactsForChatList() {
|
||||
// return (select(contacts)
|
||||
// ..where(
|
||||
// (t) =>
|
||||
// t.accepted.equals(true) &
|
||||
// t.blocked.equals(false) &
|
||||
// t.archived.equals(false),
|
||||
// )
|
||||
// ..orderBy([(t) => OrderingTerm.desc(t.lastMessageExchange)]))
|
||||
// .watch();
|
||||
// }
|
||||
|
||||
Future<List<Contact>> getAllNotBlockedContacts() {
|
||||
return (select(contacts)..where((t) => t.blocked.equals(false))).get();
|
||||
}
|
||||
|
|
@ -188,31 +84,15 @@ class ContactsDao extends DatabaseAccessor<TwonlyDB> with _$ContactsDaoMixin {
|
|||
return query.map((row) => row.read(count)).watchSingle();
|
||||
}
|
||||
|
||||
Stream<List<Contact>> watchAllAcceptedContacts() {
|
||||
return (select(contacts)
|
||||
..where((t) => t.blocked.equals(false) & t.accepted.equals(true)))
|
||||
.watch();
|
||||
}
|
||||
|
||||
Stream<List<Contact>> watchAllContacts() {
|
||||
return select(contacts).watch();
|
||||
}
|
||||
|
||||
Future<void> modifyFlameCounterForTesting() async {
|
||||
await update(contacts).write(
|
||||
ContactsCompanion(
|
||||
lastFlameCounterChange: Value(DateTime.now()),
|
||||
flameCounter: const Value(1337),
|
||||
lastFlameSync: const Value(null),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Stream<int> watchFlameCounter(int userId) {
|
||||
return (select(contacts)
|
||||
..where(
|
||||
(u) =>
|
||||
u.userId.equals(userId) &
|
||||
u.lastMessageReceived.isNotNull() &
|
||||
u.lastMessageSend.isNotNull(),
|
||||
))
|
||||
.watchSingle()
|
||||
.asyncMap(getFlameCounterFromContact);
|
||||
}
|
||||
}
|
||||
|
||||
String getContactDisplayName(Contact user) {
|
||||
|
|
@ -234,18 +114,3 @@ String getContactDisplayName(Contact user) {
|
|||
String applyStrikethrough(String text) {
|
||||
return text.split('').map((char) => '$char\u0336').join();
|
||||
}
|
||||
|
||||
int getFlameCounterFromContact(Contact contact) {
|
||||
if (contact.lastMessageSend == null || contact.lastMessageReceived == null) {
|
||||
return 0;
|
||||
}
|
||||
final now = DateTime.now();
|
||||
final startOfToday = DateTime(now.year, now.month, now.day);
|
||||
final twoDaysAgo = startOfToday.subtract(const Duration(days: 2));
|
||||
if (contact.lastMessageSend!.isAfter(twoDaysAgo) &&
|
||||
contact.lastMessageReceived!.isAfter(twoDaysAgo)) {
|
||||
return contact.flameCounter + 1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,7 +32,56 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
|
|||
.get();
|
||||
}
|
||||
|
||||
Future<List<Group>> getDirectChat(int userId) async {
|
||||
Future<void> insertGroup(GroupsCompanion group) async {
|
||||
await into(groups).insert(group);
|
||||
}
|
||||
|
||||
Future<List<Contact>> getGroupContact(String groupId) async {
|
||||
final query = select(contacts).join([
|
||||
leftOuterJoin(
|
||||
groupMembers,
|
||||
groupMembers.contactId.equalsExp(contacts.userId) &
|
||||
groupMembers.groupId.equals(groupId),
|
||||
),
|
||||
]);
|
||||
return query.map((row) => row.readTable(contacts)).get();
|
||||
}
|
||||
|
||||
Stream<List<Group>> watchGroups() {
|
||||
return select(groups).watch();
|
||||
}
|
||||
|
||||
Stream<Group?> watchGroup(String groupId) {
|
||||
return (select(groups)..where((t) => t.groupId.equals(groupId)))
|
||||
.watchSingleOrNull();
|
||||
}
|
||||
|
||||
Stream<List<Group>> watchGroupsForChatList() {
|
||||
return (select(groups)..where((t) => t.archived.equals(false))).watch();
|
||||
}
|
||||
|
||||
Future<Group?> getGroup(String groupId) {
|
||||
return (select(groups)..where((t) => t.groupId.equals(groupId)))
|
||||
.getSingleOrNull();
|
||||
}
|
||||
|
||||
Stream<int> watchFlameCounter(String groupId) {
|
||||
return (select(groups)
|
||||
..where(
|
||||
(u) =>
|
||||
u.groupId.equals(groupId) &
|
||||
u.lastMessageReceived.isNotNull() &
|
||||
u.lastMessageSend.isNotNull(),
|
||||
))
|
||||
.watchSingle()
|
||||
.asyncMap(getFlameCounterFromGroup);
|
||||
}
|
||||
|
||||
Future<List<Group>> getAllDirectChats() {
|
||||
return (select(groups)..where((t) => t.isDirectChat.equals(true))).get();
|
||||
}
|
||||
|
||||
Future<Group?> getDirectChat(int userId) async {
|
||||
final query = (select(groups).join([
|
||||
leftOuterJoin(
|
||||
groupMembers,
|
||||
|
|
@ -40,8 +89,94 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
|
|||
groupMembers.contactId.equals(userId),
|
||||
),
|
||||
])
|
||||
..where(groups.isGroupOfTwo.equals(true)));
|
||||
..where(groups.isDirectChat.equals(true)));
|
||||
|
||||
return query.map((row) => row.readTable(groups)).get();
|
||||
return query.map((row) => row.readTable(groups)).getSingleOrNull();
|
||||
}
|
||||
|
||||
Future<void> incFlameCounter(
|
||||
String groupId,
|
||||
bool received,
|
||||
DateTime timestamp,
|
||||
) async {
|
||||
final group = await (select(groups)
|
||||
..where((t) => t.groupId.equals(groupId)))
|
||||
.getSingle();
|
||||
|
||||
final totalMediaCounter = group.totalMediaCounter + 1;
|
||||
var flameCounter = group.flameCounter;
|
||||
|
||||
if (group.lastMessageReceived != null && group.lastMessageSend != null) {
|
||||
final now = DateTime.now();
|
||||
final startOfToday = DateTime(now.year, now.month, now.day);
|
||||
final twoDaysAgo = startOfToday.subtract(const Duration(days: 2));
|
||||
if (group.lastMessageSend!.isBefore(twoDaysAgo) ||
|
||||
group.lastMessageReceived!.isBefore(twoDaysAgo)) {
|
||||
flameCounter = 0;
|
||||
}
|
||||
}
|
||||
|
||||
var lastMessageSend = const Value<DateTime?>.absent();
|
||||
var lastMessageReceived = const Value<DateTime?>.absent();
|
||||
var lastFlameCounterChange = const Value<DateTime?>.absent();
|
||||
|
||||
if (group.lastFlameCounterChange != null) {
|
||||
final now = DateTime.now();
|
||||
final startOfToday = DateTime(now.year, now.month, now.day);
|
||||
|
||||
if (group.lastFlameCounterChange!.isBefore(startOfToday)) {
|
||||
// last flame update was yesterday. check if it can be updated.
|
||||
var updateFlame = false;
|
||||
if (received) {
|
||||
if (group.lastMessageSend != null &&
|
||||
group.lastMessageSend!.isAfter(startOfToday)) {
|
||||
// today a message was already send -> update flame
|
||||
updateFlame = true;
|
||||
}
|
||||
} else if (group.lastMessageReceived != null &&
|
||||
group.lastMessageReceived!.isAfter(startOfToday)) {
|
||||
// today a message was already received -> update flame
|
||||
updateFlame = true;
|
||||
}
|
||||
if (updateFlame) {
|
||||
flameCounter += 1;
|
||||
lastFlameCounterChange = Value(timestamp);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// There where no message until no...
|
||||
lastFlameCounterChange = Value(timestamp);
|
||||
}
|
||||
|
||||
if (received) {
|
||||
lastMessageReceived = Value(timestamp);
|
||||
} else {
|
||||
lastMessageSend = Value(timestamp);
|
||||
}
|
||||
|
||||
await (update(groups)..where((t) => t.groupId.equals(groupId))).write(
|
||||
GroupsCompanion(
|
||||
totalMediaCounter: Value(totalMediaCounter),
|
||||
lastFlameCounterChange: lastFlameCounterChange,
|
||||
lastMessageReceived: lastMessageReceived,
|
||||
lastMessageSend: lastMessageSend,
|
||||
flameCounter: Value(flameCounter),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
int getFlameCounterFromGroup(Group group) {
|
||||
if (group.lastMessageSend == null || group.lastMessageReceived == null) {
|
||||
return 0;
|
||||
}
|
||||
final now = DateTime.now();
|
||||
final startOfToday = DateTime(now.year, now.month, now.day);
|
||||
final twoDaysAgo = startOfToday.subtract(const Duration(days: 2));
|
||||
if (group.lastMessageSend!.isAfter(twoDaysAgo) &&
|
||||
group.lastMessageReceived!.isAfter(twoDaysAgo)) {
|
||||
return group.flameCounter + 1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,6 +46,11 @@ class MediaFilesDao extends DatabaseAccessor<TwonlyDB>
|
|||
.getSingleOrNull();
|
||||
}
|
||||
|
||||
Stream<MediaFile?> watchMedia(String mediaId) {
|
||||
return (select(mediaFiles)..where((t) => t.mediaId.equals(mediaId)))
|
||||
.watchSingleOrNull();
|
||||
}
|
||||
|
||||
Future<void> resetPendingDownloadState() async {
|
||||
await (update(mediaFiles)
|
||||
..where(
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import 'package:twonly/src/database/tables/contacts.table.dart';
|
|||
import 'package:twonly/src/database/tables/groups.table.dart';
|
||||
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
||||
import 'package:twonly/src/database/tables/messages.table.dart';
|
||||
import 'package:twonly/src/database/tables/reactions.table.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
|
|
@ -15,7 +16,9 @@ part 'messages.dao.g.dart';
|
|||
Messages,
|
||||
Contacts,
|
||||
MediaFiles,
|
||||
Reactions,
|
||||
MessageHistories,
|
||||
GroupMembers,
|
||||
MessageActions,
|
||||
Groups,
|
||||
],
|
||||
|
|
@ -26,55 +29,39 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
|
|||
// ignore: matching_super_parameters
|
||||
MessagesDao(super.db);
|
||||
|
||||
// Stream<List<Message>> watchMessageNotOpened(int contactId) {
|
||||
// return (select(messages)
|
||||
// ..where(
|
||||
// (t) =>
|
||||
// t.openedAt.isNull() &
|
||||
// t.contactId.equals(contactId) &
|
||||
// t.errorWhileSending.equals(false),
|
||||
// )
|
||||
// ..orderBy([(t) => OrderingTerm.desc(t.sendAt)]))
|
||||
// .watch();
|
||||
// }
|
||||
Stream<List<Message>> watchMessageNotOpened(String groupId) {
|
||||
return (select(messages)
|
||||
..where((t) => t.openedAt.isNull() & t.groupId.equals(groupId))
|
||||
..orderBy([(t) => OrderingTerm.desc(t.createdAt)]))
|
||||
.watch();
|
||||
}
|
||||
|
||||
// Stream<List<Message>> watchMediaMessageNotOpened(int contactId) {
|
||||
// return (select(messages)
|
||||
// ..where(
|
||||
// (t) =>
|
||||
// t.openedAt.isNull() &
|
||||
// t.contactId.equals(contactId) &
|
||||
// t.errorWhileSending.equals(false) &
|
||||
// t.messageOtherId.isNotNull() &
|
||||
// t.kind.equals(MessageKind.media.name),
|
||||
// )
|
||||
// ..orderBy([(t) => OrderingTerm.asc(t.sendAt)]))
|
||||
// .watch();
|
||||
// }
|
||||
Stream<List<Message>> watchMediaNotOpened(String groupId) {
|
||||
return (select(messages)
|
||||
..where(
|
||||
(t) =>
|
||||
t.openedAt.isNull() &
|
||||
t.groupId.equals(groupId) &
|
||||
t.senderId.isNotNull() &
|
||||
t.type.equals(MessageType.media.name),
|
||||
)
|
||||
..orderBy([(t) => OrderingTerm.asc(t.createdAt)]))
|
||||
.watch();
|
||||
}
|
||||
|
||||
// Stream<List<Message>> watchLastMessage(int contactId) {
|
||||
// return (select(messages)
|
||||
// ..where((t) => t.contactId.equals(contactId))
|
||||
// ..orderBy([(t) => OrderingTerm.desc(t.sendAt)])
|
||||
// ..limit(1))
|
||||
// .watch();
|
||||
// }
|
||||
Stream<List<Message>> watchLastMessage(String groupId) {
|
||||
return (select(messages)
|
||||
..where((t) => t.groupId.equals(groupId))
|
||||
..orderBy([(t) => OrderingTerm.desc(t.createdAt)])
|
||||
..limit(1))
|
||||
.watch();
|
||||
}
|
||||
|
||||
// Stream<List<Message>> watchAllMessagesFrom(int contactId) {
|
||||
// return (select(messages)
|
||||
// ..where(
|
||||
// (t) =>
|
||||
// t.contactId.equals(contactId) &
|
||||
// t.contentJson.isNotNull() &
|
||||
// (t.openedAt.isNull() |
|
||||
// t.mediaStored.equals(true) |
|
||||
// t.openedAt.isBiggerThanValue(
|
||||
// DateTime.now().subtract(const Duration(days: 1)),
|
||||
// )),
|
||||
// )
|
||||
// ..orderBy([(t) => OrderingTerm.asc(t.sendAt)]))
|
||||
// .watch();
|
||||
// }
|
||||
Stream<List<Message>> watchByGroupId(String groupId) {
|
||||
return ((select(messages)..where((t) => t.groupId.equals(groupId)))
|
||||
..orderBy([(t) => OrderingTerm.asc(t.createdAt)]))
|
||||
.watch();
|
||||
}
|
||||
|
||||
// Future<void> removeOldMessages() {
|
||||
// return (update(messages)
|
||||
|
|
@ -92,22 +79,6 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
|
|||
// .write(const MessagesCompanion(contentJson: Value(null)));
|
||||
// }
|
||||
|
||||
// Future<void> handleMediaFilesOlderThan30Days() {
|
||||
// /// media files will be deleted by the server after 30 days, so delete them here also
|
||||
// return (update(messages)
|
||||
// ..where(
|
||||
// (t) => (t.kind.equals(MessageKind.media.name) &
|
||||
// t.openedAt.isNull() &
|
||||
// t.messageOtherId.isNull() &
|
||||
// (t.sendAt.isSmallerThanValue(
|
||||
// DateTime.now().subtract(
|
||||
// const Duration(days: 30),
|
||||
// ),
|
||||
// ))),
|
||||
// ))
|
||||
// .write(const MessagesCompanion(errorWhileSending: Value(true)));
|
||||
// }
|
||||
|
||||
// Future<List<Message>> getAllMessagesPendingDownloading() {
|
||||
// return (select(messages)
|
||||
// ..where(
|
||||
|
|
@ -155,18 +126,18 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
|
|||
// .get();
|
||||
// }
|
||||
|
||||
// Future<void> openedAllNonMediaMessages(int contactId) {
|
||||
// final updates = MessagesCompanion(openedAt: Value(DateTime.now()));
|
||||
// return (update(messages)
|
||||
// ..where(
|
||||
// (t) =>
|
||||
// t.contactId.equals(contactId) &
|
||||
// t.messageOtherId.isNotNull() &
|
||||
// t.openedAt.isNull() &
|
||||
// t.kind.equals(MessageKind.media.name).not(),
|
||||
// ))
|
||||
// .write(updates);
|
||||
// }
|
||||
Future<void> openedAllTextMessages(String groupId) {
|
||||
final updates = MessagesCompanion(openedAt: Value(DateTime.now()));
|
||||
return (update(messages)
|
||||
..where(
|
||||
(t) =>
|
||||
t.groupId.equals(groupId) &
|
||||
t.senderId.isNotNull() &
|
||||
t.openedAt.isNull() &
|
||||
t.type.equals(MessageType.text.name),
|
||||
))
|
||||
.write(updates);
|
||||
}
|
||||
|
||||
Future<void> handleMessageDeletion(
|
||||
int contactId,
|
||||
|
|
@ -259,6 +230,22 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
|
|||
);
|
||||
}
|
||||
|
||||
Future<bool> haveAllMembers(
|
||||
String groupId,
|
||||
String messageId,
|
||||
MessageActionType action,
|
||||
) async {
|
||||
final members = await twonlyDB.groupsDao.getGroupMembers(groupId);
|
||||
|
||||
final actions = await (select(messageActions)
|
||||
..where(
|
||||
(t) => t.type.equals(action.name) & t.messageId.equals(messageId),
|
||||
))
|
||||
.get();
|
||||
|
||||
return members.length == actions.length;
|
||||
}
|
||||
|
||||
// Future<void> updateMessageByOtherUser(
|
||||
// int userId,
|
||||
// int messageId,
|
||||
|
|
@ -321,6 +308,29 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
|
|||
}
|
||||
}
|
||||
|
||||
Future<MessageAction?> getLastMessageAction(String messageId) async {
|
||||
return (((select(messageActions)
|
||||
..where(
|
||||
(t) => t.messageId.equals(messageId),
|
||||
))
|
||||
..orderBy([(t) => OrderingTerm.desc(t.actionAt)]))
|
||||
..limit(1))
|
||||
.getSingleOrNull();
|
||||
}
|
||||
|
||||
Future<void> reopenedMedia(String messageId) async {
|
||||
await (delete(messageActions)
|
||||
..where(
|
||||
(t) =>
|
||||
t.messageId.equals(messageId) &
|
||||
t.contactId.isNull() &
|
||||
t.type.equals(
|
||||
MessageActionType.openedAt.name,
|
||||
),
|
||||
))
|
||||
.go();
|
||||
}
|
||||
|
||||
// Future<void> deleteMessagesByContactId(int contactId) {
|
||||
// return (delete(messages)
|
||||
// ..where(
|
||||
|
|
@ -342,9 +352,9 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
|
|||
// .go();
|
||||
// }
|
||||
|
||||
// Future<void> deleteMessagesByMessageId(int messageId) {
|
||||
// return (delete(messages)..where((t) => t.messageId.equals(messageId))).go();
|
||||
// }
|
||||
Future<void> deleteMessagesById(String messageId) {
|
||||
return (delete(messages)..where((t) => t.messageId.equals(messageId))).go();
|
||||
}
|
||||
|
||||
// Future<void> deleteAllMessagesByContactId(int contactId) {
|
||||
// return (delete(messages)..where((t) => t.contactId.equals(contactId))).go();
|
||||
|
|
|
|||
|
|
@ -8,7 +8,9 @@ mixin _$MessagesDaoMixin on DatabaseAccessor<TwonlyDB> {
|
|||
$ContactsTable get contacts => attachedDatabase.contacts;
|
||||
$MediaFilesTable get mediaFiles => attachedDatabase.mediaFiles;
|
||||
$MessagesTable get messages => attachedDatabase.messages;
|
||||
$ReactionsTable get reactions => attachedDatabase.reactions;
|
||||
$MessageHistoriesTable get messageHistories =>
|
||||
attachedDatabase.messageHistories;
|
||||
$GroupMembersTable get groupMembers => attachedDatabase.groupMembers;
|
||||
$MessageActionsTable get messageActions => attachedDatabase.messageActions;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,4 +43,15 @@ class ReactionsDao extends DatabaseAccessor<TwonlyDB> with _$ReactionsDaoMixin {
|
|||
Log.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
Stream<List<Reaction>> watchReactions(String messageId) {
|
||||
return (select(reactions)
|
||||
..where((t) => t.messageId.equals(messageId))
|
||||
..orderBy([(t) => OrderingTerm.desc(t.createdAt)]))
|
||||
.watch();
|
||||
}
|
||||
|
||||
Future<void> insertReaction(ReactionsCompanion reaction) async {
|
||||
await into(reactions).insert(reaction);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,28 +13,12 @@ class Contacts extends Table {
|
|||
|
||||
BoolColumn get accepted => boolean().withDefault(const Constant(false))();
|
||||
BoolColumn get requested => boolean().withDefault(const Constant(false))();
|
||||
BoolColumn get hidden => boolean().withDefault(const Constant(false))();
|
||||
BoolColumn get blocked => boolean().withDefault(const Constant(false))();
|
||||
BoolColumn get verified => boolean().withDefault(const Constant(false))();
|
||||
BoolColumn get deleted => boolean().withDefault(const Constant(false))();
|
||||
|
||||
BoolColumn get alsoBestFriend =>
|
||||
boolean().withDefault(const Constant(false))();
|
||||
|
||||
IntColumn get deleteMessagesAfterXMinutes =>
|
||||
integer().withDefault(const Constant(60 * 24))();
|
||||
|
||||
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
|
||||
|
||||
IntColumn get totalMediaCounter => integer().withDefault(const Constant(0))();
|
||||
|
||||
DateTimeColumn get lastMessageSend => dateTime().nullable()();
|
||||
DateTimeColumn get lastMessageReceived => dateTime().nullable()();
|
||||
DateTimeColumn get lastFlameCounterChange => dateTime().nullable()();
|
||||
DateTimeColumn get lastFlameSync => dateTime().nullable()();
|
||||
|
||||
IntColumn get flameCounter => integer().withDefault(const Constant(0))();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {userId};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,15 +7,31 @@ class Groups extends Table {
|
|||
TextColumn get groupId => text().clientDefault(() => uuid.v4())();
|
||||
|
||||
BoolColumn get isGroupAdmin => boolean()();
|
||||
BoolColumn get isGroupOfTwo => boolean()();
|
||||
BoolColumn get isDirectChat => boolean()();
|
||||
BoolColumn get pinned => boolean().withDefault(const Constant(false))();
|
||||
BoolColumn get archived => boolean().withDefault(const Constant(false))();
|
||||
|
||||
TextColumn get groupName => text()();
|
||||
|
||||
IntColumn get totalMediaCounter => integer().withDefault(const Constant(0))();
|
||||
|
||||
BoolColumn get alsoBestFriend =>
|
||||
boolean().withDefault(const Constant(false))();
|
||||
|
||||
IntColumn get deleteMessagesAfterMilliseconds =>
|
||||
integer().withDefault(const Constant(1000 * 60 * 24))();
|
||||
|
||||
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
|
||||
|
||||
DateTimeColumn get lastMessageSend => dateTime().nullable()();
|
||||
DateTimeColumn get lastMessageReceived => dateTime().nullable()();
|
||||
DateTimeColumn get lastFlameCounterChange => dateTime().nullable()();
|
||||
DateTimeColumn get lastFlameSync => dateTime().nullable()();
|
||||
|
||||
IntColumn get flameCounter => integer().withDefault(const Constant(0))();
|
||||
|
||||
DateTimeColumn get lastMessageExchange =>
|
||||
dateTime().withDefault(currentDateAndTime)();
|
||||
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {groupId};
|
||||
|
|
|
|||
|
|
@ -33,9 +33,9 @@ class Messages extends Table {
|
|||
BoolColumn get isDeletedFromSender =>
|
||||
boolean().withDefault(const Constant(false))();
|
||||
|
||||
BoolColumn get isEdited => boolean().withDefault(const Constant(false))();
|
||||
|
||||
DateTimeColumn get openedAt => dateTime().nullable()();
|
||||
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
|
||||
DateTimeColumn get modifiedAt => dateTime().nullable()();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {messageId};
|
||||
|
|
@ -43,15 +43,12 @@ class Messages extends Table {
|
|||
|
||||
enum MessageActionType {
|
||||
openedAt,
|
||||
modifiedAt,
|
||||
ackByUserAt,
|
||||
ackByServerAt,
|
||||
}
|
||||
|
||||
@DataClassName('MessageAction')
|
||||
class MessageActions extends Table {
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
|
||||
TextColumn get messageId =>
|
||||
text().references(Messages, #messageId, onDelete: KeyAction.cascade)();
|
||||
|
||||
|
|
@ -59,10 +56,11 @@ class MessageActions extends Table {
|
|||
integer().references(Contacts, #contactId, onDelete: KeyAction.cascade)();
|
||||
|
||||
TextColumn get type => textEnum<MessageActionType>()();
|
||||
|
||||
DateTimeColumn get actionAt => dateTime().withDefault(currentDateAndTime)();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {id};
|
||||
Set<Column> get primaryKey => {messageId, contactId, type};
|
||||
}
|
||||
|
||||
@DataClassName('MessageHistory')
|
||||
|
|
@ -72,8 +70,9 @@ class MessageHistories extends Table {
|
|||
TextColumn get messageId =>
|
||||
text().references(Messages, #messageId, onDelete: KeyAction.cascade)();
|
||||
|
||||
IntColumn get contactId =>
|
||||
integer().references(Contacts, #contactId, onDelete: KeyAction.cascade)();
|
||||
IntColumn get contactId => integer()
|
||||
.nullable()
|
||||
.references(Contacts, #contactId, onDelete: KeyAction.cascade)();
|
||||
|
||||
TextColumn get content => text().nullable()();
|
||||
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ part 'twonly.db.g.dart';
|
|||
SignalSessionStores,
|
||||
SignalContactPreKeys,
|
||||
SignalContactSignedPreKeys,
|
||||
MessageActions
|
||||
MessageActions,
|
||||
],
|
||||
daos: [
|
||||
MessagesDao,
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -72,7 +72,7 @@ class UserData {
|
|||
|
||||
List<String>? tutorialDisplayed;
|
||||
|
||||
int? myBestFriendContactId;
|
||||
String? myBestFriendGroupId;
|
||||
|
||||
DateTime? signalLastSignedPreKeyUpdated;
|
||||
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) => UserData(
|
|||
..tutorialDisplayed = (json['tutorialDisplayed'] as List<dynamic>?)
|
||||
?.map((e) => e as String)
|
||||
.toList()
|
||||
..myBestFriendContactId = (json['myBestFriendContactId'] as num?)?.toInt()
|
||||
..myBestFriendGroupId = json['myBestFriendGroupId'] as String?
|
||||
..signalLastSignedPreKeyUpdated =
|
||||
json['signalLastSignedPreKeyUpdated'] == null
|
||||
? null
|
||||
|
|
@ -97,7 +97,7 @@ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
|
|||
'lastPlanBallance': instance.lastPlanBallance,
|
||||
'additionalUserInvites': instance.additionalUserInvites,
|
||||
'tutorialDisplayed': instance.tutorialDisplayed,
|
||||
'myBestFriendContactId': instance.myBestFriendContactId,
|
||||
'myBestFriendGroupId': instance.myBestFriendGroupId,
|
||||
'signalLastSignedPreKeyUpdated':
|
||||
instance.signalLastSignedPreKeyUpdated?.toIso8601String(),
|
||||
'currentPreKeyIndexStart': instance.currentPreKeyIndexStart,
|
||||
|
|
|
|||
|
|
@ -663,14 +663,14 @@ class EncryptedContent_Media extends $pb.GeneratedMessage {
|
|||
class EncryptedContent_MediaUpdate extends $pb.GeneratedMessage {
|
||||
factory EncryptedContent_MediaUpdate({
|
||||
EncryptedContent_MediaUpdate_Type? type,
|
||||
$core.String? targetMessageId,
|
||||
$core.String? targetMediaId,
|
||||
}) {
|
||||
final $result = create();
|
||||
if (type != null) {
|
||||
$result.type = type;
|
||||
}
|
||||
if (targetMessageId != null) {
|
||||
$result.targetMessageId = targetMessageId;
|
||||
if (targetMediaId != null) {
|
||||
$result.targetMediaId = targetMediaId;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
|
@ -680,7 +680,7 @@ class EncryptedContent_MediaUpdate extends $pb.GeneratedMessage {
|
|||
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'EncryptedContent.MediaUpdate', createEmptyInstance: create)
|
||||
..e<EncryptedContent_MediaUpdate_Type>(1, _omitFieldNames ? '' : 'type', $pb.PbFieldType.OE, defaultOrMaker: EncryptedContent_MediaUpdate_Type.REOPENED, valueOf: EncryptedContent_MediaUpdate_Type.valueOf, enumValues: EncryptedContent_MediaUpdate_Type.values)
|
||||
..aOS(2, _omitFieldNames ? '' : 'targetMessageId', protoName: 'targetMessageId')
|
||||
..aOS(2, _omitFieldNames ? '' : 'targetMediaId', protoName: 'targetMediaId')
|
||||
..hasRequiredFields = false
|
||||
;
|
||||
|
||||
|
|
@ -715,13 +715,13 @@ class EncryptedContent_MediaUpdate extends $pb.GeneratedMessage {
|
|||
void clearType() => clearField(1);
|
||||
|
||||
@$pb.TagNumber(2)
|
||||
$core.String get targetMessageId => $_getSZ(1);
|
||||
$core.String get targetMediaId => $_getSZ(1);
|
||||
@$pb.TagNumber(2)
|
||||
set targetMessageId($core.String v) { $_setString(1, v); }
|
||||
set targetMediaId($core.String v) { $_setString(1, v); }
|
||||
@$pb.TagNumber(2)
|
||||
$core.bool hasTargetMessageId() => $_has(1);
|
||||
$core.bool hasTargetMediaId() => $_has(1);
|
||||
@$pb.TagNumber(2)
|
||||
void clearTargetMessageId() => clearField(2);
|
||||
void clearTargetMediaId() => clearField(2);
|
||||
}
|
||||
|
||||
class EncryptedContent_ContactRequest extends $pb.GeneratedMessage {
|
||||
|
|
@ -1025,8 +1025,8 @@ class EncryptedContent_FlameSync extends $pb.GeneratedMessage {
|
|||
class EncryptedContent extends $pb.GeneratedMessage {
|
||||
factory EncryptedContent({
|
||||
$core.String? groupId,
|
||||
$core.bool? isDirectChat,
|
||||
$fixnum.Int64? senderProfileCounter,
|
||||
EncryptedContent_TextMessage? textMessage,
|
||||
EncryptedContent_MessageUpdate? messageUpdate,
|
||||
EncryptedContent_Media? media,
|
||||
EncryptedContent_MediaUpdate? mediaUpdate,
|
||||
|
|
@ -1035,17 +1035,18 @@ class EncryptedContent extends $pb.GeneratedMessage {
|
|||
EncryptedContent_FlameSync? flameSync,
|
||||
EncryptedContent_PushKeys? pushKeys,
|
||||
EncryptedContent_Reaction? reaction,
|
||||
EncryptedContent_TextMessage? textMessage,
|
||||
}) {
|
||||
final $result = create();
|
||||
if (groupId != null) {
|
||||
$result.groupId = groupId;
|
||||
}
|
||||
if (isDirectChat != null) {
|
||||
$result.isDirectChat = isDirectChat;
|
||||
}
|
||||
if (senderProfileCounter != null) {
|
||||
$result.senderProfileCounter = senderProfileCounter;
|
||||
}
|
||||
if (textMessage != null) {
|
||||
$result.textMessage = textMessage;
|
||||
}
|
||||
if (messageUpdate != null) {
|
||||
$result.messageUpdate = messageUpdate;
|
||||
}
|
||||
|
|
@ -1070,6 +1071,9 @@ class EncryptedContent extends $pb.GeneratedMessage {
|
|||
if (reaction != null) {
|
||||
$result.reaction = reaction;
|
||||
}
|
||||
if (textMessage != null) {
|
||||
$result.textMessage = textMessage;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
EncryptedContent._() : super();
|
||||
|
|
@ -1078,8 +1082,8 @@ class EncryptedContent extends $pb.GeneratedMessage {
|
|||
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'EncryptedContent', createEmptyInstance: create)
|
||||
..aOS(2, _omitFieldNames ? '' : 'groupId', protoName: 'groupId')
|
||||
..aInt64(3, _omitFieldNames ? '' : 'senderProfileCounter', protoName: 'senderProfileCounter')
|
||||
..aOM<EncryptedContent_TextMessage>(4, _omitFieldNames ? '' : 'textMessage', protoName: 'textMessage', subBuilder: EncryptedContent_TextMessage.create)
|
||||
..aOB(3, _omitFieldNames ? '' : 'isDirectChat', protoName: 'isDirectChat')
|
||||
..aInt64(4, _omitFieldNames ? '' : 'senderProfileCounter', protoName: 'senderProfileCounter')
|
||||
..aOM<EncryptedContent_MessageUpdate>(5, _omitFieldNames ? '' : 'messageUpdate', protoName: 'messageUpdate', subBuilder: EncryptedContent_MessageUpdate.create)
|
||||
..aOM<EncryptedContent_Media>(6, _omitFieldNames ? '' : 'media', subBuilder: EncryptedContent_Media.create)
|
||||
..aOM<EncryptedContent_MediaUpdate>(7, _omitFieldNames ? '' : 'mediaUpdate', protoName: 'mediaUpdate', subBuilder: EncryptedContent_MediaUpdate.create)
|
||||
|
|
@ -1088,6 +1092,7 @@ class EncryptedContent extends $pb.GeneratedMessage {
|
|||
..aOM<EncryptedContent_FlameSync>(10, _omitFieldNames ? '' : 'flameSync', protoName: 'flameSync', subBuilder: EncryptedContent_FlameSync.create)
|
||||
..aOM<EncryptedContent_PushKeys>(11, _omitFieldNames ? '' : 'pushKeys', protoName: 'pushKeys', subBuilder: EncryptedContent_PushKeys.create)
|
||||
..aOM<EncryptedContent_Reaction>(12, _omitFieldNames ? '' : 'reaction', subBuilder: EncryptedContent_Reaction.create)
|
||||
..aOM<EncryptedContent_TextMessage>(13, _omitFieldNames ? '' : 'textMessage', protoName: 'textMessage', subBuilder: EncryptedContent_TextMessage.create)
|
||||
..hasRequiredFields = false
|
||||
;
|
||||
|
||||
|
|
@ -1121,26 +1126,24 @@ class EncryptedContent extends $pb.GeneratedMessage {
|
|||
@$pb.TagNumber(2)
|
||||
void clearGroupId() => clearField(2);
|
||||
|
||||
/// / This can be added, so the receiver can check weather he is up to date with the current profile
|
||||
@$pb.TagNumber(3)
|
||||
$fixnum.Int64 get senderProfileCounter => $_getI64(1);
|
||||
$core.bool get isDirectChat => $_getBF(1);
|
||||
@$pb.TagNumber(3)
|
||||
set senderProfileCounter($fixnum.Int64 v) { $_setInt64(1, v); }
|
||||
set isDirectChat($core.bool v) { $_setBool(1, v); }
|
||||
@$pb.TagNumber(3)
|
||||
$core.bool hasSenderProfileCounter() => $_has(1);
|
||||
$core.bool hasIsDirectChat() => $_has(1);
|
||||
@$pb.TagNumber(3)
|
||||
void clearSenderProfileCounter() => clearField(3);
|
||||
void clearIsDirectChat() => clearField(3);
|
||||
|
||||
/// / This can be added, so the receiver can check weather he is up to date with the current profile
|
||||
@$pb.TagNumber(4)
|
||||
EncryptedContent_TextMessage get textMessage => $_getN(2);
|
||||
$fixnum.Int64 get senderProfileCounter => $_getI64(2);
|
||||
@$pb.TagNumber(4)
|
||||
set textMessage(EncryptedContent_TextMessage v) { setField(4, v); }
|
||||
set senderProfileCounter($fixnum.Int64 v) { $_setInt64(2, v); }
|
||||
@$pb.TagNumber(4)
|
||||
$core.bool hasTextMessage() => $_has(2);
|
||||
$core.bool hasSenderProfileCounter() => $_has(2);
|
||||
@$pb.TagNumber(4)
|
||||
void clearTextMessage() => clearField(4);
|
||||
@$pb.TagNumber(4)
|
||||
EncryptedContent_TextMessage ensureTextMessage() => $_ensure(2);
|
||||
void clearSenderProfileCounter() => clearField(4);
|
||||
|
||||
@$pb.TagNumber(5)
|
||||
EncryptedContent_MessageUpdate get messageUpdate => $_getN(3);
|
||||
|
|
@ -1229,6 +1232,17 @@ class EncryptedContent extends $pb.GeneratedMessage {
|
|||
void clearReaction() => clearField(12);
|
||||
@$pb.TagNumber(12)
|
||||
EncryptedContent_Reaction ensureReaction() => $_ensure(10);
|
||||
|
||||
@$pb.TagNumber(13)
|
||||
EncryptedContent_TextMessage get textMessage => $_getN(11);
|
||||
@$pb.TagNumber(13)
|
||||
set textMessage(EncryptedContent_TextMessage v) { setField(13, v); }
|
||||
@$pb.TagNumber(13)
|
||||
$core.bool hasTextMessage() => $_has(11);
|
||||
@$pb.TagNumber(13)
|
||||
void clearTextMessage() => clearField(13);
|
||||
@$pb.TagNumber(13)
|
||||
EncryptedContent_TextMessage ensureTextMessage() => $_ensure(11);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -95,8 +95,8 @@ const EncryptedContent$json = {
|
|||
'1': 'EncryptedContent',
|
||||
'2': [
|
||||
{'1': 'groupId', '3': 2, '4': 1, '5': 9, '9': 0, '10': 'groupId', '17': true},
|
||||
{'1': 'senderProfileCounter', '3': 3, '4': 1, '5': 3, '9': 1, '10': 'senderProfileCounter', '17': true},
|
||||
{'1': 'textMessage', '3': 4, '4': 1, '5': 11, '6': '.EncryptedContent.TextMessage', '9': 2, '10': 'textMessage', '17': true},
|
||||
{'1': 'isDirectChat', '3': 3, '4': 1, '5': 8, '9': 1, '10': 'isDirectChat', '17': true},
|
||||
{'1': 'senderProfileCounter', '3': 4, '4': 1, '5': 3, '9': 2, '10': 'senderProfileCounter', '17': true},
|
||||
{'1': 'messageUpdate', '3': 5, '4': 1, '5': 11, '6': '.EncryptedContent.MessageUpdate', '9': 3, '10': 'messageUpdate', '17': true},
|
||||
{'1': 'media', '3': 6, '4': 1, '5': 11, '6': '.EncryptedContent.Media', '9': 4, '10': 'media', '17': true},
|
||||
{'1': 'mediaUpdate', '3': 7, '4': 1, '5': 11, '6': '.EncryptedContent.MediaUpdate', '9': 5, '10': 'mediaUpdate', '17': true},
|
||||
|
|
@ -105,12 +105,13 @@ const EncryptedContent$json = {
|
|||
{'1': 'flameSync', '3': 10, '4': 1, '5': 11, '6': '.EncryptedContent.FlameSync', '9': 8, '10': 'flameSync', '17': true},
|
||||
{'1': 'pushKeys', '3': 11, '4': 1, '5': 11, '6': '.EncryptedContent.PushKeys', '9': 9, '10': 'pushKeys', '17': true},
|
||||
{'1': 'reaction', '3': 12, '4': 1, '5': 11, '6': '.EncryptedContent.Reaction', '9': 10, '10': 'reaction', '17': true},
|
||||
{'1': 'textMessage', '3': 13, '4': 1, '5': 11, '6': '.EncryptedContent.TextMessage', '9': 11, '10': 'textMessage', '17': true},
|
||||
],
|
||||
'3': [EncryptedContent_TextMessage$json, EncryptedContent_Reaction$json, EncryptedContent_MessageUpdate$json, EncryptedContent_Media$json, EncryptedContent_MediaUpdate$json, EncryptedContent_ContactRequest$json, EncryptedContent_ContactUpdate$json, EncryptedContent_PushKeys$json, EncryptedContent_FlameSync$json],
|
||||
'8': [
|
||||
{'1': '_groupId'},
|
||||
{'1': '_isDirectChat'},
|
||||
{'1': '_senderProfileCounter'},
|
||||
{'1': '_textMessage'},
|
||||
{'1': '_messageUpdate'},
|
||||
{'1': '_media'},
|
||||
{'1': '_mediaUpdate'},
|
||||
|
|
@ -119,6 +120,7 @@ const EncryptedContent$json = {
|
|||
{'1': '_flameSync'},
|
||||
{'1': '_pushKeys'},
|
||||
{'1': '_reaction'},
|
||||
{'1': '_textMessage'},
|
||||
],
|
||||
};
|
||||
|
||||
|
|
@ -219,7 +221,7 @@ const EncryptedContent_MediaUpdate$json = {
|
|||
'1': 'MediaUpdate',
|
||||
'2': [
|
||||
{'1': 'type', '3': 1, '4': 1, '5': 14, '6': '.EncryptedContent.MediaUpdate.Type', '10': 'type'},
|
||||
{'1': 'targetMessageId', '3': 2, '4': 1, '5': 9, '10': 'targetMessageId'},
|
||||
{'1': 'targetMediaId', '3': 2, '4': 1, '5': 9, '10': 'targetMediaId'},
|
||||
],
|
||||
'4': [EncryptedContent_MediaUpdate_Type$json],
|
||||
};
|
||||
|
|
@ -315,59 +317,60 @@ const EncryptedContent_FlameSync$json = {
|
|||
|
||||
/// Descriptor for `EncryptedContent`. Decode as a `google.protobuf.DescriptorProto`.
|
||||
final $typed_data.Uint8List encryptedContentDescriptor = $convert.base64Decode(
|
||||
'ChBFbmNyeXB0ZWRDb250ZW50Eh0KB2dyb3VwSWQYAiABKAlIAFIHZ3JvdXBJZIgBARI3ChRzZW'
|
||||
'5kZXJQcm9maWxlQ291bnRlchgDIAEoA0gBUhRzZW5kZXJQcm9maWxlQ291bnRlcogBARJECgt0'
|
||||
'ZXh0TWVzc2FnZRgEIAEoCzIdLkVuY3J5cHRlZENvbnRlbnQuVGV4dE1lc3NhZ2VIAlILdGV4dE'
|
||||
'1lc3NhZ2WIAQESSgoNbWVzc2FnZVVwZGF0ZRgFIAEoCzIfLkVuY3J5cHRlZENvbnRlbnQuTWVz'
|
||||
'c2FnZVVwZGF0ZUgDUg1tZXNzYWdlVXBkYXRliAEBEjIKBW1lZGlhGAYgASgLMhcuRW5jcnlwdG'
|
||||
'VkQ29udGVudC5NZWRpYUgEUgVtZWRpYYgBARJECgttZWRpYVVwZGF0ZRgHIAEoCzIdLkVuY3J5'
|
||||
'cHRlZENvbnRlbnQuTWVkaWFVcGRhdGVIBVILbWVkaWFVcGRhdGWIAQESSgoNY29udGFjdFVwZG'
|
||||
'F0ZRgIIAEoCzIfLkVuY3J5cHRlZENvbnRlbnQuQ29udGFjdFVwZGF0ZUgGUg1jb250YWN0VXBk'
|
||||
'YXRliAEBEk0KDmNvbnRhY3RSZXF1ZXN0GAkgASgLMiAuRW5jcnlwdGVkQ29udGVudC5Db250YW'
|
||||
'N0UmVxdWVzdEgHUg5jb250YWN0UmVxdWVzdIgBARI+CglmbGFtZVN5bmMYCiABKAsyGy5FbmNy'
|
||||
'eXB0ZWRDb250ZW50LkZsYW1lU3luY0gIUglmbGFtZVN5bmOIAQESOwoIcHVzaEtleXMYCyABKA'
|
||||
'syGi5FbmNyeXB0ZWRDb250ZW50LlB1c2hLZXlzSAlSCHB1c2hLZXlziAEBEjsKCHJlYWN0aW9u'
|
||||
'GAwgASgLMhouRW5jcnlwdGVkQ29udGVudC5SZWFjdGlvbkgKUghyZWFjdGlvbogBARqpAQoLVG'
|
||||
'V4dE1lc3NhZ2USKAoPc2VuZGVyTWVzc2FnZUlkGAEgASgJUg9zZW5kZXJNZXNzYWdlSWQSEgoE'
|
||||
'dGV4dBgCIAEoCVIEdGV4dBIcCgl0aW1lc3RhbXAYAyABKANSCXRpbWVzdGFtcBIrCg5xdW90ZU'
|
||||
'1lc3NhZ2VJZBgEIAEoCUgAUg5xdW90ZU1lc3NhZ2VJZIgBAUIRCg9fcXVvdGVNZXNzYWdlSWQa'
|
||||
'gQEKCFJlYWN0aW9uEigKD3RhcmdldE1lc3NhZ2VJZBgBIAEoCVIPdGFyZ2V0TWVzc2FnZUlkEh'
|
||||
'kKBWVtb2ppGAIgASgJSABSBWVtb2ppiAEBEhsKBnJlbW92ZRgDIAEoCEgBUgZyZW1vdmWIAQFC'
|
||||
'CAoGX2Vtb2ppQgkKB19yZW1vdmUatwIKDU1lc3NhZ2VVcGRhdGUSOAoEdHlwZRgBIAEoDjIkLk'
|
||||
'VuY3J5cHRlZENvbnRlbnQuTWVzc2FnZVVwZGF0ZS5UeXBlUgR0eXBlEi0KD3NlbmRlck1lc3Nh'
|
||||
'Z2VJZBgCIAEoCUgAUg9zZW5kZXJNZXNzYWdlSWSIAQESOgoYbXVsdGlwbGVTZW5kZXJNZXNzYW'
|
||||
'dlSWRzGAMgAygJUhhtdWx0aXBsZVNlbmRlck1lc3NhZ2VJZHMSFwoEdGV4dBgEIAEoCUgBUgR0'
|
||||
'ZXh0iAEBEhwKCXRpbWVzdGFtcBgFIAEoA1IJdGltZXN0YW1wIi0KBFR5cGUSCgoGREVMRVRFEA'
|
||||
'ASDQoJRURJVF9URVhUEAESCgoGT1BFTkVEEAJCEgoQX3NlbmRlck1lc3NhZ2VJZEIHCgVfdGV4'
|
||||
'dBqMBQoFTWVkaWESKAoPc2VuZGVyTWVzc2FnZUlkGAEgASgJUg9zZW5kZXJNZXNzYWdlSWQSMA'
|
||||
'oEdHlwZRgCIAEoDjIcLkVuY3J5cHRlZENvbnRlbnQuTWVkaWEuVHlwZVIEdHlwZRJDChpkaXNw'
|
||||
'bGF5TGltaXRJbk1pbGxpc2Vjb25kcxgDIAEoA0gAUhpkaXNwbGF5TGltaXRJbk1pbGxpc2Vjb2'
|
||||
'5kc4gBARI2ChZyZXF1aXJlc0F1dGhlbnRpY2F0aW9uGAQgASgIUhZyZXF1aXJlc0F1dGhlbnRp'
|
||||
'Y2F0aW9uEhwKCXRpbWVzdGFtcBgFIAEoA1IJdGltZXN0YW1wEisKDnF1b3RlTWVzc2FnZUlkGA'
|
||||
'YgASgJSAFSDnF1b3RlTWVzc2FnZUlkiAEBEikKDWRvd25sb2FkVG9rZW4YByABKAxIAlINZG93'
|
||||
'bmxvYWRUb2tlbogBARIpCg1lbmNyeXB0aW9uS2V5GAggASgMSANSDWVuY3J5cHRpb25LZXmIAQ'
|
||||
'ESKQoNZW5jcnlwdGlvbk1hYxgJIAEoDEgEUg1lbmNyeXB0aW9uTWFjiAEBEi0KD2VuY3J5cHRp'
|
||||
'b25Ob25jZRgKIAEoDEgFUg9lbmNyeXB0aW9uTm9uY2WIAQEiMwoEVHlwZRIMCghSRVVQTE9BRB'
|
||||
'AAEgkKBUlNQUdFEAESCQoFVklERU8QAhIHCgNHSUYQA0IdChtfZGlzcGxheUxpbWl0SW5NaWxs'
|
||||
'aXNlY29uZHNCEQoPX3F1b3RlTWVzc2FnZUlkQhAKDl9kb3dubG9hZFRva2VuQhAKDl9lbmNyeX'
|
||||
'B0aW9uS2V5QhAKDl9lbmNyeXB0aW9uTWFjQhIKEF9lbmNyeXB0aW9uTm9uY2UapwEKC01lZGlh'
|
||||
'VXBkYXRlEjYKBHR5cGUYASABKA4yIi5FbmNyeXB0ZWRDb250ZW50Lk1lZGlhVXBkYXRlLlR5cG'
|
||||
'VSBHR5cGUSKAoPdGFyZ2V0TWVzc2FnZUlkGAIgASgJUg90YXJnZXRNZXNzYWdlSWQiNgoEVHlw'
|
||||
'ZRIMCghSRU9QRU5FRBAAEgoKBlNUT1JFRBABEhQKEERFQ1JZUFRJT05fRVJST1IQAhp4Cg5Db2'
|
||||
'50YWN0UmVxdWVzdBI5CgR0eXBlGAEgASgOMiUuRW5jcnlwdGVkQ29udGVudC5Db250YWN0UmVx'
|
||||
'dWVzdC5UeXBlUgR0eXBlIisKBFR5cGUSCwoHUkVRVUVTVBAAEgoKBlJFSkVDVBABEgoKBkFDQ0'
|
||||
'VQVBACGtIBCg1Db250YWN0VXBkYXRlEjgKBHR5cGUYASABKA4yJC5FbmNyeXB0ZWRDb250ZW50'
|
||||
'LkNvbnRhY3RVcGRhdGUuVHlwZVIEdHlwZRIhCglhdmF0YXJTdmcYAiABKAlIAFIJYXZhdGFyU3'
|
||||
'ZniAEBEiUKC2Rpc3BsYXlOYW1lGAMgASgJSAFSC2Rpc3BsYXlOYW1liAEBIh8KBFR5cGUSCwoH'
|
||||
'UkVRVUVTVBAAEgoKBlVQREFURRABQgwKCl9hdmF0YXJTdmdCDgoMX2Rpc3BsYXlOYW1lGtUBCg'
|
||||
'hQdXNoS2V5cxIzCgR0eXBlGAEgASgOMh8uRW5jcnlwdGVkQ29udGVudC5QdXNoS2V5cy5UeXBl'
|
||||
'UgR0eXBlEhkKBWtleUlkGAIgASgDSABSBWtleUlkiAEBEhUKA2tleRgDIAEoDEgBUgNrZXmIAQ'
|
||||
'ESIQoJY3JlYXRlZEF0GAQgASgDSAJSCWNyZWF0ZWRBdIgBASIfCgRUeXBlEgsKB1JFUVVFU1QQ'
|
||||
'ABIKCgZVUERBVEUQAUIICgZfa2V5SWRCBgoEX2tleUIMCgpfY3JlYXRlZEF0GocBCglGbGFtZV'
|
||||
'N5bmMSIgoMZmxhbWVDb3VudGVyGAEgASgDUgxmbGFtZUNvdW50ZXISNgoWbGFzdEZsYW1lQ291'
|
||||
'bnRlckNoYW5nZRgCIAEoA1IWbGFzdEZsYW1lQ291bnRlckNoYW5nZRIeCgpiZXN0RnJpZW5kGA'
|
||||
'MgASgIUgpiZXN0RnJpZW5kQgoKCF9ncm91cElkQhcKFV9zZW5kZXJQcm9maWxlQ291bnRlckIO'
|
||||
'CgxfdGV4dE1lc3NhZ2VCEAoOX21lc3NhZ2VVcGRhdGVCCAoGX21lZGlhQg4KDF9tZWRpYVVwZG'
|
||||
'F0ZUIQCg5fY29udGFjdFVwZGF0ZUIRCg9fY29udGFjdFJlcXVlc3RCDAoKX2ZsYW1lU3luY0IL'
|
||||
'CglfcHVzaEtleXNCCwoJX3JlYWN0aW9u');
|
||||
'ChBFbmNyeXB0ZWRDb250ZW50Eh0KB2dyb3VwSWQYAiABKAlIAFIHZ3JvdXBJZIgBARInCgxpc0'
|
||||
'RpcmVjdENoYXQYAyABKAhIAVIMaXNEaXJlY3RDaGF0iAEBEjcKFHNlbmRlclByb2ZpbGVDb3Vu'
|
||||
'dGVyGAQgASgDSAJSFHNlbmRlclByb2ZpbGVDb3VudGVyiAEBEkoKDW1lc3NhZ2VVcGRhdGUYBS'
|
||||
'ABKAsyHy5FbmNyeXB0ZWRDb250ZW50Lk1lc3NhZ2VVcGRhdGVIA1INbWVzc2FnZVVwZGF0ZYgB'
|
||||
'ARIyCgVtZWRpYRgGIAEoCzIXLkVuY3J5cHRlZENvbnRlbnQuTWVkaWFIBFIFbWVkaWGIAQESRA'
|
||||
'oLbWVkaWFVcGRhdGUYByABKAsyHS5FbmNyeXB0ZWRDb250ZW50Lk1lZGlhVXBkYXRlSAVSC21l'
|
||||
'ZGlhVXBkYXRliAEBEkoKDWNvbnRhY3RVcGRhdGUYCCABKAsyHy5FbmNyeXB0ZWRDb250ZW50Lk'
|
||||
'NvbnRhY3RVcGRhdGVIBlINY29udGFjdFVwZGF0ZYgBARJNCg5jb250YWN0UmVxdWVzdBgJIAEo'
|
||||
'CzIgLkVuY3J5cHRlZENvbnRlbnQuQ29udGFjdFJlcXVlc3RIB1IOY29udGFjdFJlcXVlc3SIAQ'
|
||||
'ESPgoJZmxhbWVTeW5jGAogASgLMhsuRW5jcnlwdGVkQ29udGVudC5GbGFtZVN5bmNICFIJZmxh'
|
||||
'bWVTeW5jiAEBEjsKCHB1c2hLZXlzGAsgASgLMhouRW5jcnlwdGVkQ29udGVudC5QdXNoS2V5c0'
|
||||
'gJUghwdXNoS2V5c4gBARI7CghyZWFjdGlvbhgMIAEoCzIaLkVuY3J5cHRlZENvbnRlbnQuUmVh'
|
||||
'Y3Rpb25IClIIcmVhY3Rpb26IAQESRAoLdGV4dE1lc3NhZ2UYDSABKAsyHS5FbmNyeXB0ZWRDb2'
|
||||
'50ZW50LlRleHRNZXNzYWdlSAtSC3RleHRNZXNzYWdliAEBGqkBCgtUZXh0TWVzc2FnZRIoCg9z'
|
||||
'ZW5kZXJNZXNzYWdlSWQYASABKAlSD3NlbmRlck1lc3NhZ2VJZBISCgR0ZXh0GAIgASgJUgR0ZX'
|
||||
'h0EhwKCXRpbWVzdGFtcBgDIAEoA1IJdGltZXN0YW1wEisKDnF1b3RlTWVzc2FnZUlkGAQgASgJ'
|
||||
'SABSDnF1b3RlTWVzc2FnZUlkiAEBQhEKD19xdW90ZU1lc3NhZ2VJZBqBAQoIUmVhY3Rpb24SKA'
|
||||
'oPdGFyZ2V0TWVzc2FnZUlkGAEgASgJUg90YXJnZXRNZXNzYWdlSWQSGQoFZW1vamkYAiABKAlI'
|
||||
'AFIFZW1vammIAQESGwoGcmVtb3ZlGAMgASgISAFSBnJlbW92ZYgBAUIICgZfZW1vamlCCQoHX3'
|
||||
'JlbW92ZRq3AgoNTWVzc2FnZVVwZGF0ZRI4CgR0eXBlGAEgASgOMiQuRW5jcnlwdGVkQ29udGVu'
|
||||
'dC5NZXNzYWdlVXBkYXRlLlR5cGVSBHR5cGUSLQoPc2VuZGVyTWVzc2FnZUlkGAIgASgJSABSD3'
|
||||
'NlbmRlck1lc3NhZ2VJZIgBARI6ChhtdWx0aXBsZVNlbmRlck1lc3NhZ2VJZHMYAyADKAlSGG11'
|
||||
'bHRpcGxlU2VuZGVyTWVzc2FnZUlkcxIXCgR0ZXh0GAQgASgJSAFSBHRleHSIAQESHAoJdGltZX'
|
||||
'N0YW1wGAUgASgDUgl0aW1lc3RhbXAiLQoEVHlwZRIKCgZERUxFVEUQABINCglFRElUX1RFWFQQ'
|
||||
'ARIKCgZPUEVORUQQAkISChBfc2VuZGVyTWVzc2FnZUlkQgcKBV90ZXh0GowFCgVNZWRpYRIoCg'
|
||||
'9zZW5kZXJNZXNzYWdlSWQYASABKAlSD3NlbmRlck1lc3NhZ2VJZBIwCgR0eXBlGAIgASgOMhwu'
|
||||
'RW5jcnlwdGVkQ29udGVudC5NZWRpYS5UeXBlUgR0eXBlEkMKGmRpc3BsYXlMaW1pdEluTWlsbG'
|
||||
'lzZWNvbmRzGAMgASgDSABSGmRpc3BsYXlMaW1pdEluTWlsbGlzZWNvbmRziAEBEjYKFnJlcXVp'
|
||||
'cmVzQXV0aGVudGljYXRpb24YBCABKAhSFnJlcXVpcmVzQXV0aGVudGljYXRpb24SHAoJdGltZX'
|
||||
'N0YW1wGAUgASgDUgl0aW1lc3RhbXASKwoOcXVvdGVNZXNzYWdlSWQYBiABKAlIAVIOcXVvdGVN'
|
||||
'ZXNzYWdlSWSIAQESKQoNZG93bmxvYWRUb2tlbhgHIAEoDEgCUg1kb3dubG9hZFRva2VuiAEBEi'
|
||||
'kKDWVuY3J5cHRpb25LZXkYCCABKAxIA1INZW5jcnlwdGlvbktleYgBARIpCg1lbmNyeXB0aW9u'
|
||||
'TWFjGAkgASgMSARSDWVuY3J5cHRpb25NYWOIAQESLQoPZW5jcnlwdGlvbk5vbmNlGAogASgMSA'
|
||||
'VSD2VuY3J5cHRpb25Ob25jZYgBASIzCgRUeXBlEgwKCFJFVVBMT0FEEAASCQoFSU1BR0UQARIJ'
|
||||
'CgVWSURFTxACEgcKA0dJRhADQh0KG19kaXNwbGF5TGltaXRJbk1pbGxpc2Vjb25kc0IRCg9fcX'
|
||||
'VvdGVNZXNzYWdlSWRCEAoOX2Rvd25sb2FkVG9rZW5CEAoOX2VuY3J5cHRpb25LZXlCEAoOX2Vu'
|
||||
'Y3J5cHRpb25NYWNCEgoQX2VuY3J5cHRpb25Ob25jZRqjAQoLTWVkaWFVcGRhdGUSNgoEdHlwZR'
|
||||
'gBIAEoDjIiLkVuY3J5cHRlZENvbnRlbnQuTWVkaWFVcGRhdGUuVHlwZVIEdHlwZRIkCg10YXJn'
|
||||
'ZXRNZWRpYUlkGAIgASgJUg10YXJnZXRNZWRpYUlkIjYKBFR5cGUSDAoIUkVPUEVORUQQABIKCg'
|
||||
'ZTVE9SRUQQARIUChBERUNSWVBUSU9OX0VSUk9SEAIaeAoOQ29udGFjdFJlcXVlc3QSOQoEdHlw'
|
||||
'ZRgBIAEoDjIlLkVuY3J5cHRlZENvbnRlbnQuQ29udGFjdFJlcXVlc3QuVHlwZVIEdHlwZSIrCg'
|
||||
'RUeXBlEgsKB1JFUVVFU1QQABIKCgZSRUpFQ1QQARIKCgZBQ0NFUFQQAhrSAQoNQ29udGFjdFVw'
|
||||
'ZGF0ZRI4CgR0eXBlGAEgASgOMiQuRW5jcnlwdGVkQ29udGVudC5Db250YWN0VXBkYXRlLlR5cG'
|
||||
'VSBHR5cGUSIQoJYXZhdGFyU3ZnGAIgASgJSABSCWF2YXRhclN2Z4gBARIlCgtkaXNwbGF5TmFt'
|
||||
'ZRgDIAEoCUgBUgtkaXNwbGF5TmFtZYgBASIfCgRUeXBlEgsKB1JFUVVFU1QQABIKCgZVUERBVE'
|
||||
'UQAUIMCgpfYXZhdGFyU3ZnQg4KDF9kaXNwbGF5TmFtZRrVAQoIUHVzaEtleXMSMwoEdHlwZRgB'
|
||||
'IAEoDjIfLkVuY3J5cHRlZENvbnRlbnQuUHVzaEtleXMuVHlwZVIEdHlwZRIZCgVrZXlJZBgCIA'
|
||||
'EoA0gAUgVrZXlJZIgBARIVCgNrZXkYAyABKAxIAVIDa2V5iAEBEiEKCWNyZWF0ZWRBdBgEIAEo'
|
||||
'A0gCUgljcmVhdGVkQXSIAQEiHwoEVHlwZRILCgdSRVFVRVNUEAASCgoGVVBEQVRFEAFCCAoGX2'
|
||||
'tleUlkQgYKBF9rZXlCDAoKX2NyZWF0ZWRBdBqHAQoJRmxhbWVTeW5jEiIKDGZsYW1lQ291bnRl'
|
||||
'chgBIAEoA1IMZmxhbWVDb3VudGVyEjYKFmxhc3RGbGFtZUNvdW50ZXJDaGFuZ2UYAiABKANSFm'
|
||||
'xhc3RGbGFtZUNvdW50ZXJDaGFuZ2USHgoKYmVzdEZyaWVuZBgDIAEoCFIKYmVzdEZyaWVuZEIK'
|
||||
'CghfZ3JvdXBJZEIPCg1faXNEaXJlY3RDaGF0QhcKFV9zZW5kZXJQcm9maWxlQ291bnRlckIQCg'
|
||||
'5fbWVzc2FnZVVwZGF0ZUIICgZfbWVkaWFCDgoMX21lZGlhVXBkYXRlQhAKDl9jb250YWN0VXBk'
|
||||
'YXRlQhEKD19jb250YWN0UmVxdWVzdEIMCgpfZmxhbWVTeW5jQgsKCV9wdXNoS2V5c0ILCglfcm'
|
||||
'VhY3Rpb25CDgoMX3RleHRNZXNzYWdl');
|
||||
|
||||
|
|
|
|||
|
|
@ -30,11 +30,11 @@ message PlaintextContent {
|
|||
message EncryptedContent {
|
||||
|
||||
optional string groupId = 2;
|
||||
optional bool isDirectChat = 3;
|
||||
|
||||
/// This can be added, so the receiver can check weather he is up to date with the current profile
|
||||
optional int64 senderProfileCounter = 3;
|
||||
optional int64 senderProfileCounter = 4;
|
||||
|
||||
optional TextMessage textMessage = 4;
|
||||
optional MessageUpdate messageUpdate = 5;
|
||||
optional Media media = 6;
|
||||
optional MediaUpdate mediaUpdate = 7;
|
||||
|
|
@ -43,6 +43,7 @@ message EncryptedContent {
|
|||
optional FlameSync flameSync = 10;
|
||||
optional PushKeys pushKeys = 11;
|
||||
optional Reaction reaction = 12;
|
||||
optional TextMessage textMessage = 13;
|
||||
|
||||
message TextMessage {
|
||||
string senderMessageId = 1;
|
||||
|
|
@ -98,7 +99,7 @@ message EncryptedContent {
|
|||
DECRYPTION_ERROR = 2;
|
||||
}
|
||||
Type type = 1;
|
||||
string targetMessageId = 2;
|
||||
string targetMediaId = 2;
|
||||
}
|
||||
|
||||
message ContactRequest {
|
||||
|
|
|
|||
|
|
@ -207,7 +207,8 @@ Future<void> requestMediaReupload(String mediaId) async {
|
|||
final messages = await twonlyDB.messagesDao.getMessagesByMediaId(mediaId);
|
||||
if (messages.length != 1 || messages.first.senderId == null) {
|
||||
Log.error(
|
||||
'Media file has none or more than one sender. That is not possible');
|
||||
'Media file has none or more than one sender. That is not possible',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -216,7 +217,7 @@ Future<void> requestMediaReupload(String mediaId) async {
|
|||
EncryptedContent(
|
||||
mediaUpdate: EncryptedContent_MediaUpdate(
|
||||
type: EncryptedContent_MediaUpdate_Type.DECRYPTION_ERROR,
|
||||
targetMessageId: mediaId,
|
||||
targetMediaId: mediaId,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -119,6 +119,15 @@ Future<void> _createUploadRequest(MediaFileService media) async {
|
|||
for (final message in messages) {
|
||||
final groupMembers =
|
||||
await twonlyDB.groupsDao.getGroupMembers(message.groupId);
|
||||
|
||||
if (media.mediaFile.reuploadRequestedBy == null) {
|
||||
await twonlyDB.groupsDao.incFlameCounter(
|
||||
message.groupId,
|
||||
false,
|
||||
message.createdAt,
|
||||
);
|
||||
}
|
||||
|
||||
for (final groupMember in groupMembers) {
|
||||
/// only send the upload to the users
|
||||
if (media.mediaFile.reuploadRequestedBy != null) {
|
||||
|
|
@ -128,12 +137,6 @@ Future<void> _createUploadRequest(MediaFileService media) async {
|
|||
}
|
||||
}
|
||||
|
||||
await twonlyDB.contactsDao.incFlameCounter(
|
||||
groupMember.contactId,
|
||||
false,
|
||||
message.createdAt,
|
||||
);
|
||||
|
||||
final downloadToken = getRandomUint8List(32);
|
||||
|
||||
var type = EncryptedContent_Media_Type.IMAGE;
|
||||
|
|
@ -169,7 +172,8 @@ Future<void> _createUploadRequest(MediaFileService media) async {
|
|||
|
||||
if (cipherText == null) {
|
||||
Log.error(
|
||||
'Could not generate ciphertext message for ${groupMember.contactId}');
|
||||
'Could not generate ciphertext message for ${groupMember.contactId}',
|
||||
);
|
||||
}
|
||||
|
||||
final messageOnSuccess = TextMessage()
|
||||
|
|
|
|||
|
|
@ -161,11 +161,13 @@ Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({
|
|||
Future<void> insertAndSendTextMessage(
|
||||
String groupId,
|
||||
String textMessage,
|
||||
String? quotesMessageId,
|
||||
) async {
|
||||
final message = await twonlyDB.messagesDao.insertMessage(
|
||||
MessagesCompanion(
|
||||
groupId: Value(groupId),
|
||||
content: Value(textMessage),
|
||||
quotesMessageId: Value(quotesMessageId),
|
||||
),
|
||||
);
|
||||
if (message == null) {
|
||||
|
|
@ -173,19 +175,40 @@ Future<void> insertAndSendTextMessage(
|
|||
return;
|
||||
}
|
||||
|
||||
final encryptedContent = pb.EncryptedContent(
|
||||
textMessage: pb.EncryptedContent_TextMessage(
|
||||
senderMessageId: message.messageId,
|
||||
text: textMessage,
|
||||
timestamp: Int64(message.createdAt.millisecondsSinceEpoch),
|
||||
),
|
||||
);
|
||||
|
||||
if (quotesMessageId != null) {
|
||||
encryptedContent.textMessage.quoteMessageId = quotesMessageId;
|
||||
}
|
||||
|
||||
await sendCipherTextToGroup(groupId, encryptedContent);
|
||||
}
|
||||
|
||||
Future<void> sendCipherTextToGroup(
|
||||
String groupId,
|
||||
pb.EncryptedContent encryptedContent,
|
||||
) async {
|
||||
final groupMembers = await twonlyDB.groupsDao.getGroupMembers(groupId);
|
||||
final group = await twonlyDB.groupsDao.getGroup(groupId);
|
||||
if (group == null) return;
|
||||
|
||||
encryptedContent
|
||||
..groupId = groupId
|
||||
..isDirectChat = group.isDirectChat;
|
||||
|
||||
for (final groupMember in groupMembers) {
|
||||
unawaited(sendCipherText(
|
||||
groupMember.contactId,
|
||||
pb.EncryptedContent(
|
||||
textMessage: pb.EncryptedContent_TextMessage(
|
||||
senderMessageId: message.messageId,
|
||||
text: textMessage,
|
||||
timestamp: Int64(message.createdAt.millisecondsSinceEpoch),
|
||||
),
|
||||
unawaited(
|
||||
sendCipherText(
|
||||
groupMember.contactId,
|
||||
encryptedContent,
|
||||
),
|
||||
));
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -82,24 +82,22 @@ Future<void> handleFlameSync(
|
|||
EncryptedContent_FlameSync flameSync,
|
||||
) async {
|
||||
Log.info('Got a flameSync from $contactId');
|
||||
final contact = await twonlyDB.contactsDao
|
||||
.getContactByUserId(contactId)
|
||||
.getSingleOrNull();
|
||||
|
||||
if (contact == null || contact.lastFlameCounterChange != null) return;
|
||||
final group = await twonlyDB.groupsDao.getDirectChat(contactId);
|
||||
if (group == null || group.lastFlameCounterChange != null) return;
|
||||
|
||||
var updates = ContactsCompanion(
|
||||
var updates = GroupsCompanion(
|
||||
alsoBestFriend: Value(flameSync.bestFriend),
|
||||
);
|
||||
if (isToday(contact.lastFlameCounterChange!) &&
|
||||
if (isToday(group.lastFlameCounterChange!) &&
|
||||
isToday(fromTimestamp(flameSync.lastFlameCounterChange))) {
|
||||
if (flameSync.flameCounter > contact.flameCounter) {
|
||||
updates = ContactsCompanion(
|
||||
if (flameSync.flameCounter > group.flameCounter) {
|
||||
updates = GroupsCompanion(
|
||||
flameCounter: Value(flameSync.flameCounter.toInt()),
|
||||
);
|
||||
}
|
||||
}
|
||||
await twonlyDB.contactsDao.updateContact(contactId, updates);
|
||||
await twonlyDB.groupsDao.updateGroup(group.groupId, updates);
|
||||
}
|
||||
|
||||
Future<int?> checkForProfileUpdate(
|
||||
|
|
|
|||
|
|
@ -100,8 +100,8 @@ Future<void> handleMedia(
|
|||
);
|
||||
if (message != null) {
|
||||
Log.info('Inserted a new media message with ID: ${message.messageId}');
|
||||
await twonlyDB.contactsDao.incFlameCounter(
|
||||
fromUserId,
|
||||
await twonlyDB.groupsDao.incFlameCounter(
|
||||
message.groupId,
|
||||
true,
|
||||
fromTimestamp(media.timestamp),
|
||||
);
|
||||
|
|
@ -115,15 +115,17 @@ Future<void> handleMediaUpdate(
|
|||
String groupId,
|
||||
EncryptedContent_MediaUpdate mediaUpdate,
|
||||
) async {
|
||||
final message = await twonlyDB.messagesDao
|
||||
.getMessageById(mediaUpdate.targetMessageId)
|
||||
.getSingleOrNull();
|
||||
if (message == null || message.mediaId == null) return;
|
||||
final messages = await twonlyDB.messagesDao
|
||||
.getMessagesByMediaId(mediaUpdate.targetMediaId);
|
||||
if (messages.length != 1) return;
|
||||
final message = messages.first;
|
||||
if (message.senderId != fromUserId) return;
|
||||
final mediaFile =
|
||||
await twonlyDB.mediaFilesDao.getMediaFileById(message.mediaId!);
|
||||
if (mediaFile == null) {
|
||||
Log.info(
|
||||
'Got media file update, but media file was not found ${message.mediaId}');
|
||||
'Got media file update, but media file was not found ${message.mediaId}',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,8 @@ Future<void> handleMessageUpdate(
|
|||
switch (messageUpdate.type) {
|
||||
case EncryptedContent_MessageUpdate_Type.OPENED:
|
||||
Log.info(
|
||||
'Opened message ${messageUpdate.multipleSenderMessageIds.length}');
|
||||
'Opened message ${messageUpdate.multipleSenderMessageIds.length}',
|
||||
);
|
||||
for (final senderMessageId in messageUpdate.multipleSenderMessageIds) {
|
||||
await twonlyDB.messagesDao.handleMessageOpened(
|
||||
contactId,
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ Future<void> handleMediaError(MediaFile media) async {
|
|||
EncryptedContent(
|
||||
mediaUpdate: EncryptedContent_MediaUpdate(
|
||||
type: EncryptedContent_MediaUpdate_Type.DECRYPTION_ERROR,
|
||||
targetMessageId: message.messageId,
|
||||
targetMediaId: message.mediaId,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import 'package:collection/collection.dart';
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:fixnum/fixnum.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||
import 'package:twonly/src/database/daos/groups.dao.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
|
||||
import 'package:twonly/src/services/api/messages.dart';
|
||||
|
|
@ -10,49 +10,52 @@ import 'package:twonly/src/utils/misc.dart';
|
|||
import 'package:twonly/src/utils/storage.dart';
|
||||
|
||||
Future<void> syncFlameCounters() async {
|
||||
final user = await getUser();
|
||||
if (user == null) return;
|
||||
|
||||
final contacts = await twonlyDB.contactsDao.getAllNotBlockedContacts();
|
||||
if (contacts.isEmpty) return;
|
||||
final maxMessageCounter = contacts.map((x) => x.totalMediaCounter).max;
|
||||
final groups = await twonlyDB.groupsDao.getAllDirectChats();
|
||||
if (groups.isEmpty) return;
|
||||
final maxMessageCounter = groups.map((x) => x.totalMediaCounter).max;
|
||||
final bestFriend =
|
||||
contacts.firstWhere((x) => x.totalMediaCounter == maxMessageCounter);
|
||||
groups.firstWhere((x) => x.totalMediaCounter == maxMessageCounter);
|
||||
|
||||
if (user.myBestFriendContactId != bestFriend.userId) {
|
||||
if (gUser.myBestFriendGroupId != bestFriend.groupId) {
|
||||
await updateUserdata((user) {
|
||||
user.myBestFriendContactId = bestFriend.userId;
|
||||
user.myBestFriendGroupId = bestFriend.groupId;
|
||||
return user;
|
||||
});
|
||||
}
|
||||
|
||||
for (final contact in contacts) {
|
||||
if (contact.lastFlameCounterChange == null || contact.deleted) continue;
|
||||
if (!isToday(contact.lastFlameCounterChange!)) continue;
|
||||
if (contact.lastFlameSync != null) {
|
||||
if (isToday(contact.lastFlameSync!)) continue;
|
||||
for (final group in groups) {
|
||||
if (group.lastFlameCounterChange == null) continue;
|
||||
if (!isToday(group.lastFlameCounterChange!)) continue;
|
||||
if (group.lastFlameSync != null) {
|
||||
if (isToday(group.lastFlameSync!)) continue;
|
||||
}
|
||||
|
||||
final flameCounter = getFlameCounterFromContact(contact) - 1;
|
||||
final flameCounter = getFlameCounterFromGroup(group) - 1;
|
||||
|
||||
// only sync when flame counter is higher than three days
|
||||
if (flameCounter < 1 && bestFriend.userId != contact.userId) continue;
|
||||
// only sync when flame counter is higher than three days or when they are bestFriends
|
||||
if (flameCounter < 1 && bestFriend.groupId != group.groupId) continue;
|
||||
|
||||
final groupMembers =
|
||||
await twonlyDB.groupsDao.getGroupMembers(group.groupId);
|
||||
if (groupMembers.length != 1) {
|
||||
continue; // flame sync is only done for groups of two
|
||||
}
|
||||
|
||||
await sendCipherText(
|
||||
contact.userId,
|
||||
groupMembers.first.contactId,
|
||||
EncryptedContent(
|
||||
flameSync: EncryptedContent_FlameSync(
|
||||
flameCounter: Int64(flameCounter),
|
||||
lastFlameCounterChange:
|
||||
Int64(contact.lastFlameCounterChange!.millisecondsSinceEpoch),
|
||||
bestFriend: contact.userId == bestFriend.userId,
|
||||
Int64(group.lastFlameCounterChange!.millisecondsSinceEpoch),
|
||||
bestFriend: group.groupId == bestFriend.groupId,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await twonlyDB.contactsDao.updateContact(
|
||||
contact.userId,
|
||||
ContactsCompanion(
|
||||
await twonlyDB.groupsDao.updateGroup(
|
||||
group.groupId,
|
||||
GroupsCompanion(
|
||||
lastFlameSync: Value(DateTime.now()),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@ class MediaFileService {
|
|||
originalPath,
|
||||
storedPath,
|
||||
thumbnailPath,
|
||||
uploadRequestPath
|
||||
uploadRequestPath,
|
||||
];
|
||||
|
||||
for (final path in pathsToRemove) {
|
||||
|
|
@ -146,11 +146,13 @@ class MediaFileService {
|
|||
String namePrefix = '',
|
||||
String extensionParam = '',
|
||||
}) {
|
||||
final mediaBaseDir = Directory(join(
|
||||
applicationSupportDirectory.path,
|
||||
'mediafiles',
|
||||
directory,
|
||||
));
|
||||
final mediaBaseDir = Directory(
|
||||
join(
|
||||
applicationSupportDirectory.path,
|
||||
'mediafiles',
|
||||
directory,
|
||||
),
|
||||
);
|
||||
if (!mediaBaseDir.existsSync()) {
|
||||
mediaBaseDir.createSync(recursive: true);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import 'dart:math';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_image_compress/flutter_image_compress.dart';
|
||||
|
|
@ -8,11 +7,14 @@ import 'package:intl/intl.dart';
|
|||
import 'package:local_auth/local_auth.dart';
|
||||
import 'package:pie_menu/pie_menu.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
||||
import 'package:twonly/src/database/tables/messages.table.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/localization/generated/app_localizations.dart';
|
||||
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
|
||||
import 'package:twonly/src/providers/settings.provider.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
|
||||
extension ShortCutsExtension on BuildContext {
|
||||
AppLocalizations get lang => AppLocalizations.of(this)!;
|
||||
|
|
@ -284,3 +286,28 @@ PieTheme getPieCanvasTheme(BuildContext context) {
|
|||
),
|
||||
);
|
||||
}
|
||||
|
||||
Color getMessageColorFromType(
|
||||
Message message,
|
||||
MediaFile? mediaFile,
|
||||
BuildContext context,
|
||||
) {
|
||||
Color color;
|
||||
|
||||
if (message.type == MessageType.text) {
|
||||
color = Colors.blueAccent;
|
||||
} else if (mediaFile != null) {
|
||||
if (mediaFile.requiresAuthentication) {
|
||||
color = context.color.primary;
|
||||
} else {
|
||||
if (mediaFile.type == MediaType.video) {
|
||||
color = const Color.fromARGB(255, 243, 33, 208);
|
||||
} else {
|
||||
color = Colors.redAccent;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return (isDarkMode(context)) ? Colors.white : Colors.black;
|
||||
}
|
||||
return color;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,37 +1,31 @@
|
|||
// ignore_for_file: strict_raw_type
|
||||
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/views/components/avatar_icon.component.dart';
|
||||
import 'package:twonly/src/views/components/flame.dart';
|
||||
import 'package:twonly/src/views/components/headline.dart';
|
||||
import 'package:twonly/src/views/components/initialsavatar.dart';
|
||||
|
||||
class BestFriendsSelector extends StatelessWidget {
|
||||
const BestFriendsSelector({
|
||||
required this.users,
|
||||
required this.isRealTwonly,
|
||||
required this.updateStatus,
|
||||
required this.selectedUserIds,
|
||||
required this.groups,
|
||||
required this.selectedGroupIds,
|
||||
required this.updateSelectedGroupIds,
|
||||
required this.title,
|
||||
required this.showSelectAll,
|
||||
super.key,
|
||||
});
|
||||
final List<Contact> users;
|
||||
final void Function(int, bool) updateStatus;
|
||||
final HashSet<int> selectedUserIds;
|
||||
final bool isRealTwonly;
|
||||
final List<Group> groups;
|
||||
final HashSet<String> selectedGroupIds;
|
||||
final void Function(String, bool) updateSelectedGroupIds;
|
||||
final String title;
|
||||
final bool showSelectAll;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (users.isEmpty) {
|
||||
if (groups.isEmpty) {
|
||||
return Container();
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Row(
|
||||
|
|
@ -39,11 +33,11 @@ class BestFriendsSelector extends StatelessWidget {
|
|||
Expanded(
|
||||
child: HeadLineComponent(title),
|
||||
),
|
||||
if (!isRealTwonly)
|
||||
if (showSelectAll)
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
for (final user in users) {
|
||||
updateStatus(user.userId, true);
|
||||
for (final group in groups) {
|
||||
updateSelectedGroupIds(group.groupId, true);
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
|
|
@ -70,7 +64,7 @@ class BestFriendsSelector extends StatelessWidget {
|
|||
Column(
|
||||
spacing: 8,
|
||||
children: List.generate(
|
||||
(users.length + 1) ~/ 2,
|
||||
(groups.length + 1) ~/ 2,
|
||||
(rowIndex) {
|
||||
final firstUserIndex = rowIndex * 2;
|
||||
final secondUserIndex = firstUserIndex + 1;
|
||||
|
|
@ -79,21 +73,19 @@ class BestFriendsSelector extends StatelessWidget {
|
|||
children: [
|
||||
Expanded(
|
||||
child: UserCheckbox(
|
||||
isChecked: selectedUserIds
|
||||
.contains(users[firstUserIndex].userId),
|
||||
user: users[firstUserIndex],
|
||||
onChanged: updateStatus,
|
||||
isRealTwonly: isRealTwonly,
|
||||
isChecked: selectedGroupIds
|
||||
.contains(groups[firstUserIndex].groupId),
|
||||
group: groups[firstUserIndex],
|
||||
onChanged: updateSelectedGroupIds,
|
||||
),
|
||||
),
|
||||
if (secondUserIndex < users.length)
|
||||
if (secondUserIndex < groups.length)
|
||||
Expanded(
|
||||
child: UserCheckbox(
|
||||
isChecked: selectedUserIds
|
||||
.contains(users[secondUserIndex].userId),
|
||||
user: users[secondUserIndex],
|
||||
onChanged: updateStatus,
|
||||
isRealTwonly: isRealTwonly,
|
||||
isChecked: selectedGroupIds
|
||||
.contains(groups[secondUserIndex].groupId),
|
||||
group: groups[secondUserIndex],
|
||||
onChanged: updateSelectedGroupIds,
|
||||
),
|
||||
)
|
||||
else
|
||||
|
|
@ -112,28 +104,24 @@ class BestFriendsSelector extends StatelessWidget {
|
|||
|
||||
class UserCheckbox extends StatelessWidget {
|
||||
const UserCheckbox({
|
||||
required this.user,
|
||||
required this.group,
|
||||
required this.onChanged,
|
||||
required this.isRealTwonly,
|
||||
required this.isChecked,
|
||||
super.key,
|
||||
});
|
||||
final Contact user;
|
||||
final void Function(int, bool) onChanged;
|
||||
final Group group;
|
||||
final void Function(String, bool) onChanged;
|
||||
final bool isChecked;
|
||||
final bool isRealTwonly;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final displayName = getContactDisplayName(user);
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 3,
|
||||
), // Padding inside the container
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
onChanged(user.userId, !isChecked);
|
||||
onChanged(group.groupId, !isChecked);
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
|
|
@ -149,8 +137,8 @@ class UserCheckbox extends StatelessWidget {
|
|||
),
|
||||
child: Row(
|
||||
children: [
|
||||
ContactAvatar(
|
||||
contact: user,
|
||||
AvatarIcon(
|
||||
group: group,
|
||||
fontSize: 12,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
|
|
@ -160,28 +148,21 @@ class UserCheckbox extends StatelessWidget {
|
|||
Row(
|
||||
children: [
|
||||
Text(
|
||||
displayName.length > 8
|
||||
? '${displayName.substring(0, 8)}...'
|
||||
: displayName,
|
||||
group.groupName.length > 12
|
||||
? '${group.groupName.substring(0, 9)}...'
|
||||
: group.groupName,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
StreamBuilder(
|
||||
stream: twonlyDB.contactsDao.watchFlameCounter(user.userId),
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData || snapshot.data! == 0) {
|
||||
return Container();
|
||||
}
|
||||
return FlameCounterWidget(user, snapshot.data!);
|
||||
},
|
||||
),
|
||||
FlameCounterWidget(groupId: group.groupId),
|
||||
],
|
||||
),
|
||||
Expanded(child: Container()),
|
||||
Checkbox(
|
||||
value: isChecked,
|
||||
side: WidgetStateBorderSide.resolveWith(
|
||||
// ignore: strict_raw_type
|
||||
(Set states) {
|
||||
if (states.contains(WidgetState.selected)) {
|
||||
return const BorderSide(width: 0);
|
||||
|
|
@ -192,7 +173,7 @@ class UserCheckbox extends StatelessWidget {
|
|||
},
|
||||
),
|
||||
onChanged: (bool? value) {
|
||||
onChanged(user.userId, value ?? false);
|
||||
onChanged(group.groupId, value ?? false);
|
||||
},
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -186,7 +186,8 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
onPressed: () async {
|
||||
if (media.type != MediaType.video) {
|
||||
await mediaService.setDisplayLimit(
|
||||
(media.displayLimitInMilliseconds == null) ? 0 : null);
|
||||
(media.displayLimitInMilliseconds == null) ? 0 : null,
|
||||
);
|
||||
if (!mounted) return;
|
||||
setState(() {});
|
||||
return;
|
||||
|
|
@ -465,8 +466,9 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
: const FaIcon(FontAwesomeIcons.solidPaperPlane),
|
||||
onPressed: () async {
|
||||
if (sendingOrLoadingImage) return;
|
||||
if (widget.sendToGroup == null)
|
||||
if (widget.sendToGroup == null) {
|
||||
return pushShareImageView();
|
||||
}
|
||||
await sendImageToSinglePerson();
|
||||
},
|
||||
style: ButtonStyle(
|
||||
|
|
|
|||
|
|
@ -2,23 +2,19 @@
|
|||
|
||||
import 'dart:async';
|
||||
import 'dart:collection';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||
import 'package:twonly/src/database/daos/groups.dao.dart';
|
||||
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/services/api/mediafiles/upload.service.dart';
|
||||
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/views/camera/share_image_components/best_friends_selector.dart';
|
||||
import 'package:twonly/src/views/components/avatar_icon.component.dart';
|
||||
import 'package:twonly/src/views/components/flame.dart';
|
||||
import 'package:twonly/src/views/components/headline.dart';
|
||||
import 'package:twonly/src/views/components/initialsavatar.dart';
|
||||
import 'package:twonly/src/views/components/verified_shield.dart';
|
||||
import 'package:twonly/src/views/settings/subscription/subscription.view.dart';
|
||||
|
||||
class ShareImageView extends StatefulWidget {
|
||||
const ShareImageView({
|
||||
|
|
@ -38,28 +34,27 @@ class ShareImageView extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _ShareImageView extends State<ShareImageView> {
|
||||
List<Contact> contacts = [];
|
||||
List<Contact> _otherUsers = [];
|
||||
List<Contact> _bestFriends = [];
|
||||
List<Contact> _pinnedContacts = [];
|
||||
Uint8List? imageBytes;
|
||||
List<Group> contacts = [];
|
||||
List<Group> _otherUsers = [];
|
||||
List<Group> _bestFriends = [];
|
||||
List<Group> _pinnedContacts = [];
|
||||
|
||||
bool sendingImage = false;
|
||||
bool mediaStoreFutureReady = false;
|
||||
bool hideArchivedUsers = true;
|
||||
final TextEditingController searchUserName = TextEditingController();
|
||||
late StreamSubscription<List<Contact>> contactSub;
|
||||
late StreamSubscription<List<Group>> allGroupSub;
|
||||
String lastQuery = '';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
final allContacts = twonlyDB.contactsDao.watchContactsForShareView();
|
||||
|
||||
contactSub = allContacts.listen((allContacts) async {
|
||||
allGroupSub = twonlyDB.groupsDao.watchGroups().listen((allGroups) async {
|
||||
setState(() {
|
||||
contacts = allContacts;
|
||||
contacts = allGroups;
|
||||
});
|
||||
await updateUsers(allContacts.where((x) => !x.archived).toList());
|
||||
await updateGroups(allGroups.where((x) => !x.archived).toList());
|
||||
});
|
||||
|
||||
unawaited(initAsync());
|
||||
|
|
@ -69,6 +64,7 @@ class _ShareImageView extends State<ShareImageView> {
|
|||
if (widget.mediaStoreFuture != null) {
|
||||
await widget.mediaStoreFuture;
|
||||
}
|
||||
mediaStoreFutureReady = true;
|
||||
await widget.mediaFileService.setUploadState(UploadState.preprocessing);
|
||||
unawaited(startBackgroundMediaUpload(widget.mediaFileService));
|
||||
if (!mounted) return;
|
||||
|
|
@ -77,16 +73,17 @@ class _ShareImageView extends State<ShareImageView> {
|
|||
|
||||
@override
|
||||
void dispose() {
|
||||
unawaited(contactSub.cancel());
|
||||
unawaited(allGroupSub.cancel());
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> updateUsers(List<Contact> users) async {
|
||||
Future<void> updateGroups(List<Group> groups) async {
|
||||
// Sort contacts by flameCounter and then by totalMediaCounter
|
||||
users.sort((a, b) {
|
||||
groups.sort((a, b) {
|
||||
// First, compare by flameCounter
|
||||
final flameComparison = getFlameCounterFromContact(b)
|
||||
.compareTo(getFlameCounterFromContact(a));
|
||||
|
||||
final flameComparison =
|
||||
getFlameCounterFromGroup(b).compareTo(getFlameCounterFromGroup(a));
|
||||
if (flameComparison != 0) {
|
||||
return flameComparison; // Sort by flameCounter in descending order
|
||||
}
|
||||
|
|
@ -97,18 +94,18 @@ class _ShareImageView extends State<ShareImageView> {
|
|||
});
|
||||
|
||||
// Separate best friends and other users
|
||||
final bestFriends = <Contact>[];
|
||||
final otherUsers = <Contact>[];
|
||||
final pinnedContacts = users.where((c) => c.pinned).toList();
|
||||
final bestFriends = <Group>[];
|
||||
final otherUsers = <Group>[];
|
||||
final pinnedContacts = groups.where((c) => c.pinned).toList();
|
||||
|
||||
for (final contact in users) {
|
||||
if (contact.pinned) continue;
|
||||
if (!contact.archived &&
|
||||
(getFlameCounterFromContact(contact)) > 0 &&
|
||||
for (final group in groups) {
|
||||
if (group.pinned) continue;
|
||||
if (!group.archived &&
|
||||
(getFlameCounterFromGroup(group)) > 0 &&
|
||||
bestFriends.length < 6) {
|
||||
bestFriends.add(contact);
|
||||
bestFriends.add(group);
|
||||
} else {
|
||||
otherUsers.add(contact);
|
||||
otherUsers.add(group);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -122,13 +119,13 @@ class _ShareImageView extends State<ShareImageView> {
|
|||
Future<void> _filterUsers(String query) async {
|
||||
lastQuery = query;
|
||||
if (query.isEmpty) {
|
||||
await updateUsers(
|
||||
await updateGroups(
|
||||
contacts
|
||||
.where(
|
||||
(x) =>
|
||||
!x.archived ||
|
||||
!hideArchivedUsers ||
|
||||
widget.selectedUserIds.contains(x.userId),
|
||||
widget.selectedGroupIds.contains(x.groupId),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
|
|
@ -136,16 +133,14 @@ class _ShareImageView extends State<ShareImageView> {
|
|||
}
|
||||
final usersFiltered = contacts
|
||||
.where(
|
||||
(user) => getContactDisplayName(user)
|
||||
.toLowerCase()
|
||||
.contains(query.toLowerCase()),
|
||||
(user) => user.groupName.toLowerCase().contains(query.toLowerCase()),
|
||||
)
|
||||
.toList();
|
||||
await updateUsers(usersFiltered);
|
||||
await updateGroups(usersFiltered);
|
||||
}
|
||||
|
||||
void updateStatus(int userId, bool checked) {
|
||||
widget.updateStatus(userId, checked);
|
||||
void updateSelectedGroupIds(String groupId, bool checked) {
|
||||
widget.updateSelectedGroupIds(groupId, checked);
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
|
|
@ -173,19 +168,21 @@ class _ShareImageView extends State<ShareImageView> {
|
|||
),
|
||||
if (_pinnedContacts.isNotEmpty) const SizedBox(height: 10),
|
||||
BestFriendsSelector(
|
||||
users: _pinnedContacts,
|
||||
selectedUserIds: widget.selectedUserIds,
|
||||
isRealTwonly: widget.isRealTwonly,
|
||||
updateStatus: updateStatus,
|
||||
groups: _pinnedContacts,
|
||||
selectedGroupIds: widget.selectedGroupIds,
|
||||
updateSelectedGroupIds: updateSelectedGroupIds,
|
||||
title: context.lang.shareImagePinnedContacts,
|
||||
showSelectAll:
|
||||
!widget.mediaFileService.mediaFile.requiresAuthentication,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
BestFriendsSelector(
|
||||
users: _bestFriends,
|
||||
selectedUserIds: widget.selectedUserIds,
|
||||
isRealTwonly: widget.isRealTwonly,
|
||||
updateStatus: updateStatus,
|
||||
groups: _bestFriends,
|
||||
selectedGroupIds: widget.selectedGroupIds,
|
||||
updateSelectedGroupIds: updateSelectedGroupIds,
|
||||
title: context.lang.shareImageBestFriends,
|
||||
showSelectAll:
|
||||
!widget.mediaFileService.mediaFile.requiresAuthentication,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
if (_otherUsers.isNotEmpty)
|
||||
|
|
@ -229,9 +226,8 @@ class _ShareImageView extends State<ShareImageView> {
|
|||
Expanded(
|
||||
child: UserList(
|
||||
List.from(_otherUsers),
|
||||
selectedUserIds: widget.selectedUserIds,
|
||||
isRealTwonly: widget.isRealTwonly,
|
||||
updateStatus: updateStatus,
|
||||
selectedGroupIds: widget.selectedGroupIds,
|
||||
updateSelectedGroupIds: updateSelectedGroupIds,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
@ -246,7 +242,7 @@ class _ShareImageView extends State<ShareImageView> {
|
|||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
FilledButton.icon(
|
||||
icon: imageBytes == null || sendingImage
|
||||
icon: !mediaStoreFutureReady || sendingImage
|
||||
? SizedBox(
|
||||
height: 12,
|
||||
width: 12,
|
||||
|
|
@ -257,50 +253,28 @@ class _ShareImageView extends State<ShareImageView> {
|
|||
)
|
||||
: const FaIcon(FontAwesomeIcons.solidPaperPlane),
|
||||
onPressed: () async {
|
||||
if (imageBytes == null || widget.selectedUserIds.isEmpty) {
|
||||
if (!mediaStoreFutureReady ||
|
||||
widget.selectedGroupIds.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
final err = await isAllowedToSend();
|
||||
if (!context.mounted) return;
|
||||
setState(() {
|
||||
sendingImage = true;
|
||||
});
|
||||
|
||||
if (err != null) {
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return SubscriptionView(
|
||||
redirectError: err,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
} else {
|
||||
setState(() {
|
||||
sendingImage = true;
|
||||
});
|
||||
await insertMediaFileInMessagesTable(
|
||||
widget.mediaFileService,
|
||||
widget.selectedGroupIds.toList(),
|
||||
);
|
||||
|
||||
await finalizeUpload(
|
||||
widget.mediaUploadId,
|
||||
widget.selectedUserIds.toList(),
|
||||
widget.isRealTwonly,
|
||||
widget.videoUploadHandler != null,
|
||||
widget.mirrorVideo,
|
||||
widget.maxShowTime,
|
||||
);
|
||||
|
||||
/// trigger the upload of the media file.
|
||||
unawaited(handleNextMediaUploadSteps(widget.mediaUploadId));
|
||||
|
||||
if (context.mounted) {
|
||||
Navigator.pop(context, true);
|
||||
// if (widget.preselectedUser != null) {
|
||||
// Navigator.pop(context, true);
|
||||
// } else {
|
||||
// Navigator.popUntil(context, (route) => route.isFirst, true);
|
||||
// globalUpdateOfHomeViewPageIndex(1);
|
||||
// }
|
||||
}
|
||||
if (context.mounted) {
|
||||
Navigator.pop(context, true);
|
||||
// if (widget.preselectedUser != null) {
|
||||
// Navigator.pop(context, true);
|
||||
// } else {
|
||||
// Navigator.popUntil(context, (route) => route.isFirst, true);
|
||||
// globalUpdateOfHomeViewPageIndex(1);
|
||||
// }
|
||||
}
|
||||
},
|
||||
style: ButtonStyle(
|
||||
|
|
@ -308,7 +282,7 @@ class _ShareImageView extends State<ShareImageView> {
|
|||
const EdgeInsets.symmetric(vertical: 10, horizontal: 30),
|
||||
),
|
||||
backgroundColor: WidgetStateProperty.all<Color>(
|
||||
imageBytes == null || widget.selectedUserIds.isEmpty
|
||||
mediaStoreFutureReady || widget.selectedGroupIds.isEmpty
|
||||
? Theme.of(context).colorScheme.secondary
|
||||
: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
|
|
@ -328,52 +302,42 @@ class _ShareImageView extends State<ShareImageView> {
|
|||
|
||||
class UserList extends StatelessWidget {
|
||||
const UserList(
|
||||
this.users, {
|
||||
required this.selectedUserIds,
|
||||
required this.updateStatus,
|
||||
required this.isRealTwonly,
|
||||
this.groups, {
|
||||
required this.selectedGroupIds,
|
||||
required this.updateSelectedGroupIds,
|
||||
super.key,
|
||||
});
|
||||
final void Function(int, bool) updateStatus;
|
||||
final List<Contact> users;
|
||||
final bool isRealTwonly;
|
||||
final HashSet<int> selectedUserIds;
|
||||
final void Function(String, bool) updateSelectedGroupIds;
|
||||
final List<Group> groups;
|
||||
final HashSet<String> selectedGroupIds;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Step 1: Sort the users alphabetically
|
||||
users
|
||||
groups
|
||||
.sort((a, b) => b.lastMessageExchange.compareTo(a.lastMessageExchange));
|
||||
|
||||
return ListView.builder(
|
||||
restorationId: 'new_message_users_list',
|
||||
itemCount: users.length,
|
||||
itemCount: groups.length,
|
||||
itemBuilder: (BuildContext context, int i) {
|
||||
final user = users[i];
|
||||
final flameCounter = getFlameCounterFromContact(user);
|
||||
final group = groups[i];
|
||||
return ListTile(
|
||||
title: Row(
|
||||
children: [
|
||||
if (isRealTwonly)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 1),
|
||||
child: VerifiedShield(user),
|
||||
),
|
||||
Text(getContactDisplayName(user)),
|
||||
if (flameCounter >= 1)
|
||||
FlameCounterWidget(
|
||||
user,
|
||||
flameCounter,
|
||||
prefix: true,
|
||||
),
|
||||
Text(group.groupName),
|
||||
FlameCounterWidget(
|
||||
groupId: group.groupId,
|
||||
prefix: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
leading: ContactAvatar(
|
||||
contact: user,
|
||||
leading: AvatarIcon(
|
||||
group: group,
|
||||
fontSize: 15,
|
||||
),
|
||||
trailing: Checkbox(
|
||||
value: selectedUserIds.contains(user.userId),
|
||||
value: selectedGroupIds.contains(group.groupId),
|
||||
side: WidgetStateBorderSide.resolveWith(
|
||||
(Set states) {
|
||||
if (states.contains(WidgetState.selected)) {
|
||||
|
|
@ -384,11 +348,14 @@ class UserList extends StatelessWidget {
|
|||
),
|
||||
onChanged: (bool? value) {
|
||||
if (value == null) return;
|
||||
updateStatus(user.userId, value);
|
||||
updateSelectedGroupIds(group.groupId, value);
|
||||
},
|
||||
),
|
||||
onTap: () {
|
||||
updateStatus(user.userId, !selectedUserIds.contains(user.userId));
|
||||
updateSelectedGroupIds(
|
||||
group.groupId,
|
||||
!selectedGroupIds.contains(group.groupId),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,15 +1,12 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:drift/drift.dart' hide Column;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||
import 'package:twonly/src/database/tables/messages_table.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/model/json/message_old.dart';
|
||||
import 'package:twonly/src/model/protobuf/push_notification/push_notification.pbserver.dart';
|
||||
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
|
||||
import 'package:twonly/src/services/api/messages.dart';
|
||||
import 'package:twonly/src/services/api/utils.dart';
|
||||
import 'package:twonly/src/services/notifications/pushkeys.notifications.dart';
|
||||
|
|
@ -17,8 +14,8 @@ import 'package:twonly/src/services/signal/session.signal.dart';
|
|||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/utils/storage.dart';
|
||||
import 'package:twonly/src/views/components/alert_dialog.dart';
|
||||
import 'package:twonly/src/views/components/avatar_icon.component.dart';
|
||||
import 'package:twonly/src/views/components/headline.dart';
|
||||
import 'package:twonly/src/views/components/initialsavatar.dart';
|
||||
|
||||
class AddNewUserView extends StatefulWidget {
|
||||
const AddNewUserView({super.key});
|
||||
|
|
@ -97,19 +94,18 @@ class _SearchUsernameView extends State<AddNewUserView> {
|
|||
|
||||
if (added > 0) {
|
||||
if (await createNewSignalSession(userdata)) {
|
||||
// before notifying the other party, add
|
||||
// 1. Setup notifications keys with the other user
|
||||
await setupNotificationWithUsers(
|
||||
forceContact: userdata.userId.toInt(),
|
||||
);
|
||||
await encryptAndSendMessageAsync(
|
||||
null,
|
||||
// 2. Then send user request
|
||||
await sendCipherText(
|
||||
userdata.userId.toInt(),
|
||||
MessageJson(
|
||||
kind: MessageKind.contactRequest,
|
||||
timestamp: DateTime.now(),
|
||||
content: MessageContent(),
|
||||
EncryptedContent(
|
||||
contactRequest: EncryptedContent_ContactRequest(
|
||||
type: EncryptedContent_ContactRequest_Type.REQUEST,
|
||||
),
|
||||
),
|
||||
pushNotification: PushNotification(kind: PushKind.contactRequest),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -198,7 +194,7 @@ class ContactsListView extends StatelessWidget {
|
|||
child: IconButton(
|
||||
icon: const FaIcon(FontAwesomeIcons.boxArchive, size: 15),
|
||||
onPressed: () async {
|
||||
const update = ContactsCompanion(archived: Value(true));
|
||||
const update = ContactsCompanion(requested: Value(false));
|
||||
await twonlyDB.contactsDao.updateContact(contact.userId, update);
|
||||
},
|
||||
),
|
||||
|
|
@ -234,17 +230,18 @@ class ContactsListView extends StatelessWidget {
|
|||
IconButton(
|
||||
icon: const Icon(Icons.check, color: Colors.green),
|
||||
onPressed: () async {
|
||||
const update = ContactsCompanion(accepted: Value(true));
|
||||
const update = ContactsCompanion(
|
||||
accepted: Value(true),
|
||||
requested: Value(false),
|
||||
);
|
||||
await twonlyDB.contactsDao.updateContact(contact.userId, update);
|
||||
await encryptAndSendMessageAsync(
|
||||
null,
|
||||
await sendCipherText(
|
||||
contact.userId,
|
||||
MessageJson(
|
||||
kind: MessageKind.acceptRequest,
|
||||
timestamp: DateTime.now(),
|
||||
content: MessageContent(),
|
||||
EncryptedContent(
|
||||
contactRequest: EncryptedContent_ContactRequest(
|
||||
type: EncryptedContent_ContactRequest_Type.ACCEPT,
|
||||
),
|
||||
),
|
||||
pushNotification: PushNotification(kind: PushKind.acceptRequest),
|
||||
);
|
||||
await notifyContactsAboutProfileChange();
|
||||
},
|
||||
|
|
@ -261,7 +258,7 @@ class ContactsListView extends StatelessWidget {
|
|||
final displayName = getContactDisplayName(contact);
|
||||
return ListTile(
|
||||
title: Text(displayName),
|
||||
leading: ContactAvatar(contact: contact),
|
||||
leading: AvatarIcon(contact: contact),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: contact.requested
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:cryptography_plus/cryptography_plus.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
|
@ -7,27 +6,18 @@ import 'package:flutter/services.dart';
|
|||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/model/json/userdata.dart';
|
||||
import 'package:twonly/src/providers/connection.provider.dart';
|
||||
import 'package:twonly/src/services/api/mediafiles/download.service.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/utils/storage.dart';
|
||||
import 'package:twonly/src/views/camera/camera_send_to_view.dart';
|
||||
import 'package:twonly/src/views/chats/add_new_user.view.dart';
|
||||
import 'package:twonly/src/views/chats/chat_list_components/backup_notice.card.dart';
|
||||
import 'package:twonly/src/views/chats/chat_list_components/connection_info.comp.dart';
|
||||
import 'package:twonly/src/views/chats/chat_list_components/feedback_btn.dart';
|
||||
import 'package:twonly/src/views/chats/chat_list_components/last_message_time.dart';
|
||||
import 'package:twonly/src/views/chats/chat_messages.view.dart';
|
||||
import 'package:twonly/src/views/chats/chat_messages_components/message_send_state_icon.dart';
|
||||
import 'package:twonly/src/views/chats/media_viewer.view.dart';
|
||||
import 'package:twonly/src/views/chats/chat_list_components/group_list_item.dart';
|
||||
import 'package:twonly/src/views/chats/start_new_chat.view.dart';
|
||||
import 'package:twonly/src/views/components/flame.dart';
|
||||
import 'package:twonly/src/views/components/initialsavatar.dart';
|
||||
import 'package:twonly/src/views/components/avatar_icon.component.dart';
|
||||
import 'package:twonly/src/views/components/notification_badge.dart';
|
||||
import 'package:twonly/src/views/components/user_context_menu.component.dart';
|
||||
import 'package:twonly/src/views/settings/help/changelog.view.dart';
|
||||
import 'package:twonly/src/views/settings/profile/profile.view.dart';
|
||||
import 'package:twonly/src/views/settings/settings_main.view.dart';
|
||||
|
|
@ -41,10 +31,9 @@ class ChatListView extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _ChatListViewState extends State<ChatListView> {
|
||||
late StreamSubscription<List<Contact>> _contactsSub;
|
||||
List<Contact> _contacts = [];
|
||||
List<Contact> _pinnedContacts = [];
|
||||
UserData? _user;
|
||||
late StreamSubscription<List<Group>> _contactsSub;
|
||||
List<Group> _groupsNotPinned = [];
|
||||
List<Group> _groupsPinned = [];
|
||||
|
||||
GlobalKey firstUserListItemKey = GlobalKey();
|
||||
GlobalKey searchForOtherUsers = GlobalKey();
|
||||
|
|
@ -58,11 +47,11 @@ class _ChatListViewState extends State<ChatListView> {
|
|||
}
|
||||
|
||||
Future<void> initAsync() async {
|
||||
final stream = twonlyDB.contactsDao.watchContactsForChatList();
|
||||
_contactsSub = stream.listen((contacts) {
|
||||
final stream = twonlyDB.groupsDao.watchGroups();
|
||||
_contactsSub = stream.listen((groups) {
|
||||
setState(() {
|
||||
_contacts = contacts.where((x) => !x.pinned).toList();
|
||||
_pinnedContacts = contacts.where((x) => x.pinned).toList();
|
||||
_groupsNotPinned = groups.where((x) => !x.pinned).toList();
|
||||
_groupsPinned = groups.where((x) => x.pinned).toList();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -71,21 +60,16 @@ class _ChatListViewState extends State<ChatListView> {
|
|||
if (!mounted) return;
|
||||
await showChatListTutorialSearchOtherUsers(context, searchForOtherUsers);
|
||||
if (!mounted) return;
|
||||
if (_contacts.isNotEmpty) {
|
||||
if (_groupsNotPinned.isNotEmpty) {
|
||||
await showChatListTutorialContextMenu(context, firstUserListItemKey);
|
||||
}
|
||||
});
|
||||
|
||||
final user = await getUser();
|
||||
if (user == null) return;
|
||||
setState(() {
|
||||
_user = user;
|
||||
});
|
||||
final changeLog = await rootBundle.loadString('CHANGELOG.md');
|
||||
final changeLogHash =
|
||||
(await compute(Sha256().hash, changeLog.codeUnits)).bytes;
|
||||
if (!user.hideChangeLog &&
|
||||
user.lastChangeLogHash.toString() != changeLogHash.toString()) {
|
||||
if (!gUser.hideChangeLog &&
|
||||
gUser.lastChangeLogHash.toString() != changeLogHash.toString()) {
|
||||
await updateUserdata((u) {
|
||||
u.lastChangeLogHash = changeLogHash;
|
||||
return u;
|
||||
|
|
@ -93,7 +77,7 @@ class _ChatListViewState extends State<ChatListView> {
|
|||
if (!mounted) return;
|
||||
// only show changelog to people who already have contacts
|
||||
// this prevents that this is shown directly after the user registered
|
||||
if (_contacts.isNotEmpty) {
|
||||
if (_groupsNotPinned.isNotEmpty) {
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
|
|
@ -133,12 +117,11 @@ class _ChatListViewState extends State<ChatListView> {
|
|||
},
|
||||
),
|
||||
);
|
||||
_user = await getUser();
|
||||
if (!mounted) return;
|
||||
setState(() {});
|
||||
setState(() {}); // gUser has updated
|
||||
},
|
||||
child: ContactAvatar(
|
||||
userData: _user,
|
||||
child: AvatarIcon(
|
||||
userData: gUser,
|
||||
fontSize: 14,
|
||||
color: context.color.onSurface.withAlpha(20),
|
||||
),
|
||||
|
|
@ -210,9 +193,8 @@ class _ChatListViewState extends State<ChatListView> {
|
|||
builder: (context) => const SettingsMainView(),
|
||||
),
|
||||
);
|
||||
_user = await getUser();
|
||||
if (!mounted) return;
|
||||
setState(() {});
|
||||
setState(() {}); // gUser may has changed...
|
||||
},
|
||||
icon: const FaIcon(FontAwesomeIcons.gear, size: 19),
|
||||
),
|
||||
|
|
@ -227,7 +209,7 @@ class _ChatListViewState extends State<ChatListView> {
|
|||
child: isConnected ? Container() : const ConnectionInfo(),
|
||||
),
|
||||
Positioned.fill(
|
||||
child: (_contacts.isEmpty && _pinnedContacts.isEmpty)
|
||||
child: (_groupsNotPinned.isEmpty && _groupsPinned.isEmpty)
|
||||
? Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
|
|
@ -252,9 +234,9 @@ class _ChatListViewState extends State<ChatListView> {
|
|||
await Future.delayed(const Duration(seconds: 1));
|
||||
},
|
||||
child: ListView.builder(
|
||||
itemCount: _pinnedContacts.length +
|
||||
(_pinnedContacts.isNotEmpty ? 1 : 0) +
|
||||
_contacts.length +
|
||||
itemCount: _groupsPinned.length +
|
||||
(_groupsPinned.isNotEmpty ? 1 : 0) +
|
||||
_groupsNotPinned.length +
|
||||
1,
|
||||
itemBuilder: (context, index) {
|
||||
if (index == 0) {
|
||||
|
|
@ -262,11 +244,11 @@ class _ChatListViewState extends State<ChatListView> {
|
|||
}
|
||||
index -= 1;
|
||||
// Check if the index is for the pinned users
|
||||
if (index < _pinnedContacts.length) {
|
||||
final contact = _pinnedContacts[index];
|
||||
return UserListItem(
|
||||
key: ValueKey(contact.userId),
|
||||
user: contact,
|
||||
if (index < _groupsPinned.length) {
|
||||
final group = _groupsPinned[index];
|
||||
return GroupListItem(
|
||||
key: ValueKey(group.groupId),
|
||||
group: group,
|
||||
firstUserListItemKey: (index == 0 || index == 1)
|
||||
? firstUserListItemKey
|
||||
: null,
|
||||
|
|
@ -274,21 +256,21 @@ class _ChatListViewState extends State<ChatListView> {
|
|||
}
|
||||
|
||||
// If there are pinned users, account for the Divider
|
||||
var adjustedIndex = index - _pinnedContacts.length;
|
||||
if (_pinnedContacts.isNotEmpty && adjustedIndex == 0) {
|
||||
var adjustedIndex = index - _groupsPinned.length;
|
||||
if (_groupsPinned.isNotEmpty && adjustedIndex == 0) {
|
||||
return const Divider();
|
||||
}
|
||||
|
||||
// Adjust the index for the contacts list
|
||||
adjustedIndex -= (_pinnedContacts.isNotEmpty ? 1 : 0);
|
||||
adjustedIndex -= (_groupsPinned.isNotEmpty ? 1 : 0);
|
||||
|
||||
// Get the contacts that are not pinned
|
||||
final contact = _contacts.elementAt(
|
||||
final group = _groupsNotPinned.elementAt(
|
||||
adjustedIndex,
|
||||
);
|
||||
return UserListItem(
|
||||
key: ValueKey(contact.userId),
|
||||
user: contact,
|
||||
return GroupListItem(
|
||||
key: ValueKey(group.groupId),
|
||||
group: group,
|
||||
firstUserListItemKey:
|
||||
(index == 0) ? firstUserListItemKey : null,
|
||||
);
|
||||
|
|
@ -317,219 +299,3 @@ class _ChatListViewState extends State<ChatListView> {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
class UserListItem extends StatefulWidget {
|
||||
const UserListItem({
|
||||
required this.user,
|
||||
required this.firstUserListItemKey,
|
||||
super.key,
|
||||
});
|
||||
final Contact user;
|
||||
final GlobalKey? firstUserListItemKey;
|
||||
|
||||
@override
|
||||
State<UserListItem> createState() => _UserListItem();
|
||||
}
|
||||
|
||||
class _UserListItem extends State<UserListItem> {
|
||||
MessageSendState state = MessageSendState.send;
|
||||
Message? currentMessage;
|
||||
|
||||
List<Message> messagesNotOpened = [];
|
||||
late StreamSubscription<List<Message>> messagesNotOpenedStream;
|
||||
|
||||
List<Message> lastMessages = [];
|
||||
late StreamSubscription<List<Message>> lastMessageStream;
|
||||
|
||||
List<Message> previewMessages = [];
|
||||
bool hasNonOpenedMediaFile = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
initStreams();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
messagesNotOpenedStream.cancel();
|
||||
lastMessageStream.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void initStreams() {
|
||||
lastMessageStream = twonlyDB.messagesDao
|
||||
.watchLastMessage(widget.user.userId)
|
||||
.listen((update) {
|
||||
updateState(update, messagesNotOpened);
|
||||
});
|
||||
|
||||
messagesNotOpenedStream = twonlyDB.messagesDao
|
||||
.watchMessageNotOpened(widget.user.userId)
|
||||
.listen((update) {
|
||||
updateState(lastMessages, update);
|
||||
});
|
||||
}
|
||||
|
||||
void updateState(
|
||||
List<Message> newLastMessages,
|
||||
List<Message> newMessagesNotOpened,
|
||||
) {
|
||||
if (newLastMessages.isEmpty) {
|
||||
// there are no messages at all
|
||||
currentMessage = null;
|
||||
previewMessages = [];
|
||||
} else if (newMessagesNotOpened.isEmpty) {
|
||||
// there are no not opened messages show just the last message in the table
|
||||
currentMessage = newLastMessages.last;
|
||||
previewMessages = newLastMessages;
|
||||
} else {
|
||||
// filter first for received messages
|
||||
final receivedMessages =
|
||||
newMessagesNotOpened.where((x) => x.messageOtherId != null).toList();
|
||||
|
||||
if (receivedMessages.isNotEmpty) {
|
||||
previewMessages = receivedMessages;
|
||||
currentMessage = receivedMessages.first;
|
||||
} else {
|
||||
previewMessages = newMessagesNotOpened;
|
||||
currentMessage = newMessagesNotOpened.first;
|
||||
}
|
||||
}
|
||||
|
||||
final msgs =
|
||||
previewMessages.where((x) => x.kind == MessageKind.media).toList();
|
||||
if (msgs.isNotEmpty &&
|
||||
msgs.first.kind == MessageKind.media &&
|
||||
msgs.first.messageOtherId != null &&
|
||||
msgs.first.openedAt == null) {
|
||||
hasNonOpenedMediaFile = true;
|
||||
} else {
|
||||
hasNonOpenedMediaFile = false;
|
||||
}
|
||||
|
||||
lastMessages = newLastMessages;
|
||||
messagesNotOpened = newMessagesNotOpened;
|
||||
setState(() {
|
||||
// sets lastMessages, messagesNotOpened and currentMessage
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> onTap() async {
|
||||
if (currentMessage == null) {
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return CameraSendToView(widget.user);
|
||||
},
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasNonOpenedMediaFile) {
|
||||
final msgs =
|
||||
previewMessages.where((x) => x.kind == MessageKind.media).toList();
|
||||
switch (msgs.first.downloadState) {
|
||||
case DownloadState.pending:
|
||||
await startDownloadMedia(msgs.first, true);
|
||||
return;
|
||||
case DownloadState.downloaded:
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return MediaViewerView(widget.user);
|
||||
},
|
||||
),
|
||||
);
|
||||
return;
|
||||
case DownloadState.downloading:
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!mounted) return;
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return ChatMessagesView(widget.user);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final flameCounter = getFlameCounterFromContact(widget.user);
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 50,
|
||||
child: SizedBox(
|
||||
key: widget.firstUserListItemKey,
|
||||
height: 20,
|
||||
width: 20,
|
||||
),
|
||||
),
|
||||
UserContextMenu(
|
||||
contact: widget.user,
|
||||
child: ListTile(
|
||||
title: Text(
|
||||
getContactDisplayName(widget.user),
|
||||
),
|
||||
subtitle: (widget.user.deleted)
|
||||
? Text(context.lang.userDeletedAccount)
|
||||
: (currentMessage == null)
|
||||
? Text(context.lang.chatsTapToSend)
|
||||
: Row(
|
||||
children: [
|
||||
MessageSendStateIcon(previewMessages),
|
||||
const Text('•'),
|
||||
const SizedBox(width: 5),
|
||||
if (currentMessage != null)
|
||||
LastMessageTime(message: currentMessage!),
|
||||
if (flameCounter > 0)
|
||||
FlameCounterWidget(
|
||||
widget.user,
|
||||
flameCounter,
|
||||
prefix: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
leading: ContactAvatar(contact: widget.user),
|
||||
trailing: (widget.user.deleted)
|
||||
? null
|
||||
: IconButton(
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
if (hasNonOpenedMediaFile) {
|
||||
return ChatMessagesView(widget.user);
|
||||
} else {
|
||||
return CameraSendToView(widget.user);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: FaIcon(
|
||||
hasNonOpenedMediaFile
|
||||
? FontAwesomeIcons.solidComments
|
||||
: FontAwesomeIcons.camera,
|
||||
color: context.color.outline.withAlpha(150),
|
||||
),
|
||||
),
|
||||
onTap: onTap,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
227
lib/src/views/chats/chat_list_components/group_list_item.dart
Normal file
227
lib/src/views/chats/chat_list_components/group_list_item.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
|
||||
|
|
@ -21,10 +22,13 @@ class _LastMessageTimeState extends State<LastMessageTime> {
|
|||
void initState() {
|
||||
super.initState();
|
||||
// Change the color every 200 milliseconds
|
||||
updateTime = Timer.periodic(const Duration(milliseconds: 500), (timer) {
|
||||
updateTime =
|
||||
Timer.periodic(const Duration(milliseconds: 500), (timer) async {
|
||||
final lastAction = await twonlyDB.messagesDao
|
||||
.getLastMessageAction(widget.message.messageId);
|
||||
setState(() {
|
||||
lastMessageInSeconds = DateTime.now()
|
||||
.difference(widget.message.openedAt ?? widget.message.sendAt)
|
||||
.difference(lastAction?.actionAt ?? widget.message.createdAt)
|
||||
.inSeconds;
|
||||
if (lastMessageInSeconds < 0) {
|
||||
lastMessageInSeconds = 0;
|
||||
|
|
|
|||
|
|
@ -1,19 +1,13 @@
|
|||
import 'dart:async';
|
||||
import 'dart:collection';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:pie_menu/pie_menu.dart';
|
||||
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||
import 'package:twonly/src/database/tables/messages_table.dart';
|
||||
import 'package:twonly/src/database/tables/messages.table.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/model/json/message_old.dart';
|
||||
import 'package:twonly/src/model/memory_item.model.dart';
|
||||
import 'package:twonly/src/model/protobuf/push_notification/push_notification.pb.dart';
|
||||
import 'package:twonly/src/services/api/messages.dart';
|
||||
import 'package:twonly/src/services/notifications/background.notifications.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
|
|
@ -21,25 +15,17 @@ import 'package:twonly/src/views/camera/camera_send_to_view.dart';
|
|||
import 'package:twonly/src/views/chats/chat_messages_components/chat_date_chip.dart';
|
||||
import 'package:twonly/src/views/chats/chat_messages_components/chat_list_entry.dart';
|
||||
import 'package:twonly/src/views/chats/chat_messages_components/response_container.dart';
|
||||
import 'package:twonly/src/views/components/animate_icon.dart';
|
||||
import 'package:twonly/src/views/components/initialsavatar.dart';
|
||||
import 'package:twonly/src/views/components/user_context_menu.component.dart';
|
||||
import 'package:twonly/src/views/components/verified_shield.dart';
|
||||
import 'package:twonly/src/views/components/avatar_icon.component.dart';
|
||||
import 'package:twonly/src/views/contact/contact.view.dart';
|
||||
import 'package:twonly/src/views/groups/group.view.dart';
|
||||
import 'package:twonly/src/views/tutorial/tutorials.dart';
|
||||
|
||||
Color getMessageColor(Message message) {
|
||||
return (message.messageOtherId == null)
|
||||
return (message.senderId == null)
|
||||
? const Color.fromARGB(255, 58, 136, 102)
|
||||
: const Color.fromARGB(233, 68, 137, 255);
|
||||
}
|
||||
|
||||
class ChatMessage {
|
||||
ChatMessage({required this.message, required this.responseTo});
|
||||
final Message message;
|
||||
final Message? responseTo;
|
||||
}
|
||||
|
||||
class ChatItem {
|
||||
const ChatItem._({this.message, this.date, this.time});
|
||||
factory ChatItem.date(DateTime date) {
|
||||
|
|
@ -48,10 +34,10 @@ class ChatItem {
|
|||
factory ChatItem.time(DateTime time) {
|
||||
return ChatItem._(time: time);
|
||||
}
|
||||
factory ChatItem.message(ChatMessage message) {
|
||||
factory ChatItem.message(Message message) {
|
||||
return ChatItem._(message: message);
|
||||
}
|
||||
final ChatMessage? message;
|
||||
final Message? message;
|
||||
final DateTime? date;
|
||||
final DateTime? time;
|
||||
bool get isMessage => message != null;
|
||||
|
|
@ -72,14 +58,13 @@ class ChatMessagesView extends StatefulWidget {
|
|||
class _ChatMessagesViewState extends State<ChatMessagesView> {
|
||||
TextEditingController newMessageController = TextEditingController();
|
||||
HashSet<int> alreadyReportedOpened = HashSet<int>();
|
||||
late Contact user;
|
||||
late Group group;
|
||||
String currentInputText = '';
|
||||
late StreamSubscription<Contact?> userSub;
|
||||
late StreamSubscription<Group?> userSub;
|
||||
late StreamSubscription<List<Message>> messageSub;
|
||||
List<ChatItem> messages = [];
|
||||
List<MemoryItem> galleryItems = [];
|
||||
Map<int, List<Message>> emojiReactionsToMessageId = {};
|
||||
Message? responseToMessage;
|
||||
Message? quotesMessage;
|
||||
GlobalKey verifyShieldKey = GlobalKey();
|
||||
late FocusNode textFieldFocus;
|
||||
Timer? tutorial;
|
||||
|
|
@ -89,7 +74,7 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
|||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
user = widget.contact;
|
||||
group = widget.group;
|
||||
textFieldFocus = FocusNode();
|
||||
initStreams();
|
||||
|
||||
|
|
@ -110,118 +95,59 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
|||
}
|
||||
|
||||
Future<void> initStreams() async {
|
||||
await twonlyDB.messagesDao.removeOldMessages();
|
||||
final contact = twonlyDB.contactsDao.watchContact(widget.contact.userId);
|
||||
userSub = contact.listen((contact) {
|
||||
if (contact == null) return;
|
||||
final groupStream = twonlyDB.groupsDao.watchGroup(group.groupId);
|
||||
userSub = groupStream.listen((newGroup) {
|
||||
if (newGroup == null) return;
|
||||
setState(() {
|
||||
user = contact;
|
||||
group = newGroup;
|
||||
});
|
||||
});
|
||||
|
||||
final msgStream =
|
||||
twonlyDB.messagesDao.watchAllMessagesFrom(widget.contact.userId);
|
||||
final msgStream = twonlyDB.messagesDao.watchByGroupId(group.groupId);
|
||||
messageSub = msgStream.listen((newMessages) async {
|
||||
// if (!context.mounted) return;
|
||||
if (Platform.isAndroid) {
|
||||
await flutterLocalNotificationsPlugin.cancel(widget.contact.userId);
|
||||
} else {
|
||||
await flutterLocalNotificationsPlugin.cancelAll();
|
||||
}
|
||||
await flutterLocalNotificationsPlugin.cancelAll();
|
||||
|
||||
final chatItems = <ChatItem>[];
|
||||
final storedMediaFiles = <Message>[];
|
||||
|
||||
DateTime? lastDate;
|
||||
final tmpEmojiReactionsToMessageId = <int, List<Message>>{};
|
||||
|
||||
// only send openedMessage to one text message, as receiver will then set all as read...
|
||||
List<int> openedTextMessageOtherIds;
|
||||
|
||||
final messageOtherMessageIdToMyMessageId = <int, int>{};
|
||||
final messageIdToMessage = <int, Message>{};
|
||||
|
||||
/// there is probably a better way...
|
||||
for (final msg in newMessages) {
|
||||
if (msg.messageOtherId != null) {
|
||||
messageOtherMessageIdToMyMessageId[msg.messageOtherId!] =
|
||||
msg.messageId;
|
||||
}
|
||||
messageIdToMessage[msg.messageId] = msg;
|
||||
}
|
||||
final openedMessages = <int, List<String>>{};
|
||||
|
||||
for (final msg in newMessages) {
|
||||
if (msg.kind == MessageKind.textMessage &&
|
||||
msg.messageOtherId != null &&
|
||||
msg.openedAt == null &&
|
||||
(openedTextMessageOtherIds == null ||
|
||||
openedTextMessageOtherIds < msg.messageOtherId!)) {
|
||||
openedTextMessageOtherIds.add(msg.messageOtherId);
|
||||
if (msg.type == MessageType.text &&
|
||||
msg.senderId != null &&
|
||||
msg.openedAt == null) {
|
||||
openedMessages[msg.senderId!]!.add(msg.messageId);
|
||||
}
|
||||
|
||||
Message? responseTo;
|
||||
|
||||
if (msg.kind == MessageKind.media && msg.mediaStored) {
|
||||
if (msg.type == MessageType.media && msg.mediaStored) {
|
||||
storedMediaFiles.add(msg);
|
||||
}
|
||||
|
||||
final responseId = msg.responseToMessageId ??
|
||||
messageOtherMessageIdToMyMessageId[msg.responseToOtherMessageId];
|
||||
|
||||
var isReaction = false;
|
||||
if (responseId != null) {
|
||||
responseTo = messageIdToMessage[responseId];
|
||||
final content = MessageContent.fromJson(
|
||||
msg.kind,
|
||||
jsonDecode(msg.contentJson!) as Map,
|
||||
);
|
||||
if (content is TextMessageContent) {
|
||||
if (isEmoji(content.text)) {
|
||||
isReaction = true;
|
||||
tmpEmojiReactionsToMessageId
|
||||
.putIfAbsent(responseId, () => [])
|
||||
.add(msg);
|
||||
}
|
||||
}
|
||||
if (msg.kind == MessageKind.reopenedMedia) {
|
||||
isReaction = true;
|
||||
tmpEmojiReactionsToMessageId
|
||||
.putIfAbsent(responseId, () => [])
|
||||
.add(msg);
|
||||
}
|
||||
}
|
||||
if (!isReaction) {
|
||||
if (lastDate == null ||
|
||||
msg.sendAt.day != lastDate.day ||
|
||||
msg.sendAt.month != lastDate.month ||
|
||||
msg.sendAt.year != lastDate.year) {
|
||||
chatItems.add(ChatItem.date(msg.sendAt));
|
||||
lastDate = msg.sendAt;
|
||||
} else if (msg.sendAt.difference(lastDate).inMinutes >= 20) {
|
||||
chatItems.add(ChatItem.time(msg.sendAt));
|
||||
lastDate = msg.sendAt;
|
||||
}
|
||||
chatItems.add(
|
||||
ChatItem.message(
|
||||
ChatMessage(
|
||||
message: msg,
|
||||
responseTo: responseTo,
|
||||
),
|
||||
),
|
||||
);
|
||||
if (lastDate == null ||
|
||||
msg.createdAt.day != lastDate.day ||
|
||||
msg.createdAt.month != lastDate.month ||
|
||||
msg.createdAt.year != lastDate.year) {
|
||||
chatItems.add(ChatItem.date(msg.createdAt));
|
||||
lastDate = msg.createdAt;
|
||||
} else if (msg.createdAt.difference(lastDate).inMinutes >= 20) {
|
||||
chatItems.add(ChatItem.time(msg.createdAt));
|
||||
lastDate = msg.createdAt;
|
||||
}
|
||||
chatItems.add(ChatItem.message(msg));
|
||||
}
|
||||
|
||||
if (openedTextMessageOtherIds.isNotEmpty) {
|
||||
for (final contactId in openedMessages.keys) {
|
||||
await notifyContactAboutOpeningMessage(
|
||||
widget.contact.userId,
|
||||
openedTextMessageOtherIds,
|
||||
contactId,
|
||||
openedMessages[contactId]!,
|
||||
);
|
||||
}
|
||||
|
||||
await twonlyDB.messagesDao
|
||||
.openedAllNonMediaMessages(widget.contact.userId);
|
||||
await twonlyDB.messagesDao.openedAllTextMessages(widget.group.groupId);
|
||||
|
||||
setState(() {
|
||||
emojiReactionsToMessageId = tmpEmojiReactionsToMessageId;
|
||||
messages = chatItems.reversed.toList();
|
||||
});
|
||||
|
||||
|
|
@ -234,33 +160,21 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
|||
Future<void> _sendMessage() async {
|
||||
if (newMessageController.text == '') return;
|
||||
|
||||
await sendTextMessage(
|
||||
user.userId,
|
||||
TextMessageContent(
|
||||
text: newMessageController.text,
|
||||
responseToMessageId: responseToMessage?.messageOtherId,
|
||||
responseToOtherMessageId: responseToMessage?.messageId,
|
||||
),
|
||||
PushNotification(
|
||||
kind: (responseToMessage == null)
|
||||
? PushKind.text
|
||||
: (isEmoji(newMessageController.text))
|
||||
? PushKind.reaction
|
||||
: PushKind.response,
|
||||
reactionContent: (isEmoji(newMessageController.text))
|
||||
? newMessageController.text
|
||||
: null,
|
||||
),
|
||||
await insertAndSendTextMessage(
|
||||
group.groupId,
|
||||
newMessageController.text,
|
||||
quotesMessage?.messageId,
|
||||
);
|
||||
|
||||
newMessageController.clear();
|
||||
currentInputText = '';
|
||||
responseToMessage = null;
|
||||
quotesMessage = null;
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
Future<void> scrollToMessage(int messageId) async {
|
||||
Future<void> scrollToMessage(String messageId) async {
|
||||
final index = messages.indexWhere(
|
||||
(x) => x.isMessage && x.message!.message.messageId == messageId,
|
||||
(x) => x.isMessage && x.message!.messageId == messageId,
|
||||
);
|
||||
if (index == -1) return;
|
||||
setState(() {
|
||||
|
|
@ -286,20 +200,34 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
|||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return ContactView(widget.contact.userId);
|
||||
},
|
||||
),
|
||||
);
|
||||
onTap: () async {
|
||||
if (widget.group.isDirectChat) {
|
||||
final member = await twonlyDB.groupsDao
|
||||
.getGroupMembers(widget.group.groupId);
|
||||
if (!context.mounted) return;
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return ContactView(member.first.contactId);
|
||||
},
|
||||
),
|
||||
);
|
||||
} else {
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return GroupView(widget.group);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
ContactAvatar(
|
||||
contact: user,
|
||||
AvatarIcon(
|
||||
group: group,
|
||||
fontSize: 19,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
|
|
@ -308,10 +236,10 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
|||
color: Colors.transparent,
|
||||
child: Row(
|
||||
children: [
|
||||
Text(getContactDisplayName(user)),
|
||||
Text(group.groupName),
|
||||
const SizedBox(width: 10),
|
||||
if (user.verified)
|
||||
VerifiedShield(key: verifyShieldKey, user),
|
||||
// if (group.verified)
|
||||
// VerifiedShield(key: verifyShieldKey, group),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
@ -345,7 +273,7 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
|||
return Transform.translate(
|
||||
offset: Offset(
|
||||
(focusedScrollItem == i)
|
||||
? (chatMessage.message.messageOtherId == null)
|
||||
? (chatMessage.quotesMessageId == null)
|
||||
? -8
|
||||
: 8
|
||||
: 0,
|
||||
|
|
@ -354,19 +282,15 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
|||
child: Transform.scale(
|
||||
scale: (focusedScrollItem == i) ? 1.05 : 1,
|
||||
child: ChatListEntry(
|
||||
key:
|
||||
Key(chatMessage.message.messageId.toString()),
|
||||
key: Key(chatMessage.messageId),
|
||||
chatMessage,
|
||||
user,
|
||||
group,
|
||||
galleryItems,
|
||||
isLastMessageFromSameUser(messages, i),
|
||||
emojiReactionsToMessageId[
|
||||
chatMessage.message.messageId] ??
|
||||
[],
|
||||
scrollToMessage: scrollToMessage,
|
||||
onResponseTriggered: () {
|
||||
setState(() {
|
||||
responseToMessage = chatMessage.message;
|
||||
quotesMessage = chatMessage;
|
||||
});
|
||||
textFieldFocus.requestFocus();
|
||||
},
|
||||
|
|
@ -377,7 +301,7 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
|||
},
|
||||
),
|
||||
),
|
||||
if (responseToMessage != null && !user.deleted)
|
||||
if (quotesMessage != null)
|
||||
Container(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 20,
|
||||
|
|
@ -388,15 +312,15 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
|||
children: [
|
||||
Expanded(
|
||||
child: ResponsePreview(
|
||||
message: responseToMessage!,
|
||||
message: quotesMessage,
|
||||
showBorder: true,
|
||||
contact: user,
|
||||
group: group,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
responseToMessage = null;
|
||||
quotesMessage = null;
|
||||
});
|
||||
},
|
||||
icon: const FaIcon(
|
||||
|
|
@ -415,50 +339,48 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
|||
top: 10,
|
||||
),
|
||||
child: Row(
|
||||
children: (user.deleted)
|
||||
? []
|
||||
: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: newMessageController,
|
||||
focusNode: textFieldFocus,
|
||||
keyboardType: TextInputType.multiline,
|
||||
maxLines: 4,
|
||||
minLines: 1,
|
||||
onChanged: (value) {
|
||||
currentInputText = value;
|
||||
setState(() {});
|
||||
},
|
||||
onSubmitted: (_) {
|
||||
_sendMessage();
|
||||
},
|
||||
decoration: inputTextMessageDeco(context),
|
||||
),
|
||||
),
|
||||
if (currentInputText != '')
|
||||
IconButton(
|
||||
padding: const EdgeInsets.all(15),
|
||||
icon: const FaIcon(
|
||||
FontAwesomeIcons.solidPaperPlane,
|
||||
),
|
||||
onPressed: _sendMessage,
|
||||
)
|
||||
else
|
||||
IconButton(
|
||||
icon: const FaIcon(FontAwesomeIcons.camera),
|
||||
padding: const EdgeInsets.all(15),
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return CameraSendToView(widget.contact);
|
||||
},
|
||||
),
|
||||
);
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: newMessageController,
|
||||
focusNode: textFieldFocus,
|
||||
keyboardType: TextInputType.multiline,
|
||||
maxLines: 4,
|
||||
minLines: 1,
|
||||
onChanged: (value) {
|
||||
currentInputText = value;
|
||||
setState(() {});
|
||||
},
|
||||
onSubmitted: (_) {
|
||||
_sendMessage();
|
||||
},
|
||||
decoration: inputTextMessageDeco(context),
|
||||
),
|
||||
),
|
||||
if (currentInputText != '')
|
||||
IconButton(
|
||||
padding: const EdgeInsets.all(15),
|
||||
icon: const FaIcon(
|
||||
FontAwesomeIcons.solidPaperPlane,
|
||||
),
|
||||
onPressed: _sendMessage,
|
||||
)
|
||||
else
|
||||
IconButton(
|
||||
icon: const FaIcon(FontAwesomeIcons.camera),
|
||||
padding: const EdgeInsets.all(15),
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return CameraSendToView(widget.group);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
@ -479,11 +401,11 @@ bool isLastMessageFromSameUser(List<ChatItem> messages, int index) {
|
|||
final currentMessage = messages[index];
|
||||
|
||||
if (lastMessage.isMessage && currentMessage.isMessage) {
|
||||
// Check if both messages have the same messageOtherId (or both are null)
|
||||
return (lastMessage.message!.message.messageOtherId == null &&
|
||||
currentMessage.message!.message.messageOtherId == null) ||
|
||||
(lastMessage.message!.message.messageOtherId != null &&
|
||||
currentMessage.message!.message.messageOtherId != null);
|
||||
// Check if both messages have the same quotesMessageId (or both are null)
|
||||
return (lastMessage.message!.quotesMessageId == null &&
|
||||
currentMessage.message!.quotesMessageId == null) ||
|
||||
(lastMessage.message!.quotesMessageId != null &&
|
||||
currentMessage.message!.quotesMessageId != null);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:twonly/src/database/tables/messages.table.dart'
|
||||
hide MessageActions;
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/model/json/message_old.dart';
|
||||
import 'package:twonly/src/model/memory_item.model.dart';
|
||||
import 'package:twonly/src/views/chats/chat_messages.view.dart';
|
||||
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
||||
import 'package:twonly/src/views/chats/chat_messages_components/chat_media_entry.dart';
|
||||
import 'package:twonly/src/views/chats/chat_messages_components/chat_reaction_row.dart';
|
||||
import 'package:twonly/src/views/chats/chat_messages_components/chat_text_entry.dart';
|
||||
|
|
@ -14,21 +13,19 @@ import 'package:twonly/src/views/chats/chat_messages_components/response_contain
|
|||
|
||||
class ChatListEntry extends StatefulWidget {
|
||||
const ChatListEntry(
|
||||
this.msg,
|
||||
this.contact,
|
||||
this.message,
|
||||
this.group,
|
||||
this.galleryItems,
|
||||
this.lastMessageFromSameUser,
|
||||
this.otherReactions, {
|
||||
this.lastMessageFromSameUser, {
|
||||
required this.onResponseTriggered,
|
||||
required this.scrollToMessage,
|
||||
super.key,
|
||||
});
|
||||
final ChatMessage msg;
|
||||
final Contact contact;
|
||||
final Message message;
|
||||
final Group group;
|
||||
final bool lastMessageFromSameUser;
|
||||
final List<Message> otherReactions;
|
||||
final List<MemoryItem> galleryItems;
|
||||
final void Function(int) scrollToMessage;
|
||||
final void Function(String) scrollToMessage;
|
||||
final void Function() onResponseTriggered;
|
||||
|
||||
@override
|
||||
|
|
@ -36,26 +33,22 @@ class ChatListEntry extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _ChatListEntryState extends State<ChatListEntry> {
|
||||
MessageContent? content;
|
||||
String? textMessage;
|
||||
MediaFileService? mediaService;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
initAsync();
|
||||
super.initState();
|
||||
final msgContent = MessageContent.fromJson(
|
||||
widget.msg.message.kind,
|
||||
jsonDecode(widget.msg.message.contentJson!) as Map,
|
||||
);
|
||||
if (msgContent is TextMessageContent) {
|
||||
textMessage = msgContent.text;
|
||||
}
|
||||
content = msgContent;
|
||||
}
|
||||
|
||||
Future<void> initAsync() async {
|
||||
mediaService = await MediaFileService.fromMediaId(widget.message.messageId);
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (content == null) return Container();
|
||||
final right = widget.msg.message.messageOtherId == null;
|
||||
final right = widget.message.senderId == null;
|
||||
|
||||
return Align(
|
||||
alignment: right ? Alignment.centerRight : Alignment.centerLeft,
|
||||
|
|
@ -64,7 +57,7 @@ class _ChatListEntryState extends State<ChatListEntry> {
|
|||
? const EdgeInsets.only(top: 5, right: 10, left: 10)
|
||||
: const EdgeInsets.only(top: 5, bottom: 20, right: 10, left: 10),
|
||||
child: MessageContextMenu(
|
||||
message: widget.msg.message,
|
||||
message: widget.message,
|
||||
onResponseTriggered: widget.onResponseTriggered,
|
||||
child: Column(
|
||||
mainAxisAlignment:
|
||||
|
|
@ -73,36 +66,36 @@ class _ChatListEntryState extends State<ChatListEntry> {
|
|||
right ? CrossAxisAlignment.end : CrossAxisAlignment.start,
|
||||
children: [
|
||||
MessageActions(
|
||||
message: widget.msg.message,
|
||||
message: widget.message,
|
||||
onResponseTriggered: widget.onResponseTriggered,
|
||||
child: Stack(
|
||||
alignment:
|
||||
right ? Alignment.centerRight : Alignment.centerLeft,
|
||||
children: [
|
||||
ResponseContainer(
|
||||
msg: widget.msg,
|
||||
contact: widget.contact,
|
||||
msg: widget.message,
|
||||
group: widget.group,
|
||||
mediaService: mediaService,
|
||||
scrollToMessage: widget.scrollToMessage,
|
||||
child: (textMessage != null)
|
||||
child: (widget.message.type == MessageType.text)
|
||||
? ChatTextEntry(
|
||||
message: widget.msg,
|
||||
text: textMessage!,
|
||||
hasReaction: widget.otherReactions.isNotEmpty,
|
||||
message: widget.message,
|
||||
)
|
||||
: ChatMediaEntry(
|
||||
message: widget.msg.message,
|
||||
contact: widget.contact,
|
||||
galleryItems: widget.galleryItems,
|
||||
content: content!,
|
||||
),
|
||||
: (mediaService == null)
|
||||
? null
|
||||
: ChatMediaEntry(
|
||||
message: widget.message,
|
||||
group: widget.group,
|
||||
mediaService: mediaService!,
|
||||
galleryItems: widget.galleryItems,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: 5,
|
||||
left: 5,
|
||||
right: 5,
|
||||
child: ReactionRow(
|
||||
otherReactions: widget.otherReactions,
|
||||
message: widget.msg.message,
|
||||
message: widget.message,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -1,35 +1,33 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:drift/drift.dart' show Value;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/database/tables/messages_table.dart';
|
||||
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
||||
import 'package:twonly/src/database/tables/messages.table.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/model/json/message_old.dart';
|
||||
import 'package:twonly/src/model/memory_item.model.dart';
|
||||
import 'package:twonly/src/model/protobuf/push_notification/push_notification.pbserver.dart';
|
||||
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'
|
||||
hide Message;
|
||||
import 'package:twonly/src/services/api/mediafiles/download.service.dart'
|
||||
as received;
|
||||
import 'package:twonly/src/services/api/messages.dart';
|
||||
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/views/camera/share_image_editor_view.dart';
|
||||
import 'package:twonly/src/views/chats/chat_messages_components/in_chat_media_viewer.dart';
|
||||
import 'package:twonly/src/views/chats/media_viewer.view.dart';
|
||||
import 'package:twonly/src/views/tutorial/tutorials.dart';
|
||||
|
||||
class ChatMediaEntry extends StatefulWidget {
|
||||
const ChatMediaEntry({
|
||||
required this.message,
|
||||
required this.contact,
|
||||
required this.content,
|
||||
required this.group,
|
||||
required this.galleryItems,
|
||||
required this.mediaService,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final Message message;
|
||||
final Contact contact;
|
||||
final MessageContent content;
|
||||
final Group group;
|
||||
final List<MemoryItem> galleryItems;
|
||||
final MediaFileService mediaService;
|
||||
|
||||
@override
|
||||
State<ChatMediaEntry> createState() => _ChatMediaEntryState();
|
||||
|
|
@ -39,97 +37,58 @@ class _ChatMediaEntryState extends State<ChatMediaEntry> {
|
|||
GlobalKey reopenMediaFile = GlobalKey();
|
||||
bool canBeReopened = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
unawaited(checkIfTutorialCanBeShown());
|
||||
}
|
||||
|
||||
Future<void> checkIfTutorialCanBeShown() async {
|
||||
if (widget.message.openedAt == null &&
|
||||
widget.message.messageOtherId != null ||
|
||||
widget.message.mediaStored) {
|
||||
return;
|
||||
}
|
||||
final content = getMediaContent(widget.message);
|
||||
if (content == null ||
|
||||
content.isRealTwonly ||
|
||||
content.maxShowTime != gMediaShowInfinite) {
|
||||
return;
|
||||
}
|
||||
if (await received.existsMediaFile(widget.message.messageId, 'png')) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
canBeReopened = true;
|
||||
});
|
||||
}
|
||||
Future.delayed(const Duration(seconds: 1), () async {
|
||||
if (!mounted) return;
|
||||
await showReopenMediaFilesTutorial(context, reopenMediaFile);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> onDoubleTap() async {
|
||||
if (widget.message.openedAt == null &&
|
||||
widget.message.messageOtherId != null ||
|
||||
widget.message.mediaStored) {
|
||||
if (widget.message.openedAt == null || widget.message.mediaStored) {
|
||||
return;
|
||||
}
|
||||
if (await received.existsMediaFile(widget.message.messageId, 'png')) {
|
||||
await encryptAndSendMessageAsync(
|
||||
null,
|
||||
widget.contact.userId,
|
||||
MessageJson(
|
||||
kind: MessageKind.reopenedMedia,
|
||||
messageSenderId: widget.message.messageId,
|
||||
content: ReopenedMediaFileContent(
|
||||
messageId: widget.message.messageOtherId!,
|
||||
if (widget.mediaService.tempPath.existsSync()) {
|
||||
await sendCipherTextToGroup(
|
||||
widget.message.groupId,
|
||||
EncryptedContent(
|
||||
mediaUpdate: EncryptedContent_MediaUpdate(
|
||||
type: EncryptedContent_MediaUpdate_Type.REOPENED,
|
||||
targetMediaId: widget.message.mediaId,
|
||||
),
|
||||
timestamp: DateTime.now(),
|
||||
),
|
||||
pushNotification: PushNotification(
|
||||
kind: PushKind.reopenedMedia,
|
||||
),
|
||||
);
|
||||
await twonlyDB.messagesDao.updateMessageByMessageId(
|
||||
widget.message.messageId,
|
||||
const MessagesCompanion(openedAt: Value(null)),
|
||||
);
|
||||
await twonlyDB.messagesDao.reopenedMedia(widget.message.messageId);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> onTap() async {
|
||||
if (widget.message.downloadState == DownloadState.downloaded &&
|
||||
if (widget.mediaService.mediaFile.downloadState ==
|
||||
DownloadState.downloaded &&
|
||||
widget.message.openedAt == null) {
|
||||
if (!mounted) return;
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return MediaViewerView(
|
||||
widget.contact,
|
||||
widget.group,
|
||||
initialMessage: widget.message,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
await checkIfTutorialCanBeShown();
|
||||
} else if (widget.message.downloadState == DownloadState.pending) {
|
||||
await received.startDownloadMedia(widget.message, true);
|
||||
} else if (widget.mediaService.mediaFile.downloadState ==
|
||||
DownloadState.pending) {
|
||||
await received.startDownloadMedia(widget.mediaService.mediaFile, true);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final color = getMessageColorFromType(
|
||||
widget.content,
|
||||
widget.message,
|
||||
widget.mediaService.mediaFile,
|
||||
context,
|
||||
);
|
||||
|
||||
return GestureDetector(
|
||||
key: reopenMediaFile,
|
||||
onDoubleTap: onDoubleTap,
|
||||
onTap: widget.message.kind == MessageKind.media ? onTap : null,
|
||||
onTap: (widget.message.type == MessageType.media) ? onTap : null,
|
||||
child: SizedBox(
|
||||
width: 150,
|
||||
height: widget.message.mediaStored ? 271 : null,
|
||||
|
|
@ -139,7 +98,8 @@ class _ChatMediaEntryState extends State<ChatMediaEntry> {
|
|||
borderRadius: BorderRadius.circular(12),
|
||||
child: InChatMediaViewer(
|
||||
message: widget.message,
|
||||
contact: widget.contact,
|
||||
group: widget.group,
|
||||
mediaService: widget.mediaService,
|
||||
color: color,
|
||||
galleryItems: widget.galleryItems,
|
||||
canBeReopened: canBeReopened,
|
||||
|
|
|
|||
|
|
@ -1,20 +1,15 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/model/json/message_old.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/views/components/animate_icon.dart';
|
||||
|
||||
class ReactionRow extends StatefulWidget {
|
||||
const ReactionRow({
|
||||
required this.otherReactions,
|
||||
required this.message,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final List<Message> otherReactions;
|
||||
final Message message;
|
||||
|
||||
@override
|
||||
|
|
@ -22,65 +17,79 @@ class ReactionRow extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _ReactionRowState extends State<ReactionRow> {
|
||||
List<Reaction> reactions = [];
|
||||
StreamSubscription<List<Reaction>>? reactionsSub;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
initAsync();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
reactionsSub?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> initAsync() async {
|
||||
final stream =
|
||||
twonlyDB.reactionsDao.watchReactions(widget.message.messageId);
|
||||
|
||||
reactionsSub = stream.listen((update) {
|
||||
setState(() {
|
||||
reactions = update;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final children = <Widget>[];
|
||||
var hasOneTextReaction = false;
|
||||
var hasOneReopened = false;
|
||||
for (final reaction in widget.otherReactions.reversed) {
|
||||
final content = MessageContent.fromJson(
|
||||
reaction.kind,
|
||||
jsonDecode(reaction.contentJson!) as Map,
|
||||
);
|
||||
|
||||
if (content is ReopenedMediaFileContent) {
|
||||
if (hasOneReopened) continue;
|
||||
hasOneReopened = true;
|
||||
children.add(
|
||||
Expanded(
|
||||
child: Align(
|
||||
alignment: Alignment.bottomRight,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: 3),
|
||||
child: FaIcon(
|
||||
FontAwesomeIcons.repeat,
|
||||
size: 12,
|
||||
color: isDarkMode(context) ? Colors.white : Colors.black,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
for (final reaction in reactions) {
|
||||
// if (content is ReopenedMediaFileContent) {
|
||||
// if (hasOneReopened) continue;
|
||||
// hasOneReopened = true;
|
||||
// children.add(
|
||||
// Expanded(
|
||||
// child: Align(
|
||||
// alignment: Alignment.bottomRight,
|
||||
// child: Padding(
|
||||
// padding: const EdgeInsets.only(right: 3),
|
||||
// child: FaIcon(
|
||||
// FontAwesomeIcons.repeat,
|
||||
// size: 12,
|
||||
// color: isDarkMode(context) ? Colors.white : Colors.black,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
// only show one reaction
|
||||
if (hasOneTextReaction) continue;
|
||||
|
||||
if (content is TextMessageContent) {
|
||||
hasOneTextReaction = true;
|
||||
if (!isEmoji(content.text)) continue;
|
||||
late Widget child;
|
||||
if (EmojiAnimation.animatedIcons.containsKey(content.text)) {
|
||||
child = SizedBox(
|
||||
height: 18,
|
||||
child: EmojiAnimation(emoji: content.text),
|
||||
);
|
||||
} else {
|
||||
child = Text(content.text, style: const TextStyle(fontSize: 14));
|
||||
}
|
||||
children.insert(
|
||||
0,
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 3),
|
||||
child: child,
|
||||
),
|
||||
late Widget child;
|
||||
if (EmojiAnimation.animatedIcons.containsKey(reaction.emoji)) {
|
||||
child = SizedBox(
|
||||
height: 18,
|
||||
child: EmojiAnimation(emoji: reaction.emoji),
|
||||
);
|
||||
} else {
|
||||
child = Text(reaction.emoji, style: const TextStyle(fontSize: 14));
|
||||
}
|
||||
children.insert(
|
||||
0,
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 3),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (children.isEmpty) return Container();
|
||||
|
||||
return Row(
|
||||
mainAxisAlignment: widget.message.messageOtherId == null
|
||||
mainAxisAlignment: widget.message.senderId == null
|
||||
? MainAxisAlignment.end
|
||||
: MainAxisAlignment.end,
|
||||
children: children,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/views/chats/chat_messages.view.dart';
|
||||
import 'package:twonly/src/views/components/animate_icon.dart';
|
||||
import 'package:twonly/src/views/components/better_text.dart';
|
||||
|
|
@ -6,17 +7,14 @@ import 'package:twonly/src/views/components/better_text.dart';
|
|||
class ChatTextEntry extends StatelessWidget {
|
||||
const ChatTextEntry({
|
||||
required this.message,
|
||||
required this.text,
|
||||
required this.hasReaction,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final String text;
|
||||
final ChatMessage message;
|
||||
final bool hasReaction;
|
||||
final Message message;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final text = message.content ?? '';
|
||||
if (EmojiAnimation.supported(text)) {
|
||||
return Container(
|
||||
constraints: const BoxConstraints(
|
||||
|
|
@ -33,16 +31,10 @@ class ChatTextEntry extends StatelessWidget {
|
|||
constraints: BoxConstraints(
|
||||
maxWidth: MediaQuery.of(context).size.width * 0.8,
|
||||
),
|
||||
padding: EdgeInsets.only(
|
||||
left: 10,
|
||||
top: 4,
|
||||
bottom: 4,
|
||||
right: hasReaction ? 30 : 10,
|
||||
),
|
||||
padding: const EdgeInsets.only(left: 10, top: 4, bottom: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: message.responseTo == null
|
||||
? getMessageColor(message.message)
|
||||
: null,
|
||||
color:
|
||||
message.quotesMessageId == null ? getMessageColor(message) : null,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: BetterText(text: text),
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/model/memory_item.model.dart';
|
||||
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
||||
import 'package:twonly/src/views/chats/chat_messages_components/message_send_state_icon.dart';
|
||||
import 'package:twonly/src/views/memories/memories_item_thumbnail.dart';
|
||||
import 'package:twonly/src/views/memories/memories_photo_slider.view.dart';
|
||||
|
|
@ -11,7 +11,8 @@ import 'package:twonly/src/views/memories/memories_photo_slider.view.dart';
|
|||
class InChatMediaViewer extends StatefulWidget {
|
||||
const InChatMediaViewer({
|
||||
required this.message,
|
||||
required this.contact,
|
||||
required this.group,
|
||||
required this.mediaService,
|
||||
required this.color,
|
||||
required this.galleryItems,
|
||||
required this.canBeReopened,
|
||||
|
|
@ -19,7 +20,8 @@ class InChatMediaViewer extends StatefulWidget {
|
|||
});
|
||||
|
||||
final Message message;
|
||||
final Contact contact;
|
||||
final Group group;
|
||||
final MediaFileService mediaService;
|
||||
final List<MemoryItem> galleryItems;
|
||||
final Color color;
|
||||
final bool canBeReopened;
|
||||
|
|
@ -56,8 +58,7 @@ class _InChatMediaViewerState extends State<InChatMediaViewer> {
|
|||
bool loadIndex() {
|
||||
if (widget.message.mediaStored) {
|
||||
final index = widget.galleryItems.indexWhere(
|
||||
(x) =>
|
||||
x.id == (widget.message.mediaUploadId ?? widget.message.messageId),
|
||||
(x) => x.mediaService.mediaFile.mediaId == (widget.message.messageId),
|
||||
);
|
||||
if (index != -1) {
|
||||
galleryItemIndex = index;
|
||||
|
|
@ -83,7 +84,7 @@ class _InChatMediaViewerState extends State<InChatMediaViewer> {
|
|||
if (widget.message.mediaStored) return;
|
||||
|
||||
final stream = twonlyDB.messagesDao
|
||||
.getMessageByMessageId(widget.message.messageId)
|
||||
.getMessageById(widget.message.messageId)
|
||||
.watchSingleOrNull();
|
||||
messageStream = stream.listen((updated) async {
|
||||
if (updated != null) {
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
// ignore_for_file: inference_failure_on_function_invocation
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:pie_menu/pie_menu.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/database/tables/messages_table.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/model/json/message_old.dart';
|
||||
import 'package:twonly/src/model/protobuf/push_notification/push_notification.pbserver.dart';
|
||||
import 'package:twonly/src/model/protobuf/client/generated/messages.pbserver.dart'
|
||||
as pb;
|
||||
import 'package:twonly/src/services/api/messages.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/views/camera/image_editor/data/layer.dart';
|
||||
|
|
@ -48,24 +48,22 @@ class MessageContextMenu extends StatelessWidget {
|
|||
) as EmojiLayerData?;
|
||||
if (layer == null) return;
|
||||
|
||||
await sendTextMessage(
|
||||
message.contactId,
|
||||
TextMessageContent(
|
||||
text: layer.text,
|
||||
responseToMessageId: message.messageOtherId,
|
||||
responseToOtherMessageId:
|
||||
(message.messageOtherId == null) ? message.messageId : null,
|
||||
await twonlyDB.reactionsDao.insertReaction(
|
||||
ReactionsCompanion(
|
||||
messageId: Value(message.messageId),
|
||||
emoji: Value(layer.text),
|
||||
),
|
||||
);
|
||||
|
||||
await sendCipherTextToGroup(
|
||||
message.groupId,
|
||||
pb.EncryptedContent(
|
||||
reaction: pb.EncryptedContent_Reaction(
|
||||
targetMessageId: message.messageId,
|
||||
emoji: layer.text,
|
||||
remove: false,
|
||||
),
|
||||
),
|
||||
(message.messageOtherId != null)
|
||||
? PushNotification(
|
||||
kind: (message.kind == MessageKind.textMessage)
|
||||
? PushKind.reactionToText
|
||||
: (getMediaContent(message)!.isVideo)
|
||||
? PushKind.reactionToVideo
|
||||
: PushKind.reactionToImage,
|
||||
reactionContent: layer.text,
|
||||
)
|
||||
: null,
|
||||
);
|
||||
},
|
||||
child: const FaIcon(FontAwesomeIcons.faceLaugh),
|
||||
|
|
@ -75,15 +73,15 @@ class MessageContextMenu extends StatelessWidget {
|
|||
onSelect: onResponseTriggered,
|
||||
child: const FaIcon(FontAwesomeIcons.reply),
|
||||
),
|
||||
PieAction(
|
||||
tooltip: Text(context.lang.copy),
|
||||
onSelect: () async {
|
||||
final text = getMessageText(message);
|
||||
await Clipboard.setData(ClipboardData(text: text));
|
||||
await HapticFeedback.heavyImpact();
|
||||
},
|
||||
child: const FaIcon(FontAwesomeIcons.solidCopy),
|
||||
),
|
||||
if (message.content != null)
|
||||
PieAction(
|
||||
tooltip: Text(context.lang.copy),
|
||||
onSelect: () async {
|
||||
await Clipboard.setData(ClipboardData(text: message.content!));
|
||||
await HapticFeedback.heavyImpact();
|
||||
},
|
||||
child: const FaIcon(FontAwesomeIcons.solidCopy),
|
||||
),
|
||||
PieAction(
|
||||
tooltip: Text(context.lang.delete),
|
||||
onSelect: () async {
|
||||
|
|
@ -94,8 +92,7 @@ class MessageContextMenu extends StatelessWidget {
|
|||
customOk: context.lang.deleteOkBtn,
|
||||
);
|
||||
if (delete) {
|
||||
await twonlyDB.messagesDao
|
||||
.deleteMessagesByMessageId(message.messageId);
|
||||
await twonlyDB.messagesDao.deleteMessagesById(message.messageId);
|
||||
}
|
||||
},
|
||||
child: const FaIcon(FontAwesomeIcons.trash),
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import 'dart:collection';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:twonly/src/database/tables/messages_table.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
||||
import 'package:twonly/src/database/tables/messages.table.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/model/json/message_old.dart';
|
||||
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/views/components/animate_icon.dart';
|
||||
|
||||
|
|
@ -18,17 +18,23 @@ enum MessageSendState {
|
|||
sending,
|
||||
}
|
||||
|
||||
MessageSendState messageSendStateFromMessage(Message msg) {
|
||||
Future<MessageSendState> messageSendStateFromMessage(Message msg) async {
|
||||
MessageSendState state;
|
||||
|
||||
if (!msg.acknowledgeByServer) {
|
||||
if (msg.messageOtherId == null) {
|
||||
final ackByServer = await twonlyDB.messagesDao.haveAllMembers(
|
||||
msg.groupId,
|
||||
msg.messageId,
|
||||
MessageActionType.ackByServerAt,
|
||||
);
|
||||
|
||||
if (!ackByServer) {
|
||||
if (msg.senderId == null) {
|
||||
state = MessageSendState.sending;
|
||||
} else {
|
||||
state = MessageSendState.receiving;
|
||||
}
|
||||
} else {
|
||||
if (msg.messageOtherId == null) {
|
||||
if (msg.senderId == null) {
|
||||
// message send
|
||||
if (msg.openedAt == null) {
|
||||
state = MessageSendState.send;
|
||||
|
|
@ -63,9 +69,113 @@ class MessageSendStateIcon extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _MessageSendStateIconState extends State<MessageSendStateIcon> {
|
||||
List<Widget> icons = <Widget>[];
|
||||
String text = '';
|
||||
Widget? textWidget;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
initAsync();
|
||||
}
|
||||
|
||||
Future<void> initAsync() async {
|
||||
final kindsAlreadyShown = HashSet<MessageType>();
|
||||
|
||||
for (final message in widget.messages) {
|
||||
if (icons.length == 2) break;
|
||||
if (kindsAlreadyShown.contains(message.type)) continue;
|
||||
kindsAlreadyShown.add(message.type);
|
||||
|
||||
final state = await messageSendStateFromMessage(message);
|
||||
|
||||
final mediaFile = message.mediaId == null
|
||||
? null
|
||||
: await MediaFileService.fromMediaId(message.mediaId!);
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
final color =
|
||||
getMessageColorFromType(message, mediaFile?.mediaFile, context);
|
||||
|
||||
Widget icon = const Placeholder();
|
||||
textWidget = null;
|
||||
|
||||
switch (state) {
|
||||
case MessageSendState.receivedOpened:
|
||||
icon = Icon(Icons.crop_square, size: 14, color: color);
|
||||
if (message.content != null) {
|
||||
if (isEmoji(message.content!)) {
|
||||
icon = Text(
|
||||
message.content!,
|
||||
style: const TextStyle(fontSize: 12),
|
||||
);
|
||||
}
|
||||
}
|
||||
text = context.lang.messageSendState_Received;
|
||||
if (widget.canBeReopened) {
|
||||
textWidget = Text(
|
||||
context.lang.doubleClickToReopen,
|
||||
style: const TextStyle(fontSize: 9),
|
||||
);
|
||||
}
|
||||
case MessageSendState.sendOpened:
|
||||
icon = FaIcon(FontAwesomeIcons.paperPlane, size: 12, color: color);
|
||||
text = context.lang.messageSendState_Opened;
|
||||
case MessageSendState.received:
|
||||
icon = Icon(Icons.square_rounded, size: 14, color: color);
|
||||
text = context.lang.messageSendState_Received;
|
||||
if (message.type == MessageType.media) {
|
||||
if (mediaFile!.mediaFile.downloadState == DownloadState.pending) {
|
||||
text = context.lang.messageSendState_TapToLoad;
|
||||
}
|
||||
if (mediaFile.mediaFile.downloadState ==
|
||||
DownloadState.downloading) {
|
||||
text = context.lang.messageSendState_Loading;
|
||||
icon = getLoaderIcon(color);
|
||||
}
|
||||
}
|
||||
case MessageSendState.send:
|
||||
icon =
|
||||
FaIcon(FontAwesomeIcons.solidPaperPlane, size: 12, color: color);
|
||||
text = context.lang.messageSendState_Send;
|
||||
case MessageSendState.sending:
|
||||
icon = getLoaderIcon(color);
|
||||
text = context.lang.messageSendState_Sending;
|
||||
case MessageSendState.receiving:
|
||||
icon = getLoaderIcon(color);
|
||||
text = context.lang.messageSendState_Received;
|
||||
}
|
||||
|
||||
if (message.mediaStored) {
|
||||
icon = FaIcon(FontAwesomeIcons.floppyDisk, size: 12, color: color);
|
||||
text = context.lang.messageStoredInGallery;
|
||||
}
|
||||
|
||||
if (mediaFile != null) {
|
||||
if (mediaFile.mediaFile.stored) {
|
||||
icon = FaIcon(FontAwesomeIcons.repeat, size: 12, color: color);
|
||||
text = context.lang.messageReopened;
|
||||
}
|
||||
|
||||
if (mediaFile.mediaFile.reuploadRequestedBy != null) {
|
||||
icon =
|
||||
FaIcon(FontAwesomeIcons.clockRotateLeft, size: 12, color: color);
|
||||
textWidget = Text(
|
||||
context.lang.retransmissionRequested,
|
||||
style: const TextStyle(fontSize: 9),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (message.type == MessageType.media) {
|
||||
icons.insert(0, icon);
|
||||
} else {
|
||||
icons.add(icon);
|
||||
}
|
||||
}
|
||||
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
Widget getLoaderIcon(Color color) {
|
||||
|
|
@ -83,108 +193,6 @@ class _MessageSendStateIconState extends State<MessageSendStateIcon> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final icons = <Widget>[];
|
||||
var text = '';
|
||||
|
||||
final kindsAlreadyShown = HashSet<MessageKind>();
|
||||
Widget? textWidget;
|
||||
|
||||
for (final message in widget.messages) {
|
||||
if (icons.length == 2) break;
|
||||
if (kindsAlreadyShown.contains(message.kind)) continue;
|
||||
kindsAlreadyShown.add(message.kind);
|
||||
|
||||
final state = messageSendStateFromMessage(message);
|
||||
late Color color;
|
||||
MessageContent? content;
|
||||
|
||||
if (message.contentJson == null) {
|
||||
color = getMessageColorFromType(TextMessageContent(text: ''), context);
|
||||
} else {
|
||||
content = MessageContent.fromJson(
|
||||
message.kind,
|
||||
jsonDecode(message.contentJson!) as Map,
|
||||
);
|
||||
if (content == null) continue;
|
||||
color = getMessageColorFromType(content, context);
|
||||
}
|
||||
|
||||
Widget icon = const Placeholder();
|
||||
textWidget = null;
|
||||
|
||||
switch (state) {
|
||||
case MessageSendState.receivedOpened:
|
||||
icon = Icon(Icons.crop_square, size: 14, color: color);
|
||||
if (content is TextMessageContent) {
|
||||
if (isEmoji(content.text)) {
|
||||
icon = Text(content.text, style: const TextStyle(fontSize: 12));
|
||||
}
|
||||
}
|
||||
text = context.lang.messageSendState_Received;
|
||||
if (widget.canBeReopened) {
|
||||
textWidget = Text(
|
||||
context.lang.doubleClickToReopen,
|
||||
style: const TextStyle(fontSize: 9),
|
||||
);
|
||||
}
|
||||
case MessageSendState.sendOpened:
|
||||
icon = FaIcon(FontAwesomeIcons.paperPlane, size: 12, color: color);
|
||||
text = context.lang.messageSendState_Opened;
|
||||
case MessageSendState.received:
|
||||
icon = Icon(Icons.square_rounded, size: 14, color: color);
|
||||
text = context.lang.messageSendState_Received;
|
||||
if (message.kind == MessageKind.media) {
|
||||
if (message.downloadState == DownloadState.pending) {
|
||||
text = context.lang.messageSendState_TapToLoad;
|
||||
}
|
||||
if (message.downloadState == DownloadState.downloading) {
|
||||
text = context.lang.messageSendState_Loading;
|
||||
icon = getLoaderIcon(color);
|
||||
}
|
||||
}
|
||||
case MessageSendState.send:
|
||||
icon =
|
||||
FaIcon(FontAwesomeIcons.solidPaperPlane, size: 12, color: color);
|
||||
text = context.lang.messageSendState_Send;
|
||||
case MessageSendState.sending:
|
||||
icon = getLoaderIcon(color);
|
||||
text = context.lang.messageSendState_Sending;
|
||||
case MessageSendState.receiving:
|
||||
icon = getLoaderIcon(color);
|
||||
text = context.lang.messageSendState_Received;
|
||||
}
|
||||
|
||||
if (message.kind == MessageKind.storedMediaFile) {
|
||||
icon = FaIcon(FontAwesomeIcons.floppyDisk, size: 12, color: color);
|
||||
text = context.lang.messageStoredInGallery;
|
||||
}
|
||||
|
||||
if (message.kind == MessageKind.reopenedMedia) {
|
||||
icon = FaIcon(FontAwesomeIcons.repeat, size: 12, color: color);
|
||||
text = context.lang.messageReopened;
|
||||
}
|
||||
|
||||
if (message.errorWhileSending) {
|
||||
icon =
|
||||
FaIcon(FontAwesomeIcons.circleExclamation, size: 12, color: color);
|
||||
text = 'Error';
|
||||
}
|
||||
|
||||
if (message.mediaRetransmissionState == MediaRetransmitting.requested) {
|
||||
icon = FaIcon(FontAwesomeIcons.clockRotateLeft, size: 12, color: color);
|
||||
textWidget = Text(
|
||||
context.lang.retransmissionRequested,
|
||||
style: const TextStyle(fontSize: 9),
|
||||
);
|
||||
}
|
||||
|
||||
if (message.kind == MessageKind.media) {
|
||||
icons.insert(0, icon);
|
||||
} else {
|
||||
icons.add(icon);
|
||||
}
|
||||
}
|
||||
|
||||
if (icons.isEmpty) return Container();
|
||||
|
||||
var icon = icons[0];
|
||||
|
|
@ -215,7 +223,7 @@ class _MessageSendStateIconState extends State<MessageSendStateIcon> {
|
|||
icon,
|
||||
const SizedBox(width: 3),
|
||||
if (textWidget != null)
|
||||
textWidget
|
||||
textWidget!
|
||||
else
|
||||
Text(
|
||||
text,
|
||||
|
|
|
|||
|
|
@ -1,29 +1,27 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||
import 'package:twonly/src/database/tables/messages_table.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
||||
import 'package:twonly/src/database/tables/messages.table.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/model/json/message_old.dart';
|
||||
import 'package:twonly/src/model/memory_item.model.dart';
|
||||
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/views/chats/chat_messages.view.dart';
|
||||
|
||||
class ResponseContainer extends StatefulWidget {
|
||||
const ResponseContainer({
|
||||
required this.msg,
|
||||
required this.contact,
|
||||
required this.group,
|
||||
required this.child,
|
||||
required this.scrollToMessage,
|
||||
required this.mediaService,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final ChatMessage msg;
|
||||
final Widget child;
|
||||
final Contact contact;
|
||||
final void Function(int) scrollToMessage;
|
||||
final Message msg;
|
||||
final Widget? child;
|
||||
final Group group;
|
||||
final MediaFileService? mediaService;
|
||||
final void Function(String) scrollToMessage;
|
||||
|
||||
@override
|
||||
State<ResponseContainer> createState() => _ResponseContainerState();
|
||||
|
|
@ -57,17 +55,20 @@ class _ResponseContainerState extends State<ResponseContainer> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.msg.responseTo == null) {
|
||||
return widget.child;
|
||||
if (widget.msg.quotesMessageId == null) {
|
||||
if (widget.child == null) {
|
||||
return Container();
|
||||
}
|
||||
return widget.child!;
|
||||
}
|
||||
return GestureDetector(
|
||||
onTap: () => widget.scrollToMessage(widget.msg.responseTo!.messageId),
|
||||
onTap: () => widget.scrollToMessage(widget.msg.quotesMessageId!),
|
||||
child: Container(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: MediaQuery.of(context).size.width * 0.8,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: getMessageColor(widget.msg.message),
|
||||
color: getMessageColor(widget.msg),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
|
|
@ -88,8 +89,8 @@ class _ResponseContainerState extends State<ResponseContainer> {
|
|||
),
|
||||
),
|
||||
child: ResponsePreview(
|
||||
contact: widget.contact,
|
||||
message: widget.msg.responseTo!,
|
||||
group: widget.group,
|
||||
messageId: widget.msg.quotesMessageId,
|
||||
showBorder: false,
|
||||
),
|
||||
),
|
||||
|
|
@ -108,14 +109,16 @@ class _ResponseContainerState extends State<ResponseContainer> {
|
|||
|
||||
class ResponsePreview extends StatefulWidget {
|
||||
const ResponsePreview({
|
||||
required this.message,
|
||||
required this.contact,
|
||||
required this.group,
|
||||
required this.showBorder,
|
||||
this.message,
|
||||
this.messageId,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final Message message;
|
||||
final Contact contact;
|
||||
final Message? message;
|
||||
final String? messageId;
|
||||
final Group group;
|
||||
final bool showBorder;
|
||||
|
||||
@override
|
||||
|
|
@ -123,56 +126,49 @@ class ResponsePreview extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _ResponsePreviewState extends State<ResponsePreview> {
|
||||
File? thumbnailPath;
|
||||
Message? message;
|
||||
MediaFileService? mediaService;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
message = widget.message;
|
||||
initAsync();
|
||||
super.initState();
|
||||
unawaited(initAsync());
|
||||
}
|
||||
|
||||
Future<void> initAsync() async {
|
||||
final items = await MemoryItem.convertFromMessages([widget.message]);
|
||||
if (items.length == 1 && mounted) {
|
||||
setState(() {
|
||||
thumbnailPath = items.values.first.thumbnailPath;
|
||||
});
|
||||
message ??= await twonlyDB.messagesDao
|
||||
.getMessageById(widget.messageId!)
|
||||
.getSingleOrNull();
|
||||
if (message?.mediaId != null) {
|
||||
mediaService = await MediaFileService.fromMediaId(message!.mediaId!);
|
||||
}
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (message == null) return Container();
|
||||
String? subtitle;
|
||||
|
||||
if (widget.message.kind == MessageKind.textMessage) {
|
||||
if (widget.message.contentJson != null) {
|
||||
final content = MessageContent.fromJson(
|
||||
MessageKind.textMessage,
|
||||
jsonDecode(widget.message.contentJson!) as Map,
|
||||
);
|
||||
if (content is TextMessageContent) {
|
||||
subtitle = truncateString(content.text);
|
||||
}
|
||||
if (message!.type == MessageType.text) {
|
||||
if (message!.content != null) {
|
||||
subtitle = truncateString(message!.content!);
|
||||
}
|
||||
}
|
||||
if (widget.message.kind == MessageKind.media) {
|
||||
final content = MessageContent.fromJson(
|
||||
MessageKind.media,
|
||||
jsonDecode(widget.message.contentJson!) as Map,
|
||||
);
|
||||
if (content is MediaMessageContent) {
|
||||
subtitle = content.isVideo ? 'Video' : 'Image';
|
||||
}
|
||||
if (message!.type == MessageType.media && mediaService != null) {
|
||||
subtitle =
|
||||
mediaService!.mediaFile.type == MediaType.video ? 'Video' : 'Image';
|
||||
}
|
||||
|
||||
var username = 'You';
|
||||
if (widget.message.messageOtherId != null) {
|
||||
username = getContactDisplayName(widget.contact);
|
||||
if (message!.senderId != null) {
|
||||
username = message!.senderId.toString();
|
||||
}
|
||||
|
||||
final color = getMessageColor(widget.message);
|
||||
final color = getMessageColor(message!);
|
||||
|
||||
if (!widget.message.mediaStored) {
|
||||
if (!message!.mediaStored) {
|
||||
return Container(
|
||||
padding: widget.showBorder
|
||||
? const EdgeInsets.only(left: 10, right: 10)
|
||||
|
|
@ -225,10 +221,10 @@ class _ResponsePreviewState extends State<ResponsePreview> {
|
|||
],
|
||||
),
|
||||
),
|
||||
if (thumbnailPath != null)
|
||||
if (mediaService != null)
|
||||
SizedBox(
|
||||
height: widget.showBorder ? 100 : 210,
|
||||
child: Image.file(thumbnailPath!),
|
||||
child: Image.file(mediaService!.thumbnailPath),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,29 +1,24 @@
|
|||
// ignore_for_file: avoid_dynamic_calls
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:drift/drift.dart' hide Column;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:lottie/lottie.dart';
|
||||
import 'package:no_screenshot/no_screenshot.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||
import 'package:twonly/src/database/tables/messages_table.dart';
|
||||
import 'package:twonly/src/database/tables/mediafiles.table.dart'
|
||||
show DownloadState, MediaType;
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/model/json/message_old.dart';
|
||||
import 'package:twonly/src/model/protobuf/push_notification/push_notification.pb.dart';
|
||||
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'
|
||||
as pb;
|
||||
import 'package:twonly/src/services/api/mediafiles/download.service.dart';
|
||||
import 'package:twonly/src/services/api/messages.dart';
|
||||
import 'package:twonly/src/services/api/utils.dart';
|
||||
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
||||
import 'package:twonly/src/services/notifications/background.notifications.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/utils/storage.dart';
|
||||
import 'package:twonly/src/views/camera/camera_send_to_view.dart';
|
||||
import 'package:twonly/src/views/camera/share_image_editor_view.dart';
|
||||
import 'package:twonly/src/views/chats/media_viewer_components/reaction_buttons.component.dart';
|
||||
import 'package:twonly/src/views/components/animate_icon.dart';
|
||||
import 'package:twonly/src/views/components/media_view_sizing.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
|
|
@ -31,8 +26,8 @@ import 'package:video_player/video_player.dart';
|
|||
final NoScreenshot _noScreenshot = NoScreenshot.instance;
|
||||
|
||||
class MediaViewerView extends StatefulWidget {
|
||||
const MediaViewerView(this.contact, {super.key, this.initialMessage});
|
||||
final Contact contact;
|
||||
const MediaViewerView(this.group, {super.key, this.initialMessage});
|
||||
final Group group;
|
||||
|
||||
final Message? initialMessage;
|
||||
|
||||
|
|
@ -48,23 +43,21 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
double mediaViewerDistanceFromBottom = 0;
|
||||
|
||||
// current image related
|
||||
Uint8List? imageBytes;
|
||||
String? videoPath;
|
||||
VideoPlayerController? videoController;
|
||||
|
||||
MediaFileService? currentMedia;
|
||||
Message? currentMessage;
|
||||
|
||||
DateTime? canBeSeenUntil;
|
||||
int maxShowTime = gMediaShowInfinite;
|
||||
double progress = 0;
|
||||
bool isRealTwonly = false;
|
||||
bool mirrorVideo = false;
|
||||
bool isDownloading = false;
|
||||
bool showSendTextMessageInput = false;
|
||||
final GlobalKey mediaWidgetKey = GlobalKey();
|
||||
|
||||
bool imageSaved = false;
|
||||
bool imageSaving = false;
|
||||
bool displayTwonlyPresent = true;
|
||||
|
||||
StreamSubscription<Message?>? downloadStateListener;
|
||||
StreamSubscription<MediaFile?>? downloadStateListener;
|
||||
|
||||
List<Message> allMediaFiles = [];
|
||||
late StreamSubscription<List<Message>> _subscription;
|
||||
|
|
@ -94,20 +87,21 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
|
||||
Future<void> asyncLoadNextMedia(bool firstRun) async {
|
||||
final messages =
|
||||
twonlyDB.messagesDao.watchMediaMessageNotOpened(widget.contact.userId);
|
||||
twonlyDB.messagesDao.watchMediaNotOpened(widget.group.groupId);
|
||||
|
||||
_subscription = messages.listen((messages) {
|
||||
_subscription = messages.listen((messages) async {
|
||||
for (final msg in messages) {
|
||||
// if (!allMediaFiles.any((m) => m.messageId == msg.messageId)) {
|
||||
// allMediaFiles.add(msg);
|
||||
// }
|
||||
// Find the index of the existing message with the same messageId
|
||||
if (msg.mediaId == currentMedia?.mediaFile.mediaId) {
|
||||
// The update of the current Media in case of a download is done in loadCurrentMediaFile
|
||||
continue;
|
||||
}
|
||||
|
||||
/// If the messages was already there just replace it and go to the next...
|
||||
|
||||
final index =
|
||||
allMediaFiles.indexWhere((m) => m.messageId == msg.messageId);
|
||||
|
||||
if (index >= 1) {
|
||||
// to not modify the first message
|
||||
// If the message exists, replace it
|
||||
allMediaFiles[index] = msg;
|
||||
} else if (index == -1) {
|
||||
// If the message does not exist, add it
|
||||
|
|
@ -116,101 +110,90 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
}
|
||||
setState(() {});
|
||||
if (firstRun) {
|
||||
loadCurrentMediaFile();
|
||||
// ignore: parameter_assignments
|
||||
firstRun = false;
|
||||
await loadCurrentMediaFile();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> nextMediaOrExit() async {
|
||||
if (!mounted) return;
|
||||
await videoController?.dispose();
|
||||
nextMediaTimer?.cancel();
|
||||
progressTimer?.cancel();
|
||||
if (allMediaFiles.isNotEmpty) {
|
||||
try {
|
||||
if (!imageSaved && maxShowTime != gMediaShowInfinite) {
|
||||
await deleteMediaFile(allMediaFiles.first.messageId, 'mp4');
|
||||
await deleteMediaFile(allMediaFiles.first.messageId, 'png');
|
||||
}
|
||||
} catch (e) {
|
||||
Log.error('$e');
|
||||
/// Remove the current media file in case it is not set to unlimited
|
||||
if (currentMedia != null) {
|
||||
if (!imageSaved &&
|
||||
currentMedia!.mediaFile.displayLimitInMilliseconds != null) {
|
||||
currentMedia!.fullMediaRemoval();
|
||||
}
|
||||
}
|
||||
if (allMediaFiles.isEmpty || allMediaFiles.length == 1) {
|
||||
if (mounted) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
|
||||
await videoController?.dispose();
|
||||
if (!mounted) return;
|
||||
|
||||
nextMediaTimer?.cancel();
|
||||
progressTimer?.cancel();
|
||||
|
||||
if (allMediaFiles.isEmpty) {
|
||||
Navigator.pop(context);
|
||||
} else {
|
||||
allMediaFiles.removeAt(0);
|
||||
await loadCurrentMediaFile();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> loadCurrentMediaFile({bool showTwonly = false}) async {
|
||||
if (!mounted) return;
|
||||
if (!context.mounted || allMediaFiles.isEmpty) return nextMediaOrExit();
|
||||
if (!mounted || !context.mounted) return;
|
||||
if (allMediaFiles.isEmpty) return nextMediaOrExit();
|
||||
await _noScreenshot.screenshotOff();
|
||||
|
||||
setState(() {
|
||||
videoController = null;
|
||||
imageBytes = null;
|
||||
currentMedia = null;
|
||||
currentMessage = null;
|
||||
canBeSeenUntil = null;
|
||||
maxShowTime = gMediaShowInfinite;
|
||||
imageSaving = false;
|
||||
imageSaved = false;
|
||||
mirrorVideo = false;
|
||||
progress = 0;
|
||||
videoPath = null;
|
||||
isDownloading = false;
|
||||
isRealTwonly = false;
|
||||
showSendTextMessageInput = false;
|
||||
});
|
||||
|
||||
if (Platform.isAndroid) {
|
||||
await flutterLocalNotificationsPlugin
|
||||
.cancel(allMediaFiles.first.contactId);
|
||||
} else {
|
||||
await flutterLocalNotificationsPlugin.cancelAll();
|
||||
}
|
||||
// if (Platform.isAndroid) {
|
||||
// await flutterLocalNotificationsPlugin
|
||||
// .cancel(allMediaFiles.first.contactId);
|
||||
// } else {
|
||||
await flutterLocalNotificationsPlugin.cancelAll();
|
||||
// }
|
||||
|
||||
if (allMediaFiles.first.downloadState != DownloadState.downloaded) {
|
||||
setState(() {
|
||||
isDownloading = true;
|
||||
});
|
||||
await startDownloadMedia(allMediaFiles.first, true);
|
||||
final stream =
|
||||
twonlyDB.mediaFilesDao.watchMedia(currentMedia!.mediaFile.mediaId);
|
||||
|
||||
final stream = twonlyDB.messagesDao
|
||||
.getMessageByMessageId(allMediaFiles.first.messageId)
|
||||
.watchSingleOrNull();
|
||||
await downloadStateListener?.cancel();
|
||||
downloadStateListener = stream.listen((updated) async {
|
||||
if (updated != null) {
|
||||
if (updated.downloadState == DownloadState.downloaded) {
|
||||
await downloadStateListener?.cancel();
|
||||
await handleNextDownloadedMedia(updated, showTwonly);
|
||||
// start downloading all the other possible missing media files.
|
||||
await tryDownloadAllMediaFiles(force: true);
|
||||
}
|
||||
var downloadTriggered = false;
|
||||
|
||||
await downloadStateListener?.cancel();
|
||||
downloadStateListener = stream.listen((updated) async {
|
||||
if (updated == null) return;
|
||||
if (updated.downloadState != DownloadState.downloaded) {
|
||||
if (!downloadTriggered) {
|
||||
downloadTriggered = true;
|
||||
await startDownloadMedia(currentMedia!.mediaFile, true);
|
||||
unawaited(tryDownloadAllMediaFiles(force: true));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
await handleNextDownloadedMedia(allMediaFiles.first, showTwonly);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
await downloadStateListener?.cancel();
|
||||
await handleNextDownloadedMedia(showTwonly);
|
||||
// start downloading all the other possible missing media files.
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> handleNextDownloadedMedia(
|
||||
Message current,
|
||||
bool showTwonly,
|
||||
) async {
|
||||
final content =
|
||||
MediaMessageContent.fromJson(jsonDecode(current.contentJson!) as Map);
|
||||
currentMessage = allMediaFiles.removeAt(0);
|
||||
final currentMediaLocal =
|
||||
await MediaFileService.fromMediaId(currentMessage!.mediaId!);
|
||||
if (currentMediaLocal == null || !mounted) return;
|
||||
|
||||
if (content.isRealTwonly) {
|
||||
setState(() {
|
||||
isRealTwonly = true;
|
||||
});
|
||||
if (currentMediaLocal.mediaFile.requiresAuthentication) {
|
||||
if (!showTwonly) return;
|
||||
|
||||
final isAuth = await authenticateUser(
|
||||
|
|
@ -224,66 +207,56 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
}
|
||||
|
||||
await notifyContactAboutOpeningMessage(
|
||||
current.contactId,
|
||||
[current.messageOtherId!],
|
||||
currentMessage!.senderId!,
|
||||
[currentMessage!.messageId],
|
||||
);
|
||||
|
||||
await twonlyDB.messagesDao.updateMessageByMessageId(
|
||||
current.messageId,
|
||||
await twonlyDB.messagesDao.updateMessageId(
|
||||
currentMessage!.messageId,
|
||||
MessagesCompanion(openedAt: Value(DateTime.now())),
|
||||
);
|
||||
|
||||
if (content.isVideo) {
|
||||
final videoPathTmp = await getVideoPath(current.messageId);
|
||||
if (videoPathTmp != null) {
|
||||
videoController = VideoPlayerController.file(File(videoPathTmp.path));
|
||||
await videoController
|
||||
?.setLooping(content.maxShowTime == gMediaShowInfinite);
|
||||
await videoController?.initialize().then((_) {
|
||||
videoController!.play();
|
||||
videoController?.addListener(() {
|
||||
setState(() {
|
||||
progress = 1 -
|
||||
videoController!.value.position.inSeconds /
|
||||
videoController!.value.duration.inSeconds;
|
||||
});
|
||||
if (content.maxShowTime != gMediaShowInfinite) {
|
||||
if (videoController?.value.position ==
|
||||
videoController?.value.duration) {
|
||||
nextMediaOrExit();
|
||||
}
|
||||
}
|
||||
});
|
||||
setState(() {
|
||||
videoPath = videoPathTmp.path;
|
||||
});
|
||||
// ignore: invalid_return_type_for_catch_error, argument_type_not_assignable_to_error_handler
|
||||
}).catchError(Log.error);
|
||||
}
|
||||
}
|
||||
|
||||
imageBytes = await getImageBytes(current.messageId);
|
||||
|
||||
if ((imageBytes == null && !content.isVideo) ||
|
||||
(content.isVideo && videoController == null)) {
|
||||
Log.error('media files are not found...');
|
||||
// When the message should be downloaded but imageBytes are null then a error happened
|
||||
await handleMediaError(current);
|
||||
if (!currentMediaLocal.tempPath.existsSync()) {
|
||||
Log.error('Temp media file not found...');
|
||||
await handleMediaError(currentMediaLocal.mediaFile);
|
||||
return nextMediaOrExit();
|
||||
}
|
||||
|
||||
if (!content.isVideo) {
|
||||
if (content.maxShowTime != gMediaShowInfinite) {
|
||||
if (currentMediaLocal.mediaFile.type == MediaType.video) {
|
||||
videoController = VideoPlayerController.file(currentMediaLocal.tempPath);
|
||||
await videoController?.setLooping(
|
||||
currentMediaLocal.mediaFile.displayLimitInMilliseconds == null,
|
||||
);
|
||||
await videoController?.initialize().then((_) {
|
||||
videoController!.play();
|
||||
videoController?.addListener(() {
|
||||
setState(() {
|
||||
progress = 1 -
|
||||
videoController!.value.position.inSeconds /
|
||||
videoController!.value.duration.inSeconds;
|
||||
});
|
||||
if (currentMediaLocal.mediaFile.displayLimitInMilliseconds != null) {
|
||||
if (videoController?.value.position ==
|
||||
videoController?.value.duration) {
|
||||
nextMediaOrExit();
|
||||
}
|
||||
}
|
||||
});
|
||||
// ignore: invalid_return_type_for_catch_error, argument_type_not_assignable_to_error_handler
|
||||
}).catchError(Log.error);
|
||||
} else {
|
||||
if (currentMediaLocal.mediaFile.displayLimitInMilliseconds != null) {
|
||||
canBeSeenUntil = DateTime.now().add(
|
||||
Duration(seconds: content.maxShowTime),
|
||||
Duration(
|
||||
milliseconds:
|
||||
currentMediaLocal.mediaFile.displayLimitInMilliseconds!,
|
||||
),
|
||||
);
|
||||
startTimer();
|
||||
}
|
||||
}
|
||||
setState(() {
|
||||
maxShowTime = content.maxShowTime;
|
||||
isDownloading = false;
|
||||
mirrorVideo = content.mirrorVideo;
|
||||
currentMedia = currentMediaLocal;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -299,44 +272,37 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
if (canBeSeenUntil != null) {
|
||||
final difference = canBeSeenUntil!.difference(DateTime.now());
|
||||
// Calculate the progress as a value between 0.0 and 1.0
|
||||
progress = difference.inMilliseconds / (maxShowTime * 1000);
|
||||
progress = difference.inMilliseconds /
|
||||
(currentMedia!.mediaFile.displayLimitInMilliseconds!);
|
||||
setState(() {});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> onPressedSaveToGallery() async {
|
||||
if (allMediaFiles.first.messageOtherId == null) {
|
||||
return; // should not be possible
|
||||
}
|
||||
setState(() {
|
||||
imageSaving = true;
|
||||
});
|
||||
await twonlyDB.messagesDao.updateMessageByMessageId(
|
||||
allMediaFiles.first.messageId,
|
||||
const MessagesCompanion(mediaStored: Value(true)),
|
||||
);
|
||||
await encryptAndSendMessageAsync(
|
||||
null,
|
||||
widget.contact.userId,
|
||||
MessageJson(
|
||||
kind: MessageKind.storedMediaFile,
|
||||
messageSenderId: allMediaFiles.first.messageId,
|
||||
messageReceiverId: allMediaFiles.first.messageOtherId,
|
||||
content: MessageContent(),
|
||||
timestamp: DateTime.now(),
|
||||
await currentMedia!.storeMediaFile();
|
||||
await sendCipherTextToGroup(
|
||||
widget.group.groupId,
|
||||
pb.EncryptedContent(
|
||||
mediaUpdate: pb.EncryptedContent_MediaUpdate(
|
||||
type: pb.EncryptedContent_MediaUpdate_Type.STORED,
|
||||
targetMediaId: currentMedia!.mediaFile.mediaId,
|
||||
),
|
||||
),
|
||||
pushNotification: PushNotification(kind: PushKind.storedMediaFile),
|
||||
);
|
||||
setState(() {
|
||||
imageSaved = true;
|
||||
});
|
||||
final user = await getUser();
|
||||
if (user != null && (user.storeMediaFilesInGallery)) {
|
||||
if (videoPath != null) {
|
||||
await saveVideoToGallery(videoPath!);
|
||||
} else {
|
||||
await saveImageToGallery(imageBytes!);
|
||||
|
||||
if (gUser.storeMediaFilesInGallery) {
|
||||
if (currentMedia!.mediaFile.type == MediaType.video) {
|
||||
await saveVideoToGallery(currentMedia!.storedPath.path);
|
||||
} else if (currentMedia!.mediaFile.type == MediaType.image) {
|
||||
final imageBytes = await currentMedia!.storedPath.readAsBytes();
|
||||
await saveImageToGallery(imageBytes);
|
||||
}
|
||||
}
|
||||
setState(() {
|
||||
|
|
@ -358,7 +324,8 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
key: mediaWidgetKey,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
if (maxShowTime == gMediaShowInfinite)
|
||||
if (currentMedia != null &&
|
||||
currentMedia!.mediaFile.displayLimitInMilliseconds == null)
|
||||
OutlinedButton(
|
||||
style: OutlinedButton.styleFrom(
|
||||
iconColor: imageSaved
|
||||
|
|
@ -368,7 +335,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
? Theme.of(context).colorScheme.outline
|
||||
: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
onPressed: onPressedSaveToGallery,
|
||||
onPressed: (currentMedia == null) ? null : onPressedSaveToGallery,
|
||||
child: Row(
|
||||
children: [
|
||||
if (imageSaving)
|
||||
|
|
@ -450,11 +417,12 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return CameraSendToView(widget.contact);
|
||||
return CameraSendToView(widget.group);
|
||||
},
|
||||
),
|
||||
);
|
||||
if (mounted && maxShowTime != gMediaShowInfinite) {
|
||||
if (mounted &&
|
||||
currentMedia!.mediaFile.displayLimitInMilliseconds != null) {
|
||||
await nextMediaOrExit();
|
||||
} else {
|
||||
await videoController?.play();
|
||||
|
|
@ -477,7 +445,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
if ((imageBytes != null || videoController != null) &&
|
||||
if ((currentMedia != null || videoController != null) &&
|
||||
(canBeSeenUntil == null || progress >= 0))
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
|
|
@ -497,15 +465,12 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
children: [
|
||||
if (videoController != null)
|
||||
Positioned.fill(
|
||||
child: Transform.flip(
|
||||
flipX: mirrorVideo,
|
||||
child: VideoPlayer(videoController!),
|
||||
),
|
||||
child: VideoPlayer(videoController!),
|
||||
),
|
||||
if (imageBytes != null)
|
||||
if (currentMedia!.mediaFile.type == MediaType.image)
|
||||
Positioned.fill(
|
||||
child: Image.memory(
|
||||
imageBytes!,
|
||||
child: Image.file(
|
||||
currentMedia!.tempPath,
|
||||
fit: BoxFit.contain,
|
||||
frameBuilder: (
|
||||
context,
|
||||
|
|
@ -534,7 +499,9 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
),
|
||||
),
|
||||
),
|
||||
if (isRealTwonly && imageBytes == null)
|
||||
if (currentMedia != null &&
|
||||
currentMedia!.mediaFile.requiresAuthentication &&
|
||||
displayTwonlyPresent)
|
||||
Positioned.fill(
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
|
|
@ -570,7 +537,8 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
],
|
||||
),
|
||||
),
|
||||
if (isDownloading)
|
||||
if (currentMedia?.mediaFile.downloadState !=
|
||||
DownloadState.downloaded)
|
||||
const Positioned.fill(
|
||||
child: Center(
|
||||
child: SizedBox(
|
||||
|
|
@ -602,7 +570,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
left: showSendTextMessageInput ? 0 : null,
|
||||
right: showSendTextMessageInput ? 0 : 15,
|
||||
child: Text(
|
||||
getContactDisplayName(widget.contact),
|
||||
widget.group.groupName,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: showSendTextMessageInput ? 24 : 14,
|
||||
|
|
@ -658,18 +626,12 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
),
|
||||
IconButton(
|
||||
icon: const FaIcon(FontAwesomeIcons.solidPaperPlane),
|
||||
onPressed: () {
|
||||
onPressed: () async {
|
||||
if (textMessageController.text.isNotEmpty) {
|
||||
sendTextMessage(
|
||||
widget.contact.userId,
|
||||
TextMessageContent(
|
||||
text: textMessageController.text,
|
||||
responseToMessageId:
|
||||
allMediaFiles.first.messageOtherId,
|
||||
),
|
||||
PushNotification(
|
||||
kind: PushKind.response,
|
||||
),
|
||||
await insertAndSendTextMessage(
|
||||
widget.group.groupId,
|
||||
textMessageController.text,
|
||||
currentMessage!.messageId,
|
||||
);
|
||||
textMessageController.clear();
|
||||
}
|
||||
|
|
@ -683,14 +645,13 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
),
|
||||
),
|
||||
),
|
||||
if (allMediaFiles.isNotEmpty)
|
||||
if (currentMedia != null)
|
||||
ReactionButtons(
|
||||
show: showShortReactions,
|
||||
textInputFocused: showSendTextMessageInput,
|
||||
mediaViewerDistanceFromBottom: mediaViewerDistanceFromBottom,
|
||||
userId: widget.contact.userId,
|
||||
responseToMessageId: allMediaFiles.first.messageOtherId!,
|
||||
isVideo: videoController != null,
|
||||
groupId: widget.group.groupId,
|
||||
messageId: currentMessage!.messageId,
|
||||
hide: () {
|
||||
setState(() {
|
||||
showShortReactions = false;
|
||||
|
|
@ -704,195 +665,3 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ReactionButtons extends StatefulWidget {
|
||||
const ReactionButtons({
|
||||
required this.show,
|
||||
required this.textInputFocused,
|
||||
required this.userId,
|
||||
required this.mediaViewerDistanceFromBottom,
|
||||
required this.responseToMessageId,
|
||||
required this.isVideo,
|
||||
required this.hide,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final double mediaViewerDistanceFromBottom;
|
||||
final bool show;
|
||||
final bool isVideo;
|
||||
final bool textInputFocused;
|
||||
final int userId;
|
||||
final int responseToMessageId;
|
||||
final void Function() hide;
|
||||
|
||||
@override
|
||||
State<ReactionButtons> createState() => _ReactionButtonsState();
|
||||
}
|
||||
|
||||
class _ReactionButtonsState extends State<ReactionButtons> {
|
||||
int selectedShortReaction = -1;
|
||||
|
||||
List<String> selectedEmojis =
|
||||
EmojiAnimation.animatedIcons.keys.toList().sublist(0, 6);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
initAsync();
|
||||
}
|
||||
|
||||
Future<void> initAsync() async {
|
||||
final user = await getUser();
|
||||
if (user != null && user.preSelectedEmojies != null) {
|
||||
selectedEmojis = user.preSelectedEmojies!;
|
||||
}
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final firstRowEmojis = selectedEmojis.take(6).toList();
|
||||
final secondRowEmojis =
|
||||
selectedEmojis.length > 6 ? selectedEmojis.skip(6).toList() : [];
|
||||
|
||||
return AnimatedPositioned(
|
||||
duration: const Duration(milliseconds: 200), // Animation duration
|
||||
bottom: widget.show
|
||||
? (widget.textInputFocused
|
||||
? 50
|
||||
: widget.mediaViewerDistanceFromBottom)
|
||||
: widget.mediaViewerDistanceFromBottom - 20,
|
||||
left: widget.show ? 0 : MediaQuery.sizeOf(context).width / 2,
|
||||
right: widget.show ? 0 : MediaQuery.sizeOf(context).width / 2,
|
||||
curve: Curves.linearToEaseOut,
|
||||
child: AnimatedOpacity(
|
||||
opacity: widget.show ? 1.0 : 0.0, // Fade in/out
|
||||
duration: const Duration(milliseconds: 150),
|
||||
child: Container(
|
||||
color: widget.show ? Colors.black.withAlpha(0) : Colors.transparent,
|
||||
padding:
|
||||
widget.show ? const EdgeInsets.symmetric(vertical: 32) : null,
|
||||
child: Column(
|
||||
children: [
|
||||
if (secondRowEmojis.isNotEmpty)
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: secondRowEmojis
|
||||
.map(
|
||||
(emoji) => EmojiReactionWidget(
|
||||
userId: widget.userId,
|
||||
responseToMessageId: widget.responseToMessageId,
|
||||
hide: widget.hide,
|
||||
show: widget.show,
|
||||
isVideo: widget.isVideo,
|
||||
emoji: emoji as String,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
if (secondRowEmojis.isNotEmpty) const SizedBox(height: 15),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: firstRowEmojis
|
||||
.map(
|
||||
(emoji) => EmojiReactionWidget(
|
||||
userId: widget.userId,
|
||||
responseToMessageId: widget.responseToMessageId,
|
||||
hide: widget.hide,
|
||||
show: widget.show,
|
||||
isVideo: widget.isVideo,
|
||||
emoji: emoji,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class EmojiReactionWidget extends StatefulWidget {
|
||||
const EmojiReactionWidget({
|
||||
required this.userId,
|
||||
required this.responseToMessageId,
|
||||
required this.hide,
|
||||
required this.isVideo,
|
||||
required this.show,
|
||||
required this.emoji,
|
||||
super.key,
|
||||
});
|
||||
final int userId;
|
||||
final int responseToMessageId;
|
||||
final Function hide;
|
||||
final bool show;
|
||||
final bool isVideo;
|
||||
final String emoji;
|
||||
|
||||
@override
|
||||
State<EmojiReactionWidget> createState() => _EmojiReactionWidgetState();
|
||||
}
|
||||
|
||||
class _EmojiReactionWidgetState extends State<EmojiReactionWidget> {
|
||||
int selectedShortReaction = -1;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedSize(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.linearToEaseOut,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
sendTextMessage(
|
||||
widget.userId,
|
||||
TextMessageContent(
|
||||
text: widget.emoji,
|
||||
responseToMessageId: widget.responseToMessageId,
|
||||
),
|
||||
PushNotification(
|
||||
kind: widget.isVideo
|
||||
? PushKind.reactionToVideo
|
||||
: PushKind.reactionToImage,
|
||||
reactionContent: widget.emoji,
|
||||
),
|
||||
);
|
||||
setState(() {
|
||||
selectedShortReaction = 0; // Assuming index is 0 for this example
|
||||
});
|
||||
Future.delayed(const Duration(milliseconds: 300), () {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
widget.hide();
|
||||
selectedShortReaction = -1;
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
child: (selectedShortReaction ==
|
||||
0) // Assuming index is 0 for this example
|
||||
? EmojiAnimationFlying(
|
||||
emoji: widget.emoji,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
startPosition: 0,
|
||||
size: (widget.show) ? 40 : 10,
|
||||
)
|
||||
: AnimatedOpacity(
|
||||
opacity: (selectedShortReaction == -1) ? 1 : 0, // Fade in/out
|
||||
duration: const Duration(milliseconds: 150),
|
||||
child: SizedBox(
|
||||
width: widget.show ? 40 : 10,
|
||||
child: Center(
|
||||
child: EmojiAnimation(
|
||||
emoji: widget.emoji,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:drift/drift.dart' hide Column;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
|
|
@ -10,8 +9,8 @@ import 'package:twonly/src/database/twonly.db.dart';
|
|||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/views/chats/add_new_user.view.dart';
|
||||
import 'package:twonly/src/views/chats/chat_messages.view.dart';
|
||||
import 'package:twonly/src/views/components/avatar_icon.component.dart';
|
||||
import 'package:twonly/src/views/components/flame.dart';
|
||||
import 'package:twonly/src/views/components/initialsavatar.dart';
|
||||
import 'package:twonly/src/views/components/user_context_menu.component.dart';
|
||||
|
||||
class StartNewChatView extends StatefulWidget {
|
||||
|
|
@ -30,7 +29,7 @@ class _StartNewChatView extends State<StartNewChatView> {
|
|||
void initState() {
|
||||
super.initState();
|
||||
|
||||
final stream = twonlyDB.contactsDao.watchContactsForStartNewChat();
|
||||
final stream = twonlyDB.contactsDao.watchAllAcceptedContacts();
|
||||
|
||||
contactSub = stream.listen((update) async {
|
||||
update.sort(
|
||||
|
|
@ -147,7 +146,6 @@ class UserList extends StatelessWidget {
|
|||
return const Divider();
|
||||
}
|
||||
final user = users[i - 2];
|
||||
final flameCounter = getFlameCounterFromContact(user);
|
||||
return UserContextMenu(
|
||||
key: Key(user.userId.toString()),
|
||||
contact: user,
|
||||
|
|
@ -155,40 +153,37 @@ class UserList extends StatelessWidget {
|
|||
title: Row(
|
||||
children: [
|
||||
Text(getContactDisplayName(user)),
|
||||
if (flameCounter >= 1)
|
||||
FlameCounterWidget(
|
||||
user,
|
||||
flameCounter,
|
||||
prefix: true,
|
||||
),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
icon: FaIcon(
|
||||
FontAwesomeIcons.boxOpen,
|
||||
size: 13,
|
||||
color: user.archived ? null : Colors.transparent,
|
||||
),
|
||||
onPressed: user.archived
|
||||
? () async {
|
||||
const update =
|
||||
ContactsCompanion(archived: Value(false));
|
||||
await twonlyDB.contactsDao
|
||||
.updateContact(user.userId, update);
|
||||
}
|
||||
: null,
|
||||
FlameCounterWidget(
|
||||
contactId: user.userId,
|
||||
prefix: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
leading: ContactAvatar(
|
||||
leading: AvatarIcon(
|
||||
contact: user,
|
||||
fontSize: 13,
|
||||
),
|
||||
onTap: () async {
|
||||
var directChat =
|
||||
await twonlyDB.groupsDao.getDirectChat(user.userId);
|
||||
if (directChat == null) {
|
||||
await twonlyDB.groupsDao.insertGroup(
|
||||
GroupsCompanion(
|
||||
isDirectChat: const Value(true),
|
||||
groupName: Value(
|
||||
getContactDisplayName(user),
|
||||
),
|
||||
),
|
||||
);
|
||||
directChat =
|
||||
await twonlyDB.groupsDao.getDirectChat(user.userId);
|
||||
}
|
||||
if (!context.mounted) return;
|
||||
await Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return ChatMessagesView(user);
|
||||
return ChatMessagesView(directChat!);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
|
|
|||
94
lib/src/views/components/avatar_icon.component.dart
Normal file
94
lib/src/views/components/avatar_icon.component.dart
Normal 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();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,26 +1,65 @@
|
|||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/views/components/animate_icon.dart';
|
||||
|
||||
class FlameCounterWidget extends StatelessWidget {
|
||||
const FlameCounterWidget(
|
||||
this.user,
|
||||
this.flameCounter, {
|
||||
class FlameCounterWidget extends StatefulWidget {
|
||||
const FlameCounterWidget({
|
||||
this.groupId,
|
||||
this.contactId,
|
||||
this.prefix = false,
|
||||
super.key,
|
||||
});
|
||||
final Contact user;
|
||||
final int flameCounter;
|
||||
final String? groupId;
|
||||
final int? contactId;
|
||||
final bool prefix;
|
||||
|
||||
@override
|
||||
State<FlameCounterWidget> createState() => _FlameCounterWidgetState();
|
||||
}
|
||||
|
||||
class _FlameCounterWidgetState extends State<FlameCounterWidget> {
|
||||
int flameCounter = 0;
|
||||
bool isBestFriend = false;
|
||||
|
||||
StreamSubscription<int>? flameCounterSub;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
initAsync();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
flameCounterSub?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> initAsync() async {
|
||||
var groupId = widget.groupId;
|
||||
if (widget.groupId == null && widget.contactId != null) {
|
||||
final group = await twonlyDB.groupsDao.getDirectChat(widget.contactId!);
|
||||
groupId = group?.groupId;
|
||||
}
|
||||
if (groupId != null) {
|
||||
isBestFriend = gUser.myBestFriendGroupId == groupId;
|
||||
final stream = twonlyDB.groupsDao.watchFlameCounter(groupId);
|
||||
flameCounterSub = stream.listen((counter) {
|
||||
setState(() {
|
||||
flameCounter = counter;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
if (prefix) const SizedBox(width: 5),
|
||||
if (prefix) const Text('•'),
|
||||
if (prefix) const SizedBox(width: 5),
|
||||
if (widget.prefix) const SizedBox(width: 5),
|
||||
if (widget.prefix) const Text('•'),
|
||||
if (widget.prefix) const SizedBox(width: 5),
|
||||
Text(
|
||||
flameCounter.toString(),
|
||||
style: const TextStyle(fontSize: 13),
|
||||
|
|
@ -28,7 +67,7 @@ class FlameCounterWidget extends StatelessWidget {
|
|||
SizedBox(
|
||||
height: 15,
|
||||
child: EmojiAnimation(
|
||||
emoji: (globalBestFriendUserId == user.userId) ? '❤️🔥' : '🔥',
|
||||
emoji: isBestFriend ? '❤️🔥' : '🔥',
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -5,8 +5,8 @@ import 'package:twonly/src/database/twonly.db.dart';
|
|||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/views/contact/contact.view.dart';
|
||||
|
||||
class UserContextMenuBlocked extends StatefulWidget {
|
||||
const UserContextMenuBlocked({
|
||||
class UserContextMenu extends StatefulWidget {
|
||||
const UserContextMenu({
|
||||
required this.contact,
|
||||
required this.child,
|
||||
super.key,
|
||||
|
|
@ -15,10 +15,10 @@ class UserContextMenuBlocked extends StatefulWidget {
|
|||
final Contact contact;
|
||||
|
||||
@override
|
||||
State<UserContextMenuBlocked> createState() => _UserContextMenuBlocked();
|
||||
State<UserContextMenu> createState() => _UserContextMenuBlocked();
|
||||
}
|
||||
|
||||
class _UserContextMenuBlocked extends State<UserContextMenuBlocked> {
|
||||
class _UserContextMenuBlocked extends State<UserContextMenu> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PieMenu(
|
||||
|
|
|
|||
|
|
@ -7,9 +7,8 @@ import 'package:twonly/src/database/twonly.db.dart';
|
|||
import 'package:twonly/src/services/api/utils.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/views/components/alert_dialog.dart';
|
||||
import 'package:twonly/src/views/components/avatar_icon.component.dart';
|
||||
import 'package:twonly/src/views/components/better_list_title.dart';
|
||||
import 'package:twonly/src/views/components/flame.dart';
|
||||
import 'package:twonly/src/views/components/initialsavatar.dart';
|
||||
import 'package:twonly/src/views/components/verified_shield.dart';
|
||||
import 'package:twonly/src/views/contact/contact_verify.view.dart';
|
||||
|
||||
|
|
@ -105,12 +104,12 @@ class _ContactViewState extends State<ContactView> {
|
|||
return Container();
|
||||
}
|
||||
final contact = snapshot.data!;
|
||||
final flameCounter = getFlameCounterFromContact(contact);
|
||||
// final flameCounter = getFlameCounterFromContact(contact);
|
||||
return ListView(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: ContactAvatar(contact: contact, fontSize: 30),
|
||||
child: AvatarIcon(contact: contact, fontSize: 30),
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
|
|
@ -123,12 +122,12 @@ class _ContactViewState extends State<ContactView> {
|
|||
getContactDisplayName(contact),
|
||||
style: const TextStyle(fontSize: 20),
|
||||
),
|
||||
if (flameCounter > 0)
|
||||
FlameCounterWidget(
|
||||
contact,
|
||||
flameCounter,
|
||||
prefix: true,
|
||||
),
|
||||
// if (flameCounter > 0)
|
||||
// FlameCounterWidget(
|
||||
// contact,
|
||||
// flameCounter,
|
||||
// prefix: true,
|
||||
// ),
|
||||
],
|
||||
),
|
||||
if (getContactDisplayName(contact) != contact.username)
|
||||
|
|
|
|||
18
lib/src/views/groups/group.view.dart
Normal file
18
lib/src/views/groups/group.view.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
|
|
@ -208,7 +208,8 @@ class _MemoriesPhotoSliderViewState extends State<MemoriesPhotoSliderView> {
|
|||
minScale: PhotoViewComputedScale.contained,
|
||||
maxScale: PhotoViewComputedScale.covered * 4.1,
|
||||
heroAttributes: PhotoViewHeroAttributes(
|
||||
tag: item.mediaService.mediaFile.mediaId),
|
||||
tag: item.mediaService.mediaFile.mediaId,
|
||||
),
|
||||
)
|
||||
: PhotoViewGalleryPageOptions(
|
||||
imageProvider: FileImage(item.mediaService.storedPath),
|
||||
|
|
@ -216,7 +217,8 @@ class _MemoriesPhotoSliderViewState extends State<MemoriesPhotoSliderView> {
|
|||
minScale: PhotoViewComputedScale.contained,
|
||||
maxScale: PhotoViewComputedScale.covered * 4.1,
|
||||
heroAttributes: PhotoViewHeroAttributes(
|
||||
tag: item.mediaService.mediaFile.mediaId),
|
||||
tag: item.mediaService.mediaFile.mediaId,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,20 +43,18 @@ class _AutomatedTestingViewState extends State<AutomatedTestingView> {
|
|||
await twonlyDB.contactsDao.getContactsByUsername(username);
|
||||
|
||||
for (final contact in contacts) {
|
||||
final groups =
|
||||
final group =
|
||||
await twonlyDB.groupsDao.getDirectChat(contact.userId);
|
||||
|
||||
for (final group in groups) {
|
||||
for (var i = 0; i < 200; i++) {
|
||||
setState(() {
|
||||
lotsOfMessagesStatus =
|
||||
'At message $i to ${contact.username}.';
|
||||
});
|
||||
await insertAndSendTextMessage(
|
||||
group.groupId,
|
||||
'Message $i.',
|
||||
);
|
||||
}
|
||||
for (var i = 0; i < 200; i++) {
|
||||
setState(() {
|
||||
lotsOfMessagesStatus =
|
||||
'At message $i to ${contact.username}.';
|
||||
});
|
||||
await insertAndSendTextMessage(
|
||||
group!.groupId,
|
||||
'Message $i.',
|
||||
null,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/services/flame.service.dart';
|
||||
import 'package:twonly/src/utils/storage.dart';
|
||||
import 'package:twonly/src/views/settings/developer/automated_testing.view.dart';
|
||||
import 'package:twonly/src/views/settings/developer/retransmission_data.view.dart';
|
||||
|
|
@ -69,14 +66,14 @@ class _DeveloperSettingsViewState extends State<DeveloperSettingsView> {
|
|||
);
|
||||
},
|
||||
),
|
||||
if (kDebugMode)
|
||||
ListTile(
|
||||
title: const Text('FlameSync Test'),
|
||||
onTap: () async {
|
||||
await twonlyDB.contactsDao.modifyFlameCounterForTesting();
|
||||
await syncFlameCounters();
|
||||
},
|
||||
),
|
||||
// if (kDebugMode)
|
||||
// ListTile(
|
||||
// title: const Text('FlameSync Test'),
|
||||
// onTap: () async {
|
||||
// await twonlyDB.contactsDao.modifyFlameCounterForTesting();
|
||||
// await syncFlameCounters();
|
||||
// },
|
||||
// ),
|
||||
if (kDebugMode)
|
||||
ListTile(
|
||||
title: const Text('Automated Testing'),
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import 'package:twonly/globals.dart';
|
|||
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/views/components/initialsavatar.dart';
|
||||
import 'package:twonly/src/views/components/avatar_icon.component.dart';
|
||||
import 'package:twonly/src/views/components/user_context_menu.component.dart';
|
||||
|
||||
class PrivacyViewBlockUsers extends StatefulWidget {
|
||||
|
|
@ -108,7 +108,7 @@ class UserList extends StatelessWidget {
|
|||
itemCount: users.length,
|
||||
itemBuilder: (BuildContext context, int i) {
|
||||
final user = users[i];
|
||||
return UserContextMenuBlocked(
|
||||
return UserContextMenu(
|
||||
contact: user,
|
||||
child: ListTile(
|
||||
title: Row(
|
||||
|
|
@ -116,7 +116,7 @@ class UserList extends StatelessWidget {
|
|||
Text(getContactDisplayName(user)),
|
||||
],
|
||||
),
|
||||
leading: ContactAvatar(contact: user, fontSize: 15),
|
||||
leading: AvatarIcon(contact: user, fontSize: 15),
|
||||
trailing: Checkbox(
|
||||
value: user.blocked,
|
||||
onChanged: (bool? value) async {
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
|||
import 'package:twonly/src/model/json/userdata.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/utils/storage.dart';
|
||||
import 'package:twonly/src/views/components/avatar_icon.component.dart';
|
||||
import 'package:twonly/src/views/components/better_list_title.dart';
|
||||
import 'package:twonly/src/views/components/initialsavatar.dart';
|
||||
import 'package:twonly/src/views/settings/account.view.dart';
|
||||
import 'package:twonly/src/views/settings/appearance.view.dart';
|
||||
import 'package:twonly/src/views/settings/backup/backup.view.dart';
|
||||
|
|
@ -72,7 +72,7 @@ class _SettingsMainViewState extends State<SettingsMainView> {
|
|||
color: context.color.surface.withAlpha(0),
|
||||
child: Row(
|
||||
children: [
|
||||
ContactAvatar(
|
||||
AvatarIcon(
|
||||
userData: userData,
|
||||
fontSize: 30,
|
||||
),
|
||||
|
|
|
|||
Loading…
Reference in a new issue