эмозе

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 bool hasPhotoWithCaption;
final bool hasMultiplePhotosNoCaption;
final Map? reactionInfo;
_BubbleCtx({
required this.context,
@@ -32,6 +33,7 @@ class _BubbleCtx {
required this.contentType,
required this.hasPhotoWithCaption,
required this.hasMultiplePhotosNoCaption,
this.reactionInfo,
}) : dim = text.withValues(alpha: 0.7);
}
@@ -262,16 +264,6 @@ class MessageBubble extends StatelessWidget {
final hasMultiPhotos = _computeHasMultiplePhotosNoCaption();
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 bottomMargin = _bottomMarginFor(contentType, shape);
final padding = _paddingFor(contentType, shape);
@@ -282,6 +274,12 @@ class MessageBubble extends StatelessWidget {
nextMessage?.senderId != message.senderId &&
prevMessage?.senderId == message.senderId;
final bubbleListenable = Listenable.merge([
AppBubbleShape.current,
AppBubbleBehavior.current,
?reactionsListenable,
]);
return GestureDetector(
onTap: Haptics.tap,
child: Padding(
@@ -312,46 +310,63 @@ class MessageBubble extends StatelessWidget {
isMe ? CrossAxisAlignment.end : CrossAxisAlignment.start,
children: [
ListenableBuilder(
listenable: Listenable.merge(
[AppBubbleShape.current, AppBubbleBehavior.current],
),
builder: (context, child) {
return 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,
listenable: bubbleListenable,
builder: (context, _) {
final reactionInfo = _resolveReactionInfo();
final ctx = _BubbleCtx(
context: context,
cs: cs,
text: textColor,
shape: shape,
contentType: contentType,
hasPhotoWithCaption: hasPhotoCap,
hasMultiplePhotosNoCaption: hasMultiPhotos,
reactionInfo: reactionInfo,
);
return AnimatedSize(
duration: const Duration(milliseconds: 150),
curve: Curves.easeOutCubic,
alignment: isMe
? 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) {
switch (ctx.contentType) {
case MessageType.control:
@@ -380,9 +405,18 @@ class MessageBubble extends StatelessWidget {
}
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'];
if (counters is! List || counters.isEmpty) return const SizedBox.shrink();
if (counters is! List || counters.isEmpty) return const [];
final yourReaction = info['yourReaction']?.toString();
final chips = <Widget>[];
@@ -394,30 +428,24 @@ class MessageBubble extends StatelessWidget {
final isYours = yourReaction == reaction;
chips.add(
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: isYours
? cs.primary.withValues(alpha: 0.18)
: cs.surfaceContainerHighest.withValues(alpha: 0.6),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: isYours
? cs.primary.withValues(alpha: 0.45)
: cs.outlineVariant.withValues(alpha: 0.35),
width: 1,
),
? cs.primary.withValues(alpha: 0.22)
: Colors.black.withValues(alpha: 0.18),
borderRadius: BorderRadius.circular(10),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(reaction, style: const TextStyle(fontSize: 14)),
Text(reaction, style: const TextStyle(fontSize: 13)),
if (count is int && count > 1) ...[
const SizedBox(width: 4),
const SizedBox(width: 3),
Text(
count.toString(),
style: TextStyle(
color: isYours ? cs.primary : cs.onSurfaceVariant,
fontSize: 12,
fontSize: 11,
fontWeight: FontWeight.w600,
),
),
@@ -427,11 +455,7 @@ class MessageBubble extends StatelessWidget {
),
);
}
if (chips.isEmpty) return const SizedBox.shrink();
return Padding(
padding: const EdgeInsets.only(top: 4),
child: Wrap(spacing: 4, runSpacing: 4, children: chips),
);
return chips;
}
Widget _buildControlContent(ColorScheme cs) {
@@ -504,49 +528,95 @@ class MessageBubble extends StatelessWidget {
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(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (message.senderId != message.accountId &&
prevMessage?.senderId != message.senderId &&
chatType == "CHAT")
if (showSender)
Text(
displaySender ?? "",
textAlign: TextAlign.left,
style: TextStyle(color: ctx.text),
),
Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Flexible(
child: isForwarded
? _buildForwardedInlineText(ctx, forwarded)
: Text(
message.text ?? '',
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),
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Flexible(child: textWidget),
const SizedBox(width: 8),
Padding(
padding: const EdgeInsets.only(bottom: 2),
child: metaWidget,
),
),
if (isMe) ...[
const SizedBox(width: 4),
_buildStatusIcon(ctx),
if (isMe) ...[
const SizedBox(width: 4),
_buildStatusIcon(ctx),
],
],
],
),
),
],
);
}