fix: вроде починил пересланные сообщения с каналов

This commit is contained in:
Jganenokk
2026-07-27 14:48:38 +07:00
parent 3b34ef0535
commit de95fd7c65
11 changed files with 613 additions and 143 deletions
@@ -14,6 +14,9 @@ enum MessageType { text, attachment, voice, control }
enum BubbleShape { singleTop, singleBottom, singleMiddle, groupedMiddle }
typedef ForwardedSourceTap =
void Function(ForwardedMessageAttachment forwarded);
final Expando<({bool full, String text})> _clockTextCache = Expando();
({IconData icon, Color color}) messageStatusVisual(
@@ -69,6 +72,7 @@ class BubbleContext {
final ValueListenable<int>? otherReadTime;
final ValueListenable<List<double>>? uploadProgress;
final void Function(StickerAttachment sticker)? onStickerTap;
final ForwardedSourceTap? onForwardedSourceTap;
BubbleContext({
required this.context,
@@ -88,6 +92,7 @@ class BubbleContext {
this.otherReadTime,
this.uploadProgress,
this.onStickerTap,
this.onForwardedSourceTap,
this.reactionInfo,
}) : dim = text.withValues(alpha: 0.7);
@@ -4,66 +4,113 @@ import 'package:material_symbols_icons/symbols.dart';
import '../../../../backend/modules/messages.dart';
import '../../../../models/attachment.dart';
import '../../formatted_message_text.dart';
import 'bubble_context.dart';
import 'contact_bubble.dart';
import 'file_bubble.dart';
import 'photo_bubble.dart';
import 'sticker_bubble.dart';
Widget _forwardedHeader(
BubbleContext ctx,
ForwardedMessageAttachment forwarded,
) {
final headerColor = ctx.dim;
final displaySender =
String _forwardedSourceName(ForwardedMessageAttachment forwarded) {
final resolved =
forwarded.originalSenderName ??
ContactCache.get(forwarded.originalSenderId) ??
forwarded.originalSenderId.toString();
final senderAvatar =
forwarded.originalSenderAvatar ??
ContactCache.getAvatar(forwarded.originalSenderId);
return Padding(
padding: const EdgeInsets.only(left: 8, top: 8, right: 8),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Symbols.forward, size: 14, color: headerColor),
const SizedBox(width: 4),
if (senderAvatar != null && senderAvatar.isNotEmpty)
CircleAvatar(
radius: 10,
backgroundImage: CachedNetworkImageProvider(
senderAvatar,
maxWidth: 96,
maxHeight: 96,
ContactCache.get(forwarded.originalSenderId);
if (resolved != null && resolved.isNotEmpty) return resolved;
if (forwarded.isChannel) return 'Канал';
if (forwarded.originalSenderId != 0) {
return forwarded.originalSenderId.toString();
}
return 'Сообщение';
}
String? _forwardedSourceAvatar(ForwardedMessageAttachment forwarded) =>
forwarded.originalSenderAvatar ??
ContactCache.getAvatar(forwarded.originalSenderId);
class ForwardedHeader extends StatelessWidget {
final BubbleContext ctx;
final ForwardedMessageAttachment forwarded;
final EdgeInsetsGeometry padding;
const ForwardedHeader({
super.key,
required this.ctx,
required this.forwarded,
this.padding = const EdgeInsets.only(left: 8, top: 8, right: 8),
});
@override
Widget build(BuildContext context) {
final headerColor = ctx.dim;
final displaySender = _forwardedSourceName(forwarded);
final senderAvatar = _forwardedSourceAvatar(forwarded);
final content = Padding(
padding: padding,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Symbols.forward, size: 14, color: headerColor),
const SizedBox(width: 4),
if (senderAvatar != null && senderAvatar.isNotEmpty)
CircleAvatar(
radius: 10,
backgroundImage: CachedNetworkImageProvider(
senderAvatar,
maxWidth: 96,
maxHeight: 96,
),
backgroundColor: ctx.cs.primaryContainer,
)
else
CircleAvatar(
radius: 10,
backgroundColor: ctx.cs.primaryContainer,
child: Text(
displaySender.isNotEmpty ? displaySender[0].toUpperCase() : '?',
style: TextStyle(fontSize: 9, color: ctx.cs.onPrimaryContainer),
),
),
backgroundColor: ctx.cs.primaryContainer,
)
else
CircleAvatar(
radius: 10,
backgroundColor: ctx.cs.primaryContainer,
const SizedBox(width: 6),
Flexible(
child: Text(
displaySender.isNotEmpty ? displaySender[0].toUpperCase() : '?',
style: TextStyle(fontSize: 9, color: ctx.cs.onPrimaryContainer),
displaySender,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: headerColor,
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
),
const SizedBox(width: 6),
Flexible(
child: Text(
displaySender,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: headerColor,
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
),
],
),
);
],
),
);
final onTap = ctx.onForwardedSourceTap;
if (onTap == null) return content;
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => onTap(forwarded),
child: content,
);
}
}
Widget buildForwardedMessageText(
BubbleContext ctx,
ForwardedMessageAttachment forwarded, {
double fontSize = 14,
}) {
final text = forwarded.originalText ?? '';
final style = TextStyle(color: ctx.text, fontSize: fontSize, height: 1.3);
if (FormattedMessageText.isFormatted(text, forwarded.originalFormatRanges)) {
return FormattedMessageText(
text: text,
ranges: forwarded.originalFormatRanges,
style: style,
);
}
return Text(text, style: style);
}
class ForwardedPhotoBubble extends StatelessWidget {
@@ -80,27 +127,26 @@ class ForwardedPhotoBubble extends StatelessWidget {
@override
Widget build(BuildContext context) {
final message = ctx.message;
final hasCaption = message.text != null && message.text!.isNotEmpty;
final hasCaption = forwarded.originalText?.isNotEmpty ?? false;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
_forwardedHeader(ctx, forwarded),
const SizedBox(height: 4),
if (hasCaption) ...[
Padding(
padding: const EdgeInsets.only(left: 8),
child: Text(
message.text ?? '',
style: TextStyle(color: ctx.text, fontSize: 16, height: 1.3),
),
return SizedBox(
width: PhotoBubble.layoutWidth(photos),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
ForwardedHeader(ctx: ctx, forwarded: forwarded),
const SizedBox(height: 4),
PhotoBubble(
ctx: ctx,
photos: photos,
caption: hasCaption
? buildForwardedMessageText(ctx, forwarded, fontSize: 16)
: null,
hasContentAbove: true,
),
const SizedBox(height: 6),
],
PhotoBubble(ctx: ctx, photos: photos),
],
),
);
}
}
@@ -124,8 +170,15 @@ class ForwardedGenericBubble extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: [
_forwardedHeader(ctx, forwarded),
ForwardedHeader(ctx: ctx, forwarded: forwarded),
const SizedBox(height: 4),
if (forwarded.originalText?.isNotEmpty ?? false) ...[
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: buildForwardedMessageText(ctx, forwarded),
),
const SizedBox(height: 6),
],
...attachments.map((a) {
if (a is FileAttachment) {
return FileBubble(ctx: ctx, file: a, fill: true);
@@ -159,8 +212,15 @@ class ForwardedStickerBubble extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
_forwardedHeader(ctx, forwarded),
ForwardedHeader(ctx: ctx, forwarded: forwarded),
const SizedBox(height: 4),
if (forwarded.originalText?.isNotEmpty ?? false) ...[
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: buildForwardedMessageText(ctx, forwarded),
),
const SizedBox(height: 6),
],
StickerBubble(ctx: ctx, sticker: sticker),
],
);
@@ -185,8 +245,15 @@ class ForwardedContactBubble extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
_forwardedHeader(ctx, forwarded),
ForwardedHeader(ctx: ctx, forwarded: forwarded),
const SizedBox(height: 4),
if (forwarded.originalText?.isNotEmpty ?? false) ...[
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: buildForwardedMessageText(ctx, forwarded),
),
const SizedBox(height: 6),
],
buildContactCard(
ctx,
firstName: contact.firstName,
@@ -21,18 +21,42 @@ class PhotoBubble extends StatelessWidget {
final BubbleContext ctx;
final List<PhotoAttachment> photos;
final Widget? caption;
final bool hasContentAbove;
const PhotoBubble({super.key, required this.ctx, required this.photos});
const PhotoBubble({
super.key,
required this.ctx,
required this.photos,
this.caption,
this.hasContentAbove = false,
});
static double layoutWidth(List<PhotoAttachment> photos) {
if (photos.length != 1) return BubbleContext.photoMaxSize;
return (photos.single.width?.toDouble() ?? 200).clamp(
BubbleContext.photoMinSize,
BubbleContext.photoMaxSize,
);
}
@override
Widget build(BuildContext context) {
final message = ctx.message;
final hasCaption = message.text != null && message.text!.isNotEmpty;
final hasMessageCaption = message.text != null && message.text!.isNotEmpty;
final resolvedCaption =
caption ?? (hasMessageCaption ? ctx.caption() : null);
final hasCaption = resolvedCaption != null;
final count = photos.length;
Widget photosWidget;
if (count == 1) {
photosWidget = _buildSinglePhoto(ctx, photos[0]);
photosWidget = _buildSinglePhoto(
ctx,
photos[0],
hasCaption: hasCaption,
hasContentAbove: hasContentAbove,
);
} else if (count == 2) {
photosWidget = _buildTwoPhotos(ctx, photos[0], photos[1]);
} else {
@@ -53,15 +77,8 @@ class PhotoBubble extends StatelessWidget {
}
if (count == 1) {
final photo = photos[0];
final pw = photo.width?.toDouble() ?? 200;
final photoWidth = pw.clamp(
BubbleContext.photoMinSize,
BubbleContext.photoMaxSize,
);
return SizedBox(
width: photoWidth,
width: layoutWidth(photos),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
@@ -76,7 +93,7 @@ class PhotoBubble extends StatelessWidget {
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Expanded(child: ctx.caption()),
Expanded(child: resolvedCaption),
ctx.meta(),
],
),
@@ -100,7 +117,7 @@ class PhotoBubble extends StatelessWidget {
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Expanded(child: ctx.caption()),
Expanded(child: resolvedCaption),
ctx.meta(),
],
),
@@ -109,7 +126,12 @@ class PhotoBubble extends StatelessWidget {
);
}
Widget _buildSinglePhoto(BubbleContext ctx, PhotoAttachment photo) {
Widget _buildSinglePhoto(
BubbleContext ctx,
PhotoAttachment photo, {
required bool hasCaption,
required bool hasContentAbove,
}) {
final width = photo.width?.toDouble() ?? 200;
final height = photo.height?.toDouble() ?? 200;
@@ -123,8 +145,8 @@ class PhotoBubble extends StatelessWidget {
);
final dpr = MediaQuery.of(ctx.context).devicePixelRatio;
final matchTop = ctx.hasPhotoWithCaption;
final matchBottom = !ctx.hasPhotoWithCaption;
final matchTop = hasCaption && !hasContentAbove;
final matchBottom = !hasCaption;
final topR = matchTop ? _bigRadius : _photoRadius;
final bottomL = matchBottom