feat(search): экран поиска — чаты, контакты, сообщения и поиск по номеру

This commit is contained in:
klockky
2026-06-21 12:48:08 +00:00
parent ad04225a62
commit 23a1bed46a
5 changed files with 567 additions and 32 deletions
+82
View File
@@ -161,6 +161,22 @@ class CachedChat {
};
}
class ChatSearchHit {
final int id;
final String type;
final String? title;
final String? avatarUrl;
final String? subtitle;
const ChatSearchHit({
required this.id,
required this.type,
this.title,
this.avatarUrl,
this.subtitle,
});
}
sealed class MessageEvent {
final int chatId;
const MessageEvent(this.chatId);
@@ -1151,6 +1167,72 @@ class ChatsModule {
return packet.payload;
}
static List<ChatSearchHit> _parseSearchResult(dynamic payload) {
final result = (payload as Map?)?['result'];
if (result is! List) return const [];
final hits = <ChatSearchHit>[];
for (final item in result) {
if (item is! Map) continue;
final chat = item['chat'];
if (chat is! Map) continue;
final id = chat['id'];
if (id is! int) continue;
final last = chat['lastMessage'];
final link = chat['link'];
hits.add(ChatSearchHit(
id: id,
type: (chat['type'] as String?) ?? 'CHAT',
title: chat['title'] as String?,
avatarUrl: chat['baseIconUrl'] as String?,
subtitle: link is String && link.isNotEmpty
? '@$link'
: (last is Map ? last['text'] as String? : null),
));
}
return hits;
}
static Future<List<ChatSearchHit>> searchChats(
Api api,
String query, {
int count = 50,
}) async {
final term = query.trim();
if (term.isEmpty) return const [];
try {
final packet = await api.sendRequest(Opcode.chatSearch, {
'count': count,
'query': term,
});
if (packet.isError) return const [];
return _parseSearchResult(packet.payload);
} catch (e) {
logger.w('searchChats failed: $e');
return const [];
}
}
static Future<List<ChatSearchHit>> searchPublic(
Api api,
String query, {
int count = 20,
}) async {
final term = query.trim();
if (term.isEmpty) return const [];
try {
final packet = await api.sendRequest(Opcode.publicSearch, {
'type': 'ALL',
'count': count,
'query': term,
});
if (packet.isError) return const [];
return _parseSearchResult(packet.payload);
} catch (e) {
logger.w('searchPublic failed: $e');
return const [];
}
}
static Future<CachedChat?> createGroupChat(
Api api, {
required String title,
+46
View File
@@ -54,9 +54,55 @@ class CachedContact {
}
}
class PhoneLookupResult {
final int id;
final String? name;
final String? avatarUrl;
const PhoneLookupResult({required this.id, this.name, this.avatarUrl});
}
class ContactsModule {
static final ValueNotifier<int> revision = ValueNotifier<int>(0);
static Future<PhoneLookupResult?> findByPhone(Api api, String phone) async {
final normalized = _normalizePhone(phone);
if (normalized == null) return null;
final packet = await api.sendRequest(Opcode.contactInfoByPhone, {
'phone': normalized,
});
if (packet.isError) return null;
final contact = (packet.payload as Map?)?['contact'];
if (contact is! Map) return null;
final id = contact['id'];
if (id is! int) return null;
String? name;
final names = contact['names'];
if (names is List) {
final n = names.firstWhere((e) => e is Map, orElse: () => null);
if (n is Map) {
final first =
(n['firstName'] as String?) ?? (n['name'] as String?) ?? '';
final last = (n['lastName'] as String?) ?? '';
final full = '$first $last'.trim();
if (full.isNotEmpty) name = full;
}
}
return PhoneLookupResult(
id: id,
name: name,
avatarUrl: contact['baseUrl'] as String?,
);
}
static String? _normalizePhone(String raw) {
final digits = raw.replaceAll(RegExp(r'[^\d]'), '');
if (digits.length < 5) return null;
return '+$digits';
}
static Future<CachedContact?> addContact(
Api api,
int id,