import 'package:clock/clock.dart' show clock; import 'package:drift/drift.dart'; import 'package:hashlib/random.dart'; import 'package:twonly/core/bridge.dart' show FlameState; import 'package:twonly/core/bridge/groups.dart' as rust_groups; import 'package:twonly/locator.dart'; import 'package:twonly/src/database/tables/groups.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/utils/misc.dart'; part 'groups.dao.g.dart'; @DriftAccessor( tables: [ Groups, GroupMembers, GroupHistories, ], ) class GroupsDao extends DatabaseAccessor with _$GroupsDaoMixin { // this constructor is required so that the main database can create an instance // of this object. // ignore: matching_super_parameters GroupsDao(super.db); Future deleteGroup(String groupId) async { await (delete(groups)..where((t) => t.groupId.equals(groupId))).go(); } Future updateGroup( String groupId, GroupsCompanion updates, ) async { await (update( groups, )..where((c) => c.groupId.equals(groupId))).write(updates); } Future> getGroupNonLeftMembers(String groupId) async { return (select(groupMembers)..where( (t) => t.groupId.equals(groupId) & (t.memberState.equals(MemberState.leftGroup.name).not() | t.memberState.isNull()), )) .get(); } Future> getAllGroupMembers(String groupId) async { return (select( groupMembers, )..where((t) => t.groupId.equals(groupId))).get(); } Future createNewGroup(GroupsCompanion group) async { return _insertGroup(group); } Future insertOrUpdateGroupMember(GroupMembersCompanion members) async { await into(groupMembers).insertOnConflictUpdate(members); } Future insertGroupAction(GroupHistoriesCompanion action) async { var insertAction = action; if (!action.groupHistoryId.present) { insertAction = action.copyWith( groupHistoryId: Value(uuid.v4()), ); } await into(groupHistories).insert(insertAction); } Stream> watchGroupActions( String groupId, { DateTime? since, }) { return (select(groupHistories) ..where( (t) => t.groupId.equals(groupId) & (since == null ? const Constant(true) : t.actionAt.isBiggerOrEqualValue(since)), ) ..orderBy([(t) => OrderingTerm.asc(t.actionAt)])) .watch(); } Future createNewDirectChat( int contactId, GroupsCompanion group, ) async { final groupIdDirectChat = getUUIDforDirectChat( contactId, userService.currentUser.userId, ); final insertGroup = group.copyWith( groupId: Value(groupIdDirectChat), isDirectChat: const Value(true), isGroupAdmin: const Value(true), joinedGroup: const Value(true), ); final result = await _insertGroup(insertGroup); if (result != null) { await into(groupMembers).insertOnConflictUpdate( GroupMembersCompanion( groupId: Value(result.groupId), contactId: Value( contactId, ), ), ); } return result; } Future _insertGroup(GroupsCompanion group) async { await into(groups).insertOnConflictUpdate(group); return (select( groups, )..where((t) => t.groupId.equals(group.groupId.value))).getSingleOrNull(); } Future> getGroupContact(String groupId) async { final query = (select(contacts).join([ leftOuterJoin( groupMembers, groupMembers.contactId.equalsExp(contacts.userId), useColumns: false, ), ]) ..orderBy([OrderingTerm.desc(groupMembers.lastMessage)]) ..where(groupMembers.groupId.equals(groupId))); return query.map((row) => row.readTable(contacts)).get(); } Stream> watchGroupContact(String groupId) { final query = (select(contacts).join([ leftOuterJoin( groupMembers, groupMembers.contactId.equalsExp(contacts.userId), useColumns: false, ), ]) ..orderBy([OrderingTerm.desc(groupMembers.lastMessage)]) ..where(groupMembers.groupId.equals(groupId))); return query.map((row) => row.readTable(contacts)).watch(); } Stream> watchGroupMembers(String groupId) { final query = (select(groupMembers)..where((t) => t.groupId.equals(groupId))).join([ leftOuterJoin( contacts, contacts.userId.equalsExp(groupMembers.contactId), ), ]); return query .map((row) => (row.readTable(contacts), row.readTable(groupMembers))) .watch(); } Stream> watchAllGroupMembers() { final query = select(groupMembers).join([ innerJoin(contacts, contacts.userId.equalsExp(groupMembers.contactId)), ]); return query .map((row) => (row.readTable(contacts), row.readTable(groupMembers))) .watch(); } Stream> watchGroupsForShareImage() { return (select(groups)..where( (g) => g.leftGroup.equals(false) & g.deletedContent.equals(false), )) .watch(); } Stream> watchGroupsAllowedForWidgetShare() { final query = select(groups).join([ innerJoin( groupMembers, groupMembers.groupId.equalsExp(groups.groupId), ), innerJoin( contacts, contacts.userId.equalsExp(groupMembers.contactId), ), ])..where( groups.isDirectChat.equals(true) & groups.leftGroup.equals(false) & groups.deletedContent.equals(false) & contacts.widgetSharingAllowed.equals(true), ); return query.map((row) => row.readTable(groups)).watch(); } Stream> watchContactGroupMember(int contactId) { return (select(groupMembers)..where( (g) => g.contactId.equals(contactId), )) .watch(); } Stream> watchTypingGroupMembers() { return (select( groupMembers, )..where((member) => member.lastTypeIndicator.isNotNull())).watch(); } Stream watchGroup(String groupId) { return (select( groups, )..where((t) => t.groupId.equals(groupId))).watchSingleOrNull(); } Stream> watchGroupsForChatList() { return (select(groups) ..where((t) => t.deletedContent.equals(false)) ..orderBy([(t) => OrderingTerm.desc(t.lastMessageExchange)])) .watch(); } Stream> watchGroupsForStartNewChat() { return (select(groups) ..where((t) => t.isDirectChat.equals(false)) ..orderBy([(t) => OrderingTerm.asc(t.groupName)])) .watch(); } Future getGroup(String groupId) { return (select( groups, )..where((t) => t.groupId.equals(groupId))).getSingleOrNull(); } /// Drift watches the row only to learn *when* to ask again: the counter /// itself is derived in Rust, because it depends on the current day as much /// as on the stored columns. Stream watchFlameCounter(String groupId) { return (select(groups)..where( (u) => u.groupId.equals(groupId) & u.lastMessageReceived.isNotNull() & u.lastMessageSend.isNotNull(), )) .watchSingleOrNull() .asyncMap((group) { if (group == null) { return const FlameState( counter: 0, isExpiring: false, isBestFriend: false, ); } return rust_groups.flameState(groupId: groupId); }); } Future> getAllDirectChats() { return (select(groups)..where((t) => t.isDirectChat.equals(true))).get(); } Future> getAllGroups() { return select(groups).get(); } Future getDirectChat(int userId) async { final query = ((select(groups)..where((t) => t.isDirectChat.equals(true))).join([ leftOuterJoin( groupMembers, groupMembers.groupId.equalsExp(groups.groupId), ), ])..where(groupMembers.contactId.equals(userId))); return query.map((row) => row.readTable(groups)).getSingleOrNull(); } Stream watchSumTotalMediaCounter() { final query = selectOnly(groups) ..addColumns([groups.totalMediaCounter.sum()]); return query.watch().map((rows) { final expr = rows.first.read(groups.totalMediaCounter.sum()); return expr ?? 0; }); } Future increaseLastMessageExchange( String groupId, DateTime newLastMessage, ) async { final now = clock.now(); final clampedLastMessage = newLastMessage.isAfter(now) ? now : newLastMessage; await (update(groups)..where( (t) => t.groupId.equals(groupId) & (t.lastMessageExchange.isSmallerThanValue(clampedLastMessage)), )) .write(GroupsCompanion(lastMessageExchange: Value(clampedLastMessage))); } Future increaseMemberLastMessage( String groupId, int contactId, DateTime newLastMessage, ) async { final now = clock.now(); final clampedLastMessage = newLastMessage.isAfter(now) ? now : newLastMessage; await (update(groupMembers)..where( (t) => t.groupId.equals(groupId) & t.contactId.equals(contactId) & (t.lastMessage.isNull() | t.lastMessage.isSmallerThanValue(clampedLastMessage)), )) .write(GroupMembersCompanion(lastMessage: Value(clampedLastMessage))); } Stream> watchNonDirectGroupsForMember(int contactId) { final query = select(groups).join([ innerJoin( groupMembers, groupMembers.groupId.equalsExp(groups.groupId), ), ])..where( groups.isDirectChat.equals(false) & groupMembers.contactId.equals(contactId), ); return query.map((row) => row.readTable(groups)).watch(); } }