forked from Nexus/nexus
Remove flutter chat (#26)
Had to squash merge manually as Forgejo was erroring
This commit is contained in:
parent
bd1d5ea745
commit
16cf126df4
111 changed files with 3162 additions and 2366 deletions
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
|
|
@ -6,7 +6,9 @@
|
||||||
"Gomuks",
|
"Gomuks",
|
||||||
"Homeserver",
|
"Homeserver",
|
||||||
"localpart",
|
"localpart",
|
||||||
|
"msgtype",
|
||||||
"muks",
|
"muks",
|
||||||
"prefs"
|
"prefs",
|
||||||
|
"unban"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
12
flake.lock
generated
|
|
@ -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": {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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", {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
32
lib/controllers/members_by_status_controller.dart
Normal file
32
lib/controllers/members_by_status_controller.dart
Normal 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);
|
||||||
|
}
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
56
lib/controllers/reactions_controller.dart
Normal file
56
lib/controllers/reactions_controller.dart
Normal 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);
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -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
Loading…
Reference in a new issue