эмозе
This commit is contained in:
@@ -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),
|
],
|
||||||
],
|
],
|
||||||
],
|
),
|
||||||
),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user