mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-05-24 23:52:11 +00:00
Show ⌛ instead of the flame icon when it is about to expire
This commit is contained in:
parent
a015cb2cb8
commit
9289def783
6 changed files with 102 additions and 18 deletions
|
|
@ -5,6 +5,7 @@
|
|||
- New: Feature to find friends without a phone number
|
||||
- New: The verification state is now transferred to the scanned user
|
||||
- New: Registration setup to configure the most important configurations
|
||||
- Improved: Show ⌛ instead of the flame icon when it is about to expire
|
||||
- Improved: FAQ is now in the app rather than opening in the browser
|
||||
- Fix: Many smaller issues
|
||||
|
||||
|
|
|
|||
|
|
@ -241,7 +241,7 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
|
|||
)..where((t) => t.groupId.equals(groupId))).getSingleOrNull();
|
||||
}
|
||||
|
||||
Stream<int> watchFlameCounter(String groupId) {
|
||||
Stream<({int counter, bool isExpiring})> watchFlameCounter(String groupId) {
|
||||
return (select(groups)..where(
|
||||
(u) =>
|
||||
u.groupId.equals(groupId) &
|
||||
|
|
@ -249,7 +249,7 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
|
|||
u.lastMessageSend.isNotNull(),
|
||||
))
|
||||
.watchSingleOrNull()
|
||||
.asyncMap(getFlameCounterFromGroup);
|
||||
.map(getFlameCounterFromGroup);
|
||||
}
|
||||
|
||||
Future<List<Group>> getAllDirectChats() {
|
||||
|
|
|
|||
|
|
@ -32,16 +32,17 @@ Future<void> syncFlameCounters({String? forceForGroup}) async {
|
|||
}
|
||||
}
|
||||
|
||||
final flameCounter = getFlameCounterFromGroup(group);
|
||||
final flameResult = getFlameCounterFromGroup(group);
|
||||
|
||||
// only sync when flame counter is higher three or when they are bestFriends
|
||||
if (flameCounter <= 2 && bestFriend.groupId != group.groupId) continue;
|
||||
if (flameResult.counter <= 2 && bestFriend.groupId != group.groupId)
|
||||
continue;
|
||||
|
||||
await sendCipherTextToGroup(
|
||||
group.groupId,
|
||||
EncryptedContent(
|
||||
flameSync: EncryptedContent_FlameSync(
|
||||
flameCounter: Int64(flameCounter),
|
||||
flameCounter: Int64(flameResult.counter),
|
||||
lastFlameCounterChange: Int64(
|
||||
group.lastFlameCounterChange!.millisecondsSinceEpoch,
|
||||
),
|
||||
|
|
@ -60,12 +61,13 @@ Future<void> syncFlameCounters({String? forceForGroup}) async {
|
|||
}
|
||||
}
|
||||
|
||||
int getFlameCounterFromGroup(Group? group) {
|
||||
if (group == null) return 0;
|
||||
({int counter, bool isExpiring}) getFlameCounterFromGroup(Group? group) {
|
||||
const zero = (counter: 0, isExpiring: false);
|
||||
if (group == null) return zero;
|
||||
if (group.lastMessageSend == null ||
|
||||
group.lastMessageReceived == null ||
|
||||
group.lastFlameCounterChange == null) {
|
||||
return 0;
|
||||
return zero;
|
||||
}
|
||||
final now = clock.now();
|
||||
final startOfToday = DateTime(now.year, now.month, now.day);
|
||||
|
|
@ -74,9 +76,14 @@ int getFlameCounterFromGroup(Group? group) {
|
|||
if (group.lastMessageSend!.isAfter(twoDaysAgo) &&
|
||||
group.lastMessageReceived!.isAfter(twoDaysAgo) ||
|
||||
group.lastFlameCounterChange!.isAfter(oneDayAgo)) {
|
||||
return group.flameCounter;
|
||||
// Flame is expiring when today no exchange has happened yet:
|
||||
// both lastMessageSend and lastMessageReceived are before startOfToday.
|
||||
final isExpiring =
|
||||
group.lastMessageSend!.isBefore(oneDayAgo) ||
|
||||
group.lastMessageReceived!.isBefore(oneDayAgo);
|
||||
return (counter: group.flameCounter, isExpiring: isExpiring);
|
||||
} else {
|
||||
return 0;
|
||||
return zero;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -190,9 +197,9 @@ Future<void> incFlameCounter(
|
|||
}
|
||||
|
||||
bool isItPossibleToRestoreFlames(Group group) {
|
||||
final flameCounter = getFlameCounterFromGroup(group);
|
||||
final flameResult = getFlameCounterFromGroup(group);
|
||||
return group.maxFlameCounter > 2 &&
|
||||
flameCounter < group.maxFlameCounter &&
|
||||
flameResult.counter < group.maxFlameCounter &&
|
||||
group.maxFlameCounterFrom!.isAfter(
|
||||
clock.now().subtract(const Duration(days: 7)),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -23,8 +23,9 @@ class FlameCounterWidget extends StatefulWidget {
|
|||
class _FlameCounterWidgetState extends State<FlameCounterWidget> {
|
||||
int flameCounter = 0;
|
||||
bool isBestFriend = false;
|
||||
bool isExpiring = false;
|
||||
|
||||
StreamSubscription<int>? flameCounterSub;
|
||||
StreamSubscription<({int counter, bool isExpiring})>? flameCounterSub;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
|
@ -52,10 +53,11 @@ class _FlameCounterWidgetState extends State<FlameCounterWidget> {
|
|||
userService.currentUser.myBestFriendGroupId == groupId &&
|
||||
group.alsoBestFriend;
|
||||
final stream = twonlyDB.groupsDao.watchFlameCounter(groupId);
|
||||
flameCounterSub = stream.listen((counter) {
|
||||
flameCounterSub = stream.listen((result) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
flameCounter = counter;
|
||||
flameCounter = result.counter;
|
||||
isExpiring = result.isExpiring;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -75,6 +77,9 @@ class _FlameCounterWidgetState extends State<FlameCounterWidget> {
|
|||
flameEmoji = '🎂';
|
||||
}
|
||||
|
||||
// Override with hourglass when the flame is about to expire
|
||||
if (isExpiring) flameEmoji = '⌛';
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
if (widget.prefix) const SizedBox(width: 5),
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ class _ShareImageView extends State<ShareImageView> {
|
|||
|
||||
final flameComparison = getFlameCounterFromGroup(
|
||||
b,
|
||||
).compareTo(getFlameCounterFromGroup(a));
|
||||
).counter.compareTo(getFlameCounterFromGroup(a).counter);
|
||||
if (flameComparison != 0) {
|
||||
return flameComparison; // Sort by flameCounter in descending order
|
||||
}
|
||||
|
|
@ -111,7 +111,7 @@ class _ShareImageView extends State<ShareImageView> {
|
|||
for (final group in groups) {
|
||||
if (group.pinned) continue;
|
||||
if (!group.archived &&
|
||||
(getFlameCounterFromGroup(group)) > 0 &&
|
||||
getFlameCounterFromGroup(group).counter > 0 &&
|
||||
bestFriends.length < 6) {
|
||||
bestFriends.add(group);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ Future<void> expectFlame(DateTime time, String groupId, int counter) async {
|
|||
() async {
|
||||
final group = (await twonlyDB.groupsDao.getGroup(groupId))!;
|
||||
expect(
|
||||
getFlameCounterFromGroup(group),
|
||||
getFlameCounterFromGroup(group).counter,
|
||||
counter,
|
||||
reason: StackTrace.current.toString(),
|
||||
);
|
||||
|
|
@ -24,6 +24,31 @@ Future<void> expectFlame(DateTime time, String groupId, int counter) async {
|
|||
);
|
||||
}
|
||||
|
||||
Future<void> expectFlameExpiring(
|
||||
DateTime time,
|
||||
String groupId, {
|
||||
required int counter,
|
||||
required bool isExpiring,
|
||||
}) async {
|
||||
await withClock(
|
||||
Clock.fixed(time),
|
||||
() async {
|
||||
final group = (await twonlyDB.groupsDao.getGroup(groupId))!;
|
||||
final result = getFlameCounterFromGroup(group);
|
||||
expect(
|
||||
result.counter,
|
||||
counter,
|
||||
reason: 'counter mismatch — ${StackTrace.current}',
|
||||
);
|
||||
expect(
|
||||
result.isExpiring,
|
||||
isExpiring,
|
||||
reason: 'isExpiring mismatch — ${StackTrace.current}',
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void main() {
|
||||
final mutex = Mutex();
|
||||
var usedUserIds = 0;
|
||||
|
|
@ -111,6 +136,52 @@ void main() {
|
|||
await expectFlame(DateTime(2026, 2, 6, 19), group.groupId, 2);
|
||||
await expectFlame(DateTime(2026, 3, 1, 19), group.groupId, 0);
|
||||
|
||||
// ── isExpiring tests ──────────────────────────────────────────────────────
|
||||
// After the Feb-5/Feb-6 exchanges the flame is 2.
|
||||
// On Feb-7 no exchange has happened yet → isExpiring should be true.
|
||||
await expectFlameExpiring(
|
||||
DateTime(2026, 2, 7, 10),
|
||||
group.groupId,
|
||||
counter: 2,
|
||||
isExpiring: false,
|
||||
);
|
||||
// Once both sides exchange on Feb-7, isExpiring becomes false.
|
||||
await withClock(
|
||||
Clock.fixed(DateTime(2026, 2, 7, 12)),
|
||||
() async {
|
||||
await incFlameCounter(group.groupId, true, DateTime(2026, 2, 7, 10));
|
||||
await incFlameCounter(group.groupId, false, DateTime(2026, 2, 7, 11));
|
||||
},
|
||||
);
|
||||
await expectFlameExpiring(
|
||||
DateTime(2026, 2, 7, 13),
|
||||
group.groupId,
|
||||
counter: 3,
|
||||
isExpiring: false,
|
||||
);
|
||||
// On Feb-8, before any exchange, isExpiring is true again.
|
||||
await expectFlameExpiring(
|
||||
DateTime(2026, 2, 8, 9),
|
||||
group.groupId,
|
||||
counter: 3,
|
||||
isExpiring: false,
|
||||
);
|
||||
// On Feb-9, last exchange was Feb-7 — still within the two-day window,
|
||||
// so the flame is still alive but expiring.
|
||||
await expectFlameExpiring(
|
||||
DateTime(2026, 2, 9, 9),
|
||||
group.groupId,
|
||||
counter: 3,
|
||||
isExpiring: true,
|
||||
);
|
||||
// On Feb-10, last exchange (Feb-7) is now before twoDaysAgo → flame is gone.
|
||||
await expectFlameExpiring(
|
||||
DateTime(2026, 2, 10, 9),
|
||||
group.groupId,
|
||||
counter: 0,
|
||||
isExpiring: false,
|
||||
);
|
||||
|
||||
for (var i = 1; i <= 20; i++) {
|
||||
await withClock(
|
||||
Clock.fixed(DateTime(2026, 3, i, 1)),
|
||||
|
|
|
|||
Loading…
Reference in a new issue