parent
427e06be3e
commit
a54004736b
3 changed files with 427 additions and 143 deletions
|
|
@ -16,6 +16,7 @@ final class const RoomAppbar({
|
|||
required final void Function() onOpenDrawer,
|
||||
final void Function(BuildContext context)? onOpenMemberList,
|
||||
final void Function()? onOpenPinnedMessagesList,
|
||||
final void Function()? onOpenSearch,
|
||||
super.key,
|
||||
}) extends ConsumerWidget implements PreferredSizeWidget {
|
||||
@override
|
||||
|
|
@ -61,6 +62,11 @@ final class const RoomAppbar({
|
|||
room == null
|
||||
? []
|
||||
: [
|
||||
IconButton(
|
||||
onPressed: onOpenSearch?.call,
|
||||
icon: Icon(Icons.search),
|
||||
tooltip: "Search this room",
|
||||
),
|
||||
IconButton(
|
||||
onPressed: onOpenPinnedMessagesList?.call,
|
||||
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/member_list.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/main.dart";
|
||||
import "package:nexus/widgets/room_chat/chat_timeline.dart";
|
||||
|
|
@ -198,115 +199,132 @@ final class const RoomChat({
|
|||
);
|
||||
|
||||
return Scaffold(
|
||||
endDrawer: PinnedEventsDrawer(
|
||||
endDrawer: SearchDrawer(
|
||||
roomId,
|
||||
getEventOptions: getEventOptions,
|
||||
jumpToEvent: jumpToEvent,
|
||||
),
|
||||
body: Builder(
|
||||
builder: (middleContext) => Scaffold(
|
||||
endDrawer: showMembersByDefault ? null : MemberList(roomId),
|
||||
appBar: RoomAppbar(
|
||||
roomId: roomId,
|
||||
isDesktop: isDesktop,
|
||||
onOpenDrawer: Scaffold.of(context).openDrawer,
|
||||
onOpenMemberList: (thisContext) {
|
||||
ref
|
||||
.read(MemberListOpenedController.provider.notifier)
|
||||
.set(!memberListOpened);
|
||||
Scaffold.of(thisContext).openEndDrawer();
|
||||
},
|
||||
onOpenPinnedMessagesList: Scaffold.of(middleContext).openEndDrawer,
|
||||
builder: (searchContext) => Scaffold(
|
||||
endDrawer: PinnedEventsDrawer(
|
||||
roomId,
|
||||
getEventOptions: getEventOptions,
|
||||
jumpToEvent: jumpToEvent,
|
||||
),
|
||||
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),
|
||||
],
|
||||
),
|
||||
body: Builder(
|
||||
builder: (middleContext) => Scaffold(
|
||||
endDrawer: showMembersByDefault ? null : MemberList(roomId),
|
||||
appBar: RoomAppbar(
|
||||
roomId: roomId,
|
||||
isDesktop: isDesktop,
|
||||
onOpenDrawer: Scaffold.of(context).openDrawer,
|
||||
onOpenMemberList: (thisContext) {
|
||||
ref
|
||||
.read(MemberListOpenedController.provider.notifier)
|
||||
.set(!memberListOpened);
|
||||
Scaffold.of(thisContext).openEndDrawer();
|
||||
},
|
||||
onOpenPinnedMessagesList: Scaffold.of(middleContext)
|
||||
.openEndDrawer,
|
||||
onOpenSearch: Scaffold.of(searchContext).openEndDrawer,
|
||||
),
|
||||
],
|
||||
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