1
0
Fork 0
forked from Nexus/nexus

Remove flutter chat (#26)

Had to squash merge manually as Forgejo was erroring
This commit is contained in:
Henry Hiles 2026-05-21 16:58:22 -04:00
commit 16cf126df4
111 changed files with 3162 additions and 2366 deletions

View file

@ -1,17 +1,13 @@
import "dart:async";
import "dart:math";
import "package:collection/collection.dart";
import "package:fast_immutable_collections/fast_immutable_collections.dart";
import "package:flutter_chat_core/flutter_chat_core.dart";
import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:fluttertagger/fluttertagger.dart";
import "package:nexus/controllers/client_controller.dart";
import "package:nexus/controllers/message_controller.dart";
import "package:nexus/controllers/messages_controller.dart";
import "package:nexus/controllers/new_events_controller.dart";
import "package:nexus/controllers/rooms_controller.dart";
import "package:nexus/controllers/selected_room_controller.dart";
import "package:nexus/models/configs/messages_config.dart";
import "package:nexus/models/configs/message_config.dart";
import "package:nexus/models/content/reaction.dart";
import "package:nexus/models/event.dart";
import "package:nexus/models/requests/get_related_events_request.dart";
import "package:nexus/models/requests/get_room_state_request.dart";
import "package:nexus/models/requests/paginate_request.dart";
@ -21,203 +17,75 @@ import "package:nexus/models/requests/send_event_request.dart";
import "package:nexus/models/requests/send_message_request.dart";
import "package:nexus/models/room.dart";
class RoomChatController extends AsyncNotifier<InMemoryChatController> {
class RoomChatController extends AsyncNotifier<IList<Event>> {
final String roomId;
RoomChatController(this.roomId);
@override
Future<InMemoryChatController> build() async {
Future<IList<Event>> build() async {
final client = ref.watch(ClientController.provider.notifier);
var room = ref.read(RoomsController.provider)[roomId];
if (room == null) return InMemoryChatController();
final state = await client.getRoomState(
GetRoomStateRequest(roomId: roomId),
final room = ref.watch(
RoomsController.provider.select((rooms) => rooms[roomId]),
);
if (room == null) return const IList.empty();
ref
.read(RoomsController.provider.notifier)
.update(
{
roomId: Room(
events: state,
state: state.fold(
const IMap.empty(),
(previousValue, stateEvent) => previousValue.add(
stateEvent.type,
(previousValue[stateEvent.type] ?? const IMap.empty()).addAll(
IMap({
if (stateEvent.stateKey != null)
stateEvent.stateKey!: stateEvent.rowId,
}),
),
),
),
),
}.toIMap(),
const ISet.empty(),
);
room = ref.read(RoomsController.provider)[roomId];
if (room == null) return InMemoryChatController();
final messages = await ref.watch(
MessagesController.provider(
MessagesConfig(
room: room,
events: room.timeline
.map(
(timelineRowTuple) => room!.events.firstWhereOrNull(
(event) => event.rowId == timelineRowTuple.eventRowId,
),
)
.nonNulls
.toIList(),
),
).future,
);
final controller = InMemoryChatController(messages: messages.toList());
ref.onDispose(
ref.listen(NewEventsController.provider(roomId), (_, next) async {
for (final event in next) {
if (event.type == "m.reaction") {
final message = controller.messages.firstWhereOrNull(
(message) =>
message.id == event.content["m.relates_to"]?["event_id"],
);
final key = event.content["m.relates_to"]?["key"];
if (message == null || key == null || !ref.mounted) return;
return await controller.updateMessage(
message,
message.copyWith(
reactions: IMap(message.reactions)
.update(
key,
(reactors) => [...reactors, event.authorId],
ifAbsent: () => [event.authorId],
)
.unlock,
),
);
}
if (event.type == "m.room.redaction") {
final controller = await future;
final redactsId = event.content["redacts"];
final originalMessage = controller.messages.firstWhereOrNull(
(message) => message.id == redactsId,
);
if (!ref.mounted) return;
if (originalMessage != null) {
return await controller.removeMessage(originalMessage);
}
final redacts = ref
.read(SelectedRoomController.provider)
?.events
.firstWhere((event) => event.eventId == redactsId);
if (redacts?.type == "m.reaction") {
final message = controller.messages.firstWhereOrNull(
(message) =>
message.id == redacts!.content["m.relates_to"]?["event_id"],
);
final key = redacts!.content["m.relates_to"]?["key"];
if (message == null || key == null || !ref.mounted) return;
return await controller.updateMessage(
message,
message.copyWith(
reactions: IMap(message.reactions)
.update(
key,
(reactors) =>
IList(reactors).remove(redacts.authorId).unlock,
)
.where((_, value) => value.isNotEmpty)
.unlock,
),
);
}
} else {
final message = await ref.watch(
MessageController.provider(
MessageConfig(event: event, room: room!, includeEdits: true),
).future,
);
if (event.relationType == "m.replace") {
final controller = await future;
final oldMessage = controller.messages.firstWhereOrNull(
(element) => element.id == event.relatesTo,
);
if (oldMessage == null || message == null || !ref.mounted) return;
return await controller.updateMessage(
oldMessage,
message.copyWith(
id: oldMessage.id,
replyToMessageId: oldMessage.replyToMessageId,
metadata: {
...(oldMessage.metadata ?? {}),
...(message.metadata ?? {})
.toIMap()
.where((key, value) => value != null)
.unlock,
},
),
);
}
if (message != null && ref.mounted) {
await insertMessage(message);
}
}
}
}, weak: true).close,
);
ref.onDispose(controller.dispose);
// While there are under 20 messages, try up to load more messages until theres no more or we have 20 messages.
for (var more = true; more == true && controller.messages.length < 20;) {
more = await loadOlder(controller);
}
return controller;
}
Future<void> insertMessage(Message message) async {
final controller = await future;
final oldMessage = message.metadata?["txnId"] == null
? null
: controller.messages.firstWhereOrNull(
(element) =>
element.metadata?["txnId"] == message.metadata?["txnId"],
);
return oldMessage == null
? controller.insertMessage(message)
: controller.updateMessage(oldMessage, message);
}
Future<void> deleteMessage(Message message, {String? reason}) => ref
.watch(ClientController.provider.notifier)
.redactEvent(
RedactEventRequest(eventId: message.id, roomId: roomId, reason: reason),
if (!room.hasFetchedState) {
final state = await client.getRoomState(
GetRoomStateRequest(roomId: roomId),
);
Future<bool> loadOlder([InMemoryChatController? chatController]) async {
await ref.read(RoomsController.provider.notifier).addState(roomId, state);
}
// While there are under 20 events, try to load more
// until there's no more or the conditions are met.
if (room.hasMore && room.timeline.length < 20) {
loadOlder();
}
return room.timeline
.toEntryIList(compare: (a, b) => (b?.key ?? 0).compareTo(a?.key ?? 0))
.map((entry) {
if (entry.value == null) return null;
final foundEvent = room.events[entry.value!];
final editedEvent =
foundEvent == null || foundEvent.lastEditRowId == 0
? null
: room.events[foundEvent.lastEditRowId];
return editedEvent == null
? foundEvent
: foundEvent?.copyWith(content: editedEvent.content);
})
.nonNulls
.toIList();
}
Future<void> deleteMessage(Event event, {String? reason}) => ref
.watch(ClientController.provider.notifier)
.redactEvent(
RedactEventRequest(
eventId: event.eventId,
roomId: roomId,
reason: reason,
),
);
Future<bool> loadOlder() async {
final timelineKeys = ref
.read(RoomsController.provider.select((value) => value[roomId]))
?.timeline
.keys;
final response = await ref
.watch(ClientController.provider.notifier)
.paginate(
PaginateRequest(
roomId: roomId,
maxTimelineId: ref
.read(RoomsController.provider)[roomId]
?.timeline
.firstOrNull
?.timelineRowId,
maxTimelineId: timelineKeys?.isNotEmpty == true
? timelineKeys?.reduce(min)
: null,
),
);
@ -226,42 +94,22 @@ class RoomChatController extends AsyncNotifier<InMemoryChatController> {
.update(
IMap({
roomId: Room(
events: response.events.addAll(response.relatedEvents),
events: IMap.fromIterable(
response.events.addAll(response.relatedEvents),
keyMapper: (event) => event.rowId,
valueMapper: (event) => event,
),
hasMore: response.hasMore,
timeline: response.events
.map(
(event) => TimelineRowTuple(
timelineRowId: event.timelineRowId,
eventRowId: event.rowId,
),
)
.toIList(),
timeline: IMap.fromIterable(
response.events,
keyMapper: (event) => event.timelineRowId,
valueMapper: (event) => event.rowId,
),
),
}),
const ISet.empty(),
addToNewEvents: false,
);
final room = ref.read(RoomsController.provider)[roomId];
if (room != null) {
final messages = await ref.watch(
MessagesController.provider(
MessagesConfig(room: room, events: response.events.reversed),
).future,
);
final controller = chatController ?? await future;
await controller.insertAllMessages(
messages
.where(
(newMessage) => !controller.messages.any(
(message) => message.id == newMessage.id,
),
)
.toList(),
index: 0,
);
}
return response.hasMore;
}
@ -270,7 +118,7 @@ class RoomChatController extends AsyncNotifier<InMemoryChatController> {
bool shouldMention = true,
required IList<Tag> tags,
required RelationType relationType,
Message? relation,
Event? relation,
}) async {
var taggedMessage = text;
@ -285,7 +133,6 @@ class RoomChatController extends AsyncNotifier<InMemoryChatController> {
}
final client = ref.watch(ClientController.provider.notifier);
final room = ref.read(RoomsController.provider)[roomId];
final event = await client.sendMessage(
SendMessageRequest(
roomId: roomId,
@ -294,52 +141,46 @@ class RoomChatController extends AsyncNotifier<InMemoryChatController> {
if (shouldMention == true &&
relation != null &&
relationType == RelationType.reply)
relation.authorId,
relation.sender,
].toIList(),
room: taggedMessage.contains("@room"),
),
text: taggedMessage,
relation: relation == null
? null
: Relation(eventId: relation.id, relationType: relationType),
),
);
final message = room == null
? null
: await ref.watch(
MessageController.provider(
MessageConfig(room: room, event: event),
).future,
);
if (message != null) insertMessage(message);
}
Future<void> scrollToMessage(Message message) async {
final controller = await future;
Future<void> setFlashing(bool flashing) => controller.updateMessage(
message,
message.copyWith(
metadata: {...(message.metadata ?? {}), "flashing": flashing},
: Relation(eventId: relation.eventId, relationType: relationType),
),
);
await setFlashing(true);
Timer(Duration(seconds: 1), () => setFlashing(false));
return await controller.scrollToMessage(message.id);
// TODO: Add new event to timeline whilst its sending
// ref
// .watch(RoomsController.provider.notifier)
// .update(
// {
// roomId: Room(
// events: [event].toIList(),
// timeline: [
// TimelineRowTuple(
// timelineRowId: event.timelineRowId,
// eventRowId: event.rowId,
// ),
// ].toIList(),
// ),
// }.toIMap(),
// const ISet.empty(),
// );
}
Future<void> removeReaction(
String reaction,
Message message,
Event event,
String userId,
) async {
final client = ref.watch(ClientController.provider.notifier);
final allReactionEvents = await client.getRelatedEvents(
GetRelatedEventsRequest(
roomId: roomId,
eventId: message.id,
eventId: event.eventId,
relationType: "m.annotation",
),
);
@ -349,9 +190,11 @@ class RoomChatController extends AsyncNotifier<InMemoryChatController> {
.toIList();
final reactionEvent = reactionEvents?.firstWhereOrNull(
(event) =>
event.authorId == userId &&
event.content["m.relates_to"]?["key"] == reaction,
(event) => switch (event.content) {
ReactionContent(:final key) =>
key == reaction && event.sender == userId,
_ => false,
},
);
if (reactionEvent != null) {
@ -363,7 +206,7 @@ class RoomChatController extends AsyncNotifier<InMemoryChatController> {
}
}
Future<void> sendReaction(String reaction, Message message) async {
Future<void> sendReaction(String reaction, Event event) async {
final client = ref.watch(ClientController.provider.notifier);
await client.sendEvent(
@ -372,7 +215,7 @@ class RoomChatController extends AsyncNotifier<InMemoryChatController> {
type: "m.reaction",
content: {
"m.relates_to": {
"event_id": message.id,
"event_id": event.eventId,
"rel_type": "m.annotation",
"key": reaction,
},
@ -384,7 +227,7 @@ class RoomChatController extends AsyncNotifier<InMemoryChatController> {
}
static final provider = AsyncNotifierProvider.family
.autoDispose<RoomChatController, InMemoryChatController, String>(
.autoDispose<RoomChatController, IList<Event>, String>(
RoomChatController.new,
);
}