fix(search): поиск по сообщениям через chatSearch + просмотр каналов из поиска

This commit is contained in:
klockky
2026-06-21 13:37:31 +00:00
parent 23a1bed46a
commit 721515729c
4 changed files with 134 additions and 35 deletions
+73 -3
View File
@@ -177,6 +177,22 @@ class ChatSearchHit {
});
}
class MessageSearchHit {
final int chatId;
final String? messageId;
final String? text;
final int time;
final int senderId;
const MessageSearchHit({
required this.chatId,
this.messageId,
this.text,
required this.time,
required this.senderId,
});
}
sealed class MessageEvent {
final int chatId;
const MessageEvent(this.chatId);
@@ -1192,7 +1208,28 @@ class ChatsModule {
return hits;
}
static Future<List<ChatSearchHit>> searchChats(
static List<MessageSearchHit> _parseMessageResult(dynamic payload) {
final result = (payload as Map?)?['result'];
if (result is! List) return const [];
final hits = <MessageSearchHit>[];
for (final item in result) {
if (item is! Map) continue;
final message = item['message'];
if (message is! Map) continue;
final chatId = item['chatId'];
if (chatId is! int || chatId == 0) continue;
hits.add(MessageSearchHit(
chatId: chatId,
messageId: message['id']?.toString(),
text: message['text'] as String?,
time: (message['time'] as int?) ?? 0,
senderId: (message['sender'] as int?) ?? 0,
));
}
return hits;
}
static Future<List<MessageSearchHit>> searchMessages(
Api api,
String query, {
int count = 50,
@@ -1205,9 +1242,9 @@ class ChatsModule {
'query': term,
});
if (packet.isError) return const [];
return _parseSearchResult(packet.payload);
return _parseMessageResult(packet.payload);
} catch (e) {
logger.w('searchChats failed: $e');
logger.w('searchMessages failed: $e');
return const [];
}
}
@@ -1233,6 +1270,39 @@ class ChatsModule {
}
}
static Future<void> subscribeChat(
Api api,
int chatId, {
bool subscribe = true,
}) async {
try {
await api.sendRequest(Opcode.chatSubscribe, {
'chatId': chatId,
'subscribe': subscribe,
});
} catch (e) {
logger.w('subscribeChat failed: $e');
}
}
static Future<bool> ensureChatCached(
Api api,
int accountId,
int chatId,
) async {
final rows = await AppDatabase.loadChat(accountId, chatId);
if (rows.isNotEmpty) return true;
try {
final info = await getChatInfo(api, chatId);
if (info == null) return false;
await cacheServerChat(info, accountId);
return true;
} catch (e) {
logger.w('ensureChatCached failed for $chatId: $e');
return false;
}
}
static Future<CachedChat?> createGroupChat(
Api api, {
required String title,
+8 -11
View File
@@ -601,24 +601,21 @@ class AppDatabase {
);
}
static Future<List<Map<String, dynamic>>> searchMessages(
static Future<List<Map<String, dynamic>>> searchChatsByTitle(
int accountId,
String query, {
int limit = 50,
int limit = 30,
}) async {
final term = query.trim();
if (term.isEmpty) return const [];
final db = await _instance;
final like = '%${_escapeLike(term)}%';
return db.rawQuery(
'SELECT m.id AS id, m.chat_id AS chat_id, m.sender_id AS sender_id, '
'm.text AS text, m.time AS time, '
'c.title AS chat_title, c.icon_url AS chat_icon, c.type AS chat_type '
'FROM messages m '
'LEFT JOIN chats_cache c ON c.id = m.chat_id AND c.account_id = m.account_id '
"WHERE m.account_id = ? AND m.deleted = 0 AND m.text LIKE ? ESCAPE '\\' "
'ORDER BY m.time DESC LIMIT ?',
[accountId, like, limit],
return db.query(
'chats_cache',
where: "account_id = ? AND title LIKE ? ESCAPE '\\'",
whereArgs: [accountId, like],
orderBy: 'last_event_time DESC',
limit: limit,
);
}
@@ -409,6 +409,11 @@ class _ChatScreenState extends State<ChatScreen>
}
try {
final cachedRows = await AppDatabase.loadChat(_myId, widget.chatId);
if (cachedRows.isEmpty) {
await ChatsModule.ensureChatCached(api, _myId, widget.chatId);
await ChatsModule.subscribeChat(api, widget.chatId);
}
final serverMessages = await messagesModule.fetchHistory(
_myId,
widget.chatId,
+48 -21
View File
@@ -29,8 +29,9 @@ class _SearchScreenState extends State<SearchScreen> {
bool _loading = false;
PhoneLookupResult? _phoneResult;
List<Map<String, dynamic>> _contacts = const [];
List<ChatSearchHit> _chats = const [];
List<Map<String, dynamic>> _messages = const [];
List<Map<String, dynamic>> _chats = const [];
List<MessageSearchHit> _messages = const [];
Map<int, Map<String, dynamic>> _msgChatMeta = const {};
List<ChatSearchHit> _public = const [];
@override
@@ -62,10 +63,14 @@ class _SearchScreenState extends State<SearchScreen> {
_contacts = const [];
_chats = const [];
_messages = const [];
_msgChatMeta = const {};
_public = const [];
});
return;
}
if (_phoneResult != null) {
setState(() => _phoneResult = null);
}
_debounce = Timer(const Duration(milliseconds: 300), _runSearch);
}
@@ -81,10 +86,10 @@ class _SearchScreenState extends State<SearchScreen> {
accountId == null
? Future.value(const <Map<String, dynamic>>[])
: AppDatabase.searchContacts(accountId, query),
ChatsModule.searchChats(api, query),
accountId == null
? Future.value(const <Map<String, dynamic>>[])
: AppDatabase.searchMessages(accountId, query),
: AppDatabase.searchChatsByTitle(accountId, query),
ChatsModule.searchMessages(api, query),
ChatsModule.searchPublic(api, query),
phoneQuery == null
? Future<PhoneLookupResult?>.value(null)
@@ -93,17 +98,27 @@ class _SearchScreenState extends State<SearchScreen> {
if (!mounted || token != _seq) return;
final chats = results[1] as List<ChatSearchHit>;
final chatIds = chats.map((c) => c.id).toSet();
final chats = results[1] as List<Map<String, dynamic>>;
final messages = results[2] as List<MessageSearchHit>;
final localChatIds = chats.map((c) => c['id'] as int).toSet();
final public = (results[3] as List<ChatSearchHit>)
.where((c) => !chatIds.contains(c.id))
.where((c) => !localChatIds.contains(c.id))
.toList();
var meta = <int, Map<String, dynamic>>{};
if (accountId != null && messages.isNotEmpty) {
final ids = messages.map((m) => m.chatId).toSet().toList();
final rows = await AppDatabase.loadChatsByIds(accountId, ids);
meta = {for (final r in rows) r['id'] as int: r};
if (!mounted || token != _seq) return;
}
setState(() {
_phoneResult = results[4] as PhoneLookupResult?;
_contacts = results[0] as List<Map<String, dynamic>>;
_chats = chats;
_messages = results[2] as List<Map<String, dynamic>>;
_messages = messages;
_msgChatMeta = meta;
_public = public;
_loading = false;
});
@@ -246,22 +261,21 @@ class _SearchScreenState extends State<SearchScreen> {
],
if (_chats.isNotEmpty) ...[
_sectionHeader(cs, 'Чаты'),
for (final hit in _chats) _chatTile(hit),
for (final row in _chats)
_ResultTile(
name: (row['title'] as String?) ?? '',
imageUrl: row['icon_url'] as String?,
onTap: () => _openChat(
row['id'] as int,
(row['title'] as String?) ?? '',
row['icon_url'] as String?,
(row['type'] as String?) ?? 'CHAT',
),
),
],
if (_messages.isNotEmpty) ...[
_sectionHeader(cs, 'Сообщения'),
for (final row in _messages)
_ResultTile(
name: (row['chat_title'] as String?) ?? '',
imageUrl: row['chat_icon'] as String?,
subtitle: (row['text'] as String?)?.trim(),
onTap: () => _openChat(
row['chat_id'] as int,
(row['chat_title'] as String?) ?? '',
row['chat_icon'] as String?,
(row['chat_type'] as String?) ?? 'CHAT',
),
),
for (final hit in _messages) _messageTile(hit),
],
if (_public.isNotEmpty) ...[
_sectionHeader(cs, 'Глобальный поиск'),
@@ -279,6 +293,19 @@ class _SearchScreenState extends State<SearchScreen> {
onTap: () => _openChat(hit.id, hit.title ?? '', hit.avatarUrl, hit.type),
);
Widget _messageTile(MessageSearchHit hit) {
final meta = _msgChatMeta[hit.chatId];
final title = (meta?['title'] as String?) ?? 'Чат';
final icon = meta?['icon_url'] as String?;
final type = (meta?['type'] as String?) ?? 'CHAT';
return _ResultTile(
name: title,
imageUrl: icon,
subtitle: hit.text?.trim(),
onTap: () => _openChat(hit.chatId, title, icon, type),
);
}
Widget _sectionHeader(ColorScheme cs, String title) => Padding(
padding: const EdgeInsets.fromLTRB(20, 16, 20, 6),
child: Text(