reply rendering
This commit is contained in:
parent
a453114c17
commit
2a4525a78f
6 changed files with 226 additions and 109 deletions
21
lib/controllers/message_controller.dart
Normal file
21
lib/controllers/message_controller.dart
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
import "package:flutter_chat_core/flutter_chat_core.dart";
|
||||||
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
|
import "package:nexus/controllers/current_room_controller.dart";
|
||||||
|
import "package:nexus/helpers/extension_helper.dart";
|
||||||
|
|
||||||
|
class MessageController extends AsyncNotifier<TextMessage?> {
|
||||||
|
final String id;
|
||||||
|
MessageController(this.id);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<TextMessage?> build() async {
|
||||||
|
final room = await ref.watch(CurrentRoomController.provider.future);
|
||||||
|
final event = await room.roomData.getEventById(id);
|
||||||
|
return (await event?.toMessage(mustBeText: true)) as TextMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
static final provider =
|
||||||
|
AsyncNotifierProvider.family<MessageController, TextMessage?, String>(
|
||||||
|
MessageController.new,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,7 @@ 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:matrix/matrix.dart";
|
import "package:matrix/matrix.dart";
|
||||||
|
import "package:nexus/helpers/extension_helper.dart";
|
||||||
|
|
||||||
class RoomChatController extends AsyncNotifier<ChatController> {
|
class RoomChatController extends AsyncNotifier<ChatController> {
|
||||||
RoomChatController(this.room);
|
RoomChatController(this.room);
|
||||||
|
|
@ -13,7 +14,7 @@ class RoomChatController extends AsyncNotifier<ChatController> {
|
||||||
final timeline = await room.getTimeline();
|
final timeline = await room.getTimeline();
|
||||||
room.client.onTimelineEvent.stream.listen((event) async {
|
room.client.onTimelineEvent.stream.listen((event) async {
|
||||||
if (event.roomId != room.id) return;
|
if (event.roomId != room.id) return;
|
||||||
final message = await toMessage(event);
|
final message = await event.toMessage();
|
||||||
if (message != null) {
|
if (message != null) {
|
||||||
await insertMessage(message);
|
await insertMessage(message);
|
||||||
}
|
}
|
||||||
|
|
@ -21,7 +22,7 @@ class RoomChatController extends AsyncNotifier<ChatController> {
|
||||||
|
|
||||||
return InMemoryChatController(
|
return InMemoryChatController(
|
||||||
messages: (await Future.wait(
|
messages: (await Future.wait(
|
||||||
timeline.events.map(toMessage),
|
timeline.events.map((event) => event.toMessage()),
|
||||||
)).toList().reversed.nonNulls.toList(),
|
)).toList().reversed.nonNulls.toList(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -42,78 +43,6 @@ class RoomChatController extends AsyncNotifier<ChatController> {
|
||||||
return controller.updateMessage(message, newMessage);
|
return controller.updateMessage(message, newMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Message?> toMessage(Event event) async {
|
|
||||||
final replyId = event.relationshipType == RelationshipTypes.reply
|
|
||||||
? event.relationshipEventId
|
|
||||||
: null;
|
|
||||||
final metadata = {
|
|
||||||
"eventType": event.type,
|
|
||||||
"displayName": event.senderFromMemoryOrFallback.displayName,
|
|
||||||
"txnId": event.transactionId,
|
|
||||||
};
|
|
||||||
return event.redacted
|
|
||||||
? Message.text(
|
|
||||||
metadata: metadata,
|
|
||||||
id: event.eventId,
|
|
||||||
authorId: event.senderId,
|
|
||||||
text: "~~This message has been redacted.~~",
|
|
||||||
deletedAt: event.redactedBecause?.originServerTs,
|
|
||||||
)
|
|
||||||
: switch (event.type) {
|
|
||||||
EventTypes.Message => switch (event.messageType) {
|
|
||||||
MessageTypes.Image => Message.image(
|
|
||||||
metadata: metadata,
|
|
||||||
id: event.eventId,
|
|
||||||
authorId: event.senderId,
|
|
||||||
source: (await event.getAttachmentUri()).toString(),
|
|
||||||
replyToMessageId: replyId,
|
|
||||||
deliveredAt: event.originServerTs,
|
|
||||||
),
|
|
||||||
MessageTypes.Audio => Message.audio(
|
|
||||||
metadata: metadata,
|
|
||||||
id: event.eventId,
|
|
||||||
authorId: event.senderId,
|
|
||||||
text: event.body,
|
|
||||||
replyToMessageId: replyId,
|
|
||||||
source: (await event.getAttachmentUri()).toString(),
|
|
||||||
deliveredAt: event.originServerTs,
|
|
||||||
duration: Duration(hours: 1),
|
|
||||||
),
|
|
||||||
MessageTypes.File => Message.file(
|
|
||||||
name: event.content["filename"].toString(),
|
|
||||||
metadata: metadata,
|
|
||||||
id: event.eventId,
|
|
||||||
authorId: event.senderId,
|
|
||||||
source: (await event.getAttachmentUri()).toString(),
|
|
||||||
replyToMessageId: replyId,
|
|
||||||
deliveredAt: event.originServerTs,
|
|
||||||
),
|
|
||||||
_ => Message.text(
|
|
||||||
metadata: metadata,
|
|
||||||
id: event.eventId,
|
|
||||||
authorId: event.senderId,
|
|
||||||
text: event.body,
|
|
||||||
replyToMessageId: replyId,
|
|
||||||
deliveredAt: event.originServerTs,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
EventTypes.RoomMember => Message.system(
|
|
||||||
metadata: metadata,
|
|
||||||
id: event.eventId,
|
|
||||||
authorId: event.senderId,
|
|
||||||
text:
|
|
||||||
"${event.senderFromMemoryOrFallback.calcDisplayname()} joined the room.",
|
|
||||||
),
|
|
||||||
EventTypes.Redaction => null,
|
|
||||||
_ => Message.unsupported(
|
|
||||||
metadata: metadata,
|
|
||||||
id: event.eventId,
|
|
||||||
authorId: event.senderId,
|
|
||||||
replyToMessageId: replyId,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> send(String message) async {
|
Future<void> send(String message) async {
|
||||||
await room.sendTextEvent(message);
|
await room.sendTextEvent(message);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import "package:flutter/widgets.dart";
|
import "package:flutter/widgets.dart";
|
||||||
|
import "package:flutter_chat_core/flutter_chat_core.dart";
|
||||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
import "package:matrix/matrix.dart";
|
import "package:matrix/matrix.dart";
|
||||||
import "package:nexus/models/full_room.dart";
|
import "package:nexus/models/full_room.dart";
|
||||||
|
|
@ -37,3 +38,84 @@ extension GetImage on Uri {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension ToMessage on Event {
|
||||||
|
Future<Message?> toMessage({bool mustBeText = false}) async {
|
||||||
|
final replyId = relationshipType == RelationshipTypes.reply
|
||||||
|
? relationshipEventId
|
||||||
|
: null;
|
||||||
|
final metadata = {
|
||||||
|
"eventType": type,
|
||||||
|
"displayName": senderFromMemoryOrFallback.displayName,
|
||||||
|
"txnId": transactionId,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (redacted) {
|
||||||
|
return Message.text(
|
||||||
|
metadata: metadata,
|
||||||
|
id: eventId,
|
||||||
|
authorId: senderId,
|
||||||
|
text: "~~This message has been redacted.~~",
|
||||||
|
deletedAt: redactedBecause?.originServerTs,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final asText = Message.text(
|
||||||
|
metadata: metadata,
|
||||||
|
id: eventId,
|
||||||
|
authorId: senderId,
|
||||||
|
text: body,
|
||||||
|
replyToMessageId: replyId,
|
||||||
|
deliveredAt: originServerTs,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (mustBeText) return asText;
|
||||||
|
|
||||||
|
return switch (type) {
|
||||||
|
EventTypes.Message => switch (messageType) {
|
||||||
|
MessageTypes.Image => Message.image(
|
||||||
|
metadata: metadata,
|
||||||
|
id: eventId,
|
||||||
|
authorId: senderId,
|
||||||
|
source: (await getAttachmentUri()).toString(),
|
||||||
|
replyToMessageId: replyId,
|
||||||
|
deliveredAt: originServerTs,
|
||||||
|
),
|
||||||
|
MessageTypes.Audio => Message.audio(
|
||||||
|
metadata: metadata,
|
||||||
|
id: eventId,
|
||||||
|
authorId: senderId,
|
||||||
|
text: body,
|
||||||
|
replyToMessageId: replyId,
|
||||||
|
source: (await getAttachmentUri()).toString(),
|
||||||
|
deliveredAt: originServerTs,
|
||||||
|
duration: Duration(hours: 1),
|
||||||
|
),
|
||||||
|
MessageTypes.File => Message.file(
|
||||||
|
name: content["filename"].toString(),
|
||||||
|
metadata: metadata,
|
||||||
|
id: eventId,
|
||||||
|
authorId: senderId,
|
||||||
|
source: (await getAttachmentUri()).toString(),
|
||||||
|
replyToMessageId: replyId,
|
||||||
|
deliveredAt: originServerTs,
|
||||||
|
),
|
||||||
|
_ => asText,
|
||||||
|
},
|
||||||
|
EventTypes.RoomMember => Message.system(
|
||||||
|
metadata: metadata,
|
||||||
|
id: eventId,
|
||||||
|
authorId: senderId,
|
||||||
|
text:
|
||||||
|
"${senderFromMemoryOrFallback.calcDisplayname()} joined the room.",
|
||||||
|
),
|
||||||
|
EventTypes.Redaction => null,
|
||||||
|
_ => Message.unsupported(
|
||||||
|
metadata: metadata,
|
||||||
|
id: eventId,
|
||||||
|
authorId: senderId,
|
||||||
|
replyToMessageId: replyId,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,9 @@ import "package:dynamic_system_colors/dynamic_system_colors.dart";
|
||||||
import "package:window_size/window_size.dart";
|
import "package:window_size/window_size.dart";
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
ScaledWidgetsFlutterBinding.ensureInitialized(scaleFactor: (_) => 1.3);
|
ScaledWidgetsFlutterBinding.ensureInitialized(
|
||||||
|
scaleFactor: (size) => size.width > 1080 ? 1.3 : 1,
|
||||||
|
);
|
||||||
|
|
||||||
await windowManager.ensureInitialized();
|
await windowManager.ensureInitialized();
|
||||||
await windowManager.waitUntilReadyToShow(
|
await windowManager.waitUntilReadyToShow(
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import "package:nexus/controllers/current_room_controller.dart";
|
||||||
import "package:nexus/controllers/room_chat_controller.dart";
|
import "package:nexus/controllers/room_chat_controller.dart";
|
||||||
import "package:nexus/helpers/extension_helper.dart";
|
import "package:nexus/helpers/extension_helper.dart";
|
||||||
import "package:nexus/helpers/launch_helper.dart";
|
import "package:nexus/helpers/launch_helper.dart";
|
||||||
|
import "package:nexus/widgets/top_widget.dart";
|
||||||
import "package:nexus/widgets/room_avatar.dart";
|
import "package:nexus/widgets/room_avatar.dart";
|
||||||
|
|
||||||
class RoomChat extends HookConsumerWidget {
|
class RoomChat extends HookConsumerWidget {
|
||||||
|
|
@ -86,16 +87,11 @@ class RoomChat extends HookConsumerWidget {
|
||||||
required bool isSentByMe,
|
required bool isSentByMe,
|
||||||
MessageGroupStatus? groupStatus,
|
MessageGroupStatus? groupStatus,
|
||||||
}) => kDebugMode
|
}) => kDebugMode
|
||||||
? FlyerChatTextMessage(
|
? Text(
|
||||||
message: TextMessage(
|
"${message.authorId} sent ${message.metadata?["eventType"]}",
|
||||||
id: message.id,
|
style: theme.textTheme.labelSmall?.copyWith(
|
||||||
authorId: message.authorId,
|
color: Colors.grey,
|
||||||
text:
|
|
||||||
"Unsupported message type: ${message.metadata?["eventType"]}",
|
|
||||||
),
|
),
|
||||||
receivedBackgroundColor: Colors.red,
|
|
||||||
sentBackgroundColor: Colors.red,
|
|
||||||
index: index,
|
|
||||||
)
|
)
|
||||||
: SizedBox.shrink(),
|
: SizedBox.shrink(),
|
||||||
textMessageBuilder:
|
textMessageBuilder:
|
||||||
|
|
@ -114,31 +110,9 @@ class RoomChat extends HookConsumerWidget {
|
||||||
SizedBox(height: 8),
|
SizedBox(height: 8),
|
||||||
|
|
||||||
FlyerChatTextMessage(
|
FlyerChatTextMessage(
|
||||||
topWidget: Padding(
|
topWidget: TopWidget(
|
||||||
padding: EdgeInsets.only(bottom: 12),
|
message,
|
||||||
child: InkWell(
|
headers: headers,
|
||||||
onTap: () => showAboutDialog(
|
|
||||||
context: context,
|
|
||||||
), // TODO: Show user profile
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
spacing: 8,
|
|
||||||
children: [
|
|
||||||
Avatar(
|
|
||||||
userId: message.authorId,
|
|
||||||
headers: headers,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
message.metadata?["displayName"] ??
|
|
||||||
message.authorId,
|
|
||||||
style: theme.textTheme.titleMedium
|
|
||||||
?.copyWith(
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
message: message.copyWith(
|
message: message.copyWith(
|
||||||
text: message.text.replaceAllMapped(
|
text: message.text.replaceAllMapped(
|
||||||
|
|
@ -188,6 +162,7 @@ class RoomChat extends HookConsumerWidget {
|
||||||
required bool isSentByMe,
|
required bool isSentByMe,
|
||||||
MessageGroupStatus? groupStatus,
|
MessageGroupStatus? groupStatus,
|
||||||
}) => FlyerChatImageMessage(
|
}) => FlyerChatImageMessage(
|
||||||
|
topWidget: TopWidget(message, headers: headers),
|
||||||
message: message,
|
message: message,
|
||||||
index: index,
|
index: index,
|
||||||
headers: headers,
|
headers: headers,
|
||||||
|
|
@ -204,6 +179,7 @@ class RoomChat extends HookConsumerWidget {
|
||||||
context: context,
|
context: context,
|
||||||
), // TODO: Download
|
), // TODO: Download
|
||||||
child: FlyerChatFileMessage(
|
child: FlyerChatFileMessage(
|
||||||
|
topWidget: TopWidget(message, headers: headers),
|
||||||
message: message,
|
message: message,
|
||||||
index: index,
|
index: index,
|
||||||
),
|
),
|
||||||
|
|
|
||||||
107
lib/widgets/top_widget.dart
Normal file
107
lib/widgets/top_widget.dart
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
import "dart:math";
|
||||||
|
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
import "package:flutter_chat_core/flutter_chat_core.dart";
|
||||||
|
import "package:flutter_chat_ui/flutter_chat_ui.dart";
|
||||||
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
|
import "package:nexus/controllers/message_controller.dart";
|
||||||
|
import "package:nexus/helpers/extension_helper.dart";
|
||||||
|
|
||||||
|
class TopWidget extends ConsumerWidget {
|
||||||
|
final Message message;
|
||||||
|
final Map<String, String> headers;
|
||||||
|
const TopWidget(this.message, {required this.headers, super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) => Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
if (message.replyToMessageId != null) ...[
|
||||||
|
ref
|
||||||
|
.watch(MessageController.provider(message.replyToMessageId!))
|
||||||
|
.betterWhen(
|
||||||
|
loading: SizedBox.shrink,
|
||||||
|
data: (replyMessage) {
|
||||||
|
if (replyMessage == null) return SizedBox.shrink();
|
||||||
|
final replyText = message is TextMessage
|
||||||
|
? replyMessage.text.substring(
|
||||||
|
0,
|
||||||
|
min(
|
||||||
|
max(
|
||||||
|
min(
|
||||||
|
(message as TextMessage).text.length - 20,
|
||||||
|
replyMessage.text.length,
|
||||||
|
),
|
||||||
|
40,
|
||||||
|
),
|
||||||
|
replyMessage.text.length,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: replyMessage.text;
|
||||||
|
return InkWell(
|
||||||
|
onTap: () => showAboutDialog(
|
||||||
|
context: context,
|
||||||
|
), // TODO: Scroll to message
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border(
|
||||||
|
left: BorderSide(
|
||||||
|
width: 4,
|
||||||
|
color: Theme.of(context).dividerColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.only(left: 8),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
spacing: 8,
|
||||||
|
children: [
|
||||||
|
Avatar(
|
||||||
|
userId: replyMessage.authorId,
|
||||||
|
headers: headers,
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
replyMessage.metadata?["displayName"] ??
|
||||||
|
replyMessage.authorId,
|
||||||
|
style: Theme.of(context).textTheme.labelMedium
|
||||||
|
?.copyWith(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
Flexible(
|
||||||
|
child: Text(
|
||||||
|
replyText,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: Theme.of(context).textTheme.labelMedium,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
SizedBox(height: 12),
|
||||||
|
],
|
||||||
|
InkWell(
|
||||||
|
onTap: () =>
|
||||||
|
showAboutDialog(context: context), // TODO: Show user profile
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
spacing: 8,
|
||||||
|
children: [
|
||||||
|
Avatar(userId: message.authorId, headers: headers),
|
||||||
|
Text(
|
||||||
|
message.metadata?["displayName"] ?? message.authorId,
|
||||||
|
style: Theme.of(
|
||||||
|
context,
|
||||||
|
).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 4),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue