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 - Fix: Issue with contact requests
- Improve: Video compression with progress updates - Improve: Video compression with progress updates
- Improve: Show message "Flames restored"
## 0.0.96 ## 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 kind: PushKind = .reaction
var messageID: String { var messageID: String {
get {return _messageID ?? String()} get {_messageID ?? String()}
set {_messageID = newValue} set {_messageID = newValue}
} }
/// Returns true if `messageID` has been explicitly set. /// 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. /// Clears the value of `messageID`. Subsequent reads from it will return its default value.
mutating func clearMessageID() {self._messageID = nil} mutating func clearMessageID() {self._messageID = nil}
var additionalContent: String { var additionalContent: String {
get {return _additionalContent ?? String()} get {_additionalContent ?? String()}
set {_additionalContent = newValue} set {_additionalContent = newValue}
} }
/// Returns true if `additionalContent` has been explicitly set. /// 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. /// Clears the value of `additionalContent`. Subsequent reads from it will return its default value.
mutating func clearAdditionalContent() {self._additionalContent = nil} mutating func clearAdditionalContent() {self._additionalContent = nil}
@ -190,11 +190,11 @@ struct PushUser: Sendable {
var blocked: Bool = false var blocked: Bool = false
var lastMessageID: String { var lastMessageID: String {
get {return _lastMessageID ?? String()} get {_lastMessageID ?? String()}
set {_lastMessageID = newValue} set {_lastMessageID = newValue}
} }
/// Returns true if `lastMessageID` has been explicitly set. /// 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. /// Clears the value of `lastMessageID`. Subsequent reads from it will return its default value.
mutating func clearLastMessageID() {self._lastMessageID = nil} 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/groups.table.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart';
enum MessageType { media, text, contacts } enum MessageType { media, text, contacts, restoreFlameCounter }
@DataClassName('Message') @DataClassName('Message')
class Messages extends Table { class Messages extends Table {

View file

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

View file

@ -1695,4 +1695,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get verificationBadgeRedDesc => String get verificationBadgeRedDesc =>
'Unbekannter Kontakt, dessen Identität bisher nicht verifiziert wurde.'; '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 @override
String get verificationBadgeRedDesc => String get verificationBadgeRedDesc =>
'Unknown contact whose identity has not yet been verified.'; '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 @override
String get verificationBadgeRedDesc => String get verificationBadgeRedDesc =>
'Unknown contact whose identity has not yet been verified.'; '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 { enum Type {
LINK = 0; LINK = 0;
CONTACTS = 1; CONTACTS = 1;
RESTORED_FLAME_COUNTER = 2;
} }
Type type = 1; Type type = 1;
optional string link = 2; optional string link = 2;
repeated SharedContact contacts = 3; repeated SharedContact contacts = 3;
optional int64 restored_flame_counter = 4;
} }

View file

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

View file

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

View file

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

View file

@ -303,6 +303,8 @@ Color getMessageColorFromType(
Color color; Color color;
if (message.type == MessageType.text.name) { if (message.type == MessageType.text.name) {
color = Colors.orange;
} else if (message.type == MessageType.text.name) {
color = Colors.blueAccent; color = Colors.blueAccent;
} else if (mediaFile != null) { } else if (mediaFile != null) {
if (mediaFile.requiresAuthentication) { 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/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_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_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_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_text_entry.dart';
import 'package:twonly/src/views/chats/chat_messages_components/entries/chat_unkown.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(); 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', '💯': '100.lottie',
'🎉': 'party-popper.lottie', '🎉': 'party-popper.lottie',
'🎊': 'confetti-ball.lottie', '🎊': 'confetti-ball.lottie',
'🎂': 'birthday-cake.json',
'🧡': 'orange-heart.lottie', '🧡': 'orange-heart.lottie',
'💛': 'yellow-heart.lottie', '💛': 'yellow-heart.lottie',
'💚': 'green-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 SizedBox(width: 5),
if (widget.prefix) const Text(''), if (widget.prefix) const Text(''),
if (widget.prefix) const SizedBox(width: 5), if (widget.prefix) const SizedBox(width: 5),
Text( if (flameCounter != 100)
flameCounter.toString(), Text(
style: const TextStyle(fontSize: 13), flameCounter.toString(),
), style: const TextStyle(fontSize: 13),
),
SizedBox( SizedBox(
height: 15, height: 15,
child: EmojiAnimation( child: EmojiAnimation(

View file

@ -1,11 +1,18 @@
import 'dart:async'; import 'dart:async';
import 'package:clock/clock.dart'; import 'package:clock/clock.dart';
import 'package:drift/drift.dart' show Value; import 'package:drift/drift.dart' show Value;
import 'package:fixnum/fixnum.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/constants/routes.keys.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/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/flame.service.dart';
import 'package:twonly/src/services/subscription.service.dart'; import 'package:twonly/src/services/subscription.service.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
@ -46,7 +53,8 @@ class _MaxFlameListTitleState extends State<MaxFlameListTitle> {
} }
Future<void> _restoreFlames() async { Future<void> _restoreFlames() async {
if (!isUserAllowed(getCurrentPlan(), PremiumFeatures.RestoreFlames)) { if (!isUserAllowed(getCurrentPlan(), PremiumFeatures.RestoreFlames) &&
kReleaseMode) {
await context.push(Routes.settingsSubscription); await context.push(Routes.settingsSubscription);
return; return;
} }
@ -60,7 +68,40 @@ class _MaxFlameListTitleState extends State<MaxFlameListTitle> {
lastFlameCounterChange: Value(clock.now()), 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 syncFlameCounters(forceForGroup: _groupId);
await sendCipherTextToGroup(
_groupId,
encryptedContent,
messageId: message.messageId,
);
} }
@override @override

View file

@ -1,10 +1,13 @@
import 'dart:async'; import 'dart:async';
import 'package:clock/clock.dart';
import 'package:drift/drift.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:restart_app/restart_app.dart'; import 'package:restart_app/restart_app.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/constants/routes.keys.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/utils/storage.dart';
import 'package:twonly/src/views/components/alert_dialog.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) if (!kReleaseMode)
ListTile( ListTile(
title: const Text('Automated Testing'), title: const Text('Automated Testing'),