From f4bb93b39fda82d9ffebc3db074363544c27fec4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B0=D1=88=D0=B5=20=D0=98=D0=BC=D1=8F?= Date: Sun, 5 Apr 2026 23:39:04 +0700 Subject: [PATCH] =?UTF-8?q?=D1=8F=20=D1=83=D0=BC=D0=B5=D1=80=20=D0=BF?= =?UTF-8?q?=D0=BE=D0=BA=D0=B0=20=D1=84=D0=B8=D0=BA=D1=81=D0=B8=D0=BB=20?= =?UTF-8?q?=D1=84=D1=80=D0=BE=D0=BD=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/frontend/widgets/message_bubble.dart | 150 ++++++++++++----------- lib/models/attachment.dart | 56 ++++++++- 2 files changed, 131 insertions(+), 75 deletions(-) diff --git a/lib/frontend/widgets/message_bubble.dart b/lib/frontend/widgets/message_bubble.dart index e6b213a..c3a5b62 100644 --- a/lib/frontend/widgets/message_bubble.dart +++ b/lib/frontend/widgets/message_bubble.dart @@ -21,9 +21,6 @@ class MessageBubble extends StatelessWidget { return false; final hasPhoto = message.attachments!.any((a) => a is PhotoAttachment); final hasCaption = message.text != null && message.text!.isNotEmpty; - debugPrint( - 'DEBUG _hasPhotoWithCaption: hasPhoto=$hasPhoto, hasCaption=$hasCaption, text="${message.text}", shape=$shape', - ); return hasPhoto && hasCaption; } @@ -81,6 +78,7 @@ class MessageBubble extends StatelessWidget { MessageType get contentType { if (message.attachments != null && message.attachments!.isNotEmpty) { final first = message.attachments!.first; + if (first is UnknownAttachment) return MessageType.text; if (first.type == AttachmentType.audio) return MessageType.voice; return MessageType.attachment; } @@ -251,16 +249,7 @@ class MessageBubble extends StatelessWidget { return const EdgeInsets.symmetric(horizontal: 14, vertical: 10); } case MessageType.attachment: - switch (shape) { - case BubbleShape.groupedMiddle: - return const EdgeInsets.all(0); - case BubbleShape.singleTop: - return const EdgeInsets.all(0); - case BubbleShape.singleBottom: - return const EdgeInsets.all(0); - case BubbleShape.singleMiddle: - return const EdgeInsets.all(0); - } + return const EdgeInsets.all(0); case MessageType.voice: switch (shape) { case BubbleShape.groupedMiddle: @@ -395,7 +384,12 @@ class MessageBubble extends StatelessWidget { } if (count == 1) { - return IntrinsicWidth( + final photo = photos[0]; + final pw = photo.width?.toDouble() ?? 200; + final photoWidth = pw.clamp(photoMinSize, photoMaxSize); + + return SizedBox( + width: photoWidth, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, @@ -410,7 +404,7 @@ class MessageBubble extends StatelessWidget { child: Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ - Expanded(child: _buildCaption(ctx)), + Flexible(child: _buildCaption(ctx)), _buildMeta(ctx), ], ), @@ -744,68 +738,82 @@ class MessageBubble extends StatelessWidget { ? Colors.white : (isDark ? cs.onSurface : const Color(0xFF1C1C1E)); final subtitleColor = isMe - ? Colors.white.withValues(alpha: 0.7) + ? Colors.white.withValues(alpha: 0.65) : (isDark ? cs.onSurfaceVariant : const Color(0xFF8E8E93)); - return Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Container( - width: 220, - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: isMe - ? Colors.white.withValues(alpha: 0.15) - : (isDark ? cs.surface : const Color(0xFFFFFFFF)), - borderRadius: BorderRadius.circular(12), + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), + child: Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + width: 38, + height: 38, + decoration: BoxDecoration( + color: isMe + ? Colors.white.withValues(alpha: 0.2) + : cs.primaryContainer, + borderRadius: BorderRadius.circular(10), + ), + child: Icon( + Symbols.description, + color: isMe ? Colors.white : cs.primary, + size: 20, + ), ), - child: Row( - children: [ - Container( - width: 40, - height: 40, - decoration: BoxDecoration( - color: isMe - ? Colors.white.withValues(alpha: 0.2) - : cs.primaryContainer, - borderRadius: BorderRadius.circular(8), + const SizedBox(width: 10), + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + name, + style: TextStyle( + color: textColor, + fontSize: 14, + fontWeight: FontWeight.w500, + height: 1.2, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, ), - child: Icon( - Symbols.file_download, - color: isMe ? Colors.white : cs.primary, - size: 22, + const SizedBox(height: 2), + Text( + sizeStr, + style: TextStyle( + color: subtitleColor, + fontSize: 12, + height: 1.2, + ), ), - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - name, - style: TextStyle( - color: textColor, - fontSize: 14, - fontWeight: FontWeight.w500, - ), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - const SizedBox(height: 2), - Text( - 'Скачать • $sizeStr', - style: TextStyle(color: subtitleColor, fontSize: 12), - ), - ], - ), - ), - ], + ], + ), ), - ), - _buildMeta(ctx), - ], + const SizedBox(width: 12), + GestureDetector( + onTap: () {}, + child: Container( + width: 34, + height: 34, + decoration: BoxDecoration( + color: isMe + ? Colors.white.withValues(alpha: 0.15) + : (isDark + ? cs.surfaceContainerHighest + : const Color(0xFFE5E5EA)), + shape: BoxShape.circle, + ), + child: Icon( + Symbols.download, + color: isMe ? Colors.white : cs.primary, + size: 18, + ), + ), + ), + ], + ), ); } diff --git a/lib/models/attachment.dart b/lib/models/attachment.dart index 2270210..dbaf79e 100644 --- a/lib/models/attachment.dart +++ b/lib/models/attachment.dart @@ -41,6 +41,10 @@ abstract class MessageAttachment { return LocationAttachment.fromMap(map); case 'CONTROL': return ControlAttachment.fromMap(map); + case 'SHARE': + return FileAttachment.fromMap(map); + case 'INLINE_KEYBOARD': + return UnknownAttachment(map); default: return UnknownAttachment(map); } @@ -125,8 +129,19 @@ class VideoAttachment extends MessageAttachment { }) : super(type: AttachmentType.video); factory VideoAttachment.fromMap(Map map) { + String? previewStr; + final previewRaw = map['previewData']; + if (previewRaw is String) { + previewStr = previewRaw; + } else if (previewRaw is List) { + try { + final bytes = List.from(previewRaw); + previewStr = String.fromCharCodes(bytes); + } catch (_) {} + } + return VideoAttachment( - previewData: map['previewData'] as String?, + previewData: previewStr, baseUrl: map['baseUrl'] as String?, videoId: map['videoId'] as int?, videoToken: map['videoToken'] as String?, @@ -170,8 +185,19 @@ class AudioAttachment extends MessageAttachment { }) : super(type: AttachmentType.audio); factory AudioAttachment.fromMap(Map map) { + String? previewStr; + final previewRaw = map['previewData']; + if (previewRaw is String) { + previewStr = previewRaw; + } else if (previewRaw is List) { + try { + final bytes = List.from(previewRaw); + previewStr = String.fromCharCodes(bytes); + } catch (_) {} + } + return AudioAttachment( - previewData: map['previewData'] as String?, + previewData: previewStr, baseUrl: map['baseUrl'] as String?, audioId: map['audioId'] as int?, audioToken: map['audioToken'] as String?, @@ -211,8 +237,19 @@ class FileAttachment extends MessageAttachment { }) : super(type: AttachmentType.file); factory FileAttachment.fromMap(Map map) { + String? previewStr; + final previewRaw = map['previewData']; + if (previewRaw is String) { + previewStr = previewRaw; + } else if (previewRaw is List) { + try { + final bytes = List.from(previewRaw); + previewStr = String.fromCharCodes(bytes); + } catch (_) {} + } + return FileAttachment( - previewData: map['previewData'] as String?, + previewData: previewStr, baseUrl: map['baseUrl'] as String?, fileId: map['fileId'] as int?, fileToken: map['fileToken'] as String?, @@ -250,8 +287,19 @@ class StickerAttachment extends MessageAttachment { }) : super(type: AttachmentType.sticker); factory StickerAttachment.fromMap(Map map) { + String? previewStr; + final previewRaw = map['previewData']; + if (previewRaw is String) { + previewStr = previewRaw; + } else if (previewRaw is List) { + try { + final bytes = List.from(previewRaw); + previewStr = String.fromCharCodes(bytes); + } catch (_) {} + } + return StickerAttachment( - previewData: map['previewData'] as String?, + previewData: previewStr, baseUrl: map['baseUrl'] as String?, stickerId: map['stickerId'] as String?, stickerPackId: map['stickerPackId'] as String?,