reorganize and rename widgets
This commit is contained in:
parent
05d25869ef
commit
d5f29dc344
3 changed files with 568 additions and 547 deletions
125
lib/widgets/threads/thread_drawer.dart
Normal file
125
lib/widgets/threads/thread_drawer.dart
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
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/client.dart";
|
||||
import "package:nexus/controllers/room_chat.dart";
|
||||
import "package:nexus/controllers/rooms.dart";
|
||||
import "package:nexus/helpers/extensions/build_event_options.dart";
|
||||
import "package:nexus/helpers/hooks/chat_composer.dart";
|
||||
import "package:nexus/helpers/hooks/chat_scroll.dart";
|
||||
import "package:nexus/widgets/room_chat/chat_body.dart";
|
||||
|
||||
/// The thread from [threadRoot], with a composer to send to it.
|
||||
final class const ThreadDrawer(
|
||||
final String roomId, {
|
||||
required final String threadRoot,
|
||||
|
||||
/// An event in the thread to scroll to once it loads.
|
||||
final int? initialEvent,
|
||||
required final String userId,
|
||||
|
||||
/// Jumps to events outside this thread, like ones replied to from it.
|
||||
required final JumpToEvent jumpToRoomEvent,
|
||||
super.key,
|
||||
}) extends HookConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final composer = ChatComposer.use(ref, (
|
||||
roomId: roomId,
|
||||
threadRoot: threadRoot,
|
||||
));
|
||||
final contextualEvent = useState(initialEvent);
|
||||
|
||||
final ChatParams chat = (
|
||||
roomId: roomId,
|
||||
contextualEvent: contextualEvent.value,
|
||||
threadRoot: threadRoot,
|
||||
);
|
||||
final controllerProvider = RoomChatController.provider(chat);
|
||||
final controllerData = ref.watch(controllerProvider);
|
||||
|
||||
final scroll = ChatScroll.use(
|
||||
controllerData: controllerData,
|
||||
paginate: ref.watch(controllerProvider.notifier).paginate,
|
||||
contextualEvent: contextualEvent,
|
||||
markRead: () async {
|
||||
final room = ref.read(
|
||||
RoomsController.provider.select((rooms) => rooms[roomId]),
|
||||
);
|
||||
|
||||
if (room != null) {
|
||||
await ref
|
||||
.read(ClientController.provider.notifier)
|
||||
.markRead(room, threadRoot: threadRoot);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
final initialEventLoaded =
|
||||
controllerData.value?.timeline.contains(initialEvent) ?? false;
|
||||
// Only once, so returning to the newest events doesn't jump back to it
|
||||
final jumpedToInitialEvent = useRef(false);
|
||||
useEffect(() {
|
||||
if (initialEvent case final rowId?
|
||||
when initialEventLoaded && !jumpedToInitialEvent.value) {
|
||||
jumpedToInitialEvent.value = true;
|
||||
WidgetsBinding.instance.addPostFrameCallback(
|
||||
(_) => scroll.jumpToEvent(rowId),
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [initialEventLoaded]);
|
||||
|
||||
// This chat only has the thread, so other events are left to the room.
|
||||
// Ones in the thread that aren't loaded are loaded around, like the room.
|
||||
Future<void> jumpToEvent(int rowId) async {
|
||||
final isInThread =
|
||||
ref
|
||||
.read(RoomsController.provider)[roomId]
|
||||
?.events[rowId]
|
||||
?.threadRoot ==
|
||||
threadRoot;
|
||||
if (isInThread ||
|
||||
(controllerData.value?.timeline.contains(rowId) ?? false)) {
|
||||
return scroll.jumpToEvent(rowId);
|
||||
}
|
||||
|
||||
Scaffold.of(context).closeEndDrawer();
|
||||
await jumpToRoomEvent(rowId);
|
||||
}
|
||||
|
||||
return Drawer(
|
||||
width: 400,
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.transparent,
|
||||
appBar: AppBar(
|
||||
scrolledUnderElevation: 0,
|
||||
leading: Icon(Icons.forum),
|
||||
title: Text("Thread"),
|
||||
actionsPadding: .only(right: 4),
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: Scaffold.of(context).closeEndDrawer,
|
||||
icon: Icon(Icons.close),
|
||||
tooltip: "Close thread",
|
||||
),
|
||||
],
|
||||
),
|
||||
body: ChatBody(
|
||||
chat: chat,
|
||||
scroll: scroll,
|
||||
jumpToEvent: jumpToEvent,
|
||||
getEventOptions: (event) => event.buildEventOptions(
|
||||
context: context,
|
||||
ref: ref,
|
||||
roomId: roomId,
|
||||
userId: userId,
|
||||
onRelation: composer.relate,
|
||||
),
|
||||
composer: composer,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
88
lib/widgets/threads/thread_summary.dart
Normal file
88
lib/widgets/threads/thread_summary.dart
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||
import "package:material_ui/material_ui.dart";
|
||||
import "package:nexus/controllers/rooms.dart";
|
||||
import "package:nexus/helpers/extensions/thread_events.dart";
|
||||
import "package:nexus/models/event.dart";
|
||||
import "package:nexus/widgets/lazy_loading/message_avatar.dart";
|
||||
import "package:nexus/widgets/lazy_loading/message_displayname.dart";
|
||||
import "package:nexus/widgets/renderers/event.dart";
|
||||
|
||||
/// How many replies are in the thread from [root], and the latest one, or nothing if it has none.
|
||||
class const ThreadSummary(
|
||||
final Event root, {
|
||||
required final VoidCallback onTap,
|
||||
super.key,
|
||||
}) extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
|
||||
final thread = ref.watch(
|
||||
RoomsController.provider.select(
|
||||
(rooms) => rooms[root.roomId]?.thread(root),
|
||||
),
|
||||
);
|
||||
|
||||
if (thread case (:final count, :final latest) when count > 0) {
|
||||
return Card(
|
||||
margin: .only(top: 8),
|
||||
color: colorScheme.surfaceContainerHigh,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: .symmetric(vertical: 12, horizontal: 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: .start,
|
||||
spacing: 8,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisSize: .min,
|
||||
spacing: 6,
|
||||
children: [
|
||||
Icon(Icons.forum, size: 16, color: colorScheme.primary),
|
||||
Text(
|
||||
"$count ${count == 1 ? "reply" : "replies"}",
|
||||
style: theme.textTheme.labelLarge?.copyWith(
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
Icons.chevron_right,
|
||||
size: 16,
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
],
|
||||
),
|
||||
if (latest != null)
|
||||
IgnorePointer(
|
||||
child: Row(
|
||||
mainAxisSize: .min,
|
||||
spacing: 8,
|
||||
children: [
|
||||
MessageAvatar(latest, height: 24),
|
||||
Flexible(
|
||||
child: MessageDisplayname(latest, shouldWrap: false),
|
||||
),
|
||||
Flexible(
|
||||
flex: 2,
|
||||
child: EventRenderer(
|
||||
latest.rowId,
|
||||
roomId: latest.roomId,
|
||||
textOnly: true,
|
||||
maxLines: 1,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return SizedBox.shrink();
|
||||
}
|
||||
}
|
||||
157
lib/widgets/threads/threads_drawer.dart
Normal file
157
lib/widgets/threads/threads_drawer.dart
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||
import "package:material_ui/material_ui.dart";
|
||||
import "package:nexus/controllers/thread_list.dart";
|
||||
import "package:nexus/controllers/rooms.dart";
|
||||
import "package:nexus/helpers/extensions/thread_events.dart";
|
||||
import "package:nexus/main.dart";
|
||||
import "package:nexus/widgets/common/error_dialog.dart";
|
||||
import "package:nexus/widgets/common/loading.dart";
|
||||
import "package:nexus/widgets/renderers/event_preview.dart";
|
||||
import "package:nexus/widgets/common/timestamp.dart";
|
||||
|
||||
final class const ThreadsDrawer(
|
||||
final String roomId, {
|
||||
required final void Function(String threadRoot) onOpenThread,
|
||||
super.key,
|
||||
}) extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final threads = ref.watch(
|
||||
RoomsController.provider.select(
|
||||
(rooms) => rooms[roomId]?.threads ?? const .empty(),
|
||||
),
|
||||
);
|
||||
final serverProvider = ThreadListController.provider(roomId);
|
||||
final server = ref.watch(serverProvider);
|
||||
final theme = Theme.of(context);
|
||||
|
||||
void open(String threadRoot) {
|
||||
Navigator.of(context).pop();
|
||||
onOpenThread(threadRoot);
|
||||
}
|
||||
|
||||
return Drawer(
|
||||
width: 400,
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.transparent,
|
||||
appBar: AppBar(
|
||||
scrolledUnderElevation: 0,
|
||||
leading: Icon(Icons.forum),
|
||||
title: Text("Threads"),
|
||||
actionsPadding: .only(right: 4),
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: Scaffold.of(context).closeEndDrawer,
|
||||
icon: Icon(Icons.close),
|
||||
tooltip: "Close threads",
|
||||
),
|
||||
],
|
||||
),
|
||||
body: switch (server) {
|
||||
AsyncError(:final error, :final stackTrace) => ErrorDialog(
|
||||
error,
|
||||
stackTrace,
|
||||
),
|
||||
AsyncValue(hasValue: true, value: final nextBatch)
|
||||
when threads.isNotEmpty =>
|
||||
ListView.builder(
|
||||
padding: .all(8),
|
||||
itemCount: threads.length + (nextBatch == null ? 0 : 1),
|
||||
itemBuilder: (context, index) {
|
||||
if (index == threads.length) {
|
||||
return Center(
|
||||
child: TextButton(
|
||||
onPressed: server.isLoading
|
||||
? null
|
||||
: () => ref
|
||||
.read(serverProvider.notifier)
|
||||
.loadMore()
|
||||
.onError(showError),
|
||||
child: Text("Load more"),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final (:root, :info) = threads[index];
|
||||
return Card(
|
||||
margin: .symmetric(vertical: 4),
|
||||
color: theme.colorScheme.surfaceContainerHigh,
|
||||
clipBehavior: .antiAlias,
|
||||
child: InkWell(
|
||||
onTap: () => open(root.eventId),
|
||||
child: Padding(
|
||||
padding: .symmetric(vertical: 8, horizontal: 12),
|
||||
child: Column(
|
||||
crossAxisAlignment: .start,
|
||||
spacing: 4,
|
||||
children: [
|
||||
EventPreview(root.rowId, roomId: roomId),
|
||||
Row(
|
||||
spacing: 6,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.forum,
|
||||
size: 16,
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
Text(
|
||||
"${info.count} ${info.count == 1 ? "reply" : "replies"}",
|
||||
style: theme.textTheme.labelLarge?.copyWith(
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
),
|
||||
if (info.latest case final latest?)
|
||||
Expanded(
|
||||
child: Row(
|
||||
mainAxisAlignment: .end,
|
||||
spacing: 4,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
"Last active:",
|
||||
maxLines: 1,
|
||||
overflow: .ellipsis,
|
||||
style: theme.textTheme.labelSmall
|
||||
?.copyWith(color: Colors.grey),
|
||||
),
|
||||
),
|
||||
Timestamp(latest.timestamp),
|
||||
],
|
||||
),
|
||||
)
|
||||
else
|
||||
Spacer(),
|
||||
Icon(
|
||||
Icons.chevron_right,
|
||||
size: 16,
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
AsyncLoading() => Loading(),
|
||||
AsyncData() => Center(
|
||||
child: Column(
|
||||
mainAxisSize: .min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.forum_outlined,
|
||||
size: 48,
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
SizedBox(height: 12),
|
||||
Text("No threads", style: theme.textTheme.headlineSmall),
|
||||
],
|
||||
),
|
||||
),
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue