Improve: Show message "Flames restored"

This commit is contained in:
otsmr 2026-03-13 23:09:42 +01:00
parent c85914672e
commit c1065772f8
19 changed files with 192 additions and 15 deletions

View file

@ -4,6 +4,7 @@
- Fix: Issue with contact requests
- Improve: Video compression with progress updates
- Improve: Show message "Flames restored"
## 0.0.96

File diff suppressed because one or more lines are too long

View file

@ -141,20 +141,20 @@ struct PushNotification: Sendable {
var kind: PushKind = .reaction
var messageID: String {
get {return _messageID ?? String()}
get {_messageID ?? String()}
set {_messageID = newValue}
}
/// Returns true if `messageID` has been explicitly set.
var hasMessageID: Bool {return self._messageID != nil}
var hasMessageID: Bool {self._messageID != nil}
/// Clears the value of `messageID`. Subsequent reads from it will return its default value.
mutating func clearMessageID() {self._messageID = nil}
var additionalContent: String {
get {return _additionalContent ?? String()}
get {_additionalContent ?? String()}
set {_additionalContent = newValue}
}
/// Returns true if `additionalContent` has been explicitly set.
var hasAdditionalContent: Bool {return self._additionalContent != nil}
var hasAdditionalContent: Bool {self._additionalContent != nil}
/// Clears the value of `additionalContent`. Subsequent reads from it will return its default value.
mutating func clearAdditionalContent() {self._additionalContent = nil}
@ -190,11 +190,11 @@ struct PushUser: Sendable {
var blocked: Bool = false
var lastMessageID: String {
get {return _lastMessageID ?? String()}
get {_lastMessageID ?? String()}
set {_lastMessageID = newValue}
}
/// Returns true if `lastMessageID` has been explicitly set.
var hasLastMessageID: Bool {return self._lastMessageID != nil}
var hasLastMessageID: Bool {self._lastMessageID != nil}
/// Clears the value of `lastMessageID`. Subsequent reads from it will return its default value.
mutating func clearLastMessageID() {self._lastMessageID = nil}

View file

@ -3,7 +3,7 @@ import 'package:twonly/src/database/tables/contacts.table.dart';
import 'package:twonly/src/database/tables/groups.table.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart';
enum MessageType { media, text, contacts }
enum MessageType { media, text, contacts, restoreFlameCounter }
@DataClassName('Message')
class Messages extends Table {

View file

@ -3033,6 +3033,12 @@ abstract class AppLocalizations {
/// In en, this message translates to:
/// **'Unknown contact whose identity has not yet been verified.'**
String get verificationBadgeRedDesc;
/// No description provided for @chatEntryFlameRestored.
///
/// In en, this message translates to:
/// **'{count} flames restored'**
String chatEntryFlameRestored(Object count);
}
class _AppLocalizationsDelegate

View file

@ -1695,4 +1695,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get verificationBadgeRedDesc =>
'Unbekannter Kontakt, dessen Identität bisher nicht verifiziert wurde.';
@override
String chatEntryFlameRestored(Object count) {
return '$count Flammen wiederhergestellt';
}
}

View file

@ -1683,4 +1683,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get verificationBadgeRedDesc =>
'Unknown contact whose identity has not yet been verified.';
@override
String chatEntryFlameRestored(Object count) {
return '$count flames restored';
}
}

View file

@ -1683,4 +1683,9 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get verificationBadgeRedDesc =>
'Unknown contact whose identity has not yet been verified.';
@override
String chatEntryFlameRestored(Object count) {
return '$count flames restored';
}
}

View file

@ -10,9 +10,11 @@ message AdditionalMessageData {
enum Type {
LINK = 0;
CONTACTS = 1;
RESTORED_FLAME_COUNTER = 2;
}
Type type = 1;
optional string link = 2;
repeated SharedContact contacts = 3;
optional int64 restored_flame_counter = 4;
}

View file

@ -106,11 +106,14 @@ class AdditionalMessageData extends $pb.GeneratedMessage {
AdditionalMessageData_Type? type,
$core.String? link,
$core.Iterable<SharedContact>? contacts,
$fixnum.Int64? restoredFlameCounter,
}) {
final result = create();
if (type != null) result.type = type;
if (link != null) result.link = link;
if (contacts != null) result.contacts.addAll(contacts);
if (restoredFlameCounter != null)
result.restoredFlameCounter = restoredFlameCounter;
return result;
}
@ -135,6 +138,7 @@ class AdditionalMessageData extends $pb.GeneratedMessage {
..pc<SharedContact>(
3, _omitFieldNames ? '' : 'contacts', $pb.PbFieldType.PM,
subBuilder: SharedContact.create)
..aInt64(4, _omitFieldNames ? '' : 'restoredFlameCounter')
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
@ -180,6 +184,15 @@ class AdditionalMessageData extends $pb.GeneratedMessage {
@$pb.TagNumber(3)
$pb.PbList<SharedContact> get contacts => $_getList(2);
@$pb.TagNumber(4)
$fixnum.Int64 get restoredFlameCounter => $_getI64(3);
@$pb.TagNumber(4)
set restoredFlameCounter($fixnum.Int64 value) => $_setInt64(3, value);
@$pb.TagNumber(4)
$core.bool hasRestoredFlameCounter() => $_has(3);
@$pb.TagNumber(4)
void clearRestoredFlameCounter() => $_clearField(4);
}
const $core.bool _omitFieldNames =

View file

@ -19,15 +19,19 @@ class AdditionalMessageData_Type extends $pb.ProtobufEnum {
AdditionalMessageData_Type._(0, _omitEnumNames ? '' : 'LINK');
static const AdditionalMessageData_Type CONTACTS =
AdditionalMessageData_Type._(1, _omitEnumNames ? '' : 'CONTACTS');
static const AdditionalMessageData_Type RESTORED_FLAME_COUNTER =
AdditionalMessageData_Type._(
2, _omitEnumNames ? '' : 'RESTORED_FLAME_COUNTER');
static const $core.List<AdditionalMessageData_Type> values =
<AdditionalMessageData_Type>[
LINK,
CONTACTS,
RESTORED_FLAME_COUNTER,
];
static final $core.List<AdditionalMessageData_Type?> _byValue =
$pb.ProtobufEnum.$_initByValueList(values, 1);
$pb.ProtobufEnum.$_initByValueList(values, 2);
static AdditionalMessageData_Type? valueOf($core.int value) =>
value < 0 || value >= _byValue.length ? null : _byValue[value];

View file

@ -57,10 +57,20 @@ const AdditionalMessageData$json = {
'6': '.SharedContact',
'10': 'contacts'
},
{
'1': 'restored_flame_counter',
'3': 4,
'4': 1,
'5': 3,
'9': 1,
'10': 'restoredFlameCounter',
'17': true
},
],
'4': [AdditionalMessageData_Type$json],
'8': [
{'1': '_link'},
{'1': '_restored_flame_counter'},
],
};
@ -70,6 +80,7 @@ const AdditionalMessageData_Type$json = {
'2': [
{'1': 'LINK', '2': 0},
{'1': 'CONTACTS', '2': 1},
{'1': 'RESTORED_FLAME_COUNTER', '2': 2},
],
};
@ -77,5 +88,7 @@ const AdditionalMessageData_Type$json = {
final $typed_data.Uint8List additionalMessageDataDescriptor = $convert.base64Decode(
'ChVBZGRpdGlvbmFsTWVzc2FnZURhdGESLwoEdHlwZRgBIAEoDjIbLkFkZGl0aW9uYWxNZXNzYW'
'dlRGF0YS5UeXBlUgR0eXBlEhcKBGxpbmsYAiABKAlIAFIEbGlua4gBARIqCghjb250YWN0cxgD'
'IAMoCzIOLlNoYXJlZENvbnRhY3RSCGNvbnRhY3RzIh4KBFR5cGUSCAoETElOSxAAEgwKCENPTl'
'RBQ1RTEAFCBwoFX2xpbms=');
'IAMoCzIOLlNoYXJlZENvbnRhY3RSCGNvbnRhY3RzEjkKFnJlc3RvcmVkX2ZsYW1lX2NvdW50ZX'
'IYBCABKANIAVIUcmVzdG9yZWRGbGFtZUNvdW50ZXKIAQEiOgoEVHlwZRIICgRMSU5LEAASDAoI'
'Q09OVEFDVFMQARIaChZSRVNUT1JFRF9GTEFNRV9DT1VOVEVSEAJCBwoFX2xpbmtCGQoXX3Jlc3'
'RvcmVkX2ZsYW1lX2NvdW50ZXI=');

View file

@ -303,6 +303,8 @@ Color getMessageColorFromType(
Color color;
if (message.type == MessageType.text.name) {
color = Colors.orange;
} else if (message.type == MessageType.text.name) {
color = Colors.blueAccent;
} else if (mediaFile != null) {
if (mediaFile.requiresAuthentication) {

View file

@ -13,6 +13,7 @@ import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
import 'package:twonly/src/views/chats/chat_messages_components/chat_reaction_row.dart';
import 'package:twonly/src/views/chats/chat_messages_components/entries/chat_audio_entry.dart';
import 'package:twonly/src/views/chats/chat_messages_components/entries/chat_contacts.entry.dart';
import 'package:twonly/src/views/chats/chat_messages_components/entries/chat_flame_restored.entry.dart';
import 'package:twonly/src/views/chats/chat_messages_components/entries/chat_media_entry.dart';
import 'package:twonly/src/views/chats/chat_messages_components/entries/chat_text_entry.dart';
import 'package:twonly/src/views/chats/chat_messages_components/entries/chat_unkown.entry.dart';
@ -132,6 +133,12 @@ class _ChatListEntryState extends State<ChatListEntry> {
);
}
if (widget.message.type == MessageType.restoreFlameCounter.name) {
return ChatFlameRestoredEntry(
message: widget.message,
);
}
return const ChatUnknownEntry();
}

View file

@ -0,0 +1,49 @@
import 'package:flutter/material.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/client/generated/data.pb.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/components/better_text.dart';
class ChatFlameRestoredEntry extends StatelessWidget {
const ChatFlameRestoredEntry({
required this.message,
super.key,
});
final Message message;
@override
Widget build(BuildContext context) {
AdditionalMessageData? data;
if (message.additionalMessageData != null) {
try {
data = AdditionalMessageData.fromBuffer(
message.additionalMessageData!,
);
} catch (e) {
data = null;
}
}
if (data == null || !data.hasRestoredFlameCounter()) {
return const SizedBox.shrink();
}
return Container(
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.8,
),
padding: const EdgeInsets.only(left: 10, top: 6, bottom: 6, right: 10),
decoration: BoxDecoration(
color: Colors.orange,
borderRadius: BorderRadius.circular(12),
),
child: BetterText(
text: context.lang
.chatEntryFlameRestored(data.restoredFlameCounter.toInt()),
textColor: isDarkMode(context) ? Colors.black : Colors.black,
),
);
}
}

View file

@ -113,6 +113,7 @@ class EmojiAnimation extends StatelessWidget {
'💯': '100.lottie',
'🎉': 'party-popper.lottie',
'🎊': 'confetti-ball.lottie',
'🎂': 'birthday-cake.json',
'🧡': 'orange-heart.lottie',
'💛': 'yellow-heart.lottie',
'💚': 'green-heart.lottie',

View file

@ -82,10 +82,11 @@ class _FlameCounterWidgetState extends State<FlameCounterWidget> {
if (widget.prefix) const SizedBox(width: 5),
if (widget.prefix) const Text(''),
if (widget.prefix) const SizedBox(width: 5),
Text(
flameCounter.toString(),
style: const TextStyle(fontSize: 13),
),
if (flameCounter != 100)
Text(
flameCounter.toString(),
style: const TextStyle(fontSize: 13),
),
SizedBox(
height: 15,
child: EmojiAnimation(

View file

@ -1,11 +1,18 @@
import 'dart:async';
import 'package:clock/clock.dart';
import 'package:drift/drift.dart' show Value;
import 'package:fixnum/fixnum.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/constants/routes.keys.dart';
import 'package:twonly/src/database/tables/messages.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/client/generated/data.pb.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'
as pb;
import 'package:twonly/src/services/api/messages.dart';
import 'package:twonly/src/services/flame.service.dart';
import 'package:twonly/src/services/subscription.service.dart';
import 'package:twonly/src/utils/log.dart';
@ -46,7 +53,8 @@ class _MaxFlameListTitleState extends State<MaxFlameListTitle> {
}
Future<void> _restoreFlames() async {
if (!isUserAllowed(getCurrentPlan(), PremiumFeatures.RestoreFlames)) {
if (!isUserAllowed(getCurrentPlan(), PremiumFeatures.RestoreFlames) &&
kReleaseMode) {
await context.push(Routes.settingsSubscription);
return;
}
@ -60,7 +68,40 @@ class _MaxFlameListTitleState extends State<MaxFlameListTitle> {
lastFlameCounterChange: Value(clock.now()),
),
);
final addData = AdditionalMessageData(
type: AdditionalMessageData_Type.RESTORED_FLAME_COUNTER,
restoredFlameCounter: Int64(_group!.maxFlameCounter),
);
final message = await twonlyDB.messagesDao.insertMessage(
MessagesCompanion(
groupId: Value(_groupId),
type: Value(MessageType.restoreFlameCounter.name),
additionalMessageData: Value(addData.writeToBuffer()),
),
);
if (message == null) {
Log.error('Could not insert message into database');
return;
}
final encryptedContent = pb.EncryptedContent(
additionalDataMessage: pb.EncryptedContent_AdditionalDataMessage(
senderMessageId: message.messageId,
additionalMessageData: addData.writeToBuffer(),
timestamp: Int64(message.createdAt.millisecondsSinceEpoch),
type: MessageType.restoreFlameCounter.name,
),
);
await syncFlameCounters(forceForGroup: _groupId);
await sendCipherTextToGroup(
_groupId,
encryptedContent,
messageId: message.messageId,
);
}
@override

View file

@ -1,10 +1,13 @@
import 'dart:async';
import 'package:clock/clock.dart';
import 'package:drift/drift.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:restart_app/restart_app.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/constants/routes.keys.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/utils/storage.dart';
import 'package:twonly/src/views/components/alert_dialog.dart';
@ -67,6 +70,24 @@ class _DeveloperSettingsViewState extends State<DeveloperSettingsView> {
}
},
),
if (!kReleaseMode)
ListTile(
title: const Text('Make it possible to reset flames'),
onTap: () async {
final chats = await twonlyDB.groupsDao.getAllDirectChats();
for (final chat in chats) {
await twonlyDB.groupsDao.updateGroup(
chat.groupId,
GroupsCompanion(
flameCounter: const Value(0),
maxFlameCounter: const Value(365),
lastFlameCounterChange: Value(clock.now()),
),
);
}
},
),
if (!kReleaseMode)
ListTile(
title: const Text('Automated Testing'),