я как бы еще не доделал, это такой промежуточный пуш знаете

This commit is contained in:
Jganenokk
2026-04-03 19:35:45 +07:00
parent 98c72e5d16
commit 91069f53ea
17 changed files with 987 additions and 417 deletions
+30 -20
View File
@@ -5,6 +5,14 @@ import '../../core/storage/app_database.dart';
import '../../core/storage/token_storage.dart';
import '../../core/utils/logger.dart';
import 'chats.dart';
import 'contacts.dart';
class ServerException implements Exception {
final String message;
const ServerException(this.message);
@override
String toString() => message;
}
enum AuthRequestType {
startAuth('START_AUTH'),
@@ -115,11 +123,13 @@ class LoginResult {
final ProfileData profile;
final String? updatedToken;
final int serverTime;
final Map<dynamic, dynamic> raw;
const LoginResult({
required this.profile,
required this.updatedToken,
required this.serverTime,
required this.raw,
});
}
@@ -131,14 +141,12 @@ class AccountModule {
Future<RequestCodeResult> requestCode(
String phone, {
String language = 'ru',
}) =>
_requestCodeInternal(phone, AuthRequestType.startAuth, language);
}) => _requestCodeInternal(phone, AuthRequestType.startAuth, language);
Future<RequestCodeResult> resendCode(
String phone, {
String language = 'ru',
}) =>
_requestCodeInternal(phone, AuthRequestType.resend, language);
}) => _requestCodeInternal(phone, AuthRequestType.resend, language);
Future<VerifyCodeResult> verifyCode(String code, String token) async {
_ensureOnline();
@@ -157,7 +165,9 @@ class AccountModule {
final data = packet.payload;
if (data is! Map) {
throw Exception('verifyCode: неожиданный тип payload: ${data.runtimeType}');
throw Exception(
'verifyCode: неожиданный тип payload: ${data.runtimeType}',
);
}
final result = VerifyCodeResult(payload: data.cast<dynamic, dynamic>());
@@ -181,7 +191,8 @@ class AccountModule {
}) async {
_ensureOnline();
final resolvedAccountId = accountId ?? await TokenStorage.getActiveAccountId();
final resolvedAccountId =
accountId ?? await TokenStorage.getActiveAccountId();
if (resolvedAccountId == null) {
throw StateError('login: нет активного аккаунта');
}
@@ -191,13 +202,7 @@ class AccountModule {
throw StateError('login: нет токена для аккаунта $resolvedAccountId');
}
final resolvedSyncParams =
syncParams ?? await LoginSyncParams.fromDatabase(resolvedAccountId);
final requestPayload = _buildLoginPayload(authToken, resolvedSyncParams);
logger.i('LOGIN opcode=${Opcode.login} '
'account=$resolvedAccountId warm=${resolvedSyncParams != null}');
final requestPayload = _buildLoginPayload(authToken, syncParams);
final packet = await _api.sendRequest(Opcode.login, requestPayload);
@@ -262,7 +267,9 @@ class AccountModule {
final data = packet.payload;
if (data is! Map) {
throw Exception('checkPassword: неожиданный тип payload: ${data.runtimeType}');
throw Exception(
'checkPassword: неожиданный тип payload: ${data.runtimeType}',
);
}
if (data['error'] != null) {
@@ -310,7 +317,8 @@ class AccountModule {
) {
final payload = <dynamic, dynamic>{
'token': token,
'interactive': true
'interactive': true,
if (_api.userAgent != null) 'userAgent': _api.userAgent,
};
if (sync != null) {
@@ -342,7 +350,6 @@ class AccountModule {
final updatedToken = data['token'] as String?;
if (updatedToken != null) {
await TokenStorage.saveToken(updatedToken, accountId);
logger.i('Обновлённый токен аккаунта $accountId сохранён');
}
final profileMap = data['profile'];
@@ -356,15 +363,16 @@ class AccountModule {
final profile = ProfileData.fromServerMap(contact.cast<dynamic, dynamic>());
await AppDatabase.saveProfile(profile);
await AppDatabase.setActiveAccount(profile.id);
logger.i('Профиль сохранён: id=${profile.id}, name=${profile.firstName}');
await _saveSyncState(data, serverTime, profile.id);
await ContactsModule.syncFromLoginPayload(data, profile.id);
await ChatsModule.syncFromLoginPayload(data, profile.id, profile.id);
return LoginResult(
profile: profile,
updatedToken: updatedToken,
serverTime: serverTime,
raw: data,
);
}
@@ -415,7 +423,9 @@ class AccountModule {
final data = packet.payload;
if (data is! Map) {
throw Exception('requestCode: неожиданный тип payload: ${data.runtimeType}');
throw Exception(
'requestCode: неожиданный тип payload: ${data.runtimeType}',
);
}
final token = data['token'];
@@ -439,8 +449,8 @@ class AccountModule {
if (packet.isError) {
final errMsg = packet.payload is Map
? (packet.payload as Map)['message'] ?? packet.payload.toString()
: packet.payload?.toString() ?? 'unknown error';
throw Exception('$method: ошибка от сервера — $errMsg');
: packet.payload?.toString() ?? 'Неизвестная ошибка';
throw ServerException(errMsg.toString());
}
}
}
+96 -42
View File
@@ -13,6 +13,10 @@ class CachedChat {
final int unreadCount;
final int lastEventTime;
final int cachedAt;
final int? favIndex;
final int dontDisturbUntil;
final bool isOnline;
final int seenTime;
const CachedChat({
required this.id,
@@ -27,37 +31,49 @@ class CachedChat {
required this.unreadCount,
required this.lastEventTime,
required this.cachedAt,
this.favIndex,
required this.dontDisturbUntil,
required this.isOnline,
required this.seenTime,
});
factory CachedChat.fromDbRow(Map<String, dynamic> row) => CachedChat(
id: row['id'] as int,
accountId: row['account_id'] as int,
type: row['type'] as String,
title: row['title'] as String?,
iconUrl: row['icon_url'] as String?,
lastMsgId: row['last_msg_id'] as int?,
lastMsgTime: row['last_msg_time'] as int?,
lastMsgText: row['last_msg_text'] as String?,
lastMsgSenderId: row['last_msg_sender'] as int?,
unreadCount: row['unread_count'] as int,
lastEventTime: row['last_event_time'] as int,
cachedAt: row['cached_at'] as int,
);
id: row['id'] as int,
accountId: row['account_id'] as int,
type: row['type'] as String,
title: row['title'] as String?,
iconUrl: row['icon_url'] as String?,
lastMsgId: row['last_msg_id'] as int?,
lastMsgTime: row['last_msg_time'] as int?,
lastMsgText: row['last_msg_text'] as String?,
lastMsgSenderId: row['last_msg_sender'] as int?,
unreadCount: row['unread_count'] as int,
lastEventTime: row['last_event_time'] as int,
cachedAt: row['cached_at'] as int,
favIndex: row['fav_index'] as int?,
dontDisturbUntil: row['dont_disturb_until'] as int,
isOnline: (row['is_online'] as int) == 1,
seenTime: row['seen_time'] as int,
);
Map<String, dynamic> toDbRow() => {
'id': id,
'account_id': accountId,
'type': type,
'title': title,
'icon_url': iconUrl,
'last_msg_id': lastMsgId,
'last_msg_time': lastMsgTime,
'last_msg_text': lastMsgText,
'last_msg_sender': lastMsgSenderId,
'unread_count': unreadCount,
'last_event_time': lastEventTime,
'cached_at': cachedAt,
};
'id': id,
'account_id': accountId,
'type': type,
'title': title,
'icon_url': iconUrl,
'last_msg_id': lastMsgId,
'last_msg_time': lastMsgTime,
'last_msg_text': lastMsgText,
'last_msg_sender': lastMsgSenderId,
'unread_count': unreadCount,
'last_event_time': lastEventTime,
'cached_at': cachedAt,
'fav_index': favIndex,
'dont_disturb_until': dontDisturbUntil,
'is_online': isOnline ? 1 : 0,
'seen_time': seenTime,
};
}
class ChatsModule {
@@ -75,23 +91,35 @@ class ChatsModule {
if (chats is! List || chats.isEmpty) return;
final contactsMap = _buildContactsMap(data['contacts']);
// Config contains mute setup and fav indexes: config -> chats -> id
final configMap = data['config'] is Map ? data['config'] as Map : {};
final chatsConfig = configMap['chats'] is Map
? configMap['chats'] as Map
: {};
// Presence for online statuses
final presenceMap = data['presence'] is Map ? data['presence'] as Map : {};
final cachedAt = DateTime.now().millisecondsSinceEpoch;
final existingRows = await AppDatabase.loadChats(accountId);
final existing = {
for (final row in existingRows) row['id'] as int: CachedChat.fromDbRow(row),
for (final row in existingRows)
row['id'] as int: CachedChat.fromDbRow(row),
};
final rows = chats
.whereType<Map>()
.map((c) => _parseChat(
c.cast<dynamic, dynamic>(),
accountId,
currentUserId,
contactsMap,
existing,
cachedAt,
))
.map(
(c) => _parseChat(
c.cast<dynamic, dynamic>(),
accountId,
currentUserId,
contactsMap,
chatsConfig,
presenceMap,
existing,
cachedAt,
),
)
.whereType<CachedChat>()
.map((c) => c.toDbRow())
.toList();
@@ -109,7 +137,7 @@ class ChatsModule {
static Future<void> clearCache(int accountId) =>
AppDatabase.clearChatsCache(accountId);
// internal
// internal
static Map<int, Map<dynamic, dynamic>> _buildContactsMap(dynamic contacts) {
if (contacts is! List) return {};
@@ -126,6 +154,8 @@ class ChatsModule {
int accountId,
int currentUserId,
Map<int, Map<dynamic, dynamic>> contactsMap,
Map<dynamic, dynamic> chatsConfig,
Map<dynamic, dynamic> presenceMap,
Map<int, CachedChat> existing,
int cachedAt,
) {
@@ -133,19 +163,19 @@ class ChatsModule {
if (id is! int) return null;
final type = (chat['type'] as String?) ?? 'DIALOG';
int? otherId;
String? title;
String? iconUrl;
if (type == 'DIALOG') {
final otherId = _otherParticipantId(chat['participants'], currentUserId);
otherId = _otherParticipantId(chat['participants'], currentUserId);
final contact = otherId != null ? contactsMap[otherId] : null;
if (contact != null) {
title = _nameFromContact(contact);
iconUrl = contact['baseUrl'] as String?;
} else {
// Warm start: контакты не пришли — берём из кэша
title = existing[id]?.title;
iconUrl = existing[id]?.iconUrl;
}
@@ -167,6 +197,24 @@ class ChatsModule {
lastMsgSenderId = lastMsg['sender'] as int?;
}
final config = chatsConfig[id.toString()] ?? chatsConfig[id];
int? favIndex;
int dontDisturbUntil = 0;
if (config is Map) {
favIndex = config['favIndex'] as int?;
dontDisturbUntil = (config['dontDisturbUntil'] as int?) ?? 0;
}
int seenTime = 0;
bool isOnline = false;
if (type == 'DIALOG' && otherId != null) {
final presence = presenceMap[otherId.toString()] ?? presenceMap[otherId];
if (presence is Map) {
seenTime = (presence['seen'] as int?) ?? 0;
isOnline = (presence['status'] as int?) == 1;
}
}
return CachedChat(
id: id,
accountId: accountId,
@@ -180,6 +228,10 @@ class ChatsModule {
unreadCount: (chat['newMessages'] as int?) ?? 0,
lastEventTime: (chat['lastEventTime'] as int?) ?? 0,
cachedAt: cachedAt,
favIndex: favIndex,
dontDisturbUntil: dontDisturbUntil,
isOnline: isOnline,
seenTime: seenTime,
);
}
@@ -195,10 +247,12 @@ class ChatsModule {
static String? _nameFromContact(Map<dynamic, dynamic> contact) {
final names = contact['names'];
if (names is! List || names.isEmpty) return null;
final name = names.firstWhere(
(n) => n is Map && n['type'] == 'ONEME',
orElse: () => names.first,
) as Map;
final name =
names.firstWhere(
(n) => n is Map && n['type'] == 'ONEME',
orElse: () => names.first,
)
as Map;
return name['name'] as String?;
}
}
+56
View File
@@ -0,0 +1,56 @@
import '../../core/storage/app_database.dart';
class ContactsModule {
static Future<void> syncFromLoginPayload(
Map<dynamic, dynamic> data,
int accountId,
) async {
final contacts = data['contacts'];
if (contacts is! List || contacts.isEmpty) return;
final rows = contacts
.whereType<Map>()
.map((c) => _parseContact(c.cast<dynamic, dynamic>(), accountId))
.whereType<Map<String, dynamic>>()
.toList();
if (rows.isNotEmpty) {
await AppDatabase.saveContacts(rows);
}
}
static Map<String, dynamic>? _parseContact(
Map<dynamic, dynamic> 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 name =
names.firstWhere(
(n) => n is Map && n['type'] == 'ONEME',
orElse: () => names.first,
)
as Map;
firstName = (name['firstName'] as String?) ?? '';
lastName = name['lastName'] as String?;
}
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,
};
}
}