mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-05-25 05:22:13 +00:00
Improved: Flame restore experience
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
This commit is contained in:
parent
b7c4832ee2
commit
cd5409d021
7 changed files with 168 additions and 81 deletions
|
|
@ -5,6 +5,7 @@
|
||||||
- New: Adds an "Ask a Friend" button to new contact suggestions.
|
- New: Adds an "Ask a Friend" button to new contact suggestions.
|
||||||
- New: Adds security profiles.
|
- New: Adds security profiles.
|
||||||
- Improved: Onboarding flow for new users.
|
- Improved: Onboarding flow for new users.
|
||||||
|
- Improved: Flame restore experience.
|
||||||
- Improved: The blue verification checkmark now displays the total number of verifications.
|
- Improved: The blue verification checkmark now displays the total number of verifications.
|
||||||
- Fix: Issue with receiving messages when user closed app while decrypting
|
- Fix: Issue with receiving messages when user closed app while decrypting
|
||||||
- Fix: Background message fetching reliability.
|
- Fix: Background message fetching reliability.
|
||||||
|
|
|
||||||
|
|
@ -65,23 +65,18 @@ Future<void> syncFlameCounters({String? forceForGroup}) async {
|
||||||
({int counter, bool isExpiring}) getFlameCounterFromGroup(Group? group) {
|
({int counter, bool isExpiring}) getFlameCounterFromGroup(Group? group) {
|
||||||
const zero = (counter: 0, isExpiring: false);
|
const zero = (counter: 0, isExpiring: false);
|
||||||
if (group == null) return zero;
|
if (group == null) return zero;
|
||||||
if (group.lastMessageSend == null ||
|
if (group.lastMessageSend == null || group.lastMessageReceived == null || group.lastFlameCounterChange == null) {
|
||||||
group.lastMessageReceived == null ||
|
|
||||||
group.lastFlameCounterChange == null) {
|
|
||||||
return zero;
|
return zero;
|
||||||
}
|
}
|
||||||
final now = clock.now();
|
final now = clock.now();
|
||||||
final startOfToday = DateTime(now.year, now.month, now.day);
|
final startOfToday = DateTime(now.year, now.month, now.day);
|
||||||
final twoDaysAgo = startOfToday.subtract(const Duration(days: 2));
|
final twoDaysAgo = startOfToday.subtract(const Duration(days: 2));
|
||||||
final oneDayAgo = startOfToday.subtract(const Duration(days: 1));
|
final oneDayAgo = startOfToday.subtract(const Duration(days: 1));
|
||||||
if (group.lastMessageSend!.isAfter(twoDaysAgo) &&
|
if (group.lastMessageSend!.isAfter(twoDaysAgo) && group.lastMessageReceived!.isAfter(twoDaysAgo) ||
|
||||||
group.lastMessageReceived!.isAfter(twoDaysAgo) ||
|
|
||||||
group.lastFlameCounterChange!.isAfter(oneDayAgo)) {
|
group.lastFlameCounterChange!.isAfter(oneDayAgo)) {
|
||||||
// Flame is expiring when today no exchange has happened yet:
|
// Flame is expiring when today no exchange has happened yet:
|
||||||
// both lastMessageSend and lastMessageReceived are before startOfToday.
|
// both lastMessageSend and lastMessageReceived are before startOfToday.
|
||||||
final isExpiring =
|
final isExpiring = group.lastMessageSend!.isBefore(oneDayAgo) || group.lastMessageReceived!.isBefore(oneDayAgo);
|
||||||
group.lastMessageSend!.isBefore(oneDayAgo) ||
|
|
||||||
group.lastMessageReceived!.isBefore(oneDayAgo);
|
|
||||||
return (counter: group.flameCounter, isExpiring: isExpiring);
|
return (counter: group.flameCounter, isExpiring: isExpiring);
|
||||||
} else {
|
} else {
|
||||||
return zero;
|
return zero;
|
||||||
|
|
@ -122,8 +117,7 @@ Future<void> incFlameCounter(
|
||||||
final now = clock.now();
|
final now = clock.now();
|
||||||
final startOfToday = DateTime(now.year, now.month, now.day);
|
final startOfToday = DateTime(now.year, now.month, now.day);
|
||||||
final twoDaysAgo = startOfToday.subtract(const Duration(days: 2));
|
final twoDaysAgo = startOfToday.subtract(const Duration(days: 2));
|
||||||
if (group.lastMessageSend!.isBefore(twoDaysAgo) ||
|
if (group.lastMessageSend!.isBefore(twoDaysAgo) || group.lastMessageReceived!.isBefore(twoDaysAgo)) {
|
||||||
group.lastMessageReceived!.isBefore(twoDaysAgo)) {
|
|
||||||
flameCounter = 0;
|
flameCounter = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -135,25 +129,21 @@ Future<void> incFlameCounter(
|
||||||
final now = clock.now();
|
final now = clock.now();
|
||||||
final startOfToday = DateTime(now.year, now.month, now.day);
|
final startOfToday = DateTime(now.year, now.month, now.day);
|
||||||
|
|
||||||
if (group.lastFlameCounterChange == null ||
|
if (group.lastFlameCounterChange == null || group.lastFlameCounterChange!.isBefore(startOfToday)) {
|
||||||
group.lastFlameCounterChange!.isBefore(startOfToday)) {
|
|
||||||
// last flame update was yesterday. check if it can be updated.
|
// last flame update was yesterday. check if it can be updated.
|
||||||
var updateFlame = false;
|
var updateFlame = false;
|
||||||
if (received) {
|
if (received) {
|
||||||
if (group.lastMessageSend != null &&
|
if (group.lastMessageSend != null && group.lastMessageSend!.isAfter(startOfToday)) {
|
||||||
group.lastMessageSend!.isAfter(startOfToday)) {
|
|
||||||
// today a message was already send -> update flame
|
// today a message was already send -> update flame
|
||||||
updateFlame = true;
|
updateFlame = true;
|
||||||
}
|
}
|
||||||
} else if (group.lastMessageReceived != null &&
|
} else if (group.lastMessageReceived != null && group.lastMessageReceived!.isAfter(startOfToday)) {
|
||||||
group.lastMessageReceived!.isAfter(startOfToday)) {
|
|
||||||
// today a message was already received -> update flame
|
// today a message was already received -> update flame
|
||||||
updateFlame = true;
|
updateFlame = true;
|
||||||
}
|
}
|
||||||
if (updateFlame) {
|
if (updateFlame) {
|
||||||
flameCounter += 1;
|
flameCounter += 1;
|
||||||
if (group.lastFlameCounterChange == null ||
|
if (group.lastFlameCounterChange == null || group.lastFlameCounterChange!.isBefore(timestamp)) {
|
||||||
group.lastFlameCounterChange!.isBefore(timestamp)) {
|
|
||||||
// only update if the timestamp is newer
|
// only update if the timestamp is newer
|
||||||
lastFlameCounterChange = Value(timestamp);
|
lastFlameCounterChange = Value(timestamp);
|
||||||
}
|
}
|
||||||
|
|
@ -170,13 +160,11 @@ Future<void> incFlameCounter(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (received) {
|
if (received) {
|
||||||
if (group.lastMessageReceived == null ||
|
if (group.lastMessageReceived == null || group.lastMessageReceived!.isBefore(timestamp)) {
|
||||||
group.lastMessageReceived!.isBefore(timestamp)) {
|
|
||||||
lastMessageReceived = Value(timestamp);
|
lastMessageReceived = Value(timestamp);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (group.lastMessageSend == null ||
|
if (group.lastMessageSend == null || group.lastMessageSend!.isBefore(timestamp)) {
|
||||||
group.lastMessageSend!.isBefore(timestamp)) {
|
|
||||||
lastMessageSend = Value(timestamp);
|
lastMessageSend = Value(timestamp);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -203,3 +191,18 @@ bool isItPossibleToRestoreFlames(Group group) {
|
||||||
clock.now().subtract(const Duration(days: 7)),
|
clock.now().subtract(const Duration(days: 7)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> restoreFlames(String groupId) async {
|
||||||
|
final group = await twonlyDB.groupsDao.getGroup(groupId);
|
||||||
|
if (group == null) return;
|
||||||
|
final now = clock.now();
|
||||||
|
await twonlyDB.groupsDao.updateGroup(
|
||||||
|
groupId,
|
||||||
|
GroupsCompanion(
|
||||||
|
flameCounter: Value(group.maxFlameCounter),
|
||||||
|
lastFlameCounterChange: Value(now),
|
||||||
|
lastMessageSend: Value(now),
|
||||||
|
lastMessageReceived: Value(now),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import 'package:flutter/material.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/data.pb.dart';
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
|
import 'package:twonly/src/visual/components/animate_icon.comp.dart';
|
||||||
import 'package:twonly/src/visual/elements/better_text.element.dart';
|
import 'package:twonly/src/visual/elements/better_text.element.dart';
|
||||||
|
|
||||||
import 'package:twonly/src/visual/views/chats/chat_messages_components/entries/common.dart';
|
import 'package:twonly/src/visual/views/chats/chat_messages_components/entries/common.dart';
|
||||||
|
|
@ -42,15 +43,28 @@ class ChatFlameRestoredEntry extends StatelessWidget {
|
||||||
),
|
),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.orange,
|
color: info.color,
|
||||||
borderRadius: borderRadius,
|
borderRadius: borderRadius,
|
||||||
),
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const SizedBox(
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
child: EmojiAnimationComp(emoji: '🔥'),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Flexible(
|
||||||
child: BetterText(
|
child: BetterText(
|
||||||
text: context.lang.chatEntryFlameRestored(
|
text: context.lang.chatEntryFlameRestored(
|
||||||
data.restoredFlameCounter.toInt(),
|
data.restoredFlameCounter.toInt(),
|
||||||
),
|
),
|
||||||
textColor: isDarkMode(context) ? Colors.black : Colors.black,
|
textColor: isDarkMode(context) ? Colors.black : Colors.black,
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import 'package:twonly/src/visual/views/camera/camera_send_to.view.dart';
|
||||||
import 'package:twonly/src/visual/views/chats/chat_messages_components/bottom_sheets/share_additional.bottom_sheet.dart';
|
import 'package:twonly/src/visual/views/chats/chat_messages_components/bottom_sheets/share_additional.bottom_sheet.dart';
|
||||||
import 'package:twonly/src/visual/views/chats/chat_messages_components/entries/chat_audio_entry.dart';
|
import 'package:twonly/src/visual/views/chats/chat_messages_components/entries/chat_audio_entry.dart';
|
||||||
import 'package:twonly/src/visual/views/chats/chat_messages_components/user_discovery_manual_approval.comp.dart';
|
import 'package:twonly/src/visual/views/chats/chat_messages_components/user_discovery_manual_approval.comp.dart';
|
||||||
|
import 'package:twonly/src/visual/views/contact/contact_components/restore_flame.comp.dart';
|
||||||
|
|
||||||
class MessageInput extends StatefulWidget {
|
class MessageInput extends StatefulWidget {
|
||||||
const MessageInput({
|
const MessageInput({
|
||||||
|
|
@ -53,6 +54,7 @@ class _MessageInputState extends State<MessageInput> {
|
||||||
RecordingState _recordingState = RecordingState.none;
|
RecordingState _recordingState = RecordingState.none;
|
||||||
Timer? _nextTypingIndicator;
|
Timer? _nextTypingIndicator;
|
||||||
DateTime? _lastTextChangeTime;
|
DateTime? _lastTextChangeTime;
|
||||||
|
int? _contactId;
|
||||||
|
|
||||||
Future<void> _sendMessage() async {
|
Future<void> _sendMessage() async {
|
||||||
if (_textFieldController.text == '') return;
|
if (_textFieldController.text == '') return;
|
||||||
|
|
@ -83,13 +85,13 @@ class _MessageInputState extends State<MessageInput> {
|
||||||
) async {
|
) async {
|
||||||
if (widget.textFieldFocus.hasFocus &&
|
if (widget.textFieldFocus.hasFocus &&
|
||||||
_lastTextChangeTime != null &&
|
_lastTextChangeTime != null &&
|
||||||
DateTime.now().difference(_lastTextChangeTime!) <=
|
DateTime.now().difference(_lastTextChangeTime!) <= const Duration(seconds: 6)) {
|
||||||
const Duration(seconds: 6)) {
|
|
||||||
await sendTypingIndication(widget.group.groupId, true);
|
await sendTypingIndication(widget.group.groupId, true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
_initializeControllers();
|
_initializeControllers();
|
||||||
|
_loadContactId();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -205,11 +207,35 @@ class _MessageInputState extends State<MessageInput> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _loadContactId() async {
|
||||||
|
if (widget.group.isDirectChat) {
|
||||||
|
final members = await twonlyDB.groupsDao.getGroupContact(widget.group.groupId);
|
||||||
|
if (members.isNotEmpty && mounted) {
|
||||||
|
setState(() {
|
||||||
|
_contactId = members.first.userId;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
UserDiscoveryManualApprovalComp(group: widget.group),
|
UserDiscoveryManualApprovalComp(group: widget.group),
|
||||||
|
if (_contactId != null)
|
||||||
|
Container(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: context.color.surfaceContainerHighest,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
child: RestoreFlameComp(
|
||||||
|
contactId: _contactId!,
|
||||||
|
flameOnRightSide: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(
|
padding: const EdgeInsets.only(
|
||||||
bottom: 10,
|
bottom: 10,
|
||||||
|
|
@ -252,9 +278,7 @@ class _MessageInputState extends State<MessageInput> {
|
||||||
),
|
),
|
||||||
child: FaIcon(
|
child: FaIcon(
|
||||||
size: 20,
|
size: 20,
|
||||||
_emojiShowing
|
_emojiShowing ? FontAwesomeIcons.keyboard : FontAwesomeIcons.faceSmile,
|
||||||
? FontAwesomeIcons.keyboard
|
|
||||||
: FontAwesomeIcons.faceSmile,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -266,8 +290,7 @@ class _MessageInputState extends State<MessageInput> {
|
||||||
controller: _textFieldController,
|
controller: _textFieldController,
|
||||||
focusNode: widget.textFieldFocus,
|
focusNode: widget.textFieldFocus,
|
||||||
keyboardType: TextInputType.multiline,
|
keyboardType: TextInputType.multiline,
|
||||||
showCursor:
|
showCursor: _recordingState != RecordingState.recording,
|
||||||
_recordingState != RecordingState.recording,
|
|
||||||
maxLines: 4,
|
maxLines: 4,
|
||||||
minLines: 1,
|
minLines: 1,
|
||||||
onChanged: (value) async {
|
onChanged: (value) async {
|
||||||
|
|
@ -318,9 +341,7 @@ class _MessageInputState extends State<MessageInput> {
|
||||||
_currentDuration,
|
_currentDuration,
|
||||||
),
|
),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: isDarkMode(context)
|
color: isDarkMode(context) ? Colors.white : Colors.black,
|
||||||
? Colors.white
|
|
||||||
: Colors.black,
|
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -370,17 +391,13 @@ class _MessageInputState extends State<MessageInput> {
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onLongPressMoveUpdate: (details) {
|
onLongPressMoveUpdate: (details) {
|
||||||
if (_audioRecordingLock) return;
|
if (_audioRecordingLock) return;
|
||||||
if (_recordingOffset.dy -
|
if (_recordingOffset.dy - details.localPosition.dy >= 100) {
|
||||||
details.localPosition.dy >=
|
|
||||||
100) {
|
|
||||||
HapticFeedback.heavyImpact();
|
HapticFeedback.heavyImpact();
|
||||||
setState(() {
|
setState(() {
|
||||||
_audioRecordingLock = true;
|
_audioRecordingLock = true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (_recordingOffset.dx -
|
if (_recordingOffset.dx - details.localPosition.dx >= 90 &&
|
||||||
details.localPosition.dx >=
|
|
||||||
90 &&
|
|
||||||
_recordingState == RecordingState.recording) {
|
_recordingState == RecordingState.recording) {
|
||||||
_recordingState = RecordingState.none;
|
_recordingState = RecordingState.none;
|
||||||
HapticFeedback.heavyImpact();
|
HapticFeedback.heavyImpact();
|
||||||
|
|
@ -388,13 +405,9 @@ class _MessageInputState extends State<MessageInput> {
|
||||||
}
|
}
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
final a =
|
final a = _recordingOffset.dx - details.localPosition.dx;
|
||||||
_recordingOffset.dx -
|
|
||||||
details.localPosition.dx;
|
|
||||||
if (a > 0 && a <= 90) {
|
if (a > 0 && a <= 90) {
|
||||||
_cancelSlideOffset =
|
_cancelSlideOffset = _recordingOffset.dx - details.localPosition.dx;
|
||||||
_recordingOffset.dx -
|
|
||||||
details.localPosition.dx;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
@ -414,8 +427,7 @@ class _MessageInputState extends State<MessageInput> {
|
||||||
child: Stack(
|
child: Stack(
|
||||||
clipBehavior: Clip.none,
|
clipBehavior: Clip.none,
|
||||||
children: [
|
children: [
|
||||||
if (_recordingState == RecordingState.recording &&
|
if (_recordingState == RecordingState.recording && !_audioRecordingLock)
|
||||||
!_audioRecordingLock)
|
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
top: -120,
|
top: -120,
|
||||||
left: -5,
|
left: -5,
|
||||||
|
|
@ -426,9 +438,7 @@ class _MessageInputState extends State<MessageInput> {
|
||||||
height: 60,
|
height: 60,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(90),
|
borderRadius: BorderRadius.circular(90),
|
||||||
color: isDarkMode(context)
|
color: isDarkMode(context) ? Colors.black : Colors.white,
|
||||||
? Colors.black
|
|
||||||
: Colors.white,
|
|
||||||
),
|
),
|
||||||
child: const Center(
|
child: const Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
|
|
@ -448,8 +458,7 @@ class _MessageInputState extends State<MessageInput> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (_recordingState == RecordingState.recording &&
|
if (_recordingState == RecordingState.recording && !_audioRecordingLock)
|
||||||
!_audioRecordingLock)
|
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
top: -20,
|
top: -20,
|
||||||
left: -25,
|
left: -25,
|
||||||
|
|
@ -476,15 +485,10 @@ class _MessageInputState extends State<MessageInput> {
|
||||||
),
|
),
|
||||||
child: FaIcon(
|
child: FaIcon(
|
||||||
size: 20,
|
size: 20,
|
||||||
color:
|
color: (_recordingState == RecordingState.recording) ? Colors.white : null,
|
||||||
(_recordingState ==
|
|
||||||
RecordingState.recording)
|
|
||||||
? Colors.white
|
|
||||||
: null,
|
|
||||||
(_recordingState == RecordingState.none)
|
(_recordingState == RecordingState.none)
|
||||||
? FontAwesomeIcons.microphone
|
? FontAwesomeIcons.microphone
|
||||||
: (_recordingState ==
|
: (_recordingState == RecordingState.recording)
|
||||||
RecordingState.recording)
|
|
||||||
? FontAwesomeIcons.stop
|
? FontAwesomeIcons.stop
|
||||||
: FontAwesomeIcons.play,
|
: FontAwesomeIcons.play,
|
||||||
),
|
),
|
||||||
|
|
@ -504,9 +508,7 @@ class _MessageInputState extends State<MessageInput> {
|
||||||
color: context.color.primary,
|
color: context.color.primary,
|
||||||
FontAwesomeIcons.solidPaperPlane,
|
FontAwesomeIcons.solidPaperPlane,
|
||||||
),
|
),
|
||||||
onPressed: _audioRecordingLock
|
onPressed: _audioRecordingLock ? _stopAudioRecording : _sendMessage,
|
||||||
? _stopAudioRecording
|
|
||||||
: _sendMessage,
|
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
IconButton(
|
IconButton(
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
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:fixnum/fixnum.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
@ -11,8 +10,7 @@ import 'package:twonly/src/constants/routes.keys.dart';
|
||||||
import 'package:twonly/src/database/tables/messages.table.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/data.pb.dart';
|
||||||
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'
|
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart' as pb;
|
||||||
as pb;
|
|
||||||
import 'package:twonly/src/services/api/messages.api.dart';
|
import 'package:twonly/src/services/api/messages.api.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';
|
||||||
|
|
@ -24,9 +22,11 @@ import 'package:twonly/src/visual/elements/better_list_title.element.dart';
|
||||||
class RestoreFlameComp extends StatefulWidget {
|
class RestoreFlameComp extends StatefulWidget {
|
||||||
const RestoreFlameComp({
|
const RestoreFlameComp({
|
||||||
required this.contactId,
|
required this.contactId,
|
||||||
|
this.flameOnRightSide = false,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
final int contactId;
|
final int contactId;
|
||||||
|
final bool flameOnRightSide;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<RestoreFlameComp> createState() => _RestoreFlameCompState();
|
State<RestoreFlameComp> createState() => _RestoreFlameCompState();
|
||||||
|
|
@ -60,21 +60,15 @@ class _RestoreFlameCompState extends State<RestoreFlameComp> {
|
||||||
final currentPlan = planFromString(
|
final currentPlan = planFromString(
|
||||||
userService.currentUser.subscriptionPlan,
|
userService.currentUser.subscriptionPlan,
|
||||||
);
|
);
|
||||||
if (!isUserAllowed(currentPlan, PremiumFeatures.RestoreFlames) &&
|
if (!isUserAllowed(currentPlan, PremiumFeatures.RestoreFlames) && kReleaseMode) {
|
||||||
kReleaseMode) {
|
|
||||||
await context.push(Routes.settingsSubscription);
|
await context.push(Routes.settingsSubscription);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Log.info(
|
Log.info(
|
||||||
'Restoring flames from ${_group!.flameCounter} to ${_group!.maxFlameCounter}',
|
'Restoring flames from ${_group!.flameCounter} to ${_group!.maxFlameCounter}',
|
||||||
);
|
);
|
||||||
await twonlyDB.groupsDao.updateGroup(
|
|
||||||
_groupId,
|
await restoreFlames(_groupId);
|
||||||
GroupsCompanion(
|
|
||||||
flameCounter: Value(_group!.maxFlameCounter),
|
|
||||||
lastFlameCounterChange: Value(clock.now()),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
final addData = AdditionalMessageData(
|
final addData = AdditionalMessageData(
|
||||||
type: AdditionalMessageData_Type.RESTORED_FLAME_COUNTER,
|
type: AdditionalMessageData_Type.RESTORED_FLAME_COUNTER,
|
||||||
|
|
@ -116,6 +110,22 @@ class _RestoreFlameCompState extends State<RestoreFlameComp> {
|
||||||
if (_group == null || !isItPossibleToRestoreFlames(_group!)) {
|
if (_group == null || !isItPossibleToRestoreFlames(_group!)) {
|
||||||
return Container();
|
return Container();
|
||||||
}
|
}
|
||||||
|
if (widget.flameOnRightSide) {
|
||||||
|
return ListTile(
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
onTap: _restoreFlames,
|
||||||
|
title: Text(
|
||||||
|
'Restore your ${_group!.maxFlameCounter} lost flames',
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.w500),
|
||||||
|
),
|
||||||
|
trailing: const SizedBox(
|
||||||
|
width: 24,
|
||||||
|
child: EmojiAnimationComp(
|
||||||
|
emoji: '🔥',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
return BetterListTile(
|
return BetterListTile(
|
||||||
onTap: _restoreFlames,
|
onTap: _restoreFlames,
|
||||||
leading: const SizedBox(
|
leading: const SizedBox(
|
||||||
|
|
|
||||||
|
|
@ -87,7 +87,7 @@ void main() {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('test flame counter', () async {
|
test('normal flame expiring', () async {
|
||||||
final contactId = await getAndCreateUserId();
|
final contactId = await getAndCreateUserId();
|
||||||
final contact = (await twonlyDB.contactsDao.getContactById(contactId))!;
|
final contact = (await twonlyDB.contactsDao.getContactById(contactId))!;
|
||||||
await twonlyDB.groupsDao.createNewDirectChat(
|
await twonlyDB.groupsDao.createNewDirectChat(
|
||||||
|
|
@ -182,6 +182,21 @@ void main() {
|
||||||
counter: 0,
|
counter: 0,
|
||||||
isExpiring: false,
|
isExpiring: false,
|
||||||
);
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('isRestore Possible', () async {
|
||||||
|
final contactId = await getAndCreateUserId();
|
||||||
|
final contact = (await twonlyDB.contactsDao.getContactById(contactId))!;
|
||||||
|
await twonlyDB.groupsDao.createNewDirectChat(
|
||||||
|
contactId,
|
||||||
|
GroupsCompanion(
|
||||||
|
groupName: Value(
|
||||||
|
getContactDisplayName(contact),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final group = (await twonlyDB.groupsDao.getDirectChat(contactId))!;
|
||||||
|
|
||||||
for (var i = 1; i <= 20; i++) {
|
for (var i = 1; i <= 20; i++) {
|
||||||
await withClock(
|
await withClock(
|
||||||
|
|
@ -226,6 +241,48 @@ void main() {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('flame restoring', () async {
|
||||||
|
final contactId = await getAndCreateUserId();
|
||||||
|
final contact = (await twonlyDB.contactsDao.getContactById(contactId))!;
|
||||||
|
await twonlyDB.groupsDao.createNewDirectChat(
|
||||||
|
contactId,
|
||||||
|
GroupsCompanion(
|
||||||
|
groupName: Value(
|
||||||
|
getContactDisplayName(contact),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final group = (await twonlyDB.groupsDao.getDirectChat(contactId))!;
|
||||||
|
|
||||||
|
for (var i = 1; i <= 5; i++) {
|
||||||
|
await withClock(
|
||||||
|
Clock.fixed(DateTime(2026, 3, i, 1)),
|
||||||
|
() async {
|
||||||
|
await incFlameCounter(group.groupId, true, DateTime(2026, 3, i, 2));
|
||||||
|
await incFlameCounter(group.groupId, false, DateTime(2026, 3, i, 3));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await expectFlame(DateTime(2026, 3, 5, 19), group.groupId, 5);
|
||||||
|
await expectFlame(DateTime(2026, 3, 8, 19), group.groupId, 0);
|
||||||
|
|
||||||
|
await withClock(
|
||||||
|
Clock.fixed(DateTime(2026, 3, 9, 12)),
|
||||||
|
() async {
|
||||||
|
await restoreFlames(group.groupId);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
await expectFlameExpiring(
|
||||||
|
DateTime(2026, 3, 9, 13),
|
||||||
|
group.groupId,
|
||||||
|
counter: 5,
|
||||||
|
isExpiring: false,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
tearDown(() async {
|
tearDown(() async {
|
||||||
await twonlyDB.close();
|
await twonlyDB.close();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
// ignore_for_file: avoid_print, avoid_dynamic_calls
|
// ignore_for_file: avoid_dynamic_calls
|
||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue