Watch
1
0
Fork
You've already forked nexus
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

@ -6,7 +6,9 @@
"Gomuks", "Gomuks",
"Homeserver", "Homeserver",
"localpart", "localpart",
"msgtype",
"muks", "muks",
"prefs" "prefs",
"unban"
] ]
} }

View file

@ -15,9 +15,6 @@ A simple and user-friendly Matrix client made with Flutter and a Gomuks backend.
## Progress ## Progress
- [x] New logo
- [x] Move from the Dart SDK to the Gomuks Backend with Dart bindings: https://git.federated.nexus/Nexus/nexus/pulls/2
- [ ] Allow using remote Gomuks over websocket
- [ ] Platform Support - [ ] Platform Support
- [x] Linux - [x] Linux
- [ ] Windows (WIP) - [ ] Windows (WIP)
@ -42,7 +39,7 @@ A simple and user-friendly Matrix client made with Flutter and a Gomuks backend.
- [x] `matrix:` Uri - [x] `matrix:` Uri
- [x] Matrix.to link - [x] Matrix.to link
- [ ] From space - [ ] From space
- [ ] Exploring - [ ] From directory
- [x] Leaving - [x] Leaving
- [x] Subspaces - [x] Subspaces
- [x] Messages - [x] Messages
@ -116,6 +113,7 @@ A simple and user-friendly Matrix client made with Flutter and a Gomuks backend.
- [ ] Settings - [ ] Settings
- [ ] Matrix: URIs vs Matrix.to links - [ ] Matrix: URIs vs Matrix.to links
- [ ] Light/Dark mode - [ ] Light/Dark mode
- [ ] Remote Gomuks instance
- [ ] SSD or CSD - [ ] SSD or CSD
- [ ] Align your message bubbles to left or right - [ ] Align your message bubbles to left or right
- [ ] Show media by default - [ ] Show media by default

12
flake.lock generated
View file

@ -5,11 +5,11 @@
"nixpkgs-lib": "nixpkgs-lib" "nixpkgs-lib": "nixpkgs-lib"
}, },
"locked": { "locked": {
"lastModified": 1777988971, "lastModified": 1778716662,
"narHash": "sha256-qIoWPDs+0/8JecyYgE3gpKQxW/4bLW/gp45vow9ioCQ=", "narHash": "sha256-m1Yf0wZ8j1OHjTc2UwHwyQRSnNeSgLJOd7q5Y45hzi4=",
"owner": "hercules-ci", "owner": "hercules-ci",
"repo": "flake-parts", "repo": "flake-parts",
"rev": "0678d8986be1661af6bb555f3489f2fdfc31f6ff", "rev": "f7c1a2d347e4c52d5fb8d10cb4d94b5884e546fb",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -88,11 +88,11 @@
}, },
"nixpkgs_2": { "nixpkgs_2": {
"locked": { "locked": {
"lastModified": 1777954456, "lastModified": 1778869304,
"narHash": "sha256-hGdgeU2Nk87RAuZyYjyDjFL6LK7dAZN5RE9+hrDTkDU=", "narHash": "sha256-30sZNZoA1cqF5JNO9fVX+wgiQYjB7HJqqJ4ztCDeBZE=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "549bd84d6279f9852cae6225e372cc67fb91a4c1", "rev": "d233902339c02a9c334e7e593de68855ad26c4cb",
"type": "github" "type": "github"
}, },
"original": { "original": {

View file

@ -1,47 +1,31 @@
import "dart:async"; import "dart:async";
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: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/configs/user_config.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/event.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.authorId).future, UserController.provider(
UserConfig(roomId: event.roomId, userId: 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.authorId,
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.authorId.localpart,
userId: message.authorId,
); );
} }
static final provider = static final provider =
AsyncNotifierProvider.family<AuthorController, Membership, Message>( AsyncNotifierProvider.family<AuthorController, MembershipContent, Event>(
AuthorController.new, AuthorController.new,
); );
} }

View file

@ -1,15 +1,13 @@
import "dart:developer";
import "dart:ffi"; import "dart:ffi";
import "dart:io"; import "dart:io";
import "dart:isolate"; import "dart:isolate";
import "package:collection/collection.dart"; import "dart:math";
import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:fast_immutable_collections/fast_immutable_collections.dart";
import "package:ffi/ffi.dart"; import "package:ffi/ffi.dart";
import "package:flutter/foundation.dart"; 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/new_events_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 +15,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,12 +80,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.type) {
ref // ref.watch(provider.notifier).addEvent(event); TODO
.watch(
NewEventsController.provider(event.roomId).notifier,
)
.add(IList([event]));
} }
break; break;
case "sync_complete": case "sync_complete":
@ -127,9 +122,12 @@ class ClientController extends AsyncNotifier<int> {
} }
debugPrint("Finished handling $muksEventType..."); debugPrint("Finished handling $muksEventType...");
} catch (error, stackTrace) { } catch (error, stackTrace) {
debugger(); if (kDebugMode) {
showError(error, stackTrace);
debugPrintStack(stackTrace: stackTrace, label: error.toString()); debugPrintStack(stackTrace: stackTrace, label: error.toString());
rethrow;
} else {
showError(error, stackTrace);
}
} }
}); });
@ -220,11 +218,6 @@ class ClientController extends AsyncNotifier<int> {
} }
Future<Event?> getEvent(GetEventRequest request) async { Future<Event?> getEvent(GetEventRequest request) async {
final event = request.room.events.firstWhereOrNull(
(event) => event.eventId == request.eventId,
);
if (event != null) return event;
final json = await _sendCommand("get_event", request.toJson()); final json = await _sendCommand("get_event", request.toJson());
return json == null ? null : Event.fromJson(json); return json == null ? null : Event.fromJson(json);
} }
@ -232,8 +225,10 @@ class ClientController extends AsyncNotifier<int> {
Future<Paginate> paginate(PaginateRequest request) async => Future<Paginate> paginate(PaginateRequest request) async =>
Paginate.fromJson(await _sendCommand("paginate", request.toJson())); Paginate.fromJson(await _sendCommand("paginate", request.toJson()));
Future<Profile> getProfile(String userId) async => Future<Profile> getProfile(String userId) async => Profile.fromJsonWithCatch({
Profile.fromJson(await _sendCommand("get_profile", {"user_id": userId})); ...(await _sendCommand("get_profile", {"user_id": userId})),
"id": userId,
});
Future<void> reportEvent(ReportRequest request) => Future<void> reportEvent(ReportRequest request) =>
_sendCommand("report_event", request.toJson()); _sendCommand("report_event", request.toJson());
@ -242,9 +237,8 @@ class ClientController extends AsyncNotifier<int> {
_sendCommand("set_membership", request.toJson()); _sendCommand("set_membership", request.toJson());
Future<void> markRead(Room room) async { Future<void> markRead(Room room) async {
final event = room.events.firstWhereOrNull( final eventRowId = room.timeline[room.timeline.keys.reduce(max)];
(event) => event.rowId == room.timeline.last.eventRowId, final event = eventRowId == null ? null : room.events[eventRowId];
);
if (event == null || room.metadata == null) return; if (event == null || room.metadata == null) return;
await _sendCommand("mark_read", { await _sendCommand("mark_read", {

View file

@ -1,5 +1,7 @@
import "package:collection/collection.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/rooms_controller.dart";
import "package:nexus/models/event.dart"; import "package:nexus/models/event.dart";
import "package:nexus/models/requests/get_event_request.dart"; import "package:nexus/models/requests/get_event_request.dart";
@ -9,8 +11,18 @@ class EventController extends AsyncNotifier<Event?> {
@override @override
Future<Event?> build() async { Future<Event?> build() async {
final client = ref.watch(ClientController.provider.notifier); final room = ref.watch(
return await client.getEvent(request).onError((_, _) => null); RoomsController.provider.select((value) => value[request.roomId]),
);
final event = room?.events.values.firstWhereOrNull(
(event) => event.eventId == request.eventId,
);
return event ??
await ref
.watch(ClientController.provider.notifier)
.getEvent(request)
.onError((_, _) => null);
} }
static final provider = AsyncNotifierProvider.family static final provider = AsyncNotifierProvider.family

View file

@ -12,14 +12,14 @@ class KeyController extends Notifier<String?> {
String? build() => String? build() =>
ref.watch(SharedPrefsController.provider).requireValue.getString(key); ref.watch(SharedPrefsController.provider).requireValue.getString(key);
Future<void> set(String? id) async { Future<void> set(String? value) async {
final prefs = ref.watch(SharedPrefsController.provider).requireValue; final prefs = ref.watch(SharedPrefsController.provider).requireValue;
state = id; state = value;
if (id == null) { if (value == null) {
prefs.remove(key); prefs.remove(key);
} else { } else {
prefs.setString(key, id); prefs.setString(key, value);
} }
} }

View file

@ -0,0 +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/configs/members_by_status_config.dart";
import "package:nexus/models/content/membership.dart";
import "package:nexus/models/event.dart";
class MembersByStatusController extends AsyncNotifier<ISet<Event>> {
final MembersByStatusConfig config;
MembersByStatusController(this.config);
@override
Future<ISet<Event>> build() => ref.watch(
MembersController.provider(config.roomId).selectAsync(
(members) => members
.where(
(membership) => switch (membership.content) {
MembershipContent(:final status) => config.status == status,
_ => false,
},
)
.toISet(),
),
);
static final provider =
AsyncNotifierProvider.family<
MembersByStatusController,
ISet<Event>,
MembersByStatusConfig
>(MembersByStatusController.new);
}

View file

@ -1,25 +0,0 @@
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/membership_status.dart";
class MembersByTypeController extends AsyncNotifier<IList<Membership>> {
final MembershipStatus status;
MembersByTypeController(this.status);
@override
Future<IList<Membership>> build() => ref.watch(
MembersController.provider.selectAsync(
(members) =>
members.where((membership) => membership.status == status).toIList(),
),
);
static final provider =
AsyncNotifierProvider.family<
MembersByTypeController,
IList<Membership>,
MembershipStatus
>(MembersByTypeController.new);
}

View file

@ -1,52 +1,46 @@
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/rooms_controller.dart";
import "package:nexus/controllers/selected_room_controller.dart"; import "package:nexus/models/content/content.dart";
import "package:nexus/models/membership.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<ISet<Event>> {
@override final String roomId;
Future<IList<Membership>> build() async { MembersController(this.roomId);
final data = ref.watch(
SelectedRoomController.provider.select(
(value) => value?.metadata == null
? null
: (value!.metadata!.id, value.metadata!.hasMemberList),
),
);
if (data == null) return const IList.empty();
final state = await ref @override
Future<ISet<Event>> build() async {
final room = ref.watch(
RoomsController.provider.select((value) => value[roomId]),
);
if (room == null) return const ISet.empty();
if (!room.hasFetchedMembers) {
final fetchedState = await ref
.watch(ClientController.provider.notifier) .watch(ClientController.provider.notifier)
.getRoomState( .getRoomState(
GetRoomStateRequest( GetRoomStateRequest(
roomId: data.$1, roomId: roomId,
fetchMembers: data.$2 == false, fetchMembers: room.metadata?.hasMemberList ?? true,
includeMembers: true, includeMembers: true,
), ),
); );
return state.nonNulls await ref
.where((state) => state.type == "m.room.member") .read(RoomsController.provider.notifier)
.map( .addState(roomId, fetchedState, isMembers: true);
(membership) => Membership.fromContent(
membership.content,
membership.stateKey!,
ref.watch(
ClientStateController.provider.select(
(value) => value?.homeserverUrl,
),
) ??
"",
),
)
.toIList();
} }
static final provider = return room.state[EventType.membership.type]?.values
AsyncNotifierProvider<MembersController, IList<Membership>>( .map((rowId) => room.events[rowId])
MembersController.new, .nonNulls
); .toISet() ??
const ISet.empty();
}
static final provider = AsyncNotifierProvider.autoDispose
.family<MembersController, ISet<Event>, String>(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_chat_core/flutter_chat_core.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.authorId),
ifAbsent: () => IList([event.authorId]),
);
})
.map((key, value) => MapEntry(key, value.unlock))
.unlock;
final asText =
Message.text(
metadata: metadata,
id: config.event.eventId,
reactions: reactions,
authorId: event.authorId,
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,
authorId: event.authorId,
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,
// authorId: senderId,
// ),
("m.sticker" || "m.room.message") => switch (content["msgtype"]) {
null || "m.image" => Message.image(
id: config.event.eventId,
authorId: event.authorId,
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,
authorId: event.authorId,
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.authorId == 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.authorId} 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,
authorId: event.authorId,
replyToMessageId: replyId,
)
: null),
};
} catch (error) {
return null;
}
}
static final provider = AsyncNotifierProvider.family
.autoDispose<MessageController, Message?, MessageConfig>(
MessageController.new,
);
}

View file

@ -1,27 +0,0 @@
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: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

@ -1,18 +0,0 @@
import "package:fast_immutable_collections/fast_immutable_collections.dart";
import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:nexus/models/event.dart";
class NewEventsController extends Notifier<IList<Event>> {
final String roomId;
NewEventsController(this.roomId);
@override
IList<Event> build() => const IList.empty();
void add(IList<Event> newEvents) => state = newEvents;
static final provider = NotifierProvider.autoDispose
.family<NewEventsController, IList<Event>, String>(
NewEventsController.new,
);
}

View file

@ -1,8 +1,9 @@
import "package:collection/collection.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/controllers/selected_room_controller.dart"; import "package:nexus/controllers/rooms_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> {
@ -11,58 +12,62 @@ class PowerLevelController extends Notifier<bool> {
@override @override
bool build() { bool build() {
final room = ref.watch(SelectedRoomController.provider); if (config case EventPowerLevelConfig(:final eventType)) {
final event = room?.events.firstWhereOrNull( assert(
(event) => event.rowId == room.state["m.room.power_levels"]?[""], eventType != EventType.redaction,
"Checking power level for a redaction should use [PowerLevelConfig.redaction].",
); );
final user = ref.watch(ClientStateController.provider)?.userId; }
if (event == null || user == null) return false;
final users = (event.content["users"] as Map<String, dynamic>? ?? {}); final room = ref.watch(
final events = (event.content["events"] as Map<String, dynamic>? ?? {}); RoomsController.provider.select((value) => value[config.roomId]),
);
int powerLevelOf(String userId) => users.containsKey(userId) final eventRowId = room?.state[EventType.powerLevels.type]?[""];
? (users[userId] as int)
: (event.content["users_default"] as int? ?? 0); final event = eventRowId == null ? null : room?.events[eventRowId];
final content = event?.content is PowerLevelsContent
? event!.content
: PowerLevelsContent();
final user = ref.watch(
ClientStateController.provider.select((value) => value?.userId),
);
if (user == null || content is! PowerLevelsContent) return false;
int powerLevelOf(String userId) =>
content.users[userId] ?? content.usersDefault;
final userLevel = powerLevelOf(user); final userLevel = powerLevelOf(user);
final targetLevel = config.targetUser != null
? powerLevelOf(config.targetUser!)
: null;
if (config.action != null) { return switch (config) {
return switch (config.action!) { EventPowerLevelConfig(:final eventType) =>
MembershipAction.invite => userLevel >= (content.events[eventType.type] ?? content.eventsDefault),
userLevel >= (event.content["invite"] as int? ?? 0),
MembershipActionPowerLevelConfig(:final action, :final targetUser) =>
switch (action) {
MembershipAction.invite => userLevel >= content.invite,
MembershipAction.kick => MembershipAction.kick =>
targetLevel != null && userLevel >= content.kick && userLevel > powerLevelOf(targetUser),
userLevel >= (event.content["kick"] as int? ?? 50) &&
userLevel > targetLevel,
MembershipAction.ban => MembershipAction.ban =>
targetLevel != null && userLevel >= content.ban && userLevel > powerLevelOf(targetUser),
userLevel >= (event.content["ban"] as int? ?? 50) &&
userLevel > targetLevel,
MembershipAction.unban => MembershipAction.unban => userLevel >= content.ban,
userLevel >= (event.content["ban"] as int? ?? 50), },
StatePowerLevelConfig(:final eventType) =>
userLevel >= (content.events[eventType.type] ?? content.stateDefault),
RedactionPowerLevelConfig(:final targetUser) =>
userLevel >=
(targetUser == user
? (content.events[EventType.redaction.type] ??
content.eventsDefault)
: content.redact),
}; };
} }
if (config.eventType == "m.room.redaction") {
return userLevel >= (event.content["redact"] as int? ?? 50);
}
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));
return userLevel >= requiredLevel;
}
static final provider = NotifierProvider.autoDispose static final provider = NotifierProvider.autoDispose
.family<PowerLevelController, bool, PowerLevelConfig>( .family<PowerLevelController, bool, PowerLevelConfig>(
PowerLevelController.new, PowerLevelController.new,

View file

@ -12,6 +12,8 @@ class ProfileController extends AsyncNotifier<Profile> {
return client.getProfile(userId); return client.getProfile(userId);
} }
static final provider = AsyncNotifierProvider.autoDispose static final provider =
.family<ProfileController, Profile, String>(ProfileController.new); AsyncNotifierProvider.family<ProfileController, Profile, String>(
ProfileController.new,
);
} }

View file

@ -0,0 +1,56 @@
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/rooms_controller.dart";
import "package:nexus/models/configs/reactions_config.dart";
import "package:nexus/models/content/reaction.dart";
import "package:nexus/models/requests/get_related_events_request.dart";
class ReactionsController extends AsyncNotifier<IMap<String, IList<String>>> {
final ReactionsConfig config;
ReactionsController(this.config);
@override
Future<IMap<String, IList<String>>> build() async {
final eventInfo = ref.watch(
RoomsController.provider.select((value) {
final event = value[config.roomId]?.events[config.eventRowId];
return event == null ? null : (event.eventId, event.reactions);
}),
);
final reactionEvents = eventInfo?.$2.isNotEmpty == true
? await ref
.watch(ClientController.provider.notifier)
.getRelatedEvents(
GetRelatedEventsRequest(
roomId: config.roomId,
eventId: eventInfo!.$1,
relationType: "m.annotation",
),
)
: null;
return reactionEvents
?.where((event) => event.redactedBy == null)
.fold<IMap<String, IList<String>>>(IMap(), (acc, event) {
if (event.content case ReactionContent(:final key?)) {
return acc.update(
key,
(list) => list.add(event.sender),
ifAbsent: () => IList([event.sender]),
);
}
return acc;
}) ??
const IMap.empty();
}
static final provider =
AsyncNotifierProvider.family<
ReactionsController,
IMap<String, IList<String>>,
ReactionsConfig
>(ReactionsController.new);
}

View file

@ -1,17 +1,13 @@
import "dart:async"; import "dart:async";
import "dart:math";
import "package:collection/collection.dart"; 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_chat_core/flutter_chat_core.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/new_events_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/event.dart";
import "package:nexus/models/configs/message_config.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";
import "package:nexus/models/requests/paginate_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/requests/send_message_request.dart";
import "package:nexus/models/room.dart"; import "package:nexus/models/room.dart";
class RoomChatController extends AsyncNotifier<InMemoryChatController> { class RoomChatController extends AsyncNotifier<IList<Event>> {
final String roomId; final String roomId;
RoomChatController(this.roomId); RoomChatController(this.roomId);
@override @override
Future<InMemoryChatController> 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(
if (room == null) return InMemoryChatController(); RoomsController.provider.select((rooms) => rooms[roomId]),
);
if (room == null) return const IList.empty();
if (!room.hasFetchedState) {
final state = await client.getRoomState( final state = await client.getRoomState(
GetRoomStateRequest(roomId: roomId), GetRoomStateRequest(roomId: roomId),
); );
ref await ref.read(RoomsController.provider.notifier).addState(roomId, state);
.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") { // While there are under 20 events, try to load more
final controller = await future; // until there's no more or the conditions are met.
final redactsId = event.content["redacts"]; if (room.hasMore && room.timeline.length < 20) {
final originalMessage = controller.messages.firstWhereOrNull( loadOlder();
(message) => message.id == redactsId,
);
if (!ref.mounted) return;
if (originalMessage != null) {
return await controller.removeMessage(originalMessage);
} }
final redacts = ref return room.timeline
.read(SelectedRoomController.provider) .toEntryIList(compare: (a, b) => (b?.key ?? 0).compareTo(a?.key ?? 0))
?.events .map((entry) {
.firstWhere((event) => event.eventId == redactsId); if (entry.value == null) return null;
if (redacts?.type == "m.reaction") { final foundEvent = room.events[entry.value!];
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( final editedEvent =
message, foundEvent == null || foundEvent.lastEditRowId == 0
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 ? null
: controller.messages.firstWhereOrNull( : room.events[foundEvent.lastEditRowId];
(element) =>
element.metadata?["txnId"] == message.metadata?["txnId"],
);
return oldMessage == null return editedEvent == null
? controller.insertMessage(message) ? foundEvent
: controller.updateMessage(oldMessage, message); : foundEvent?.copyWith(content: editedEvent.content);
})
.nonNulls
.toIList();
} }
Future<void> deleteMessage(Message message, {String? reason}) => ref Future<void> deleteMessage(Event event, {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([InMemoryChatController? chatController]) async { Future<bool> loadOlder() async {
final timelineKeys = ref
.read(RoomsController.provider.select((value) => value[roomId]))
?.timeline
.keys;
final response = await ref final response = await ref
.watch(ClientController.provider.notifier) .watch(ClientController.provider.notifier)
.paginate( .paginate(
PaginateRequest( PaginateRequest(
roomId: roomId, roomId: roomId,
maxTimelineId: ref maxTimelineId: timelineKeys?.isNotEmpty == true
.read(RoomsController.provider)[roomId] ? timelineKeys?.reduce(min)
?.timeline : null,
.firstOrNull
?.timelineRowId,
), ),
); );
@ -226,42 +94,22 @@ class RoomChatController extends AsyncNotifier<InMemoryChatController> {
.update( .update(
IMap({ IMap({
roomId: Room( 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, hasMore: response.hasMore,
timeline: response.events timeline: IMap.fromIterable(
.map( response.events,
(event) => TimelineRowTuple( keyMapper: (event) => event.timelineRowId,
timelineRowId: event.timelineRowId, valueMapper: (event) => event.rowId,
eventRowId: event.rowId,
), ),
)
.toIList(),
), ),
}), }),
const ISet.empty(), 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; return response.hasMore;
} }
@ -270,7 +118,7 @@ class RoomChatController extends AsyncNotifier<InMemoryChatController> {
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;
@ -285,7 +133,6 @@ class RoomChatController extends AsyncNotifier<InMemoryChatController> {
} }
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,
@ -294,52 +141,46 @@ class RoomChatController extends AsyncNotifier<InMemoryChatController> {
if (shouldMention == true && if (shouldMention == true &&
relation != null && relation != null &&
relationType == RelationType.reply) relationType == RelationType.reply)
relation.authorId, relation.sender,
].toIList(), ].toIList(),
room: taggedMessage.contains("@room"), room: taggedMessage.contains("@room"),
), ),
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);
}
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},
), ),
); );
await setFlashing(true); // TODO: Add new event to timeline whilst its sending
Timer(Duration(seconds: 1), () => setFlashing(false)); // ref
// .watch(RoomsController.provider.notifier)
return await controller.scrollToMessage(message.id); // .update(
// {
// roomId: Room(
// events: [event].toIList(),
// timeline: [
// TimelineRowTuple(
// timelineRowId: event.timelineRowId,
// eventRowId: event.rowId,
// ),
// ].toIList(),
// ),
// }.toIMap(),
// const ISet.empty(),
// );
} }
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",
), ),
); );
@ -349,9 +190,11 @@ class RoomChatController extends AsyncNotifier<InMemoryChatController> {
.toIList(); .toIList();
final reactionEvent = reactionEvents?.firstWhereOrNull( final reactionEvent = reactionEvents?.firstWhereOrNull(
(event) => (event) => switch (event.content) {
event.authorId == userId && ReactionContent(:final key) =>
event.content["m.relates_to"]?["key"] == reaction, key == reaction && event.sender == userId,
_ => false,
},
); );
if (reactionEvent != null) { 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); final client = ref.watch(ClientController.provider.notifier);
await client.sendEvent( await client.sendEvent(
@ -372,7 +215,7 @@ class RoomChatController extends AsyncNotifier<InMemoryChatController> {
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,
}, },
@ -384,7 +227,7 @@ class RoomChatController extends AsyncNotifier<InMemoryChatController> {
} }
static final provider = AsyncNotifierProvider.family static final provider = AsyncNotifierProvider.family
.autoDispose<RoomChatController, InMemoryChatController, String>( .autoDispose<RoomChatController, IList<Event>, String>(
RoomChatController.new, RoomChatController.new,
); );
} }

