This commit is contained in:
otsmr 2025-11-05 23:12:10 +01:00
parent 07d36c133c
commit 8d45c8e9ce
24 changed files with 588 additions and 181 deletions

View file

@ -229,7 +229,7 @@ func getPushNotificationText(pushNotification: PushNotification) -> (String, Str
.reactionToText: "hat mit {{content}} auf deinen Text reagiert.",
.reactionToImage: "hat mit {{content}} auf dein Bild reagiert.",
.response: "hat dir{inGroup} geantwortet.",
.addedToGroup: "hat dich zu \"{{content}}\" hinzugefügt."
.addedToGroup: "hat dich zu \"{{content}}\" hinzugefügt.",
]
} else { // Default to English
pushNotificationText = [
@ -247,7 +247,7 @@ func getPushNotificationText(pushNotification: PushNotification) -> (String, Str
.reactionToText: "has reacted with {{content}} to your text.",
.reactionToImage: "has reacted with {{content}} to your image.",
.response: "has responded{inGroup}.",
.addedToGroup: "has added you to \"{{content}}\""
.addedToGroup: "has added you to \"{{content}}\"",
]
}
@ -257,9 +257,10 @@ func getPushNotificationText(pushNotification: PushNotification) -> (String, Str
content.replace("{{content}}", with: pushNotification.additionalContent)
content.replace("{inGroup}", with: " in {inGroup}")
content.replace("{inGroup}", with: pushNotification.additionalContent)
} else {
content.replace("{inGroup}", with: "")
}
// Return the corresponding message or an empty string if not found
return (content, title)
}

View file

