nexus/lib/widgets/room_chat/room_chat.dart

282 lines
9.5 KiB
Dart

import "package:fast_immutable_collections/fast_immutable_collections.dart";
import "package:material_ui/material_ui.dart";
import "package:flutter/services.dart";
import "package:flutter_hooks/flutter_hooks.dart";
import "package:hooks_riverpod/hooks_riverpod.dart";
import "package:measure_size/measure_size.dart";
import "package:nexus/controllers/client.dart";
import "package:nexus/controllers/client_state.dart";
import "package:nexus/controllers/member_list_opened.dart";
import "package:nexus/controllers/rooms.dart";
import "package:nexus/controllers/room_chat.dart";
import "package:nexus/helpers/hooks/chat_scroll.dart";
import "package:nexus/models/event.dart";
import "package:nexus/models/relation_type.dart";
import "package:nexus/widgets/composer/composer.dart";
import "package:nexus/widgets/error_dialog.dart";
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/main.dart";
import "package:nexus/widgets/room_chat/chat_timeline.dart";
import "package:nexus/helpers/extensions/build_event_options.dart";
import "package:nexus/controllers/jump_request.dart";
final class const RoomChat({
required final String? roomId,
required final bool isDesktop,
required final bool showMembersByDefault,
super.key,
}) extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final relatedEvent = useState<int?>(null);
final relationType = useState(RelationType.reply);
final pendingJump = ref.read(JumpRequestController.provider);
final contextualEvent = useState<int?>(
pendingJump != null && pendingJump.roomId == this.roomId
? pendingJump.rowId
: null,
);
final highlightedEvent = useState<int?>(null);
final composerSize = useState<double>(64);
final userId = ref.watch(ClientStateController.provider)?.userId;
final memberListOpened = ref
.watch(MemberListOpenedController.provider)
.requireValue;
final theme = Theme.of(context);
final nothing = Center(
child: Text(
"Nothing to see here...",
style: theme.textTheme.headlineMedium,
),
);
if (userId == null || this.roomId == null) {
return Scaffold(
appBar: RoomAppbar(
roomId: this.roomId,
isDesktop: isDesktop,
onOpenDrawer: Scaffold.of(context).openDrawer,
),
body: nothing,
);
}
final roomId = this.roomId!;
final jumpRowId = ref.watch(
JumpRequestController.provider.select(
(request) => request?.roomId == roomId ? request?.rowId : null,
),
);
final controllerProvider = RoomChatController.provider((
roomId: roomId,
contextualEvent: contextualEvent.value,
));
final notifier = ref.watch(controllerProvider.notifier);
final client = ref.read(ClientController.provider.notifier);
final controllerData = ref.watch(controllerProvider);
final scroll = ChatScroll.use(
controllerData: controllerData,
paginate: notifier.paginate,
contextualEvent: contextualEvent,
markRead: () async {
final room = ref.read(
RoomsController.provider.select((rooms) => rooms[roomId]),
);
if (room != null) {
await client.markRead(room);
}
},
);
final lastComposerHeight = useRef(composerSize.value);
useEffect(() {
final delta = composerSize.value - lastComposerHeight.value;
lastComposerHeight.value = composerSize.value;
if (delta != 0) {
final wasAtBottom = scroll.atBottom;
WidgetsBinding.instance.addPostFrameCallback((_) {
final controller = scroll.scrollController;
if (!controller.hasClients) return;
final position = controller.position;
controller.jumpTo(
wasAtBottom ? position.minScrollExtent : position.pixels - delta,
);
});
}
return null;
}, [composerSize.value]);
final composerNode = useFocusNode(
onKeyEvent: (_, event) {
if (event is KeyDownEvent && event.logicalKey == .escape) {
relatedEvent.value = null;
return .handled;
}
return .ignored;
},
);
Future<void> jumpToEvent(int rowId) async {
highlightedEvent.value = rowId;
await scroll.jumpToEvent(rowId);
await Future.delayed(.new(milliseconds: 700), () {
if (highlightedEvent.value == rowId) {
highlightedEvent.value = null;
}
});
}
useEffect(() {
if (jumpRowId == null) return null;
void check() {
if (!context.mounted) return;
if (scroll.scrollController.hasClients) {
jumpToEvent(jumpRowId);
Future.microtask(
() => ref.read(JumpRequestController.provider.notifier).consume(),
);
} else {
WidgetsBinding.instance.addPostFrameCallback((_) => check());
}
}
check();
return null;
}, [jumpRowId]);
IList<PopupMenuEntry> getEventOptions(Event event) =>
event.buildEventOptions(
context: context,
ref: ref,
roomId: roomId,
userId: userId,
onRelation: (rowId, type) {
relatedEvent.value = rowId;
relationType.value = type;
composerNode.requestFocus();
},
);
return Scaffold(
endDrawer: PinnedEventsDrawer(
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,
),
body: 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,
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: const Duration(milliseconds: 200),
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 == true && showMembersByDefault)
MemberList(roomId),
],
),
),
),
);
}
}