remove voice code make optimizations. Fix channels race conditions. add reply function

This commit is contained in:
zach
2025-12-30 19:27:25 -07:00
parent 6ff950d426
commit baf92ef672
582 changed files with 814 additions and 179108 deletions
+13 -5
View File
@@ -1,15 +1,15 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:shared_preferences/shared_preferences.dart';
import '../models/channel_message.dart';
import '../helpers/smaz.dart';
import 'prefs_manager.dart';
class ChannelMessageStore {
static const String _keyPrefix = 'channel_messages_';
/// Save messages for a specific channel
Future<void> saveChannelMessages(int channelIndex, List<ChannelMessage> messages) async {
final prefs = await SharedPreferences.getInstance();
final prefs = PrefsManager.instance;
final key = '$_keyPrefix$channelIndex';
// Convert messages to JSON
@@ -21,7 +21,7 @@ class ChannelMessageStore {
/// Load messages for a specific channel
Future<List<ChannelMessage>> loadChannelMessages(int channelIndex) async {
final prefs = await SharedPreferences.getInstance();
final prefs = PrefsManager.instance;
final key = '$_keyPrefix$channelIndex';
final jsonString = prefs.getString(key);
@@ -38,14 +38,14 @@ class ChannelMessageStore {
/// Clear messages for a specific channel
Future<void> clearChannelMessages(int channelIndex) async {
final prefs = await SharedPreferences.getInstance();
final prefs = PrefsManager.instance;
final key = '$_keyPrefix$channelIndex';
await prefs.remove(key);
}
/// Clear all channel messages
Future<void> clearAllChannelMessages() async {
final prefs = await SharedPreferences.getInstance();
final prefs = PrefsManager.instance;
final keys = prefs.getKeys().where((k) => k.startsWith(_keyPrefix));
for (var key in keys) {
await prefs.remove(key);
@@ -67,6 +67,10 @@ class ChannelMessageStore {
'pathBytes': base64Encode(msg.pathBytes),
'pathVariants': msg.pathVariants.map(base64Encode).toList(),
'repeats': msg.repeats.map(_repeatToJson).toList(),
'messageId': msg.messageId,
'replyToMessageId': msg.replyToMessageId,
'replyToSenderName': msg.replyToSenderName,
'replyToText': msg.replyToText,
};
}
@@ -96,6 +100,10 @@ class ChannelMessageStore {
.toList() ??
const [],
channelIndex: json['channelIndex'] as int?,
messageId: json['messageId'] as String?,
replyToMessageId: json['replyToMessageId'] as String?,
replyToSenderName: json['replyToSenderName'] as String?,
replyToText: json['replyToText'] as String?,
);
}
+3 -3
View File
@@ -1,16 +1,16 @@
import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
import 'prefs_manager.dart';
class ChannelOrderStore {
static const String _key = 'channel_order';
Future<void> saveChannelOrder(List<int> order) async {
final prefs = await SharedPreferences.getInstance();
final prefs = PrefsManager.instance;
await prefs.setString(_key, jsonEncode(order));
}
Future<List<int>> loadChannelOrder() async {
final prefs = await SharedPreferences.getInstance();
final prefs = PrefsManager.instance;
final raw = prefs.getString(_key);
if (raw == null || raw.isEmpty) return [];
try {
+3 -3
View File
@@ -1,16 +1,16 @@
import 'package:shared_preferences/shared_preferences.dart';
import 'prefs_manager.dart';
class ChannelSettingsStore {
static const String _smazKeyPrefix = 'channel_smaz_';
Future<bool> loadSmazEnabled(int channelIndex) async {
final prefs = await SharedPreferences.getInstance();
final prefs = PrefsManager.instance;
final key = '$_smazKeyPrefix$channelIndex';
return prefs.getBool(key) ?? false;
}
Future<void> saveSmazEnabled(int channelIndex, bool enabled) async {
final prefs = await SharedPreferences.getInstance();
final prefs = PrefsManager.instance;
final key = '$_smazKeyPrefix$channelIndex';
await prefs.setBool(key, enabled);
}
+3 -3
View File
@@ -1,12 +1,12 @@
import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
import '../models/contact_group.dart';
import 'prefs_manager.dart';
class ContactGroupStore {
static const String _key = 'contact_groups';
Future<List<ContactGroup>> loadGroups() async {
final prefs = await SharedPreferences.getInstance();
final prefs = PrefsManager.instance;
final raw = prefs.getString(_key);
if (raw == null || raw.isEmpty) return [];
@@ -25,7 +25,7 @@ class ContactGroupStore {
}
Future<void> saveGroups(List<ContactGroup> groups) async {
final prefs = await SharedPreferences.getInstance();
final prefs = PrefsManager.instance;
final encoded = jsonEncode(groups.map((group) => group.toJson()).toList());
await prefs.setString(_key, encoded);
}
+3 -3
View File
@@ -1,16 +1,16 @@
import 'package:shared_preferences/shared_preferences.dart';
import 'prefs_manager.dart';
class ContactSettingsStore {
static const String _smazKeyPrefix = 'contact_smaz_';
Future<bool> loadSmazEnabled(String contactKeyHex) async {
final prefs = await SharedPreferences.getInstance();
final prefs = PrefsManager.instance;
final key = '$_smazKeyPrefix$contactKeyHex';
return prefs.getBool(key) ?? false;
}
Future<void> saveSmazEnabled(String contactKeyHex, bool enabled) async {
final prefs = await SharedPreferences.getInstance();
final prefs = PrefsManager.instance;
final key = '$_smazKeyPrefix$contactKeyHex';
await prefs.setBool(key, enabled);
}
+3 -4
View File
@@ -1,15 +1,14 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:shared_preferences/shared_preferences.dart';
import '../models/contact.dart';
import 'prefs_manager.dart';
class ContactStore {
static const String _key = 'contacts';
Future<List<Contact>> loadContacts() async {
final prefs = await SharedPreferences.getInstance();
final prefs = PrefsManager.instance;
final jsonStr = prefs.getString(_key);
if (jsonStr == null) return [];
@@ -22,7 +21,7 @@ class ContactStore {
}
Future<void> saveContacts(List<Contact> contacts) async {
final prefs = await SharedPreferences.getInstance();
final prefs = PrefsManager.instance;
final jsonList = contacts.map(_toJson).toList();
await prefs.setString(_key, jsonEncode(jsonList));
}
+4 -12
View File
@@ -1,21 +1,21 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:shared_preferences/shared_preferences.dart';
import '../models/message.dart';
import '../helpers/smaz.dart';
import 'prefs_manager.dart';
class MessageStore {
static const String _keyPrefix = 'messages_';
Future<void> saveMessages(String contactKeyHex, List<Message> messages) async {
final prefs = await SharedPreferences.getInstance();
final prefs = PrefsManager.instance;
final key = '$_keyPrefix$contactKeyHex';
final jsonList = messages.map(_messageToJson).toList();
await prefs.setString(key, jsonEncode(jsonList));
}
Future<List<Message>> loadMessages(String contactKeyHex) async {
final prefs = await SharedPreferences.getInstance();
final prefs = PrefsManager.instance;
final key = '$_keyPrefix$contactKeyHex';
final jsonString = prefs.getString(key);
if (jsonString == null) return [];
@@ -29,7 +29,7 @@ class MessageStore {
}
Future<void> clearMessages(String contactKeyHex) async {
final prefs = await SharedPreferences.getInstance();
final prefs = PrefsManager.instance;
final key = '$_keyPrefix$contactKeyHex';
await prefs.remove(key);
}
@@ -41,10 +41,6 @@ class MessageStore {
'timestamp': msg.timestamp.millisecondsSinceEpoch,
'isOutgoing': msg.isOutgoing,
'isCli': msg.isCli,
'isVoice': msg.isVoice,
'voicePath': msg.voicePath,
'voiceDurationMs': msg.voiceDurationMs,
'voiceCodec': msg.voiceCodec,
'status': msg.status.index,
'messageId': msg.messageId,
'retryCount': msg.retryCount,
@@ -69,10 +65,6 @@ class MessageStore {
timestamp: DateTime.fromMillisecondsSinceEpoch(json['timestamp'] as int),
isOutgoing: json['isOutgoing'] as bool,
isCli: isCli,
isVoice: json['isVoice'] as bool? ?? false,
voicePath: json['voicePath'] as String?,
voiceDurationMs: json['voiceDurationMs'] as int?,
voiceCodec: json['voiceCodec'] as String?,
status: MessageStatus.values[json['status'] as int],
messageId: json['messageId'] as String?,
retryCount: json['retryCount'] as int? ?? 0,
+33
View File
@@ -0,0 +1,33 @@
import 'package:shared_preferences/shared_preferences.dart';
/// Singleton wrapper for SharedPreferences to avoid redundant getInstance() calls.
///
/// BEFORE: Every storage operation called SharedPreferences.getInstance()
/// AFTER: Single getInstance() on app startup, reused throughout lifecycle
///
/// This eliminates 30+ redundant platform channel calls across the app.
class PrefsManager {
PrefsManager._();
static SharedPreferences? _instance;
/// Initialize the cached instance. Call this once during app startup in main().
static Future<void> initialize() async {
_instance ??= await SharedPreferences.getInstance();
}
/// Get the cached SharedPreferences instance.
/// Throws StateError if initialize() hasn't been called.
static SharedPreferences get instance {
if (_instance == null) {
throw StateError(
'PrefsManager not initialized. Call PrefsManager.initialize() in main() before use.');
}
return _instance!;
}
/// For testing: reset the instance
static void reset() {
_instance = null;
}
}
+75 -10
View File
@@ -1,13 +1,30 @@
import 'dart:async';
import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
import 'prefs_manager.dart';
/// Storage for unread message tracking with debounced writes to reduce I/O.
class UnreadStore {
static const String _contactLastReadKey = 'contact_last_read';
static const String _channelLastReadKey = 'channel_last_read';
// Debounce timers to batch rapid writes
Timer? _contactSaveTimer;
Timer? _channelSaveTimer;
static const Duration _saveDebounceDuration = Duration(milliseconds: 500);
// Pending write data
Map<String, int>? _pendingContactLastRead;
Map<int, int>? _pendingChannelLastRead;
/// Dispose timers when no longer needed
void dispose() {
_contactSaveTimer?.cancel();
_channelSaveTimer?.cancel();
}
Future<Map<String, int>> loadContactLastRead() async {
final prefs = await SharedPreferences.getInstance();
final prefs = PrefsManager.instance;
final jsonStr = prefs.getString(_contactLastReadKey);
if (jsonStr == null) return {};
@@ -19,14 +36,24 @@ class UnreadStore {
}
}
Future<void> saveContactLastRead(Map<String, int> lastReadMs) async {
final prefs = await SharedPreferences.getInstance();
final jsonStr = jsonEncode(lastReadMs);
await prefs.setString(_contactLastReadKey, jsonStr);
/// Save contact last read timestamps with debouncing.
/// Writes are delayed by 500ms and batched to reduce I/O operations.
void saveContactLastRead(Map<String, int> lastReadMs) {
_pendingContactLastRead = lastReadMs;
// Cancel existing timer
_contactSaveTimer?.cancel();
// Schedule new write
_contactSaveTimer = Timer(_saveDebounceDuration, () async {
if (_pendingContactLastRead != null) {
await _flushContactLastRead();
}
});
}
Future<Map<int, int>> loadChannelLastRead() async {
final prefs = await SharedPreferences.getInstance();
final prefs = PrefsManager.instance;
final jsonStr = prefs.getString(_channelLastReadKey);
if (jsonStr == null) return {};
@@ -38,10 +65,48 @@ class UnreadStore {
}
}
Future<void> saveChannelLastRead(Map<int, int> lastReadMs) async {
final prefs = await SharedPreferences.getInstance();
final asString = lastReadMs.map((key, value) => MapEntry(key.toString(), value));
/// Save channel last read timestamps with debouncing.
/// Writes are delayed by 500ms and batched to reduce I/O operations.
void saveChannelLastRead(Map<int, int> lastReadMs) {
_pendingChannelLastRead = lastReadMs;
_channelSaveTimer?.cancel();
_channelSaveTimer = Timer(_saveDebounceDuration, () async {
if (_pendingChannelLastRead != null) {
await _flushChannelLastRead();
}
});
}
Future<void> _flushContactLastRead() async {
if (_pendingContactLastRead == null) return;
final prefs = PrefsManager.instance;
final jsonStr = jsonEncode(_pendingContactLastRead);
await prefs.setString(_contactLastReadKey, jsonStr);
_pendingContactLastRead = null;
}
Future<void> _flushChannelLastRead() async {
if (_pendingChannelLastRead == null) return;
final prefs = PrefsManager.instance;
final asString =
_pendingChannelLastRead!.map((key, value) => MapEntry(key.toString(), value));
final jsonStr = jsonEncode(asString);
await prefs.setString(_channelLastReadKey, jsonStr);
_pendingChannelLastRead = null;
}
/// Immediately flush pending writes (call before app termination or disposal)
Future<void> flush() async {
_contactSaveTimer?.cancel();
_channelSaveTimer?.cancel();
await Future.wait([
_flushContactLastRead(),
_flushChannelLastRead(),
]);
}
}