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