diff --git a/lib/pages/chat/chat_event_list.dart b/lib/pages/chat/chat_event_list.dart index 57ac404ab..21482422c 100644 --- a/lib/pages/chat/chat_event_list.dart +++ b/lib/pages/chat/chat_event_list.dart @@ -152,6 +152,7 @@ class ChatEventList extends StatelessWidget { nextEvent: i + 1 < events.length ? events[i + 1] : null, previousEvent: i > 0 ? events[i - 1] : null, wallpaperMode: hasWallpaper, + scrollController: controller.scrollController, ), ); }, diff --git a/lib/pages/chat/events/message.dart b/lib/pages/chat/events/message.dart index 32ab44ab0..87d23f33f 100644 --- a/lib/pages/chat/events/message.dart +++ b/lib/pages/chat/events/message.dart @@ -1,3 +1,5 @@ +import 'dart:ui' as ui; + import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -36,6 +38,7 @@ class Message extends StatelessWidget { final bool animateIn; final void Function()? resetAnimateIn; final bool wallpaperMode; + final ScrollController scrollController; const Message( this.event, { @@ -54,6 +57,7 @@ class Message extends StatelessWidget { this.animateIn = false, this.resetAnimateIn, this.wallpaperMode = false, + required this.scrollController, super.key, }); @@ -323,118 +327,132 @@ class Message extends StatelessWidget { borderRadius: borderRadius, ), clipBehavior: Clip.antiAlias, - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular( - AppConfig.borderRadius, + child: BubbleBackground( + colors: [ + theme.brightness == Brightness.light + ? theme.colorScheme.tertiary + : theme.colorScheme + .tertiaryContainer, + color, + ], + ignore: noBubble || !ownMessage, + scrollController: scrollController, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular( + AppConfig.borderRadius, + ), ), - ), - padding: noBubble || noPadding - ? EdgeInsets.zero - : const EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, - ), - constraints: const BoxConstraints( - maxWidth: - FluffyThemes.columnWidth * 1.5, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - if (event.relationshipType == - RelationshipTypes.reply) - FutureBuilder( - future: event - .getReplyEvent(timeline), - builder: ( - BuildContext context, - snapshot, - ) { - final replyEvent = snapshot - .hasData - ? snapshot.data! - : Event( - eventId: event - .relationshipEventId!, - content: { - 'msgtype': 'm.text', - 'body': '...', - }, - senderId: - event.senderId, - type: - 'm.room.message', - room: event.room, - status: - EventStatus.sent, - originServerTs: - DateTime.now(), - ); - return Padding( - padding: - const EdgeInsets.only( - bottom: 4.0, - ), - child: InkWell( - borderRadius: ReplyContent - .borderRadius, - onTap: () => - scrollToEventId( - replyEvent.eventId, + padding: noBubble || noPadding + ? EdgeInsets.zero + : const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + constraints: const BoxConstraints( + maxWidth: + FluffyThemes.columnWidth * 1.5, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + if (event.relationshipType == + RelationshipTypes.reply) + FutureBuilder( + future: event + .getReplyEvent(timeline), + builder: ( + BuildContext context, + snapshot, + ) { + final replyEvent = snapshot + .hasData + ? snapshot.data! + : Event( + eventId: event + .relationshipEventId!, + content: { + 'msgtype': + 'm.text', + 'body': '...', + }, + senderId: + event.senderId, + type: + 'm.room.message', + room: event.room, + status: EventStatus + .sent, + originServerTs: + DateTime.now(), + ); + return Padding( + padding: + const EdgeInsets.only( + bottom: 4.0, ), - child: AbsorbPointer( - child: ReplyContent( - replyEvent, - ownMessage: - ownMessage, - timeline: timeline, + child: InkWell( + borderRadius: + ReplyContent + .borderRadius, + onTap: () => + scrollToEventId( + replyEvent.eventId, + ), + child: AbsorbPointer( + child: ReplyContent( + replyEvent, + ownMessage: + ownMessage, + timeline: timeline, + ), ), ), - ), - ); - }, - ), - MessageContent( - displayEvent, - textColor: textColor, - linkColor: linkColor, - onInfoTab: onInfoTab, - borderRadius: borderRadius, - timeline: timeline, - ), - if (event.hasAggregatedEvents( - timeline, - RelationshipTypes.edit, - )) - Padding( - padding: const EdgeInsets.only( - top: 4.0, + ); + }, ), - child: Row( - mainAxisSize: - MainAxisSize.min, - children: [ - Icon( - Icons.edit_outlined, - color: textColor - .withAlpha(164), - size: 14, - ), - Text( - ' - ${displayEvent.originServerTs.localizedTimeShort(context)}', - style: TextStyle( + MessageContent( + displayEvent, + textColor: textColor, + linkColor: linkColor, + onInfoTab: onInfoTab, + borderRadius: borderRadius, + timeline: timeline, + ), + if (event.hasAggregatedEvents( + timeline, + RelationshipTypes.edit, + )) + Padding( + padding: + const EdgeInsets.only( + top: 4.0, + ), + child: Row( + mainAxisSize: + MainAxisSize.min, + children: [ + Icon( + Icons.edit_outlined, color: textColor .withAlpha(164), - fontSize: 12, + size: 14, ), - ), - ], + Text( + ' - ${displayEvent.originServerTs.localizedTimeShort(context)}', + style: TextStyle( + color: textColor + .withAlpha(164), + fontSize: 12, + ), + ), + ], + ), ), - ), - ], + ], + ), ), ), ), @@ -574,3 +592,72 @@ class Message extends StatelessWidget { ); } } + +class BubbleBackground extends StatelessWidget { + const BubbleBackground({ + super.key, + required this.scrollController, + required this.colors, + required this.ignore, + required this.child, + }); + + final ScrollController scrollController; + final List colors; + final bool ignore; + final Widget child; + + @override + Widget build(BuildContext context) { + if (ignore) return child; + return CustomPaint( + painter: BubblePainter( + repaint: scrollController, + colors: colors, + context: context, + ), + child: child, + ); + } +} + +class BubblePainter extends CustomPainter { + BubblePainter({ + required this.context, + required this.colors, + required super.repaint, + }); + + final BuildContext context; + final List colors; + ScrollableState? _scrollable; + + @override + void paint(Canvas canvas, Size size) { + final scrollable = _scrollable ??= Scrollable.of(context); + final scrollableBox = scrollable.context.findRenderObject() as RenderBox; + final scrollableRect = Offset.zero & scrollableBox.size; + final bubbleBox = context.findRenderObject() as RenderBox; + + final origin = + bubbleBox.localToGlobal(Offset.zero, ancestor: scrollableBox); + final paint = Paint() + ..shader = ui.Gradient.linear( + scrollableRect.topCenter, + scrollableRect.bottomCenter, + colors, + [0.0, 1.0], + TileMode.clamp, + Matrix4.translationValues(-origin.dx, -origin.dy, 0.0).storage, + ); + canvas.drawRect(Offset.zero & size, paint); + } + + @override + bool shouldRepaint(BubblePainter oldDelegate) { + final scrollable = Scrollable.of(context); + final oldScrollable = _scrollable; + _scrollable = scrollable; + return scrollable.position != oldScrollable?.position; + } +}