Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions mobile/lib/controllers/chat/inbox_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,16 @@ class InboxController extends ChangeNotifier {
bool isLoading = false;
bool hasLoadedOnce = false;

/// Why the last load failed, shown instead of an empty inbox.
String? loadError;

List<InboxItem> get groups => inbox.where((item) => item.isGroup).toList();
int get unreadChats => inbox.where((item) => item.unreadCount > 0).length;

void clearInbox() {
inbox = [];
hasLoadedOnce = false;
loadError = null;
notifyListeners();
}

Expand All @@ -36,6 +40,7 @@ class InboxController extends ChangeNotifier {
}
} catch (e) {
debugPrint("Failed to load local inbox cache: $e");
loadError = "Couldn't open your saved conversations.";
}

if (isOffline) return;
Expand Down Expand Up @@ -88,6 +93,7 @@ class InboxController extends ChangeNotifier {

inbox = combined;
hasLoadedOnce = true;
loadError = null;
await _replaceInboxInDb(inbox);

for (final chat in chatsToCatchUp) {
Expand All @@ -97,6 +103,10 @@ class InboxController extends ChangeNotifier {
}
} catch (e) {
debugPrint("Inbox fetch failed: $e");
loadError = ApiService.errorMessage(
e,
fallback: "Couldn't load your conversations.",
);
} finally {
isLoading = false;
notifyListeners();
Expand Down
8 changes: 8 additions & 0 deletions mobile/lib/pages/home/home_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,14 @@ class _ChatsTabState extends State<_ChatsTab> {
title: 'No matches',
message: 'No chat matches "$_query".',
)
: inbox.loadError != null
? EmptyState(
icon: Icons.cloud_off_rounded,
title: 'Couldn\'t load chats',
message: inbox.loadError!,
actionLabel: 'Try again',
onAction: () => inbox.loadInbox(),
)
: EmptyState(
icon: Icons.forum_rounded,
title: 'No conversations yet',
Expand Down
31 changes: 27 additions & 4 deletions mobile/lib/services/db_services.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,24 @@ import '../core/message_envelope.dart';
class DatabaseHelper {
static final DatabaseHelper instance = DatabaseHelper._init();
static Database? _database;
static Future<Database>? _opening;
final FlutterSecureStorage _secureStorage = const FlutterSecureStorage();

static const String _dbName = 'secure_chat.db';

DatabaseHelper._init();

/// Opens the database once. Concurrent first callers share the same
/// future, so the encryption key is only ever generated once.
Future<Database> get database async {
if (_database != null) return _database!;
_database = await _initDB(_dbName);
return _database!;
final db = _database;
if (db != null) return db;
final opening = _opening ??= _initDB(_dbName);
try {
return _database = await opening;
} finally {
_opening = null;
}
}

Future<String> _getEncryptionKey() async {
Expand Down Expand Up @@ -63,7 +71,22 @@ class DatabaseHelper {
final path = join(dbPath, filePath);
final password = await _getEncryptionKey();

return await openDatabase(
try {
return await _open(path, password);
} on DatabaseException catch (e) {
if (!await databaseExists(path)) rethrow;
// The file was encrypted with a key we no longer have. It only holds a
// cache and this device's Signal keys, which are regenerated and
// republished on the next sign-in, so start over instead of failing on
// every launch.
debugPrint('Local database unreadable, recreating it: $e');
await deleteDatabase(path);
return await _open(path, password);
}
}

Future<Database> _open(String path, String password) {
return openDatabase(
path,
version: 2,
password: password,
Expand Down
Loading