treewide(refactor): rename files to follow the same style

This commit is contained in:
Henry Hiles 2026-07-18 12:12:54 -04:00
commit 7992f0e815
Signed by: Henry-Hiles
SSH key fingerprint: SHA256:VKQUdS31Q90KvX7EkKMHMBpUspcmItAh86a+v7PGiIs
81 changed files with 167 additions and 167 deletions

View file

@ -0,0 +1,16 @@
import "package:fast_immutable_collections/fast_immutable_collections.dart";
import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:nexus/models/account_data.dart";
class AccountDataController extends Notifier<IMap<String, AccountData>> {
@override
IMap<String, AccountData> build() => .new();
void update(IMap<String, AccountData> newData) =>
state = .new({...state.unlock, ...newData.unlock});
static final provider =
NotifierProvider<AccountDataController, IMap<String, AccountData>>(
AccountDataController.new,
);
}

View file

@ -0,0 +1,30 @@
import "dart:async";
import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:nexus/controllers/user.dart";
import "package:nexus/models/content/membership.dart";
import "package:nexus/models/event.dart";
class AuthorController extends AsyncNotifier<MembershipContent> {
final Event event;
AuthorController(this.event);
@override
Future<MembershipContent> build() async {
final member = await ref.watch(
UserController.provider(
.new(roomId: event.roomId, userId: event.sender),
).future,
);
return .new(
status: member.status,
avatarUrl: event.pmp?.avatarUrl ?? member.avatarUrl,
displayName: event.pmp?.displayName ?? member.displayName,
);
}
static final provider =
AsyncNotifierProvider.family<AuthorController, MembershipContent, Event>(
AuthorController.new,
);
}

301
lib/controllers/client.dart Normal file
View file

@ -0,0 +1,301 @@
import "dart:ffi";
import "dart:io";
import "dart:isolate";
import "dart:math";
import "package:fast_immutable_collections/fast_immutable_collections.dart";
import "package:ffi/ffi.dart";
import "package:flutter/foundation.dart";
import "package:nexus/controllers/account_data.dart";
import "package:nexus/controllers/client_state.dart";
import "package:nexus/controllers/init_complete.dart";
import "package:nexus/controllers/rooms.dart";
import "package:nexus/controllers/space_edges.dart";
import "package:nexus/controllers/sync_status.dart";
import "package:nexus/controllers/top_level_spaces.dart";
import "package:nexus/helpers/extensions/gomuks_buffer.dart";
import "package:nexus/main.dart";
import "package:nexus/models/event.dart";
import "package:nexus/models/paginate.dart";
import "package:nexus/models/requests/get_event.dart";
import "package:nexus/models/requests/get_related_events.dart";
import "package:nexus/models/requests/get_room_state.dart";
import "package:nexus/models/requests/join_room.dart";
import "package:nexus/models/requests/login.dart";
import "package:nexus/models/profile.dart";
import "package:nexus/models/requests/paginate.dart";
import "package:nexus/models/requests/redact_event.dart";
import "package:nexus/models/requests/report.dart";
import "package:nexus/models/requests/send_event.dart";
import "package:nexus/models/requests/send_message.dart";
import "package:nexus/models/requests/set_membership.dart";
import "package:nexus/models/requests/set_state.dart";
import "package:nexus/models/room.dart";
import "package:nexus/models/sync_data.dart";
import "package:nexus/src/third_party/gomuks.g.dart";
import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:path_provider/path_provider.dart";
class ClientController extends AsyncNotifier<int> {
@override
Future<int> build() async {
final Pointer<Char> root;
if (Platform.isAndroid) {
final dir = await getApplicationSupportDirectory();
root = "${dir.path}/gomuks".toNativeUtf8().cast();
} else {
root = nullptr.cast();
}
final handle = GomuksInit(root);
final callable =
NativeCallable<
Void Function(Pointer<Char>, Int64, GomuksOwnedBuffer)
>.listener((
Pointer<Char> command,
int requestId,
GomuksOwnedBuffer data,
) {
try {
final muksEventType = command.cast<Utf8>().toDartString();
debugPrint("Handling $muksEventType...");
final decodedMuksEvent = data.toJson();
switch (muksEventType) {
case "client_state":
ref
.watch(ClientStateController.provider.notifier)
.set(.fromJson(decodedMuksEvent));
break;
case "sync_status":
ref
.watch(SyncStatusController.provider.notifier)
.set(.fromJson(decodedMuksEvent));
break;
case "init_complete":
ref.watch(InitCompleteController.provider.notifier).complete();
break;
case "send_complete":
final event = Event.fromJson(decodedMuksEvent["event"]);
ref
.watch(RoomsController.provider.notifier)
.update(
.new({
event.roomId: .new(events: .new({event.rowId: event})),
}),
.new(),
);
break;
case "sync_complete":
final syncData = SyncData.fromJson(decodedMuksEvent);
final roomProvider = RoomsController.provider;
final accountDataProvider = AccountDataController.provider;
if (syncData.clearState) {
ref.invalidate(roomProvider);
ref.invalidate(accountDataProvider);
}
ref
.watch(roomProvider.notifier)
.update(syncData.rooms, syncData.leftRooms);
ref
.watch(accountDataProvider.notifier)
.update(syncData.accountData);
if (syncData.topLevelSpaces != null) {
ref
.watch(TopLevelSpacesController.provider.notifier)
.set(syncData.topLevelSpaces!);
}
if (syncData.spaceEdges != null) {
ref
.watch(SpaceEdgesController.provider.notifier)
.set(syncData.spaceEdges!);
}
// ref
// .watch(SyncStatusController.provider.notifier)
// .set(SyncStatus.fromJson(decodedMuksEvent));
break;
default:
debugPrint("Unhandled event: $muksEventType");
}
debugPrint("Finished handling $muksEventType...");
} catch (error, stackTrace) {
if (kDebugMode) {
debugPrintStack(stackTrace: stackTrace, label: error.toString());
rethrow;
} else {
showError(error, stackTrace);
}
}
});
ref.onDispose(() => GomuksDestroy(handle));
ref.onDispose(callable.close);
final errorCode = GomuksStart(handle, callable.nativeFunction);
if (errorCode == 0) return handle;
throw Exception("GomuksStart returned error code $errorCode");
}
Future<dynamic> _sendCommand(
String command, [
Map<String, dynamic> data = const {},
]) async {
final bufferPointer = data.toGomuksBufferPtr();
final handle = await future;
final response = await Isolate.run(
() => GomuksSubmitCommand(
handle,
command.toNativeUtf8().cast<Char>(),
bufferPointer.ref,
),
);
calloc.free(bufferPointer);
final json = response.buf.toJson();
if (response.command.cast<Utf8>().toDartString() == "error") {
throw json;
}
return json;
}
Future<void> redactEvent(RedactEventRequest report) =>
_sendCommand("redact_event", report.toJson());
Future<Event> sendMessage(SendMessageRequest request) async =>
Event.fromJson(await _sendCommand("send_message", request.toJson()));
Future<Event> sendEvent(SendEventRequest request) async {
final json = request.toJson();
final content = request.content.toJson();
return Event.fromJson(
await _sendCommand("send_event", {
...json,
"content": {
...content,
"m.relates_to": {
...((content["m.relates_to"] as Map<String, dynamic>?) ?? {}),
"event_id": request.relatesTo,
"rel_type": request.relationType,
},
},
}),
);
}
Future<String?> setState(SetStateRequest request) async =>
await _sendCommand("set_state", request.toJson());
Future<String?> verify(String recoveryKey) async {
try {
await _sendCommand("verify", {"recovery_key": recoveryKey});
return null;
} catch (error) {
return error.toString();
}
}
Future<String> joinRoom(JoinRoomRequest request) async {
final response = await _sendCommand("join_room", request.toJson());
return response["room_id"];
}
Future<String?> getAccessToken() async {
final response = await _sendCommand("get_account_info", {});
return response?["access_token"];
}
Future<void> leaveRoom(Room room) async {
if (room.metadata == null) return;
await _sendCommand("leave_room", {"room_id": room.metadata!.id});
}
// (await _sendCommand("get_event_context", {
// "room_id": request.roomId,
// "event_id": r"$OqZT4NuTj0J1-771IOEEWRI4XdumRNu6ighlvO3K3gc",
// }));
Future<IList<Event>> getRoomState(GetRoomStateRequest request) async {
Future<List?> getState(GetRoomStateRequest request) async =>
(await _sendCommand("get_room_state", request.toJson())) as List?;
final response = await getState(request);
return .new(
(response ?? await getState(request.copyWith(refetch: true)) ?? []).map(
(event) => .fromJson(event),
),
);
}
Future<IList<Event>?> getRelatedEvents(
GetRelatedEventsRequest request,
) async {
final response =
(await _sendCommand("get_related_events", request.toJson())) as List?;
return .new(response?.map((event) => .fromJson(event)));
}
Future<Event?> getEvent(GetEventRequest request) async {
final json = await _sendCommand("get_event", request.toJson());
return json == null ? null : .fromJson(json);
}
Future<Paginate> paginate(PaginateRequest request) async =>
.fromJson(await _sendCommand("paginate", request.toJson()));
Future<Profile> getProfile(String userId) async {
final json = await _sendCommand("get_profile", {"user_id": userId});
return .fromJsonWithCatch({...json, "id": userId});
}
Future<void> reportEvent(ReportRequest request) =>
_sendCommand("report_event", request.toJson());
Future<void> setMembership(SetMembershipRequest request) =>
_sendCommand("set_membership", request.toJson());
Future<void> markRead(Room room) async {
final eventRowId = room.timeline[room.timeline.keys.reduce(max)];
final event = eventRowId == null ? null : room.events[eventRowId];
if (event == null || room.metadata == null) return;
await _sendCommand("mark_read", {
"room_id": room.metadata!.id,
"receipt_type": "m.read",
"event_id": event.eventId,
});
}
Future<String?> login(LoginRequest login) async {
try {
await _sendCommand("login", login.toJson());
return null;
} catch (error) {
return error.toString();
}
}
Future<Uri?> discoverHomeserver(Uri homeserver) async {
try {
final response = await _sendCommand("discover_homeserver", {
"user_id": "@fake-user:${homeserver.host}",
});
return Uri.parse(response["m.homeserver"]?["base_url"]);
} catch (error) {
return null;
}
}
static final provider = AsyncNotifierProvider<ClientController, int>(
ClientController.new,
);
}

View file

@ -0,0 +1,13 @@
import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:nexus/models/client_state.dart";
class ClientStateController extends Notifier<ClientState?> {
@override
Null build() => null;
void set(ClientState newState) => state = newState;
static final provider = NotifierProvider<ClientStateController, ClientState?>(
ClientStateController.new,
);
}

View file

@ -0,0 +1,11 @@
import "package:cross_cache/cross_cache.dart";
import "package:flutter_riverpod/flutter_riverpod.dart";
class CrossCacheController extends Notifier<CrossCache> {
@override
CrossCache build() => .new();
static final provider = NotifierProvider<CrossCacheController, CrossCache>(
CrossCacheController.new,
);
}

View file

@ -0,0 +1,84 @@
import "dart:convert";
import "package:emoji_text_field/models/emoji_category.dart";
import "package:fast_immutable_collections/fast_immutable_collections.dart";
import "package:flutter/material.dart";
import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:http/http.dart";
import "package:nexus/models/emoji.dart";
typedef EmojiTuple = (IMap<String, EmojiCategory>, IMap<String, List<String>>);
class EmojiController extends AsyncNotifier<EmojiTuple> {
@override
Future<EmojiTuple> build() async {
final response = await get(
.https("github.com", "github/gemoji/raw/refs/heads/master/db/emoji.json"),
);
if (response.statusCode != 200) {
throw Exception("Failed to load emoji data");
}
final data = json.decode(response.body);
final entries = (data as List)
.cast<Map<String, dynamic>>()
.map(Emoji.fromJson)
.toIList();
final categoryMap = entries.fold<IMap<String, IList<String>>>(
.new(),
(acc, entry) => acc.update(
entry.category,
(list) => list.add(entry.emoji),
ifAbsent: () => .new([entry.emoji]),
),
);
final keywordMap = entries.fold<IMap<String, IList<String>>>(
.new(),
(acc, entry) => acc.add(
entry.emoji,
.new([...entry.tags, ...entry.aliases, entry.description]),
),
);
final customCategories = IMap.fromEntries(
categoryMap.entries.map(
(entry) => MapEntry(
entry.key,
EmojiCategory(
name: entry.key,
icon: switch (entry.key) {
"Smileys & Emotion" => Icons.emoji_emotions,
"People & Body" => Icons.emoji_people,
"Animals & Nature" => Icons.emoji_nature,
"Food & Drink" => Icons.emoji_food_beverage,
"Travel & Places" => Icons.travel_explore,
"Activities" => Icons.sports_soccer,
"Objects" => Icons.emoji_objects,
"Symbols" => Icons.emoji_symbols,
"Flags" => Icons.emoji_flags,
_ => Icons.category,
},
emojis: entry.value.toList(growable: false),
),
),
),
);
final customKeywords = IMap(
.fromEntries(
keywordMap.entries.map(
(e) => .new(e.key, e.value.toList(growable: false)),
),
),
);
return (customCategories, customKeywords);
}
static final provider = AsyncNotifierProvider<EmojiController, EmojiTuple>(
EmojiController.new,
);
}

View file

@ -0,0 +1,32 @@
import "package:collection/collection.dart";
import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:nexus/controllers/client.dart";
import "package:nexus/controllers/rooms.dart";
import "package:nexus/models/event.dart";
import "package:nexus/models/requests/get_event.dart";
class EventController extends AsyncNotifier<Event?> {
final GetEventRequest request;
EventController(this.request);
@override
Future<Event?> build() async {
final room = ref.watch(
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
.autoDispose<EventController, Event?, GetEventRequest>(
EventController.new,
);
}

View file

@ -0,0 +1,20 @@
import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:nexus/controllers/client.dart";
import "package:nexus/controllers/client_state.dart";
class HeaderController extends AsyncNotifier<Map<String, String>> {
@override
Future<Map<String, String>> build() async {
if (ref.watch(ClientStateController.provider)?.isLoggedIn != true) {
return {};
}
final client = ref.watch(ClientController.provider.notifier);
final accessToken = await client.getAccessToken();
return {"authorization": "Bearer $accessToken"};
}
static final provider =
AsyncNotifierProvider<HeaderController, Map<String, String>>(
HeaderController.new,
);
}

View file

@ -0,0 +1,11 @@
import "package:flutter_riverpod/flutter_riverpod.dart";
class InitCompleteController extends Notifier<bool> {
@override
bool build() => false;
void complete() => state = true;
static final provider = NotifierProvider<InitCompleteController, bool>(
InitCompleteController.new,
);
}

30
lib/controllers/key.dart Normal file
View file

@ -0,0 +1,30 @@
import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:nexus/controllers/shared_prefs.dart";
class KeyController extends Notifier<String?> {
final String key;
KeyController(this.key);
static const String spaceKey = "space";
static const String roomKey = "room";
@override
String? build() =>
ref.watch(SharedPrefsController.provider).requireValue.getString(key);
Future<void> set(String? value) async {
final prefs = ref.watch(SharedPrefsController.provider).requireValue;
state = value;
if (value == null) {
prefs.remove(key);
} else {
prefs.setString(key, value);
}
}
static final provider =
NotifierProvider.family<KeyController, String?, String>(
KeyController.new,
);
}

View file

@ -0,0 +1,46 @@
import "package:fast_immutable_collections/fast_immutable_collections.dart";
import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:nexus/controllers/client.dart";
import "package:nexus/controllers/rooms.dart";
import "package:nexus/models/content/content.dart";
import "package:nexus/models/event.dart";
import "package:nexus/models/requests/get_room_state.dart";
class MembersController extends AsyncNotifier<ISet<Event>> {
final String roomId;
MembersController(this.roomId);
@override
Future<ISet<Event>> build() async {
final room = ref.watch(
RoomsController.provider.select((value) => value[roomId]),
);
if (room == null) return .new();
if (!room.hasFetchedMembers) {
final fetchedState = await ref
.watch(ClientController.provider.notifier)
.getRoomState(
GetRoomStateRequest(
roomId: roomId,
fetchMembers: !(room.metadata?.hasMemberList ?? false),
includeMembers: true,
),
);
await ref
.read(RoomsController.provider.notifier)
.addState(roomId, fetchedState, isMembers: true);
}
return room.state[EventType.membership.type]?.values
.map((rowId) => room.events[rowId])
.nonNulls
.toISet() ??
.new();
}
static final provider = AsyncNotifierProvider.autoDispose
.family<MembersController, ISet<Event>, String>(MembersController.new);
}

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.dart";
import "package:nexus/models/configs/members_by_status.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

@ -0,0 +1,64 @@
import "package:fast_immutable_collections/fast_immutable_collections.dart";
import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:nexus/controllers/members_by_status.dart";
import "package:nexus/controllers/room_creators.dart";
import "package:nexus/controllers/rooms.dart";
import "package:nexus/models/configs/members_by_status.dart";
import "package:nexus/models/content/content.dart";
import "package:nexus/models/content/power_levels.dart";
import "package:nexus/models/event.dart";
class MembersGroupedController
extends AsyncNotifier<IList<MapEntry<int?, ISet<Event>>>> {
final MembersByStatusConfig config;
MembersGroupedController(this.config);
@override
Future<IList<MapEntry<int?, ISet<Event>>>> build() async {
final room = ref.watch(
RoomsController.provider.select((value) => value[config.roomId]),
);
final roomCreators = room == null
? null
: ref.watch((RoomCreatorsController.provider(room)));
final powerLevelsRowId = room?.state[EventType.powerLevels.type]?[""];
final powerLevelsEvent = powerLevelsRowId == null
? null
: room?.events[powerLevelsRowId];
final content = switch (powerLevelsEvent?.content) {
PowerLevelsContent content => content,
_ => PowerLevelsContent(),
};
final members = await ref.watch(
MembersByStatusController.provider(config).future,
);
return members
.fold<IMap<int?, ISet<Event>>>(.new(), (result, event) {
final groupKey = roomCreators?.contains(event.stateKey!) == true
? null
: content.users[event.stateKey!] ?? content.usersDefault;
return result.update(
groupKey,
(value) => value.add(event),
ifAbsent: () => .new({event}),
);
})
.toEntryIList(
compare: (a, b) =>
(b?.key ?? double.infinity).compareTo(a?.key ?? double.infinity),
);
}
static final provider =
AsyncNotifierProvider.family<
MembersGroupedController,
IList<MapEntry<int?, ISet<Event>>>,
MembersByStatusConfig
>(MembersGroupedController.new);
}

View file

@ -0,0 +1,19 @@
import "dart:async";
import "package:fast_immutable_collections/fast_immutable_collections.dart";
import "package:flutter_riverpod/flutter_riverpod.dart";
class MultiProviderController extends AsyncNotifier<void> {
MultiProviderController(this.providers);
final IList<AsyncNotifierProvider> providers;
@override
Future<void> build() =>
.wait(providers.map((provider) => ref.watch(provider.future)));
static final provider =
AsyncNotifierProvider.family<
MultiProviderController,
void,
IList<AsyncNotifierProvider>
>(MultiProviderController.new);
}

View file

@ -0,0 +1,30 @@
import "package:fast_immutable_collections/fast_immutable_collections.dart";
import "package:hooks_riverpod/hooks_riverpod.dart";
import "package:nexus/controllers/event.dart";
import "package:nexus/controllers/pinned_ids.dart";
import "package:nexus/models/event.dart";
class PinnedEventsController extends AsyncNotifier<IList<Event>> {
final String roomId;
PinnedEventsController(this.roomId);
@override
Future<IList<Event>> build() async {
final pinIds = ref.watch(PinnedIdsController.provider(roomId));
return (await Future.wait(
pinIds.map(
(eventId) => ref.watch(
EventController.provider(
.new(eventId: eventId, roomId: roomId),
).future,
),
),
)).nonNulls.toIList();
}
static final provider = AsyncNotifierProvider.family
.autoDispose<PinnedEventsController, IList<Event>, String>(
PinnedEventsController.new,
);
}

View file

@ -0,0 +1,53 @@
import "package:fast_immutable_collections/fast_immutable_collections.dart";
import "package:hooks_riverpod/hooks_riverpod.dart";
import "package:nexus/controllers/client.dart";
import "package:nexus/controllers/rooms.dart";
import "package:nexus/models/content/content.dart";
import "package:nexus/models/content/pinned_events.dart";
class PinnedIdsController extends Notifier<IList<String>> {
final String roomId;
PinnedIdsController(this.roomId);
@override
IList<String> build() {
final room = ref.watch(
RoomsController.provider.select((rooms) => rooms[roomId]),
);
if (room == null) return .new();
final pinnedRowId = room.state[EventType.pinnedEvents.type]?[""];
final pinnedStateEvent = pinnedRowId == null
? null
: room.events[pinnedRowId];
if (pinnedStateEvent?.content case PinnedEventsContent content) {
return content.pinnedEvents;
}
return .new();
}
Future<void> addPin(String eventId) async =>
setPinned(.new(pinnedEvents: .new(state.add(eventId))));
Future<void> removePin(String eventId) async =>
setPinned(.new(pinnedEvents: .new(state.remove(eventId))));
Future<void> setPinned(PinnedEventsContent content) => ref
.read(ClientController.provider.notifier)
.setState(
.new(
roomId: roomId,
type: EventType.pinnedEvents.type,
stateKey: "",
content: content,
),
);
static final provider = NotifierProvider.family
.autoDispose<PinnedIdsController, IList<String>, String>(
PinnedIdsController.new,
);
}

View file

@ -0,0 +1,81 @@
import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:nexus/controllers/client_state.dart";
import "package:nexus/controllers/room_creators.dart";
import "package:nexus/controllers/rooms.dart";
import "package:nexus/models/configs/power_level.dart";
import "package:nexus/models/content/content.dart";
import "package:nexus/models/content/power_levels.dart";
class PowerLevelController extends Notifier<bool> {
final PowerLevelConfig config;
PowerLevelController(this.config);
@override
bool build() {
if (config case EventPowerLevelConfig(:final eventType)) {
assert(
eventType != .redaction,
"Checking power level for a redaction should use [PowerLevelConfig.redaction].",
);
}
final room = ref.watch(
RoomsController.provider.select((value) => value[config.roomId]),
);
final roomCreators = room == null
? null
: ref.watch(RoomCreatorsController.provider(room));
final eventRowId = room?.state[EventType.powerLevels.type]?[""];
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;
double powerLevelOf(String userId) => roomCreators?.contains(userId) == true
? double.infinity
: (content.users[userId] ?? content.usersDefault).toDouble();
final userLevel = powerLevelOf(user);
return switch (config) {
EventPowerLevelConfig(:final eventType) =>
userLevel >= (content.events[eventType.type] ?? content.eventsDefault),
MembershipActionPowerLevelConfig(:final action, :final targetUser) =>
switch (action) {
.invite => userLevel >= content.invite,
.kick =>
userLevel >= content.kick && userLevel > powerLevelOf(targetUser),
.ban =>
userLevel >= content.ban && userLevel > powerLevelOf(targetUser),
.unban => userLevel >= content.ban,
},
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),
};
}
static final provider = NotifierProvider.autoDispose
.family<PowerLevelController, bool, PowerLevelConfig>(
PowerLevelController.new,
);
}

View file

@ -0,0 +1,17 @@
import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:nexus/controllers/client.dart";
import "package:nexus/models/profile.dart";
class ProfileController extends AsyncNotifier<Profile> {
final String userId;
ProfileController(this.userId);
@override
Future<Profile> build() {
final client = ref.watch(ClientController.provider.notifier);
return client.getProfile(userId);
}
static final provider = AsyncNotifierProvider.family
.autoDispose<ProfileController, Profile, String>(ProfileController.new);
}

View file

@ -0,0 +1,55 @@
import "package:fast_immutable_collections/fast_immutable_collections.dart";
import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:nexus/controllers/client.dart";
import "package:nexus/controllers/rooms.dart";
import "package:nexus/models/configs/reactions.dart";
import "package:nexus/models/content/reaction.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(
.new(
roomId: config.roomId,
eventId: eventInfo!.$1,
relationType: "m.annotation",
),
)
: null;
return reactionEvents
?.where((event) => event.redactedBy == null)
.fold<IMap<String, IList<String>>>(.new(), (acc, event) {
if (event.content case ReactionContent(:final key?)) {
return acc.update(
key,
(list) => list.add(event.sender),
ifAbsent: () => .new([event.sender]),
);
}
return acc;
}) ??
.new();
}
static final provider =
AsyncNotifierProvider.family<
ReactionsController,
IMap<String, IList<String>>,
ReactionsConfig
>(ReactionsController.new);
}

View file

@ -0,0 +1,221 @@
import "dart:async";
import "dart:math";
import "package:collection/collection.dart";
import "package:fast_immutable_collections/fast_immutable_collections.dart";
import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:fluttertagger/fluttertagger.dart";
import "package:nexus/controllers/client.dart";
import "package:nexus/controllers/rooms.dart";
import "package:nexus/models/content/content.dart";
import "package:nexus/models/content/reaction.dart";
import "package:nexus/models/event.dart";
import "package:nexus/models/requests/redact_event.dart";
import "package:nexus/models/relation_type.dart";
import "package:nexus/models/requests/send_message.dart";
import "package:nexus/models/room.dart";
class RoomChatController extends AsyncNotifier<IList<Event>?> {
final String roomId;
RoomChatController(this.roomId);
@override
Future<IList<Event>?> build() async {
final client = ref.watch(ClientController.provider.notifier);
final room = ref.watch(
RoomsController.provider.select((rooms) => rooms[roomId]),
);
if (room == null) return null;
if (!room.hasFetchedState) {
final state = await client.getRoomState(.new(roomId: roomId));
await ref.read(RoomsController.provider.notifier).addState(roomId, state);
}
// While there are under 20 events, try to load more
// until there's no more or the conditions are met.
if (room.hasMore && room.timeline.length < 20) {
loadOlder();
}
return room.timeline
.toEntryIList(compare: (a, b) => (a?.key ?? 0).compareTo(b?.key ?? 0))
.map((element) => element.value)
.toIList()
.addAll(room.sticky)
.map((entry) {
final foundEvent = entry == null ? null : room.events[entry];
final editedEvent =
foundEvent == null || foundEvent.lastEditRowId == 0
? null
: room.events[foundEvent.lastEditRowId];
return editedEvent == null
? foundEvent
: foundEvent?.copyWith(
content: editedEvent.content,
localContent: editedEvent.localContent,
);
})
.nonNulls
.toIList();
}
Future<void> deleteMessage(Event event, {String? reason}) => ref
.watch(ClientController.provider.notifier)
.redactEvent(
RedactEventRequest(
eventId: event.eventId,
roomId: roomId,
reason: reason,
),
);
Future<bool> loadOlder() async {
final timelineKeys = ref
.read(RoomsController.provider.select((value) => value[roomId]))
?.timeline
.keys;
final response = await ref
.watch(ClientController.provider.notifier)
.paginate(
.new(
roomId: roomId,
maxTimelineId: timelineKeys?.isNotEmpty == true
? timelineKeys?.reduce(min)
: null,
),
);
ref
.watch(RoomsController.provider.notifier)
.update(
IMap({
roomId: Room(
events: IMap.fromIterable(
response.events.addAll(response.relatedEvents),
keyMapper: (event) => event.rowId,
valueMapper: (event) => event,
),
hasMore: response.hasMore,
timeline: IMap.fromIterable(
response.events,
keyMapper: (event) => event.timelineRowId,
valueMapper: (event) => event.rowId,
),
),
}),
.new(),
);
return response.hasMore;
}
Future<void> send(
String text, {
bool shouldMention = true,
required IList<Tag> tags,
required RelationType relationType,
Event? relation,
}) async {
var taggedMessage = text;
for (final tag in tags) {
final escaped = RegExp.escape(tag.id);
final pattern = RegExp(r"@+(" + escaped + r")(#[^#]*#)?");
taggedMessage = taggedMessage.replaceAllMapped(
pattern,
(match) => match.group(1)!,
);
}
final client = ref.watch(ClientController.provider.notifier);
final event = await client.sendMessage(
SendMessageRequest(
roomId: roomId,
mentions: Mentions(
userIds: [
if (shouldMention == true &&
relation != null &&
relationType == RelationType.reply)
relation.sender,
].toIList(),
room: taggedMessage.contains("@room"),
),
text: taggedMessage,
relation: relation == null
? null
: .new(eventId: relation.eventId, relationType: relationType),
),
);
ref
.watch(RoomsController.provider.notifier)
.update(
.new({
roomId: .new(
events: .new({event.rowId: event}),
sticky: .new({event.rowId}),
),
}),
.new(),
);
}
Future<void> removeReaction(
String reaction,
Event event,
String userId,
) async {
final client = ref.watch(ClientController.provider.notifier);
final allReactionEvents = await client.getRelatedEvents(
.new(
roomId: roomId,
eventId: event.eventId,
relationType: "m.annotation",
),
);
final reactionEvents = allReactionEvents
?.where((event) => event.redactedBy == null)
.toIList();
final reactionEvent = reactionEvents?.firstWhereOrNull(
(event) => switch (event.content) {
ReactionContent(:final key) =>
key == reaction && event.sender == userId,
_ => false,
},
);
if (reactionEvent != null) {
await ref
.watch(ClientController.provider.notifier)
.redactEvent(.new(eventId: reactionEvent.eventId, roomId: roomId));
}
}
Future<void> sendReaction(String reaction, Event event) async {
final client = ref.watch(ClientController.provider.notifier);
await client.sendEvent(
.new(
roomId: roomId,
type: EventType.reaction.type,
content: ReactionContent(key: reaction),
synchronous: true,
disableEncryption: true,
relatesTo: event.eventId,
relationType: "m.annotation",
),
);
}
static final provider = AsyncNotifierProvider.family
.autoDispose<RoomChatController, IList<Event>?, String>(
RoomChatController.new,
);
}

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