port all controllers to new event format

This commit is contained in:
Henry Hiles 2026-05-16 15:24:05 -04:00
commit d0b148ad5b
Signed by: Henry-Hiles
SSH key fingerprint: SHA256:VKQUdS31Q90KvX7EkKMHMBpUspcmItAh86a+v7PGiIs
14 changed files with 137 additions and 562 deletions

View file

@ -8,6 +8,7 @@
"localpart", "localpart",
"msgtype", "msgtype",
"muks", "muks",
"prefs" "prefs",
"unban"
] ]
} }

View file

@ -1,46 +1,28 @@
import "dart:async"; import "dart:async";
import "package:fast_immutable_collections/fast_immutable_collections.dart";
import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:nexus/controllers/client_state_controller.dart";
import "package:nexus/controllers/user_controller.dart"; import "package:nexus/controllers/user_controller.dart";
import "package:nexus/helpers/extensions/get_localpart.dart"; import "package:nexus/models/content/membership.dart";
import "package:nexus/models/membership.dart"; import "package:nexus/models/event.dart";
import "package:nexus/models/membership_status.dart";
class AuthorController extends AsyncNotifier<Membership> { class AuthorController extends AsyncNotifier<MembershipContent> {
final Message message; final Event event;
AuthorController(this.message); AuthorController(this.event);
@override @override
Future<Membership> build() async { Future<MembershipContent> build() async {
final member = await ref.watch( final member = await ref.watch(
UserController.provider(message.sender).future, UserController.provider(event.sender).future,
); );
final pmp = message.metadata?["pmp"] == null return MembershipContent(
? null status: member.status,
: Membership.fromContent( avatarUrl: event.pmp?.avatarUrl ?? member.avatarUrl,
IMap(message.metadata?["pmp"]), displayName: event.pmp?.displayName ?? member.displayName,
message.sender,
ref.watch(
ClientStateController.provider.select(
(value) => value?.homeserverUrl,
),
) ??
"",
);
return Membership(
status: member?.status ?? MembershipStatus.leave,
avatarUrl: pmp?.avatarUrl ?? member?.avatarUrl,
displayName:
pmp?.displayName ?? member?.displayName ?? message.sender.localpart,
userId: message.sender,
); );
} }
static final provider = static final provider =
AsyncNotifierProvider.family<AuthorController, Membership, Message>( AsyncNotifierProvider.family<AuthorController, MembershipContent, Event>(
AuthorController.new, AuthorController.new,
); );
} }

View file

