эмозе

This commit is contained in:
Jganenok
2026-05-28 21:00:38 +07:00
parent 0947863697
commit 1481a4af2c
+167 -97
View File
@@ -23,6 +23,7 @@ class _BubbleCtx {
final MessageType contentType; final MessageType contentType;
final bool hasPhotoWithCaption; final bool hasPhotoWithCaption;
final bool hasMultiplePhotosNoCaption; final bool hasMultiplePhotosNoCaption;
final Map? reactionInfo;
_BubbleCtx({ _BubbleCtx({
required this.context, required this.context,
@@ -32,6 +33,7 @@ class _BubbleCtx {
required this.contentType, required this.contentType,
required this.hasPhotoWithCaption, required this.hasPhotoWithCaption,
required this.hasMultiplePhotosNoCaption, required this.hasMultiplePhotosNoCaption,
this.reactionInfo,
}) : dim = text.withValues(alpha: 0.7); }) : dim = text.withValues(alpha: 0.7);
} }
@@ -262,16 +264,6 @@ class MessageBubble extends StatelessWidget {
final hasMultiPhotos = _computeHasMultiplePhotosNoCaption(); final hasMultiPhotos = _computeHasMultiplePhotosNoCaption();
final textColor = bubbleTextColor(context); final textColor = bubbleTextColor(context);
final ctx = _BubbleCtx(
context: context,
cs: cs,
text: textColor,
shape: shape,
contentType: contentType,
hasPhotoWithCaption: hasPhotoCap,
hasMultiplePhotosNoCaption: hasMultiPhotos,
);
final topMargin = _topMarginFor(contentType, shape); final topMargin = _topMarginFor(contentType, shape);
final bottomMargin = _bottomMarginFor(contentType, shape); final bottomMargin = _bottomMarginFor(contentType, shape);
final padding = _paddingFor(contentType, shape); final padding = _paddingFor(contentType, shape);
@@ -282,6 +274,12 @@ class MessageBubble extends StatelessWidget {
nextMessage?.senderId != message.senderId && nextMessage?.senderId != message.senderId &&
prevMessage?.senderId == message.senderId; prevMessage?.senderId == message.senderId;
final bubbleListenable = Listenable.merge([
AppBubbleShape.current,
AppBubbleBehavior.current,
?reactionsListenable,
]);
return GestureDetector( return GestureDetector(
onTap: Haptics.tap, onTap: Haptics.tap,
child: Padding( child: Padding(
@@ -312,46 +310,63 @@ class MessageBubble extends StatelessWidget {
isMe ? CrossAxisAlignment.end : CrossAxisAlignment.start, isMe ? CrossAxisAlignment.end : CrossAxisAlignment.start,
children: [ children: [
ListenableBuilder( ListenableBuilder(
listenable: Listenable.merge( listenable: bubbleListenable,
[AppBubbleShape.current, AppBubbleBehavior.current], builder: (context, _) {
), final reactionInfo = _resolveReactionInfo();
builder: (context, child) { final ctx = _BubbleCtx(
return Container( context: context,
constraints: BoxConstraints( cs: cs,
maxWidth: MediaQuery.sizeOf(context).width * 0.75, text: textColor,
), shape: shape,
decoration: BoxDecoration( contentType: contentType,
color: isMe hasPhotoWithCaption: hasPhotoCap,
? cs.primaryContainer hasMultiplePhotosNoCaption: hasMultiPhotos,
: cs.surfaceContainerHighest, reactionInfo: reactionInfo,
borderRadius: _borderRadiusFor( );
AppBubbleShape.current.value, return AnimatedSize(
AppBubbleBehavior.current.value, duration: const Duration(milliseconds: 150),
shape, curve: Curves.easeOutCubic,
hasPhotoCap, alignment: isMe
hasMultiPhotos, ? Alignment.bottomRight
: Alignment.bottomLeft,
child: Container(
constraints: BoxConstraints(
maxWidth:
MediaQuery.sizeOf(context).width * 0.75,
), ),
decoration: BoxDecoration(
color: isMe
? cs.primaryContainer
: cs.surfaceContainerHighest,
borderRadius: _borderRadiusFor(
AppBubbleShape.current.value,
AppBubbleBehavior.current.value,
shape,
hasPhotoCap,
hasMultiPhotos,
),
),
padding: padding,
child: _buildContent(ctx),
), ),
padding: padding,
child: child,
); );
}, },
child: _buildContent(ctx),
),
AnimatedSize(
duration: const Duration(milliseconds: 150),
curve: Curves.easeOutCubic,
alignment: isMe
? Alignment.centerRight
: Alignment.centerLeft,
child: reactionsListenable != null
? ValueListenableBuilder<Map<String, dynamic>?>(
valueListenable: reactionsListenable!,
builder: (context, info, _) =>
_buildReactionsBarFor(cs, info),
)
: _buildReactionsBar(cs),
), ),
if (contentType != MessageType.text)
AnimatedSize(
duration: const Duration(milliseconds: 150),
curve: Curves.easeOutCubic,
alignment: isMe
? Alignment.centerRight
: Alignment.centerLeft,
child: reactionsListenable != null
? ValueListenableBuilder<Map<String, dynamic>?>(
valueListenable: reactionsListenable!,
builder: (context, info, _) =>
_buildReactionsBarFor(cs, info),
)
: _buildReactionsBar(cs),
),
], ],
), ),
], ],
@@ -361,6 +376,16 @@ class MessageBubble extends StatelessWidget {
); );
} }
Map? _resolveReactionInfo() {
if (reactionsListenable != null) {
final v = reactionsListenable!.value;
if (v != null) return v;
}
final info = message.payload?['reactionInfo'];
if (info is Map) return info;
return null;
}
Widget _buildContent(_BubbleCtx ctx) { Widget _buildContent(_BubbleCtx ctx) {
switch (ctx.contentType) { switch (ctx.contentType) {
case MessageType.control: case MessageType.control:
@@ -380,9 +405,18 @@ class MessageBubble extends StatelessWidget {
} }
Widget _buildReactionsBarFor(ColorScheme cs, Map? info) { Widget _buildReactionsBarFor(ColorScheme cs, Map? info) {
if (info == null) return const SizedBox.shrink(); final chips = _buildReactionChipsFor(cs, info);
if (chips.isEmpty) return const SizedBox.shrink();
return Padding(
padding: const EdgeInsets.only(top: 4),
child: Wrap(spacing: 4, runSpacing: 4, children: chips),
);
}
List<Widget> _buildReactionChipsFor(ColorScheme cs, Map? info) {
if (info == null) return const [];
final counters = info['counters']; final counters = info['counters'];
if (counters is! List || counters.isEmpty) return const SizedBox.shrink(); if (counters is! List || counters.isEmpty) return const [];
final yourReaction = info['yourReaction']?.toString(); final yourReaction = info['yourReaction']?.toString();
final chips = <Widget>[]; final chips = <Widget>[];
@@ -394,30 +428,24 @@ class MessageBubble extends StatelessWidget {
final isYours = yourReaction == reaction; final isYours = yourReaction == reaction;
chips.add( chips.add(
Container( Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration( decoration: BoxDecoration(
color: isYours color: isYours
? cs.primary.withValues(alpha: 0.18) ? cs.primary.withValues(alpha: 0.22)
: cs.surfaceContainerHighest.withValues(alpha: 0.6), : Colors.black.withValues(alpha: 0.18),
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(10),
border: Border.all(
color: isYours
? cs.primary.withValues(alpha: 0.45)
: cs.outlineVariant.withValues(alpha: 0.35),
width: 1,
),
), ),
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text(reaction, style: const TextStyle(fontSize: 14)), Text(reaction, style: const TextStyle(fontSize: 13)),
if (count is int && count > 1) ...[ if (count is int && count > 1) ...[
const SizedBox(width: 4), const SizedBox(width: 3),
Text( Text(
count.toString(), count.toString(),
style: TextStyle( style: TextStyle(
color: isYours ? cs.primary : cs.onSurfaceVariant, color: isYours ? cs.primary : cs.onSurfaceVariant,
fontSize: 12, fontSize: 11,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
), ),
@@ -427,11 +455,7 @@ class MessageBubble extends StatelessWidget {
), ),
); );
} }
if (chips.isEmpty) return const SizedBox.shrink(); return chips;
return Padding(
padding: const EdgeInsets.only(top: 4),
child: Wrap(spacing: 4, runSpacing: 4, children: chips),
);
} }
Widget _buildControlContent(ColorScheme cs) { Widget _buildControlContent(ColorScheme cs) {
@@ -504,49 +528,95 @@ class MessageBubble extends StatelessWidget {
final displaySender = ContactCache.get(message.senderId); final displaySender = ContactCache.get(message.senderId);
final reactionChips = _buildReactionChipsFor(ctx.cs, ctx.reactionInfo);
final hasReactions = reactionChips.isNotEmpty;
final textWidget = isForwarded
? _buildForwardedInlineText(ctx, forwarded)
: Text(
message.text ?? '',
style: TextStyle(
color: ctx.text,
fontSize: 16,
height: 1.3,
),
);
final metaWidget = Text(
message.status == 'EDITED'
? '${_formatTime(message.time)} ред.'
: _formatTime(message.time),
style: TextStyle(color: ctx.dim, fontSize: 10),
);
final showSender = message.senderId != message.accountId &&
prevMessage?.senderId != message.senderId &&
chatType == "CHAT";
if (hasReactions) {
return IntrinsicWidth(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (showSender)
Text(
displaySender ?? "",
textAlign: TextAlign.left,
style: TextStyle(color: ctx.text),
),
textWidget,
const SizedBox(height: 6),
Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Expanded(
child: Wrap(
spacing: 4,
runSpacing: 4,
children: reactionChips,
),
),
const SizedBox(width: 8),
Padding(
padding: const EdgeInsets.only(bottom: 2),
child: metaWidget,
),
if (isMe) ...[
const SizedBox(width: 4),
_buildStatusIcon(ctx),
],
],
),
],
),
);
}
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if (message.senderId != message.accountId && if (showSender)
prevMessage?.senderId != message.senderId &&
chatType == "CHAT")
Text( Text(
displaySender ?? "", displaySender ?? "",
textAlign: TextAlign.left, textAlign: TextAlign.left,
style: TextStyle(color: ctx.text), style: TextStyle(color: ctx.text),
), ),
Row( Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end,
children: [ children: [
Flexible( Flexible(child: textWidget),
child: isForwarded const SizedBox(width: 8),
? _buildForwardedInlineText(ctx, forwarded) Padding(
: Text( padding: const EdgeInsets.only(bottom: 2),
message.text ?? '', child: metaWidget,
style: TextStyle(
color: ctx.text,
fontSize: 16,
height: 1.3,
),
),
),
const SizedBox(width: 8),
Padding(
padding: const EdgeInsets.only(bottom: 2),
child: Text(
message.status == 'EDITED'
? '${_formatTime(message.time)} ред.'
: _formatTime(message.time),
style: TextStyle(color: ctx.dim, fontSize: 10),
), ),
), if (isMe) ...[
if (isMe) ...[ const SizedBox(width: 4),
const SizedBox(width: 4), _buildStatusIcon(ctx),
_buildStatusIcon(ctx), ],
], ],
], ),
),
], ],
); );
} }