import 'package:flutter/foundation.dart'; import '../../core/protocol/opcode_map.dart'; import '../../core/storage/app_database.dart'; import '../api.dart'; import 'messages.dart'; class CachedContact { final int id; final int accountId; final String firstName; final String? lastName; final int phone; final int? photoId; final String? baseUrl; final String? baseRawUrl; final int updateTime; final Set options; const CachedContact({ required this.id, required this.accountId, required this.firstName, this.lastName, required this.phone, this.photoId, this.baseUrl, this.baseRawUrl, required this.updateTime, this.options = const {}, }); bool get isOfficial => options.contains('OFFICIAL'); bool get isBot => options.contains('BOT'); bool get isServiceAccount => options.contains('SERVICE_ACCOUNT'); bool get isVerified => isOfficial; factory CachedContact.fromDbRow(Map row) => CachedContact( id: row['id'] as int, accountId: row['account_id'] as int, firstName: row['first_name'] as String, lastName: row['last_name'] as String?, phone: row['phone'] as int, photoId: row['photo_id'] as int?, baseUrl: row['base_url'] as String?, baseRawUrl: row['base_raw_url'] as String?, updateTime: row['update_time'] as int, options: _decodeOptions(row['options']), ); static Set _decodeOptions(dynamic raw) { if (raw is! String || raw.isEmpty) return const {}; return raw.split(',').where((s) => s.isNotEmpty).toSet(); } } class ContactsModule { static final ValueNotifier revision = ValueNotifier(0); static Future addContact( Api api, int id, String firstName, { int phone = 0, }) async { final resp = await api.sendRequest(Opcode.contactUpdate, { 'action': 'ADD', 'contactId': id, 'firstName': firstName, }); final profile = await AppDatabase.loadActiveProfile(); if (profile == null) return null; final data = resp.payload; final contact = (data is Map && data['contact'] is Map) ? (data['contact'] as Map).cast() : null; final row = contact != null ? _parseContact(contact, profile.id) : { 'id': id, 'account_id': profile.id, 'first_name': firstName, 'last_name': null, 'phone': 0, 'photo_id': null, 'base_url': null, 'base_raw_url': null, 'update_time': 0, 'options': null, }; if (row == null) return null; if (phone > 0 && ((row['phone'] as int?) ?? 0) == 0) { row['phone'] = phone; } await AppDatabase.saveContacts([row]); if (contact != null) _primeContactCache(contact); revision.value++; return CachedContact.fromDbRow(row); } static Future syncFromLoginPayload( Map data, int accountId, ) async { final contacts = data['contacts']; if (contacts is! List || contacts.isEmpty) return; final rows = >[]; for (final raw in contacts.whereType()) { final contact = raw.cast(); final row = _parseContact(contact, accountId); if (row != null) rows.add(row); _primeContactCache(contact); } if (rows.isNotEmpty) { await AppDatabase.saveContacts(rows); } } static void _primeContactCache(Map contact) { final id = contact['id']; if (id is! int) return; final names = contact['names']; if (names is List && names.isNotEmpty) { final nameRaw = names.firstWhere( (n) => n is Map && n['type'] == 'ONEME', orElse: () => names.firstWhere((n) => n is Map, orElse: () => null), ); if (nameRaw is Map) { final firstName = (nameRaw['firstName'] as String?) ?? ''; final lastName = nameRaw['lastName'] as String?; final fullName = (lastName != null && lastName.isNotEmpty) ? '$firstName $lastName' : firstName; if (fullName.isNotEmpty) ContactCache.put(id, fullName); } } final baseUrl = contact['baseUrl'] as String?; if (baseUrl != null && baseUrl.isNotEmpty) { ContactCache.putAvatar(id, baseUrl); } } static Future> getContacts(int accountId) async { final rows = await AppDatabase.loadContacts(accountId); return rows.map(CachedContact.fromDbRow).toList(); } /// Прогревает in-memory ContactCache из локальных контактов. /// Нужно вызывать на cold start: иначе кэш пуст до следующего логина. static Future primeCacheFromDb(int accountId) async { final contacts = await getContacts(accountId); for (final c in contacts) { final fullName = (c.lastName != null && c.lastName!.isNotEmpty) ? '${c.firstName} ${c.lastName}' : c.firstName; if (fullName.isNotEmpty) ContactCache.put(c.id, fullName); if (c.baseUrl != null && c.baseUrl!.isNotEmpty) { ContactCache.putAvatar(c.id, c.baseUrl); } } } static Map? _parseContact( Map contact, int accountId, ) { final id = contact['id']; if (id is! int) return null; String firstName = ''; String? lastName; final names = contact['names']; if (names is List && names.isNotEmpty) { final nameRaw = names.firstWhere( (n) => n is Map && n['type'] == 'ONEME', orElse: () => names.firstWhere((n) => n is Map, orElse: () => null), ); if (nameRaw is! Map) return null; final name = nameRaw; firstName = (name['firstName'] as String?) ?? ''; lastName = name['lastName'] as String?; } final optionsRaw = contact['options']; String? optionsStr; if (optionsRaw is List) { optionsStr = optionsRaw.whereType().join(','); } return { 'id': id, 'account_id': accountId, 'first_name': firstName, 'last_name': lastName, 'phone': (contact['phone'] as int?) ?? 0, 'photo_id': contact['photoId'] as int?, 'base_url': contact['baseUrl'] as String?, 'base_raw_url': contact['baseRawUrl'] as String?, 'update_time': (contact['updateTime'] as int?) ?? 0, 'options': optionsStr, }; } }