@ -9,7 +9,6 @@ import "package:flutter/foundation.dart";
import "package:nexus/controllers/account_data_controller.dart"; import "package:nexus/controllers/account_data_controller.dart";
import "package:nexus/controllers/client_state_controller.dart"; import "package:nexus/controllers/client_state_controller.dart";
import "package:nexus/controllers/init_complete_controller.dart"; import "package:nexus/controllers/init_complete_controller.dart";
import "package:nexus/controllers/room_chat_controller.dart";
import "package:nexus/controllers/rooms_controller.dart"; import "package:nexus/controllers/rooms_controller.dart";
import "package:nexus/controllers/space_edges_controller.dart"; import "package:nexus/controllers/space_edges_controller.dart";
import "package:nexus/controllers/sync_status_controller.dart"; import "package:nexus/controllers/sync_status_controller.dart";
@ -17,6 +16,7 @@ import "package:nexus/controllers/top_level_spaces_controller.dart";
import "package:nexus/helpers/extensions/gomuks_buffer.dart"; import "package:nexus/helpers/extensions/gomuks_buffer.dart";
import "package:nexus/main.dart"; import "package:nexus/main.dart";
import "package:nexus/models/client_state.dart"; import "package:nexus/models/client_state.dart";
import "package:nexus/models/content/content.dart";
import "package:nexus/models/event.dart"; import "package:nexus/models/event.dart";
import "package:nexus/models/paginate.dart"; import "package:nexus/models/paginate.dart";
import "package:nexus/models/requests/get_event_request.dart"; import "package:nexus/models/requests/get_event_request.dart";
@ -81,11 +81,8 @@ class ClientController extends AsyncNotifier<int> {
case "send_complete": case "send_complete":
final event = Event.fromJson(decodedMuksEvent["event"]); final event = Event.fromJson(decodedMuksEvent["event"]);
if (event.type == "m.room.message") { if (event.type == EventType.message) {
final provider = RoomChatController.provider(event.roomId); // ref.watch(provider.notifier).addEvent(event); TODO
if (ref.exists(provider)) {
ref.watch(provider.notifier).addEvent(event);
}
} }
break; break;
case "sync_complete": case "sync_complete":

View file

@ -1,25 +1,32 @@
import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:fast_immutable_collections/fast_immutable_collections.dart";
import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:nexus/controllers/members_controller.dart"; import "package:nexus/controllers/members_controller.dart";
import "package:nexus/models/membership.dart"; import "package:nexus/models/content/membership.dart";
import "package:nexus/models/event.dart";
import "package:nexus/models/membership_status.dart"; import "package:nexus/models/membership_status.dart";
class MembersByTypeController extends AsyncNotifier<IList<Membership>> { class MembersByTypeController extends AsyncNotifier<IList<Event>> {
final MembershipStatus status; final MembershipStatus filterStatus;
MembersByTypeController(this.status); MembersByTypeController(this.filterStatus);
@override @override
Future<IList<Membership>> build() => ref.watch( Future<IList<Event>> build() => ref.watch(
MembersController.provider.selectAsync( MembersController.provider.selectAsync(
(members) => (members) => members
members.where((membership) => membership.status == status).toIList(), .where(
(membership) => switch (membership.content) {
MembershipContent(:final status) => filterStatus == status,
_ => false,
},
)
.toIList(),
), ),
); );
static final provider = static final provider =
AsyncNotifierProvider.family< AsyncNotifierProvider.family<
MembersByTypeController, MembersByTypeController,
IList<Membership>, IList<Event>,
MembershipStatus MembershipStatus
>(MembersByTypeController.new); >(MembersByTypeController.new);
} }

View file

@ -1,14 +1,14 @@
import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:fast_immutable_collections/fast_immutable_collections.dart";
import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:nexus/controllers/client_controller.dart"; import "package:nexus/controllers/client_controller.dart";
import "package:nexus/controllers/client_state_controller.dart";
import "package:nexus/controllers/selected_room_controller.dart"; import "package:nexus/controllers/selected_room_controller.dart";
import "package:nexus/models/membership.dart"; import "package:nexus/models/content/content.dart";
import "package:nexus/models/event.dart";
import "package:nexus/models/requests/get_room_state_request.dart"; import "package:nexus/models/requests/get_room_state_request.dart";
class MembersController extends AsyncNotifier<IList<Membership>> { class MembersController extends AsyncNotifier<IList<Event>> {
@override @override
Future<IList<Membership>> build() async { Future<IList<Event>> build() async {
final data = ref.watch( final data = ref.watch(
SelectedRoomController.provider.select( SelectedRoomController.provider.select(
(value) => value?.metadata == null (value) => value?.metadata == null
@ -28,25 +28,11 @@ class MembersController extends AsyncNotifier<IList<Membership>> {
), ),
); );
return state.nonNulls return state.where((state) => state.type == EventType.membership).toIList();
.where((state) => state.type == "m.room.member")
.map(
(membership) => Membership.fromContent(
membership.content,
membership.stateKey!,
ref.watch(
ClientStateController.provider.select(
(value) => value?.homeserverUrl,
),
) ??
"",
),
)
.toIList();
} }
static final provider = static final provider =
AsyncNotifierProvider<MembersController, IList<Membership>>( AsyncNotifierProvider<MembersController, IList<Event>>(
MembersController.new, MembersController.new,
); );
} }

View file

@ -1,214 +0,0 @@
import "package:collection/collection.dart";
import "package:fast_immutable_collections/fast_immutable_collections.dart";
import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:nexus/controllers/client_controller.dart";
import "package:nexus/controllers/client_state_controller.dart";
import "package:nexus/helpers/extensions/mxc_to_https.dart";
import "package:nexus/models/configs/message_config.dart";
import "package:nexus/models/requests/get_related_events_request.dart";
class MessageController extends AsyncNotifier<Message?> {
final MessageConfig config;
MessageController(this.config);
@override
Future<Message?> build() async {
try {
final isEdit = config.event.relationType == "m.replace";
if ((isEdit && !config.includeEdits) || config.room.metadata == null) {
return null;
}
final event = config.event.lastEditRowId == null
? config.event
: config.room.events.firstWhereOrNull(
(e) => e.rowId == config.event.lastEditRowId,
) ??
config.event;
final decrypted = (event.decrypted ?? event.content);
final type = (config.event.decryptedType ?? config.event.type);
final content = decrypted["m.new_content"] == null
? decrypted
: IMap(decrypted["m.new_content"]);
final homeserver = ref
.read(ClientStateController.provider)
?.homeserverUrl;
final source = homeserver == null || content["url"] == null
? "null"
: Uri.parse(content["url"]).mxcToHttps(homeserver).toString();
final metadata = {
"body": config.event.redactedBy == null
? (content["body"] ?? "")
: "Deleted Message",
"flashing": false,
"timelineId": event.timelineRowId,
"big": event.localContent?.bigEmoji == true,
"eventType": type,
"pmp": content["com.beeper.per_message_profile"],
"error": event.sendError,
"format": content["format"] ?? content["format"],
"editSource": event.localContent?.editSource ?? content["body"],
"txnId": config.event.transactionId,
};
final editedAt = event.relationType == "m.replace"
? event.timestamp
: null;
if ((event.redactedBy != null && !config.alwaysReturn) ||
(!config.includeEdits &&
(config.event.relationType == "m.replace"))) {
return null;
}
final replyId =
config.event.content["m.relates_to"]?["m.in_reply_to"]?["event_id"];
final reactionEvents = config.event.reactions.isEmpty && !isEdit
? null
: await ref
.watch(ClientController.provider.notifier)
.getRelatedEvents(
GetRelatedEventsRequest(
roomId: config.room.metadata!.id,
eventId:
(isEdit ? config.event.relatesTo : null) ??
config.event.eventId,
relationType: "m.annotation",
),
);
final reactions = reactionEvents
?.where((event) => event.redactedBy == null)
.fold<IMap<String, IList<String>>>(IMap(), (acc, event) {
final key = event.content["m.relates_to"]?["key"];
if (key == null) return acc;
return acc.update(
key,
(list) => list.add(event.sender),
ifAbsent: () => IList([event.sender]),
);
})
.map((key, value) => MapEntry(key, value.unlock))
.unlock;
final asText =
Message.text(
metadata: metadata,
id: config.event.eventId,
reactions: reactions,
sender: event.sender,
text: content["formatted_body"] ?? content["body"] ?? "",
replyToMessageId: replyId,
deliveredAt: config.event.timestamp,
editedAt: editedAt,
)
as TextMessage;
Message toSystemMessage(String content) => Message.system(
metadata: {...metadata, "body": content},
id: config.event.eventId,
reactions: reactions,
sender: event.sender,
deliveredAt: config.event.timestamp,
text: content,
);
return switch (type) {
"m.room.encrypted" => asText.copyWith(
text: "Unable to decrypt message.",
metadata: {...metadata, "body": "Unable to decrypt message."},
),
// "org.matrix.msc3381.poll.start" => Message.custom(
// metadata: {
// ...metadata,
// "poll": event.parsedPollEventContent.pollStartContent,
// "responses": event.getPollResponses(timeline),
// },
// id: eventId,
// deliveredAt: originServerTs,
// sender: senderId,
// ),
("m.sticker" || "m.room.message") => switch (content["msgtype"]) {
null || "m.image" => Message.image(
id: config.event.eventId,
sender: event.sender,
reactions: reactions,
source: source,
replyToMessageId: replyId,
metadata: metadata,
text: asText.text,
deliveredAt: config.event.timestamp,
blurhash: (content["info"] as Map?)?["xyz.amorgan.blurhash"],
),
"m.audio" || "m.file" => Message.file(
name: content["filename"].toString(),
size: content["info"]["size"],
metadata: metadata,
id: config.event.eventId,
reactions: reactions,
sender: event.sender,
source: source,
replyToMessageId: replyId,
deliveredAt: config.event.timestamp,
),
_ => asText,
},
"m.room.member" =>
content["membership"] == event.unsigned["prev_content"]?["membership"]
? null
: toSystemMessage(
"${content["displayname"] ?? event.stateKey} ${switch (content["membership"]) {
"invite" => "was invited to",
"join" => "joined",
"leave" => event.sender == event.stateKey ? "left" : (event.unsigned["prev_content"]?["membership"] == "ban" ? "was unbanned from" : "was kicked from"),
"ban" => "was banned from",
"knock" => "asked to join",
_ => "did something relating to",
}} the room. ${content["reason"] ?? ""}",
),
"m.room.server_acl" => toSystemMessage(
"${event.sender} updated the server ban list.",
),
"m.room.redaction" =>
config.alwaysReturn
? asText.copyWith(
metadata: {
...(asText.metadata ?? {}),
"body": "Deleted Message",
},
)
: null,
_ =>
config.alwaysReturn
? asText
: (
// Turn this on for debugging purposes
false
// ignore: dead_code
? Message.unsupported(
metadata: metadata,
reactions: reactions,
id: config.event.eventId,
sender: event.sender,
replyToMessageId: replyId,
)
: null),
};
} catch (error) {
return null;
}
}
static final provider = AsyncNotifierProvider.family
.autoDispose<MessageController, Message?, MessageConfig>(
MessageController.new,
);
}

View file

@ -1,26 +0,0 @@
import "package:fast_immutable_collections/fast_immutable_collections.dart";
import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:nexus/controllers/message_controller.dart";
import "package:nexus/models/configs/message_config.dart";
import "package:nexus/models/configs/messages_config.dart";
class MessagesController extends AsyncNotifier<IList<Message>> {
final MessagesConfig config;
MessagesController(this.config);
@override
Future<IList<Message>> build() async => (await Future.wait(
config.events.map(
(event) => ref.watch(
MessageController.provider(
MessageConfig(event: event, room: config.room),
).future,
),
),
)).nonNulls.toIList();
static final provider = AsyncNotifierProvider.family
.autoDispose<MessagesController, IList<Message>, MessagesConfig>(
MessagesController.new,
);
}

View file

@ -3,6 +3,8 @@ import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:nexus/controllers/client_state_controller.dart"; import "package:nexus/controllers/client_state_controller.dart";
import "package:nexus/controllers/selected_room_controller.dart"; import "package:nexus/controllers/selected_room_controller.dart";
import "package:nexus/models/configs/power_level_config.dart"; import "package:nexus/models/configs/power_level_config.dart";
import "package:nexus/models/content/content.dart";
import "package:nexus/models/content/power_levels.dart";
import "package:nexus/models/requests/membership_action.dart"; import "package:nexus/models/requests/membership_action.dart";
class PowerLevelController extends Notifier<bool> { class PowerLevelController extends Notifier<bool> {
@ -13,17 +15,15 @@ class PowerLevelController extends Notifier<bool> {
bool build() { bool build() {
final room = ref.watch(SelectedRoomController.provider); final room = ref.watch(SelectedRoomController.provider);
final event = room?.events.firstWhereOrNull( final event = room?.events.firstWhereOrNull(
(event) => event.rowId == room.state["m.room.power_levels"]?[""], (event) => event.rowId == room.state[EventType.powerLevels.type]?[""],
); );
final user = ref.watch(ClientStateController.provider)?.userId; final user = ref.watch(ClientStateController.provider)?.userId;
if (event == null || user == null) return false; if (user == null || event?.content is! PowerLevelsContent) return false;
final users = (event.content["users"] as Map<String, dynamic>? ?? {}); final content = event?.content as PowerLevelsContent;
final events = (event.content["events"] as Map<String, dynamic>? ?? {});
int powerLevelOf(String userId) => users.containsKey(userId) int powerLevelOf(String userId) =>
? (users[userId] as int) content.users[userId] ?? content.usersDefault;
: (event.content["users_default"] as int? ?? 0);
final userLevel = powerLevelOf(user); final userLevel = powerLevelOf(user);
final targetLevel = config.targetUser != null final targetLevel = config.targetUser != null
@ -32,33 +32,29 @@ class PowerLevelController extends Notifier<bool> {
if (config.action != null) { if (config.action != null) {
return switch (config.action!) { return switch (config.action!) {
MembershipAction.invite => MembershipAction.invite => userLevel >= content.invite,
userLevel >= (event.content["invite"] as int? ?? 0),
MembershipAction.kick => MembershipAction.kick =>
targetLevel != null && targetLevel != null &&
userLevel >= (event.content["kick"] as int? ?? 50) && userLevel >= content.kick &&
userLevel > targetLevel, userLevel > targetLevel,
MembershipAction.ban => MembershipAction.ban =>
targetLevel != null && targetLevel != null &&
userLevel >= (event.content["ban"] as int? ?? 50) && userLevel >= content.ban &&
userLevel > targetLevel, userLevel > targetLevel,
MembershipAction.unban => MembershipAction.unban => userLevel >= content.ban,
userLevel >= (event.content["ban"] as int? ?? 50),
}; };
} }
if (config.eventType == "m.room.redaction") { if (config.eventType == "m.room.redaction") {
return userLevel >= (event.content["redact"] as int? ?? 50); return userLevel >= content.redact;
} }
final requiredLevel = events.containsKey(config.eventType) final requiredLevel =
? (events[config.eventType] as int) content.events[config.eventType] ??
: (config.isStateEvent (config.isStateEvent ? content.stateDefault : content.eventsDefault);
? (event.content["state_default"] as int? ?? 50)
: (event.content["events_default"] as int? ?? 0));
return userLevel >= requiredLevel; return userLevel >= requiredLevel;
} }

View file

@ -4,12 +4,8 @@ import "package:fast_immutable_collections/fast_immutable_collections.dart";
import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:fluttertagger/fluttertagger.dart"; import "package:fluttertagger/fluttertagger.dart";
import "package:nexus/controllers/client_controller.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/rooms_controller.dart"; import "package:nexus/controllers/rooms_controller.dart";
import "package:nexus/controllers/selected_room_controller.dart"; import "package:nexus/models/content/reaction.dart";
import "package:nexus/models/configs/messages_config.dart";
import "package:nexus/models/configs/message_config.dart";
import "package:nexus/models/event.dart"; import "package:nexus/models/event.dart";
import "package:nexus/models/requests/get_related_events_request.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/get_room_state_request.dart";
@ -27,8 +23,9 @@ class RoomChatController extends AsyncNotifier<IList<Event>> {
@override @override
Future<IList<Event>> build() async { Future<IList<Event>> build() async {
final client = ref.watch(ClientController.provider.notifier); final client = ref.watch(ClientController.provider.notifier);
var room = ref.read(RoomsController.provider)[roomId]; final room = ref.watch(RoomsController.provider)[roomId];
if (room == null) return InMemoryChatController(); if (room == null) return const IList.empty();
final state = await client.getRoomState( final state = await client.getRoomState(
GetRoomStateRequest(roomId: roomId), GetRoomStateRequest(roomId: roomId),
); );
@ -42,8 +39,9 @@ class RoomChatController extends AsyncNotifier<IList<Event>> {
state: state.fold( state: state.fold(
const IMap.empty(), const IMap.empty(),
(previousValue, stateEvent) => previousValue.add( (previousValue, stateEvent) => previousValue.add(
stateEvent.type, stateEvent.type.type,
(previousValue[stateEvent.type] ?? const IMap.empty()).addAll( (previousValue[stateEvent.type.type] ?? const IMap.empty())
.addAll(
IMap({ IMap({
if (stateEvent.stateKey != null) if (stateEvent.stateKey != null)
stateEvent.stateKey!: stateEvent.rowId, stateEvent.stateKey!: stateEvent.rowId,
@ -56,152 +54,29 @@ class RoomChatController extends AsyncNotifier<IList<Event>> {
const ISet.empty(), const ISet.empty(),
); );
room = ref.read(RoomsController.provider)[roomId]; // While there are under 20 messages, try up to load more messages until there's no more or we have 20 messages.
if (room == null) return InMemoryChatController(); if (room.hasMore && room.events.length < 20) {
loadOlder();
}
final messages = await ref.watch( return room.timeline
MessagesController.provider(
MessagesConfig(
room: room,
events: room.timeline
.map( .map(
(timelineRowTuple) => room!.events.firstWhereOrNull( (timeline) => room.events.firstWhereOrNull(
(event) => event.rowId == timelineRowTuple.eventRowId, (event) => event.rowId == timeline.eventRowId,
), ),
) )
.nonNulls .nonNulls
.toIList(), .toIList();
),
).future,
);
// While there are under 20 messages, try up to load more messages until there's no more or we have 20 messages.
final controller = InMemoryChatController(messages: messages.toList());
for (var more = true; more == true && controller.messages.length < 20;) {
more = await loadOlder(controller);
} }
ref.onDispose(controller.dispose); Future<void> deleteMessage(Event event, {String? reason}) => ref
return controller;
}
Future<void> addEvent(Event event) async {
final controller = await future;
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.sender],
ifAbsent: () => [event.sender],
)
.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.sender).unlock,
)
.where((_, value) => value.isNotEmpty)
.unlock,
),
);
}
} else {
final message = await ref.watch(
MessageController.provider(
MessageConfig(
event: event,
room: ref.read(RoomsController.provider)[roomId]!,
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);
}
}
}
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) .watch(ClientController.provider.notifier)
.redactEvent( .redactEvent(
RedactEventRequest(eventId: message.id, roomId: roomId, reason: reason), RedactEventRequest(
eventId: event.eventId,
roomId: roomId,
reason: reason,
),
); );
Future<bool> loadOlder() async { Future<bool> loadOlder() async {
@ -247,7 +122,7 @@ class RoomChatController extends AsyncNotifier<IList<Event>> {
bool shouldMention = true, bool shouldMention = true,
required IList<Tag> tags, required IList<Tag> tags,
required RelationType relationType, required RelationType relationType,
Message? relation, Event? relation,
}) async { }) async {
var taggedMessage = text; var taggedMessage = text;
@ -262,7 +137,6 @@ class RoomChatController extends AsyncNotifier<IList<Event>> {
} }
final client = ref.watch(ClientController.provider.notifier); final client = ref.watch(ClientController.provider.notifier);
final room = ref.read(RoomsController.provider)[roomId];
final event = await client.sendMessage( final event = await client.sendMessage(
SendMessageRequest( SendMessageRequest(
roomId: roomId, roomId: roomId,
@ -278,45 +152,39 @@ class RoomChatController extends AsyncNotifier<IList<Event>> {
text: taggedMessage, text: taggedMessage,
relation: relation == null relation: relation == null
? null ? null
: Relation(eventId: relation.id, relationType: relationType), : Relation(eventId: relation.eventId, relationType: relationType),
), ),
); );
final message = room == null
? null
: await ref.watch(
MessageController.provider(
MessageConfig(room: room, event: event),
).future,
);
if (message != null) insertMessage(message); // state = state TODO
} }
Future<void> scrollToMessage(Message message) async { Future<void> scrollToEvent(Event event) async {
final controller = await future; // TODO: Impl
Future<void> setFlashing(bool flashing) => controller.updateMessage( // final controller = await future;
message, // Future<void> setFlashing(bool flashing) => controller.updateMessage(
message.copyWith( // message,
metadata: {...(message.metadata ?? {}), "flashing": flashing}, // message.copyWith(
), // metadata: {...(message.metadata ?? {}), "flashing": flashing},
); // ),
// );
await setFlashing(true); // await setFlashing(true);
Timer(Duration(seconds: 1), () => setFlashing(false)); // Timer(Duration(seconds: 1), () => setFlashing(false));
return await controller.scrollToMessage(message.id); // return await controller.scrollToMessage(message.id);
} }
Future<void> removeReaction( Future<void> removeReaction(
String reaction, String reaction,
Message message, Event event,
String userId, String userId,
) async { ) async {
final client = ref.watch(ClientController.provider.notifier); final client = ref.watch(ClientController.provider.notifier);
final allReactionEvents = await client.getRelatedEvents( final allReactionEvents = await client.getRelatedEvents(
GetRelatedEventsRequest( GetRelatedEventsRequest(
roomId: roomId, roomId: roomId,
eventId: message.id, eventId: event.eventId,
relationType: "m.annotation", relationType: "m.annotation",
), ),
); );
@ -326,9 +194,11 @@ class RoomChatController extends AsyncNotifier<IList<Event>> {
.toIList(); .toIList();
final reactionEvent = reactionEvents?.firstWhereOrNull( final reactionEvent = reactionEvents?.firstWhereOrNull(
(event) => (event) => switch (event.content) {
event.sender == userId && ReactionContent(:final key) =>
event.content["m.relates_to"]?["key"] == reaction, key == reaction && event.sender == userId,
_ => false,
},
); );
if (reactionEvent != null) { if (reactionEvent != null) {
@ -340,7 +210,7 @@ class RoomChatController extends AsyncNotifier<IList<Event>> {
} }
} }
Future<void> sendReaction(String reaction, Message message) async { Future<void> sendReaction(String reaction, Event event) async {
final client = ref.watch(ClientController.provider.notifier); final client = ref.watch(ClientController.provider.notifier);
await client.sendEvent( await client.sendEvent(
@ -349,7 +219,7 @@ class RoomChatController extends AsyncNotifier<IList<Event>> {
type: "m.reaction", type: "m.reaction",
content: { content: {
"m.relates_to": { "m.relates_to": {
"event_id": message.id, "event_id": event.eventId,
"rel_type": "m.annotation", "rel_type": "m.annotation",
"key": reaction, "key": reaction,
}, },

View file

@ -4,37 +4,37 @@ import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:nexus/controllers/members_controller.dart"; import "package:nexus/controllers/members_controller.dart";
import "package:nexus/controllers/profile_controller.dart"; import "package:nexus/controllers/profile_controller.dart";
import "package:nexus/helpers/extensions/get_localpart.dart"; import "package:nexus/helpers/extensions/get_localpart.dart";
import "package:nexus/models/membership.dart"; import "package:nexus/models/content/membership.dart";
import "package:nexus/models/membership_status.dart"; import "package:nexus/models/membership_status.dart";
class UserController extends AsyncNotifier<Membership?> { class UserController extends AsyncNotifier<MembershipContent> {
final String userId; final String userId;
UserController(this.userId); UserController(this.userId);
@override @override
Future<Membership?> build() async { Future<MembershipContent> build() async {
final member = await ref.watch( final member = await ref.watch(
MembersController.provider.selectAsync( MembersController.provider.selectAsync(
(value) => (value) => value.firstWhereOrNull(
value.firstWhereOrNull((membership) => membership.userId == userId), (membership) => membership.stateKey == userId,
),
), ),
); );
if (member != null) return member; if (member is MembershipContent) {
return member!.content as MembershipContent;
}
final profile = await ref.watch(ProfileController.provider(userId).future); final profile = await ref.watch(ProfileController.provider(userId).future);
return Membership( return MembershipContent(
status: MembershipStatus.leave, status: MembershipStatus.leave,
avatarUrl: profile.avatarUrl == null avatarUrl: profile.avatarUrl,
? null
: Uri.tryParse(profile.avatarUrl!),
displayName: profile.displayName ?? userId.localpart, displayName: profile.displayName ?? userId.localpart,
userId: userId,
); );
} }
static final provider = static final provider =
AsyncNotifierProvider.family<UserController, Membership?, String>( AsyncNotifierProvider.family<UserController, MembershipContent, String>(
UserController.new, UserController.new,
); );
} }

View file

@ -2,6 +2,10 @@ import "package:collection/collection.dart";
import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:fast_immutable_collections/fast_immutable_collections.dart";
import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:nexus/controllers/client_state_controller.dart"; import "package:nexus/controllers/client_state_controller.dart";
import "package:nexus/models/content/content.dart";
import "package:nexus/models/content/membership.dart";
import "package:nexus/models/content/power_levels.dart";
import "package:nexus/models/membership_status.dart";
import "package:nexus/models/room.dart"; import "package:nexus/models/room.dart";
class ViaController extends Notifier<String> { class ViaController extends Notifier<String> {
@ -22,23 +26,27 @@ class ViaController extends Notifier<String> {
addUserId(ref.watch(ClientStateController.provider)?.userId); addUserId(ref.watch(ClientStateController.provider)?.userId);
final powerLevels = room.events.firstWhereOrNull( final powerLevels = room.events.firstWhereOrNull(
(event) => event.rowId == room.state["m.room.power_levels"]?[""], (event) => event.rowId == room.state[EventType.powerLevels.type]?[""],
); );
for (final userId in IMap(powerLevels?.content["users"]).keys) { if (powerLevels?.content case PowerLevelsContent(:final users)) {
for (final userId in users.keys) {
addUserId(userId); addUserId(userId);
if (servers.length >= 5) break; if (servers.length >= 5) break;
} }
}
final members = room.state["m.room.member"]?.values.toIList(); final members = room.state[EventType.membership.type]?.values.toIList();
for (var i = 0; servers.length < 5; i++) { for (var i = 0; servers.length < 5; i++) {
final member = room.events.firstWhereOrNull( final member = room.events.firstWhereOrNull(
(event) => event.rowId == members?.getOrNull(i), (event) => event.rowId == members?.getOrNull(i),
); );
if (member?.content["membership"] == "join") { if (member?.content case MembershipContent(:final status)) {
if (status == MembershipStatus.join) {
addUserId(member?.stateKey); addUserId(member?.stateKey);
} }
}
if (members?.getOrNull(i) == null) break; if (members?.getOrNull(i) == null) break;
} }

View file

@ -9,9 +9,9 @@ abstract class MembershipContent extends Content with _$MembershipContent {
MembershipContent._(); MembershipContent._();
const factory MembershipContent({ const factory MembershipContent({
@JsonKey(name: "displayname") required String displayName, @JsonKey(name: "displayname") required String displayName,
required MembershipStatus membership, @JsonKey(name: "membership") required MembershipStatus status,
required Uri? avatarUrl, Uri? avatarUrl,
required String? reason, String? reason,
}) = _MembershipContent; }) = _MembershipContent;
factory MembershipContent.fromJson(Map<String, Object?> json) => factory MembershipContent.fromJson(Map<String, Object?> json) =>

View file

@ -1,32 +0,0 @@
import "package:fast_immutable_collections/fast_immutable_collections.dart";
import "package:freezed_annotation/freezed_annotation.dart";
import "package:nexus/helpers/extensions/mxc_to_https.dart";
import "package:nexus/models/membership_status.dart";
part "membership.freezed.dart";
@freezed
abstract class Membership with _$Membership {
const Membership._();
const factory Membership({
required MembershipStatus status,
required Uri? avatarUrl,
required String displayName,
required String userId,
}) = _Membership;
factory Membership.fromContent(
IMap<String, dynamic> content,
String userId,
String homeserver,
) => Membership(
status: MembershipStatus.values.firstWhere(
(status) => status.name == content["membership"],
orElse: () => MembershipStatus.leave,
),
avatarUrl: Uri.tryParse(
content["avatar_url"] ?? "",
)?.mxcToHttps(homeserver),
userId: userId,
displayName: content["displayname"] ?? userId.substring(1).split(":").first,
);
}

View file

@ -386,7 +386,7 @@ class RoomChat extends HookConsumerWidget {
message, message,
content: message.text, content: message.text,
groupStatus: groupStatus, groupStatus: groupStatus,
onTapReply: notifier.scrollToMessage, onTapReply: notifier.scrollToEvent,
updateMessage: controller.updateMessage, updateMessage: controller.updateMessage,
isSentByMe: isSentByMe, isSentByMe: isSentByMe,
), ),
@ -402,7 +402,7 @@ class RoomChat extends HookConsumerWidget {
message, message,
content: message.text, content: message.text,
groupStatus: groupStatus, groupStatus: groupStatus,
onTapReply: notifier.scrollToMessage, onTapReply: notifier.scrollToEvent,
updateMessage: controller.updateMessage, updateMessage: controller.updateMessage,
isSentByMe: isSentByMe, isSentByMe: isSentByMe,
extra: ExpandableImageMessage(message), extra: ExpandableImageMessage(message),
@ -429,7 +429,7 @@ class RoomChat extends HookConsumerWidget {
child: FlyerChatFileMessage( child: FlyerChatFileMessage(
topWidget: ReplyWidget( topWidget: ReplyWidget(
message, message,
onTapReply: notifier.scrollToMessage, onTapReply: notifier.scrollToEvent,
groupStatus: groupStatus, groupStatus: groupStatus,
), ),
message: message, message: message,