allow deletion of chats and groups

This commit is contained in:
otsmr 2025-11-03 16:54:49 +01:00
parent 678a22dd11
commit 5235a01626
16 changed files with 339 additions and 127 deletions

View file

@ -164,7 +164,11 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
} }
Stream<List<Group>> watchGroupsForShareImage() { Stream<List<Group>> watchGroupsForShareImage() {
return (select(groups)..where((g) => g.leftGroup.equals(false))).watch(); return (select(groups)
..where(
(g) => g.leftGroup.equals(false) & g.deletedContent.equals(false),
))
.watch();
} }
Stream<Group?> watchGroup(String groupId) { Stream<Group?> watchGroup(String groupId) {
@ -174,10 +178,18 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
Stream<List<Group>> watchGroupsForChatList() { Stream<List<Group>> watchGroupsForChatList() {
return (select(groups) return (select(groups)
..where((t) => t.deletedContent.equals(false))
..orderBy([(t) => OrderingTerm.desc(t.lastMessageExchange)])) ..orderBy([(t) => OrderingTerm.desc(t.lastMessageExchange)]))
.watch(); .watch();
} }
Stream<List<Group>> watchGroupsForStartNewChat() {
return (select(groups)
..where((t) => t.isDirectChat.equals(false))
..orderBy([(t) => OrderingTerm.asc(t.groupName)]))
.watch();
}
Future<Group?> getGroup(String groupId) { Future<Group?> getGroup(String groupId) {
return (select(groups)..where((t) => t.groupId.equals(groupId))) return (select(groups)..where((t) => t.groupId.equals(groupId)))
.getSingleOrNull(); .getSingleOrNull();

View file

@ -380,6 +380,7 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
GroupsCompanion( GroupsCompanion(
lastMessageExchange: Value(DateTime.now()), lastMessageExchange: Value(DateTime.now()),
archived: const Value(false), archived: const Value(false),
deletedContent: const Value(false),
), ),
); );
@ -468,6 +469,10 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
return (delete(messages)..where((t) => t.messageId.equals(messageId))).go(); return (delete(messages)..where((t) => t.messageId.equals(messageId))).go();
} }
Future<void> deleteMessagesByGroupId(String groupId) {
return (delete(messages)..where((t) => t.groupId.equals(groupId))).go();
}
// Future<void> deleteAllMessagesByContactId(int contactId) { // Future<void> deleteAllMessagesByContactId(int contactId) {
// return (delete(messages)..where((t) => t.contactId.equals(contactId))).go(); // return (delete(messages)..where((t) => t.contactId.equals(contactId))).go();
// } // }

View file

@ -14,6 +14,8 @@ class Groups extends Table {
BoolColumn get joinedGroup => boolean().withDefault(const Constant(false))(); BoolColumn get joinedGroup => boolean().withDefault(const Constant(false))();
BoolColumn get leftGroup => boolean().withDefault(const Constant(false))(); BoolColumn get leftGroup => boolean().withDefault(const Constant(false))();
BoolColumn get deletedContent =>
boolean().withDefault(const Constant(false))();
IntColumn get stateVersionId => integer().withDefault(const Constant(0))(); IntColumn get stateVersionId => integer().withDefault(const Constant(0))();

View file

@ -724,6 +724,16 @@ class $GroupsTable extends Groups with TableInfo<$GroupsTable, Group> {
defaultConstraints: defaultConstraints:
GeneratedColumn.constraintIsAlways('CHECK ("left_group" IN (0, 1))'), GeneratedColumn.constraintIsAlways('CHECK ("left_group" IN (0, 1))'),
defaultValue: const Constant(false)); defaultValue: const Constant(false));
static const VerificationMeta _deletedContentMeta =
const VerificationMeta('deletedContent');
@override
late final GeneratedColumn<bool> deletedContent = GeneratedColumn<bool>(
'deleted_content', aliasedName, false,
type: DriftSqlType.bool,
requiredDuringInsert: false,
defaultConstraints: GeneratedColumn.constraintIsAlways(
'CHECK ("deleted_content" IN (0, 1))'),
defaultValue: const Constant(false));
static const VerificationMeta _stateVersionIdMeta = static const VerificationMeta _stateVersionIdMeta =
const VerificationMeta('stateVersionId'); const VerificationMeta('stateVersionId');
@override @override
@ -848,6 +858,7 @@ class $GroupsTable extends Groups with TableInfo<$GroupsTable, Group> {
archived, archived,
joinedGroup, joinedGroup,
leftGroup, leftGroup,
deletedContent,
stateVersionId, stateVersionId,
stateEncryptionKey, stateEncryptionKey,
myGroupPrivateKey, myGroupPrivateKey,
@ -911,6 +922,12 @@ class $GroupsTable extends Groups with TableInfo<$GroupsTable, Group> {
context.handle(_leftGroupMeta, context.handle(_leftGroupMeta,
leftGroup.isAcceptableOrUnknown(data['left_group']!, _leftGroupMeta)); leftGroup.isAcceptableOrUnknown(data['left_group']!, _leftGroupMeta));
} }
if (data.containsKey('deleted_content')) {
context.handle(
_deletedContentMeta,
deletedContent.isAcceptableOrUnknown(
data['deleted_content']!, _deletedContentMeta));
}
if (data.containsKey('state_version_id')) { if (data.containsKey('state_version_id')) {
context.handle( context.handle(
_stateVersionIdMeta, _stateVersionIdMeta,
@ -1029,6 +1046,8 @@ class $GroupsTable extends Groups with TableInfo<$GroupsTable, Group> {
.read(DriftSqlType.bool, data['${effectivePrefix}joined_group'])!, .read(DriftSqlType.bool, data['${effectivePrefix}joined_group'])!,
leftGroup: attachedDatabase.typeMapping leftGroup: attachedDatabase.typeMapping
.read(DriftSqlType.bool, data['${effectivePrefix}left_group'])!, .read(DriftSqlType.bool, data['${effectivePrefix}left_group'])!,
deletedContent: attachedDatabase.typeMapping
.read(DriftSqlType.bool, data['${effectivePrefix}deleted_content'])!,
stateVersionId: attachedDatabase.typeMapping stateVersionId: attachedDatabase.typeMapping
.read(DriftSqlType.int, data['${effectivePrefix}state_version_id'])!, .read(DriftSqlType.int, data['${effectivePrefix}state_version_id'])!,
stateEncryptionKey: attachedDatabase.typeMapping.read( stateEncryptionKey: attachedDatabase.typeMapping.read(
@ -1083,6 +1102,7 @@ class Group extends DataClass implements Insertable<Group> {
final bool archived; final bool archived;
final bool joinedGroup; final bool joinedGroup;
final bool leftGroup; final bool leftGroup;
final bool deletedContent;
final int stateVersionId; final int stateVersionId;
final Uint8List? stateEncryptionKey; final Uint8List? stateEncryptionKey;
final Uint8List? myGroupPrivateKey; final Uint8List? myGroupPrivateKey;
@ -1107,6 +1127,7 @@ class Group extends DataClass implements Insertable<Group> {
required this.archived, required this.archived,
required this.joinedGroup, required this.joinedGroup,
required this.leftGroup, required this.leftGroup,
required this.deletedContent,
required this.stateVersionId, required this.stateVersionId,
this.stateEncryptionKey, this.stateEncryptionKey,
this.myGroupPrivateKey, this.myGroupPrivateKey,
@ -1133,6 +1154,7 @@ class Group extends DataClass implements Insertable<Group> {
map['archived'] = Variable<bool>(archived); map['archived'] = Variable<bool>(archived);
map['joined_group'] = Variable<bool>(joinedGroup); map['joined_group'] = Variable<bool>(joinedGroup);
map['left_group'] = Variable<bool>(leftGroup); map['left_group'] = Variable<bool>(leftGroup);
map['deleted_content'] = Variable<bool>(deletedContent);
map['state_version_id'] = Variable<int>(stateVersionId); map['state_version_id'] = Variable<int>(stateVersionId);
if (!nullToAbsent || stateEncryptionKey != null) { if (!nullToAbsent || stateEncryptionKey != null) {
map['state_encryption_key'] = Variable<Uint8List>(stateEncryptionKey); map['state_encryption_key'] = Variable<Uint8List>(stateEncryptionKey);
@ -1177,6 +1199,7 @@ class Group extends DataClass implements Insertable<Group> {
archived: Value(archived), archived: Value(archived),
joinedGroup: Value(joinedGroup), joinedGroup: Value(joinedGroup),
leftGroup: Value(leftGroup), leftGroup: Value(leftGroup),
deletedContent: Value(deletedContent),
stateVersionId: Value(stateVersionId), stateVersionId: Value(stateVersionId),
stateEncryptionKey: stateEncryptionKey == null && nullToAbsent stateEncryptionKey: stateEncryptionKey == null && nullToAbsent
? const Value.absent() ? const Value.absent()
@ -1221,6 +1244,7 @@ class Group extends DataClass implements Insertable<Group> {
archived: serializer.fromJson<bool>(json['archived']), archived: serializer.fromJson<bool>(json['archived']),
joinedGroup: serializer.fromJson<bool>(json['joinedGroup']), joinedGroup: serializer.fromJson<bool>(json['joinedGroup']),
leftGroup: serializer.fromJson<bool>(json['leftGroup']), leftGroup: serializer.fromJson<bool>(json['leftGroup']),
deletedContent: serializer.fromJson<bool>(json['deletedContent']),
stateVersionId: serializer.fromJson<int>(json['stateVersionId']), stateVersionId: serializer.fromJson<int>(json['stateVersionId']),
stateEncryptionKey: stateEncryptionKey:
serializer.fromJson<Uint8List?>(json['stateEncryptionKey']), serializer.fromJson<Uint8List?>(json['stateEncryptionKey']),
@ -1257,6 +1281,7 @@ class Group extends DataClass implements Insertable<Group> {
'archived': serializer.toJson<bool>(archived), 'archived': serializer.toJson<bool>(archived),
'joinedGroup': serializer.toJson<bool>(joinedGroup), 'joinedGroup': serializer.toJson<bool>(joinedGroup),
'leftGroup': serializer.toJson<bool>(leftGroup), 'leftGroup': serializer.toJson<bool>(leftGroup),
'deletedContent': serializer.toJson<bool>(deletedContent),
'stateVersionId': serializer.toJson<int>(stateVersionId), 'stateVersionId': serializer.toJson<int>(stateVersionId),
'stateEncryptionKey': serializer.toJson<Uint8List?>(stateEncryptionKey), 'stateEncryptionKey': serializer.toJson<Uint8List?>(stateEncryptionKey),
'myGroupPrivateKey': serializer.toJson<Uint8List?>(myGroupPrivateKey), 'myGroupPrivateKey': serializer.toJson<Uint8List?>(myGroupPrivateKey),
@ -1286,6 +1311,7 @@ class Group extends DataClass implements Insertable<Group> {
bool? archived, bool? archived,
bool? joinedGroup, bool? joinedGroup,
bool? leftGroup, bool? leftGroup,
bool? deletedContent,
int? stateVersionId, int? stateVersionId,
Value<Uint8List?> stateEncryptionKey = const Value.absent(), Value<Uint8List?> stateEncryptionKey = const Value.absent(),
Value<Uint8List?> myGroupPrivateKey = const Value.absent(), Value<Uint8List?> myGroupPrivateKey = const Value.absent(),
@ -1310,6 +1336,7 @@ class Group extends DataClass implements Insertable<Group> {
archived: archived ?? this.archived, archived: archived ?? this.archived,
joinedGroup: joinedGroup ?? this.joinedGroup, joinedGroup: joinedGroup ?? this.joinedGroup,
leftGroup: leftGroup ?? this.leftGroup, leftGroup: leftGroup ?? this.leftGroup,
deletedContent: deletedContent ?? this.deletedContent,
stateVersionId: stateVersionId ?? this.stateVersionId, stateVersionId: stateVersionId ?? this.stateVersionId,
stateEncryptionKey: stateEncryptionKey.present stateEncryptionKey: stateEncryptionKey.present
? stateEncryptionKey.value ? stateEncryptionKey.value
@ -1355,6 +1382,9 @@ class Group extends DataClass implements Insertable<Group> {
joinedGroup: joinedGroup:
data.joinedGroup.present ? data.joinedGroup.value : this.joinedGroup, data.joinedGroup.present ? data.joinedGroup.value : this.joinedGroup,
leftGroup: data.leftGroup.present ? data.leftGroup.value : this.leftGroup, leftGroup: data.leftGroup.present ? data.leftGroup.value : this.leftGroup,
deletedContent: data.deletedContent.present
? data.deletedContent.value
: this.deletedContent,
stateVersionId: data.stateVersionId.present stateVersionId: data.stateVersionId.present
? data.stateVersionId.value ? data.stateVersionId.value
: this.stateVersionId, : this.stateVersionId,
@ -1413,6 +1443,7 @@ class Group extends DataClass implements Insertable<Group> {
..write('archived: $archived, ') ..write('archived: $archived, ')
..write('joinedGroup: $joinedGroup, ') ..write('joinedGroup: $joinedGroup, ')
..write('leftGroup: $leftGroup, ') ..write('leftGroup: $leftGroup, ')
..write('deletedContent: $deletedContent, ')
..write('stateVersionId: $stateVersionId, ') ..write('stateVersionId: $stateVersionId, ')
..write('stateEncryptionKey: $stateEncryptionKey, ') ..write('stateEncryptionKey: $stateEncryptionKey, ')
..write('myGroupPrivateKey: $myGroupPrivateKey, ') ..write('myGroupPrivateKey: $myGroupPrivateKey, ')
@ -1443,6 +1474,7 @@ class Group extends DataClass implements Insertable<Group> {
archived, archived,
joinedGroup, joinedGroup,
leftGroup, leftGroup,
deletedContent,
stateVersionId, stateVersionId,
$driftBlobEquality.hash(stateEncryptionKey), $driftBlobEquality.hash(stateEncryptionKey),
$driftBlobEquality.hash(myGroupPrivateKey), $driftBlobEquality.hash(myGroupPrivateKey),
@ -1471,6 +1503,7 @@ class Group extends DataClass implements Insertable<Group> {
other.archived == this.archived && other.archived == this.archived &&
other.joinedGroup == this.joinedGroup && other.joinedGroup == this.joinedGroup &&
other.leftGroup == this.leftGroup && other.leftGroup == this.leftGroup &&
other.deletedContent == this.deletedContent &&
other.stateVersionId == this.stateVersionId && other.stateVersionId == this.stateVersionId &&
$driftBlobEquality.equals( $driftBlobEquality.equals(
other.stateEncryptionKey, this.stateEncryptionKey) && other.stateEncryptionKey, this.stateEncryptionKey) &&
@ -1500,6 +1533,7 @@ class GroupsCompanion extends UpdateCompanion<Group> {
final Value<bool> archived; final Value<bool> archived;
final Value<bool> joinedGroup; final Value<bool> joinedGroup;
final Value<bool> leftGroup; final Value<bool> leftGroup;
final Value<bool> deletedContent;
final Value<int> stateVersionId; final Value<int> stateVersionId;
final Value<Uint8List?> stateEncryptionKey; final Value<Uint8List?> stateEncryptionKey;
final Value<Uint8List?> myGroupPrivateKey; final Value<Uint8List?> myGroupPrivateKey;
@ -1525,6 +1559,7 @@ class GroupsCompanion extends UpdateCompanion<Group> {
this.archived = const Value.absent(), this.archived = const Value.absent(),
this.joinedGroup = const Value.absent(), this.joinedGroup = const Value.absent(),
this.leftGroup = const Value.absent(), this.leftGroup = const Value.absent(),
this.deletedContent = const Value.absent(),
this.stateVersionId = const Value.absent(), this.stateVersionId = const Value.absent(),
this.stateEncryptionKey = const Value.absent(), this.stateEncryptionKey = const Value.absent(),
this.myGroupPrivateKey = const Value.absent(), this.myGroupPrivateKey = const Value.absent(),
@ -1551,6 +1586,7 @@ class GroupsCompanion extends UpdateCompanion<Group> {
this.archived = const Value.absent(), this.archived = const Value.absent(),
this.joinedGroup = const Value.absent(), this.joinedGroup = const Value.absent(),
this.leftGroup = const Value.absent(), this.leftGroup = const Value.absent(),
this.deletedContent = const Value.absent(),
this.stateVersionId = const Value.absent(), this.stateVersionId = const Value.absent(),
this.stateEncryptionKey = const Value.absent(), this.stateEncryptionKey = const Value.absent(),
this.myGroupPrivateKey = const Value.absent(), this.myGroupPrivateKey = const Value.absent(),
@ -1578,6 +1614,7 @@ class GroupsCompanion extends UpdateCompanion<Group> {
Expression<bool>? archived, Expression<bool>? archived,
Expression<bool>? joinedGroup, Expression<bool>? joinedGroup,
Expression<bool>? leftGroup, Expression<bool>? leftGroup,
Expression<bool>? deletedContent,
Expression<int>? stateVersionId, Expression<int>? stateVersionId,
Expression<Uint8List>? stateEncryptionKey, Expression<Uint8List>? stateEncryptionKey,
Expression<Uint8List>? myGroupPrivateKey, Expression<Uint8List>? myGroupPrivateKey,
@ -1604,6 +1641,7 @@ class GroupsCompanion extends UpdateCompanion<Group> {
if (archived != null) 'archived': archived, if (archived != null) 'archived': archived,
if (joinedGroup != null) 'joined_group': joinedGroup, if (joinedGroup != null) 'joined_group': joinedGroup,
if (leftGroup != null) 'left_group': leftGroup, if (leftGroup != null) 'left_group': leftGroup,
if (deletedContent != null) 'deleted_content': deletedContent,
if (stateVersionId != null) 'state_version_id': stateVersionId, if (stateVersionId != null) 'state_version_id': stateVersionId,
if (stateEncryptionKey != null) if (stateEncryptionKey != null)
'state_encryption_key': stateEncryptionKey, 'state_encryption_key': stateEncryptionKey,
@ -1638,6 +1676,7 @@ class GroupsCompanion extends UpdateCompanion<Group> {
Value<bool>? archived, Value<bool>? archived,
Value<bool>? joinedGroup, Value<bool>? joinedGroup,
Value<bool>? leftGroup, Value<bool>? leftGroup,
Value<bool>? deletedContent,
Value<int>? stateVersionId, Value<int>? stateVersionId,
Value<Uint8List?>? stateEncryptionKey, Value<Uint8List?>? stateEncryptionKey,
Value<Uint8List?>? myGroupPrivateKey, Value<Uint8List?>? myGroupPrivateKey,
@ -1663,6 +1702,7 @@ class GroupsCompanion extends UpdateCompanion<Group> {
archived: archived ?? this.archived, archived: archived ?? this.archived,
joinedGroup: joinedGroup ?? this.joinedGroup, joinedGroup: joinedGroup ?? this.joinedGroup,
leftGroup: leftGroup ?? this.leftGroup, leftGroup: leftGroup ?? this.leftGroup,
deletedContent: deletedContent ?? this.deletedContent,
stateVersionId: stateVersionId ?? this.stateVersionId, stateVersionId: stateVersionId ?? this.stateVersionId,
stateEncryptionKey: stateEncryptionKey ?? this.stateEncryptionKey, stateEncryptionKey: stateEncryptionKey ?? this.stateEncryptionKey,
myGroupPrivateKey: myGroupPrivateKey ?? this.myGroupPrivateKey, myGroupPrivateKey: myGroupPrivateKey ?? this.myGroupPrivateKey,
@ -1709,6 +1749,9 @@ class GroupsCompanion extends UpdateCompanion<Group> {
if (leftGroup.present) { if (leftGroup.present) {
map['left_group'] = Variable<bool>(leftGroup.value); map['left_group'] = Variable<bool>(leftGroup.value);
} }
if (deletedContent.present) {
map['deleted_content'] = Variable<bool>(deletedContent.value);
}
if (stateVersionId.present) { if (stateVersionId.present) {
map['state_version_id'] = Variable<int>(stateVersionId.value); map['state_version_id'] = Variable<int>(stateVersionId.value);
} }
@ -1780,6 +1823,7 @@ class GroupsCompanion extends UpdateCompanion<Group> {
..write('archived: $archived, ') ..write('archived: $archived, ')
..write('joinedGroup: $joinedGroup, ') ..write('joinedGroup: $joinedGroup, ')
..write('leftGroup: $leftGroup, ') ..write('leftGroup: $leftGroup, ')
..write('deletedContent: $deletedContent, ')
..write('stateVersionId: $stateVersionId, ') ..write('stateVersionId: $stateVersionId, ')
..write('stateEncryptionKey: $stateEncryptionKey, ') ..write('stateEncryptionKey: $stateEncryptionKey, ')
..write('myGroupPrivateKey: $myGroupPrivateKey, ') ..write('myGroupPrivateKey: $myGroupPrivateKey, ')
@ -8333,6 +8377,7 @@ typedef $$GroupsTableCreateCompanionBuilder = GroupsCompanion Function({
Value<bool> archived, Value<bool> archived,
Value<bool> joinedGroup, Value<bool> joinedGroup,
Value<bool> leftGroup, Value<bool> leftGroup,
Value<bool> deletedContent,
Value<int> stateVersionId, Value<int> stateVersionId,
Value<Uint8List?> stateEncryptionKey, Value<Uint8List?> stateEncryptionKey,
Value<Uint8List?> myGroupPrivateKey, Value<Uint8List?> myGroupPrivateKey,
@ -8359,6 +8404,7 @@ typedef $$GroupsTableUpdateCompanionBuilder = GroupsCompanion Function({
Value<bool> archived, Value<bool> archived,
Value<bool> joinedGroup, Value<bool> joinedGroup,
Value<bool> leftGroup, Value<bool> leftGroup,
Value<bool> deletedContent,
Value<int> stateVersionId, Value<int> stateVersionId,
Value<Uint8List?> stateEncryptionKey, Value<Uint8List?> stateEncryptionKey,
Value<Uint8List?> myGroupPrivateKey, Value<Uint8List?> myGroupPrivateKey,
@ -8459,6 +8505,10 @@ class $$GroupsTableFilterComposer extends Composer<_$TwonlyDB, $GroupsTable> {
ColumnFilters<bool> get leftGroup => $composableBuilder( ColumnFilters<bool> get leftGroup => $composableBuilder(
column: $table.leftGroup, builder: (column) => ColumnFilters(column)); column: $table.leftGroup, builder: (column) => ColumnFilters(column));
ColumnFilters<bool> get deletedContent => $composableBuilder(
column: $table.deletedContent,
builder: (column) => ColumnFilters(column));
ColumnFilters<int> get stateVersionId => $composableBuilder( ColumnFilters<int> get stateVersionId => $composableBuilder(
column: $table.stateVersionId, column: $table.stateVersionId,
builder: (column) => ColumnFilters(column)); builder: (column) => ColumnFilters(column));
@ -8614,6 +8664,10 @@ class $$GroupsTableOrderingComposer extends Composer<_$TwonlyDB, $GroupsTable> {
ColumnOrderings<bool> get leftGroup => $composableBuilder( ColumnOrderings<bool> get leftGroup => $composableBuilder(
column: $table.leftGroup, builder: (column) => ColumnOrderings(column)); column: $table.leftGroup, builder: (column) => ColumnOrderings(column));
ColumnOrderings<bool> get deletedContent => $composableBuilder(
column: $table.deletedContent,
builder: (column) => ColumnOrderings(column));
ColumnOrderings<int> get stateVersionId => $composableBuilder( ColumnOrderings<int> get stateVersionId => $composableBuilder(
column: $table.stateVersionId, column: $table.stateVersionId,
builder: (column) => ColumnOrderings(column)); builder: (column) => ColumnOrderings(column));
@ -8708,6 +8762,9 @@ class $$GroupsTableAnnotationComposer
GeneratedColumn<bool> get leftGroup => GeneratedColumn<bool> get leftGroup =>
$composableBuilder(column: $table.leftGroup, builder: (column) => column); $composableBuilder(column: $table.leftGroup, builder: (column) => column);
GeneratedColumn<bool> get deletedContent => $composableBuilder(
column: $table.deletedContent, builder: (column) => column);
GeneratedColumn<int> get stateVersionId => $composableBuilder( GeneratedColumn<int> get stateVersionId => $composableBuilder(
column: $table.stateVersionId, builder: (column) => column); column: $table.stateVersionId, builder: (column) => column);
@ -8853,6 +8910,7 @@ class $$GroupsTableTableManager extends RootTableManager<
Value<bool> archived = const Value.absent(), Value<bool> archived = const Value.absent(),
Value<bool> joinedGroup = const Value.absent(), Value<bool> joinedGroup = const Value.absent(),
Value<bool> leftGroup = const Value.absent(), Value<bool> leftGroup = const Value.absent(),
Value<bool> deletedContent = const Value.absent(),
Value<int> stateVersionId = const Value.absent(), Value<int> stateVersionId = const Value.absent(),
Value<Uint8List?> stateEncryptionKey = const Value.absent(), Value<Uint8List?> stateEncryptionKey = const Value.absent(),
Value<Uint8List?> myGroupPrivateKey = const Value.absent(), Value<Uint8List?> myGroupPrivateKey = const Value.absent(),
@ -8879,6 +8937,7 @@ class $$GroupsTableTableManager extends RootTableManager<
archived: archived, archived: archived,
joinedGroup: joinedGroup, joinedGroup: joinedGroup,
leftGroup: leftGroup, leftGroup: leftGroup,
deletedContent: deletedContent,
stateVersionId: stateVersionId, stateVersionId: stateVersionId,
stateEncryptionKey: stateEncryptionKey, stateEncryptionKey: stateEncryptionKey,
myGroupPrivateKey: myGroupPrivateKey, myGroupPrivateKey: myGroupPrivateKey,
@ -8905,6 +8964,7 @@ class $$GroupsTableTableManager extends RootTableManager<
Value<bool> archived = const Value.absent(), Value<bool> archived = const Value.absent(),
Value<bool> joinedGroup = const Value.absent(), Value<bool> joinedGroup = const Value.absent(),
Value<bool> leftGroup = const Value.absent(), Value<bool> leftGroup = const Value.absent(),
Value<bool> deletedContent = const Value.absent(),
Value<int> stateVersionId = const Value.absent(), Value<int> stateVersionId = const Value.absent(),
Value<Uint8List?> stateEncryptionKey = const Value.absent(), Value<Uint8List?> stateEncryptionKey = const Value.absent(),
Value<Uint8List?> myGroupPrivateKey = const Value.absent(), Value<Uint8List?> myGroupPrivateKey = const Value.absent(),
@ -8931,6 +8991,7 @@ class $$GroupsTableTableManager extends RootTableManager<
archived: archived, archived: archived,
joinedGroup: joinedGroup, joinedGroup: joinedGroup,
leftGroup: leftGroup, leftGroup: leftGroup,
deletedContent: deletedContent,
stateVersionId: stateVersionId, stateVersionId: stateVersionId,
stateEncryptionKey: stateEncryptionKey, stateEncryptionKey: stateEncryptionKey,
myGroupPrivateKey: myGroupPrivateKey, myGroupPrivateKey: myGroupPrivateKey,

View file

@ -80,6 +80,7 @@
"@shareImageUserNotVerifiedDesc": {}, "@shareImageUserNotVerifiedDesc": {},
"shareImageShowArchived": "Archivierte Benutzer anzeigen", "shareImageShowArchived": "Archivierte Benutzer anzeigen",
"@shareImageShowArchived": {}, "@shareImageShowArchived": {},
"startNewChatSearchHint": "Name, Benutzername oder Gruppenname",
"searchUsernameInput": "Benutzername", "searchUsernameInput": "Benutzername",
"@searchUsernameInput": {}, "@searchUsernameInput": {},
"searchUsernameTitle": "Benutzernamen suchen", "searchUsernameTitle": "Benutzernamen suchen",
@ -692,6 +693,9 @@
"@durationShortHour": {}, "@durationShortHour": {},
"durationShortDays": "Tagen", "durationShortDays": "Tagen",
"@durationShortDays": {}, "@durationShortDays": {},
"contacts": "Kontakte",
"groups": "Gruppen",
"@groups": {},
"newGroup": "Neue Gruppe", "newGroup": "Neue Gruppe",
"@newGroup": {}, "@newGroup": {},
"selectMembers": "Mitglieder auswählen", "selectMembers": "Mitglieder auswählen",

View file

@ -66,6 +66,7 @@
"@shareImagedEditorSavedImage": {}, "@shareImagedEditorSavedImage": {},
"shareImageSearchAllContacts": "Search all contacts", "shareImageSearchAllContacts": "Search all contacts",
"@shareImageSearchAllContacts": {}, "@shareImageSearchAllContacts": {},
"startNewChatSearchHint": "Name, username or groupname",
"shareImagedSelectAll": "Select all", "shareImagedSelectAll": "Select all",
"@shareImagedSelectAll": {}, "@shareImagedSelectAll": {},
"startNewChatTitle": "Select Contact", "startNewChatTitle": "Select Contact",
@ -516,6 +517,8 @@
"durationShortMinute": "Min.", "durationShortMinute": "Min.",
"durationShortHour": "Hrs.", "durationShortHour": "Hrs.",
"durationShortDays": "Days", "durationShortDays": "Days",
"contacts": "Contacts",
"groups": "Groups",
"newGroup": "New group", "newGroup": "New group",
"selectMembers": "Select members", "selectMembers": "Select members",
"selectGroupName": "Select group name", "selectGroupName": "Select group name",

View file

@ -302,6 +302,12 @@ abstract class AppLocalizations {
/// **'Search all contacts'** /// **'Search all contacts'**
String get shareImageSearchAllContacts; String get shareImageSearchAllContacts;
/// No description provided for @startNewChatSearchHint.
///
/// In en, this message translates to:
/// **'Name, username or groupname'**
String get startNewChatSearchHint;
/// No description provided for @shareImagedSelectAll. /// No description provided for @shareImagedSelectAll.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@ -2198,6 +2204,18 @@ abstract class AppLocalizations {
/// **'Days'** /// **'Days'**
String get durationShortDays; String get durationShortDays;
/// No description provided for @contacts.
///
/// In en, this message translates to:
/// **'Contacts'**
String get contacts;
/// No description provided for @groups.
///
/// In en, this message translates to:
/// **'Groups'**
String get groups;
/// No description provided for @newGroup. /// No description provided for @newGroup.
/// ///
/// In en, this message translates to: /// In en, this message translates to:

View file

@ -122,6 +122,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get shareImageSearchAllContacts => 'Alle Kontakte durchsuchen'; String get shareImageSearchAllContacts => 'Alle Kontakte durchsuchen';
@override
String get startNewChatSearchHint => 'Name, Benutzername oder Gruppenname';
@override @override
String get shareImagedSelectAll => 'Alle auswählen'; String get shareImagedSelectAll => 'Alle auswählen';
@ -1166,6 +1169,12 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get durationShortDays => 'Tagen'; String get durationShortDays => 'Tagen';
@override
String get contacts => 'Kontakte';
@override
String get groups => 'Gruppen';
@override @override
String get newGroup => 'Neue Gruppe'; String get newGroup => 'Neue Gruppe';

View file

@ -121,6 +121,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get shareImageSearchAllContacts => 'Search all contacts'; String get shareImageSearchAllContacts => 'Search all contacts';
@override
String get startNewChatSearchHint => 'Name, username or groupname';
@override @override
String get shareImagedSelectAll => 'Select all'; String get shareImagedSelectAll => 'Select all';
@ -1159,6 +1162,12 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get durationShortDays => 'Days'; String get durationShortDays => 'Days';
@override
String get contacts => 'Contacts';
@override
String get groups => 'Groups';
@override @override
String get newGroup => 'New group'; String get newGroup => 'New group';

View file

@ -74,6 +74,7 @@ Future<void> insertMediaFileInMessagesTable(
message.groupId, message.groupId,
const GroupsCompanion( const GroupsCompanion(
archived: Value(false), archived: Value(false),
deletedContent: Value(false),
), ),
); );
} else { } else {

View file

@ -191,7 +191,7 @@ class ContactsListView extends StatelessWidget {
Tooltip( Tooltip(
message: context.lang.searchUserNameArchiveUserTooltip, message: context.lang.searchUserNameArchiveUserTooltip,
child: IconButton( child: IconButton(
icon: const FaIcon(FontAwesomeIcons.boxArchive, size: 15), icon: const FaIcon(Icons.archive_outlined, size: 15),
onPressed: () async { onPressed: () async {
const update = ContactsCompanion(requested: Value(false)); const update = ContactsCompanion(requested: Value(false));
await twonlyDB.contactsDao.updateContact(contact.userId, update); await twonlyDB.contactsDao.updateContact(contact.userId, update);

View file

@ -76,9 +76,9 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
String currentInputText = ''; String currentInputText = '';
late StreamSubscription<Group?> userSub; late StreamSubscription<Group?> userSub;
late StreamSubscription<List<Message>> messageSub; late StreamSubscription<List<Message>> messageSub;
late StreamSubscription<List<GroupHistory>>? groupActionsSub; StreamSubscription<List<GroupHistory>>? groupActionsSub;
late StreamSubscription<List<Contact>>? contactSub; StreamSubscription<List<Contact>>? contactSub;
late StreamSubscription<Future<List<(Message, Contact)>>>? StreamSubscription<Future<List<(Message, Contact)>>>?
lastOpenedMessageByContactSub; lastOpenedMessageByContactSub;
Map<int, Contact> userIdToContact = {}; Map<int, Contact> userIdToContact = {};

View file

@ -47,16 +47,16 @@ class ChatTextEntry extends StatelessWidget {
var displayTime = !combineTextMessageWithNext(message, nextMessage); var displayTime = !combineTextMessageWithNext(message, nextMessage);
var displayUserName = ''; var displayUserName = '';
if (message.senderId != null && userIdToContact != null) { if (message.senderId != null &&
userIdToContact != null &&
userIdToContact![message.senderId] != null) {
if (prevMessage == null) { if (prevMessage == null) {
displayUserName = displayUserName =
getContactDisplayName(userIdToContact![message.senderId]!); getContactDisplayName(userIdToContact![message.senderId]!);
} else { } else {
if (!combineTextMessageWithNext(prevMessage!, message)) { if (!combineTextMessageWithNext(prevMessage!, message)) {
if (userIdToContact![message.senderId] != null) { displayUserName =
displayUserName = getContactDisplayName(userIdToContact![message.senderId]!);
getContactDisplayName(userIdToContact![message.senderId]!);
}
} }
} }
} }

View file

@ -10,6 +10,7 @@ import 'package:twonly/src/views/chats/add_new_user.view.dart';
import 'package:twonly/src/views/chats/chat_messages.view.dart'; import 'package:twonly/src/views/chats/chat_messages.view.dart';
import 'package:twonly/src/views/components/avatar_icon.component.dart'; import 'package:twonly/src/views/components/avatar_icon.component.dart';
import 'package:twonly/src/views/components/flame.dart'; import 'package:twonly/src/views/components/flame.dart';
import 'package:twonly/src/views/components/group_context_menu.component.dart';
import 'package:twonly/src/views/components/user_context_menu.component.dart'; import 'package:twonly/src/views/components/user_context_menu.component.dart';
import 'package:twonly/src/views/groups/group_create_select_members.view.dart'; import 'package:twonly/src/views/groups/group_create_select_members.view.dart';
@ -20,18 +21,20 @@ class StartNewChatView extends StatefulWidget {
} }
class _StartNewChatView extends State<StartNewChatView> { class _StartNewChatView extends State<StartNewChatView> {
List<Contact> contacts = []; List<Contact> filteredContacts = [];
List<Group> filteredGroups = [];
List<Contact> allContacts = []; List<Contact> allContacts = [];
List<Group> allNonDirectGroups = [];
final TextEditingController searchUserName = TextEditingController(); final TextEditingController searchUserName = TextEditingController();
late StreamSubscription<List<Contact>> contactSub; late StreamSubscription<List<Contact>> contactSub;
late StreamSubscription<List<Group>> allNonDirectGroupsSub;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
final stream = twonlyDB.contactsDao.watchAllAcceptedContacts(); contactSub =
twonlyDB.contactsDao.watchAllAcceptedContacts().listen((update) async {
contactSub = stream.listen((update) async {
update.sort( update.sort(
(a, b) => getContactDisplayName(a).compareTo(getContactDisplayName(b)), (a, b) => getContactDisplayName(a).compareTo(getContactDisplayName(b)),
); );
@ -40,18 +43,28 @@ class _StartNewChatView extends State<StartNewChatView> {
}); });
await filterUsers(); await filterUsers();
}); });
allNonDirectGroupsSub =
twonlyDB.groupsDao.watchGroupsForStartNewChat().listen((update) async {
setState(() {
allNonDirectGroups = update;
});
await filterUsers();
});
} }
@override @override
void dispose() { void dispose() {
unawaited(contactSub.cancel()); allNonDirectGroupsSub.cancel();
contactSub.cancel();
super.dispose(); super.dispose();
} }
Future<void> filterUsers() async { Future<void> filterUsers() async {
if (searchUserName.value.text.isEmpty) { if (searchUserName.value.text.isEmpty) {
setState(() { setState(() {
contacts = allContacts; filteredContacts = allContacts;
filteredGroups = [];
}); });
return; return;
} }
@ -62,11 +75,54 @@ class _StartNewChatView extends State<StartNewChatView> {
.contains(searchUserName.value.text.toLowerCase()), .contains(searchUserName.value.text.toLowerCase()),
) )
.toList(); .toList();
final groupsFiltered = allNonDirectGroups
.where(
(g) => g.groupName
.toLowerCase()
.contains(searchUserName.value.text.toLowerCase()),
)
.toList();
setState(() { setState(() {
contacts = usersFiltered; filteredContacts = usersFiltered;
filteredGroups = groupsFiltered;
}); });
} }
Future<void> _onTapUser(Contact user) async {
var directChat = await twonlyDB.groupsDao.getDirectChat(user.userId);
if (directChat == null) {
await twonlyDB.groupsDao.createNewDirectChat(
user.userId,
GroupsCompanion(
groupName: Value(
getContactDisplayName(user),
),
),
);
directChat = await twonlyDB.groupsDao.getDirectChat(user.userId);
}
if (!mounted) return;
await Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) {
return ChatMessagesView(directChat!);
},
),
);
}
Future<void> _onTapGroup(Group group) async {
await Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) {
return ChatMessagesView(group);
},
),
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
@ -88,14 +144,137 @@ class _StartNewChatView extends State<StartNewChatView> {
controller: searchUserName, controller: searchUserName,
decoration: getInputDecoration( decoration: getInputDecoration(
context, context,
context.lang.shareImageSearchAllContacts, context.lang.startNewChatSearchHint,
), ),
), ),
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
Expanded( Expanded(
child: UserList( child: ListView.builder(
contacts, restorationId: 'new_message_users_list',
itemCount:
filteredContacts.length + 3 + filteredGroups.length,
itemBuilder: (BuildContext context, int i) {
if (searchUserName.text.isEmpty) {
if (i == 0) {
return ListTile(
title: Text(context.lang.newGroup),
leading: const CircleAvatar(
child: FaIcon(
FontAwesomeIcons.userGroup,
size: 13,
),
),
onTap: () async {
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
const GroupCreateSelectMembersView(),
),
);
},
);
}
if (i == 1) {
return ListTile(
title: Text(context.lang.startNewChatNewContact),
leading: const CircleAvatar(
child: FaIcon(
FontAwesomeIcons.userPlus,
size: 13,
),
),
onTap: () async {
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const AddNewUserView(),
),
);
},
);
}
if (i == 2) {
return const Divider();
}
i = i - 3;
} else {
if (i == 0) {
return filteredContacts.isNotEmpty
? ListTile(
title: Text(
context.lang.contacts,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
)
: Container();
} else {
i -= 1;
}
}
if (i < filteredContacts.length) {
return UserContextMenu(
key: Key(filteredContacts[i].userId.toString()),
contact: filteredContacts[i],
child: ListTile(
title: Row(
children: [
Text(getContactDisplayName(filteredContacts[i])),
FlameCounterWidget(
contactId: filteredContacts[i].userId,
prefix: true,
),
],
),
leading: AvatarIcon(
contactId: filteredContacts[i].userId,
fontSize: 13,
),
onTap: () => _onTapUser(filteredContacts[i]),
),
);
}
i -= filteredContacts.length;
if (i == 0) {
return filteredGroups.isNotEmpty
? ListTile(
title: Text(
context.lang.groups,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
)
: Container();
}
i -= 1;
if (i < filteredGroups.length) {
return GroupContextMenu(
key: Key(filteredGroups[i].groupId),
group: filteredGroups[i],
child: ListTile(
title: Text(
filteredGroups[i].groupName,
),
leading: AvatarIcon(
group: filteredGroups[i],
fontSize: 13,
),
onTap: () => _onTapGroup(filteredGroups[i]),
),
);
}
return Container();
},
), ),
), ),
], ],
@ -105,107 +284,3 @@ class _StartNewChatView extends State<StartNewChatView> {
); );
} }
} }
class UserList extends StatelessWidget {
const UserList(
this.users, {
super.key,
});
final List<Contact> users;
@override
Widget build(BuildContext context) {
return ListView.builder(
restorationId: 'new_message_users_list',
itemCount: users.length + 3,
itemBuilder: (BuildContext context, int i) {
if (i == 1) {
return ListTile(
title: Text(context.lang.startNewChatNewContact),
leading: const CircleAvatar(
child: FaIcon(
FontAwesomeIcons.userPlus,
size: 13,
),
),
onTap: () async {
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const AddNewUserView(),
),
);
},
);
}
if (i == 0) {
return ListTile(
title: Text(context.lang.newGroup),
leading: const CircleAvatar(
child: FaIcon(
FontAwesomeIcons.userGroup,
size: 13,
),
),
onTap: () async {
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const GroupCreateSelectMembersView(),
),
);
},
);
}
if (i == 2) {
return const Divider();
}
final user = users[i - 3];
return UserContextMenu(
key: Key(user.userId.toString()),
contact: user,
child: ListTile(
title: Row(
children: [
Text(getContactDisplayName(user)),
FlameCounterWidget(
contactId: user.userId,
prefix: true,
),
],
),
leading: AvatarIcon(
contactId: user.userId,
fontSize: 13,
),
onTap: () async {
var directChat =
await twonlyDB.groupsDao.getDirectChat(user.userId);
if (directChat == null) {
await twonlyDB.groupsDao.createNewDirectChat(
user.userId,
GroupsCompanion(
groupName: Value(
getContactDisplayName(user),
),
),
);
directChat =
await twonlyDB.groupsDao.getDirectChat(user.userId);
}
if (!context.mounted) return;
await Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) {
return ChatMessagesView(directChat!);
},
),
);
},
),
);
},
);
}
}

View file

@ -29,7 +29,7 @@ class GroupContextMenu extends StatelessWidget {
await twonlyDB.groupsDao.updateGroup(group.groupId, update); await twonlyDB.groupsDao.updateGroup(group.groupId, update);
} }
}, },
icon: FontAwesomeIcons.boxArchive, icon: Icons.archive_outlined,
), ),
if (group.archived) if (group.archived)
ContextMenuItem( ContextMenuItem(
@ -40,7 +40,7 @@ class GroupContextMenu extends StatelessWidget {
await twonlyDB.groupsDao.updateGroup(group.groupId, update); await twonlyDB.groupsDao.updateGroup(group.groupId, update);
} }
}, },
icon: FontAwesomeIcons.boxOpen, icon: Icons.unarchive_outlined,
), ),
ContextMenuItem( ContextMenuItem(
title: context.lang.contextMenuOpenChat, title: context.lang.contextMenuOpenChat,
@ -71,6 +71,19 @@ class GroupContextMenu extends StatelessWidget {
? FontAwesomeIcons.thumbtackSlash ? FontAwesomeIcons.thumbtackSlash
: FontAwesomeIcons.thumbtack, : FontAwesomeIcons.thumbtack,
), ),
ContextMenuItem(
title: context.lang.delete,
icon: FontAwesomeIcons.trashCan,
onTap: () async {
await twonlyDB.messagesDao.deleteMessagesByGroupId(group.groupId);
await twonlyDB.groupsDao.updateGroup(
group.groupId,
const GroupsCompanion(
deletedContent: Value(true),
),
);
},
),
], ],
child: child, child: child,
); );

View file

@ -201,7 +201,7 @@ class _BackupViewState extends State<BackupView> {
label: 'twonly Backup', label: 'twonly Backup',
), ),
BottomNavigationBarItem( BottomNavigationBarItem(
icon: const FaIcon(FontAwesomeIcons.boxArchive, size: 17), icon: const FaIcon(Icons.archive_outlined, size: 17),
label: context.lang.backupData, label: context.lang.backupData,
), ),
], ],