View file

@ -1,9 +1,8 @@
import "package:collection/collection.dart"; import "dart:isolate";
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/models/event.dart";
import "package:nexus/controllers/new_events_controller.dart";
import "package:nexus/helpers/extensions/mxc_to_https.dart";
import "package:nexus/models/read_receipt.dart"; import "package:nexus/models/read_receipt.dart";
import "package:nexus/models/room.dart"; import "package:nexus/models/room.dart";
@ -11,55 +10,50 @@ class RoomsController extends Notifier<IMap<String, Room>> {
@override @override
IMap<String, Room> build() => const IMap.empty(); IMap<String, Room> build() => const IMap.empty();
void update( Future<void> addState(
IMap<String, Room> rooms, String roomId,
ISet<String> leftRooms, { IList<Event> state, {
bool addToNewEvents = true, bool isMembers = false,
}) { }) async => update(
final homeserver = {
ref.watch( roomId: Room(
ClientStateController.provider.select( events: IMap.fromEntries(
(value) => value?.homeserverUrl, state.map((event) => MapEntry(event.rowId, event)),
), ),
) ?? hasFetchedState: true,
""; hasFetchedMembers: isMembers,
state: await Isolate.run(() {
final newState = state.fold(
const IMap<String, IMap<String, int>>.empty(),
(previousValue, stateEvent) => previousValue.add(
stateEvent.type,
(previousValue[stateEvent.type] ?? const IMap.empty()).add(
stateEvent.stateKey!,
stateEvent.rowId,
),
),
);
return newState;
}),
),
}.toIMap(),
const ISet.empty(),
);
void update(IMap<String, Room> rooms, ISet<String> leftRooms) {
final merged = rooms.entries.fold(state, (acc, entry) { final merged = rooms.entries.fold(state, (acc, entry) {
final roomId = entry.key; final roomId = entry.key;
final incoming = entry.value; final incoming = entry.value;
final existing = acc[roomId]; final existing = acc[roomId];
final events = existing?.events.updateById(
incoming.events,
(item) => item.eventId,
);
if (addToNewEvents) {
ref
.watch(NewEventsController.provider(roomId).notifier)
.add(
incoming.timeline
.map(
(timelineTuple) => events?.firstWhereOrNull(
(event) => timelineTuple.eventRowId == event.rowId,
),
)
.nonNulls
.toIList(),
);
}
return acc.add( return acc.add(
roomId, roomId,
existing?.copyWith( existing?.copyWith(
hasMore: incoming.hasMore, hasMore: incoming.hasMore,
metadata: metadata: incoming.metadata ?? existing.metadata,
incoming.metadata?.copyWith( events: incoming.events.isEmpty
avatar: ? existing.events
incoming.metadata?.avatar?.mxcToHttps(homeserver) ?? : existing.events.addAll(incoming.events),
existing.metadata?.avatar,
) ??
existing.metadata,
events: events!,
state: incoming.state.entries.fold( state: incoming.state.entries.fold(
existing.state, existing.state,
(previousValue, event) => previousValue.add( (previousValue, event) => previousValue.add(
@ -69,15 +63,14 @@ class RoomsController extends Notifier<IMap<String, Room>> {
), ),
), ),
), ),
timeline: reset: false,
(incoming.reset hasFetchedMembers:
incoming.hasFetchedMembers || existing.hasFetchedMembers,
hasFetchedState:
incoming.hasFetchedState || existing.hasFetchedState,
timeline: (incoming.reset
? incoming.timeline ? incoming.timeline
: existing.timeline.updateById( : existing.timeline.addAll(incoming.timeline)),
incoming.timeline,
(item) => item.timelineRowId,
))
.sortedBy((element) => element.timelineRowId)
.toIList(),
receipts: incoming.receipts.entries.fold( receipts: incoming.receipts.entries.fold(
existing.receipts, existing.receipts,
(receiptAcc, event) => receiptAcc.add( (receiptAcc, event) => receiptAcc.add(
@ -88,11 +81,7 @@ class RoomsController extends Notifier<IMap<String, Room>> {
), ),
), ),
) ?? ) ??
incoming.copyWith( incoming,
metadata: incoming.metadata?.copyWith(
avatar: incoming.metadata?.avatar?.mxcToHttps(homeserver),
),
),
); );
}); });
@ -100,6 +89,7 @@ class RoomsController extends Notifier<IMap<String, Room>> {
merged, merged,
(acc, roomId) => acc.remove(roomId), (acc, roomId) => acc.remove(roomId),
); );
state = prunedList; state = prunedList;
} }

View file

@ -1,24 +0,0 @@
import "package:collection/collection.dart";
import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:nexus/controllers/key_controller.dart";
import "package:nexus/controllers/selected_space_controller.dart";
import "package:nexus/models/room.dart";
class SelectedRoomController extends Notifier<Room?> {
@override
Room? build() {
final space = ref.watch(SelectedSpaceController.provider);
final selectedRoomId = ref.watch(
KeyController.provider(KeyController.roomKey),
);
return space.children.firstWhereOrNull(
(room) => room.metadata?.id == selectedRoomId,
) ??
space.children.firstOrNull;
}
static final provider = NotifierProvider<SelectedRoomController, Room?>(
SelectedRoomController.new,
);
}

View file

@ -1,22 +0,0 @@
import "package:collection/collection.dart";
import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:nexus/controllers/key_controller.dart";
import "package:nexus/controllers/spaces_controller.dart";
import "package:nexus/models/space.dart";
class SelectedSpaceController extends Notifier<Space> {
@override
Space build() {
final spaces = ref.watch(SpacesController.provider);
final selectedSpaceId = ref.watch(
KeyController.provider(KeyController.spaceKey),
);
return spaces.firstWhereOrNull((space) => space.id == selectedSpaceId) ??
spaces.first;
}
static final provider = NotifierProvider<SelectedSpaceController, Space>(
SelectedSpaceController.new,
);
}

Some files were not shown because too many files have changed in this diff Show more