feat: Use dynamic gradient for chat bubbles

This commit is contained in:
Krille 2025-02-05 15:28:27 +01:00
commit 480513c708
No known key found for this signature in database
GPG key ID: E067ECD60F1A0652
2 changed files with 191 additions and 103 deletions

View file

@ -152,6 +152,7 @@ class ChatEventList extends StatelessWidget {
nextEvent: i + 1 < events.length ? events[i + 1] : null, nextEvent: i + 1 < events.length ? events[i + 1] : null,
previousEvent: i > 0 ? events[i - 1] : null, previousEvent: i > 0 ? events[i - 1] : null,
wallpaperMode: hasWallpaper, wallpaperMode: hasWallpaper,
scrollController: controller.scrollController,
), ),
); );
}, },

View file

@ -1,3 +1,5 @@
import 'dart:ui' as ui;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@ -36,6 +38,7 @@ class Message extends StatelessWidget {
final bool animateIn; final bool animateIn;
final void Function()? resetAnimateIn; final void Function()? resetAnimateIn;
final bool wallpaperMode; final bool wallpaperMode;
final ScrollController scrollController;
const Message( const Message(
this.event, { this.event, {
@ -54,6 +57,7 @@ class Message extends StatelessWidget {
this.animateIn = false, this.animateIn = false,
this.resetAnimateIn, this.resetAnimateIn,
this.wallpaperMode = false, this.wallpaperMode = false,
required this.scrollController,
super.key, super.key,
}); });
@ -323,118 +327,132 @@ class Message extends StatelessWidget {
borderRadius: borderRadius, borderRadius: borderRadius,
), ),
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
child: Container( child: BubbleBackground(
decoration: BoxDecoration( colors: [
borderRadius: BorderRadius.circular( theme.brightness == Brightness.light
AppConfig.borderRadius, ? theme.colorScheme.tertiary
: theme.colorScheme
.tertiaryContainer,
color,
],
ignore: noBubble || !ownMessage,
scrollController: scrollController,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(
AppConfig.borderRadius,
),
), ),
), padding: noBubble || noPadding
padding: noBubble || noPadding ? EdgeInsets.zero
? EdgeInsets.zero : const EdgeInsets.symmetric(
: const EdgeInsets.symmetric( horizontal: 16,
horizontal: 16, vertical: 8,
vertical: 8, ),
), constraints: const BoxConstraints(
constraints: const BoxConstraints( maxWidth:
maxWidth: FluffyThemes.columnWidth * 1.5,
FluffyThemes.columnWidth * 1.5, ),
), child: Column(
child: Column( mainAxisSize: MainAxisSize.min,
mainAxisSize: MainAxisSize.min, crossAxisAlignment:
crossAxisAlignment: CrossAxisAlignment.start,
CrossAxisAlignment.start, children: <Widget>[
children: <Widget>[ if (event.relationshipType ==
if (event.relationshipType == RelationshipTypes.reply)
RelationshipTypes.reply) FutureBuilder<Event?>(
FutureBuilder<Event?>( future: event
future: event .getReplyEvent(timeline),
.getReplyEvent(timeline), builder: (
builder: ( BuildContext context,
BuildContext context, snapshot,
snapshot, ) {
) { final replyEvent = snapshot
final replyEvent = snapshot .hasData
.hasData ? snapshot.data!
? snapshot.data! : Event(
: Event( eventId: event
eventId: event .relationshipEventId!,
.relationshipEventId!, content: {
content: { 'msgtype':
'msgtype': 'm.text', 'm.text',
'body': '...', 'body': '...',
}, },
senderId: senderId:
event.senderId, event.senderId,
type: type:
'm.room.message', 'm.room.message',
room: event.room, room: event.room,
status: status: EventStatus
EventStatus.sent, .sent,
originServerTs: originServerTs:
DateTime.now(), DateTime.now(),
); );
return Padding( return Padding(
padding: padding:
const EdgeInsets.only( const EdgeInsets.only(
bottom: 4.0, bottom: 4.0,
),
child: InkWell(
borderRadius: ReplyContent
.borderRadius,
onTap: () =>
scrollToEventId(
replyEvent.eventId,
), ),
child: AbsorbPointer( child: InkWell(
child: ReplyContent( borderRadius:
replyEvent, ReplyContent
ownMessage: .borderRadius,
ownMessage, onTap: () =>
timeline: timeline, 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( MessageContent(
mainAxisSize: displayEvent,
MainAxisSize.min, textColor: textColor,
children: [ linkColor: linkColor,
Icon( onInfoTab: onInfoTab,
Icons.edit_outlined, borderRadius: borderRadius,
color: textColor timeline: timeline,
.withAlpha(164), ),
size: 14, if (event.hasAggregatedEvents(
), timeline,
Text( RelationshipTypes.edit,
' - ${displayEvent.originServerTs.localizedTimeShort(context)}', ))
style: TextStyle( Padding(
padding:
const EdgeInsets.only(
top: 4.0,
),
child: Row(
mainAxisSize:
MainAxisSize.min,
children: [
Icon(
Icons.edit_outlined,
color: textColor color: textColor
.withAlpha(164), .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<Color> 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<Color> 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;
}
}