parent
427e06be3e
commit
a54004736b
11 changed files with 427 additions and 143 deletions
|
|
@ -41,6 +41,7 @@ import "package:nexus/models/requests/paginate_manual.dart";
|
||||||
import "package:nexus/models/requests/redact_event.dart";
|
import "package:nexus/models/requests/redact_event.dart";
|
||||||
import "package:nexus/models/requests/register_pusher.dart";
|
import "package:nexus/models/requests/register_pusher.dart";
|
||||||
import "package:nexus/models/requests/report.dart";
|
import "package:nexus/models/requests/report.dart";
|
||||||
|
import "package:nexus/models/requests/search.dart";
|
||||||
import "package:nexus/models/requests/send_event.dart";
|
import "package:nexus/models/requests/send_event.dart";
|
||||||
import "package:nexus/models/requests/send_message.dart";
|
import "package:nexus/models/requests/send_message.dart";
|
||||||
import "package:nexus/models/requests/set_account_data.dart";
|
import "package:nexus/models/requests/set_account_data.dart";
|
||||||
|
|
@ -276,6 +277,12 @@ class ClientController extends AsyncNotifier<int> {
|
||||||
Future<PaginateManual> paginateManual(PaginateManualRequest request) async =>
|
Future<PaginateManual> paginateManual(PaginateManualRequest request) async =>
|
||||||
.fromJson(await _sendCommand("paginate_manual", request.toJson()));
|
.fromJson(await _sendCommand("paginate_manual", request.toJson()));
|
||||||
|
|
||||||
|
Future<PaginateManual> searchLocal(SearchRequest request) async =>
|
||||||
|
.fromJson(await _sendCommand("search_local", request.toJson()));
|
||||||
|
|
||||||
|
Future<PaginateManual> searchServer(SearchRequest request) async =>
|
||||||
|
.fromJson(await _sendCommand("search_server", request.toJson()));
|
||||||
|
|
||||||
Future<EventContext> getEventContext(GetEventContextRequest request) async =>
|
Future<EventContext> getEventContext(GetEventContextRequest request) async =>
|
||||||
.fromJson(await _sendCommand("get_event_context", request.toJson()));
|
.fromJson(await _sendCommand("get_event_context", request.toJson()));
|
||||||
|
|
||||||
|
|
|
||||||
97
lib/controllers/event_search.dart
Normal file
97
lib/controllers/event_search.dart
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||||
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
|
import "package:nexus/controllers/client.dart";
|
||||||
|
import "package:nexus/controllers/rooms.dart";
|
||||||
|
import "package:nexus/models/requests/search.dart";
|
||||||
|
|
||||||
|
typedef EventSearchParams = ({String roomId, String query});
|
||||||
|
typedef EventSearchResults = ({IList<int> rowIds, String? nextBatch});
|
||||||
|
|
||||||
|
class EventSearchController(final EventSearchParams info)
|
||||||
|
extends AsyncNotifier<EventSearchResults> {
|
||||||
|
// We could have a mode without this for power users in the future
|
||||||
|
static String toFtsQuery(String query) => query
|
||||||
|
.split(RegExp(r"\s+"))
|
||||||
|
.where((word) => word.isNotEmpty)
|
||||||
|
.map((word) => '"${word.replaceAll('"', '""')}"*')
|
||||||
|
.join(" ");
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<EventSearchResults> build() async {
|
||||||
|
if (info.query.trim().isEmpty) {
|
||||||
|
return (rowIds: IList<int>(), nextBatch: null);
|
||||||
|
}
|
||||||
|
|
||||||
|
final client = ref.read(ClientController.provider.notifier);
|
||||||
|
|
||||||
|
final encrypted = ref.read(
|
||||||
|
RoomsController.provider.select(
|
||||||
|
(rooms) => rooms[info.roomId]?.metadata?.encryptionEvent != null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final request = SearchRequest(
|
||||||
|
searchTerm: encrypted ? toFtsQuery(info.query) : info.query,
|
||||||
|
roomIds: .new([info.roomId]),
|
||||||
|
);
|
||||||
|
final response = encrypted
|
||||||
|
? await client.searchLocal(request)
|
||||||
|
: await client.searchServer(request);
|
||||||
|
|
||||||
|
if (!ref.mounted) return (rowIds: IList<int>(), nextBatch: null);
|
||||||
|
|
||||||
|
ref
|
||||||
|
.read(RoomsController.provider.notifier)
|
||||||
|
.storeEvents(response.events.addAll(response.relatedEvents));
|
||||||
|
|
||||||
|
return (
|
||||||
|
rowIds: response.events.map((event) => event.rowId).toIList(),
|
||||||
|
nextBatch: response.nextBatch,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> paginate() async {
|
||||||
|
if (state.isLoading) return;
|
||||||
|
|
||||||
|
final results = await future;
|
||||||
|
if (!ref.mounted || results.nextBatch == null) return;
|
||||||
|
|
||||||
|
state = .loading();
|
||||||
|
|
||||||
|
final encrypted = ref.read(
|
||||||
|
RoomsController.provider.select(
|
||||||
|
(rooms) => rooms[info.roomId]?.metadata?.encryptionEvent != null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final request = SearchRequest(
|
||||||
|
searchTerm: encrypted ? toFtsQuery(info.query) : info.query,
|
||||||
|
roomIds: .new([info.roomId]),
|
||||||
|
nextBatch: results.nextBatch,
|
||||||
|
);
|
||||||
|
|
||||||
|
final client = ref.read(ClientController.provider.notifier);
|
||||||
|
final response = encrypted
|
||||||
|
? await client.searchLocal(request)
|
||||||
|
: await client.searchServer(request);
|
||||||
|
|
||||||
|
if (!ref.mounted) return;
|
||||||
|
|
||||||
|
ref
|
||||||
|
.read(RoomsController.provider.notifier)
|
||||||
|
.storeEvents(response.events.addAll(response.relatedEvents));
|
||||||
|
|
||||||
|
state = .data((
|
||||||
|
rowIds: results.rowIds.addAll(
|
||||||
|
response.events.map((event) => event.rowId),
|
||||||
|
),
|
||||||
|
nextBatch: response.nextBatch,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
static final provider = AsyncNotifierProvider.family
|
||||||
|
.autoDispose<
|
||||||
|
EventSearchController,
|
||||||
|
EventSearchResults,
|
||||||
|
EventSearchParams
|
||||||
|
>(EventSearchController.new);
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import "package:collection/collection.dart";
|
|
||||||
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
import "package:nexus/controllers/client.dart";
|
import "package:nexus/controllers/client.dart";
|
||||||
|
|
@ -9,26 +8,6 @@ typedef NotificationsRequest = (UnreadType? unreadType, String? roomId);
|
||||||
|
|
||||||
class NotificationsController(final NotificationsRequest request)
|
class NotificationsController(final NotificationsRequest request)
|
||||||
extends AsyncNotifier<IList<(int, String)>> {
|
extends AsyncNotifier<IList<(int, String)>> {
|
||||||
void storeEvents(Iterable<Event> events) => ref
|
|
||||||
.read(RoomsController.provider.notifier)
|
|
||||||
.update(
|
|
||||||
.new(
|
|
||||||
events
|
|
||||||
.groupListsBy((event) => event.roomId)
|
|
||||||
.map(
|
|
||||||
(roomId, roomEvents) => .new(
|
|
||||||
roomId,
|
|
||||||
.new(
|
|
||||||
events: IMap.fromIterable(
|
|
||||||
roomEvents,
|
|
||||||
keyMapper: (event) => event.rowId,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<IList<(int, String)>> build() async {
|
Future<IList<(int, String)>> build() async {
|
||||||
final client = ref.read(ClientController.provider.notifier);
|
final client = ref.read(ClientController.provider.notifier);
|
||||||
|
|
@ -43,7 +22,9 @@ class NotificationsController(final NotificationsRequest request)
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
storeEvents(mentions.events.addAll(mentions.relatedEvents));
|
ref
|
||||||
|
.read(RoomsController.provider.notifier)
|
||||||
|
.storeEvents(mentions.events.addAll(mentions.relatedEvents));
|
||||||
|
|
||||||
return .new(mentions.events.map((event) => (event.rowId, event.roomId)));
|
return .new(mentions.events.map((event) => (event.rowId, event.roomId)));
|
||||||
}
|
}
|
||||||
|
|
@ -76,9 +57,11 @@ class NotificationsController(final NotificationsRequest request)
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
storeEvents(
|
ref
|
||||||
newNotifications.events.addAll(newNotifications.relatedEvents),
|
.read(RoomsController.provider.notifier)
|
||||||
);
|
.storeEvents(
|
||||||
|
newNotifications.events.addAll(newNotifications.relatedEvents),
|
||||||
|
);
|
||||||
|
|
||||||
return currentNotifications.addAll(
|
return currentNotifications.addAll(
|
||||||
newNotifications.events.map((event) => (event.rowId, event.roomId)),
|
newNotifications.events.map((event) => (event.rowId, event.roomId)),
|
||||||
|
|
|
||||||
|
|
@ -73,16 +73,7 @@ class RoomChatController(final ChatParams info)
|
||||||
.addAll(context.after);
|
.addAll(context.after);
|
||||||
ref
|
ref
|
||||||
.read(RoomsController.provider.notifier)
|
.read(RoomsController.provider.notifier)
|
||||||
.update(
|
.storeEvents(events.addAll(context.relatedEvents));
|
||||||
.new({
|
|
||||||
info.roomId: Room(
|
|
||||||
events: IMap.fromIterable(
|
|
||||||
events.addAll(context.relatedEvents),
|
|
||||||
keyMapper: (event) => event.rowId,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
return .new(
|
return .new(
|
||||||
timeline: .new(events.map((element) => element.rowId)),
|
timeline: .new(events.map((element) => element.rowId)),
|
||||||
hasMoreBackward: true,
|
hasMoreBackward: true,
|
||||||
|
|
@ -186,17 +177,8 @@ class RoomChatController(final ChatParams info)
|
||||||
|
|
||||||
ref
|
ref
|
||||||
.read(RoomsController.provider.notifier)
|
.read(RoomsController.provider.notifier)
|
||||||
.update(
|
.storeEvents(
|
||||||
.new({
|
paginationResponse.events.addAll(paginationResponse.relatedEvents),
|
||||||
info.roomId: Room(
|
|
||||||
events: IMap.fromIterable(
|
|
||||||
paginationResponse.events.addAll(
|
|
||||||
paginationResponse.relatedEvents,
|
|
||||||
),
|
|
||||||
keyMapper: (event) => event.rowId,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
final eventRowIds =
|
final eventRowIds =
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,26 @@ class RoomsController extends Notifier<IMap<String, Room>> {
|
||||||
.new(),
|
.new(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/// Adds events to their rooms, without changing the rooms' timelines.
|
||||||
|
void storeEvents(Iterable<Event> events) => update(
|
||||||
|
.new(
|
||||||
|
events
|
||||||
|
.groupListsBy((event) => event.roomId)
|
||||||
|
.map(
|
||||||
|
(roomId, roomEvents) => .new(
|
||||||
|
roomId,
|
||||||
|
Room(
|
||||||
|
// ignore: unused_result - I am using it you donkey
|
||||||
|
events: .fromIterable(
|
||||||
|
roomEvents,
|
||||||
|
keyMapper: (event) => event.rowId,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
void update(
|
void update(
|
||||||
IMap<String, Room> rooms, [
|
IMap<String, Room> rooms, [
|
||||||
ISet<String> leftRooms = const .empty(),
|
ISet<String> leftRooms = const .empty(),
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ part "paginate_manual.g.dart";
|
||||||
@Freezed(toJson: false, fromJson: false)
|
@Freezed(toJson: false, fromJson: false)
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class const PaginateManual({
|
class const PaginateManual({
|
||||||
required final IList<Event> events,
|
final IList<Event> events = const .empty(),
|
||||||
final IList<Event> relatedEvents = const .empty(),
|
final IList<Event> relatedEvents = const .empty(),
|
||||||
required final String? nextBatch,
|
required final String? nextBatch,
|
||||||
}) with _$PaginateManual {
|
}) with _$PaginateManual {
|
||||||
|
|
|
||||||
21
lib/models/requests/search.dart
Normal file
21
lib/models/requests/search.dart
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||||
|
import "package:freezed_annotation/freezed_annotation.dart";
|
||||||
|
|
||||||
|
part "search.freezed.dart";
|
||||||
|
part "search.g.dart";
|
||||||
|
|
||||||
|
@Freezed(toJson: false, fromJson: false)
|
||||||
|
@JsonSerializable(includeIfNull: false)
|
||||||
|
class const SearchRequest({
|
||||||
|
required final String searchTerm,
|
||||||
|
final IList<String> roomIds = const .empty(),
|
||||||
|
final int limit = 20,
|
||||||
|
final bool sortByTime = true,
|
||||||
|
|
||||||
|
final String? nextBatch,
|
||||||
|
}) with _$SearchRequest {
|
||||||
|
Map<String, Object?> toJson() => _$SearchRequestToJson(this);
|
||||||
|
|
||||||
|
factory SearchRequest.fromJson(Map<String, Object?> json) =>
|
||||||
|
_$SearchRequestFromJson(json);
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import "package:freezed_annotation/freezed_annotation.dart";
|
import "package:freezed_annotation/freezed_annotation.dart";
|
||||||
|
import "package:nexus/models/content/encryption.dart";
|
||||||
import "package:nexus/models/epoch_date_time_converter.dart";
|
import "package:nexus/models/epoch_date_time_converter.dart";
|
||||||
import "package:nexus/models/lazy_load_summary.dart";
|
import "package:nexus/models/lazy_load_summary.dart";
|
||||||
|
|
||||||
|
|
@ -15,6 +16,8 @@ class const RoomMetadata({
|
||||||
final String? topic,
|
final String? topic,
|
||||||
final String? canonicalAlias,
|
final String? canonicalAlias,
|
||||||
final LazyLoadSummary? lazyLoadSummary,
|
final LazyLoadSummary? lazyLoadSummary,
|
||||||
|
|
||||||
|
final EncryptionContent? encryptionEvent,
|
||||||
required final bool hasMemberList,
|
required final bool hasMemberList,
|
||||||
@JsonKey(name: "preview_event_rowid") required final int previewEventRowID,
|
@JsonKey(name: "preview_event_rowid") required final int previewEventRowID,
|
||||||
@EpochDateTimeConverter() required final DateTime sortingTimestamp,
|
@EpochDateTimeConverter() required final DateTime sortingTimestamp,
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ final class const RoomAppbar({
|
||||||
required final void Function() onOpenDrawer,
|
required final void Function() onOpenDrawer,
|
||||||
final void Function(BuildContext context)? onOpenMemberList,
|
final void Function(BuildContext context)? onOpenMemberList,
|
||||||
final void Function()? onOpenPinnedMessagesList,
|
final void Function()? onOpenPinnedMessagesList,
|
||||||
|
final void Function()? onOpenSearch,
|
||||||
super.key,
|
super.key,
|
||||||
}) extends ConsumerWidget implements PreferredSizeWidget {
|
}) extends ConsumerWidget implements PreferredSizeWidget {
|
||||||
@override
|
@override
|
||||||
|
|
@ -61,6 +62,11 @@ final class const RoomAppbar({
|
||||||
room == null
|
room == null
|
||||||
? []
|
? []
|
||||||
: [
|
: [
|
||||||
|
IconButton(
|
||||||
|
onPressed: onOpenSearch?.call,
|
||||||
|
icon: Icon(Icons.search),
|
||||||
|
tooltip: "Search this room",
|
||||||
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: onOpenPinnedMessagesList?.call,
|
onPressed: onOpenPinnedMessagesList?.call,
|
||||||
icon: Icon(Icons.push_pin),
|
icon: Icon(Icons.push_pin),
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import "package:nexus/widgets/loading.dart";
|
||||||
import "package:nexus/widgets/pinned_events_drawer.dart";
|
import "package:nexus/widgets/pinned_events_drawer.dart";
|
||||||
import "package:nexus/widgets/member_list.dart";
|
import "package:nexus/widgets/member_list.dart";
|
||||||
import "package:nexus/widgets/room_appbar.dart";
|
import "package:nexus/widgets/room_appbar.dart";
|
||||||
|
import "package:nexus/widgets/search_drawer.dart";
|
||||||
import "package:nexus/widgets/sync_status_banner.dart";
|
import "package:nexus/widgets/sync_status_banner.dart";
|
||||||
import "package:nexus/main.dart";
|
import "package:nexus/main.dart";
|
||||||
import "package:nexus/widgets/room_chat/chat_timeline.dart";
|
import "package:nexus/widgets/room_chat/chat_timeline.dart";
|
||||||
|
|
@ -198,115 +199,132 @@ final class const RoomChat({
|
||||||
);
|
);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
endDrawer: PinnedEventsDrawer(
|
endDrawer: SearchDrawer(
|
||||||
roomId,
|
roomId,
|
||||||
getEventOptions: getEventOptions,
|
getEventOptions: getEventOptions,
|
||||||
jumpToEvent: jumpToEvent,
|
jumpToEvent: jumpToEvent,
|
||||||
),
|
),
|
||||||
body: Builder(
|
body: Builder(
|
||||||
builder: (middleContext) => Scaffold(
|
builder: (searchContext) => Scaffold(
|
||||||
endDrawer: showMembersByDefault ? null : MemberList(roomId),
|
endDrawer: PinnedEventsDrawer(
|
||||||
appBar: RoomAppbar(
|
roomId,
|
||||||
roomId: roomId,
|
getEventOptions: getEventOptions,
|
||||||
isDesktop: isDesktop,
|
jumpToEvent: jumpToEvent,
|
||||||
onOpenDrawer: Scaffold.of(context).openDrawer,
|
|
||||||
onOpenMemberList: (thisContext) {
|
|
||||||
ref
|
|
||||||
.read(MemberListOpenedController.provider.notifier)
|
|
||||||
.set(!memberListOpened);
|
|
||||||
Scaffold.of(thisContext).openEndDrawer();
|
|
||||||
},
|
|
||||||
onOpenPinnedMessagesList: Scaffold.of(middleContext).openEndDrawer,
|
|
||||||
),
|
),
|
||||||
body: Column(
|
body: Builder(
|
||||||
children: [
|
builder: (middleContext) => Scaffold(
|
||||||
const SyncStatusBanner(),
|
endDrawer: showMembersByDefault ? null : MemberList(roomId),
|
||||||
Expanded(
|
appBar: RoomAppbar(
|
||||||
child: Row(
|
roomId: roomId,
|
||||||
children: [
|
isDesktop: isDesktop,
|
||||||
Expanded(
|
onOpenDrawer: Scaffold.of(context).openDrawer,
|
||||||
child: Stack(
|
onOpenMemberList: (thisContext) {
|
||||||
children: [
|
ref
|
||||||
Positioned.fill(
|
.read(MemberListOpenedController.provider.notifier)
|
||||||
child: Padding(
|
.set(!memberListOpened);
|
||||||
padding: .symmetric(horizontal: 4),
|
Scaffold.of(thisContext).openEndDrawer();
|
||||||
child: switch (controllerData) {
|
},
|
||||||
AsyncLoading(value: _?) ||
|
onOpenPinnedMessagesList: Scaffold.of(middleContext)
|
||||||
AsyncData(value: _) => ChatTimeline(
|
.openEndDrawer,
|
||||||
scroll: scroll,
|
onOpenSearch: Scaffold.of(searchContext).openEndDrawer,
|
||||||
roomId: roomId,
|
|
||||||
jumpToEvent: jumpToEvent,
|
|
||||||
getEventOptions: getEventOptions,
|
|
||||||
onSwipe: swipeToReply && canReply
|
|
||||||
? (rowId) => onRelation(rowId, .reply)
|
|
||||||
: null,
|
|
||||||
highlightedEvent: highlightedEvent.value,
|
|
||||||
composerHeight: composerSize.value,
|
|
||||||
),
|
|
||||||
AsyncLoading _ => Loading(),
|
|
||||||
AsyncError(:final error, :final stackTrace) =>
|
|
||||||
ErrorDialog(error, stackTrace),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Positioned(
|
|
||||||
right: 16,
|
|
||||||
bottom: composerSize.value,
|
|
||||||
child: IgnorePointer(
|
|
||||||
ignoring: scroll.atBottom,
|
|
||||||
child: AnimatedOpacity(
|
|
||||||
opacity: scroll.atBottom ? 0 : 1,
|
|
||||||
duration: Durations.short4,
|
|
||||||
child: FloatingActionButton.small(
|
|
||||||
onPressed: scroll.jumpToBottom,
|
|
||||||
child: const Icon(Icons.keyboard_arrow_down),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Positioned(
|
|
||||||
bottom: 0,
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
child: MeasureSize(
|
|
||||||
onChange: (size) =>
|
|
||||||
composerSize.value = size.height,
|
|
||||||
child: Composer(
|
|
||||||
roomId,
|
|
||||||
node: composerNode,
|
|
||||||
onSend:
|
|
||||||
(
|
|
||||||
text, {
|
|
||||||
required shouldMention,
|
|
||||||
required tags,
|
|
||||||
}) => notifier
|
|
||||||
.send(
|
|
||||||
text,
|
|
||||||
tags: tags,
|
|
||||||
relationType: relationType.value,
|
|
||||||
shouldMention: shouldMention,
|
|
||||||
relatedEventRowId: relatedEvent.value,
|
|
||||||
)
|
|
||||||
.onError(showError),
|
|
||||||
relationType: relationType.value,
|
|
||||||
relatedEvent: relatedEvent.value,
|
|
||||||
onDismiss: () {
|
|
||||||
relatedEvent.value = null;
|
|
||||||
composerNode.requestFocus();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
if (memberListOpened && showMembersByDefault)
|
|
||||||
MemberList(roomId),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
body: Column(
|
||||||
|
children: [
|
||||||
|
const SyncStatusBanner(),
|
||||||
|
Expanded(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
Positioned.fill(
|
||||||
|
child: Padding(
|
||||||
|
padding: .symmetric(horizontal: 4),
|
||||||
|
child: switch (controllerData) {
|
||||||
|
AsyncLoading(value: _?) ||
|
||||||
|
AsyncData(value: _) => ChatTimeline(
|
||||||
|
scroll: scroll,
|
||||||
|
roomId: roomId,
|
||||||
|
jumpToEvent: jumpToEvent,
|
||||||
|
getEventOptions: getEventOptions,
|
||||||
|
onSwipe: swipeToReply && canReply
|
||||||
|
? (rowId) => onRelation(rowId, .reply)
|
||||||
|
: null,
|
||||||
|
highlightedEvent: highlightedEvent.value,
|
||||||
|
composerHeight: composerSize.value,
|
||||||
|
),
|
||||||
|
AsyncLoading _ => Loading(),
|
||||||
|
AsyncError(
|
||||||
|
:final error,
|
||||||
|
:final stackTrace,
|
||||||
|
) =>
|
||||||
|
ErrorDialog(error, stackTrace),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
right: 16,
|
||||||
|
bottom: composerSize.value,
|
||||||
|
child: IgnorePointer(
|
||||||
|
ignoring: scroll.atBottom,
|
||||||
|
child: AnimatedOpacity(
|
||||||
|
opacity: scroll.atBottom ? 0 : 1,
|
||||||
|
duration: Durations.short4,
|
||||||
|
child: FloatingActionButton.small(
|
||||||
|
onPressed: scroll.jumpToBottom,
|
||||||
|
child: const Icon(
|
||||||
|
Icons.keyboard_arrow_down,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
bottom: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
child: MeasureSize(
|
||||||
|
onChange: (size) =>
|
||||||
|
composerSize.value = size.height,
|
||||||
|
child: Composer(
|
||||||
|
roomId,
|
||||||
|
node: composerNode,
|
||||||
|
onSend:
|
||||||
|
(
|
||||||
|
text, {
|
||||||
|
required shouldMention,
|
||||||
|
required tags,
|
||||||
|
}) => notifier
|
||||||
|
.send(
|
||||||
|
text,
|
||||||
|
tags: tags,
|
||||||
|
relationType: relationType.value,
|
||||||
|
shouldMention: shouldMention,
|
||||||
|
relatedEventRowId:
|
||||||
|
relatedEvent.value,
|
||||||
|
)
|
||||||
|
.onError(showError),
|
||||||
|
relationType: relationType.value,
|
||||||
|
relatedEvent: relatedEvent.value,
|
||||||
|
onDismiss: () {
|
||||||
|
relatedEvent.value = null;
|
||||||
|
composerNode.requestFocus();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
if (memberListOpened && showMembersByDefault)
|
||||||
|
MemberList(roomId),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
147
lib/widgets/search_drawer.dart
Normal file
147
lib/widgets/search_drawer.dart
Normal file
|
|
@ -0,0 +1,147 @@
|
||||||
|
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||||
|
import "package:flutter_hooks/flutter_hooks.dart";
|
||||||
|
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||||
|
import "package:material_ui/material_ui.dart";
|
||||||
|
import "package:nexus/controllers/event_search.dart";
|
||||||
|
import "package:nexus/helpers/hooks/chat_scroll.dart";
|
||||||
|
import "package:nexus/main.dart";
|
||||||
|
import "package:nexus/models/event.dart";
|
||||||
|
import "package:nexus/widgets/error_dialog.dart";
|
||||||
|
import "package:nexus/widgets/loading.dart";
|
||||||
|
import "package:nexus/widgets/renderers/event.dart";
|
||||||
|
|
||||||
|
final class const SearchDrawer(
|
||||||
|
final String roomId, {
|
||||||
|
required final IList<PopupMenuEntry> Function(Event event) getEventOptions,
|
||||||
|
required final JumpToEvent jumpToEvent,
|
||||||
|
super.key,
|
||||||
|
}) extends HookConsumerWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final controller = useTextEditingController();
|
||||||
|
final query = useState("");
|
||||||
|
final provider = EventSearchController.provider((
|
||||||
|
roomId: roomId,
|
||||||
|
query: query.value,
|
||||||
|
));
|
||||||
|
final results = ref.watch(provider);
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
|
void search() {
|
||||||
|
final text = controller.text.trim();
|
||||||
|
|
||||||
|
// The provider is keyed on the query, so searching again needs a refetch
|
||||||
|
if (text == query.value) {
|
||||||
|
ref.invalidate(provider, asReload: true);
|
||||||
|
} else {
|
||||||
|
query.value = text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget placeholder(IconData icon, String text) => Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: .min,
|
||||||
|
children: [
|
||||||
|
Icon(icon, size: 48, color: theme.colorScheme.onSurface),
|
||||||
|
SizedBox(height: 12),
|
||||||
|
Text(text, style: theme.textTheme.headlineSmall),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return Drawer(
|
||||||
|
width: 400,
|
||||||
|
child: Scaffold(
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
appBar: AppBar(
|
||||||
|
scrolledUnderElevation: 0,
|
||||||
|
leading: Icon(Icons.search),
|
||||||
|
title: Text("Search"),
|
||||||
|
actionsPadding: .only(right: 4),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
onPressed: Scaffold.of(context).closeEndDrawer,
|
||||||
|
icon: Icon(Icons.close),
|
||||||
|
tooltip: "Close search",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: Column(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: .symmetric(horizontal: 12, vertical: 4),
|
||||||
|
child: TextField(
|
||||||
|
controller: controller,
|
||||||
|
autofocus: true,
|
||||||
|
textInputAction: .search,
|
||||||
|
onSubmitted: (_) => search(),
|
||||||
|
decoration: .new(
|
||||||
|
hintText: "Search this room",
|
||||||
|
prefixIcon: Icon(Icons.search),
|
||||||
|
suffixIcon: IconButton(
|
||||||
|
onPressed: search,
|
||||||
|
icon: Icon(Icons.arrow_forward),
|
||||||
|
tooltip: "Search",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: query.value.isEmpty
|
||||||
|
? placeholder(Icons.search, "Search for events")
|
||||||
|
: switch (results) {
|
||||||
|
AsyncData(:final value) when value.rowIds.isEmpty =>
|
||||||
|
placeholder(Icons.search_off, "No results"),
|
||||||
|
AsyncData(:final value) ||
|
||||||
|
AsyncLoading(:final value?) => ListView.builder(
|
||||||
|
padding: .all(8),
|
||||||
|
itemCount:
|
||||||
|
value.rowIds.length +
|
||||||
|
(value.nextBatch == null ? 0 : 1),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
if (index == value.rowIds.length) {
|
||||||
|
return Center(
|
||||||
|
child: TextButton(
|
||||||
|
onPressed: results.isLoading
|
||||||
|
? null
|
||||||
|
: () => ref
|
||||||
|
.read(provider.notifier)
|
||||||
|
.paginate()
|
||||||
|
.onError(showError),
|
||||||
|
child: Text("Load more"),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final rowId = value.rowIds[index];
|
||||||
|
return InkWell(
|
||||||
|
borderRadius: .circular(12),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
jumpToEvent(rowId);
|
||||||
|
},
|
||||||
|
child: Padding(
|
||||||
|
padding: .symmetric(vertical: 4),
|
||||||
|
child: IgnorePointer(
|
||||||
|
child: EventRenderer(
|
||||||
|
rowId,
|
||||||
|
roomId: roomId,
|
||||||
|
maxLines: 2,
|
||||||
|
getEventOptions: getEventOptions,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
AsyncLoading() => Loading(),
|
||||||
|
AsyncError(:final error, :final stackTrace) =>
|
||||||
|
ErrorDialog(error, stackTrace),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue