💄 Bug fixes search messages and optimization
This commit is contained in:
@@ -263,7 +263,6 @@
|
|||||||
"walletCurrencyShortPoints": "NSP",
|
"walletCurrencyShortPoints": "NSP",
|
||||||
"walletCurrencyGolds": "The Solar Dollars",
|
"walletCurrencyGolds": "The Solar Dollars",
|
||||||
"walletCurrencyShortGolds": "NSD",
|
"walletCurrencyShortGolds": "NSD",
|
||||||
"retry": "Retry",
|
|
||||||
"creatorHubUnselectedHint": "Pick / create a publisher to get started.",
|
"creatorHubUnselectedHint": "Pick / create a publisher to get started.",
|
||||||
"relationships": "Relationships",
|
"relationships": "Relationships",
|
||||||
"addFriend": "Send a Friend Request",
|
"addFriend": "Send a Friend Request",
|
||||||
@@ -1020,6 +1019,10 @@
|
|||||||
"searchLinks": "Links",
|
"searchLinks": "Links",
|
||||||
"searchAttachments": "Attachments",
|
"searchAttachments": "Attachments",
|
||||||
"noMessagesFound": "No messages found",
|
"noMessagesFound": "No messages found",
|
||||||
|
"Searching...": "Searching...",
|
||||||
|
"searchError": "Search failed. Please try again.",
|
||||||
|
"tryDifferentKeywords": "Try different keywords or remove search filters",
|
||||||
|
"retry": "Retry",
|
||||||
"openInBrowser": "Open in Browser",
|
"openInBrowser": "Open in Browser",
|
||||||
"highlightPost": "Highlight Post",
|
"highlightPost": "Highlight Post",
|
||||||
"filters": "Filters",
|
"filters": "Filters",
|
||||||
|
@@ -39,6 +39,7 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
bool _hasMore = true;
|
bool _hasMore = true;
|
||||||
bool _isSyncing = false;
|
bool _isSyncing = false;
|
||||||
bool _isJumping = false;
|
bool _isJumping = false;
|
||||||
|
bool _isUpdatingState = false;
|
||||||
DateTime? _lastPauseTime;
|
DateTime? _lastPauseTime;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -92,6 +93,28 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
return messages;
|
return messages;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _updateStateSafely(List<LocalChatMessage> messages) async {
|
||||||
|
if (_isUpdatingState) {
|
||||||
|
talker.log('State update already in progress, skipping');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_isUpdatingState = true;
|
||||||
|
try {
|
||||||
|
// Ensure messages are properly sorted and deduplicated
|
||||||
|
final sortedMessages = _sortMessages(messages);
|
||||||
|
final uniqueMessages = <LocalChatMessage>[];
|
||||||
|
final seenIds = <String>{};
|
||||||
|
for (final message in sortedMessages) {
|
||||||
|
if (seenIds.add(message.id)) {
|
||||||
|
uniqueMessages.add(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state = AsyncValue.data(uniqueMessages);
|
||||||
|
} finally {
|
||||||
|
_isUpdatingState = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<List<LocalChatMessage>> _getCachedMessages({
|
Future<List<LocalChatMessage>> _getCachedMessages({
|
||||||
int offset = 0,
|
int offset = 0,
|
||||||
int take = 20,
|
int take = 20,
|
||||||
@@ -668,11 +691,37 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void searchMessages(String query, {bool? withLinks, bool? withAttachments}) {
|
Future<void> searchMessages(
|
||||||
|
String query, {
|
||||||
|
bool? withLinks,
|
||||||
|
bool? withAttachments,
|
||||||
|
}) async {
|
||||||
_searchQuery = query.trim();
|
_searchQuery = query.trim();
|
||||||
_withLinks = withLinks;
|
_withLinks = withLinks;
|
||||||
_withAttachments = withAttachments;
|
_withAttachments = withAttachments;
|
||||||
loadInitial();
|
|
||||||
|
if (_searchQuery!.isEmpty) {
|
||||||
|
state = AsyncValue.data([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
talker.log('Searching messages with query: $_searchQuery');
|
||||||
|
state = const AsyncValue.loading();
|
||||||
|
|
||||||
|
try {
|
||||||
|
final messages = await _getCachedMessages(
|
||||||
|
offset: 0,
|
||||||
|
take: 50,
|
||||||
|
); // Limit initial search results
|
||||||
|
state = AsyncValue.data(messages);
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
talker.log(
|
||||||
|
'Error searching messages',
|
||||||
|
exception: e,
|
||||||
|
stackTrace: stackTrace,
|
||||||
|
);
|
||||||
|
state = AsyncValue.error(e, stackTrace);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void clearSearch() {
|
void clearSearch() {
|
||||||
@@ -717,6 +766,9 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
}
|
}
|
||||||
_isJumping = true;
|
_isJumping = true;
|
||||||
|
|
||||||
|
// Clear flashing messages when starting a new jump
|
||||||
|
ref.read(flashingMessagesProvider.notifier).state = {};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
talker.log('Fetching message $messageId');
|
talker.log('Fetching message $messageId');
|
||||||
final message = await fetchMessageById(messageId);
|
final message = await fetchMessageById(messageId);
|
||||||
@@ -772,7 +824,7 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (newMessages.isNotEmpty) {
|
if (newMessages.isNotEmpty) {
|
||||||
// Merge with current messages
|
// Merge with current messages more safely
|
||||||
final allMessages = [...currentMessages, ...newMessages];
|
final allMessages = [...currentMessages, ...newMessages];
|
||||||
final uniqueMessages = <LocalChatMessage>[];
|
final uniqueMessages = <LocalChatMessage>[];
|
||||||
final seenIds = <String>{};
|
final seenIds = <String>{};
|
||||||
@@ -781,8 +833,7 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
uniqueMessages.add(message);
|
uniqueMessages.add(message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_sortMessages(uniqueMessages);
|
await _updateStateSafely(uniqueMessages);
|
||||||
state = AsyncValue.data(uniqueMessages);
|
|
||||||
talker.log(
|
talker.log(
|
||||||
'Updated state with ${uniqueMessages.length} total messages',
|
'Updated state with ${uniqueMessages.length} total messages',
|
||||||
);
|
);
|
||||||
|
@@ -385,121 +385,121 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget chatMessageListWidget(List<LocalChatMessage> messageList) =>
|
Widget chatMessageListWidget(
|
||||||
SuperListView.builder(
|
List<LocalChatMessage> messageList,
|
||||||
listController: listController,
|
) => SuperListView.builder(
|
||||||
padding: EdgeInsets.only(
|
listController: listController,
|
||||||
top: 16,
|
padding: EdgeInsets.only(
|
||||||
bottom: 80 + MediaQuery.of(context).padding.bottom,
|
top: 16,
|
||||||
),
|
bottom: 80 + MediaQuery.of(context).padding.bottom,
|
||||||
controller: scrollController,
|
),
|
||||||
reverse: true, // Show newest messages at the bottom
|
controller: scrollController,
|
||||||
itemCount: messageList.length,
|
reverse: true, // Show newest messages at the bottom
|
||||||
findChildIndexCallback: (key) {
|
itemCount: messageList.length,
|
||||||
final valueKey = key as ValueKey;
|
findChildIndexCallback: (key) {
|
||||||
final messageId = (valueKey.value as String).substring(
|
if (key is! ValueKey<String>) return null;
|
||||||
messageKeyPrefix.length,
|
final messageId = key.value.substring(messageKeyPrefix.length);
|
||||||
);
|
final index = messageList.indexWhere((m) => m.id == messageId);
|
||||||
return messageList.indexWhere((m) => m.id == messageId);
|
// Return null for invalid indices to let SuperListView handle it properly
|
||||||
},
|
return index >= 0 ? index : null;
|
||||||
extentEstimation: (_, _) => 40,
|
},
|
||||||
itemBuilder: (context, index) {
|
extentEstimation: (_, _) => 40,
|
||||||
final message = messageList[index];
|
itemBuilder: (context, index) {
|
||||||
final nextMessage =
|
final message = messageList[index];
|
||||||
index < messageList.length - 1 ? messageList[index + 1] : null;
|
final nextMessage =
|
||||||
final isLastInGroup =
|
index < messageList.length - 1 ? messageList[index + 1] : null;
|
||||||
nextMessage == null ||
|
final isLastInGroup =
|
||||||
nextMessage.senderId != message.senderId ||
|
nextMessage == null ||
|
||||||
nextMessage.createdAt
|
nextMessage.senderId != message.senderId ||
|
||||||
.difference(message.createdAt)
|
nextMessage.createdAt
|
||||||
.inMinutes
|
.difference(message.createdAt)
|
||||||
.abs() >
|
.inMinutes
|
||||||
3;
|
.abs() >
|
||||||
|
3;
|
||||||
|
|
||||||
final key = ValueKey('$messageKeyPrefix${message.id}');
|
final key = ValueKey('$messageKeyPrefix${message.id}');
|
||||||
|
|
||||||
return chatIdentity.when(
|
return chatIdentity.when(
|
||||||
skipError: true,
|
skipError: true,
|
||||||
data:
|
data:
|
||||||
(identity) => MessageItem(
|
(identity) => MessageItem(
|
||||||
key: key,
|
key: key,
|
||||||
message: message,
|
message: message,
|
||||||
isCurrentUser: identity?.id == message.senderId,
|
isCurrentUser: identity?.id == message.senderId,
|
||||||
onAction: (action) {
|
onAction: (action) {
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case MessageItemAction.delete:
|
case MessageItemAction.delete:
|
||||||
messagesNotifier.deleteMessage(message.id);
|
messagesNotifier.deleteMessage(message.id);
|
||||||
case MessageItemAction.edit:
|
case MessageItemAction.edit:
|
||||||
messageEditingTo.value = message.toRemoteMessage();
|
messageEditingTo.value = message.toRemoteMessage();
|
||||||
messageController.text =
|
messageController.text =
|
||||||
messageEditingTo.value?.content ?? '';
|
messageEditingTo.value?.content ?? '';
|
||||||
attachments.value =
|
attachments.value =
|
||||||
messageEditingTo.value!.attachments
|
messageEditingTo.value!.attachments
|
||||||
.map((e) => UniversalFile.fromAttachment(e))
|
.map((e) => UniversalFile.fromAttachment(e))
|
||||||
.toList();
|
.toList();
|
||||||
case MessageItemAction.forward:
|
case MessageItemAction.forward:
|
||||||
messageForwardingTo.value = message.toRemoteMessage();
|
messageForwardingTo.value = message.toRemoteMessage();
|
||||||
case MessageItemAction.reply:
|
case MessageItemAction.reply:
|
||||||
messageReplyingTo.value = message.toRemoteMessage();
|
messageReplyingTo.value = message.toRemoteMessage();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onJump: (messageId) {
|
onJump: (messageId) {
|
||||||
final messageIndex = messageList.indexWhere(
|
final messageIndex = messageList.indexWhere(
|
||||||
(m) => m.id == messageId,
|
(m) => m.id == messageId,
|
||||||
);
|
);
|
||||||
if (messageIndex == -1) {
|
if (messageIndex == -1) {
|
||||||
messagesNotifier.jumpToMessage(messageId).then((index) {
|
messagesNotifier.jumpToMessage(messageId).then((index) {
|
||||||
if (index != -1) {
|
if (index != -1) {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
listController.animateToItem(
|
listController.animateToItem(
|
||||||
index: index,
|
index: index,
|
||||||
scrollController: scrollController,
|
scrollController: scrollController,
|
||||||
alignment: 0.5,
|
alignment: 0.5,
|
||||||
duration:
|
duration:
|
||||||
(estimatedDistance) =>
|
(estimatedDistance) =>
|
||||||
Duration(milliseconds: 250),
|
Duration(milliseconds: 250),
|
||||||
curve: (estimatedDistance) => Curves.easeInOut,
|
curve: (estimatedDistance) => Curves.easeInOut,
|
||||||
);
|
);
|
||||||
});
|
|
||||||
ref
|
|
||||||
.read(flashingMessagesProvider.notifier)
|
|
||||||
.update((set) => set.union({messageId}));
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
return;
|
ref
|
||||||
|
.read(flashingMessagesProvider.notifier)
|
||||||
|
.update((set) => set.union({messageId}));
|
||||||
}
|
}
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
});
|
||||||
listController.animateToItem(
|
return;
|
||||||
index: messageIndex,
|
}
|
||||||
scrollController: scrollController,
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
alignment: 0.5,
|
listController.animateToItem(
|
||||||
duration:
|
index: messageIndex,
|
||||||
(estimatedDistance) =>
|
scrollController: scrollController,
|
||||||
Duration(milliseconds: 250),
|
alignment: 0.5,
|
||||||
curve: (estimatedDistance) => Curves.easeInOut,
|
duration:
|
||||||
);
|
(estimatedDistance) => Duration(milliseconds: 250),
|
||||||
});
|
curve: (estimatedDistance) => Curves.easeInOut,
|
||||||
ref
|
);
|
||||||
.read(flashingMessagesProvider.notifier)
|
});
|
||||||
.update((set) => set.union({messageId}));
|
ref
|
||||||
},
|
.read(flashingMessagesProvider.notifier)
|
||||||
progress: attachmentProgress.value[message.id],
|
.update((set) => set.union({messageId}));
|
||||||
showAvatar: isLastInGroup,
|
},
|
||||||
),
|
progress: attachmentProgress.value[message.id],
|
||||||
loading:
|
showAvatar: isLastInGroup,
|
||||||
() => MessageItem(
|
),
|
||||||
key: key,
|
loading:
|
||||||
message: message,
|
() => MessageItem(
|
||||||
isCurrentUser: false,
|
key: key,
|
||||||
onAction: null,
|
message: message,
|
||||||
progress: null,
|
isCurrentUser: false,
|
||||||
showAvatar: false,
|
onAction: null,
|
||||||
onJump: (_) {},
|
progress: null,
|
||||||
),
|
showAvatar: false,
|
||||||
error: (_, _) => SizedBox.shrink(key: key),
|
onJump: (_) {},
|
||||||
);
|
),
|
||||||
},
|
error: (_, _) => SizedBox.shrink(key: key),
|
||||||
);
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
|
@@ -4,10 +4,12 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
|||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:island/pods/chat/messages_notifier.dart';
|
import 'package:island/pods/chat/messages_notifier.dart';
|
||||||
|
import 'package:island/pods/chat/chat_rooms.dart';
|
||||||
import 'package:island/widgets/app_scaffold.dart';
|
import 'package:island/widgets/app_scaffold.dart';
|
||||||
import 'package:island/widgets/chat/message_list_tile.dart';
|
import 'package:island/widgets/chat/message_list_tile.dart';
|
||||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||||
import 'package:super_sliver_list/super_sliver_list.dart';
|
import 'package:super_sliver_list/super_sliver_list.dart';
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
// Class to represent the result when popping from search messages
|
// Class to represent the result when popping from search messages
|
||||||
class SearchMessagesResult {
|
class SearchMessagesResult {
|
||||||
@@ -15,6 +17,9 @@ class SearchMessagesResult {
|
|||||||
const SearchMessagesResult(this.messageId);
|
const SearchMessagesResult(this.messageId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Search states for better UX
|
||||||
|
enum SearchState { idle, searching, results, noResults, error }
|
||||||
|
|
||||||
class SearchMessagesScreen extends HookConsumerWidget {
|
class SearchMessagesScreen extends HookConsumerWidget {
|
||||||
final String roomId;
|
final String roomId;
|
||||||
|
|
||||||
@@ -25,118 +30,316 @@ class SearchMessagesScreen extends HookConsumerWidget {
|
|||||||
final searchController = useTextEditingController();
|
final searchController = useTextEditingController();
|
||||||
final withLinks = useState(false);
|
final withLinks = useState(false);
|
||||||
final withAttachments = useState(false);
|
final withAttachments = useState(false);
|
||||||
|
final searchState = useState(SearchState.idle);
|
||||||
|
final searchResultCount = useState<int?>(null);
|
||||||
|
|
||||||
|
// Debounce timer for search optimization
|
||||||
|
final debounceTimer = useRef<Timer?>(null);
|
||||||
|
|
||||||
final messagesNotifier = ref.read(
|
final messagesNotifier = ref.read(
|
||||||
messagesNotifierProvider(roomId).notifier,
|
messagesNotifierProvider(roomId).notifier,
|
||||||
);
|
);
|
||||||
final messages = ref.watch(messagesNotifierProvider(roomId));
|
final messages = ref.watch(messagesNotifierProvider(roomId));
|
||||||
|
|
||||||
|
// Optimized search function with debouncing
|
||||||
|
void performSearch(String query) {
|
||||||
|
if (query.trim().isEmpty) {
|
||||||
|
searchState.value = SearchState.idle;
|
||||||
|
searchResultCount.value = null;
|
||||||
|
messagesNotifier.clearSearch();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
searchState.value = SearchState.searching;
|
||||||
|
|
||||||
|
// Cancel previous search if still active
|
||||||
|
debounceTimer.value?.cancel();
|
||||||
|
|
||||||
|
// Debounce search to avoid excessive API calls
|
||||||
|
debounceTimer.value = Timer(const Duration(milliseconds: 300), () {
|
||||||
|
messagesNotifier.searchMessages(
|
||||||
|
query.trim(),
|
||||||
|
withLinks: withLinks.value,
|
||||||
|
withAttachments: withAttachments.value,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update search state based on messages state
|
||||||
|
useEffect(() {
|
||||||
|
messages.when(
|
||||||
|
data: (messageList) {
|
||||||
|
if (searchState.value == SearchState.searching) {
|
||||||
|
searchState.value =
|
||||||
|
messageList.isEmpty
|
||||||
|
? SearchState.noResults
|
||||||
|
: SearchState.results;
|
||||||
|
searchResultCount.value = messageList.length;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
loading: () {
|
||||||
|
if (searchController.text.trim().isNotEmpty) {
|
||||||
|
searchState.value = SearchState.searching;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: (error, stack) {
|
||||||
|
searchState.value = SearchState.error;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}, [messages]);
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
// Clear search when screen is disposed
|
// Clear search when screen is disposed
|
||||||
return () {
|
return () {
|
||||||
|
debounceTimer.value?.cancel();
|
||||||
messagesNotifier.clearSearch();
|
messagesNotifier.clearSearch();
|
||||||
|
// Clear flashing messages when leaving search
|
||||||
|
ref.read(flashingMessagesProvider.notifier).state = {};
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
appBar: AppBar(title: const Text('searchMessages').tr()),
|
appBar: AppBar(
|
||||||
|
title: const Text('searchMessages').tr(),
|
||||||
|
bottom:
|
||||||
|
searchState.value == SearchState.searching
|
||||||
|
? const PreferredSize(
|
||||||
|
preferredSize: Size.fromHeight(2),
|
||||||
|
child: LinearProgressIndicator(),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
body: Column(
|
body: Column(
|
||||||
children: [
|
children: [
|
||||||
Column(
|
// Search input section
|
||||||
children: [
|
Container(
|
||||||
TextField(
|
decoration: BoxDecoration(
|
||||||
controller: searchController,
|
color: Theme.of(context).cardColor,
|
||||||
decoration: InputDecoration(
|
borderRadius: const BorderRadius.vertical(
|
||||||
hintText: 'searchMessagesHint'.tr(),
|
bottom: Radius.circular(8),
|
||||||
border: InputBorder.none,
|
|
||||||
isDense: true,
|
|
||||||
contentPadding: EdgeInsets.only(
|
|
||||||
left: 16,
|
|
||||||
right: 16,
|
|
||||||
top: 12,
|
|
||||||
bottom: 16,
|
|
||||||
),
|
|
||||||
suffix: IconButton(
|
|
||||||
iconSize: 18,
|
|
||||||
visualDensity: VisualDensity.compact,
|
|
||||||
icon: const Icon(Icons.clear),
|
|
||||||
onPressed: () {
|
|
||||||
searchController.clear();
|
|
||||||
messagesNotifier.clearSearch();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
onChanged: (query) {
|
|
||||||
messagesNotifier.searchMessages(
|
|
||||||
query,
|
|
||||||
withLinks: withLinks.value,
|
|
||||||
withAttachments: withAttachments.value,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
Row(
|
),
|
||||||
children: [
|
child: Column(
|
||||||
Expanded(
|
children: [
|
||||||
child: CheckboxListTile(
|
TextField(
|
||||||
secondary: const Icon(Symbols.link),
|
controller: searchController,
|
||||||
title: const Text('searchLinks').tr(),
|
autofocus: true,
|
||||||
value: withLinks.value,
|
decoration: InputDecoration(
|
||||||
onChanged: (bool? value) {
|
hintText: 'searchMessagesHint'.tr(),
|
||||||
withLinks.value = value!;
|
border: InputBorder.none,
|
||||||
messagesNotifier.searchMessages(
|
isDense: true,
|
||||||
searchController.text,
|
contentPadding: const EdgeInsets.only(
|
||||||
withLinks: withLinks.value,
|
left: 16,
|
||||||
withAttachments: withAttachments.value,
|
right: 16,
|
||||||
);
|
top: 12,
|
||||||
},
|
bottom: 16,
|
||||||
),
|
),
|
||||||
),
|
prefixIcon: const Icon(Icons.search, size: 20),
|
||||||
Expanded(
|
suffixIcon: Row(
|
||||||
child: CheckboxListTile(
|
mainAxisSize: MainAxisSize.min,
|
||||||
secondary: const Icon(Symbols.file_copy),
|
children: [
|
||||||
title: const Text('searchAttachments').tr(),
|
if (searchController.text.isNotEmpty)
|
||||||
value: withAttachments.value,
|
IconButton(
|
||||||
onChanged: (bool? value) {
|
iconSize: 18,
|
||||||
withAttachments.value = value!;
|
visualDensity: VisualDensity.compact,
|
||||||
messagesNotifier.searchMessages(
|
icon: const Icon(Icons.clear),
|
||||||
searchController.text,
|
onPressed: () {
|
||||||
withLinks: withLinks.value,
|
searchController.clear();
|
||||||
withAttachments: withAttachments.value,
|
performSearch('');
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const Divider(height: 1),
|
|
||||||
Expanded(
|
|
||||||
child: messages.when(
|
|
||||||
data:
|
|
||||||
(messageList) =>
|
|
||||||
messageList.isEmpty
|
|
||||||
? Center(child: Text('noMessagesFound'.tr()))
|
|
||||||
: SuperListView.builder(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
|
||||||
reverse: false, // Show newest messages at the top
|
|
||||||
itemCount: messageList.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final message = messageList[index];
|
|
||||||
return MessageListTile(
|
|
||||||
message: message,
|
|
||||||
onJump: (messageId) {
|
|
||||||
// Return the search result and pop back to room detail
|
|
||||||
context.pop(SearchMessagesResult(messageId));
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
loading: () => const Center(child: CircularProgressIndicator()),
|
if (searchResultCount.value != null &&
|
||||||
error:
|
searchState.value == SearchState.results)
|
||||||
(error, _) => Center(
|
Container(
|
||||||
child: Text('errorGeneric'.tr(args: [error.toString()])),
|
margin: const EdgeInsets.only(right: 8),
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 8,
|
||||||
|
vertical: 2,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.primary.withOpacity(0.1),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'${searchResultCount.value}',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
|
onChanged: performSearch,
|
||||||
|
),
|
||||||
|
// Search filters
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
left: 16,
|
||||||
|
right: 16,
|
||||||
|
bottom: 8,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: FilterChip(
|
||||||
|
avatar: const Icon(Symbols.link, size: 16),
|
||||||
|
label: const Text('searchLinks').tr(),
|
||||||
|
selected: withLinks.value,
|
||||||
|
onSelected: (bool? value) {
|
||||||
|
withLinks.value = value!;
|
||||||
|
performSearch(searchController.text);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: FilterChip(
|
||||||
|
avatar: const Icon(Symbols.file_copy, size: 16),
|
||||||
|
label: const Text('searchAttachments').tr(),
|
||||||
|
selected: withAttachments.value,
|
||||||
|
onSelected: (bool? value) {
|
||||||
|
withAttachments.value = value!;
|
||||||
|
performSearch(searchController.text);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Divider(height: 1),
|
||||||
|
|
||||||
|
// Search results section
|
||||||
|
Expanded(
|
||||||
|
child: messages.when(
|
||||||
|
data: (messageList) {
|
||||||
|
switch (searchState.value) {
|
||||||
|
case SearchState.idle:
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.search,
|
||||||
|
size: 64,
|
||||||
|
color: Theme.of(context).disabledColor,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'searchMessagesHint'.tr(),
|
||||||
|
style: Theme.of(
|
||||||
|
context,
|
||||||
|
).textTheme.bodyLarge?.copyWith(
|
||||||
|
color: Theme.of(context).disabledColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
case SearchState.noResults:
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.search_off,
|
||||||
|
size: 64,
|
||||||
|
color: Theme.of(context).disabledColor,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'noMessagesFound'.tr(),
|
||||||
|
style: Theme.of(
|
||||||
|
context,
|
||||||
|
).textTheme.bodyLarge?.copyWith(
|
||||||
|
color: Theme.of(context).disabledColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'tryDifferentKeywords'.tr(),
|
||||||
|
style: Theme.of(
|
||||||
|
context,
|
||||||
|
).textTheme.bodySmall?.copyWith(
|
||||||
|
color: Theme.of(context).disabledColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
case SearchState.results:
|
||||||
|
return SuperListView.builder(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
reverse: false, // Show newest messages at the top
|
||||||
|
itemCount: messageList.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final message = messageList[index];
|
||||||
|
return MessageListTile(
|
||||||
|
message: message,
|
||||||
|
onJump: (messageId) {
|
||||||
|
// Return the search result and pop back to room detail
|
||||||
|
context.pop(SearchMessagesResult(messageId));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
loading: () {
|
||||||
|
if (searchState.value == SearchState.searching) {
|
||||||
|
return const Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
CircularProgressIndicator(),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
Text('Searching...'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return const Center(child: CircularProgressIndicator());
|
||||||
|
},
|
||||||
|
error: (error, _) {
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.error_outline,
|
||||||
|
size: 64,
|
||||||
|
color: Theme.of(context).colorScheme.error,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'searchError'.tr(),
|
||||||
|
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||||
|
color: Theme.of(context).colorScheme.error,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
ElevatedButton.icon(
|
||||||
|
onPressed: () => performSearch(searchController.text),
|
||||||
|
icon: const Icon(Icons.refresh),
|
||||||
|
label: const Text('retry').tr(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@@ -298,7 +298,10 @@ class _MessageActionSheetState extends State<MessageActionSheet> {
|
|||||||
if (widget.remoteMessage.content?.isNotEmpty ?? false) ...[
|
if (widget.remoteMessage.content?.isNotEmpty ?? false) ...[
|
||||||
Container(
|
Container(
|
||||||
margin: const EdgeInsets.fromLTRB(16, 16, 16, 0),
|
margin: const EdgeInsets.fromLTRB(16, 16, 16, 0),
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
vertical: 12,
|
||||||
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Theme.of(
|
color: Theme.of(
|
||||||
context,
|
context,
|
||||||
@@ -335,6 +338,7 @@ class _MessageActionSheetState extends State<MessageActionSheet> {
|
|||||||
const Spacer(),
|
const Spacer(),
|
||||||
if (_shouldShowExpandButton)
|
if (_shouldShowExpandButton)
|
||||||
IconButton(
|
IconButton(
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
_isExpanded
|
_isExpanded
|
||||||
? Symbols.expand_less
|
? Symbols.expand_less
|
||||||
|
Reference in New Issue
Block a user