fix(search): поиск по сообщениям через chatSearch + просмотр каналов из поиска
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user