diff --git a/.vscode/settings.json b/.vscode/settings.json index 855ee97..a0d46c9 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,6 +8,7 @@ "localpart", "msgtype", "muks", - "prefs" + "prefs", + "unban" ] } diff --git a/lib/controllers/author_controller.dart b/lib/controllers/author_controller.dart index 7dcdb23..8499775 100644 --- a/lib/controllers/author_controller.dart +++ b/lib/controllers/author_controller.dart @@ -1,46 +1,28 @@ import "dart:async"; -import "package:fast_immutable_collections/fast_immutable_collections.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/helpers/extensions/get_localpart.dart"; -import "package:nexus/models/membership.dart"; -import "package:nexus/models/membership_status.dart"; +import "package:nexus/models/content/membership.dart"; +import "package:nexus/models/event.dart"; -class AuthorController extends AsyncNotifier { - final Message message; - AuthorController(this.message); +class AuthorController extends AsyncNotifier { + final Event event; + AuthorController(this.event); @override - Future build() async { + Future build() async { final member = await ref.watch( - UserController.provider(message.sender).future, + UserController.provider(event.sender).future, ); - final pmp = message.metadata?["pmp"] == null - ? null - : Membership.fromContent( - IMap(message.metadata?["pmp"]), - 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, + return MembershipContent( + status: member.status, + avatarUrl: event.pmp?.avatarUrl ?? member.avatarUrl, + displayName: event.pmp?.displayName ?? member.displayName, ); } static final provider = - AsyncNotifierProvider.family( + AsyncNotifierProvider.family( AuthorController.new, ); } diff --git a/lib/controllers/client_controller.dart b/lib/controllers/client_controller.dart index 4787b60..f26b8ca 100644 --- a/lib/controllers/client_controller.dart +++ b/lib/controllers/client_controller.dart @@ -9,7 +9,6 @@ import "package:flutter/foundation.dart"; import "package:nexus/controllers/account_data_controller.dart"; import "package:nexus/controllers/client_state_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/space_edges_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/main.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/paginate.dart"; import "package:nexus/models/requests/get_event_request.dart"; @@ -81,11 +81,8 @@ class ClientController extends AsyncNotifier { case "send_complete": final event = Event.fromJson(decodedMuksEvent["event"]); - if (event.type == "m.room.message") { - final provider = RoomChatController.provider(event.roomId); - if (ref.exists(provider)) { - ref.watch(provider.notifier).addEvent(event); - } + if (event.type == EventType.message) { + // ref.watch(provider.notifier).addEvent(event); TODO } break; case "sync_complete": diff --git a/lib/controllers/members_by_type_controller.dart b/lib/controllers/members_by_type_controller.dart index cdc8d07..c96dc27 100644 --- a/lib/controllers/members_by_type_controller.dart +++ b/lib/controllers/members_by_type_controller.dart @@ -1,25 +1,32 @@ import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:flutter_riverpod/flutter_riverpod.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"; -class MembersByTypeController extends AsyncNotifier> { - final MembershipStatus status; - MembersByTypeController(this.status); +class MembersByTypeController extends AsyncNotifier> { + final MembershipStatus filterStatus; + MembersByTypeController(this.filterStatus); @override - Future> build() => ref.watch( + Future> build() => ref.watch( MembersController.provider.selectAsync( - (members) => - members.where((membership) => membership.status == status).toIList(), + (members) => members + .where( + (membership) => switch (membership.content) { + MembershipContent(:final status) => filterStatus == status, + _ => false, + }, + ) + .toIList(), ), ); static final provider = AsyncNotifierProvider.family< MembersByTypeController, - IList, + IList, MembershipStatus >(MembersByTypeController.new); } diff --git a/lib/controllers/members_controller.dart b/lib/controllers/members_controller.dart index 39666d4..570a233 100644 --- a/lib/controllers/members_controller.dart +++ b/lib/controllers/members_controller.dart @@ -1,14 +1,14 @@ 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/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"; -class MembersController extends AsyncNotifier> { +class MembersController extends AsyncNotifier> { @override - Future> build() async { + Future> build() async { final data = ref.watch( SelectedRoomController.provider.select( (value) => value?.metadata == null @@ -28,25 +28,11 @@ class MembersController extends AsyncNotifier> { ), ); - return state.nonNulls - .where((state) => state.type == "m.room.member") - .map( - (membership) => Membership.fromContent( - membership.content, - membership.stateKey!, - ref.watch( - ClientStateController.provider.select( - (value) => value?.homeserverUrl, - ), - ) ?? - "", - ), - ) - .toIList(); + return state.where((state) => state.type == EventType.membership).toIList(); } static final provider = - AsyncNotifierProvider>( + AsyncNotifierProvider>( MembersController.new, ); } diff --git a/lib/controllers/message_controller.dart b/lib/controllers/message_controller.dart deleted file mode 100644 index d52a835..0000000 --- a/lib/controllers/message_controller.dart +++ /dev/null @@ -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 { - final MessageConfig config; - MessageController(this.config); - - @override - Future 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(), (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.new, - ); -} diff --git a/lib/controllers/messages_controller.dart b/lib/controllers/messages_controller.dart deleted file mode 100644 index 80eea26..0000000 --- a/lib/controllers/messages_controller.dart +++ /dev/null @@ -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> { - final MessagesConfig config; - MessagesController(this.config); - - @override - Future> 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, MessagesConfig>( - MessagesController.new, - ); -} diff --git a/lib/controllers/power_level_controller.dart b/lib/controllers/power_level_controller.dart index 41b5f19..7d377a5 100644 --- a/lib/controllers/power_level_controller.dart +++ b/lib/controllers/power_level_controller.dart @@ -3,6 +3,8 @@ import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:nexus/controllers/client_state_controller.dart"; import "package:nexus/controllers/selected_room_controller.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"; class PowerLevelController extends Notifier { @@ -13,17 +15,15 @@ class PowerLevelController extends Notifier { bool build() { final room = ref.watch(SelectedRoomController.provider); 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; - if (event == null || user == null) return false; + if (user == null || event?.content is! PowerLevelsContent) return false; - final users = (event.content["users"] as Map? ?? {}); - final events = (event.content["events"] as Map? ?? {}); + final content = event?.content as PowerLevelsContent; - int powerLevelOf(String userId) => users.containsKey(userId) - ? (users[userId] as int) - : (event.content["users_default"] as int? ?? 0); + int powerLevelOf(String userId) => + content.users[userId] ?? content.usersDefault; final userLevel = powerLevelOf(user); final targetLevel = config.targetUser != null @@ -32,33 +32,29 @@ class PowerLevelController extends Notifier { if (config.action != null) { return switch (config.action!) { - MembershipAction.invite => - userLevel >= (event.content["invite"] as int? ?? 0), + MembershipAction.invite => userLevel >= content.invite, MembershipAction.kick => targetLevel != null && - userLevel >= (event.content["kick"] as int? ?? 50) && + userLevel >= content.kick && userLevel > targetLevel, MembershipAction.ban => targetLevel != null && - userLevel >= (event.content["ban"] as int? ?? 50) && + userLevel >= content.ban && userLevel > targetLevel, - MembershipAction.unban => - userLevel >= (event.content["ban"] as int? ?? 50), + MembershipAction.unban => userLevel >= content.ban, }; } if (config.eventType == "m.room.redaction") { - return userLevel >= (event.content["redact"] as int? ?? 50); + return userLevel >= content.redact; } - final requiredLevel = events.containsKey(config.eventType) - ? (events[config.eventType] as int) - : (config.isStateEvent - ? (event.content["state_default"] as int? ?? 50) - : (event.content["events_default"] as int? ?? 0)); + final requiredLevel = + content.events[config.eventType] ?? + (config.isStateEvent ? content.stateDefault : content.eventsDefault); return userLevel >= requiredLevel; } diff --git a/lib/controllers/room_chat_controller.dart b/lib/controllers/room_chat_controller.dart index e99b5a2..7b970c7 100644 --- a/lib/controllers/room_chat_controller.dart +++ b/lib/controllers/room_chat_controller.dart @@ -4,12 +4,8 @@ import "package:fast_immutable_collections/fast_immutable_collections.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/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"; @@ -27,8 +23,9 @@ class RoomChatController extends AsyncNotifier> { @override Future> build() async { final client = ref.watch(ClientController.provider.notifier); - var room = ref.read(RoomsController.provider)[roomId]; - if (room == null) return InMemoryChatController(); + final room = ref.watch(RoomsController.provider)[roomId]; + if (room == null) return const IList.empty(); + final state = await client.getRoomState( GetRoomStateRequest(roomId: roomId), ); @@ -42,13 +39,14 @@ class RoomChatController extends AsyncNotifier> { 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, - }), - ), + stateEvent.type.type, + (previousValue[stateEvent.type.type] ?? const IMap.empty()) + .addAll( + IMap({ + if (stateEvent.stateKey != null) + stateEvent.stateKey!: stateEvent.rowId, + }), + ), ), ), ), @@ -56,152 +54,29 @@ class RoomChatController extends AsyncNotifier> { 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, - ); - // 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); + if (room.hasMore && room.events.length < 20) { + loadOlder(); } - ref.onDispose(controller.dispose); - return controller; + return room.timeline + .map( + (timeline) => room.events.firstWhereOrNull( + (event) => event.rowId == timeline.eventRowId, + ), + ) + .nonNulls + .toIList(); } - Future 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 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 deleteMessage(Message message, {String? reason}) => ref + Future deleteMessage(Event event, {String? reason}) => ref .watch(ClientController.provider.notifier) .redactEvent( - RedactEventRequest(eventId: message.id, roomId: roomId, reason: reason), + RedactEventRequest( + eventId: event.eventId, + roomId: roomId, + reason: reason, + ), ); Future loadOlder() async { @@ -247,7 +122,7 @@ class RoomChatController extends AsyncNotifier> { bool shouldMention = true, required IList tags, required RelationType relationType, - Message? relation, + Event? relation, }) async { var taggedMessage = text; @@ -262,7 +137,6 @@ class RoomChatController extends AsyncNotifier> { } final client = ref.watch(ClientController.provider.notifier); - final room = ref.read(RoomsController.provider)[roomId]; final event = await client.sendMessage( SendMessageRequest( roomId: roomId, @@ -278,45 +152,39 @@ class RoomChatController extends AsyncNotifier> { text: taggedMessage, relation: relation == 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 scrollToMessage(Message message) async { - final controller = await future; - Future setFlashing(bool flashing) => controller.updateMessage( - message, - message.copyWith( - metadata: {...(message.metadata ?? {}), "flashing": flashing}, - ), - ); + Future scrollToEvent(Event event) async { + // TODO: Impl + // final controller = await future; + // Future setFlashing(bool flashing) => controller.updateMessage( + // message, + // message.copyWith( + // metadata: {...(message.metadata ?? {}), "flashing": flashing}, + // ), + // ); - await setFlashing(true); - Timer(Duration(seconds: 1), () => setFlashing(false)); + // await setFlashing(true); + // Timer(Duration(seconds: 1), () => setFlashing(false)); - return await controller.scrollToMessage(message.id); + // return await controller.scrollToMessage(message.id); } Future 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", ), ); @@ -326,9 +194,11 @@ class RoomChatController extends AsyncNotifier> { .toIList(); final reactionEvent = reactionEvents?.firstWhereOrNull( - (event) => - event.sender == userId && - event.content["m.relates_to"]?["key"] == reaction, + (event) => switch (event.content) { + ReactionContent(:final key) => + key == reaction && event.sender == userId, + _ => false, + }, ); if (reactionEvent != null) { @@ -340,7 +210,7 @@ class RoomChatController extends AsyncNotifier> { } } - Future sendReaction(String reaction, Message message) async { + Future sendReaction(String reaction, Event event) async { final client = ref.watch(ClientController.provider.notifier); await client.sendEvent( @@ -349,7 +219,7 @@ class RoomChatController extends AsyncNotifier> { type: "m.reaction", content: { "m.relates_to": { - "event_id": message.id, + "event_id": event.eventId, "rel_type": "m.annotation", "key": reaction, }, diff --git a/lib/controllers/user_controller.dart b/lib/controllers/user_controller.dart index e7ca973..d3976bd 100644 --- a/lib/controllers/user_controller.dart +++ b/lib/controllers/user_controller.dart @@ -4,37 +4,37 @@ import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:nexus/controllers/members_controller.dart"; import "package:nexus/controllers/profile_controller.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"; -class UserController extends AsyncNotifier { +class UserController extends AsyncNotifier { final String userId; UserController(this.userId); @override - Future build() async { + Future build() async { final member = await ref.watch( MembersController.provider.selectAsync( - (value) => - value.firstWhereOrNull((membership) => membership.userId == userId), + (value) => value.firstWhereOrNull( + (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); - return Membership( + return MembershipContent( status: MembershipStatus.leave, - avatarUrl: profile.avatarUrl == null - ? null - : Uri.tryParse(profile.avatarUrl!), + avatarUrl: profile.avatarUrl, displayName: profile.displayName ?? userId.localpart, - userId: userId, ); } static final provider = - AsyncNotifierProvider.family( + AsyncNotifierProvider.family( UserController.new, ); } diff --git a/lib/controllers/via_controller.dart b/lib/controllers/via_controller.dart index b423947..0c24487 100644 --- a/lib/controllers/via_controller.dart +++ b/lib/controllers/via_controller.dart @@ -2,6 +2,10 @@ 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_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"; class ViaController extends Notifier { @@ -22,22 +26,26 @@ class ViaController extends Notifier { addUserId(ref.watch(ClientStateController.provider)?.userId); 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) { - addUserId(userId); - if (servers.length >= 5) break; + if (powerLevels?.content case PowerLevelsContent(:final users)) { + for (final userId in users.keys) { + addUserId(userId); + 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++) { final member = room.events.firstWhereOrNull( (event) => event.rowId == members?.getOrNull(i), ); - if (member?.content["membership"] == "join") { - addUserId(member?.stateKey); + if (member?.content case MembershipContent(:final status)) { + if (status == MembershipStatus.join) { + addUserId(member?.stateKey); + } } if (members?.getOrNull(i) == null) break; diff --git a/lib/models/content/membership.dart b/lib/models/content/membership.dart index 6e4f875..ded5f4b 100644 --- a/lib/models/content/membership.dart +++ b/lib/models/content/membership.dart @@ -9,9 +9,9 @@ abstract class MembershipContent extends Content with _$MembershipContent { MembershipContent._(); const factory MembershipContent({ @JsonKey(name: "displayname") required String displayName, - required MembershipStatus membership, - required Uri? avatarUrl, - required String? reason, + @JsonKey(name: "membership") required MembershipStatus status, + Uri? avatarUrl, + String? reason, }) = _MembershipContent; factory MembershipContent.fromJson(Map json) => diff --git a/lib/models/membership.dart b/lib/models/membership.dart deleted file mode 100644 index ce0cc42..0000000 --- a/lib/models/membership.dart +++ /dev/null @@ -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 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, - ); -} diff --git a/lib/widgets/chat_page/room_chat.dart b/lib/widgets/chat_page/room_chat.dart index dc5cb11..3507d84 100644 --- a/lib/widgets/chat_page/room_chat.dart +++ b/lib/widgets/chat_page/room_chat.dart @@ -386,7 +386,7 @@ class RoomChat extends HookConsumerWidget { message, content: message.text, groupStatus: groupStatus, - onTapReply: notifier.scrollToMessage, + onTapReply: notifier.scrollToEvent, updateMessage: controller.updateMessage, isSentByMe: isSentByMe, ), @@ -402,7 +402,7 @@ class RoomChat extends HookConsumerWidget { message, content: message.text, groupStatus: groupStatus, - onTapReply: notifier.scrollToMessage, + onTapReply: notifier.scrollToEvent, updateMessage: controller.updateMessage, isSentByMe: isSentByMe, extra: ExpandableImageMessage(message), @@ -429,7 +429,7 @@ class RoomChat extends HookConsumerWidget { child: FlyerChatFileMessage( topWidget: ReplyWidget( message, - onTapReply: notifier.scrollToMessage, + onTapReply: notifier.scrollToEvent, groupStatus: groupStatus, ), message: message,