@ -29,6 +29,10 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
return entry != null;
}
Future<void> deleteGroup(String groupId) async {
await (delete(groups)..where((t) => t.groupId.equals(groupId))).go();
}
Future<void> updateGroup(
String groupId,
GroupsCompanion updates,
@ -42,7 +46,8 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
..where(
(t) =>
t.groupId.equals(groupId) &
t.memberState.equals(MemberState.leftGroup.name).not(),
(t.memberState.equals(MemberState.leftGroup.name).not() |
t.memberState.isNull()),
))
.get();
}
@ -131,8 +136,9 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
Future<Group?> _insertGroup(GroupsCompanion group) async {
try {
final rowId = await into(groups).insert(group);
return await (select(groups)..where((t) => t.rowId.equals(rowId)))
await into(groups).insert(group);
return await (select(groups)
..where((t) => t.groupId.equals(group.groupId.value)))
.getSingle();
} catch (e) {
Log.error('Could not insert group: $e');

View file

@ -77,6 +77,7 @@ enum GroupActionType {
promoteToAdmin,
demoteToMember,
updatedGroupName,
changeDisplayMaxTime,
}
@DataClassName('GroupHistory')
@ -94,6 +95,8 @@ class GroupHistories extends Table {
TextColumn get oldGroupName => text().nullable()();
TextColumn get newGroupName => text().nullable()();
IntColumn get newDeleteMessagesAfterMilliseconds => integer().nullable()();
TextColumn get type => textEnum<GroupActionType>()();
DateTimeColumn get actionAt => dateTime().withDefault(currentDateAndTime)();

View file

@ -7038,6 +7038,13 @@ class $GroupHistoriesTable extends GroupHistories
late final GeneratedColumn<String> newGroupName = GeneratedColumn<String>(
'new_group_name', aliasedName, true,
type: DriftSqlType.string, requiredDuringInsert: false);
static const VerificationMeta _newDeleteMessagesAfterMillisecondsMeta =
const VerificationMeta('newDeleteMessagesAfterMilliseconds');
@override
late final GeneratedColumn<int> newDeleteMessagesAfterMilliseconds =
GeneratedColumn<int>(
'new_delete_messages_after_milliseconds', aliasedName, true,
type: DriftSqlType.int, requiredDuringInsert: false);
@override
late final GeneratedColumnWithTypeConverter<GroupActionType, String> type =
GeneratedColumn<String>('type', aliasedName, false,
@ -7059,6 +7066,7 @@ class $GroupHistoriesTable extends GroupHistories
affectedContactId,
oldGroupName,
newGroupName,
newDeleteMessagesAfterMilliseconds,
type,
actionAt
];
@ -7108,6 +7116,13 @@ class $GroupHistoriesTable extends GroupHistories
newGroupName.isAcceptableOrUnknown(
data['new_group_name']!, _newGroupNameMeta));
}
if (data.containsKey('new_delete_messages_after_milliseconds')) {
context.handle(
_newDeleteMessagesAfterMillisecondsMeta,
newDeleteMessagesAfterMilliseconds.isAcceptableOrUnknown(
data['new_delete_messages_after_milliseconds']!,
_newDeleteMessagesAfterMillisecondsMeta));
}
if (data.containsKey('action_at')) {
context.handle(_actionAtMeta,
actionAt.isAcceptableOrUnknown(data['action_at']!, _actionAtMeta));
@ -7133,6 +7148,9 @@ class $GroupHistoriesTable extends GroupHistories
.read(DriftSqlType.string, data['${effectivePrefix}old_group_name']),
newGroupName: attachedDatabase.typeMapping
.read(DriftSqlType.string, data['${effectivePrefix}new_group_name']),
newDeleteMessagesAfterMilliseconds: attachedDatabase.typeMapping.read(
DriftSqlType.int,
data['${effectivePrefix}new_delete_messages_after_milliseconds']),
type: $GroupHistoriesTable.$convertertype.fromSql(attachedDatabase
.typeMapping
.read(DriftSqlType.string, data['${effectivePrefix}type'])!),
@ -7157,6 +7175,7 @@ class GroupHistory extends DataClass implements Insertable<GroupHistory> {
final int? affectedContactId;
final String? oldGroupName;
final String? newGroupName;
final int? newDeleteMessagesAfterMilliseconds;
final GroupActionType type;
final DateTime actionAt;
const GroupHistory(
@ -7166,6 +7185,7 @@ class GroupHistory extends DataClass implements Insertable<GroupHistory> {
this.affectedContactId,
this.oldGroupName,
this.newGroupName,
this.newDeleteMessagesAfterMilliseconds,
required this.type,
required this.actionAt});
@override
@ -7185,6 +7205,10 @@ class GroupHistory extends DataClass implements Insertable<GroupHistory> {
if (!nullToAbsent || newGroupName != null) {
map['new_group_name'] = Variable<String>(newGroupName);
}
if (!nullToAbsent || newDeleteMessagesAfterMilliseconds != null) {
map['new_delete_messages_after_milliseconds'] =
Variable<int>(newDeleteMessagesAfterMilliseconds);
}
{
map['type'] =
Variable<String>($GroupHistoriesTable.$convertertype.toSql(type));
@ -7209,6 +7233,10 @@ class GroupHistory extends DataClass implements Insertable<GroupHistory> {
newGroupName: newGroupName == null && nullToAbsent
? const Value.absent()
: Value(newGroupName),
newDeleteMessagesAfterMilliseconds:
newDeleteMessagesAfterMilliseconds == null && nullToAbsent
? const Value.absent()
: Value(newDeleteMessagesAfterMilliseconds),
type: Value(type),
actionAt: Value(actionAt),
);
@ -7224,6 +7252,8 @@ class GroupHistory extends DataClass implements Insertable<GroupHistory> {
affectedContactId: serializer.fromJson<int?>(json['affectedContactId']),
oldGroupName: serializer.fromJson<String?>(json['oldGroupName']),
newGroupName: serializer.fromJson<String?>(json['newGroupName']),
newDeleteMessagesAfterMilliseconds:
serializer.fromJson<int?>(json['newDeleteMessagesAfterMilliseconds']),
type: $GroupHistoriesTable.$convertertype
.fromJson(serializer.fromJson<String>(json['type'])),
actionAt: serializer.fromJson<DateTime>(json['actionAt']),
@ -7239,6 +7269,8 @@ class GroupHistory extends DataClass implements Insertable<GroupHistory> {
'affectedContactId': serializer.toJson<int?>(affectedContactId),
'oldGroupName': serializer.toJson<String?>(oldGroupName),
'newGroupName': serializer.toJson<String?>(newGroupName),
'newDeleteMessagesAfterMilliseconds':
serializer.toJson<int?>(newDeleteMessagesAfterMilliseconds),
'type': serializer
.toJson<String>($GroupHistoriesTable.$convertertype.toJson(type)),
'actionAt': serializer.toJson<DateTime>(actionAt),
@ -7252,6 +7284,7 @@ class GroupHistory extends DataClass implements Insertable<GroupHistory> {
Value<int?> affectedContactId = const Value.absent(),
Value<String?> oldGroupName = const Value.absent(),
Value<String?> newGroupName = const Value.absent(),
Value<int?> newDeleteMessagesAfterMilliseconds = const Value.absent(),
GroupActionType? type,
DateTime? actionAt}) =>
GroupHistory(
@ -7265,6 +7298,10 @@ class GroupHistory extends DataClass implements Insertable<GroupHistory> {
oldGroupName.present ? oldGroupName.value : this.oldGroupName,
newGroupName:
newGroupName.present ? newGroupName.value : this.newGroupName,
newDeleteMessagesAfterMilliseconds:
newDeleteMessagesAfterMilliseconds.present
? newDeleteMessagesAfterMilliseconds.value
: this.newDeleteMessagesAfterMilliseconds,
type: type ?? this.type,
actionAt: actionAt ?? this.actionAt,
);
@ -7284,6 +7321,10 @@ class GroupHistory extends DataClass implements Insertable<GroupHistory> {
newGroupName: data.newGroupName.present
? data.newGroupName.value
: this.newGroupName,
newDeleteMessagesAfterMilliseconds:
data.newDeleteMessagesAfterMilliseconds.present
? data.newDeleteMessagesAfterMilliseconds.value
: this.newDeleteMessagesAfterMilliseconds,
type: data.type.present ? data.type.value : this.type,
actionAt: data.actionAt.present ? data.actionAt.value : this.actionAt,
);
@ -7298,6 +7339,8 @@ class GroupHistory extends DataClass implements Insertable<GroupHistory> {
..write('affectedContactId: $affectedContactId, ')
..write('oldGroupName: $oldGroupName, ')
..write('newGroupName: $newGroupName, ')
..write(
'newDeleteMessagesAfterMilliseconds: $newDeleteMessagesAfterMilliseconds, ')
..write('type: $type, ')
..write('actionAt: $actionAt')
..write(')'))
@ -7305,8 +7348,16 @@ class GroupHistory extends DataClass implements Insertable<GroupHistory> {
}
@override
int get hashCode => Object.hash(groupHistoryId, groupId, contactId,
affectedContactId, oldGroupName, newGroupName, type, actionAt);
int get hashCode => Object.hash(
groupHistoryId,
groupId,
contactId,
affectedContactId,
oldGroupName,
newGroupName,
newDeleteMessagesAfterMilliseconds,
type,
actionAt);
@override
bool operator ==(Object other) =>
identical(this, other) ||
@ -7317,6 +7368,8 @@ class GroupHistory extends DataClass implements Insertable<GroupHistory> {
other.affectedContactId == this.affectedContactId &&
other.oldGroupName == this.oldGroupName &&
other.newGroupName == this.newGroupName &&
other.newDeleteMessagesAfterMilliseconds ==
this.newDeleteMessagesAfterMilliseconds &&
other.type == this.type &&
other.actionAt == this.actionAt);
}
@ -7328,6 +7381,7 @@ class GroupHistoriesCompanion extends UpdateCompanion<GroupHistory> {
final Value<int?> affectedContactId;
final Value<String?> oldGroupName;
final Value<String?> newGroupName;
final Value<int?> newDeleteMessagesAfterMilliseconds;
final Value<GroupActionType> type;
final Value<DateTime> actionAt;
final Value<int> rowid;
@ -7338,6 +7392,7 @@ class GroupHistoriesCompanion extends UpdateCompanion<GroupHistory> {
this.affectedContactId = const Value.absent(),
this.oldGroupName = const Value.absent(),
this.newGroupName = const Value.absent(),
this.newDeleteMessagesAfterMilliseconds = const Value.absent(),
this.type = const Value.absent(),
this.actionAt = const Value.absent(),
this.rowid = const Value.absent(),
@ -7349,6 +7404,7 @@ class GroupHistoriesCompanion extends UpdateCompanion<GroupHistory> {
this.affectedContactId = const Value.absent(),
this.oldGroupName = const Value.absent(),
this.newGroupName = const Value.absent(),
this.newDeleteMessagesAfterMilliseconds = const Value.absent(),
required GroupActionType type,
this.actionAt = const Value.absent(),
this.rowid = const Value.absent(),
@ -7362,6 +7418,7 @@ class GroupHistoriesCompanion extends UpdateCompanion<GroupHistory> {
Expression<int>? affectedContactId,
Expression<String>? oldGroupName,
Expression<String>? newGroupName,
Expression<int>? newDeleteMessagesAfterMilliseconds,
Expression<String>? type,
Expression<DateTime>? actionAt,
Expression<int>? rowid,
@ -7373,6 +7430,9 @@ class GroupHistoriesCompanion extends UpdateCompanion<GroupHistory> {
if (affectedContactId != null) 'affected_contact_id': affectedContactId,
if (oldGroupName != null) 'old_group_name': oldGroupName,
if (newGroupName != null) 'new_group_name': newGroupName,
if (newDeleteMessagesAfterMilliseconds != null)
'new_delete_messages_after_milliseconds':
newDeleteMessagesAfterMilliseconds,
if (type != null) 'type': type,
if (actionAt != null) 'action_at': actionAt,
if (rowid != null) 'rowid': rowid,
@ -7386,6 +7446,7 @@ class GroupHistoriesCompanion extends UpdateCompanion<GroupHistory> {
Value<int?>? affectedContactId,
Value<String?>? oldGroupName,
Value<String?>? newGroupName,
Value<int?>? newDeleteMessagesAfterMilliseconds,
Value<GroupActionType>? type,
Value<DateTime>? actionAt,
Value<int>? rowid}) {
@ -7396,6 +7457,8 @@ class GroupHistoriesCompanion extends UpdateCompanion<GroupHistory> {
affectedContactId: affectedContactId ?? this.affectedContactId,
oldGroupName: oldGroupName ?? this.oldGroupName,
newGroupName: newGroupName ?? this.newGroupName,
newDeleteMessagesAfterMilliseconds: newDeleteMessagesAfterMilliseconds ??
this.newDeleteMessagesAfterMilliseconds,
type: type ?? this.type,
actionAt: actionAt ?? this.actionAt,
rowid: rowid ?? this.rowid,
@ -7423,6 +7486,10 @@ class GroupHistoriesCompanion extends UpdateCompanion<GroupHistory> {
if (newGroupName.present) {
map['new_group_name'] = Variable<String>(newGroupName.value);
}
if (newDeleteMessagesAfterMilliseconds.present) {
map['new_delete_messages_after_milliseconds'] =
Variable<int>(newDeleteMessagesAfterMilliseconds.value);
}
if (type.present) {
map['type'] = Variable<String>(
$GroupHistoriesTable.$convertertype.toSql(type.value));
@ -7445,6 +7512,8 @@ class GroupHistoriesCompanion extends UpdateCompanion<GroupHistory> {
..write('affectedContactId: $affectedContactId, ')
..write('oldGroupName: $oldGroupName, ')
..write('newGroupName: $newGroupName, ')
..write(
'newDeleteMessagesAfterMilliseconds: $newDeleteMessagesAfterMilliseconds, ')
..write('type: $type, ')
..write('actionAt: $actionAt, ')
..write('rowid: $rowid')
@ -13359,6 +13428,7 @@ typedef $$GroupHistoriesTableCreateCompanionBuilder = GroupHistoriesCompanion
Value<int?> affectedContactId,
Value<String?> oldGroupName,
Value<String?> newGroupName,
Value<int?> newDeleteMessagesAfterMilliseconds,
required GroupActionType type,
Value<DateTime> actionAt,
Value<int> rowid,
@ -13371,6 +13441,7 @@ typedef $$GroupHistoriesTableUpdateCompanionBuilder = GroupHistoriesCompanion
Value<int?> affectedContactId,
Value<String?> oldGroupName,
Value<String?> newGroupName,
Value<int?> newDeleteMessagesAfterMilliseconds,
Value<GroupActionType> type,
Value<DateTime> actionAt,
Value<int> rowid,
@ -13445,6 +13516,11 @@ class $$GroupHistoriesTableFilterComposer
ColumnFilters<String> get newGroupName => $composableBuilder(
column: $table.newGroupName, builder: (column) => ColumnFilters(column));
ColumnFilters<int> get newDeleteMessagesAfterMilliseconds =>
$composableBuilder(
column: $table.newDeleteMessagesAfterMilliseconds,
builder: (column) => ColumnFilters(column));
ColumnWithTypeConverterFilters<GroupActionType, GroupActionType, String>
get type => $composableBuilder(
column: $table.type,
@ -13535,6 +13611,11 @@ class $$GroupHistoriesTableOrderingComposer
column: $table.newGroupName,
builder: (column) => ColumnOrderings(column));
ColumnOrderings<int> get newDeleteMessagesAfterMilliseconds =>
$composableBuilder(
column: $table.newDeleteMessagesAfterMilliseconds,
builder: (column) => ColumnOrderings(column));
ColumnOrderings<String> get type => $composableBuilder(
column: $table.type, builder: (column) => ColumnOrderings(column));
@ -13620,6 +13701,11 @@ class $$GroupHistoriesTableAnnotationComposer
GeneratedColumn<String> get newGroupName => $composableBuilder(
column: $table.newGroupName, builder: (column) => column);
GeneratedColumn<int> get newDeleteMessagesAfterMilliseconds =>
$composableBuilder(
column: $table.newDeleteMessagesAfterMilliseconds,
builder: (column) => column);
GeneratedColumnWithTypeConverter<GroupActionType, String> get type =>
$composableBuilder(column: $table.type, builder: (column) => column);
@ -13717,6 +13803,8 @@ class $$GroupHistoriesTableTableManager extends RootTableManager<
Value<int?> affectedContactId = const Value.absent(),
Value<String?> oldGroupName = const Value.absent(),
Value<String?> newGroupName = const Value.absent(),
Value<int?> newDeleteMessagesAfterMilliseconds =
const Value.absent(),
Value<GroupActionType> type = const Value.absent(),
Value<DateTime> actionAt = const Value.absent(),
Value<int> rowid = const Value.absent(),
@ -13728,6 +13816,8 @@ class $$GroupHistoriesTableTableManager extends RootTableManager<
affectedContactId: affectedContactId,
oldGroupName: oldGroupName,
newGroupName: newGroupName,
newDeleteMessagesAfterMilliseconds:
newDeleteMessagesAfterMilliseconds,
type: type,
actionAt: actionAt,
rowid: rowid,
@ -13739,6 +13829,8 @@ class $$GroupHistoriesTableTableManager extends RootTableManager<
Value<int?> affectedContactId = const Value.absent(),
Value<String?> oldGroupName = const Value.absent(),
Value<String?> newGroupName = const Value.absent(),
Value<int?> newDeleteMessagesAfterMilliseconds =
const Value.absent(),
required GroupActionType type,
Value<DateTime> actionAt = const Value.absent(),
Value<int> rowid = const Value.absent(),
@ -13750,6 +13842,8 @@ class $$GroupHistoriesTableTableManager extends RootTableManager<
affectedContactId: affectedContactId,
oldGroupName: oldGroupName,
newGroupName: newGroupName,
newDeleteMessagesAfterMilliseconds:
newDeleteMessagesAfterMilliseconds,
type: type,
actionAt: actionAt,
rowid: rowid,

View file

@ -804,5 +804,7 @@
"leaveGroupSelectOtherAdminBody": "Um die Gruppe zu verlassen, musst du zuerst einen neuen Administrator auswählen.",
"leaveGroupSureTitle": "Gruppe verlassen",
"leaveGroupSureBody": "Willst du die Gruppe wirklich verlassen?",
"leaveGroupSureOkBtn": "Gruppe verlassen"
"leaveGroupSureOkBtn": "Gruppe verlassen",
"changeDisplayMaxTime": "{username} hat das Zeitlimit für verschwindende Nachrichten auf {time}.",
"youChangedDisplayMaxTime": "Du hat das Zeitlimit für verschwindende Nachrichten auf {time}."
}

View file

@ -582,5 +582,7 @@
"leaveGroupSelectOtherAdminBody": "To leave the group, you must first select a new administrator.",
"leaveGroupSureTitle": "Leave group",
"leaveGroupSureBody": "Do you really want to leave the group?",
"leaveGroupSureOkBtn": "Leave group"
"leaveGroupSureOkBtn": "Leave group",
"changeDisplayMaxTime": "{username} has set the time limit for disappearing messages to {time}.",
"youChangedDisplayMaxTime": "You have set the time limit for disappearing messages to {time}."
}

View file

@ -2599,6 +2599,18 @@ abstract class AppLocalizations {
/// In en, this message translates to:
/// **'Leave group'**
String get leaveGroupSureOkBtn;
/// No description provided for @changeDisplayMaxTime.
///
/// In en, this message translates to:
/// **'{username} has set the time limit for disappearing messages to {time}.'**
String changeDisplayMaxTime(Object time, Object username);
/// No description provided for @youChangedDisplayMaxTime.
///
/// In en, this message translates to:
/// **'You have set the time limit for disappearing messages to {time}.'**
String youChangedDisplayMaxTime(Object time);
}
class _AppLocalizationsDelegate

View file

@ -1421,4 +1421,14 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get leaveGroupSureOkBtn => 'Gruppe verlassen';
@override
String changeDisplayMaxTime(Object time, Object username) {
return '$username hat das Zeitlimit für verschwindende Nachrichten auf $time.';
}
@override
String youChangedDisplayMaxTime(Object time) {
return 'Du hat das Zeitlimit für verschwindende Nachrichten auf $time.';
}
}

View file

@ -1411,4 +1411,14 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get leaveGroupSureOkBtn => 'Leave group';
@override
String changeDisplayMaxTime(Object time, Object username) {
return '$username has set the time limit for disappearing messages to $time.';
}
@override
String youChangedDisplayMaxTime(Object time) {
return 'You have set the time limit for disappearing messages to $time.';
}
}

View file

@ -415,6 +415,7 @@ class EncryptedContent_GroupUpdate extends $pb.GeneratedMessage {
$core.String? groupActionType,
$fixnum.Int64? affectedContactId,
$core.String? newGroupName,
$fixnum.Int64? newDeleteMessagesAfterMilliseconds,
}) {
final $result = create();
if (groupActionType != null) {
@ -426,6 +427,9 @@ class EncryptedContent_GroupUpdate extends $pb.GeneratedMessage {
if (newGroupName != null) {
$result.newGroupName = newGroupName;
}
if (newDeleteMessagesAfterMilliseconds != null) {
$result.newDeleteMessagesAfterMilliseconds = newDeleteMessagesAfterMilliseconds;
}
return $result;
}
EncryptedContent_GroupUpdate._() : super();
@ -436,6 +440,7 @@ class EncryptedContent_GroupUpdate extends $pb.GeneratedMessage {
..aOS(1, _omitFieldNames ? '' : 'groupActionType', protoName: 'groupActionType')
..aInt64(2, _omitFieldNames ? '' : 'affectedContactId', protoName: 'affectedContactId')
..aOS(3, _omitFieldNames ? '' : 'newGroupName', protoName: 'newGroupName')
..aInt64(4, _omitFieldNames ? '' : 'newDeleteMessagesAfterMilliseconds', protoName: 'newDeleteMessagesAfterMilliseconds')
..hasRequiredFields = false
;
@ -486,6 +491,15 @@ class EncryptedContent_GroupUpdate extends $pb.GeneratedMessage {
$core.bool hasNewGroupName() => $_has(2);
@$pb.TagNumber(3)
void clearNewGroupName() => clearField(3);
@$pb.TagNumber(4)
$fixnum.Int64 get newDeleteMessagesAfterMilliseconds => $_getI64(3);
@$pb.TagNumber(4)
set newDeleteMessagesAfterMilliseconds($fixnum.Int64 v) { $_setInt64(3, v); }
@$pb.TagNumber(4)
$core.bool hasNewDeleteMessagesAfterMilliseconds() => $_has(3);
@$pb.TagNumber(4)
void clearNewDeleteMessagesAfterMilliseconds() => clearField(4);
}
class EncryptedContent_TextMessage extends $pb.GeneratedMessage {

View file

@ -170,10 +170,12 @@ const EncryptedContent_GroupUpdate$json = {
{'1': 'groupActionType', '3': 1, '4': 1, '5': 9, '10': 'groupActionType'},
{'1': 'affectedContactId', '3': 2, '4': 1, '5': 3, '9': 0, '10': 'affectedContactId', '17': true},
{'1': 'newGroupName', '3': 3, '4': 1, '5': 9, '9': 1, '10': 'newGroupName', '17': true},
{'1': 'newDeleteMessagesAfterMilliseconds', '3': 4, '4': 1, '5': 3, '9': 2, '10': 'newDeleteMessagesAfterMilliseconds', '17': true},
],
'8': [
{'1': '_affectedContactId'},
{'1': '_newGroupName'},
{'1': '_newDeleteMessagesAfterMilliseconds'},
],
};
@ -389,53 +391,55 @@ final $typed_data.Uint8List encryptedContentDescriptor = $convert.base64Decode(
'dGVkQ29udGVudC5SZXNlbmRHcm91cFB1YmxpY0tleUgPUhRyZXNlbmRHcm91cFB1YmxpY0tleY'
'gBARpRCgtHcm91cENyZWF0ZRIaCghzdGF0ZUtleRgDIAEoDFIIc3RhdGVLZXkSJgoOZ3JvdXBQ'
'dWJsaWNLZXkYBCABKAxSDmdyb3VwUHVibGljS2V5GjMKCUdyb3VwSm9pbhImCg5ncm91cFB1Ym'
'xpY0tleRgBIAEoDFIOZ3JvdXBQdWJsaWNLZXkaFgoUUmVzZW5kR3JvdXBQdWJsaWNLZXkaugEK'
'xpY0tleRgBIAEoDFIOZ3JvdXBQdWJsaWNLZXkaFgoUUmVzZW5kR3JvdXBQdWJsaWNLZXkatgIK'
'C0dyb3VwVXBkYXRlEigKD2dyb3VwQWN0aW9uVHlwZRgBIAEoCVIPZ3JvdXBBY3Rpb25UeXBlEj'
'EKEWFmZmVjdGVkQ29udGFjdElkGAIgASgDSABSEWFmZmVjdGVkQ29udGFjdElkiAEBEicKDG5l'
'd0dyb3VwTmFtZRgDIAEoCUgBUgxuZXdHcm91cE5hbWWIAQFCFAoSX2FmZmVjdGVkQ29udGFjdE'
'lkQg8KDV9uZXdHcm91cE5hbWUaqQEKC1RleHRNZXNzYWdlEigKD3NlbmRlck1lc3NhZ2VJZBgB'
'IAEoCVIPc2VuZGVyTWVzc2FnZUlkEhIKBHRleHQYAiABKAlSBHRleHQSHAoJdGltZXN0YW1wGA'
'MgASgDUgl0aW1lc3RhbXASKwoOcXVvdGVNZXNzYWdlSWQYBCABKAlIAFIOcXVvdGVNZXNzYWdl'
'SWSIAQFCEQoPX3F1b3RlTWVzc2FnZUlkGmIKCFJlYWN0aW9uEigKD3RhcmdldE1lc3NhZ2VJZB'
'gBIAEoCVIPdGFyZ2V0TWVzc2FnZUlkEhQKBWVtb2ppGAIgASgJUgVlbW9qaRIWCgZyZW1vdmUY'
'AyABKAhSBnJlbW92ZRq3AgoNTWVzc2FnZVVwZGF0ZRI4CgR0eXBlGAEgASgOMiQuRW5jcnlwdG'
'VkQ29udGVudC5NZXNzYWdlVXBkYXRlLlR5cGVSBHR5cGUSLQoPc2VuZGVyTWVzc2FnZUlkGAIg'
'ASgJSABSD3NlbmRlck1lc3NhZ2VJZIgBARI6ChhtdWx0aXBsZVRhcmdldE1lc3NhZ2VJZHMYAy'
'ADKAlSGG11bHRpcGxlVGFyZ2V0TWVzc2FnZUlkcxIXCgR0ZXh0GAQgASgJSAFSBHRleHSIAQES'
'HAoJdGltZXN0YW1wGAUgASgDUgl0aW1lc3RhbXAiLQoEVHlwZRIKCgZERUxFVEUQABINCglFRE'
'lUX1RFWFQQARIKCgZPUEVORUQQAkISChBfc2VuZGVyTWVzc2FnZUlkQgcKBV90ZXh0GowFCgVN'
'ZWRpYRIoCg9zZW5kZXJNZXNzYWdlSWQYASABKAlSD3NlbmRlck1lc3NhZ2VJZBIwCgR0eXBlGA'
'IgASgOMhwuRW5jcnlwdGVkQ29udGVudC5NZWRpYS5UeXBlUgR0eXBlEkMKGmRpc3BsYXlMaW1p'
'dEluTWlsbGlzZWNvbmRzGAMgASgDSABSGmRpc3BsYXlMaW1pdEluTWlsbGlzZWNvbmRziAEBEj'
'YKFnJlcXVpcmVzQXV0aGVudGljYXRpb24YBCABKAhSFnJlcXVpcmVzQXV0aGVudGljYXRpb24S'
'HAoJdGltZXN0YW1wGAUgASgDUgl0aW1lc3RhbXASKwoOcXVvdGVNZXNzYWdlSWQYBiABKAlIAV'
'IOcXVvdGVNZXNzYWdlSWSIAQESKQoNZG93bmxvYWRUb2tlbhgHIAEoDEgCUg1kb3dubG9hZFRv'
'a2VuiAEBEikKDWVuY3J5cHRpb25LZXkYCCABKAxIA1INZW5jcnlwdGlvbktleYgBARIpCg1lbm'
'NyeXB0aW9uTWFjGAkgASgMSARSDWVuY3J5cHRpb25NYWOIAQESLQoPZW5jcnlwdGlvbk5vbmNl'
'GAogASgMSAVSD2VuY3J5cHRpb25Ob25jZYgBASIzCgRUeXBlEgwKCFJFVVBMT0FEEAASCQoFSU'
'1BR0UQARIJCgVWSURFTxACEgcKA0dJRhADQh0KG19kaXNwbGF5TGltaXRJbk1pbGxpc2Vjb25k'
'c0IRCg9fcXVvdGVNZXNzYWdlSWRCEAoOX2Rvd25sb2FkVG9rZW5CEAoOX2VuY3J5cHRpb25LZX'
'lCEAoOX2VuY3J5cHRpb25NYWNCEgoQX2VuY3J5cHRpb25Ob25jZRqnAQoLTWVkaWFVcGRhdGUS'
'NgoEdHlwZRgBIAEoDjIiLkVuY3J5cHRlZENvbnRlbnQuTWVkaWFVcGRhdGUuVHlwZVIEdHlwZR'
'IoCg90YXJnZXRNZXNzYWdlSWQYAiABKAlSD3RhcmdldE1lc3NhZ2VJZCI2CgRUeXBlEgwKCFJF'
'T1BFTkVEEAASCgoGU1RPUkVEEAESFAoQREVDUllQVElPTl9FUlJPUhACGngKDkNvbnRhY3RSZX'
'F1ZXN0EjkKBHR5cGUYASABKA4yJS5FbmNyeXB0ZWRDb250ZW50LkNvbnRhY3RSZXF1ZXN0LlR5'
'cGVSBHR5cGUiKwoEVHlwZRILCgdSRVFVRVNUEAASCgoGUkVKRUNUEAESCgoGQUNDRVBUEAIang'
'IKDUNvbnRhY3RVcGRhdGUSOAoEdHlwZRgBIAEoDjIkLkVuY3J5cHRlZENvbnRlbnQuQ29udGFj'
'dFVwZGF0ZS5UeXBlUgR0eXBlEjUKE2F2YXRhclN2Z0NvbXByZXNzZWQYAiABKAxIAFITYXZhdG'
'FyU3ZnQ29tcHJlc3NlZIgBARIfCgh1c2VybmFtZRgDIAEoCUgBUgh1c2VybmFtZYgBARIlCgtk'
'aXNwbGF5TmFtZRgEIAEoCUgCUgtkaXNwbGF5TmFtZYgBASIfCgRUeXBlEgsKB1JFUVVFU1QQAB'
'IKCgZVUERBVEUQAUIWChRfYXZhdGFyU3ZnQ29tcHJlc3NlZEILCglfdXNlcm5hbWVCDgoMX2Rp'
'c3BsYXlOYW1lGtUBCghQdXNoS2V5cxIzCgR0eXBlGAEgASgOMh8uRW5jcnlwdGVkQ29udGVudC'
'5QdXNoS2V5cy5UeXBlUgR0eXBlEhkKBWtleUlkGAIgASgDSABSBWtleUlkiAEBEhUKA2tleRgD'
'IAEoDEgBUgNrZXmIAQESIQoJY3JlYXRlZEF0GAQgASgDSAJSCWNyZWF0ZWRBdIgBASIfCgRUeX'
'BlEgsKB1JFUVVFU1QQABIKCgZVUERBVEUQAUIICgZfa2V5SWRCBgoEX2tleUIMCgpfY3JlYXRl'
'ZEF0GocBCglGbGFtZVN5bmMSIgoMZmxhbWVDb3VudGVyGAEgASgDUgxmbGFtZUNvdW50ZXISNg'
'oWbGFzdEZsYW1lQ291bnRlckNoYW5nZRgCIAEoA1IWbGFzdEZsYW1lQ291bnRlckNoYW5nZRIe'
'CgpiZXN0RnJpZW5kGAMgASgIUgpiZXN0RnJpZW5kQgoKCF9ncm91cElkQg8KDV9pc0RpcmVjdE'
'NoYXRCFwoVX3NlbmRlclByb2ZpbGVDb3VudGVyQhAKDl9tZXNzYWdlVXBkYXRlQggKBl9tZWRp'
'YUIOCgxfbWVkaWFVcGRhdGVCEAoOX2NvbnRhY3RVcGRhdGVCEQoPX2NvbnRhY3RSZXF1ZXN0Qg'
'wKCl9mbGFtZVN5bmNCCwoJX3B1c2hLZXlzQgsKCV9yZWFjdGlvbkIOCgxfdGV4dE1lc3NhZ2VC'
'DgoMX2dyb3VwQ3JlYXRlQgwKCl9ncm91cEpvaW5CDgoMX2dyb3VwVXBkYXRlQhcKFV9yZXNlbm'
'RHcm91cFB1YmxpY0tleQ==');
'd0dyb3VwTmFtZRgDIAEoCUgBUgxuZXdHcm91cE5hbWWIAQESUwoibmV3RGVsZXRlTWVzc2FnZX'
'NBZnRlck1pbGxpc2Vjb25kcxgEIAEoA0gCUiJuZXdEZWxldGVNZXNzYWdlc0FmdGVyTWlsbGlz'
'ZWNvbmRziAEBQhQKEl9hZmZlY3RlZENvbnRhY3RJZEIPCg1fbmV3R3JvdXBOYW1lQiUKI19uZX'
'dEZWxldGVNZXNzYWdlc0FmdGVyTWlsbGlzZWNvbmRzGqkBCgtUZXh0TWVzc2FnZRIoCg9zZW5k'
'ZXJNZXNzYWdlSWQYASABKAlSD3NlbmRlck1lc3NhZ2VJZBISCgR0ZXh0GAIgASgJUgR0ZXh0Eh'
'wKCXRpbWVzdGFtcBgDIAEoA1IJdGltZXN0YW1wEisKDnF1b3RlTWVzc2FnZUlkGAQgASgJSABS'
'DnF1b3RlTWVzc2FnZUlkiAEBQhEKD19xdW90ZU1lc3NhZ2VJZBpiCghSZWFjdGlvbhIoCg90YX'
'JnZXRNZXNzYWdlSWQYASABKAlSD3RhcmdldE1lc3NhZ2VJZBIUCgVlbW9qaRgCIAEoCVIFZW1v'
'amkSFgoGcmVtb3ZlGAMgASgIUgZyZW1vdmUatwIKDU1lc3NhZ2VVcGRhdGUSOAoEdHlwZRgBIA'
'EoDjIkLkVuY3J5cHRlZENvbnRlbnQuTWVzc2FnZVVwZGF0ZS5UeXBlUgR0eXBlEi0KD3NlbmRl'
'ck1lc3NhZ2VJZBgCIAEoCUgAUg9zZW5kZXJNZXNzYWdlSWSIAQESOgoYbXVsdGlwbGVUYXJnZX'
'RNZXNzYWdlSWRzGAMgAygJUhhtdWx0aXBsZVRhcmdldE1lc3NhZ2VJZHMSFwoEdGV4dBgEIAEo'
'CUgBUgR0ZXh0iAEBEhwKCXRpbWVzdGFtcBgFIAEoA1IJdGltZXN0YW1wIi0KBFR5cGUSCgoGRE'
'VMRVRFEAASDQoJRURJVF9URVhUEAESCgoGT1BFTkVEEAJCEgoQX3NlbmRlck1lc3NhZ2VJZEIH'
'CgVfdGV4dBqMBQoFTWVkaWESKAoPc2VuZGVyTWVzc2FnZUlkGAEgASgJUg9zZW5kZXJNZXNzYW'
'dlSWQSMAoEdHlwZRgCIAEoDjIcLkVuY3J5cHRlZENvbnRlbnQuTWVkaWEuVHlwZVIEdHlwZRJD'
'ChpkaXNwbGF5TGltaXRJbk1pbGxpc2Vjb25kcxgDIAEoA0gAUhpkaXNwbGF5TGltaXRJbk1pbG'
'xpc2Vjb25kc4gBARI2ChZyZXF1aXJlc0F1dGhlbnRpY2F0aW9uGAQgASgIUhZyZXF1aXJlc0F1'
'dGhlbnRpY2F0aW9uEhwKCXRpbWVzdGFtcBgFIAEoA1IJdGltZXN0YW1wEisKDnF1b3RlTWVzc2'
'FnZUlkGAYgASgJSAFSDnF1b3RlTWVzc2FnZUlkiAEBEikKDWRvd25sb2FkVG9rZW4YByABKAxI'
'AlINZG93bmxvYWRUb2tlbogBARIpCg1lbmNyeXB0aW9uS2V5GAggASgMSANSDWVuY3J5cHRpb2'
'5LZXmIAQESKQoNZW5jcnlwdGlvbk1hYxgJIAEoDEgEUg1lbmNyeXB0aW9uTWFjiAEBEi0KD2Vu'
'Y3J5cHRpb25Ob25jZRgKIAEoDEgFUg9lbmNyeXB0aW9uTm9uY2WIAQEiMwoEVHlwZRIMCghSRV'
'VQTE9BRBAAEgkKBUlNQUdFEAESCQoFVklERU8QAhIHCgNHSUYQA0IdChtfZGlzcGxheUxpbWl0'
'SW5NaWxsaXNlY29uZHNCEQoPX3F1b3RlTWVzc2FnZUlkQhAKDl9kb3dubG9hZFRva2VuQhAKDl'
'9lbmNyeXB0aW9uS2V5QhAKDl9lbmNyeXB0aW9uTWFjQhIKEF9lbmNyeXB0aW9uTm9uY2UapwEK'
'C01lZGlhVXBkYXRlEjYKBHR5cGUYASABKA4yIi5FbmNyeXB0ZWRDb250ZW50Lk1lZGlhVXBkYX'
'RlLlR5cGVSBHR5cGUSKAoPdGFyZ2V0TWVzc2FnZUlkGAIgASgJUg90YXJnZXRNZXNzYWdlSWQi'
'NgoEVHlwZRIMCghSRU9QRU5FRBAAEgoKBlNUT1JFRBABEhQKEERFQ1JZUFRJT05fRVJST1IQAh'
'p4Cg5Db250YWN0UmVxdWVzdBI5CgR0eXBlGAEgASgOMiUuRW5jcnlwdGVkQ29udGVudC5Db250'
'YWN0UmVxdWVzdC5UeXBlUgR0eXBlIisKBFR5cGUSCwoHUkVRVUVTVBAAEgoKBlJFSkVDVBABEg'
'oKBkFDQ0VQVBACGp4CCg1Db250YWN0VXBkYXRlEjgKBHR5cGUYASABKA4yJC5FbmNyeXB0ZWRD'
'b250ZW50LkNvbnRhY3RVcGRhdGUuVHlwZVIEdHlwZRI1ChNhdmF0YXJTdmdDb21wcmVzc2VkGA'
'IgASgMSABSE2F2YXRhclN2Z0NvbXByZXNzZWSIAQESHwoIdXNlcm5hbWUYAyABKAlIAVIIdXNl'
'cm5hbWWIAQESJQoLZGlzcGxheU5hbWUYBCABKAlIAlILZGlzcGxheU5hbWWIAQEiHwoEVHlwZR'
'ILCgdSRVFVRVNUEAASCgoGVVBEQVRFEAFCFgoUX2F2YXRhclN2Z0NvbXByZXNzZWRCCwoJX3Vz'
'ZXJuYW1lQg4KDF9kaXNwbGF5TmFtZRrVAQoIUHVzaEtleXMSMwoEdHlwZRgBIAEoDjIfLkVuY3'
'J5cHRlZENvbnRlbnQuUHVzaEtleXMuVHlwZVIEdHlwZRIZCgVrZXlJZBgCIAEoA0gAUgVrZXlJ'
'ZIgBARIVCgNrZXkYAyABKAxIAVIDa2V5iAEBEiEKCWNyZWF0ZWRBdBgEIAEoA0gCUgljcmVhdG'
'VkQXSIAQEiHwoEVHlwZRILCgdSRVFVRVNUEAASCgoGVVBEQVRFEAFCCAoGX2tleUlkQgYKBF9r'
'ZXlCDAoKX2NyZWF0ZWRBdBqHAQoJRmxhbWVTeW5jEiIKDGZsYW1lQ291bnRlchgBIAEoA1IMZm'
'xhbWVDb3VudGVyEjYKFmxhc3RGbGFtZUNvdW50ZXJDaGFuZ2UYAiABKANSFmxhc3RGbGFtZUNv'
'dW50ZXJDaGFuZ2USHgoKYmVzdEZyaWVuZBgDIAEoCFIKYmVzdEZyaWVuZEIKCghfZ3JvdXBJZE'
'IPCg1faXNEaXJlY3RDaGF0QhcKFV9zZW5kZXJQcm9maWxlQ291bnRlckIQCg5fbWVzc2FnZVVw'
'ZGF0ZUIICgZfbWVkaWFCDgoMX21lZGlhVXBkYXRlQhAKDl9jb250YWN0VXBkYXRlQhEKD19jb2'
'50YWN0UmVxdWVzdEIMCgpfZmxhbWVTeW5jQgsKCV9wdXNoS2V5c0ILCglfcmVhY3Rpb25CDgoM'
'X3RleHRNZXNzYWdlQg4KDF9ncm91cENyZWF0ZUIMCgpfZ3JvdXBKb2luQg4KDF9ncm91cFVwZG'
'F0ZUIXChVfcmVzZW5kR3JvdXBQdWJsaWNLZXk=');

View file

@ -72,6 +72,7 @@ message EncryptedContent {
string groupActionType = 1; // GroupActionType.name
optional int64 affectedContactId = 2;
optional string newGroupName = 3;
optional int64 newDeleteMessagesAfterMilliseconds = 4;
}
message TextMessage {

View file

@ -128,6 +128,16 @@ Future<void> handleGroupUpdate(
contactId: Value(fromUserId),
),
);
case GroupActionType.changeDisplayMaxTime:
await twonlyDB.groupsDao.insertGroupAction(
GroupHistoriesCompanion(
groupId: Value(groupId),
type: Value(actionType),
newDeleteMessagesAfterMilliseconds:
Value(update.newDeleteMessagesAfterMilliseconds.toInt()),
contactId: Value(fromUserId),
),
);
case GroupActionType.removedMember:
case GroupActionType.addMember:
case GroupActionType.leftGroup:

View file

@ -24,7 +24,7 @@ Future<void> enableTwonlySafe(String password) async {
unawaited(performTwonlySafeBackup(force: true));
}
Future<void> disableTwonlySafe() async {
Future<void> removeTwonlySafeFromServer() async {
final serverUrl = await getTwonlySafeBackupUrl();
if (serverUrl != null) {
try {
@ -40,10 +40,6 @@ Future<void> disableTwonlySafe() async {
Log.error('Could not connect to the server.');
}
}
await updateUserdata((user) {
user.twonlySafeBackup = null;
return user;
});
}
Future<(Uint8List, Uint8List)> getMasterKey(

View file

@ -26,61 +26,63 @@ class EmojiPickerBottom extends StatelessWidget {
),
],
),
child: Column(
children: [
Container(
margin: const EdgeInsets.all(30),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(32),
color: Colors.grey,
child: SafeArea(
child: Column(
children: [
Container(
margin: const EdgeInsets.all(30),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(32),
color: Colors.grey,
),
height: 3,
width: 60,
),
height: 3,
width: 60,
),
Expanded(
child: EmojiPicker(
onEmojiSelected: (category, emoji) {
Navigator.pop(
context,
EmojiLayerData(
text: emoji.emoji,
Expanded(
child: EmojiPicker(
onEmojiSelected: (category, emoji) {
Navigator.pop(
context,
EmojiLayerData(
text: emoji.emoji,
),
);
},
// textEditingController: _textFieldController,
config: Config(
height: 400,
locale: Localizations.localeOf(context),
viewOrderConfig: const ViewOrderConfig(
top: EmojiPickerItem.searchBar,
// middle: EmojiPickerItem.emojiView,
bottom: EmojiPickerItem.categoryBar,
),
emojiTextStyle:
TextStyle(fontSize: 24 * (Platform.isIOS ? 1.2 : 1)),
emojiViewConfig: EmojiViewConfig(
backgroundColor: context.color.surfaceContainer,
),
searchViewConfig: SearchViewConfig(
backgroundColor: context.color.surfaceContainer,
buttonIconColor: Colors.white,
),
categoryViewConfig: CategoryViewConfig(
backgroundColor: context.color.surfaceContainer,
dividerColor: Colors.white,
indicatorColor: context.color.primary,
iconColorSelected: context.color.primary,
iconColor: context.color.secondary,
),
bottomActionBarConfig: BottomActionBarConfig(
backgroundColor: context.color.surfaceContainer,
buttonColor: context.color.surfaceContainer,
buttonIconColor: context.color.secondary,
),
);
},
// textEditingController: _textFieldController,
config: Config(
height: 400,
locale: Localizations.localeOf(context),
viewOrderConfig: const ViewOrderConfig(
top: EmojiPickerItem.searchBar,
// middle: EmojiPickerItem.emojiView,
bottom: EmojiPickerItem.categoryBar,
),
emojiTextStyle:
TextStyle(fontSize: 24 * (Platform.isIOS ? 1.2 : 1)),
emojiViewConfig: EmojiViewConfig(
backgroundColor: context.color.surfaceContainer,
),
searchViewConfig: SearchViewConfig(
backgroundColor: context.color.surfaceContainer,
buttonIconColor: Colors.white,
),
categoryViewConfig: CategoryViewConfig(
backgroundColor: context.color.surfaceContainer,
dividerColor: Colors.white,
indicatorColor: context.color.primary,
iconColorSelected: context.color.primary,
iconColor: context.color.secondary,
),
bottomActionBarConfig: BottomActionBarConfig(
backgroundColor: context.color.surfaceContainer,
buttonColor: context.color.surfaceContainer,
buttonIconColor: context.color.secondary,
),
),
),
),
],
],
),
),
),
);

View file

@ -114,7 +114,6 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
groupActionsSub?.cancel();
lastOpenedMessageByContactSub?.cancel();
tutorial?.cancel();
textFieldFocus.dispose();
super.dispose();
}

View file

@ -64,7 +64,7 @@ class _AllReactionsViewState extends State<AllReactionsView> {
),
),
);
if (mounted) Navigator.pop(context);
// if (mounted) Navigator.pop(context);
}
@override

View file

@ -55,6 +55,15 @@ class _ChatGroupActionState extends State<ChatGroupAction> {
final maker = (contact == null) ? '' : getContactDisplayName(contact!);
switch (widget.action.type) {
case GroupActionType.changeDisplayMaxTime:
final time = formatDuration(
context,
(widget.action.newDeleteMessagesAfterMilliseconds ?? 0 / 1000) as int,
);
text = (contact == null)
? context.lang.youChangedDisplayMaxTime(time)
: context.lang.changeDisplayMaxTime(maker, time);
icon = FontAwesomeIcons.pencil;
case GroupActionType.updatedGroupName:
text = (contact == null)
? context.lang.youChangedGroupName(widget.action.newGroupName!)

View file

@ -55,6 +55,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
bool imageSaved = false;
bool imageSaving = false;
bool displayTwonlyPresent = true;
final emojiKey = GlobalKey<EmojiFloatWidgetState>();
StreamSubscription<MediaFile?>? downloadStateListener;
@ -634,6 +635,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
mediaViewerDistanceFromBottom: mediaViewerDistanceFromBottom,
groupId: widget.group.groupId,
messageId: currentMessage!.messageId,
emojiKey: emojiKey,
hide: () {
setState(() {
showShortReactions = false;
@ -641,6 +643,9 @@ class _MediaViewerViewState extends State<MediaViewerView> {
});
},
),
Positioned.fill(
child: EmojiFloatWidget(key: emojiKey),
),
],
),
),

View file

@ -1,11 +1,46 @@
// ignore_for_file: avoid_dynamic_calls
import 'package:flutter/material.dart';
import 'package:twonly/globals.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/chats/media_viewer_components/reaction_buttons.component.dart';
import 'package:twonly/src/views/components/animate_icon.dart';
Offset getGlobalOffset(GlobalKey targetKey) {
final ctx = targetKey.currentContext;
if (ctx == null) {
return Offset.zero;
}
final renderObject = ctx.findRenderObject();
if (renderObject is RenderBox) {
return renderObject.localToGlobal(
Offset(renderObject.size.width / 2, renderObject.size.height / 2),
);
}
return Offset.zero;
}
Future<void> sendReaction(
String groupId,
String messageId,
String emoji,
) async {
await twonlyDB.reactionsDao.updateMyReaction(
messageId,
emoji,
false,
);
await sendCipherTextToGroup(
groupId,
EncryptedContent(
reaction: EncryptedContent_Reaction(
targetMessageId: messageId,
emoji: emoji,
remove: false,
),
),
);
}
class EmojiReactionWidget extends StatefulWidget {
const EmojiReactionWidget({
required this.messageId,
@ -13,74 +48,46 @@ class EmojiReactionWidget extends StatefulWidget {
required this.hide,
required this.show,
required this.emoji,
required this.emojiKey,
super.key,
});
final String messageId;
final String groupId;
final Function hide;
final void Function() hide;
final bool show;
final String emoji;
final GlobalKey<EmojiFloatWidgetState> emojiKey;
@override
State<EmojiReactionWidget> createState() => _EmojiReactionWidgetState();
}
class _EmojiReactionWidgetState extends State<EmojiReactionWidget> {
int selectedShortReaction = -1;
final GlobalKey _targetKey = GlobalKey();
@override
Widget build(BuildContext context) {
return AnimatedSize(
key: _targetKey,
duration: const Duration(milliseconds: 200),
curve: Curves.linearToEaseOut,
child: GestureDetector(
onTap: () async {
await twonlyDB.reactionsDao
.updateMyReaction(widget.messageId, widget.emoji, false);
await sendCipherTextToGroup(
widget.groupId,
EncryptedContent(
reaction: EncryptedContent_Reaction(
targetMessageId: widget.messageId,
emoji: widget.emoji,
remove: false,
),
),
await sendReaction(widget.groupId, widget.messageId, widget.emoji);
widget.emojiKey.currentState?.spawn(
getGlobalOffset(_targetKey),
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;
});
}
});
widget.hide();
},
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,
),
),
),
),
child: SizedBox(
width: widget.show ? 40 : 10,
child: Center(
child: EmojiAnimation(
emoji: widget.emoji,
),
),
),
),
);
}

View file

@ -1,6 +1,12 @@
import 'dart:async';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/camera/image_editor/data/layer.dart';
import 'package:twonly/src/views/camera/image_editor/modules/all_emojis.dart';
import 'package:twonly/src/views/chats/media_viewer_components/emoji_reactions_row.component.dart';
import 'package:twonly/src/views/components/animate_icon.dart';
@ -11,6 +17,7 @@ class ReactionButtons extends StatefulWidget {
required this.mediaViewerDistanceFromBottom,
required this.messageId,
required this.groupId,
required this.emojiKey,
required this.hide,
super.key,
});
@ -18,6 +25,7 @@ class ReactionButtons extends StatefulWidget {
final double mediaViewerDistanceFromBottom;
final bool show;
final bool textInputFocused;
final GlobalKey<EmojiFloatWidgetState> emojiKey;
final String messageId;
final String groupId;
final void Function() hide;
@ -28,6 +36,7 @@ class ReactionButtons extends StatefulWidget {
class _ReactionButtonsState extends State<ReactionButtons> {
int selectedShortReaction = -1;
final GlobalKey _keyEmojiPicker = GlobalKey();
List<String> selectedEmojis =
EmojiAnimation.animatedIcons.keys.toList().sublist(0, 6);
@ -82,6 +91,7 @@ class _ReactionButtonsState extends State<ReactionButtons> {
hide: widget.hide,
show: widget.show,
emoji: emoji as String,
emojiKey: widget.emojiKey,
),
)
.toList(),
@ -90,17 +100,53 @@ class _ReactionButtonsState extends State<ReactionButtons> {
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,
children: [
...firstRowEmojis.map(
(emoji) => EmojiReactionWidget(
messageId: widget.messageId,
groupId: widget.groupId,
hide: widget.hide,
show: widget.show,
emoji: emoji,
emojiKey: widget.emojiKey,
),
),
GestureDetector(
key: _keyEmojiPicker,
onTap: () async {
// ignore: inference_failure_on_function_invocation
final layer = await showModalBottomSheet(
context: context,
backgroundColor: context.color.surface,
builder: (BuildContext context) {
return const EmojiPickerBottom();
},
) as EmojiLayerData?;
if (layer == null) return;
await sendReaction(
widget.groupId,
widget.messageId,
layer.text,
);
widget.emojiKey.currentState?.spawn(
getGlobalOffset(_keyEmojiPicker),
layer.text,
);
widget.hide();
},
child: Container(
decoration: BoxDecoration(
color: context.color.surfaceContainer.withAlpha(100),
borderRadius: BorderRadius.circular(12),
),
)
.toList(),
padding: const EdgeInsets.all(8),
child: const FaIcon(
FontAwesomeIcons.ellipsisVertical,
size: 24,
),
),
),
],
),
],
),
@ -109,3 +155,164 @@ class _ReactionButtonsState extends State<ReactionButtons> {
);
}
}
class EmojiFloatWidget extends StatefulWidget {
const EmojiFloatWidget({
super.key,
});
@override
EmojiFloatWidgetState createState() => EmojiFloatWidgetState();
}
class EmojiFloatWidgetState extends State<EmojiFloatWidget>
with SingleTickerProviderStateMixin {
final List<_Particle> _particles = [];
late final Ticker _ticker;
final Random _rnd = Random();
Duration _lastTick = Duration.zero;
@override
void initState() {
super.initState();
_ticker = createTicker(_tick)..start();
}
void _tick(Duration elapsed) {
final dt = (_lastTick == Duration.zero)
? 0.016
: (elapsed - _lastTick).inMicroseconds / 1e6;
_lastTick = elapsed;
for (final p in List<_Particle>.from(_particles)) {
p.update(dt);
if (p.isDead) _particles.remove(p);
}
if (mounted) setState(() {});
}
@override
void dispose() {
_ticker.dispose();
super.dispose();
}
/// Call this to spawn the emoji animation from a global screen position.
void spawn(Offset globalPosition, String emoji) {
final box = context.findRenderObject() as RenderBox?;
if (box == null) return;
final local = box.globalToLocal(globalPosition);
const spawnCount = 10;
final life = const Duration(milliseconds: 2000).inMilliseconds / 1000.0;
for (var i = 0; i < spawnCount; i++) {
final dx = (_rnd.nextDouble() - 0.5) * 220;
final vx = dx;
final vy = -(100 + _rnd.nextDouble() * 80);
final rot = (_rnd.nextDouble() - 0.5) * 2;
final scale = 0.9 + _rnd.nextDouble() * 0.6;
_particles.add(
_Particle(
emoji: emoji,
x: local.dx,
y: local.dy,
vx: vx,
vy: vy,
rotation: rot,
lifetime: life,
scale: scale,
),
);
}
setState(() {});
}
@override
Widget build(BuildContext context) {
return IgnorePointer(
child: CustomPaint(
painter: _ParticlePainter(List<_Particle>.from(_particles)),
size: Size.infinite,
),
);
}
}
class _Particle {
_Particle({
required this.emoji,
required this.x,
required this.y,
required this.vx,
required this.vy,
required this.rotation,
required this.lifetime,
required this.scale,
});
final String emoji;
double x;
double y;
double vx;
double vy;
double rotation;
double age = 0;
final double lifetime;
final double scale;
bool get isDead => age >= lifetime;
void update(double dt) {
age += dt;
// vertical-only motion emphasis: mild gravity slows ascent then gently pulls down
vy += 100 * dt; // gravity (positive = down)
// slight horizontal drag to reduce sideways drift
vx *= 1 - 3.0 * dt;
// integrate position
x += vx * dt;
y += vy * dt;
// slow rotation decay
rotation *= 1 - 1.5 * dt;
}
double get progress => (age / lifetime).clamp(0.0, 1.0);
// opacity falls from 1 -> 0 as particle ages
double get opacity => (1.0 - progress).clamp(0.0, 1.0);
// scale can gently grow then shrink; here we slightly increase early
double get currentScale {
final p = progress;
if (p < 0.5) return scale * (1.0 + 0.3 * (p / 0.5));
return scale * (1.3 - 0.3 * ((p - 0.5) / 0.5));
}
}
class _ParticlePainter extends CustomPainter {
_ParticlePainter(this.particles);
final List<_Particle> particles;
@override
void paint(Canvas canvas, Size size) {
final textPainter = TextPainter(textDirection: TextDirection.ltr);
for (final p in particles) {
final tp = TextSpan(
text: p.emoji,
style: TextStyle(
fontSize: 24 * p.currentScale,
color: Colors.black.withValues(alpha: p.opacity),
),
);
textPainter
..text = tp
..layout();
canvas
..save()
..translate(p.x - textPainter.width / 2, p.y - textPainter.height / 2)
..rotate(p.rotation);
textPainter.paint(canvas, Offset.zero);
canvas.restore();
}
}
@override
bool shouldRepaint(covariant _ParticlePainter old) => true;
}

View file

@ -72,10 +72,16 @@ class _AvatarIconState extends State<AvatarIcon> {
_globalUserDataCallBackId = 'avatar_${getRandomString(10)}';
globalUserDataChangedCallBack[_globalUserDataCallBackId!] = () {
setState(() {
_avatarSVGs = [gUser.avatarSvg!];
if (gUser.avatarSvg != null) {
_avatarSVGs = [gUser.avatarSvg!];
} else {
_avatarSVGs = [];
}
});
};
_avatarSVGs.add(gUser.avatarSvg!);
if (gUser.avatarSvg != null) {
_avatarSVGs = [gUser.avatarSvg!];
}
} else if (widget.contactId != null) {
contactStream = twonlyDB.contactsDao
.watchContact(widget.contactId!)

View file

@ -82,13 +82,14 @@ class GroupContextMenu extends StatelessWidget {
context.lang.groupContextMenuDeleteGroup,
);
if (ok) {
await twonlyDB.messagesDao.deleteMessagesByGroupId(group.groupId);
await twonlyDB.groupsDao.updateGroup(
group.groupId,
const GroupsCompanion(
deletedContent: Value(true),
),
);
// await twonlyDB.messagesDao.deleteMessagesByGroupId(group.groupId);
await twonlyDB.groupsDao.deleteGroup(group.groupId);
// await twonlyDB.groupsDao.updateGroup(
// group.groupId,
// const GroupsCompanion(
// deletedContent: Value(true),
// ),
// );
}
},
),

View file

@ -6,6 +6,8 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
import 'package:twonly/src/services/api/messages.dart';
import 'package:twonly/src/services/twonly_safe/common.twonly_safe.dart';
import 'package:twonly/src/services/twonly_safe/create_backup.twonly_safe.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart';
import 'package:twonly/src/views/components/better_list_title.dart';
@ -59,6 +61,10 @@ class _ProfileViewState extends State<ProfileView> {
return;
}
// as the username has changes, remove the old from the server and then upload it again.
await removeTwonlySafeFromServer();
unawaited(performTwonlySafeBackup(force: true));
await updateUserdata((user) {
user
..username = username