diff --git a/lib/controllers/client_controller.dart b/lib/controllers/client_controller.dart index f26b8ca..4539632 100644 --- a/lib/controllers/client_controller.dart +++ b/lib/controllers/client_controller.dart @@ -1,4 +1,3 @@ -import "dart:developer"; import "dart:ffi"; import "dart:io"; import "dart:isolate"; @@ -123,9 +122,12 @@ class ClientController extends AsyncNotifier { } debugPrint("Finished handling $muksEventType..."); } catch (error, stackTrace) { - debugPrintStack(stackTrace: stackTrace, label: error.toString()); - debugger(); - showError(error, stackTrace); + if (kDebugMode) { + debugPrintStack(stackTrace: stackTrace, label: error.toString()); + rethrow; + } else { + showError(error, stackTrace); + } } }); diff --git a/lib/controllers/room_chat_controller.dart b/lib/controllers/room_chat_controller.dart index 7b970c7..5fada80 100644 --- a/lib/controllers/room_chat_controller.dart +++ b/lib/controllers/room_chat_controller.dart @@ -39,14 +39,13 @@ class RoomChatController extends AsyncNotifier> { state: state.fold( const IMap.empty(), (previousValue, stateEvent) => previousValue.add( - stateEvent.type.type, - (previousValue[stateEvent.type.type] ?? const IMap.empty()) - .addAll( - IMap({ - if (stateEvent.stateKey != null) - stateEvent.stateKey!: stateEvent.rowId, - }), - ), + stateEvent.type, + (previousValue[stateEvent.type] ?? const IMap.empty()).addAll( + IMap({ + if (stateEvent.stateKey != null) + stateEvent.stateKey!: stateEvent.rowId, + }), + ), ), ), ), diff --git a/lib/models/content/avatar.dart b/lib/models/content/avatar.dart index 650e2e6..66d4c47 100644 --- a/lib/models/content/avatar.dart +++ b/lib/models/content/avatar.dart @@ -7,7 +7,7 @@ part "avatar.g.dart"; @freezed abstract class AvatarContent extends Content with _$AvatarContent { AvatarContent._(); - const factory AvatarContent({ImageInfo? info, Uri? url}) = _AvatarContent; + factory AvatarContent({ImageInfo? info, Uri? url}) = _AvatarContent; factory AvatarContent.fromJson(Map json) => _$AvatarContentFromJson(json); diff --git a/lib/models/content/canonical_alias.dart b/lib/models/content/canonical_alias.dart index f675401..c6ac400 100644 --- a/lib/models/content/canonical_alias.dart +++ b/lib/models/content/canonical_alias.dart @@ -7,10 +7,8 @@ part "canonical_alias.g.dart"; abstract class CanonicalAliasContent extends Content with _$CanonicalAliasContent { CanonicalAliasContent._(); - const factory CanonicalAliasContent({ - String? alias, - @Default([]) altAliases, - }) = _CanonicalAliasContent; + factory CanonicalAliasContent({String? alias, @Default([]) altAliases}) = + _CanonicalAliasContent; factory CanonicalAliasContent.fromJson(Map json) => _$CanonicalAliasContentFromJson(json); diff --git a/lib/models/content/content.dart b/lib/models/content/content.dart index 760a90c..f388181 100644 --- a/lib/models/content/content.dart +++ b/lib/models/content/content.dart @@ -1,5 +1,4 @@ import "package:collection/collection.dart"; -import "package:freezed_annotation/freezed_annotation.dart"; import "package:nexus/models/content/avatar.dart"; import "package:nexus/models/content/canonical_alias.dart"; import "package:nexus/models/content/create.dart"; @@ -28,7 +27,6 @@ class Content { Content.fromJson)(eventJson); } -@JsonEnum(valueField: "type") enum EventType { encrypted("m.room.encrypted", Content.fromJson), redaction("m.room.redaction", RedactionContent.fromJson), diff --git a/lib/models/content/create.dart b/lib/models/content/create.dart index 78eef9f..08d1c22 100644 --- a/lib/models/content/create.dart +++ b/lib/models/content/create.dart @@ -7,7 +7,7 @@ part "create.g.dart"; @freezed abstract class CreateContent extends Content with _$CreateContent { CreateContent._(); - const factory CreateContent({ + factory CreateContent({ @JsonKey(name: "creator") String? creatorId, @JsonKey(name: "additional_creators") diff --git a/lib/models/content/encryption.dart b/lib/models/content/encryption.dart index 0fea339..3380632 100644 --- a/lib/models/content/encryption.dart +++ b/lib/models/content/encryption.dart @@ -6,7 +6,7 @@ part "encryption.g.dart"; @freezed abstract class EncryptionContent extends Content with _$EncryptionContent { EncryptionContent._(); - const factory EncryptionContent({ + factory EncryptionContent({ required String algorithm, @JsonKey(name: "rotation_period_ms") diff --git a/lib/models/content/join_rules.dart b/lib/models/content/join_rules.dart index a890d5c..1d14eee 100644 --- a/lib/models/content/join_rules.dart +++ b/lib/models/content/join_rules.dart @@ -8,7 +8,7 @@ part "join_rules.g.dart"; @freezed abstract class JoinRulesContent extends Content with _$JoinRulesContent { JoinRulesContent._(); - const factory JoinRulesContent({ + factory JoinRulesContent({ required JoinRule joinRule, @Default(IList.empty()) IList allow, }) = _JoinRulesContent; diff --git a/lib/models/content/membership.dart b/lib/models/content/membership.dart index ded5f4b..c963ed7 100644 --- a/lib/models/content/membership.dart +++ b/lib/models/content/membership.dart @@ -7,7 +7,7 @@ part "membership.g.dart"; @freezed abstract class MembershipContent extends Content with _$MembershipContent { MembershipContent._(); - const factory MembershipContent({ + factory MembershipContent({ @JsonKey(name: "displayname") required String displayName, @JsonKey(name: "membership") required MembershipStatus status, Uri? avatarUrl, diff --git a/lib/models/content/message.dart b/lib/models/content/message.dart index cca3af4..2408993 100644 --- a/lib/models/content/message.dart +++ b/lib/models/content/message.dart @@ -9,7 +9,7 @@ part "message.g.dart"; @Freezed(unionKey: "msgtype", fallbackUnion: "default") abstract class MessageContent extends Content with _$MessageContent { MessageContent._(); - const factory MessageContent({ + factory MessageContent({ required String msgtype, required String body, String? format, @@ -17,7 +17,7 @@ abstract class MessageContent extends Content with _$MessageContent { }) = TextMessageContent; @FreezedUnionValue("m.image") - const factory MessageContent.image({ + factory MessageContent.image({ required String body, String? format, String? formattedBody, @@ -28,7 +28,7 @@ abstract class MessageContent extends Content with _$MessageContent { }) = ImageMessageContent; @FreezedUnionValue("m.file") - const factory MessageContent.file({ + factory MessageContent.file({ required String body, String? format, String? formattedBody, @@ -39,7 +39,7 @@ abstract class MessageContent extends Content with _$MessageContent { }) = FileMessageContent; @FreezedUnionValue("m.audio") - const factory MessageContent.audio({ + factory MessageContent.audio({ required String body, String? format, String? formattedBody, @@ -50,7 +50,7 @@ abstract class MessageContent extends Content with _$MessageContent { }) = AudioMessageContent; @FreezedUnionValue("m.video") - const factory MessageContent.video({ + factory MessageContent.video({ required String body, String? format, String? formattedBody, @@ -58,13 +58,11 @@ abstract class MessageContent extends Content with _$MessageContent { String? filename, AudioInfo? info, String? url, - }) = AudioMessageContent; + }) = VideoMessageContent; @FreezedUnionValue("m.location") - const factory MessageContent.location({ - required String body, - required Uri geoUri, - }) = LocationMessageContent; + factory MessageContent.location({required String body, required Uri geoUri}) = + LocationMessageContent; factory MessageContent.fromJson(Map json) => _$MessageContentFromJson(json); diff --git a/lib/models/content/name.dart b/lib/models/content/name.dart index 35bac40..205f6bb 100644 --- a/lib/models/content/name.dart +++ b/lib/models/content/name.dart @@ -6,7 +6,7 @@ part "name.g.dart"; @freezed abstract class NameContent extends Content with _$NameContent { NameContent._(); - const factory NameContent({required String name}) = _NameContent; + factory NameContent({required String name}) = _NameContent; factory NameContent.fromJson(Map json) => _$NameContentFromJson(json); diff --git a/lib/models/content/pinned_events.dart b/lib/models/content/pinned_events.dart index a259ba4..d17a0de 100644 --- a/lib/models/content/pinned_events.dart +++ b/lib/models/content/pinned_events.dart @@ -7,9 +7,8 @@ part "pinned_events.g.dart"; @freezed abstract class PinnedEventsContent extends Content with _$PinnedEventsContent { PinnedEventsContent._(); - const factory PinnedEventsContent({ - @Default(IList.empty()) IList pinned, - }) = _PinnedEventsContent; + factory PinnedEventsContent({@Default(IList.empty()) IList pinned}) = + _PinnedEventsContent; factory PinnedEventsContent.fromJson(Map json) => _$PinnedEventsContentFromJson(json); diff --git a/lib/models/content/power_levels.dart b/lib/models/content/power_levels.dart index f2ab876..594dac0 100644 --- a/lib/models/content/power_levels.dart +++ b/lib/models/content/power_levels.dart @@ -9,7 +9,7 @@ part "power_levels.g.dart"; abstract class PowerLevelsContent extends Content with _$PowerLevelsContent { PowerLevelsContent._(); - const factory PowerLevelsContent({ + factory PowerLevelsContent({ @Default(IMap.empty()) IMap events, @Default(IMap.empty()) IMap users, Notifications? notifications, diff --git a/lib/models/content/reaction.dart b/lib/models/content/reaction.dart index 7cdec08..0361df8 100644 --- a/lib/models/content/reaction.dart +++ b/lib/models/content/reaction.dart @@ -8,7 +8,7 @@ String? keyFromJson(Map json) => json["m.relates_to"]?["key"]; @freezed abstract class ReactionContent extends Content with _$ReactionContent { ReactionContent._(); - const factory ReactionContent({@JsonKey(fromJson: keyFromJson) String? key}) = + factory ReactionContent({@JsonKey(fromJson: keyFromJson) String? key}) = _ReactionContent; factory ReactionContent.fromJson(Map json) => diff --git a/lib/models/content/redaction.dart b/lib/models/content/redaction.dart index 289d4bf..e9c1a90 100644 --- a/lib/models/content/redaction.dart +++ b/lib/models/content/redaction.dart @@ -6,7 +6,7 @@ part "redaction.g.dart"; @freezed abstract class RedactionContent extends Content with _$RedactionContent { RedactionContent._(); - const factory RedactionContent({String? reason, String? redacts}) = + factory RedactionContent({String? reason, String? redacts}) = _RedactionContent; factory RedactionContent.fromJson(Map json) => diff --git a/lib/models/content/server_acl.dart b/lib/models/content/server_acl.dart index 6ee5fea..1e50988 100644 --- a/lib/models/content/server_acl.dart +++ b/lib/models/content/server_acl.dart @@ -7,7 +7,7 @@ part "server_acl.g.dart"; @freezed abstract class ServerACLContent extends Content with _$ServerACLContent { ServerACLContent._(); - const factory ServerACLContent({ + factory ServerACLContent({ @Default(IList.empty()) IList allow, @Default(IList.empty()) IList deny, @Default(true) allowIpLiterals, diff --git a/lib/models/content/topic.dart b/lib/models/content/topic.dart index ee561c7..8fa5229 100644 --- a/lib/models/content/topic.dart +++ b/lib/models/content/topic.dart @@ -7,7 +7,7 @@ part "topic.g.dart"; @freezed abstract class TopicContent extends Content with _$TopicContent { TopicContent._(); - const factory TopicContent({ + factory TopicContent({ required String topic, @JsonKey(name: "m.topic") TopicContentBlock? content, }) = _TopicContent; @@ -18,7 +18,7 @@ abstract class TopicContent extends Content with _$TopicContent { @freezed abstract class TopicContentBlock with _$TopicContentBlock { - const factory TopicContentBlock({ + factory TopicContentBlock({ @Default(IList.empty()) @JsonKey(name: "m.text") IList representations, @@ -30,7 +30,7 @@ abstract class TopicContentBlock with _$TopicContentBlock { @freezed abstract class TextualRepresentation with _$TextualRepresentation { - const factory TextualRepresentation({ + factory TextualRepresentation({ required String body, @Default("text/plain") String mimetype, }) = _TextualRepresentation; diff --git a/lib/models/event.dart b/lib/models/event.dart index 798b505..21fbedb 100644 --- a/lib/models/event.dart +++ b/lib/models/event.dart @@ -6,8 +6,8 @@ import "package:nexus/models/profile.dart"; part "event.freezed.dart"; part "event.g.dart"; -Profile? pmpFromJson(Map json) { - final pmp = json["content"]?["com.beeper.per_message_profile"]; +Profile? pmpFromJson(Map? json) { + final pmp = json?["content"]?["com.beeper.per_message_profile"]; return pmp == null ? null : Profile.fromJson(pmp); } @@ -19,7 +19,7 @@ abstract class Event with _$Event { required String roomId, required String eventId, required String sender, - required EventType type, + required String type, String? stateKey, @EpochDateTimeConverter() required DateTime timestamp, IMap? decrypted, @@ -36,7 +36,7 @@ abstract class Event with _$Event { @JsonKey(name: "last_edit_rowid") int? lastEditRowId, @UnreadTypeConverter() UnreadType? unreadType, @JsonKey(fromJson: pmpFromJson) Profile? pmp, - @JsonKey(fromJson: Content.fromJson) required Content content, + @JsonKey(fromJson: Content.fromEventJson) required Content content, }) = _Event; factory Event.fromJson(Map json) => _$EventFromJson(json); diff --git a/lib/widgets/chat_page/event_text.dart b/lib/widgets/chat_page/event_text.dart index 17761ac..ca24584 100644 --- a/lib/widgets/chat_page/event_text.dart +++ b/lib/widgets/chat_page/event_text.dart @@ -1,3 +1,4 @@ +import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:flutter/material.dart"; import "package:nexus/models/event.dart"; @@ -5,10 +6,14 @@ class EventText extends StatelessWidget { final Event event; final bool textOnly; final int? maxLines; + final VoidCallback? onTapReply; + final IList Function(Event event)? getEventOptions; const EventText( this.event, { + this.onTapReply, this.textOnly = false, this.maxLines, + this.getEventOptions, super.key, }); diff --git a/lib/widgets/chat_page/reply_widget.dart b/lib/widgets/chat_page/reply_widget.dart deleted file mode 100644 index 24fcdd7..0000000 --- a/lib/widgets/chat_page/reply_widget.dart +++ /dev/null @@ -1,100 +0,0 @@ -import "package:flutter/material.dart"; -import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:nexus/controllers/event_controller.dart"; -import "package:nexus/controllers/message_controller.dart"; -import "package:nexus/controllers/selected_room_controller.dart"; -import "package:nexus/helpers/extensions/better_when.dart"; -import "package:nexus/models/configs/message_config.dart"; -import "package:nexus/models/requests/get_event_request.dart"; -import "package:nexus/widgets/chat_page/html/quoted.dart"; -import "package:nexus/widgets/chat_page/lazy_loading/message_avatar.dart"; -import "package:nexus/widgets/chat_page/lazy_loading/message_displayname.dart"; - -typedef OnTapReply = void Function(Message message)?; - -class ReplyWidget extends ConsumerWidget { - final Message message; - final bool alwaysShow; - final MessageGroupStatus? groupStatus; - final OnTapReply onTapReply; - const ReplyWidget( - this.message, { - required this.groupStatus, - this.onTapReply, - this.alwaysShow = false, - super.key, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final room = ref.watch(SelectedRoomController.provider); - return message.replyToMessageId == null || room == null - ? SizedBox.shrink() - : Padding( - padding: EdgeInsets.only(bottom: 12), - child: Quoted( - ref - .watch( - EventController.provider( - GetEventRequest( - room: room, - eventId: message.replyToMessageId!, - ), - ), - ) - .betterWhen( - loading: () => Text("Fetching event..."), - data: (event) => event == null - ? SizedBox.shrink() - : ref - .watch( - MessageController.provider( - MessageConfig(room: room, event: event), - ), - ) - .betterWhen( - loading: () => Text("Parsing message..."), - data: (replyMessage) { - if (replyMessage == null) { - return SizedBox.shrink(); - } - - return InkWell( - onTap: () => onTapReply?.call(replyMessage), - child: Row( - mainAxisSize: MainAxisSize.min, - spacing: 8, - children: [ - MessageAvatar(replyMessage), - Flexible( - child: MessageDisplayname( - replyMessage, - clickable: false, - style: Theme.of(context) - .textTheme - .labelMedium - ?.copyWith( - fontWeight: FontWeight.bold, - ), - ), - ), - Flexible( - child: Text( - replyMessage.metadata!["body"], - overflow: TextOverflow.ellipsis, - style: Theme.of( - context, - ).textTheme.labelMedium, - maxLines: 1, - ), - ), - ], - ), - ); - }, - ), - ), - ), - ); - } -} diff --git a/lib/widgets/chat_page/room_chat.dart b/lib/widgets/chat_page/room_chat.dart index e47fc46..7e875f6 100644 --- a/lib/widgets/chat_page/room_chat.dart +++ b/lib/widgets/chat_page/room_chat.dart @@ -11,21 +11,21 @@ import "package:nexus/controllers/selected_room_controller.dart"; import "package:nexus/controllers/room_chat_controller.dart"; import "package:nexus/controllers/via_controller.dart"; import "package:nexus/helpers/extensions/better_when.dart"; -import "package:nexus/helpers/extensions/show_context_menu.dart"; import "package:nexus/models/configs/power_level_config.dart"; import "package:nexus/models/content/content.dart"; +import "package:nexus/models/content/message.dart"; +import "package:nexus/models/event.dart"; import "package:nexus/models/relation_type.dart"; import "package:nexus/models/requests/report_request.dart"; import "package:nexus/widgets/chat_page/composer/chat_box.dart"; import "package:nexus/widgets/chat_page/emoji_picker_button.dart"; -import "package:nexus/widgets/chat_page/expandable_image_message.dart"; +import "package:nexus/widgets/chat_page/event_text.dart"; import "package:nexus/widgets/chat_page/member_list.dart"; -import "package:nexus/widgets/chat_page/wrappers/message_wrapper.dart"; import "package:nexus/widgets/chat_page/room_appbar.dart"; -import "package:nexus/widgets/chat_page/wrappers/text_message_wrapper.dart"; -import "package:nexus/widgets/chat_page/reply_widget.dart"; +import "package:nexus/widgets/chat_page/wrappers/message_wrapper.dart"; import "package:nexus/widgets/form_text_input.dart"; import "package:nexus/main.dart"; +import "package:super_sliver_list/super_sliver_list.dart"; class RoomChat extends HookConsumerWidget { final bool isDesktop; @@ -38,17 +38,21 @@ class RoomChat extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final client = ref.watch(ClientController.provider.notifier); - final relatedMessage = useState(null); - final memberListOpened = useState(showMembersByDefault); + final relatedEvent = useState(null); final relationType = useState(RelationType.reply); + + final memberListOpened = useState(showMembersByDefault); + + final listController = useRef(ListController()); + final scrollController = useScrollController(); + // TODO: Do things on scroll to top or bottom + final userId = ref.watch(ClientStateController.provider)?.userId; final roomId = ref.watch( SelectedRoomController.provider.select((value) => value?.metadata?.id), ); final theme = Theme.of(context); - final danger = theme.colorScheme.error; if (roomId == null || userId == null) { return Scaffold( @@ -73,7 +77,7 @@ class RoomChat extends HookConsumerWidget { onKeyEvent: (_, event) { if (event is KeyDownEvent && event.logicalKey == LogicalKeyboardKey.escape) { - relatedMessage.value = null; + relatedEvent.value = null; return KeyEventResult.handled; } @@ -81,8 +85,9 @@ class RoomChat extends HookConsumerWidget { }, ); - List getMessageOptions(Message message) { - final isSentByMe = message.sender == userId; + IList getEventOptions(Event event) { + final danger = theme.colorScheme.error; + final isSentByMe = event.sender == userId; return [ if (ref.watch( PowerLevelController.provider( @@ -113,7 +118,7 @@ class RoomChat extends HookConsumerWidget { onPressed: () async { Navigator.of(context).pop(); await notifier - .sendReaction(emoji, message) + .sendReaction(emoji, event) .onError(showError); }, icon: Text(emoji), @@ -123,7 +128,7 @@ class RoomChat extends HookConsumerWidget { context: context, onPressed: Navigator.of(context).pop, onSelection: (emoji) => - notifier.sendReaction(emoji, message).onError(showError), + notifier.sendReaction(emoji, event).onError(showError), ), ], ), @@ -135,16 +140,16 @@ class RoomChat extends HookConsumerWidget { )) PopupMenuItem( onTap: () { - relatedMessage.value = message; + relatedEvent.value = event; relationType.value = RelationType.reply; composerNode.requestFocus(); }, child: ListTile(leading: Icon(Icons.reply), title: Text("Reply")), ), - if (message is TextMessage && isSentByMe) + if (event.content is MessageContent && isSentByMe) PopupMenuItem( onTap: () { - relatedMessage.value = message; + relatedEvent.value = event; relationType.value = RelationType.edit; composerNode.requestFocus(); }, @@ -160,7 +165,7 @@ class RoomChat extends HookConsumerWidget { await Clipboard.setData( ClipboardData( text: - "matrix:roomid/${room.metadata?.id.substring(1)}/e/${message.id}$vias)", + "matrix:roomid/${room.metadata?.id.substring(1)}/e/${event.eventId}$vias)", ), ); }, @@ -168,7 +173,7 @@ class RoomChat extends HookConsumerWidget { ), if (ref.watch( PowerLevelController.provider( - PowerLevelConfig.redaction(targetUser: message.authorId), + PowerLevelConfig.redaction(targetUser: event.sender), ), )) PopupMenuItem( @@ -205,7 +210,7 @@ class RoomChat extends HookConsumerWidget { Navigator.of(context).pop(); await notifier .deleteMessage( - message, + event, reason: deleteReasonController.text, ) .onError(showError); @@ -254,15 +259,17 @@ class RoomChat extends HookConsumerWidget { ), TextButton( onPressed: () { - client.reportEvent( - ReportRequest( - roomId: roomId, - eventId: message.id, - reason: reasonController.text.isEmpty - ? null - : reasonController.text, - ), - ); + ref + .watch(ClientController.provider.notifier) + .reportEvent( + ReportRequest( + roomId: roomId, + eventId: event.eventId, + reason: reasonController.text.isEmpty + ? null + : reasonController.text, + ), + ); Navigator.of(context).pop(); }, child: Text("Report"), @@ -277,16 +284,9 @@ class RoomChat extends HookConsumerWidget { title: Text("Report", style: TextStyle(color: danger)), ), ), - ]; + ].toIList(); } - final chatTheme = ChatTheme.fromThemeData(theme).copyWith( - colors: ChatColors.fromThemeData(theme).copyWith( - primary: theme.colorScheme.primaryContainer, - onPrimary: theme.colorScheme.onPrimaryContainer, - ), - ); - return Scaffold( appBar: RoomAppbar( isDesktop: isDesktop, @@ -299,178 +299,55 @@ class RoomChat extends HookConsumerWidget { body: Row( children: [ Expanded( - child: Column( + child: Stack( children: [ - Expanded( + Positioned.fill( child: ref .watch(controllerProvider) .betterWhen( - data: (controller) => Chat( - currentUserId: userId, - theme: chatTheme, - onMessageSecondaryTap: - ( - context, - message, { - required index, - TapUpDetails? details, - }) => details?.globalPosition == null - ? null - : context.showContextMenu( - globalPosition: details!.globalPosition, - children: getMessageOptions(message), - ), - onMessageLongPress: - ( - context, - message, { - required details, - required index, - }) => context.showContextMenu( - globalPosition: details.globalPosition, - children: getMessageOptions(message), - ), - builders: Builders( - loadMoreBuilder: (_) => SizedBox.shrink(), - - chatAnimatedListBuilder: (_, itemBuilder) => - ChatAnimatedList( - itemBuilder: itemBuilder, - onEndReached: - ref.watch( - SelectedRoomController.provider.select( - (room) => room?.hasMore == true, - ), - ) - ? notifier.loadOlder - : null, - onStartReached: () async { - final room = ref.watch( - SelectedRoomController.provider, - ); - return room == null - ? null - : await client.markRead(room); - }, - bottomPadding: 72, - ), - - composerBuilder: (_) => ChatBox( - node: composerNode, - onSend: - ( - text, { - required shouldMention, - required tags, - }) => notifier - .send( - text, - tags: tags, - relationType: relationType.value, - shouldMention: shouldMention, - relation: relatedMessage.value, - ) - .onError(showError), - relationType: relationType.value, - relatedEvent: relatedMessage.value, - onDismiss: () => relatedMessage.value = null, + data: (events) => SuperListView.builder( + controller: scrollController, + listController: listController.value, + itemCount: events.length, + itemBuilder: (_, index) => MessageWrapper( + events[index], + EventText( + events[index], + onTapReply: () => + listController.value.animateToItem( + index: index, + scrollController: scrollController, + alignment: 0.5, + duration: (_) => + Duration(milliseconds: 250), + curve: (_) => Curves.easeInOut, + ), + getEventOptions: getEventOptions, ), - - textMessageBuilder: - ( - context, - message, - index, { - required bool isSentByMe, - MessageGroupStatus? groupStatus, - }) => TextMessageWrapper( - message, - content: message.text, - groupStatus: groupStatus, - onTapReply: notifier.scrollToEvent, - updateMessage: controller.updateMessage, - isSentByMe: isSentByMe, - ), - - imageMessageBuilder: - ( - context, - message, - index, { - required bool isSentByMe, - MessageGroupStatus? groupStatus, - }) => TextMessageWrapper( - message, - content: message.text, - groupStatus: groupStatus, - onTapReply: notifier.scrollToEvent, - updateMessage: controller.updateMessage, - isSentByMe: isSentByMe, - extra: ExpandableImageMessage(message), - ), - - fileMessageBuilder: - ( - _, - message, - index, { - required bool isSentByMe, - MessageGroupStatus? groupStatus, - }) => MessageWrapper( - message, - InkWell( - onTap: () => showDialog( - context: context, - builder: (_) => Dialog( - child: Text( - "TODO: Download Attachments", - ), - ), - ), - child: FlyerChatFileMessage( - topWidget: ReplyWidget( - message, - onTapReply: notifier.scrollToEvent, - groupStatus: groupStatus, - ), - message: message, - index: index, - ), - ), - groupStatus, - ), - - systemMessageBuilder: - ( - _, - message, - index, { - required bool isSentByMe, - MessageGroupStatus? groupStatus, - }) => FlyerChatSystemMessage( - message: message, - index: index, - ), - - unsupportedMessageBuilder: - ( - _, - message, - index, { - required bool isSentByMe, - MessageGroupStatus? groupStatus, - }) => Text( - "${message.sender} sent ${message.metadata?["eventType"]}", - style: theme.textTheme.labelSmall?.copyWith( - color: Colors.grey, - ), - ), + // TODO: Reimplement grouping + isGrouped: false, + // TODO: Reimplement flashing + isFlashing: false, ), - resolveUser: (_) async => null, - chatController: controller, ), ), ), + ChatBox( + node: composerNode, + onSend: (text, {required shouldMention, required tags}) => + notifier + .send( + text, + tags: tags, + relationType: relationType.value, + shouldMention: shouldMention, + relation: relatedEvent.value, + ) + .onError(showError), + relationType: relationType.value, + relatedEvent: relatedEvent.value, + onDismiss: () => relatedEvent.value = null, + ), ], ), ), diff --git a/lib/widgets/chat_page/wrappers/message_wrapper.dart b/lib/widgets/chat_page/wrappers/message_wrapper.dart index 472f4a2..620d859 100644 --- a/lib/widgets/chat_page/wrappers/message_wrapper.dart +++ b/lib/widgets/chat_page/wrappers/message_wrapper.dart @@ -1,27 +1,32 @@ import "package:flutter/material.dart"; +import "package:nexus/models/event.dart"; import "package:nexus/widgets/chat_page/lazy_loading/message_avatar.dart"; import "package:nexus/widgets/chat_page/lazy_loading/message_displayname.dart"; import "package:nexus/widgets/chat_page/wrappers/reaction_row.dart"; import "package:timeago/timeago.dart"; class MessageWrapper extends StatelessWidget { - final Message message; + final Event event; final Widget child; - final MessageGroupStatus? groupStatus; - const MessageWrapper(this.message, this.child, this.groupStatus, {super.key}); + final bool isGrouped; + final bool isFlashing; + const MessageWrapper( + this.event, + this.child, { + this.isGrouped = false, + this.isFlashing = false, + super.key, + }); @override Widget build(BuildContext context) { final theme = Theme.of(context); - final error = message.metadata?["error"]; return ClipRRect( borderRadius: BorderRadius.all(Radius.circular(12)), child: AnimatedContainer( - padding: message.metadata?["flashing"] == true - ? EdgeInsets.all(8) - : EdgeInsets.all(0), - color: message.metadata?["flashing"] == true + padding: isFlashing ? EdgeInsets.all(8) : EdgeInsets.all(0), + color: isFlashing ? Theme.of(context).colorScheme.onSurface.withAlpha(50) : Colors.transparent, duration: Duration(milliseconds: 250), @@ -29,48 +34,44 @@ class MessageWrapper extends StatelessWidget { spacing: 8, crossAxisAlignment: CrossAxisAlignment.start, children: [ - groupStatus?.isFirst != false - ? MessageAvatar(message, height: 40) - : SizedBox(width: 40), + isGrouped ? SizedBox(width: 40) : MessageAvatar(event, height: 40), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, spacing: 4, children: [ - if (groupStatus?.isFirst != false) + if (!isGrouped) Row( spacing: 4, children: [ Flexible( child: MessageDisplayname( - message, + event, style: theme.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, ), ), ), - if (message.deliveredAt != null && - groupStatus?.isFirst != false) - Tooltip( - message: message.deliveredAt!.toString(), - child: Text( - format(message.deliveredAt!), - style: theme.textTheme.labelSmall?.copyWith( - color: Colors.grey, - ), + Tooltip( + message: event.timestamp.toString(), + child: Text( + format(event.timestamp), + style: theme.textTheme.labelSmall?.copyWith( + color: Colors.grey, ), ), + ), ], ), child, - if (error != null && error != "not sent") + if (event.sendError != null && event.sendError != "not sent") Text( - error, + event.sendError!, style: theme.textTheme.labelSmall?.copyWith( color: theme.colorScheme.error, ), ), - ReactionRow(message), + ReactionRow(event), ], ), ), diff --git a/lib/widgets/chat_page/wrappers/reaction_row.dart b/lib/widgets/chat_page/wrappers/reaction_row.dart index da7c825..5786134 100644 --- a/lib/widgets/chat_page/wrappers/reaction_row.dart +++ b/lib/widgets/chat_page/wrappers/reaction_row.dart @@ -1,5 +1,4 @@ import "package:cross_cache/cross_cache.dart"; -import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:flutter/material.dart"; import "package:flutter_hooks/flutter_hooks.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; @@ -10,106 +9,110 @@ import "package:nexus/controllers/selected_room_controller.dart"; import "package:nexus/helpers/extensions/get_headers.dart"; import "package:nexus/helpers/extensions/mxc_to_https.dart"; import "package:nexus/main.dart"; +import "package:nexus/models/event.dart"; class ReactionRow extends ConsumerWidget { - final Message message; - const ReactionRow(this.message, {super.key}); + final Event event; + const ReactionRow(this.event, {super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final clientState = ref.watch(ClientStateController.provider); - return Wrap( - spacing: 4, - runSpacing: 4, - children: clientState?.homeserverUrl == null || message.reactions == null - ? [] - : message.reactions! - .mapTo( - (reaction, reactors) => HookBuilder( - builder: (context) { - final enabled = useState(true); - final selected = reactors.contains(clientState!.userId); - return Tooltip( - message: reactors.join(", "), - child: ChoiceChip( - showCheckmark: false, - selected: selected, - label: Row( - mainAxisSize: MainAxisSize.min, - spacing: 8, - children: [ - Flexible( - child: reaction.startsWith("mxc://") - ? Image( - height: 20, - image: CachedNetworkImage( - headers: ref.headers, - Uri.parse(reaction) - .mxcToHttps( - clientState.homeserverUrl!, - ) - .toString(), - ref.watch( - CrossCacheController.provider, - ), - ), - ) - : Text( - reaction, - overflow: TextOverflow.ellipsis, - ), - ), - Text( - reactors.length.toString(), - overflow: TextOverflow.ellipsis, - ), - ], - ), - onSelected: enabled.value - ? (value) async { - enabled.value = false; - try { - final roomId = ref.watch( - SelectedRoomController.provider.select( - (value) => value?.metadata?.id, - ), - ); - if (roomId == null || - clientState.userId == null) { - return; - } + return SizedBox.shrink(); - final controller = ref.watch( - RoomChatController.provider( - roomId, - ).notifier, - ); + // TODO: IMPL + // return Wrap( + // spacing: 4, + // runSpacing: 4, + // children: clientState?.homeserverUrl == null + // ? [] + // : event.reactions + // .mapTo( + // (reaction, reactors) => HookBuilder( + // builder: (context) { + // final enabled = useState(true); + // final selected = reactors.contains(clientState!.userId); + // return Tooltip( + // message: reactors.join(", "), + // child: ChoiceChip( + // showCheckmark: false, + // selected: selected, + // label: Row( + // mainAxisSize: MainAxisSize.min, + // spacing: 8, + // children: [ + // Flexible( + // child: reaction.startsWith("mxc://") + // ? Image( + // height: 20, + // image: CachedNetworkImage( + // headers: ref.headers, + // Uri.parse(reaction) + // .mxcToHttps( + // clientState.homeserverUrl!, + // ) + // .toString(), + // ref.watch( + // CrossCacheController.provider, + // ), + // ), + // ) + // : Text( + // reaction, + // overflow: TextOverflow.ellipsis, + // ), + // ), + // Text( + // reactors.length.toString(), + // overflow: TextOverflow.ellipsis, + // ), + // ], + // ), + // onSelected: enabled.value + // ? (value) async { + // enabled.value = false; + // try { + // final roomId = ref.watch( + // SelectedRoomController.provider.select( + // (value) => value?.metadata?.id, + // ), + // ); + // if (roomId == null || + // clientState.userId == null) { + // return; + // } - if (selected) { - await controller - .removeReaction( - reaction, - message, - clientState.userId!, - ) - .onError(showError); - } else { - await controller - .sendReaction(reaction, message) - .onError(showError); - } - } finally { - enabled.value = true; - } - } - : null, - ), - ); - }, - ), - ) - .toList(), - ); + // final controller = ref.watch( + // RoomChatController.provider( + // roomId, + // ).notifier, + // ); + + // if (selected) { + // await controller + // .removeReaction( + // reaction, + // event, + // clientState.userId!, + // ) + // .onError(showError); + // } else { + // await controller + // .sendReaction(reaction, event) + // .onError(showError); + // } + // } finally { + // enabled.value = true; + // } + // } + // : null, + // ), + // ); + // }, + // ), + // ) + // .toList(), + // ); } } diff --git a/pubspec.lock b/pubspec.lock index 10f28bd..9ccfe8d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1196,6 +1196,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.1" + super_sliver_list: + dependency: "direct main" + description: + name: super_sliver_list + sha256: b1e1e64d08ce40e459b9bb5d9f8e361617c26b8c9f3bb967760b0f436b6e3f56 + url: "https://pub.dev" + source: hosted + version: "0.4.1" synchronized: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 4e8d609..d511fe6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -52,6 +52,7 @@ dependencies: git: url: https://github.com/Henry-Hiles/emoji_text_field flutter_blurhash: ^0.9.1 + super_sliver_list: ^0.4.1 dev_dependencies: build_runner: 2.15.0