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: Feature to find friends without a phone number
|
||||||
- New: The verification state is now transferred to the scanned user
|
- New: The verification state is now transferred to the scanned user
|
||||||
- New: Registration setup to configure the most important configurations
|
- 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
|
- Improved: FAQ is now in the app rather than opening in the browser
|
||||||
- Fix: Many smaller issues
|
- Fix: Many smaller issues
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -241,7 +241,7 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
|
||||||
)..where((t) => t.groupId.equals(groupId))).getSingleOrNull();
|
)..where((t) => t.groupId.equals(groupId))).getSingleOrNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
Stream<int> watchFlameCounter(String groupId) {
|
Stream<({int counter, bool isExpiring})> watchFlameCounter(String groupId) {
|
||||||
return (select(groups)..where(
|
return (select(groups)..where(
|
||||||
(u) =>
|
(u) =>
|
||||||
u.groupId.equals(groupId) &
|
u.groupId.equals(groupId) &
|
||||||
|
|
@ -249,7 +249,7 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
|
||||||
u.lastMessageSend.isNotNull(),
|
u.lastMessageSend.isNotNull(),
|
||||||
))
|
))
|
||||||
.watchSingleOrNull()
|
.watchSingleOrNull()
|
||||||
.asyncMap(getFlameCounterFromGroup);
|
.map(getFlameCounterFromGroup);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<Group>> getAllDirectChats() {
|
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
|
// 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(
|
await sendCipherTextToGroup(
|
||||||
group.groupId,
|
group.groupId,
|
||||||
EncryptedContent(
|
EncryptedContent(
|
||||||
flameSync: EncryptedContent_FlameSync(
|
flameSync: EncryptedContent_FlameSync(
|
||||||
flameCounter: Int64(flameCounter),
|
flameCounter: Int64(flameResult.counter),
|
||||||
lastFlameCounterChange: Int64(
|
lastFlameCounterChange: Int64(
|
||||||
group.lastFlameCounterChange!.millisecondsSinceEpoch,
|
group.lastFlameCounterChange!.millisecondsSinceEpoch,
|
||||||
),
|
),
|
||||||
|
|
@ -60,12 +61,13 @@ Future<void> syncFlameCounters({String? forceForGroup}) async {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int getFlameCounterFromGroup(Group? group) {
|
({int counter, bool isExpiring}) getFlameCounterFromGroup(Group? group) {
|
||||||
if (group == null) return 0;
|
const zero = (counter: 0, isExpiring: false);
|
||||||
|
if (group == null) return zero;
|
||||||
if (group.lastMessageSend == null ||
|
if (group.lastMessageSend == null ||
|
||||||
group.lastMessageReceived == null ||
|
group.lastMessageReceived == null ||
|
||||||
group.lastFlameCounterChange == null) {
|
group.lastFlameCounterChange == null) {
|
||||||
return 0;
|
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);
|
||||||
|
|
@ -74,9 +76,14 @@ int getFlameCounterFromGroup(Group? group) {
|
||||||
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)) {
|
||||||
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 {
|
} else {
|
||||||
return 0;
|
return zero;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -190,9 +197,9 @@ Future<void> incFlameCounter(
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isItPossibleToRestoreFlames(Group group) {
|
bool isItPossibleToRestoreFlames(Group group) {
|
||||||
final flameCounter = getFlameCounterFromGroup(group);
|
final flameResult = getFlameCounterFromGroup(group);
|
||||||
return group.maxFlameCounter > 2 &&
|
return group.maxFlameCounter > 2 &&
|
||||||
flameCounter < group.maxFlameCounter &&
|
flameResult.counter < group.maxFlameCounter &&
|
||||||
group.maxFlameCounterFrom!.isAfter(
|
group.maxFlameCounterFrom!.isAfter(
|
||||||
clock.now().subtract(const Duration(days: 7)),
|
clock.now().subtract(const Duration(days: 7)),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -23,8 +23,9 @@ class FlameCounterWidget extends StatefulWidget {
|
||||||
class _FlameCounterWidgetState extends State<FlameCounterWidget> {
|
class _FlameCounterWidgetState extends State<FlameCounterWidget> {
|
||||||
int flameCounter = 0;
|
int flameCounter = 0;
|
||||||
bool isBestFriend = false;
|
bool isBestFriend = false;
|
||||||
|
bool isExpiring = false;
|
||||||
|
|
||||||
StreamSubscription<int>? flameCounterSub;
|
StreamSubscription<({int counter, bool isExpiring})>? flameCounterSub;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
|
@ -52,10 +53,11 @@ class _FlameCounterWidgetState extends State<FlameCounterWidget> {
|
||||||
userService.currentUser.myBestFriendGroupId == groupId &&
|
userService.currentUser.myBestFriendGroupId == groupId &&
|
||||||
group.alsoBestFriend;
|
group.alsoBestFriend;
|
||||||
final stream = twonlyDB.groupsDao.watchFlameCounter(groupId);
|
final stream = twonlyDB.groupsDao.watchFlameCounter(groupId);
|
||||||
flameCounterSub = stream.listen((counter) {
|
flameCounterSub = stream.listen((result) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
flameCounter = counter;
|
flameCounter = result.counter;
|
||||||
|
isExpiring = result.isExpiring;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -75,6 +77,9 @@ class _FlameCounterWidgetState extends State<FlameCounterWidget> {
|
||||||
flameEmoji = '🎂';
|
flameEmoji = '🎂';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Override with hourglass when the flame is about to expire
|
||||||
|
if (isExpiring) flameEmoji = '⌛';
|
||||||
|
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
if (widget.prefix) const SizedBox(width: 5),
|
if (widget.prefix) const SizedBox(width: 5),
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,7 @@ class _ShareImageView extends State<ShareImageView> {
|
||||||
|
|
||||||
final flameComparison = getFlameCounterFromGroup(
|
final flameComparison = getFlameCounterFromGroup(
|
||||||
b,
|
b,
|
||||||
).compareTo(getFlameCounterFromGroup(a));
|
).counter.compareTo(getFlameCounterFromGroup(a).counter);
|
||||||
if (flameComparison != 0) {
|
if (flameComparison != 0) {
|
||||||
return flameComparison; // Sort by flameCounter in descending order
|
return flameComparison; // Sort by flameCounter in descending order
|
||||||
}
|
}
|
||||||
|
|
@ -111,7 +111,7 @@ class _ShareImageView extends State<ShareImageView> {
|
||||||
for (final group in groups) {
|
for (final group in groups) {
|
||||||
if (group.pinned) continue;
|
if (group.pinned) continue;
|
||||||
if (!group.archived &&
|
if (!group.archived &&
|
||||||
(getFlameCounterFromGroup(group)) > 0 &&
|
getFlameCounterFromGroup(group).counter > 0 &&
|
||||||
bestFriends.length < 6) {
|
bestFriends.length < 6) {
|
||||||
bestFriends.add(group);
|
bestFriends.add(group);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ Future<void> expectFlame(DateTime time, String groupId, int counter) async {
|
||||||
() async {
|
() async {
|
||||||
final group = (await twonlyDB.groupsDao.getGroup(groupId))!;
|
final group = (await twonlyDB.groupsDao.getGroup(groupId))!;
|
||||||
expect(
|
expect(
|
||||||
getFlameCounterFromGroup(group),
|
getFlameCounterFromGroup(group).counter,
|
||||||
counter,
|
counter,
|
||||||
reason: StackTrace.current.toString(),
|
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() {
|
void main() {
|
||||||
final mutex = Mutex();
|
final mutex = Mutex();
|
||||||
var usedUserIds = 0;
|
var usedUserIds = 0;
|
||||||
|
|
@ -111,6 +136,52 @@ void main() {
|
||||||
await expectFlame(DateTime(2026, 2, 6, 19), group.groupId, 2);
|
await expectFlame(DateTime(2026, 2, 6, 19), group.groupId, 2);
|
||||||
await expectFlame(DateTime(2026, 3, 1, 19), group.groupId, 0);
|
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++) {
|
for (var i = 1; i <= 20; i++) {
|
||||||
await withClock(
|
await withClock(
|
||||||
Clock.fixed(DateTime(2026, 3, i, 1)),
|
Clock.fixed(DateTime(2026, 3, i, 1)),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue