forked from Nexus/nexus
treewide(refactor): rename files to follow the same style
This commit is contained in:
parent
15d441e4c4
commit
7992f0e815
81 changed files with 167 additions and 167 deletions
16
lib/controllers/account_data.dart
Normal file
16
lib/controllers/account_data.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
30
lib/controllers/author.dart
Normal file
30
lib/controllers/author.dart
Normal 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
301
lib/controllers/client.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
13
lib/controllers/client_state.dart
Normal file
13
lib/controllers/client_state.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
11
lib/controllers/cross_cache.dart
Normal file
11
lib/controllers/cross_cache.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
84
lib/controllers/emoji.dart
Normal file
84
lib/controllers/emoji.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
32
lib/controllers/event.dart
Normal file
32
lib/controllers/event.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
20
lib/controllers/header.dart
Normal file
20
lib/controllers/header.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
11
lib/controllers/init_complete.dart
Normal file
11
lib/controllers/init_complete.dart
Normal 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
30
lib/controllers/key.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
46
lib/controllers/members.dart
Normal file
46
lib/controllers/members.dart
Normal 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);
|
||||
}
|
||||
32
lib/controllers/members_by_status.dart
Normal file
32
lib/controllers/members_by_status.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.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);
|
||||
}
|
||||
64
lib/controllers/members_grouped.dart
Normal file
64
lib/controllers/members_grouped.dart
Normal 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);
|
||||
}
|
||||
19
lib/controllers/multi_provider.dart
Normal file
19
lib/controllers/multi_provider.dart
Normal 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);
|
||||
}
|
||||
30
lib/controllers/pinned_events.dart
Normal file
30
lib/controllers/pinned_events.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
53
lib/controllers/pinned_ids.dart
Normal file
53
lib/controllers/pinned_ids.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
81
lib/controllers/power_level.dart
Normal file
81
lib/controllers/power_level.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
17
lib/controllers/profile.dart
Normal file
17
lib/controllers/profile.dart
Normal 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);
|
||||
}
|
||||
55
lib/controllers/reactions.dart
Normal file
55
lib/controllers/reactions.dart
Normal 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);
|
||||
}
|
||||
221
lib/controllers/room_chat.dart
Normal file
221
lib/controllers/room_chat.dart
Normal 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
Loading…
Reference in a new issue