forked from Henry-Hiles/nexus
working message rendering
This commit is contained in:
parent
a28bced44d
commit
7c76bb6e66
20 changed files with 305 additions and 197 deletions
|
|
@ -1,6 +1,7 @@
|
||||||
import "dart:developer";
|
import "dart:developer";
|
||||||
import "dart:ffi";
|
import "dart:ffi";
|
||||||
import "dart:isolate";
|
import "dart:isolate";
|
||||||
|
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||||
import "package:ffi/ffi.dart";
|
import "package:ffi/ffi.dart";
|
||||||
import "package:flutter/foundation.dart";
|
import "package:flutter/foundation.dart";
|
||||||
import "package:nexus/controllers/client_state_controller.dart";
|
import "package:nexus/controllers/client_state_controller.dart";
|
||||||
|
|
@ -10,8 +11,12 @@ import "package:nexus/controllers/sync_status_controller.dart";
|
||||||
import "package:nexus/controllers/top_level_spaces_controller.dart";
|
import "package:nexus/controllers/top_level_spaces_controller.dart";
|
||||||
import "package:nexus/helpers/extensions/gomuks_buffer.dart";
|
import "package:nexus/helpers/extensions/gomuks_buffer.dart";
|
||||||
import "package:nexus/models/client_state.dart";
|
import "package:nexus/models/client_state.dart";
|
||||||
import "package:nexus/models/login.dart";
|
import "package:nexus/models/event.dart";
|
||||||
import "package:nexus/models/report.dart";
|
import "package:nexus/models/get_event_request.dart";
|
||||||
|
import "package:nexus/models/get_related_events_request.dart";
|
||||||
|
import "package:nexus/models/login_request.dart";
|
||||||
|
import "package:nexus/models/profile.dart";
|
||||||
|
import "package:nexus/models/report_request.dart";
|
||||||
import "package:nexus/models/room.dart";
|
import "package:nexus/models/room.dart";
|
||||||
import "package:nexus/models/sync_data.dart";
|
import "package:nexus/models/sync_data.dart";
|
||||||
import "package:nexus/models/sync_status.dart";
|
import "package:nexus/models/sync_status.dart";
|
||||||
|
|
@ -34,7 +39,7 @@ class ClientController extends AsyncNotifier<int> {
|
||||||
try {
|
try {
|
||||||
final muksEventType = command.cast<Utf8>().toDartString();
|
final muksEventType = command.cast<Utf8>().toDartString();
|
||||||
debugPrint("Handling $muksEventType...");
|
debugPrint("Handling $muksEventType...");
|
||||||
final Map<String, dynamic> decodedMuksEvent = data.toJson();
|
final decodedMuksEvent = data.toJson();
|
||||||
|
|
||||||
switch (muksEventType) {
|
switch (muksEventType) {
|
||||||
case "client_state":
|
case "client_state":
|
||||||
|
|
@ -92,10 +97,7 @@ class ClientController extends AsyncNotifier<int> {
|
||||||
throw Exception("GomuksStart returned error code $errorCode");
|
throw Exception("GomuksStart returned error code $errorCode");
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Map<String, dynamic>> sendCommand(
|
Future<dynamic> sendCommand(String command, Map<String, dynamic> data) async {
|
||||||
String command,
|
|
||||||
Map<String, dynamic> data,
|
|
||||||
) async {
|
|
||||||
final bufferPointer = data.toGomuksBufferPtr();
|
final bufferPointer = data.toGomuksBufferPtr();
|
||||||
final handle = await future;
|
final handle = await future;
|
||||||
final response = await Isolate.run(
|
final response = await Isolate.run(
|
||||||
|
|
@ -125,7 +127,27 @@ class ClientController extends AsyncNotifier<int> {
|
||||||
await sendCommand("leave_room", {"room_id": room.metadata!.id});
|
await sendCommand("leave_room", {"room_id": room.metadata!.id});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> reportEvent(Report report) =>
|
Future<IList<Event>?> getRelatedEvents(
|
||||||
|
GetRelatedEventsRequest request,
|
||||||
|
) async {
|
||||||
|
final response =
|
||||||
|
(await sendCommand("get_related_events", request.toJson())) as List?;
|
||||||
|
return response?.map((event) => Event.fromJson(event)).toIList();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Event?> getEvent(GetEventRequest request) async {
|
||||||
|
final json = await sendCommand("get_event", request.toJson());
|
||||||
|
|
||||||
|
return json == null ? null : Event.fromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Profile?> getProfile(String userId) async {
|
||||||
|
final json = await sendCommand("get_profile", {"user_id": userId});
|
||||||
|
|
||||||
|
return json == null ? null : Profile.fromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> reportEvent(ReportRequest report) =>
|
||||||
sendCommand("report_event", report.toJson());
|
sendCommand("report_event", report.toJson());
|
||||||
|
|
||||||
Future<void> markRead(Room room) async {
|
Future<void> markRead(Room room) async {
|
||||||
|
|
@ -137,7 +159,7 @@ class ClientController extends AsyncNotifier<int> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> login(Login login) async {
|
Future<bool> login(LoginRequest login) async {
|
||||||
try {
|
try {
|
||||||
await sendCommand("login", login.toJson());
|
await sendCommand("login", login.toJson());
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -151,7 +173,7 @@ class ClientController extends AsyncNotifier<int> {
|
||||||
final response = await sendCommand("discover_homeserver", {
|
final response = await sendCommand("discover_homeserver", {
|
||||||
"user_id": "@fakeuser:${homeserver.host}",
|
"user_id": "@fakeuser:${homeserver.host}",
|
||||||
});
|
});
|
||||||
return (response["m.homeserver"] as Map<String, dynamic>)["base_url"];
|
return response["m.homeserver"]?["base_url"];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
|
||||||
import "package:matrix/matrix.dart";
|
|
||||||
|
|
||||||
class EventsController extends AsyncNotifier<Timeline> {
|
|
||||||
EventsController(this.room);
|
|
||||||
final Room room;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<Timeline> build({String? from}) => room.getTimeline();
|
|
||||||
|
|
||||||
Future<void> prev() async {
|
|
||||||
final timeline = await future;
|
|
||||||
await timeline.requestHistory();
|
|
||||||
}
|
|
||||||
|
|
||||||
static final provider = AsyncNotifierProvider.autoDispose
|
|
||||||
.family<EventsController, Timeline, Room>(EventsController.new);
|
|
||||||
}
|
|
||||||
18
lib/controllers/new_events_controller.dart
Normal file
18
lib/controllers/new_events_controller.dart
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||||
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
|
import "package:nexus/models/event.dart";
|
||||||
|
|
||||||
|
class NewEventsController extends Notifier<IList<Event>> {
|
||||||
|
final String roomId;
|
||||||
|
NewEventsController(this.roomId);
|
||||||
|
|
||||||
|
@override
|
||||||
|
IList<Event> build() => const IList.empty();
|
||||||
|
|
||||||
|
void add(IList<Event> newEvents) => state = newEvents;
|
||||||
|
|
||||||
|
static final provider = NotifierProvider.autoDispose
|
||||||
|
.family<NewEventsController, IList<Event>, String>(
|
||||||
|
NewEventsController.new,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,65 +1,72 @@
|
||||||
import "package:collection/collection.dart";
|
import "package:collection/collection.dart";
|
||||||
|
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||||
import "package:flutter_chat_core/flutter_chat_core.dart";
|
import "package:flutter_chat_core/flutter_chat_core.dart";
|
||||||
import "package:flutter_chat_core/flutter_chat_core.dart" as chat;
|
import "package:flutter_chat_core/flutter_chat_core.dart" as chat;
|
||||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
import "package:fluttertagger/fluttertagger.dart" as tagger;
|
import "package:fluttertagger/fluttertagger.dart" as tagger;
|
||||||
|
import "package:nexus/controllers/client_controller.dart";
|
||||||
|
import "package:nexus/controllers/new_events_controller.dart";
|
||||||
|
import "package:nexus/controllers/selected_room_controller.dart";
|
||||||
|
import "package:nexus/helpers/extensions/event_to_message.dart";
|
||||||
|
import "package:nexus/helpers/extensions/list_to_messages.dart";
|
||||||
import "package:nexus/models/relation_type.dart";
|
import "package:nexus/models/relation_type.dart";
|
||||||
import "package:nexus/models/room.dart";
|
|
||||||
|
|
||||||
class RoomChatController extends AsyncNotifier<ChatController> {
|
class RoomChatController extends AsyncNotifier<ChatController> {
|
||||||
final Room room;
|
final String roomId;
|
||||||
RoomChatController(this.room);
|
RoomChatController(this.roomId);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<ChatController> build() async {
|
Future<ChatController> build() async {
|
||||||
// final timeline = await ref.watch(EventsController.provider(room).future);
|
final client = ref.watch(ClientController.provider.notifier);
|
||||||
|
final events =
|
||||||
|
ref.read(SelectedRoomController.provider)?.events ??
|
||||||
|
const IList.empty();
|
||||||
|
|
||||||
return InMemoryChatController();
|
ref.onDispose(
|
||||||
|
ref.listen(NewEventsController.provider(roomId), (_, next) async {
|
||||||
|
for (final event in next) {
|
||||||
|
if (event.type == "m.room.redaction") {
|
||||||
|
final controller = await future;
|
||||||
|
final message = controller.messages.firstWhereOrNull(
|
||||||
|
(message) => message.id == event.content["redacts"],
|
||||||
|
);
|
||||||
|
if (message == null) return;
|
||||||
|
|
||||||
// ref.onDispose(
|
await controller.removeMessage(message);
|
||||||
// room.client.onTimelineEvent.stream.listen((event) async {
|
} else {
|
||||||
// if (event.roomId != room.metadata.id) return;
|
final message = await event.toMessage(client, includeEdits: true);
|
||||||
|
if (event.relationType == "m.replace") {
|
||||||
|
final controller = await future;
|
||||||
|
final oldMessage = controller.messages.firstWhereOrNull(
|
||||||
|
(element) => element.id == event.relatesTo,
|
||||||
|
);
|
||||||
|
if (oldMessage == null || message == null) return;
|
||||||
|
|
||||||
// if (event.type == "m.room.redaction") {
|
return await updateMessage(
|
||||||
// final controller = await future;
|
oldMessage,
|
||||||
// final message = controller.messages.firstWhereOrNull(
|
message.copyWith(
|
||||||
// (message) => message.id == event.redacts,
|
id: oldMessage.id,
|
||||||
// );
|
replyToMessageId: oldMessage.replyToMessageId,
|
||||||
// if (message == null) return;
|
metadata: {
|
||||||
|
...(oldMessage.metadata ?? {}),
|
||||||
|
...(message.metadata ?? {})
|
||||||
|
.toIMap()
|
||||||
|
.where((key, value) => value != null)
|
||||||
|
.unlock,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (message != null) {
|
||||||
|
return await insertMessage(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).close,
|
||||||
|
);
|
||||||
|
|
||||||
// await controller.removeMessage(message);
|
final messages = await events.toMessages(client);
|
||||||
// } else {
|
return InMemoryChatController(messages: messages);
|
||||||
// final message = await event.toMessage(includeEdits: true, timeline);
|
|
||||||
// if (event.relationshipType == RelationshipTypes.edit) {
|
|
||||||
// final controller = await future;
|
|
||||||
// final oldMessage = controller.messages.firstWhereOrNull(
|
|
||||||
// (element) => element.id == event.relationshipEventId,
|
|
||||||
// );
|
|
||||||
// if (oldMessage == null || message == null) return;
|
|
||||||
// return await updateMessage(
|
|
||||||
// oldMessage,
|
|
||||||
// message.copyWith(
|
|
||||||
// id: oldMessage.id,
|
|
||||||
// replyToMessageId: oldMessage.replyToMessageId,
|
|
||||||
// metadata: {
|
|
||||||
// ...(oldMessage.metadata ?? {}),
|
|
||||||
// ...((message.metadata ?? {}).filterMap(
|
|
||||||
// (key, value) => value == null ? null : MapEntry(key, value),
|
|
||||||
// )),
|
|
||||||
// },
|
|
||||||
// ),
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
// if (message != null) {
|
|
||||||
// return await insertMessage(message);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }).cancel,
|
|
||||||
// );
|
|
||||||
|
|
||||||
// return InMemoryChatController(
|
|
||||||
// messages: await timeline.events.toMessages(room, timeline),
|
|
||||||
// );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> insertMessage(Message message) async {
|
Future<void> insertMessage(Message message) async {
|
||||||
|
|
@ -145,8 +152,8 @@ class RoomChatController extends AsyncNotifier<ChatController> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static final provider = AsyncNotifierProvider.family
|
static final provider =
|
||||||
.autoDispose<RoomChatController, ChatController, Room>(
|
AsyncNotifierProvider.family<RoomChatController, ChatController, String>(
|
||||||
RoomChatController.new,
|
RoomChatController.new,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
|
import "package:nexus/controllers/new_events_controller.dart";
|
||||||
import "package:nexus/models/read_receipt.dart";
|
import "package:nexus/models/read_receipt.dart";
|
||||||
import "package:nexus/models/room.dart";
|
import "package:nexus/models/room.dart";
|
||||||
|
|
||||||
|
|
@ -13,11 +14,18 @@ class RoomsController extends Notifier<IMap<String, Room>> {
|
||||||
final incoming = entry.value;
|
final incoming = entry.value;
|
||||||
final existing = acc[roomId];
|
final existing = acc[roomId];
|
||||||
|
|
||||||
|
ref
|
||||||
|
.watch(NewEventsController.provider(roomId).notifier)
|
||||||
|
.add(incoming.events);
|
||||||
|
|
||||||
return acc.add(
|
return acc.add(
|
||||||
roomId,
|
roomId,
|
||||||
existing?.copyWith(
|
existing?.copyWith(
|
||||||
metadata: incoming.metadata ?? existing.metadata,
|
metadata: incoming.metadata ?? existing.metadata,
|
||||||
events: existing.events.addAll(incoming.events),
|
events: existing.events.updateById(
|
||||||
|
incoming.events,
|
||||||
|
(item) => item.eventId,
|
||||||
|
),
|
||||||
state: incoming.state.entries.fold(
|
state: incoming.state.entries.fold(
|
||||||
existing.state,
|
existing.state,
|
||||||
(stateAcc, event) => stateAcc.add(
|
(stateAcc, event) => stateAcc.add(
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,36 @@
|
||||||
import "package:collection/collection.dart";
|
import "dart:developer";
|
||||||
|
|
||||||
import "package:flutter_chat_core/flutter_chat_core.dart";
|
import "package:flutter_chat_core/flutter_chat_core.dart";
|
||||||
import "package:matrix/matrix.dart";
|
import "package:nexus/controllers/client_controller.dart";
|
||||||
|
import "package:nexus/models/event.dart";
|
||||||
|
import "package:nexus/models/get_event_request.dart";
|
||||||
|
import "package:nexus/models/get_related_events_request.dart";
|
||||||
|
|
||||||
extension EventToMessage on Event {
|
extension EventToMessage on Event {
|
||||||
Future<Message?> toMessage(
|
Future<Message?> toMessage(
|
||||||
Timeline timeline, {
|
ClientController client, {
|
||||||
bool mustBeText = false,
|
bool mustBeText = false,
|
||||||
bool includeEdits = false,
|
bool includeEdits = false,
|
||||||
}) async {
|
}) async {
|
||||||
final replyId = inReplyToEventId();
|
if (relationType == "m.replace" && !includeEdits) return null;
|
||||||
final newEvent = (unsigned?["m.relations"] as Map?)?["m.replace"];
|
|
||||||
final event = newEvent == null ? this : Event.fromJson(newEvent, room);
|
|
||||||
|
|
||||||
|
final newEvents = await client.getRelatedEvents(
|
||||||
|
GetRelatedEventsRequest(
|
||||||
|
roomId: roomId,
|
||||||
|
eventId: eventId,
|
||||||
|
relationType: "m.replace",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final event = newEvents?.lastOrNull ?? this;
|
||||||
|
|
||||||
|
final replyId = this.content["m.relates_to"]?["m.in_reply_to"]?["event_id"];
|
||||||
final replyEvent = replyId == null
|
final replyEvent = replyId == null
|
||||||
? null
|
? null
|
||||||
: await room.getEventById(replyId);
|
: await client.getEvent(
|
||||||
|
GetEventRequest(roomId: roomId, eventId: replyId),
|
||||||
|
);
|
||||||
|
|
||||||
final sender =
|
final author = await client.getProfile(event.authorId);
|
||||||
await event.fetchSenderUser() ?? event.senderFromMemoryOrFallback;
|
|
||||||
final newContent = event.content["m.new_content"] as Map?;
|
final newContent = event.content["m.new_content"] as Map?;
|
||||||
final metadata = {
|
final metadata = {
|
||||||
"formatted":
|
"formatted":
|
||||||
|
|
@ -26,109 +39,108 @@ extension EventToMessage on Event {
|
||||||
event.content["formatted_body"] ??
|
event.content["formatted_body"] ??
|
||||||
event.content["body"] ??
|
event.content["body"] ??
|
||||||
"",
|
"",
|
||||||
"reply": await replyEvent?.toMessage(mustBeText: true, timeline),
|
"reply": await replyEvent?.toMessage(client, mustBeText: true),
|
||||||
"body": newContent?["body"] ?? event.content["body"],
|
"body": newContent?["body"] ?? event.content["body"],
|
||||||
"eventType": event.type,
|
"eventType": event.type,
|
||||||
"avatarUrl": sender.avatarUrl.toString(),
|
"avatarUrl": author?.avatarUrl,
|
||||||
"displayName": sender.displayName ?? sender.id,
|
"displayName": author?.displayName ?? authorId,
|
||||||
"txnId": transactionId,
|
"txnId": transactionId,
|
||||||
};
|
};
|
||||||
|
|
||||||
final editedAt = event.relationshipType == RelationshipTypes.edit
|
final editedAt = event.relationType == "m.replace" ? event.timestamp : null;
|
||||||
? event.originServerTs
|
|
||||||
: null;
|
|
||||||
|
|
||||||
if ((redacted && !mustBeText) ||
|
if ((event.redactedBy != null && !mustBeText) ||
|
||||||
(!includeEdits && (relationshipType == RelationshipTypes.edit))) {
|
(!includeEdits && (relationType == "m.replace"))) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Use server-generated preview if enabled when https://github.com/famedly/matrix-dart-sdk/issues/2195 is fixed.
|
// TODO: Use server-generated preview if enabled
|
||||||
|
|
||||||
// final match = Uri.tryParse(
|
// final match = Uri.tryParse(
|
||||||
// RegExp(regexLink, caseSensitive: false).firstMatch(body)?.group(0) ?? "",
|
// RegExp(regexLink, caseSensitive: false).firstMatch(body)?.group(0) ?? "",
|
||||||
// );
|
// );
|
||||||
|
|
||||||
// final preview = match == null
|
|
||||||
// ? null
|
|
||||||
// : await room.client.getUrlPreview(match);
|
|
||||||
|
|
||||||
final asText =
|
final asText =
|
||||||
Message.text(
|
Message.text(
|
||||||
metadata: metadata,
|
metadata: metadata,
|
||||||
id: eventId,
|
id: eventId,
|
||||||
authorId: senderId,
|
authorId: authorId,
|
||||||
text: redacted ? "This message has been deleted..." : event.body,
|
text: redactedBy == null
|
||||||
|
? event.content["body"] ?? ""
|
||||||
|
: "This message has been deleted...",
|
||||||
replyToMessageId: replyId,
|
replyToMessageId: replyId,
|
||||||
deliveredAt: originServerTs,
|
deliveredAt: timestamp,
|
||||||
editedAt: editedAt,
|
editedAt: editedAt,
|
||||||
)
|
)
|
||||||
as TextMessage;
|
as TextMessage;
|
||||||
|
|
||||||
|
final content = (decrypted ?? this.content);
|
||||||
|
|
||||||
if (mustBeText) return asText;
|
if (mustBeText) return asText;
|
||||||
return switch (type) {
|
return switch (type) {
|
||||||
EventTypes.Encrypted => asText.copyWith(
|
"m.room.encrypted" => asText.copyWith(
|
||||||
text: "Unable to decrypt message.",
|
text: "Unable to decrypt message.",
|
||||||
metadata: {...metadata, "formatted": "Unable to decrypt message."},
|
metadata: {...metadata, "formatted": "Unable to decrypt message."},
|
||||||
),
|
),
|
||||||
PollEventContent.startType => Message.custom(
|
// "org.matrix.msc3381.poll.start" => Message.custom(
|
||||||
metadata: {
|
// metadata: {
|
||||||
...metadata,
|
// ...metadata,
|
||||||
"poll": event.parsedPollEventContent.pollStartContent,
|
// "poll": event.parsedPollEventContent.pollStartContent,
|
||||||
"responses": event.getPollResponses(timeline),
|
// "responses": event.getPollResponses(timeline),
|
||||||
},
|
// },
|
||||||
id: eventId,
|
// id: eventId,
|
||||||
deliveredAt: originServerTs,
|
// deliveredAt: originServerTs,
|
||||||
authorId: senderId,
|
// authorId: senderId,
|
||||||
),
|
// ),
|
||||||
(EventTypes.Sticker || EventTypes.Message) => switch (messageType) {
|
("m.sticker" || "m.room.message") => switch (content["msgtype"]) {
|
||||||
(MessageTypes.Sticker || MessageTypes.Image) => Message.image(
|
("m.sticker" || "m.image") => Message.image(
|
||||||
metadata: metadata,
|
metadata: metadata,
|
||||||
id: eventId,
|
id: eventId,
|
||||||
authorId: senderId,
|
authorId: authorId,
|
||||||
text: event.text,
|
text: event.localContent?.sanitizedHtml,
|
||||||
source: (await getAttachmentUri()).toString(),
|
source: "(await getAttachmentUri()).toString()", // TODO
|
||||||
replyToMessageId: replyId,
|
replyToMessageId: replyId,
|
||||||
deliveredAt: originServerTs,
|
deliveredAt: timestamp,
|
||||||
blurhash: (event.content["info"] as Map?)?["xyz.amorgan.blurhash"],
|
blurhash: (event.content["info"] as Map?)?["xyz.amorgan.blurhash"],
|
||||||
),
|
),
|
||||||
MessageTypes.Audio => Message.audio(
|
"m.audio" => Message.audio(
|
||||||
metadata: metadata,
|
metadata: metadata,
|
||||||
id: eventId,
|
id: eventId,
|
||||||
authorId: senderId,
|
authorId: authorId,
|
||||||
text: event.text,
|
text: event.content["body"],
|
||||||
replyToMessageId: replyId,
|
replyToMessageId: replyId,
|
||||||
source: (await event.getAttachmentUri()).toString(),
|
source: "(await event.getAttachmentUri()).toString()", // TODO
|
||||||
deliveredAt: originServerTs,
|
deliveredAt: timestamp,
|
||||||
// TODO: See if we can figure out duration
|
// TODO: See if we can figure out duration
|
||||||
duration: Duration(hours: 1),
|
duration: Duration(hours: 1),
|
||||||
),
|
),
|
||||||
MessageTypes.File => Message.file(
|
"m.file" => Message.file(
|
||||||
name: event.content["filename"].toString(),
|
name: event.content["filename"].toString(),
|
||||||
metadata: metadata,
|
metadata: metadata,
|
||||||
id: eventId,
|
id: eventId,
|
||||||
authorId: senderId,
|
authorId: authorId,
|
||||||
source: (await event.getAttachmentUri()).toString(),
|
source: "(await event.getAttachmentUri()).toString()", // TODO
|
||||||
replyToMessageId: replyId,
|
replyToMessageId: replyId,
|
||||||
deliveredAt: originServerTs,
|
deliveredAt: timestamp,
|
||||||
),
|
),
|
||||||
_ => asText,
|
_ => asText,
|
||||||
},
|
},
|
||||||
EventTypes.RoomMember => Message.system(
|
"m.room.member" => Message.system(
|
||||||
metadata: metadata,
|
metadata: metadata,
|
||||||
id: eventId,
|
id: eventId,
|
||||||
authorId: senderId,
|
authorId: authorId,
|
||||||
|
deliveredAt: timestamp,
|
||||||
text:
|
text:
|
||||||
"${event.asUser.displayName ?? event.asUser.id} ${switch (Membership.values.firstWhereOrNull((membership) => membership.name == event.content["membership"])) {
|
"${content["displayname"] ?? event.stateKey} ${switch (event.content["membership"]) {
|
||||||
Membership.invite => "was invited to",
|
"invite" => "was invited to",
|
||||||
Membership.join => "joined",
|
"join" => "joined",
|
||||||
Membership.leave => "left",
|
"leave" => "left",
|
||||||
Membership.knock => "asked to join",
|
"knock" => "asked to join",
|
||||||
Membership.ban => "was banned from",
|
"ban" => "was banned from",
|
||||||
_ => "did something relating to",
|
_ => "did something relating to",
|
||||||
}} the room.",
|
}} the room.",
|
||||||
),
|
),
|
||||||
EventTypes.Redaction => null,
|
"m.room.redaction" => null,
|
||||||
_ =>
|
_ =>
|
||||||
// Turn this on for debugging purposes
|
// Turn this on for debugging purposes
|
||||||
false
|
false
|
||||||
|
|
@ -136,7 +148,7 @@ extension EventToMessage on Event {
|
||||||
? Message.unsupported(
|
? Message.unsupported(
|
||||||
metadata: metadata,
|
metadata: metadata,
|
||||||
id: eventId,
|
id: eventId,
|
||||||
authorId: senderId,
|
authorId: authorId,
|
||||||
replyToMessageId: replyId,
|
replyToMessageId: replyId,
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import "dart:typed_data";
|
||||||
import "package:ffi/ffi.dart";
|
import "package:ffi/ffi.dart";
|
||||||
import "package:nexus/src/third_party/gomuks.g.dart";
|
import "package:nexus/src/third_party/gomuks.g.dart";
|
||||||
|
|
||||||
extension GomuksOwnedBufferToJson on GomuksOwnedBuffer {
|
extension GomuksOwnedBufferToX on GomuksOwnedBuffer {
|
||||||
Uint8List toBytes() {
|
Uint8List toBytes() {
|
||||||
try {
|
try {
|
||||||
if (base == nullptr || length <= 0) return Uint8List(0);
|
if (base == nullptr || length <= 0) return Uint8List(0);
|
||||||
|
|
@ -14,14 +14,7 @@ extension GomuksOwnedBufferToJson on GomuksOwnedBuffer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
dynamic toJson() => jsonDecode(utf8.decode(toBytes()));
|
||||||
final bytes = toBytes();
|
|
||||||
if (bytes.isEmpty) return {};
|
|
||||||
final json = jsonDecode(utf8.decode(bytes));
|
|
||||||
|
|
||||||
if (json is Map<String, dynamic>?) return json ?? {};
|
|
||||||
throw json;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension JsonToGomuksBuffer on Map<String, dynamic> {
|
extension JsonToGomuksBuffer on Map<String, dynamic> {
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
|
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||||
import "package:flutter_chat_core/flutter_chat_core.dart";
|
import "package:flutter_chat_core/flutter_chat_core.dart";
|
||||||
import "package:matrix/matrix.dart";
|
import "package:nexus/controllers/client_controller.dart";
|
||||||
import "package:nexus/helpers/extensions/event_to_message.dart";
|
import "package:nexus/helpers/extensions/event_to_message.dart";
|
||||||
|
import "package:nexus/models/event.dart";
|
||||||
|
|
||||||
extension ListToMessages on List<MatrixEvent> {
|
extension ListToMessages on IList<Event> {
|
||||||
Future<List<Message>> toMessages(Room room, Timeline timeline) async =>
|
Future<List<Message>> toMessages(ClientController client) async =>
|
||||||
(await Future.wait(
|
(await Future.wait(
|
||||||
map((event) => Event.fromMatrixEvent(event, room).toMessage(timeline)),
|
map((event) => event.toMessage(client)),
|
||||||
)).nonNulls.toList().reversed.toList();
|
)).nonNulls.toList();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ abstract class Event with _$Event {
|
||||||
String? transactionId,
|
String? transactionId,
|
||||||
String? redactedBy,
|
String? redactedBy,
|
||||||
String? relatesTo,
|
String? relatesTo,
|
||||||
@JsonKey(name: "relates_type") String? relationType,
|
String? relationType,
|
||||||
String? decryptionError,
|
String? decryptionError,
|
||||||
String? sendError,
|
String? sendError,
|
||||||
@Default(IMap.empty()) IMap<String, int> reactions,
|
@Default(IMap.empty()) IMap<String, int> reactions,
|
||||||
|
|
|
||||||
15
lib/models/get_event_request.dart
Normal file
15
lib/models/get_event_request.dart
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
import "package:freezed_annotation/freezed_annotation.dart";
|
||||||
|
part "get_event_request.freezed.dart";
|
||||||
|
part "get_event_request.g.dart";
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
abstract class GetEventRequest with _$GetEventRequest {
|
||||||
|
const factory GetEventRequest({
|
||||||
|
required String roomId,
|
||||||
|
required String eventId,
|
||||||
|
@Default(false) bool unredact,
|
||||||
|
}) = _GetEventRequest;
|
||||||
|
|
||||||
|
factory GetEventRequest.fromJson(Map<String, Object?> json) =>
|
||||||
|
_$GetEventRequestFromJson(json);
|
||||||
|
}
|
||||||
15
lib/models/get_related_events_request.dart
Normal file
15
lib/models/get_related_events_request.dart
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
import "package:freezed_annotation/freezed_annotation.dart";
|
||||||
|
part "get_related_events_request.freezed.dart";
|
||||||
|
part "get_related_events_request.g.dart";
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
abstract class GetRelatedEventsRequest with _$GetRelatedEventsRequest {
|
||||||
|
const factory GetRelatedEventsRequest({
|
||||||
|
required String roomId,
|
||||||
|
required String eventId,
|
||||||
|
required String relationType,
|
||||||
|
}) = _GetRelatedEventsRequest;
|
||||||
|
|
||||||
|
factory GetRelatedEventsRequest.fromJson(Map<String, Object?> json) =>
|
||||||
|
_$GetRelatedEventsRequestFromJson(json);
|
||||||
|
}
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
import "package:freezed_annotation/freezed_annotation.dart";
|
|
||||||
part "login.freezed.dart";
|
|
||||||
part "login.g.dart";
|
|
||||||
|
|
||||||
@freezed
|
|
||||||
abstract class Login with _$Login {
|
|
||||||
const factory Login({
|
|
||||||
required String username,
|
|
||||||
required String password,
|
|
||||||
required String homeserverUrl,
|
|
||||||
}) = _Login;
|
|
||||||
|
|
||||||
factory Login.fromJson(Map<String, Object?> json) => _$LoginFromJson(json);
|
|
||||||
}
|
|
||||||
15
lib/models/login_request.dart
Normal file
15
lib/models/login_request.dart
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
import "package:freezed_annotation/freezed_annotation.dart";
|
||||||
|
part "login_request.freezed.dart";
|
||||||
|
part "login_request.g.dart";
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
abstract class LoginRequest with _$LoginRequest {
|
||||||
|
const factory LoginRequest({
|
||||||
|
required String username,
|
||||||
|
required String password,
|
||||||
|
required String homeserverUrl,
|
||||||
|
}) = _LoginRequest;
|
||||||
|
|
||||||
|
factory LoginRequest.fromJson(Map<String, Object?> json) =>
|
||||||
|
_$LoginRequestFromJson(json);
|
||||||
|
}
|
||||||
29
lib/models/profile.dart
Normal file
29
lib/models/profile.dart
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||||
|
import "package:freezed_annotation/freezed_annotation.dart";
|
||||||
|
part "profile.freezed.dart";
|
||||||
|
part "profile.g.dart";
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
abstract class Profile with _$Profile {
|
||||||
|
const factory Profile({
|
||||||
|
String? avatarUrl,
|
||||||
|
@JsonKey(name: "displayname") String? displayName,
|
||||||
|
@JsonKey(name: "us.cloke.msc4175.tz") String? timezone,
|
||||||
|
|
||||||
|
@Default(IList.empty())
|
||||||
|
@JsonKey(name: "io.fsky.nyx.pronouns")
|
||||||
|
IList<Pronoun> pronouns,
|
||||||
|
}) = _Profile;
|
||||||
|
|
||||||
|
factory Profile.fromJson(Map<String, Object?> json) =>
|
||||||
|
_$ProfileFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
abstract class Pronoun with _$Pronoun {
|
||||||
|
const factory Pronoun({required String language, required String summary}) =
|
||||||
|
_Pronoun;
|
||||||
|
|
||||||
|
factory Pronoun.fromJson(Map<String, Object?> json) =>
|
||||||
|
_$PronounFromJson(json);
|
||||||
|
}
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
import "package:freezed_annotation/freezed_annotation.dart";
|
|
||||||
part "report.freezed.dart";
|
|
||||||
part "report.g.dart";
|
|
||||||
|
|
||||||
@freezed
|
|
||||||
abstract class Report with _$Report {
|
|
||||||
const factory Report({
|
|
||||||
required String roomId,
|
|
||||||
required String eventId,
|
|
||||||
String? reason,
|
|
||||||
}) = _Report;
|
|
||||||
|
|
||||||
factory Report.fromJson(Map<String, Object?> json) => _$ReportFromJson(json);
|
|
||||||
}
|
|
||||||
15
lib/models/report_request.dart
Normal file
15
lib/models/report_request.dart
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
import "package:freezed_annotation/freezed_annotation.dart";
|
||||||
|
part "report_request.freezed.dart";
|
||||||
|
part "report_request.g.dart";
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
abstract class ReportRequest with _$ReportRequest {
|
||||||
|
const factory ReportRequest({
|
||||||
|
required String roomId,
|
||||||
|
required String eventId,
|
||||||
|
String? reason,
|
||||||
|
}) = _ReportRequest;
|
||||||
|
|
||||||
|
factory ReportRequest.fromJson(Map<String, Object?> json) =>
|
||||||
|
_$ReportRequestFromJson(json);
|
||||||
|
}
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
import "package:flutter/material.dart";
|
import "package:flutter/material.dart";
|
||||||
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
import "package:nexus/widgets/chat_page/sidebar.dart";
|
import "package:nexus/widgets/chat_page/sidebar.dart";
|
||||||
import "package:nexus/widgets/chat_page/room_chat.dart";
|
import "package:nexus/widgets/chat_page/room_chat.dart";
|
||||||
|
|
||||||
class ChatPage extends StatelessWidget {
|
class ChatPage extends ConsumerWidget {
|
||||||
const ChatPage({super.key});
|
const ChatPage({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => LayoutBuilder(
|
Widget build(BuildContext context, WidgetRef ref) => LayoutBuilder(
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
final isDesktop = constraints.maxWidth > 650;
|
final isDesktop = constraints.maxWidth > 650;
|
||||||
final showMembersByDefault = constraints.maxWidth > 1000;
|
final showMembersByDefault = constraints.maxWidth > 1000;
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||||
import "package:nexus/controllers/client_controller.dart";
|
import "package:nexus/controllers/client_controller.dart";
|
||||||
import "package:nexus/helpers/launch_helper.dart";
|
import "package:nexus/helpers/launch_helper.dart";
|
||||||
import "package:nexus/models/homeserver.dart";
|
import "package:nexus/models/homeserver.dart";
|
||||||
import "package:nexus/models/login.dart";
|
import "package:nexus/models/login_request.dart";
|
||||||
import "package:nexus/widgets/appbar.dart";
|
import "package:nexus/widgets/appbar.dart";
|
||||||
import "package:nexus/widgets/divider_text.dart";
|
import "package:nexus/widgets/divider_text.dart";
|
||||||
import "package:nexus/widgets/loading.dart";
|
import "package:nexus/widgets/loading.dart";
|
||||||
|
|
@ -174,7 +174,7 @@ class LoginPage extends HookConsumerWidget {
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
final succeeded = await client.login(
|
final succeeded = await client.login(
|
||||||
Login(
|
LoginRequest(
|
||||||
username: username.text,
|
username: username.text,
|
||||||
password: password.text,
|
password: password.text,
|
||||||
homeserverUrl: homeserver.value!,
|
homeserverUrl: homeserver.value!,
|
||||||
|
|
|
||||||
|
|
@ -44,9 +44,9 @@ class ChatBox extends HookConsumerWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
void send() {
|
void send() {
|
||||||
if (controller.value.text.trim().isEmpty) return;
|
if (controller.value.text.trim().isEmpty || room.metadata == null) return;
|
||||||
ref
|
ref
|
||||||
.watch(RoomChatController.provider(room).notifier)
|
.watch(RoomChatController.provider(room.metadata!.id).notifier)
|
||||||
.send(
|
.send(
|
||||||
controller.value.formattedText,
|
controller.value.formattedText,
|
||||||
relation: relatedMessage,
|
relation: relatedMessage,
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ import "package:nexus/helpers/extensions/better_when.dart";
|
||||||
import "package:nexus/helpers/extensions/get_headers.dart";
|
import "package:nexus/helpers/extensions/get_headers.dart";
|
||||||
import "package:nexus/helpers/extensions/show_context_menu.dart";
|
import "package:nexus/helpers/extensions/show_context_menu.dart";
|
||||||
import "package:nexus/models/relation_type.dart";
|
import "package:nexus/models/relation_type.dart";
|
||||||
import "package:nexus/models/report.dart";
|
import "package:nexus/models/report_request.dart";
|
||||||
import "package:nexus/widgets/chat_page/chat_box.dart";
|
import "package:nexus/widgets/chat_page/chat_box.dart";
|
||||||
import "package:nexus/widgets/chat_page/html/html.dart";
|
import "package:nexus/widgets/chat_page/html/html.dart";
|
||||||
import "package:nexus/widgets/chat_page/room_appbar.dart";
|
import "package:nexus/widgets/chat_page/room_appbar.dart";
|
||||||
|
|
@ -43,12 +43,13 @@ class RoomChat extends HookConsumerWidget {
|
||||||
final replyToMessage = useState<Message?>(null);
|
final replyToMessage = useState<Message?>(null);
|
||||||
final memberListOpened = useState<bool>(showMembersByDefault);
|
final memberListOpened = useState<bool>(showMembersByDefault);
|
||||||
final relationType = useState(RelationType.reply);
|
final relationType = useState(RelationType.reply);
|
||||||
final theme = Theme.of(context);
|
|
||||||
final danger = theme.colorScheme.error;
|
|
||||||
final room = ref.watch(SelectedRoomController.provider);
|
final room = ref.watch(SelectedRoomController.provider);
|
||||||
final userId = ref.watch(ClientStateController.provider)?.userId;
|
final userId = ref.watch(ClientStateController.provider)?.userId;
|
||||||
|
|
||||||
if (room == null || userId == null) {
|
final theme = Theme.of(context);
|
||||||
|
final danger = theme.colorScheme.error;
|
||||||
|
|
||||||
|
if (room == null || userId == null || room.metadata?.id == null) {
|
||||||
return Center(
|
return Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
"Nothing to see here...",
|
"Nothing to see here...",
|
||||||
|
|
@ -56,7 +57,8 @@ class RoomChat extends HookConsumerWidget {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
final controllerProvider = RoomChatController.provider(room);
|
|
||||||
|
final controllerProvider = RoomChatController.provider(room.metadata!.id);
|
||||||
final notifier = ref.watch(controllerProvider.notifier);
|
final notifier = ref.watch(controllerProvider.notifier);
|
||||||
|
|
||||||
List<PopupMenuEntry> getMessageOptions(Message message) {
|
List<PopupMenuEntry> getMessageOptions(Message message) {
|
||||||
|
|
@ -158,7 +160,7 @@ class RoomChat extends HookConsumerWidget {
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
if (room.metadata == null) return;
|
if (room.metadata == null) return;
|
||||||
client.reportEvent(
|
client.reportEvent(
|
||||||
Report(
|
ReportRequest(
|
||||||
roomId: room.metadata!.id,
|
roomId: room.metadata!.id,
|
||||||
eventId: message.id,
|
eventId: message.id,
|
||||||
reason: reasonController.text.isEmpty
|
reason: reasonController.text.isEmpty
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue