refactor: зарефакторил пересланные сообщения
This commit is contained in:
@@ -6,6 +6,7 @@ import '../../../../backend/modules/messages.dart';
|
||||
import '../../../../core/config/app_colors.dart';
|
||||
import '../../../../core/config/komet_settings.dart';
|
||||
import '../../../../core/utils/format.dart';
|
||||
import '../../../../core/utils/text_format.dart';
|
||||
import '../../../../models/attachment.dart';
|
||||
import '../../formatted_message_text.dart';
|
||||
import '../../sending_clock_icon.dart';
|
||||
@@ -20,6 +21,20 @@ typedef ForwardedSourceTap =
|
||||
|
||||
final Expando<({bool full, String text})> _clockTextCache = Expando();
|
||||
|
||||
class BubblePresentation {
|
||||
final String? text;
|
||||
final List<FormatRange> formatRanges;
|
||||
final String? sourceMessageId;
|
||||
final int? sourceChatId;
|
||||
|
||||
const BubblePresentation({
|
||||
this.text,
|
||||
this.formatRanges = const [],
|
||||
this.sourceMessageId,
|
||||
this.sourceChatId,
|
||||
});
|
||||
}
|
||||
|
||||
({IconData icon, Color color}) messageStatusVisual(
|
||||
String? status, {
|
||||
required Color dimColor,
|
||||
@@ -75,6 +90,7 @@ class BubbleContext {
|
||||
final ValueListenable<List<double>>? uploadProgress;
|
||||
final void Function(StickerAttachment sticker)? onStickerTap;
|
||||
final ForwardedSourceTap? onForwardedSourceTap;
|
||||
final BubblePresentation? presentation;
|
||||
|
||||
BubbleContext({
|
||||
required this.context,
|
||||
@@ -97,8 +113,43 @@ class BubbleContext {
|
||||
this.onStickerTap,
|
||||
this.onForwardedSourceTap,
|
||||
this.reactionInfo,
|
||||
this.presentation,
|
||||
}) : dim = text.withValues(alpha: 0.7);
|
||||
|
||||
String? get contentText =>
|
||||
presentation == null ? message.text : presentation!.text;
|
||||
|
||||
List<FormatRange> get contentFormatRanges =>
|
||||
presentation == null ? message.formatRanges : presentation!.formatRanges;
|
||||
|
||||
String get sourceMessageId => presentation?.sourceMessageId ?? message.id;
|
||||
|
||||
int get sourceChatId => presentation?.sourceChatId ?? message.chatId;
|
||||
|
||||
BubbleContext withPresentation(BubblePresentation value) => BubbleContext(
|
||||
context: context,
|
||||
cs: cs,
|
||||
text: text,
|
||||
shape: shape,
|
||||
contentType: contentType,
|
||||
hasPhotoWithCaption: hasPhotoWithCaption,
|
||||
hasMultiplePhotosNoCaption: hasMultiplePhotosNoCaption,
|
||||
message: message,
|
||||
isMe: isMe,
|
||||
myId: myId,
|
||||
chatType: chatType,
|
||||
chatId: chatId,
|
||||
chatName: chatName,
|
||||
photoActions: photoActions,
|
||||
overrideStatus: overrideStatus,
|
||||
otherReadTime: otherReadTime,
|
||||
uploadProgress: uploadProgress,
|
||||
onStickerTap: onStickerTap,
|
||||
onForwardedSourceTap: onForwardedSourceTap,
|
||||
reactionInfo: reactionInfo,
|
||||
presentation: value,
|
||||
);
|
||||
|
||||
String get clockText {
|
||||
final full = KometSettings.fullTimestamp.value;
|
||||
final cached = _clockTextCache[message];
|
||||
@@ -115,15 +166,16 @@ class BubbleContext {
|
||||
|
||||
Widget caption() {
|
||||
final style = TextStyle(color: text, fontSize: 16, height: 1.3);
|
||||
final ranges = message.formatRanges;
|
||||
if (FormattedMessageText.isFormatted(message.text, ranges)) {
|
||||
final captionText = contentText;
|
||||
final ranges = contentFormatRanges;
|
||||
if (FormattedMessageText.isFormatted(captionText, ranges)) {
|
||||
return FormattedMessageText(
|
||||
text: message.text!,
|
||||
text: captionText!,
|
||||
ranges: ranges,
|
||||
style: style,
|
||||
);
|
||||
}
|
||||
return Text(message.text ?? '', style: style);
|
||||
return Text(captionText ?? '', style: style);
|
||||
}
|
||||
|
||||
Widget meta() {
|
||||
|
||||
@@ -4,12 +4,7 @@ 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';
|
||||
|
||||
String _forwardedSourceName(ForwardedMessageAttachment forwarded) {
|
||||
final resolved =
|
||||
@@ -96,175 +91,30 @@ class ForwardedHeader extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
class ForwardedHeaderFloating extends StatelessWidget {
|
||||
final BubbleContext ctx;
|
||||
final ForwardedMessageAttachment forwarded;
|
||||
final List<PhotoAttachment> photos;
|
||||
|
||||
const ForwardedPhotoBubble({
|
||||
const ForwardedHeaderFloating({
|
||||
super.key,
|
||||
required this.ctx,
|
||||
required this.forwarded,
|
||||
required this.photos,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final hasCaption = forwarded.originalText?.isNotEmpty ?? false;
|
||||
|
||||
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,
|
||||
),
|
||||
],
|
||||
return Material(
|
||||
color: ctx.isMe
|
||||
? ctx.cs.primaryContainer
|
||||
: ctx.cs.surfaceContainerHighest,
|
||||
elevation: 2,
|
||||
shadowColor: Colors.black.withValues(alpha: 0.2),
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
child: ForwardedHeader(
|
||||
ctx: ctx,
|
||||
forwarded: forwarded,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ForwardedGenericBubble extends StatelessWidget {
|
||||
final BubbleContext ctx;
|
||||
final ForwardedMessageAttachment forwarded;
|
||||
final List<MessageAttachment> attachments;
|
||||
|
||||
const ForwardedGenericBubble({
|
||||
super.key,
|
||||
required this.ctx,
|
||||
required this.forwarded,
|
||||
required this.attachments,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return IntrinsicWidth(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
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);
|
||||
}
|
||||
if (a is StickerAttachment) {
|
||||
return StickerBubble(ctx: ctx, sticker: a);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
}),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ForwardedStickerBubble extends StatelessWidget {
|
||||
final BubbleContext ctx;
|
||||
final ForwardedMessageAttachment forwarded;
|
||||
final MessageAttachment sticker;
|
||||
|
||||
const ForwardedStickerBubble({
|
||||
super.key,
|
||||
required this.ctx,
|
||||
required this.forwarded,
|
||||
required this.sticker,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
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),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ForwardedContactBubble extends StatelessWidget {
|
||||
final BubbleContext ctx;
|
||||
final ForwardedMessageAttachment forwarded;
|
||||
|
||||
const ForwardedContactBubble({
|
||||
super.key,
|
||||
required this.ctx,
|
||||
required this.forwarded,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final contact = forwarded.originalContact!;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
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,
|
||||
lastName: contact.lastName,
|
||||
name: contact.name,
|
||||
photoUrl: contact.photoUrl ?? contact.baseUrl,
|
||||
phoneNumber: contact.phoneNumber,
|
||||
contactId: contact.contactId,
|
||||
userId: contact.userId,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,14 +21,12 @@ 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,
|
||||
this.caption,
|
||||
this.hasContentAbove = false,
|
||||
});
|
||||
|
||||
@@ -42,10 +40,8 @@ class PhotoBubble extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final message = ctx.message;
|
||||
final hasMessageCaption = message.text != null && message.text!.isNotEmpty;
|
||||
final resolvedCaption =
|
||||
caption ?? (hasMessageCaption ? ctx.caption() : null);
|
||||
final hasMessageCaption = ctx.contentText?.isNotEmpty ?? false;
|
||||
final resolvedCaption = hasMessageCaption ? ctx.caption() : null;
|
||||
final hasCaption = resolvedCaption != null;
|
||||
final count = photos.length;
|
||||
|
||||
|
||||
@@ -20,11 +20,11 @@ class PollBubble extends StatelessWidget {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
PollView(
|
||||
chatId: ctx.message.chatId,
|
||||
messageId: ctx.message.id,
|
||||
chatId: ctx.sourceChatId,
|
||||
messageId: ctx.sourceMessageId,
|
||||
pollId: poll.pollId,
|
||||
myId: ctx.myId,
|
||||
fallbackTitle: poll.title ?? ctx.message.text,
|
||||
fallbackTitle: poll.title ?? ctx.contentText,
|
||||
textColor: ctx.text,
|
||||
dimColor: ctx.dim,
|
||||
accentColor: ctx.isMe
|
||||
|
||||
@@ -16,8 +16,8 @@ class ShareBubble extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isMe = ctx.isMe;
|
||||
final message = ctx.message;
|
||||
final hasText = message.text != null && message.text!.isNotEmpty;
|
||||
final text = ctx.contentText;
|
||||
final hasText = text?.isNotEmpty ?? false;
|
||||
final image = share.image;
|
||||
final imageUrl = image?.baseUrl ?? image?.previewData ?? '';
|
||||
final cardColor = isMe
|
||||
@@ -123,8 +123,8 @@ class ShareBubble extends StatelessWidget {
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: FormattedMessageText(
|
||||
text: message.text!,
|
||||
ranges: message.formatRanges,
|
||||
text: text!,
|
||||
ranges: ctx.contentFormatRanges,
|
||||
style: TextStyle(
|
||||
color: ctx.text,
|
||||
fontSize: 16,
|
||||
|
||||
@@ -19,6 +19,13 @@ class VideoBubble extends StatelessWidget {
|
||||
|
||||
const VideoBubble({super.key, required this.ctx, required this.video});
|
||||
|
||||
static double layoutWidth(VideoAttachment video) {
|
||||
return (video.width?.toDouble() ?? 200.0).clamp(
|
||||
BubbleContext.photoMinSize,
|
||||
BubbleContext.photoMaxSize,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final message = ctx.message;
|
||||
@@ -27,6 +34,8 @@ class VideoBubble extends StatelessWidget {
|
||||
attachment: video,
|
||||
messageId: message.id,
|
||||
chatId: message.chatId,
|
||||
sourceMessageId: ctx.sourceMessageId,
|
||||
sourceChatId: ctx.sourceChatId,
|
||||
senderId: message.senderId,
|
||||
isMe: ctx.isMe,
|
||||
time: message.time,
|
||||
@@ -36,7 +45,9 @@ class VideoBubble extends StatelessWidget {
|
||||
uploadProgress: ctx.uploadProgress,
|
||||
);
|
||||
}
|
||||
final hasCaption = message.text != null && message.text!.isNotEmpty;
|
||||
final hasMessageCaption = ctx.contentText?.isNotEmpty ?? false;
|
||||
final resolvedCaption = hasMessageCaption ? ctx.caption() : null;
|
||||
final hasCaption = resolvedCaption != null;
|
||||
final thumb = video.thumbnail;
|
||||
final durationMs = video.duration;
|
||||
final previewUrl = (thumb != null && thumb.isNotEmpty)
|
||||
@@ -45,12 +56,8 @@ class VideoBubble extends StatelessWidget {
|
||||
? video.baseUrl!
|
||||
: (video.previewData ?? '');
|
||||
|
||||
final w = video.width;
|
||||
final h = video.height;
|
||||
final width = (w?.toDouble() ?? 200.0).clamp(
|
||||
BubbleContext.photoMinSize,
|
||||
BubbleContext.photoMaxSize,
|
||||
);
|
||||
final width = layoutWidth(video);
|
||||
final height = (h?.toDouble() ?? 150.0).clamp(
|
||||
BubbleContext.photoMinSize,
|
||||
BubbleContext.photoMaxSize,
|
||||
@@ -192,7 +199,7 @@ class VideoBubble extends StatelessWidget {
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Expanded(child: ctx.caption()),
|
||||
Expanded(child: resolvedCaption),
|
||||
ctx.meta(),
|
||||
],
|
||||
),
|
||||
@@ -212,8 +219,8 @@ class VideoBubble extends StatelessWidget {
|
||||
Haptics.tap();
|
||||
|
||||
final sources = await messagesModule.getVideoSources(
|
||||
messageId: ctx.message.id,
|
||||
chatId: ctx.message.chatId,
|
||||
messageId: ctx.sourceMessageId,
|
||||
chatId: ctx.sourceChatId,
|
||||
token: token,
|
||||
videoId: videoId,
|
||||
);
|
||||
|
||||
@@ -23,6 +23,8 @@ class VideoNoteBubble extends StatefulWidget {
|
||||
final VideoAttachment attachment;
|
||||
final String messageId;
|
||||
final int chatId;
|
||||
final String? sourceMessageId;
|
||||
final int? sourceChatId;
|
||||
final int senderId;
|
||||
final bool isMe;
|
||||
final int time;
|
||||
@@ -36,6 +38,8 @@ class VideoNoteBubble extends StatefulWidget {
|
||||
required this.attachment,
|
||||
required this.messageId,
|
||||
required this.chatId,
|
||||
this.sourceMessageId,
|
||||
this.sourceChatId,
|
||||
required this.senderId,
|
||||
required this.isMe,
|
||||
required this.time,
|
||||
@@ -217,8 +221,8 @@ class _VideoNoteBubbleState extends State<VideoNoteBubble>
|
||||
return VideoNotePreloader.load(
|
||||
_cacheName,
|
||||
() => messagesModule.getVideoUrl(
|
||||
messageId: widget.messageId,
|
||||
chatId: widget.chatId,
|
||||
messageId: widget.sourceMessageId ?? widget.messageId,
|
||||
chatId: widget.sourceChatId ?? widget.chatId,
|
||||
token: token,
|
||||
videoId: videoId,
|
||||
),
|
||||
|
||||
@@ -26,6 +26,8 @@ class VoiceMessageBubble extends StatefulWidget {
|
||||
final String? waveData;
|
||||
final int chatId;
|
||||
final String messageId;
|
||||
final int? sourceChatId;
|
||||
final String? sourceMessageId;
|
||||
final int senderId;
|
||||
final int? audioId;
|
||||
final String? preloadedText;
|
||||
@@ -45,6 +47,8 @@ class VoiceMessageBubble extends StatefulWidget {
|
||||
this.waveData,
|
||||
required this.chatId,
|
||||
required this.messageId,
|
||||
this.sourceChatId,
|
||||
this.sourceMessageId,
|
||||
required this.senderId,
|
||||
this.audioId,
|
||||
this.preloadedText,
|
||||
@@ -87,7 +91,11 @@ class _VoiceMessageBubbleState extends State<VoiceMessageBubble> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
String get _cacheName => '${widget.audioId ?? widget.messageId}.ogg';
|
||||
String get _cacheName => '${widget.audioId ?? _sourceMessageId}.ogg';
|
||||
|
||||
int get _sourceChatId => widget.sourceChatId ?? widget.chatId;
|
||||
|
||||
String get _sourceMessageId => widget.sourceMessageId ?? widget.messageId;
|
||||
|
||||
void _claimPlayback() {
|
||||
MediaPlayback.instance.activateVoice(
|
||||
@@ -434,8 +442,8 @@ class _VoiceMessageBubbleState extends State<VoiceMessageBubble> {
|
||||
return;
|
||||
}
|
||||
|
||||
if (TranscriptionCache.has(widget.messageId)) {
|
||||
final cached = TranscriptionCache.get(widget.messageId)!;
|
||||
if (TranscriptionCache.has(_sourceMessageId)) {
|
||||
final cached = TranscriptionCache.get(_sourceMessageId)!;
|
||||
setState(() {
|
||||
_transcriptionText = cached.text ?? 'не удалось распознать текст';
|
||||
_transcriptionVisible = true;
|
||||
@@ -449,12 +457,12 @@ class _VoiceMessageBubbleState extends State<VoiceMessageBubble> {
|
||||
|
||||
try {
|
||||
final result = await messagesModule.requestTranscription(
|
||||
widget.chatId,
|
||||
int.tryParse(widget.messageId) ?? 0,
|
||||
_sourceChatId,
|
||||
int.tryParse(_sourceMessageId) ?? 0,
|
||||
widget.audioId!,
|
||||
);
|
||||
|
||||
TranscriptionCache.put(widget.messageId, result);
|
||||
TranscriptionCache.put(_sourceMessageId, result);
|
||||
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
|
||||
@@ -529,21 +529,50 @@ class MessageBubble extends StatelessWidget {
|
||||
});
|
||||
|
||||
bool _computeHasPhotoWithCaption() {
|
||||
final attachments = message.attachments;
|
||||
if (attachments == null || attachments.isEmpty) return false;
|
||||
final attachments = _contentAttachments;
|
||||
if (attachments.isEmpty) return false;
|
||||
final hasPhoto = attachments.any((a) => a is PhotoAttachment);
|
||||
final hasCaption = message.text != null && message.text!.isNotEmpty;
|
||||
final hasCaption = _contentText?.isNotEmpty ?? false;
|
||||
return hasPhoto && hasCaption;
|
||||
}
|
||||
|
||||
bool _computeHasMultiplePhotosNoCaption() {
|
||||
final attachments = message.attachments;
|
||||
if (attachments == null || attachments.isEmpty) return false;
|
||||
final attachments = _contentAttachments;
|
||||
if (attachments.isEmpty) return false;
|
||||
final photoCount = attachments.whereType<PhotoAttachment>().length;
|
||||
final hasCaption = message.text != null && message.text!.isNotEmpty;
|
||||
final hasCaption = _contentText?.isNotEmpty ?? false;
|
||||
return photoCount >= 2 && !hasCaption;
|
||||
}
|
||||
|
||||
ForwardedMessageAttachment? get _forwarded => message.forwardedAttachment;
|
||||
|
||||
List<MessageAttachment> get _contentAttachments {
|
||||
final forwarded = _forwarded;
|
||||
if (forwarded != null) {
|
||||
if (forwarded.originalContact != null) {
|
||||
return [forwarded.originalContact!];
|
||||
}
|
||||
return forwarded.originalAttachments
|
||||
?.where((a) => a is! InlineKeyboardAttachment)
|
||||
.toList() ??
|
||||
const [];
|
||||
}
|
||||
return message.attachments
|
||||
?.where((a) => a is! InlineKeyboardAttachment)
|
||||
.toList() ??
|
||||
const [];
|
||||
}
|
||||
|
||||
MessageAttachment? get _primaryAttachment {
|
||||
final attachments = _contentAttachments;
|
||||
return attachments.isEmpty ? null : attachments.first;
|
||||
}
|
||||
|
||||
String? get _contentText {
|
||||
final forwarded = _forwarded;
|
||||
return forwarded == null ? message.text : forwarded.originalText;
|
||||
}
|
||||
|
||||
bool get _showsSenderName =>
|
||||
!isMe &&
|
||||
chatType == "CHAT" &&
|
||||
@@ -577,22 +606,15 @@ class MessageBubble extends StatelessWidget {
|
||||
}
|
||||
|
||||
bool get _hasShareAttachment {
|
||||
final a = message.attachments;
|
||||
return a != null && a.isNotEmpty && a.first is ShareAttachment;
|
||||
return _primaryAttachment is ShareAttachment;
|
||||
}
|
||||
|
||||
bool get _isVideoNote {
|
||||
final a = message.attachments;
|
||||
if (a == null || a.isEmpty) return false;
|
||||
final first = a.first;
|
||||
final first = _primaryAttachment;
|
||||
return first is VideoAttachment && first.isNote;
|
||||
}
|
||||
|
||||
bool get _isSticker {
|
||||
final a = message.attachments;
|
||||
if (a == null || a.isEmpty) return false;
|
||||
return a.first is StickerAttachment;
|
||||
}
|
||||
bool get _isSticker => _primaryAttachment is StickerAttachment;
|
||||
|
||||
static const int _jumboAnimojiLimit = 4;
|
||||
|
||||
@@ -621,26 +643,14 @@ class MessageBubble extends StatelessWidget {
|
||||
|
||||
MessageType _computeContentType() {
|
||||
if (message.isControl) return MessageType.control;
|
||||
final attachments = message.attachments
|
||||
?.where((a) => a is! InlineKeyboardAttachment)
|
||||
.toList();
|
||||
if (attachments != null && attachments.isNotEmpty) {
|
||||
final attachments = _contentAttachments;
|
||||
if (attachments.isNotEmpty) {
|
||||
final first = attachments.first;
|
||||
if (first is ForwardedMessageAttachment) {
|
||||
final fwd = first;
|
||||
final hasContact = fwd.originalContact != null;
|
||||
final hasPhoto =
|
||||
fwd.originalAttachments != null &&
|
||||
fwd.originalAttachments!.any((a) => a is PhotoAttachment);
|
||||
final hasOther =
|
||||
fwd.originalAttachments != null &&
|
||||
fwd.originalAttachments!.isNotEmpty;
|
||||
if (hasContact || hasPhoto || hasOther) return MessageType.attachment;
|
||||
return MessageType.text;
|
||||
}
|
||||
if (first is ContactAttachment) return MessageType.attachment;
|
||||
if (first is UnknownAttachment) return MessageType.text;
|
||||
if (first.type == AttachmentType.audio) return MessageType.voice;
|
||||
if (first.type == AttachmentType.audio) {
|
||||
return _forwarded == null ? MessageType.voice : MessageType.attachment;
|
||||
}
|
||||
if (first is ShareAttachment) {
|
||||
return AppLinkPreview.current.value
|
||||
? MessageType.attachment
|
||||
@@ -649,6 +659,8 @@ class MessageBubble extends StatelessWidget {
|
||||
return MessageType.attachment;
|
||||
}
|
||||
|
||||
if (_forwarded != null) return MessageType.text;
|
||||
|
||||
final payload = message.payload;
|
||||
if (payload == null) return MessageType.text;
|
||||
if (payload['voice'] != null) return MessageType.voice;
|
||||
@@ -1781,6 +1793,7 @@ class MessageBubble extends StatelessWidget {
|
||||
) {
|
||||
final origText = forwarded.originalText;
|
||||
final hasOrigText = origText != null && origText.isNotEmpty;
|
||||
final forwardedCtx = _forwardedContext(ctx, forwarded);
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -1793,7 +1806,7 @@ class MessageBubble extends StatelessWidget {
|
||||
),
|
||||
if (hasOrigText) ...[
|
||||
const SizedBox(height: 2),
|
||||
_wrapSelectable(buildForwardedMessageText(ctx, forwarded)),
|
||||
_wrapSelectable(forwardedCtx.caption()),
|
||||
] else ...[
|
||||
const SizedBox(height: 2),
|
||||
_wrapSelectable(
|
||||
@@ -1815,35 +1828,76 @@ class MessageBubble extends StatelessWidget {
|
||||
|
||||
final first = attachments.first;
|
||||
if (first is ForwardedMessageAttachment) {
|
||||
final fwd = first;
|
||||
if (fwd.originalContact != null) {
|
||||
return ForwardedContactBubble(ctx: ctx, forwarded: fwd);
|
||||
}
|
||||
final photos = fwd.originalAttachments
|
||||
?.whereType<PhotoAttachment>()
|
||||
.toList();
|
||||
if (photos != null && photos.isNotEmpty) {
|
||||
return ForwardedPhotoBubble(ctx: ctx, forwarded: fwd, photos: photos);
|
||||
}
|
||||
final stickers = fwd.originalAttachments
|
||||
?.whereType<StickerAttachment>()
|
||||
.toList();
|
||||
if (stickers != null && stickers.isNotEmpty) {
|
||||
return ForwardedStickerBubble(
|
||||
ctx: ctx,
|
||||
forwarded: fwd,
|
||||
sticker: stickers.first,
|
||||
);
|
||||
}
|
||||
final files = fwd.originalAttachments;
|
||||
if (files != null && files.isNotEmpty) {
|
||||
return ForwardedGenericBubble(
|
||||
ctx: ctx,
|
||||
forwarded: fwd,
|
||||
attachments: files,
|
||||
);
|
||||
}
|
||||
return _buildTextContent(ctx);
|
||||
return _buildForwardedAttachmentContent(ctx, first);
|
||||
}
|
||||
|
||||
return _buildNativeAttachmentContent(ctx, attachments);
|
||||
}
|
||||
|
||||
BubbleContext _forwardedContext(
|
||||
BubbleContext ctx,
|
||||
ForwardedMessageAttachment forwarded,
|
||||
) => ctx.withPresentation(
|
||||
BubblePresentation(
|
||||
text: forwarded.originalText,
|
||||
formatRanges: forwarded.originalFormatRanges,
|
||||
sourceMessageId: forwarded.originalMessageId,
|
||||
sourceChatId: forwarded.originalChatId,
|
||||
),
|
||||
);
|
||||
|
||||
Widget _buildForwardedAttachmentContent(
|
||||
BubbleContext ctx,
|
||||
ForwardedMessageAttachment forwarded,
|
||||
) {
|
||||
final forwardedCtx = _forwardedContext(ctx, forwarded);
|
||||
final attachments =
|
||||
forwarded.originalAttachments
|
||||
?.where((a) => a is! InlineKeyboardAttachment)
|
||||
.toList() ??
|
||||
const <MessageAttachment>[];
|
||||
final content = _buildNativeAttachmentContent(
|
||||
forwardedCtx,
|
||||
attachments,
|
||||
contact: forwarded.originalContact,
|
||||
hasContentAbove: true,
|
||||
);
|
||||
final primary =
|
||||
forwarded.originalContact ??
|
||||
(attachments.isEmpty ? null : attachments.first);
|
||||
final floatingHeader =
|
||||
primary is StickerAttachment ||
|
||||
(primary is VideoAttachment && primary.isNote);
|
||||
|
||||
if (floatingHeader) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
ForwardedHeaderFloating(ctx: ctx, forwarded: forwarded),
|
||||
const SizedBox(height: 6),
|
||||
content,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return _HeaderAboveMatchWidth(
|
||||
content: content,
|
||||
header: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 4),
|
||||
child: ForwardedHeader(ctx: ctx, forwarded: forwarded),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildNativeAttachmentContent(
|
||||
BubbleContext ctx,
|
||||
List<MessageAttachment> attachments, {
|
||||
ContactAttachment? contact,
|
||||
bool hasContentAbove = false,
|
||||
}) {
|
||||
if (contact != null) {
|
||||
return ContactBubble(ctx: ctx, contact: contact);
|
||||
}
|
||||
|
||||
final contacts = attachments.whereType<ContactAttachment>().toList();
|
||||
@@ -1866,7 +1920,11 @@ class MessageBubble extends StatelessWidget {
|
||||
return _buildGenericAttachment(ctx, attachments.first);
|
||||
}
|
||||
|
||||
return PhotoBubble(ctx: ctx, photos: photos);
|
||||
return PhotoBubble(
|
||||
ctx: ctx,
|
||||
photos: photos,
|
||||
hasContentAbove: hasContentAbove,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildGenericAttachment(
|
||||
@@ -1887,30 +1945,35 @@ class MessageBubble extends StatelessWidget {
|
||||
);
|
||||
case AttachmentType.call:
|
||||
return CallBubble(ctx: ctx, call: attachment as CallAttachment);
|
||||
case AttachmentType.audio:
|
||||
return Padding(
|
||||
padding: _paddingFor(MessageType.voice, ctx.shape),
|
||||
child: _buildVoiceAttachment(ctx, attachment as AudioAttachment),
|
||||
);
|
||||
default:
|
||||
return _buildTextContent(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildVoiceContent(BubbleContext ctx) {
|
||||
int duration = 0;
|
||||
String url = '';
|
||||
String? waveData;
|
||||
int? audioId;
|
||||
|
||||
AudioAttachment? audio;
|
||||
final attaches = message.attachments;
|
||||
if (attaches != null && attaches.isNotEmpty) {
|
||||
for (final a in attaches) {
|
||||
if (a is AudioAttachment) {
|
||||
duration = ((a.duration ?? 0) / 1000).round();
|
||||
url = a.fileUrl ?? a.baseUrl ?? '';
|
||||
waveData = a.waveform;
|
||||
audioId = a.audioId;
|
||||
audio = a;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return _buildVoiceAttachment(ctx, audio);
|
||||
}
|
||||
|
||||
Widget _buildVoiceAttachment(BubbleContext ctx, AudioAttachment? audio) {
|
||||
var duration = ((audio?.duration ?? 0) / 1000).round();
|
||||
var url = audio?.fileUrl ?? audio?.baseUrl ?? '';
|
||||
|
||||
if (duration == 0 && url.isEmpty) {
|
||||
final payload = message.payload;
|
||||
final voice = payload?['voice'] as Map<String, dynamic>?;
|
||||
@@ -1918,7 +1981,7 @@ class MessageBubble extends StatelessWidget {
|
||||
url = voice?['url']?.toString() ?? '';
|
||||
}
|
||||
|
||||
final cachedTranscription = TranscriptionCache.get(message.id);
|
||||
final cachedTranscription = TranscriptionCache.get(ctx.sourceMessageId);
|
||||
|
||||
return VoiceMessageBubble(
|
||||
duration: duration,
|
||||
@@ -1930,11 +1993,13 @@ class MessageBubble extends StatelessWidget {
|
||||
otherReadTime: otherReadTime,
|
||||
time: message.time,
|
||||
cs: ctx.cs,
|
||||
waveData: waveData,
|
||||
waveData: audio?.waveform,
|
||||
chatId: message.chatId,
|
||||
messageId: message.id,
|
||||
sourceChatId: ctx.sourceChatId,
|
||||
sourceMessageId: ctx.sourceMessageId,
|
||||
senderId: message.senderId,
|
||||
audioId: audioId,
|
||||
audioId: audio?.audioId,
|
||||
preloadedText: cachedTranscription?.text,
|
||||
uploadProgress: ctx.uploadProgress,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user