parent
5304f22456
commit
a220e27877
13 changed files with 193 additions and 143 deletions
17
lib/controllers/contextual_event.dart
Normal file
17
lib/controllers/contextual_event.dart
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
|
import "package:nexus/models/event.dart";
|
||||||
|
|
||||||
|
class ContextualEventController(String? _) extends Notifier<Event?> {
|
||||||
|
@override
|
||||||
|
Event? build() => null;
|
||||||
|
|
||||||
|
void set(Event? event) => state = event;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool updateShouldNotify(_, _) => true;
|
||||||
|
|
||||||
|
static final provider = NotifierProvider.family
|
||||||
|
.autoDispose<ContextualEventController, Event?, String?>(
|
||||||
|
ContextualEventController.new,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
|
||||||
|
|
||||||
class JumpToEventController(String? _) extends Notifier<String?> {
|
|
||||||
@override
|
|
||||||
String? build() => null;
|
|
||||||
|
|
||||||
void set(String? eventId) => state = eventId;
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool updateShouldNotify(_, _) => true;
|
|
||||||
|
|
||||||
static final provider = NotifierProvider.family
|
|
||||||
.autoDispose<JumpToEventController, String?, String?>(
|
|
||||||
JumpToEventController.new,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -18,8 +18,9 @@ import "package:nexus/models/requests/send_message.dart";
|
||||||
import "package:nexus/models/room.dart";
|
import "package:nexus/models/room.dart";
|
||||||
import "package:nexus/models/room_chat.dart";
|
import "package:nexus/models/room_chat.dart";
|
||||||
|
|
||||||
class RoomChatController(final (String roomId, String? contextualEvent) info)
|
class RoomChatController(
|
||||||
extends AsyncNotifier<RoomChat?> {
|
final (String roomId, (int rowId, String eventId)? contextualEvent) info,
|
||||||
|
) extends AsyncNotifier<RoomChat?> {
|
||||||
@override
|
@override
|
||||||
Future<RoomChat?> build() async {
|
Future<RoomChat?> build() async {
|
||||||
final (roomId, eventId) = info;
|
final (roomId, eventId) = info;
|
||||||
|
|
@ -37,29 +38,15 @@ class RoomChatController(final (String roomId, String? contextualEvent) info)
|
||||||
|
|
||||||
final timeline = room.timeline
|
final timeline = room.timeline
|
||||||
.toEntryIList(compare: (a, b) => (a?.key ?? 0).compareTo(b?.key ?? 0))
|
.toEntryIList(compare: (a, b) => (a?.key ?? 0).compareTo(b?.key ?? 0))
|
||||||
.map((element) => element.value)
|
.map((e) => e.value)
|
||||||
.toIList()
|
.toIList()
|
||||||
.addAll(room.clientSticky)
|
.addAll(room.clientSticky)
|
||||||
.map((entry) {
|
|
||||||
final foundEvent = entry == null ? null : room.events[entry];
|
|
||||||
|
|
||||||
final editedEvent =
|
|
||||||
foundEvent == null || foundEvent.lastEditRowId == 0
|
|
||||||
? null
|
|
||||||
: room.events[foundEvent.lastEditRowId];
|
|
||||||
|
|
||||||
return editedEvent == null
|
|
||||||
? foundEvent
|
|
||||||
: foundEvent?.copyWith(
|
|
||||||
content: editedEvent.content,
|
|
||||||
localContent: editedEvent.localContent,
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.nonNulls
|
.nonNulls
|
||||||
.toIList();
|
.toIList();
|
||||||
|
if (info.$2 == null || timeline.contains(info.$2!.$1)) {
|
||||||
if (info.$2 == null || timeline.map((e) => e.eventId).contains(info.$2)) {
|
ref.watch(
|
||||||
ref.watch(RoomsController.provider.select((rooms) => rooms[roomId]));
|
RoomsController.provider.select((rooms) => rooms[roomId]?.timeline),
|
||||||
|
);
|
||||||
|
|
||||||
return .new(
|
return .new(
|
||||||
timeline: timeline,
|
timeline: timeline,
|
||||||
|
|
@ -68,10 +55,25 @@ class RoomChatController(final (String roomId, String? contextualEvent) info)
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
final context = await client.getEventContext(
|
final context = await client.getEventContext(
|
||||||
.new(roomId: roomId, eventId: info.$2!),
|
.new(roomId: roomId, eventId: info.$2!.$2),
|
||||||
|
);
|
||||||
|
|
||||||
|
final events = context.before.add(context.event).addAll(context.after);
|
||||||
|
ref
|
||||||
|
.read(RoomsController.provider.notifier)
|
||||||
|
.update(
|
||||||
|
.new({
|
||||||
|
roomId: Room(
|
||||||
|
events: IMap.fromIterable(
|
||||||
|
events,
|
||||||
|
keyMapper: (event) => event.rowId,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
.new(),
|
||||||
);
|
);
|
||||||
return .new(
|
return .new(
|
||||||
timeline: context.before.add(context.event).addAll(context.after),
|
timeline: .new(events.map((element) => element.rowId)),
|
||||||
hasMoreBackward: true,
|
hasMoreBackward: true,
|
||||||
hasMoreForward: true,
|
hasMoreForward: true,
|
||||||
historicalData: .new(start: context.start, end: context.end),
|
historicalData: .new(start: context.start, end: context.end),
|
||||||
|
|
@ -153,11 +155,29 @@ class RoomChatController(final (String roomId, String? contextualEvent) info)
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
ref
|
||||||
|
.read(RoomsController.provider.notifier)
|
||||||
|
.update(
|
||||||
|
.new({
|
||||||
|
info.$1: Room(
|
||||||
|
events: IMap.fromIterable(
|
||||||
|
paginationResponse.events,
|
||||||
|
keyMapper: (event) => event.rowId,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
.new(),
|
||||||
|
);
|
||||||
|
|
||||||
|
final eventRowIds = paginationResponse.events
|
||||||
|
.map((element) => element.rowId)
|
||||||
|
.toIList();
|
||||||
|
|
||||||
state = .data(
|
state = .data(
|
||||||
.new(
|
.new(
|
||||||
timeline: direction == .forward
|
timeline: direction == .forward
|
||||||
? chat.timeline.addAll(paginationResponse.events)
|
? chat.timeline.addAll(eventRowIds)
|
||||||
: paginationResponse.events.addAll(chat.timeline),
|
: eventRowIds.addAll(chat.timeline),
|
||||||
hasMoreForward:
|
hasMoreForward:
|
||||||
direction == .forward && paginationResponse.nextBatch == null
|
direction == .forward && paginationResponse.nextBatch == null
|
||||||
? false
|
? false
|
||||||
|
|
@ -293,7 +313,9 @@ class RoomChatController(final (String roomId, String? contextualEvent) info)
|
||||||
}
|
}
|
||||||
|
|
||||||
static final provider = AsyncNotifierProvider.family
|
static final provider = AsyncNotifierProvider.family
|
||||||
.autoDispose<RoomChatController, RoomChat?, (String, String?)>(
|
.autoDispose<
|
||||||
RoomChatController.new,
|
RoomChatController,
|
||||||
);
|
RoomChat?,
|
||||||
|
(String, (int rowId, String eventId)?)
|
||||||
|
>(RoomChatController.new);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,33 +9,33 @@ import "package:nexus/models/event.dart";
|
||||||
import "package:nexus/models/room_chat.dart";
|
import "package:nexus/models/room_chat.dart";
|
||||||
|
|
||||||
final class ChatScroll({
|
final class ChatScroll({
|
||||||
required final IList<Event> historyItems,
|
required final IList<int> historyRows,
|
||||||
required final IList<Event> liveItems,
|
required final IList<int> liveRows,
|
||||||
required final GlobalKey centerKey,
|
required final GlobalKey centerKey,
|
||||||
required final ScrollController scrollController,
|
required final ScrollController scrollController,
|
||||||
required final bool atBottom,
|
required final bool atBottom,
|
||||||
required final Future<void> Function(String id) jumpToId,
|
required final Future<void> Function(Event event) jumpToEvent,
|
||||||
required final Future<void> Function() jumpToBottom,
|
required final Future<void> Function() jumpToBottom,
|
||||||
required final GlobalKey Function(String eventId) keyFor,
|
required final GlobalKey Function(int eventRowId) keyFor,
|
||||||
}) {
|
}) {
|
||||||
factory use({
|
factory use({
|
||||||
required AsyncValue<RoomChat?> controllerData,
|
required AsyncValue<RoomChat?> controllerData,
|
||||||
required Future<void> Function(Direction direction) paginate,
|
required Future<void> Function(Direction direction) paginate,
|
||||||
required Future<void> Function() markRead,
|
required Future<void> Function() markRead,
|
||||||
required ValueNotifier<String?> contextualEvent,
|
required ValueNotifier<Event?> contextualEvent,
|
||||||
}) {
|
}) {
|
||||||
final anchorId = useState<String?>(null);
|
final anchorId = useState<int?>(null);
|
||||||
|
|
||||||
final itemKeys = useMemoized(() => <String, GlobalKey>{}, []);
|
final itemKeys = useMemoized(() => <int, GlobalKey>{}, []);
|
||||||
GlobalKey keyFor(String eventId) =>
|
GlobalKey keyFor(int eventRowId) =>
|
||||||
itemKeys.putIfAbsent(eventId, GlobalKey.new);
|
itemKeys.putIfAbsent(eventRowId, GlobalKey.new);
|
||||||
|
|
||||||
final scrollController = useScrollController();
|
final scrollController = useScrollController();
|
||||||
final centerKey = useMemoized(GlobalKey.new);
|
final centerKey = useMemoized(GlobalKey.new);
|
||||||
|
|
||||||
final atBottom = useState(true);
|
final atBottom = useState(true);
|
||||||
|
|
||||||
final pendingAnchorTarget = useState<String?>(null);
|
final pendingAnchorTarget = useState<int?>(null);
|
||||||
final anchorMountedCompleter = useRef<Completer<BuildContext>?>(null);
|
final anchorMountedCompleter = useRef<Completer<BuildContext>?>(null);
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
|
|
@ -43,12 +43,12 @@ final class ChatScroll({
|
||||||
if (controllerData case AsyncData(:final value?)
|
if (controllerData case AsyncData(:final value?)
|
||||||
when value.timeline.isNotEmpty) {
|
when value.timeline.isNotEmpty) {
|
||||||
final hasContextualEvent = value.timeline.any(
|
final hasContextualEvent = value.timeline.any(
|
||||||
(event) => event.eventId == contextualEvent.value,
|
(event) => event == contextualEvent.value?.rowId,
|
||||||
);
|
);
|
||||||
|
|
||||||
anchorId.value = hasContextualEvent
|
anchorId.value = hasContextualEvent
|
||||||
? contextualEvent.value
|
? contextualEvent.value?.rowId
|
||||||
: value.timeline.last.eventId;
|
: value.timeline.last;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -60,9 +60,7 @@ final class ChatScroll({
|
||||||
if (target == null) return null;
|
if (target == null) return null;
|
||||||
|
|
||||||
final found =
|
final found =
|
||||||
controllerData.value?.timeline.any(
|
controllerData.value?.timeline.any((event) => event == target) ??
|
||||||
(event) => event.eventId == target,
|
|
||||||
) ??
|
|
||||||
false;
|
false;
|
||||||
|
|
||||||
if (found || controllerData is AsyncError) {
|
if (found || controllerData is AsyncError) {
|
||||||
|
|
@ -88,7 +86,7 @@ final class ChatScroll({
|
||||||
return null;
|
return null;
|
||||||
}, [controllerData, pendingAnchorTarget.value]);
|
}, [controllerData, pendingAnchorTarget.value]);
|
||||||
|
|
||||||
final ({IList<Event> history, IList<Event> live}) split = useMemoized(() {
|
final ({IList<int> history, IList<int> live}) split = useMemoized(() {
|
||||||
final items = controllerData.value?.timeline;
|
final items = controllerData.value?.timeline;
|
||||||
final anchor = anchorId.value;
|
final anchor = anchorId.value;
|
||||||
|
|
||||||
|
|
@ -96,7 +94,7 @@ final class ChatScroll({
|
||||||
return (history: const .empty(), live: const .empty());
|
return (history: const .empty(), live: const .empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
final anchorIndex = items.indexWhere((item) => item.eventId == anchor);
|
final anchorIndex = items.indexOf(anchor);
|
||||||
|
|
||||||
if (anchorIndex == -1) {
|
if (anchorIndex == -1) {
|
||||||
return (history: const .empty(), live: items);
|
return (history: const .empty(), live: items);
|
||||||
|
|
@ -155,15 +153,15 @@ final class ChatScroll({
|
||||||
);
|
);
|
||||||
|
|
||||||
return .new(
|
return .new(
|
||||||
historyItems: split.history,
|
historyRows: split.history,
|
||||||
liveItems: split.live,
|
liveRows: split.live,
|
||||||
centerKey: centerKey,
|
centerKey: centerKey,
|
||||||
scrollController: scrollController,
|
scrollController: scrollController,
|
||||||
atBottom: atBottom.value,
|
atBottom: atBottom.value,
|
||||||
jumpToId: (String itemId) async {
|
jumpToEvent: (Event event) async {
|
||||||
if (!scrollController.hasClients) return;
|
if (!scrollController.hasClients) return;
|
||||||
|
|
||||||
final existing = keyFor(itemId).currentContext;
|
final existing = keyFor(event.rowId).currentContext;
|
||||||
if (existing != null && existing.mounted) {
|
if (existing != null && existing.mounted) {
|
||||||
// Already mounted, just scroll
|
// Already mounted, just scroll
|
||||||
await Scrollable.ensureVisible(
|
await Scrollable.ensureVisible(
|
||||||
|
|
@ -175,8 +173,8 @@ final class ChatScroll({
|
||||||
} else {
|
} else {
|
||||||
final completer = Completer<BuildContext>();
|
final completer = Completer<BuildContext>();
|
||||||
anchorMountedCompleter.value = completer;
|
anchorMountedCompleter.value = completer;
|
||||||
pendingAnchorTarget.value = itemId;
|
pendingAnchorTarget.value = event.rowId;
|
||||||
contextualEvent.value = itemId;
|
contextualEvent.value = event;
|
||||||
|
|
||||||
final context = await completer.future;
|
final context = await completer.future;
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,8 @@ part "room_chat.g.dart";
|
||||||
@Freezed(toJson: false, fromJson: false)
|
@Freezed(toJson: false, fromJson: false)
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class const RoomChat({
|
class const RoomChat({
|
||||||
required final IList<Event> timeline,
|
/// List of eventRowId
|
||||||
|
required final IList<int> timeline,
|
||||||
required final bool hasMoreForward,
|
required final bool hasMoreForward,
|
||||||
required final bool hasMoreBackward,
|
required final bool hasMoreBackward,
|
||||||
final HistoricalData? historicalData,
|
final HistoricalData? historicalData,
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,12 @@ class const EventPreview(final Event event, {super.key})
|
||||||
runSpacing: 2,
|
runSpacing: 2,
|
||||||
children: [
|
children: [
|
||||||
if (event.content is MessageContent) MessageDisplayname(event),
|
if (event.content is MessageContent) MessageDisplayname(event),
|
||||||
EventRenderer(event, textOnly: true, maxLines: 1),
|
EventRenderer(
|
||||||
|
event.rowId,
|
||||||
|
roomId: event.roomId,
|
||||||
|
textOnly: true,
|
||||||
|
maxLines: 1,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import "package:material_ui/material_ui.dart";
|
import "package:material_ui/material_ui.dart";
|
||||||
import "package:hooks_riverpod/hooks_riverpod.dart";
|
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||||
import "package:nexus/controllers/init_complete.dart";
|
import "package:nexus/controllers/init_complete.dart";
|
||||||
import "package:nexus/controllers/jump_to_event.dart";
|
import "package:nexus/controllers/contextual_event.dart";
|
||||||
import "package:nexus/controllers/key.dart";
|
import "package:nexus/controllers/key.dart";
|
||||||
import "package:nexus/widgets/appbar.dart";
|
import "package:nexus/widgets/appbar.dart";
|
||||||
import "package:nexus/widgets/sidebar.dart";
|
import "package:nexus/widgets/sidebar.dart";
|
||||||
|
|
@ -30,7 +30,7 @@ class const ChatPage({super.key}) extends HookConsumerWidget {
|
||||||
child: Consumer(
|
child: Consumer(
|
||||||
builder: (context, ref, _) {
|
builder: (context, ref, _) {
|
||||||
final initialHighlight = ref.watch(
|
final initialHighlight = ref.watch(
|
||||||
JumpToEventController.provider(roomId),
|
ContextualEventController.provider(roomId),
|
||||||
);
|
);
|
||||||
return RoomChat(
|
return RoomChat(
|
||||||
key: ValueKey((roomId, initialHighlight)),
|
key: ValueKey((roomId, initialHighlight)),
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import "package:m3e_buttons/m3e_buttons.dart";
|
||||||
import "package:material_ui/material_ui.dart";
|
import "package:material_ui/material_ui.dart";
|
||||||
import "package:flutter_hooks/flutter_hooks.dart";
|
import "package:flutter_hooks/flutter_hooks.dart";
|
||||||
import "package:hooks_riverpod/hooks_riverpod.dart";
|
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||||
import "package:nexus/controllers/jump_to_event.dart";
|
import "package:nexus/controllers/contextual_event.dart";
|
||||||
import "package:nexus/controllers/key.dart";
|
import "package:nexus/controllers/key.dart";
|
||||||
import "package:nexus/controllers/notifications.dart";
|
import "package:nexus/controllers/notifications.dart";
|
||||||
import "package:nexus/controllers/spaces.dart";
|
import "package:nexus/controllers/spaces.dart";
|
||||||
|
|
@ -168,18 +168,21 @@ class const NotificationsPage({
|
||||||
.set(event.roomId);
|
.set(event.roomId);
|
||||||
ref
|
ref
|
||||||
.watch(
|
.watch(
|
||||||
JumpToEventController.provider(
|
ContextualEventController.provider(
|
||||||
event.roomId,
|
event.roomId,
|
||||||
).notifier,
|
).notifier,
|
||||||
)
|
)
|
||||||
.set(event.eventId);
|
.set(event);
|
||||||
|
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: IgnorePointer(
|
child: IgnorePointer(
|
||||||
child: EventRenderer(event),
|
child: EventRenderer(
|
||||||
|
event.rowId,
|
||||||
|
roomId: event.roomId,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
isHighlighted: isHighlighted,
|
isHighlighted: isHighlighted,
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import "package:nexus/widgets/renderers/event.dart";
|
||||||
final class const PinnedEventsDrawer(
|
final class const PinnedEventsDrawer(
|
||||||
final String roomId, {
|
final String roomId, {
|
||||||
required final IList<PopupMenuEntry> Function(Event event) getEventOptions,
|
required final IList<PopupMenuEntry> Function(Event event) getEventOptions,
|
||||||
required final Future<void> Function(String eventId) jumpToId,
|
required final Future<void> Function(Event event) jumpToEvent,
|
||||||
super.key,
|
super.key,
|
||||||
}) extends HookConsumerWidget {
|
}) extends HookConsumerWidget {
|
||||||
@override
|
@override
|
||||||
|
|
@ -62,15 +62,15 @@ final class const PinnedEventsDrawer(
|
||||||
borderRadius: .circular(12),
|
borderRadius: .circular(12),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
jumpToId(event.eventId);
|
jumpToEvent(event);
|
||||||
},
|
},
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: .symmetric(vertical: 4),
|
padding: .symmetric(vertical: 4),
|
||||||
child: IgnorePointer(
|
child: IgnorePointer(
|
||||||
child: EventRenderer(
|
child: EventRenderer(
|
||||||
event,
|
event.rowId,
|
||||||
|
roomId: roomId,
|
||||||
maxLines: 2,
|
maxLines: 2,
|
||||||
isGrouped: false,
|
|
||||||
getEventOptions: getEventOptions,
|
getEventOptions: getEventOptions,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import "package:nexus/helpers/mxc_image.dart";
|
||||||
import "package:nexus/models/event.dart";
|
import "package:nexus/models/event.dart";
|
||||||
import "package:nexus/widgets/error_dialog.dart";
|
import "package:nexus/widgets/error_dialog.dart";
|
||||||
import "package:nexus/main.dart";
|
import "package:nexus/main.dart";
|
||||||
|
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||||
|
|
||||||
class const ReactionRow(final Event event, {super.key}) extends ConsumerWidget {
|
class const ReactionRow(final Event event, {super.key}) extends ConsumerWidget {
|
||||||
@override
|
@override
|
||||||
|
|
@ -21,20 +22,22 @@ class const ReactionRow(final Event event, {super.key}) extends ConsumerWidget {
|
||||||
.new(roomId: event.roomId, eventRowId: event.rowId),
|
.new(roomId: event.roomId, eventRowId: event.rowId),
|
||||||
),
|
),
|
||||||
)) {
|
)) {
|
||||||
AsyncData(value: final reactions) ||
|
AsyncData(value: final IMap<String, IList<String>>? reactors) ||
|
||||||
AsyncLoading(value: final reactions?) => Wrap(
|
AsyncLoading(value: final reactors) => Wrap(
|
||||||
spacing: 4,
|
spacing: 4,
|
||||||
runSpacing: 4,
|
runSpacing: 4,
|
||||||
children: reactions
|
children: event.reactions
|
||||||
.where((_, value) => value.isNotEmpty)
|
.where((_, value) => value != 0)
|
||||||
.mapTo(
|
.mapTo(
|
||||||
(reaction, reactors) => HookBuilder(
|
(reaction, count) => HookBuilder(
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
final enabled = useState(true);
|
final enabled = useState(true);
|
||||||
|
|
||||||
final selected = reactors.contains(clientState!.userId);
|
final selected =
|
||||||
|
reactors?[reaction]?.contains(clientState!.userId) ??
|
||||||
|
false;
|
||||||
return Tooltip(
|
return Tooltip(
|
||||||
message: reactors.join(", "),
|
message: reactors?[reaction]?.join(", ") ?? "",
|
||||||
child: ChoiceChip(
|
child: ChoiceChip(
|
||||||
showCheckmark: false,
|
showCheckmark: false,
|
||||||
selected: selected,
|
selected: selected,
|
||||||
|
|
@ -53,10 +56,7 @@ class const ReactionRow(final Event event, {super.key}) extends ConsumerWidget {
|
||||||
)
|
)
|
||||||
: Text(reaction, overflow: .ellipsis),
|
: Text(reaction, overflow: .ellipsis),
|
||||||
),
|
),
|
||||||
Text(
|
Text(count.toString(), overflow: .ellipsis),
|
||||||
reactors.length.toString(),
|
|
||||||
overflow: .ellipsis,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
onSelected: enabled.value
|
onSelected: enabled.value
|
||||||
|
|
@ -75,7 +75,7 @@ class const ReactionRow(final Event event, {super.key}) extends ConsumerWidget {
|
||||||
.removeReaction(
|
.removeReaction(
|
||||||
reaction,
|
reaction,
|
||||||
event,
|
event,
|
||||||
clientState.userId!,
|
clientState!.userId!,
|
||||||
)
|
)
|
||||||
.onError(showError);
|
.onError(showError);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -100,8 +100,6 @@ class const ReactionRow(final Event event, {super.key}) extends ConsumerWidget {
|
||||||
error,
|
error,
|
||||||
stackTrace,
|
stackTrace,
|
||||||
),
|
),
|
||||||
|
|
||||||
_ => SizedBox.shrink(),
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ import "package:flutter/gestures.dart";
|
||||||
import "package:material_ui/material_ui.dart";
|
import "package:material_ui/material_ui.dart";
|
||||||
import "package:flutter_hooks/flutter_hooks.dart";
|
import "package:flutter_hooks/flutter_hooks.dart";
|
||||||
import "package:hooks_riverpod/hooks_riverpod.dart";
|
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||||
|
import "package:nexus/controllers/event.dart";
|
||||||
|
import "package:nexus/controllers/rooms.dart";
|
||||||
import "package:nexus/helpers/extensions/show_context_menu.dart";
|
import "package:nexus/helpers/extensions/show_context_menu.dart";
|
||||||
import "package:nexus/models/content/avatar.dart";
|
import "package:nexus/models/content/avatar.dart";
|
||||||
import "package:nexus/models/content/canonical_alias.dart";
|
import "package:nexus/models/content/canonical_alias.dart";
|
||||||
|
|
@ -28,11 +30,12 @@ import "package:nexus/widgets/renderers/generic_event.dart";
|
||||||
import "package:nexus/widgets/timestamp.dart";
|
import "package:nexus/widgets/timestamp.dart";
|
||||||
|
|
||||||
class const EventRenderer(
|
class const EventRenderer(
|
||||||
final Event event, {
|
final int eventRowId, {
|
||||||
|
required final String roomId,
|
||||||
|
final int? previousEventRowId,
|
||||||
final bool textOnly = false,
|
final bool textOnly = false,
|
||||||
final bool isGrouped = false,
|
|
||||||
final int? maxLines,
|
final int? maxLines,
|
||||||
final VoidCallback? onTapReply,
|
final Future<void> Function(Event event)? jumpToEvent,
|
||||||
final IList<PopupMenuEntry> Function(Event event)? getEventOptions,
|
final IList<PopupMenuEntry> Function(Event event)? getEventOptions,
|
||||||
super.key,
|
super.key,
|
||||||
}) extends HookConsumerWidget {
|
}) extends HookConsumerWidget {
|
||||||
|
|
@ -44,6 +47,26 @@ class const EventRenderer(
|
||||||
final focusNode = useFocusNode();
|
final focusNode = useFocusNode();
|
||||||
useListenable(focusNode);
|
useListenable(focusNode);
|
||||||
|
|
||||||
|
final (event, previousEvent) = ref.watch(
|
||||||
|
RoomsController.provider.select(
|
||||||
|
(value) => (
|
||||||
|
value[roomId]?.events[eventRowId],
|
||||||
|
previousEventRowId == null
|
||||||
|
? null
|
||||||
|
: value[roomId]?.events[previousEventRowId!],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (event == null) return SizedBox.shrink();
|
||||||
|
|
||||||
|
final isGrouped =
|
||||||
|
previousEvent?.content is MessageContent &&
|
||||||
|
previousEvent?.redactedBy == null &&
|
||||||
|
previousEvent?.relationType != "m.replace" &&
|
||||||
|
event.sender == previousEvent?.sender &&
|
||||||
|
event.pmp?.id == previousEvent?.pmp?.id;
|
||||||
|
|
||||||
final child = event.redactedBy != null || event.relationType == "m.replace"
|
final child = event.redactedBy != null || event.relationType == "m.replace"
|
||||||
? null
|
? null
|
||||||
: switch (event.content) {
|
: switch (event.content) {
|
||||||
|
|
@ -60,7 +83,16 @@ class const EventRenderer(
|
||||||
EncryptedContent() ||
|
EncryptedContent() ||
|
||||||
StickerContent() => MessageRenderer(
|
StickerContent() => MessageRenderer(
|
||||||
event,
|
event,
|
||||||
onTapReply: onTapReply,
|
onTapReply: jumpToEvent == null
|
||||||
|
? null
|
||||||
|
: () async {
|
||||||
|
final replyEvent = await ref.read(
|
||||||
|
EventController.provider(
|
||||||
|
.new(roomId: roomId, eventId: event.replyTo!),
|
||||||
|
).future,
|
||||||
|
);
|
||||||
|
if (replyEvent != null) await jumpToEvent!(replyEvent);
|
||||||
|
},
|
||||||
isGrouped: isGrouped,
|
isGrouped: isGrouped,
|
||||||
maxLines: maxLines,
|
maxLines: maxLines,
|
||||||
textOnly: textOnly,
|
textOnly: textOnly,
|
||||||
|
|
|
||||||
|
|
@ -1,43 +1,34 @@
|
||||||
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||||
import "package:material_ui/material_ui.dart";
|
import "package:material_ui/material_ui.dart";
|
||||||
import "package:nexus/helpers/hooks/chat_scroll.dart";
|
import "package:nexus/helpers/hooks/chat_scroll.dart";
|
||||||
import "package:nexus/models/content/message.dart";
|
|
||||||
import "package:nexus/models/event.dart";
|
import "package:nexus/models/event.dart";
|
||||||
import "package:nexus/widgets/renderers/event.dart";
|
import "package:nexus/widgets/renderers/event.dart";
|
||||||
import "package:nexus/widgets/highlight_wrapper.dart";
|
import "package:nexus/widgets/highlight_wrapper.dart";
|
||||||
import "package:super_sliver_list/super_sliver_list.dart";
|
import "package:super_sliver_list/super_sliver_list.dart";
|
||||||
|
|
||||||
class const ChatTimeline({
|
class const ChatTimeline({
|
||||||
|
required final String roomId,
|
||||||
required final ChatScroll scroll,
|
required final ChatScroll scroll,
|
||||||
required final Future<void> Function(String) jumpToId,
|
required final Future<void> Function(Event event) jumpToEvent,
|
||||||
required final IList<PopupMenuEntry> Function(Event) getEventOptions,
|
required final IList<PopupMenuEntry> Function(Event) getEventOptions,
|
||||||
required final String? highlightedEvent,
|
required final int? highlightedEvent,
|
||||||
required final double composerHeight,
|
required final double composerHeight,
|
||||||
super.key,
|
super.key,
|
||||||
}) extends StatelessWidget {
|
}) extends StatelessWidget {
|
||||||
bool isGrouped(Event event, Event? previousEvent) =>
|
|
||||||
previousEvent?.content is MessageContent &&
|
|
||||||
previousEvent?.redactedBy == null &&
|
|
||||||
previousEvent?.relationType != "m.replace" &&
|
|
||||||
event.sender == previousEvent?.sender &&
|
|
||||||
event.pmp?.id == previousEvent?.pmp?.id;
|
|
||||||
|
|
||||||
Widget eventRow(
|
Widget eventRow(
|
||||||
Event event,
|
int eventRowId,
|
||||||
Event? previousEvent, {
|
int? previousEventRowId, {
|
||||||
required Future<void> Function(String) jumpToId,
|
|
||||||
required IList<PopupMenuEntry> Function(Event) getEventOptions,
|
|
||||||
required String? highlightedEvent,
|
|
||||||
required Key key,
|
required Key key,
|
||||||
}) => HighlightWrapper(
|
}) => HighlightWrapper(
|
||||||
EventRenderer(
|
EventRenderer(
|
||||||
event,
|
eventRowId,
|
||||||
onTapReply: () => jumpToId(event.replyTo!),
|
previousEventRowId: previousEventRowId,
|
||||||
|
roomId: roomId,
|
||||||
|
jumpToEvent: jumpToEvent,
|
||||||
getEventOptions: getEventOptions,
|
getEventOptions: getEventOptions,
|
||||||
isGrouped: isGrouped(event, previousEvent),
|
|
||||||
),
|
),
|
||||||
key: key,
|
key: key,
|
||||||
isHighlighted: highlightedEvent == event.eventId,
|
isHighlighted: highlightedEvent == eventRowId,
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -50,29 +41,23 @@ class const ChatTimeline({
|
||||||
SliverToBoxAdapter(child: SizedBox(height: composerHeight)),
|
SliverToBoxAdapter(child: SizedBox(height: composerHeight)),
|
||||||
|
|
||||||
SuperSliverList.builder(
|
SuperSliverList.builder(
|
||||||
itemCount: scroll.liveItems.length,
|
itemCount: scroll.liveRows.length,
|
||||||
itemBuilder: (_, index) => eventRow(
|
itemBuilder: (_, index) => eventRow(
|
||||||
scroll.liveItems[index],
|
scroll.liveRows[index],
|
||||||
index > 0
|
index > 0
|
||||||
? scroll.liveItems.getOrNull(index - 1)
|
? scroll.liveRows.getOrNull(index - 1)
|
||||||
: scroll.historyItems.firstOrNull,
|
: scroll.historyRows.firstOrNull,
|
||||||
jumpToId: jumpToId,
|
key: scroll.keyFor(scroll.liveRows[index]),
|
||||||
getEventOptions: getEventOptions,
|
|
||||||
highlightedEvent: highlightedEvent,
|
|
||||||
key: scroll.keyFor(scroll.liveItems[index].eventId),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
SuperSliverList.builder(
|
SuperSliverList.builder(
|
||||||
key: scroll.centerKey,
|
key: scroll.centerKey,
|
||||||
itemCount: scroll.historyItems.length,
|
itemCount: scroll.historyRows.length,
|
||||||
itemBuilder: (_, index) => eventRow(
|
itemBuilder: (_, index) => eventRow(
|
||||||
scroll.historyItems[index],
|
scroll.historyRows[index],
|
||||||
scroll.historyItems.getOrNull(index + 1),
|
scroll.historyRows.getOrNull(index + 1),
|
||||||
jumpToId: jumpToId,
|
key: scroll.keyFor(scroll.historyRows[index]),
|
||||||
getEventOptions: getEventOptions,
|
|
||||||
highlightedEvent: highlightedEvent,
|
|
||||||
key: scroll.keyFor(scroll.historyItems[index].eventId),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -24,15 +24,15 @@ final class const RoomChat({
|
||||||
required final String? roomId,
|
required final String? roomId,
|
||||||
required final bool isDesktop,
|
required final bool isDesktop,
|
||||||
required final bool showMembersByDefault,
|
required final bool showMembersByDefault,
|
||||||
final String? initialHighlightedEvent,
|
final Event? initialHighlightedEvent,
|
||||||
super.key,
|
super.key,
|
||||||
}) extends HookConsumerWidget {
|
}) extends HookConsumerWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final relatedEvent = useState<Event?>(null);
|
final relatedEvent = useState<Event?>(null);
|
||||||
final relationType = useState(RelationType.reply);
|
final relationType = useState(RelationType.reply);
|
||||||
final contextualEvent = useState<String?>(initialHighlightedEvent);
|
final contextualEvent = useState<Event?>(initialHighlightedEvent);
|
||||||
final highlightedEvent = useState<String?>(null);
|
final highlightedEvent = useState<int?>(null);
|
||||||
|
|
||||||
final composerSize = useState<double>(64);
|
final composerSize = useState<double>(64);
|
||||||
|
|
||||||
|
|
@ -63,7 +63,9 @@ final class const RoomChat({
|
||||||
|
|
||||||
final controllerProvider = RoomChatController.provider((
|
final controllerProvider = RoomChatController.provider((
|
||||||
roomId,
|
roomId,
|
||||||
contextualEvent.value,
|
contextualEvent.value == null
|
||||||
|
? null
|
||||||
|
: (contextualEvent.value!.rowId, contextualEvent.value!.eventId),
|
||||||
));
|
));
|
||||||
final notifier = ref.watch(controllerProvider.notifier);
|
final notifier = ref.watch(controllerProvider.notifier);
|
||||||
|
|
||||||
|
|
@ -97,12 +99,14 @@ final class const RoomChat({
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
Future<void> jumpToId(String eventId) async {
|
Future<void> jumpToEvent(Event event) async {
|
||||||
highlightedEvent.value = eventId;
|
highlightedEvent.value = event.rowId;
|
||||||
|
|
||||||
await scroll.jumpToId(eventId);
|
await scroll.jumpToEvent(event);
|
||||||
await Future.delayed(.new(milliseconds: 700), () {
|
await Future.delayed(.new(milliseconds: 700), () {
|
||||||
if (highlightedEvent.value == eventId) highlightedEvent.value = null;
|
if (highlightedEvent.value == event.rowId) {
|
||||||
|
highlightedEvent.value = null;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -113,7 +117,7 @@ final class const RoomChat({
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
|
|
||||||
if (scroll.scrollController.hasClients) {
|
if (scroll.scrollController.hasClients) {
|
||||||
jumpToId(initialHighlightedEvent!);
|
jumpToEvent(initialHighlightedEvent!);
|
||||||
} else {
|
} else {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) => check());
|
WidgetsBinding.instance.addPostFrameCallback((_) => check());
|
||||||
}
|
}
|
||||||
|
|
@ -141,7 +145,7 @@ final class const RoomChat({
|
||||||
endDrawer: PinnedEventsDrawer(
|
endDrawer: PinnedEventsDrawer(
|
||||||
roomId,
|
roomId,
|
||||||
getEventOptions: getEventOptions,
|
getEventOptions: getEventOptions,
|
||||||
jumpToId: jumpToId,
|
jumpToEvent: jumpToEvent,
|
||||||
),
|
),
|
||||||
body: Builder(
|
body: Builder(
|
||||||
builder: (middleContext) => Scaffold(
|
builder: (middleContext) => Scaffold(
|
||||||
|
|
@ -168,7 +172,8 @@ final class const RoomChat({
|
||||||
padding: .symmetric(horizontal: 4),
|
padding: .symmetric(horizontal: 4),
|
||||||
child: ChatTimeline(
|
child: ChatTimeline(
|
||||||
scroll: scroll,
|
scroll: scroll,
|
||||||
jumpToId: jumpToId,
|
roomId: roomId,
|
||||||
|
jumpToEvent: jumpToEvent,
|
||||||
getEventOptions: getEventOptions,
|
getEventOptions: getEventOptions,
|
||||||
highlightedEvent: highlightedEvent.value,
|
highlightedEvent: highlightedEvent.value,
|
||||||
composerHeight: composerSize.value,
|
composerHeight: composerSize.value,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue