|
|
|
|
@ -302,6 +302,8 @@ class MeshCoreConnector extends ChangeNotifier {
|
|
|
|
|
final Map<String, int> _contactUnreadCount = {};
|
|
|
|
|
final Map<String, RepeaterBatterySnapshot> _repeaterBatterySnapshots = {};
|
|
|
|
|
bool _unreadStateLoaded = false;
|
|
|
|
|
int _cachedContactsUnreadTotal = 0;
|
|
|
|
|
int _cachedChannelsUnreadTotal = 0;
|
|
|
|
|
final Map<String, _RepeaterAckContext> _pendingRepeaterAcks = {};
|
|
|
|
|
String? _activeContactKey;
|
|
|
|
|
int? _activeChannelIndex;
|
|
|
|
|
@ -606,16 +608,42 @@ class MeshCoreConnector extends ChangeNotifier {
|
|
|
|
|
|
|
|
|
|
int getTotalUnreadCount() {
|
|
|
|
|
if (!_unreadStateLoaded) return 0;
|
|
|
|
|
var total = 0;
|
|
|
|
|
// Count unread contact messages
|
|
|
|
|
for (final contact in _contacts) {
|
|
|
|
|
total += getUnreadCountForContact(contact);
|
|
|
|
|
}
|
|
|
|
|
// Count unread channel messages
|
|
|
|
|
for (final channelIndex in _channelMessages.keys) {
|
|
|
|
|
total += getUnreadCountForChannelIndex(channelIndex);
|
|
|
|
|
}
|
|
|
|
|
return total;
|
|
|
|
|
return getTotalContactsUnreadCount() + getTotalChannelsUnreadCount();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int getTotalContactsUnreadCount() {
|
|
|
|
|
if (!_unreadStateLoaded) return 0;
|
|
|
|
|
return _cachedContactsUnreadTotal;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int getTotalChannelsUnreadCount() {
|
|
|
|
|
if (!_unreadStateLoaded) return 0;
|
|
|
|
|
return _cachedChannelsUnreadTotal;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Recalculates both cached unread totals from scratch.
|
|
|
|
|
/// Called when unread state is first loaded.
|
|
|
|
|
void _recalculateCachedUnreadTotals() {
|
|
|
|
|
_recalculateCachedContactsUnreadTotal();
|
|
|
|
|
_recalculateCachedChannelsUnreadTotal();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void _recalculateCachedContactsUnreadTotal() {
|
|
|
|
|
int total = 0;
|
|
|
|
|
_contactUnreadCount.forEach((contactKeyHex, count) {
|
|
|
|
|
if (_shouldTrackUnreadForContactKey(contactKeyHex)) {
|
|
|
|
|
total += count;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
_cachedContactsUnreadTotal = total;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void _recalculateCachedChannelsUnreadTotal() {
|
|
|
|
|
final allChannels = _channels.isNotEmpty ? _channels : _cachedChannels;
|
|
|
|
|
_cachedChannelsUnreadTotal = allChannels.fold(
|
|
|
|
|
0,
|
|
|
|
|
(total, ch) => total + ch.unreadCount,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool isChannelSmazEnabled(int channelIndex) {
|
|
|
|
|
@ -649,11 +677,13 @@ class MeshCoreConnector extends ChangeNotifier {
|
|
|
|
|
..clear()
|
|
|
|
|
..addAll(await _unreadStore.loadContactUnreadCount());
|
|
|
|
|
_unreadStateLoaded = true;
|
|
|
|
|
_recalculateCachedUnreadTotals();
|
|
|
|
|
notifyListeners();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<void> loadCachedChannels() async {
|
|
|
|
|
_cachedChannels = await _channelStore.loadChannels();
|
|
|
|
|
_recalculateCachedChannelsUnreadTotal();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void setActiveContact(String? contactKeyHex) {
|
|
|
|
|
@ -680,6 +710,8 @@ class MeshCoreConnector extends ChangeNotifier {
|
|
|
|
|
final previousCount = _contactUnreadCount[contactKeyHex] ?? 0;
|
|
|
|
|
if (previousCount > 0) {
|
|
|
|
|
_contactUnreadCount[contactKeyHex] = 0;
|
|
|
|
|
_cachedContactsUnreadTotal = (_cachedContactsUnreadTotal - previousCount)
|
|
|
|
|
.clamp(0, _cachedContactsUnreadTotal);
|
|
|
|
|
_appDebugLogService?.info(
|
|
|
|
|
'Contact $contactKeyHex marked as read (was $previousCount unread)',
|
|
|
|
|
tag: 'Unread',
|
|
|
|
|
@ -721,6 +753,8 @@ class MeshCoreConnector extends ChangeNotifier {
|
|
|
|
|
if (channel != null && channel.unreadCount > 0) {
|
|
|
|
|
final previousCount = channel.unreadCount;
|
|
|
|
|
channel.unreadCount = 0;
|
|
|
|
|
_cachedChannelsUnreadTotal = (_cachedChannelsUnreadTotal - previousCount)
|
|
|
|
|
.clamp(0, _cachedChannelsUnreadTotal);
|
|
|
|
|
_appDebugLogService?.info(
|
|
|
|
|
'Channel ${channel.name.isNotEmpty ? channel.name : channelIndex} marked as read (was $previousCount unread)',
|
|
|
|
|
tag: 'Unread',
|
|
|
|
|
@ -3156,6 +3190,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
|
|
|
|
unawaited(_persistContacts());
|
|
|
|
|
_conversations.remove(contact.publicKeyHex);
|
|
|
|
|
_loadedConversationKeys.remove(contact.publicKeyHex);
|
|
|
|
|
final removedCount = _contactUnreadCount[contact.publicKeyHex] ?? 0;
|
|
|
|
|
_cachedContactsUnreadTotal = (_cachedContactsUnreadTotal - removedCount)
|
|
|
|
|
.clamp(0, _cachedContactsUnreadTotal);
|
|
|
|
|
_contactUnreadCount.remove(contact.publicKeyHex);
|
|
|
|
|
_unreadStore.saveContactUnreadCount(
|
|
|
|
|
Map<String, int>.from(_contactUnreadCount),
|
|
|
|
|
@ -3549,6 +3586,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|
|
|
|
// Cache channels for offline use
|
|
|
|
|
_cachedChannels = List<Channel>.from(_channels);
|
|
|
|
|
unawaited(_channelStore.saveChannels(_channels));
|
|
|
|
|
_recalculateCachedChannelsUnreadTotal();
|
|
|
|
|
|
|
|
|
|
// Apply ordering and notify UI
|
|
|
|
|
_applyChannelOrder();
|
|
|
|
|
@ -4101,6 +4139,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
|
|
|
|
_handleDiscovery(contact, frame, noNotify: true, addActive: true);
|
|
|
|
|
|
|
|
|
|
if (contact.type == advTypeRepeater) {
|
|
|
|
|
final removedCount = _contactUnreadCount[contact.publicKeyHex] ?? 0;
|
|
|
|
|
_cachedContactsUnreadTotal = (_cachedContactsUnreadTotal - removedCount)
|
|
|
|
|
.clamp(0, _cachedContactsUnreadTotal);
|
|
|
|
|
_contactUnreadCount.remove(contact.publicKeyHex);
|
|
|
|
|
_unreadStore.saveContactUnreadCount(
|
|
|
|
|
Map<String, int>.from(_contactUnreadCount),
|
|
|
|
|
@ -4191,6 +4232,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (contact.type == advTypeRepeater) {
|
|
|
|
|
final removedCount = _contactUnreadCount[contact.publicKeyHex] ?? 0;
|
|
|
|
|
_cachedContactsUnreadTotal = (_cachedContactsUnreadTotal - removedCount)
|
|
|
|
|
.clamp(0, _cachedContactsUnreadTotal);
|
|
|
|
|
_contactUnreadCount.remove(contact.publicKeyHex);
|
|
|
|
|
_unreadStore.saveContactUnreadCount(
|
|
|
|
|
Map<String, int>.from(_contactUnreadCount),
|
|
|
|
|
@ -4464,17 +4508,13 @@ class MeshCoreConnector extends ChangeNotifier {
|
|
|
|
|
badgeCount: getTotalUnreadCount(),
|
|
|
|
|
);
|
|
|
|
|
} else if (c?.type == advTypeRoom) {
|
|
|
|
|
// Room server messages include a 4-char prefix; strip it for notifications
|
|
|
|
|
final bodyText = msg.text.length > 4
|
|
|
|
|
? msg.text.substring(4)
|
|
|
|
|
: msg.text;
|
|
|
|
|
final resolvedText =
|
|
|
|
|
(translationResult != null &&
|
|
|
|
|
translationResult.status ==
|
|
|
|
|
MessageTranslationStatus.completed &&
|
|
|
|
|
translationResult.translatedText.trim().isNotEmpty)
|
|
|
|
|
? translationResult.translatedText.trim()
|
|
|
|
|
: bodyText.trim();
|
|
|
|
|
: msg.text.trim();
|
|
|
|
|
await _notificationService.showMessageNotification(
|
|
|
|
|
contactName: c?.name ?? 'Unknown Room',
|
|
|
|
|
message: resolvedText,
|
|
|
|
|
@ -4522,16 +4562,24 @@ class MeshCoreConnector extends ChangeNotifier {
|
|
|
|
|
timestampRaw * 1000,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (txtType == 2) {
|
|
|
|
|
reader.skipBytes(4); // Skip extra 4 bytes for signed/plain variants
|
|
|
|
|
final flags = txtType;
|
|
|
|
|
final shiftedType = flags >> 2;
|
|
|
|
|
final rawType = flags;
|
|
|
|
|
final isSigned = shiftedType == txtTypeSigned || rawType == txtTypeSigned;
|
|
|
|
|
final Uint8List? roomAuthorPrefix;
|
|
|
|
|
if (isSigned) {
|
|
|
|
|
// Room-server pushed posts use signed/plain contact messages where this
|
|
|
|
|
// 4-byte "signature" field is actually the original author's pubkey
|
|
|
|
|
// prefix. Keep it as metadata; the text starts after these bytes.
|
|
|
|
|
roomAuthorPrefix = reader.readBytes(4);
|
|
|
|
|
} else {
|
|
|
|
|
roomAuthorPrefix = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
final msgText = reader.readCString();
|
|
|
|
|
|
|
|
|
|
final flags = txtType;
|
|
|
|
|
final shiftedType = flags >> 2;
|
|
|
|
|
final rawType = flags;
|
|
|
|
|
final isPlain = shiftedType == txtTypePlain || rawType == txtTypePlain;
|
|
|
|
|
final isPlain =
|
|
|
|
|
shiftedType == txtTypePlain || rawType == txtTypePlain || isSigned;
|
|
|
|
|
final isCli = shiftedType == txtTypeCliData || rawType == txtTypeCliData;
|
|
|
|
|
if (!isPlain && !isCli) {
|
|
|
|
|
appLogger.warn(
|
|
|
|
|
@ -4568,9 +4616,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|
|
|
|
status: MessageStatus.delivered,
|
|
|
|
|
pathLength: pathLength == 0xFF ? 0 : pathLength,
|
|
|
|
|
pathBytes: Uint8List(0),
|
|
|
|
|
fourByteRoomContactKey: msgText.length >= 4
|
|
|
|
|
? Uint8List.fromList(msgText.substring(0, 4).codeUnits)
|
|
|
|
|
: null,
|
|
|
|
|
fourByteRoomContactKey: roomAuthorPrefix,
|
|
|
|
|
);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
appLogger.warn('Error parsing contact direct message: $e');
|
|
|
|
|
@ -5247,6 +5293,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|
|
|
|
final channel = _findChannelByIndex(channelIndex);
|
|
|
|
|
if (channel != null) {
|
|
|
|
|
channel.unreadCount++;
|
|
|
|
|
_cachedChannelsUnreadTotal++;
|
|
|
|
|
_appDebugLogService?.info(
|
|
|
|
|
'Channel ${channel.name.isNotEmpty ? channel.name : channelIndex} unread count incremented to ${channel.unreadCount}',
|
|
|
|
|
tag: 'Unread',
|
|
|
|
|
@ -5291,6 +5338,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|
|
|
|
|
|
|
|
|
final currentCount = _contactUnreadCount[contactKey] ?? 0;
|
|
|
|
|
_contactUnreadCount[contactKey] = currentCount + 1;
|
|
|
|
|
_cachedContactsUnreadTotal++;
|
|
|
|
|
_appDebugLogService?.info(
|
|
|
|
|
'Contact $contactKey unread count incremented to ${currentCount + 1}',
|
|
|
|
|
tag: 'Unread',
|
|
|
|
|
|