feat(chat): usable 1:1 chat — bottom-anchored, Drift-backed, dated
- Anchor the message list to the bottom (`reverse: true`) so new messages stay in view instead of landing below the fold. - Move chat history from the OS keystore (O(n²) JSON blob, silent 200-msg cap) to a separate encrypted Drift/SQLCipher DB (`ChatDatabase`): indexed append, uncapped history, dedup as a unique-key invariant. It's an ephemeral per-device cache, isolated from the inventory schema, its migrations, and its sync. No data migration (pre-release). - Add day separators (Today/Yesterday/locale date) and a per-bubble time, all via ICU (12/24h per locale; Localizations locale maps Asturian → Spanish for intl date symbols). - Add peer avatars (deterministic colour from the pubkey + name initial), surface send failures that were previously silent, and make bubble text selectable (addresses, links). - New i18n keys in en/es/pt/ast; tests for grouping, formatting, avatars, scroll anchoring, storage and send errors. Docs: docs/design/chat-storage.md + open-decisions.md.
This commit is contained in:
parent
171daabce3
commit
6cff0d0b11
27 changed files with 1793 additions and 264 deletions
|
|
@ -12,6 +12,7 @@ import 'package:path_provider/path_provider.dart';
|
|||
import '../data/species_catalog.dart';
|
||||
import '../data/species_repository.dart';
|
||||
import '../data/variety_repository.dart';
|
||||
import '../db/chat_database.dart';
|
||||
import '../db/database.dart';
|
||||
import '../db/encrypted_executor.dart';
|
||||
import '../i18n/strings.g.dart';
|
||||
|
|
@ -98,6 +99,12 @@ Future<void> configureDependencies() async {
|
|||
openEncryptedExecutor(await _databaseFile(), dbKeyHex),
|
||||
);
|
||||
|
||||
// Chat history lives in its own encrypted database (same SQLCipher key),
|
||||
// isolated from the inventory schema/sync. See [Messages].
|
||||
final chatDatabase = ChatDatabase(
|
||||
openEncryptedExecutor(await _chatDatabaseFile(), dbKeyHex),
|
||||
);
|
||||
|
||||
// CRDT author id: a stable per-install slice of the root seed. Kept distinct
|
||||
// from the social-layer public key on purpose — unifying the two would rewrite
|
||||
// authorship of existing rows, a data-model decision for later.
|
||||
|
|
@ -116,7 +123,9 @@ Future<void> configureDependencies() async {
|
|||
deviceId: deviceId,
|
||||
);
|
||||
} catch (e, s) {
|
||||
debugPrint('Social identity derivation failed; inventory-only mode: $e\n$s');
|
||||
debugPrint(
|
||||
'Social identity derivation failed; inventory-only mode: $e\n$s',
|
||||
);
|
||||
}
|
||||
|
||||
// Seed the bundled species catalog before the UI opens — but only once per
|
||||
|
|
@ -152,6 +161,7 @@ Future<void> configureDependencies() async {
|
|||
getIt
|
||||
..registerSingleton<SecureKeyStore>(keyStore)
|
||||
..registerSingleton<AppDatabase>(database)
|
||||
..registerSingleton<ChatDatabase>(chatDatabase)
|
||||
..registerSingleton<SpeciesRepository>(speciesRepository)
|
||||
..registerSingleton<VarietyRepository>(varietyRepository)
|
||||
..registerSingleton<FileService>(fileService)
|
||||
|
|
@ -166,11 +176,14 @@ Future<void> configureDependencies() async {
|
|||
..registerSingleton<OfferOutbox>(OfferOutbox(secretStore))
|
||||
// Per-identity stores are namespaced by the active account's scope.
|
||||
..registerSingleton<MessageStore>(
|
||||
MessageStore(secretStore, accountScope: scope))
|
||||
MessageStore(chatDatabase, accountScope: scope),
|
||||
)
|
||||
..registerSingleton<ProfileStore>(
|
||||
ProfileStore(secretStore, accountScope: scope))
|
||||
ProfileStore(secretStore, accountScope: scope),
|
||||
)
|
||||
..registerSingleton<ProfileCache>(
|
||||
ProfileCache(secretStore, accountScope: scope))
|
||||
ProfileCache(secretStore, accountScope: scope),
|
||||
)
|
||||
..registerSingleton<ExportImportService>(
|
||||
ExportImportService(
|
||||
repository: varietyRepository,
|
||||
|
|
@ -292,21 +305,27 @@ Future<void> switchSocialAccount(int account) async {
|
|||
// Re-register the per-identity stores under the new scope, then the identity.
|
||||
getIt
|
||||
..registerSingleton<MessageStore>(
|
||||
MessageStore(secretStore, accountScope: scope))
|
||||
MessageStore(getIt<ChatDatabase>(), accountScope: scope),
|
||||
)
|
||||
..registerSingleton<ProfileStore>(
|
||||
ProfileStore(secretStore, accountScope: scope))
|
||||
ProfileStore(secretStore, accountScope: scope),
|
||||
)
|
||||
..registerSingleton<ProfileCache>(
|
||||
ProfileCache(secretStore, accountScope: scope))
|
||||
ProfileCache(secretStore, accountScope: scope),
|
||||
)
|
||||
..registerSingleton<UnreadService>(
|
||||
UnreadService(getIt<MessageStore>(), secretStore));
|
||||
UnreadService(getIt<MessageStore>(), secretStore),
|
||||
);
|
||||
|
||||
final social = await SocialService.fromRootSeedHex(
|
||||
rootSeedHex,
|
||||
account: account,
|
||||
deviceId: deviceId,
|
||||
);
|
||||
final connection =
|
||||
SocialConnection(social: social, settings: getIt<SocialSettings>());
|
||||
final connection = SocialConnection(
|
||||
social: social,
|
||||
settings: getIt<SocialSettings>(),
|
||||
);
|
||||
final inbox = InboxService(
|
||||
connection: connection,
|
||||
selfPubkey: social.publicKeyHex,
|
||||
|
|
@ -342,6 +361,11 @@ Future<File> _databaseFile() async {
|
|||
return File(p.join(dir.path, 'tane_inventory.sqlite'));
|
||||
}
|
||||
|
||||
Future<File> _chatDatabaseFile() async {
|
||||
final dir = await getApplicationDocumentsDirectory();
|
||||
return File(p.join(dir.path, 'tane_chat.sqlite'));
|
||||
}
|
||||
|
||||
/// Picks the Tesseract language pack(s) for the current locale(s), limited to
|
||||
/// the bundled packs listed in `tessdata_config.json`. Prefers the app's chosen
|
||||
/// language, then the device's ordered locales.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue