A comprehensive Flutter package for building chat interfaces with multiple messaging platform styles (Telegram, WhatsApp, Messenger, iMessage). Provides highly customizable bubble widgets, theming, callbacks, text formatting, and an extensible architecture for custom message types.
Preview |
Telegram |
|
Reactions |
Polls & Location |
RTL Support |
Web & Desktop Support |
||
- Installation
- Quick Start
- Modern Chat Features
- Architecture Overview
- VBubbleScope
- VBubbleConfig
- VBubbleTheme
- VBubbleCallbacks
- Available Bubble Widgets
- Text Formatting & Patterns
- Custom Bubbles
- Context Extensions
- Selection Mode
- Performance Optimizations
Add to your pubspec.yaml:
dependencies:
v_chat_bubbles: ^2.1.1Version 2 requires Flutter 3.44.0 or newer and Dart 3.12.0 or newer.
Then run:
flutter pub getimport 'package:v_chat_bubbles/v_chat_bubbles.dart';
class ChatScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return VBubbleScope(
style: VBubbleStyle.telegram,
config: VBubbleConfig(),
callbacks: VBubbleCallbacks(
onTap: (messageId) => print('Tapped: $messageId'),
onPatternTap: (match) => handlePatternTap(match),
),
child: ListView(
children: [
VTextBubble(
messageId: 'msg_1',
isMeSender: true,
time: '12:30',
text: 'Hello! How are you?',
status: VMessageStatus.read,
),
VTextBubble(
messageId: 'msg_2',
isMeSender: false,
time: '12:31',
text: 'I am fine, thanks!',
senderName: 'John',
avatar: VPlatformFile.fromUrl(networkUrl: 'https://example.com/avatar.jpg'),
),
],
),
);
}
}The package includes optional, backward-compatible models and widgets for current messaging experiences:
- Automatic sender/time grouping with
VMessageGrouping.resolveandVMessageGroupPosition. - Pointer hover actions, secondary-click menus, keyboard shortcuts, high contrast, and reduced-motion behavior.
- Delivery timelines, per-recipient receipts, retry state, edit history, view-once media, spoilers, and expiry countdowns.
- Actor-aware reactions, selective quote ranges, nested replies, threads, translation, voice transcripts, GIFs, and animated stickers.
- Rich link layouts, HD/shared albums, checklists, events, and live location.
final positions = VMessageGrouping.resolve(
messages
.map((m) => VMessageGroupingInfo(
senderId: m.senderId,
sentAt: m.sentAt,
))
.toList(),
timeThreshold: const Duration(minutes: 2),
);
VTextBubble(
messageId: message.id,
isMeSender: message.isMine,
time: '12:30',
text: message.text,
groupPosition: positions[index],
translation: const VMessageTranslationData(
state: VTranslationState.translated,
sourceLanguageCode: 'en',
targetLanguageCode: 'es',
translatedText: 'Hola',
),
)See example/lib/pages/modern_features_page.dart for an integrated showcase.
VBubbleScope (InheritedWidget - provides config to all children)
β
βββ VBubbleStyle (telegram, whatsapp, messenger, imessage, custom)
βββ VBubbleTheme (colors, typography, gradients)
βββ VBubbleConfig (nested configuration objects)
βββ VBubbleCallbacks (event handlers)
βββ Child Widgets
β
βββ BaseBubble (abstract base class)
β
βββ VTextBubble
βββ VImageBubble
βββ VVideoBubble
βββ VVoiceBubble
βββ VFileBubble
βββ VLocationBubble
βββ VContactBubble
βββ VPollBubble
βββ VCallBubble
βββ VGalleryBubble
βββ VCustomBubble<T>
βββ ... more
The root widget that provides configuration to all bubble descendants via InheritedWidget.
| Parameter | Type | Default | Description |
|---|---|---|---|
style |
VBubbleStyle |
telegram |
Visual style preset |
theme |
VBubbleTheme? |
null |
Custom theme (auto-generated if null) |
config |
VBubbleConfig |
VBubbleConfig() |
Configuration object |
callbacks |
VBubbleCallbacks |
VBubbleCallbacks() |
Event callbacks |
isSelectionMode |
bool |
false |
Enable multi-selection |
selectedIds |
Set<String> |
{} |
Currently selected message IDs |
menuItemsBuilder |
VMenuItemsBuilder? |
null |
Dynamic context menu items |
child |
Widget |
required | Child widget tree |
enum VBubbleStyle {
telegram, // Gradient bubbles with distinctive tails
whatsapp, // Green outgoing, white incoming
messenger, // Blue gradient outgoing, gray incoming
imessage, // Blue outgoing, gray incoming, minimal tails
custom, // Fully custom styling
}VBubbleScope(
style: VBubbleStyle.whatsapp,
theme: VBubbleTheme.whatsappLight(),
config: VBubbleConfig.groupChat(),
callbacks: _buildCallbacks(),
isSelectionMode: _isSelectionMode,
selectedIds: _selectedIds,
menuItemsBuilder: (messageId, messageType, isMeSender) {
return isMeSender
? [VDefaultMenuItems.edit, VDefaultMenuItems.delete]
: [VDefaultMenuItems.reply, VDefaultMenuItems.forward];
},
child: ListView.builder(...),
)Nested configuration object controlling all aspects of bubble behavior.
VBubbleConfig({
VPatternConfig patterns,
VGestureConfig gestures,
VAvatarConfig avatar,
VSizingConfig sizing,
VSpacingConfig spacing,
VMediaConfig media,
VTextExpansionConfig textExpansion,
VAnimationConfig animation,
VAccessibilityConfig accessibility,
VTranslationConfig translations,
})VBubbleConfig() // Default configuration
VBubbleConfig.compact() // Dense layout, smaller elements
VBubbleConfig.desktop() // Wider bubbles, more spacing
VBubbleConfig.readOnly() // No gestures (view-only mode)
VBubbleConfig.directChat() // Hidden avatars (1:1 chat)
VBubbleConfig.groupChat() // Always show avatars
VBubbleConfig.minimal() // No tails, clean look (showTails: false)
VBubbleConfig.accessible() // Enhanced accessibility
VBubbleConfig.performance() // No animations, minimal processingHide all bubble tails at once while keeping grouped layout:
VBubbleScope(
style: VBubbleStyle.telegram,
config: const VBubbleConfig(showTails: false), // no tails
child: ListView(...),
)
// Per-bubble tails are derived from VMessageGroupPosition/isSameSender
// when showTails is true (default). When false, every bubble renders
// as a rounded rectangle (uniform for Telegram/WhatsApp/iMessage/Messenger).Case-insensitive substring highlight inside text bubbles (and captions/file names) β like the pink h in hey/huh:
// Theme default (pink #FF3B82 background, white text)
// VBubbleTheme(searchHighlightStyle: TextStyle(backgroundColor: Color(0xFFFF3B82), color: Colors.white))
VTextBubble(
messageId: '1',
isMeSender: true,
time: '12:00',
text: 'hey huh why',
searchQuery: searchQuery, // ""βnone, "h"βhighlights h/H
// searchHighlightStyle: TextStyle(backgroundColor: Colors.orange), // optional override
)
// Example search bar wiring (as in example/lib/pages/chat_demo_page.dart):
// - TextField onChanged β setState(() { searchQuery = value; })
// - Pass same searchQuery to every bubble via MessageBuilder
// - VTextParser.applySearchHighlight preserves link/mention recognizers
// - File/media captions via VMediaOverlayInfo also highlight
---
### VPatternConfig
Controls text pattern detection and formatting.
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `enableLinks` | `bool` | `true` | Detect URLs |
| `enableEmails` | `bool` | `true` | Detect email addresses |
| `enablePhones` | `bool` | `true` | Detect phone numbers |
| `enableMentions` | `bool` | `false` | Detect @username |
| `enableHashtags` | `bool` | `false` | Detect #hashtag |
| `enableFormatting` | `bool` | `false` | Enable *bold*, _italic_, ~strike~, \`code\` |
| `enableCodeBlocks` | `bool` | `false` | Enable \`\`\`code blocks\`\`\` |
| `enableBlockquotes` | `bool` | `false` | Enable > blockquotes |
| `enableBulletLists` | `bool` | `false` | Enable - bullet lists |
| `enableNumberedLists` | `bool` | `false` | Enable 1. numbered lists |
| `customPatterns` | `List<VCustomPattern>?` | `null` | Custom regex patterns |
**Presets:**
```dart
VPatternConfig.standard // Links, emails, phones
VPatternConfig.none // No detection
VPatternConfig.linksOnly // Only URLs
VPatternConfig.withFormatting // Standard + inline formatting
VPatternConfig.markdown // Full markdown support (all patterns)
VPatternConfig.blocksOnly // Only block-level patterns
Custom Pattern Example:
VPatternConfig(
enableLinks: true,
customPatterns: [
VCustomPattern(
id: 'ticket',
pattern: RegExp(r'TKT-\d+'),
style: TextStyle(color: Colors.purple, fontWeight: FontWeight.bold),
isTappable: true,
),
VCustomPattern(
id: 'order',
pattern: RegExp(r'ORD#\d+'),
style: TextStyle(color: Colors.orange),
isTappable: true,
),
],
)Controls gesture interactions.
| Property | Type | Default | Description |
|---|---|---|---|
enableSwipeToReply |
bool |
true |
Swipe right to reply |
enableLongPress |
bool |
true |
Long press for context menu |
enableDoubleTapToReact |
bool |
false |
Double tap to add reaction |
enableHapticFeedback |
bool |
true |
Vibration on interactions |
enableHoverActions |
bool |
true |
Show reply/reaction/retry actions on pointer hover |
enableSecondaryTap |
bool |
true |
Open the message menu with a secondary click |
enableKeyboardShortcuts |
bool |
true |
Enable Enter/Space, Alt+R, and Shift+F10 |
swipeThreshold |
double |
64 |
Swipe distance to trigger reply |
Presets:
VGestureConfig.all // All gestures enabled
VGestureConfig.none // No gestures (read-only)Controls avatar display.
| Property | Type | Default | Description |
|---|---|---|---|
show |
bool |
true |
Show avatars |
position |
VAvatarPosition |
bottom |
Avatar position (top/bottom) |
size |
double |
32 |
Avatar diameter |
Presets:
VAvatarConfig.visible // Show avatars
VAvatarConfig.hidden // Hide avatars
VAvatarConfig.large // Larger avatars (40px)Controls bubble dimensions.
| Property | Type | Default | Description |
|---|---|---|---|
maxWidthFraction |
double |
0.75 |
Max width as fraction of screen |
maxWidth |
double? |
null |
Absolute max width |
minWidth |
double |
80 |
Minimum bubble width |
Presets:
VSizingConfig.standard // 75% max width
VSizingConfig.compact // 65% max width
VSizingConfig.wide // 85% max widthControls spacing and padding.
| Property | Type | Default | Description |
|---|---|---|---|
bubbleRadius |
double |
16 |
Bubble corner radius |
tailSize |
double |
8 |
Bubble tail size |
sameSenderSpacing |
double |
2 |
Space between same-sender messages |
differentSenderSpacing |
double |
8 |
Space between different-sender messages |
contentPaddingHorizontal |
double |
12 |
Horizontal content padding |
contentPaddingVertical |
double |
8 |
Vertical content padding |
Controls media message display.
| Property | Type | Default | Description |
|---|---|---|---|
cornerRadius |
double |
12 |
Media corner radius |
imageMaxHeight |
double |
300 |
Max image height |
videoMaxHeight |
double |
250 |
Max video height |
voiceWaveformHeight |
double |
32 |
Voice waveform height |
fileMessageWidth |
double |
240 |
File bubble width |
gallerySpacing |
double |
2 |
Gallery grid spacing |
Controls expandable text behavior.
| Property | Type | Default | Description |
|---|---|---|---|
enabled |
bool |
true |
Enable text expansion |
characterThreshold |
int |
300 |
Characters before truncation |
enableTextSelection |
bool |
false |
Enable selectable text on mobile |
Controls animation durations.
| Property | Type | Default | Description |
|---|---|---|---|
fadeIn |
Duration |
200ms |
Fade in duration |
fadeOut |
Duration |
150ms |
Fade out duration |
expand |
Duration |
300ms |
Expand animation |
collapse |
Duration |
250ms |
Collapse animation |
highlight |
Duration |
1500ms |
Highlight animation |
swipe |
Duration |
600ms |
Swipe animation |
defaultCurve |
Curve |
easeOutCubic |
Animation curve |
Presets:
VAnimationConfig.standard // Default timings
VAnimationConfig.fast // Snappy animations
VAnimationConfig.slow // Smooth animations
VAnimationConfig.none // No animations (instant)Controls accessibility features.
| Property | Type | Default | Description |
|---|---|---|---|
enableSemanticLabels |
bool |
true |
Screen reader labels |
minTapTargetSize |
double |
48 |
Minimum tap target (44+ recommended) |
enableHighContrast |
bool |
false |
High contrast mode |
semanticLabelBuilder |
Function? |
null |
Custom semantic label builder |
Controls localized strings.
VTranslationConfig(
seeMore: 'See more',
seeLess: 'See less',
edited: 'edited',
forwarded: 'Forwarded',
repliedTo: 'Replied to',
you: 'You',
// ... more
)
// Preset for locale
VTranslationConfig.forLocale(Locale('ar')) // Arabic translations
VTranslationConfig.forLocale(Locale('en')) // English translationsControls colors, typography, and visual styling.
// Style + brightness combinations
VBubbleTheme.telegramLight()
VBubbleTheme.telegramDark()
VBubbleTheme.whatsappLight()
VBubbleTheme.whatsappDark()
VBubbleTheme.messengerLight()
VBubbleTheme.messengerDark()
VBubbleTheme.imessageLight()
VBubbleTheme.imessageDark()
// Get theme by enum
VBubbleTheme.fromStyle(VBubbleStyle.telegram, brightness: Brightness.light)
// Custom theme
VBubbleTheme.custom(
outgoingBubbleColor: Colors.blue,
incomingBubbleColor: Colors.grey[200]!,
accentColor: Colors.blue,
brightness: Brightness.light,
)| Property | Type | Description |
|---|---|---|
outgoingBubbleColor |
Color |
Sent message bubble color |
incomingBubbleColor |
Color |
Received message bubble color |
outgoingTextColor |
Color |
Sent message text color |
incomingTextColor |
Color |
Received message text color |
outgoingBubbleGradient |
Gradient? |
Gradient for sent bubbles |
incomingBubbleGradient |
Gradient? |
Gradient for received bubbles |
outgoingLinkColor |
Color |
Link color in sent messages |
incomingLinkColor |
Color |
Link color in received messages |
messageTextStyle |
TextStyle |
Message body text style |
timeTextStyle |
TextStyle |
Timestamp text style |
senderNameTextStyle |
TextStyle |
Sender name text style |
captionTextStyle |
TextStyle |
Caption text style |
linkTextStyle |
TextStyle |
Link text style |
availableReactions |
List<String> |
Emoji reactions for menu |
readIconColor |
Color |
Read receipt checkmark color |
Event handlers for bubble interactions.
By default, long-pressing a bubble opens the built-in iOS-style context menu (CupertinoContextMenu). You can customize this behavior:
| Scenario | Behavior |
|---|---|
onLongPress not set |
Built-in context menu opens |
onLongPress is set |
Your custom callback is called, built-in menu does NOT open |
// Option 1: Use built-in context menu (default)
VBubbleCallbacks(
// No onLongPress - built-in menu will open
onMenuItemSelected: (messageId, item) {
// Handle menu item selection
},
)
// Option 2: Custom long press handler (replaces built-in menu)
VBubbleCallbacks(
onLongPress: (messageId, position) {
// Show your own menu at position
showMenu(
context: context,
position: RelativeRect.fromLTRB(
position.dx, position.dy, position.dx, position.dy,
),
items: [
PopupMenuItem(child: Text('Reply')),
PopupMenuItem(child: Text('Copy')),
PopupMenuItem(child: Text('Delete')),
],
);
},
)VBubbleCallbacks(
// === Core Callbacks ===
onTap: (String messageId) { },
onLongPress: (String messageId, Offset position) { }, // Replaces built-in menu when set
onSwipeReply: (String messageId) { },
onRetryMessage: (String messageId) { },
onSelectionChanged: (String messageId, bool isSelected) { },
onAvatarTap: (String senderId) { },
onReplyPreviewTap: (String originalMessageId) { },
onThreadTap: (String threadId) { },
// === Grouped Callbacks ===
onReaction: (String messageId, String emoji, VReactionAction action) { },
onReactionTap: (String messageId, String emoji, Offset position) { },
onReactionDetailsTap: (String messageId, VBubbleReaction reaction, Offset position) { },
onPatternTap: (VPatternMatch match) { },
onMediaTap: (VMediaTapData data) { },
onMenuItemSelected: (String messageId, VBubbleMenuItem item) { },
// === Type-Specific Callbacks ===
onPollVote: (String messageId, String optionId) { },
onExpandToggle: (String messageId, bool isExpanded) { },
onTranslationToggle: (String messageId, bool showTranslation) { },
onTranscriptSegmentTap: (String messageId, Duration start) { },
onChecklistItemToggle: (String messageId, String itemId, bool completed) { },
onEventResponse: (String messageId, VEventResponse response) { },
onLiveLocationTap: (String messageId, String sessionId) { },
onDownload: (String messageId) { },
onTransferStateChanged: (String messageId, VMediaTransferAction action) { },
)class VPatternMatch {
final String patternId; // 'url', 'email', 'phone', 'mention', or custom ID
final String matchedText; // The matched text (transformed if applicable)
final String rawText; // Original matched text
final String? messageId; // Parent message ID
}onPatternTap: (match) {
switch (match.patternId) {
case 'url':
launchUrl(Uri.parse(match.matchedText));
break;
case 'email':
launchUrl(Uri.parse('mailto:${match.matchedText}'));
break;
case 'phone':
launchUrl(Uri.parse('tel:${match.matchedText}'));
break;
case 'mention':
navigateToUser(match.matchedText);
break;
case 'ticket': // Custom pattern
openTicket(match.matchedText);
break;
}
}All bubble widgets share these properties:
| Property | Type | Required | Description |
|---|---|---|---|
messageId |
String |
Yes | Unique message identifier |
isMeSender |
bool |
Yes | True if current user sent |
time |
String |
Yes | Display time (e.g., "12:30") |
status |
VMessageStatus? |
No | Delivery status |
isSameSender |
bool |
No | Same sender as previous message |
groupPosition |
VMessageGroupPosition? |
No | Resolved single/first/middle/last group position |
avatar |
VPlatformFile? |
No | Sender avatar image |
senderName |
String? |
No | Sender display name |
senderColor |
Color? |
No | Sender name color |
replyTo |
VReplyData? |
No | Reply preview data |
forwardedFrom |
VForwardData? |
No | Forward info |
reactions |
List<VBubbleReaction> |
No | Message reactions |
isEdited |
bool |
No | Show "edited" label |
lifecycle |
VMessageLifecycleData? |
No | Receipts, retry state, and edit history |
isPinned |
bool |
No | Show pin indicator |
isStarred |
bool |
No | Show star indicator |
isHighlighted |
bool |
No | Highlight animation |
VTextBubble(
messageId: 'msg_1',
isMeSender: true,
time: '12:30',
text: 'Hello *world*! Check https://flutter.dev',
linkPreview: VLinkPreviewData(
url: 'https://flutter.dev',
title: 'Flutter',
description: 'Build apps for any screen',
layout: VLinkPreviewLayout.sideMedia,
image: VPlatformFile.fromUrl(networkUrl: '...'),
),
status: VMessageStatus.read,
translation: VMessageTranslationData(
state: VTranslationState.translated,
sourceLanguageCode: 'en',
targetLanguageCode: 'es',
translatedText: 'Hola mundo',
),
)VImageBubble(
messageId: 'msg_2',
isMeSender: false,
time: '12:31',
imageFile: VPlatformFile.fromUrl(networkUrl: 'https://example.com/image.jpg'),
caption: 'Beautiful sunset!',
aspectRatio: 16/9,
protection: VContentProtectionData.viewOnce(),
)VVideoBubble(
messageId: 'msg_3',
isMeSender: true,
time: '12:32',
videoFile: VPlatformFile.fromUrl(
networkUrl: 'https://example.com/video.mp4',
fileSize: 15728640, // 15 MB
),
thumbnailFile: VPlatformFile.fromUrl(networkUrl: '...'),
duration: Duration(minutes: 2, seconds: 30),
caption: 'Check this out!',
)VVoiceBubble(
messageId: 'msg_4',
isMeSender: false,
time: '12:33',
controller: VVoiceMessageController(
id: 'msg_4',
audioSrc: VPlatformFile.fromUrl(
networkUrl: 'https://example.com/voice.mp3',
),
maxDuration: Duration(minutes: 1, seconds: 30),
),
transcript: VVoiceTranscriptData(
state: VTranscriptState.ready,
text: 'Transcribed voice message',
isExpanded: true,
),
)VFileBubble(
messageId: 'msg_5',
isMeSender: true,
time: '12:34',
file: VPlatformFile.fromUrl(
networkUrl: 'https://example.com/document.pdf',
fileSize: 2457600, // 2.4 MB
),
transferState: VTransferState.completed,
)VLocationBubble(
messageId: 'msg_6',
isMeSender: false,
time: '12:35',
locationData: VLocationData(
latitude: 40.7128,
longitude: -74.0060,
address: 'New York, NY',
staticMapUrl: 'https://maps.example.com/static/...',
),
)VContactBubble(
messageId: 'msg_7',
isMeSender: true,
time: '12:36',
contactData: VContactData(
name: 'John Smith',
phoneNumber: '+1-555-123-4567',
avatar: VPlatformFile.fromUrl(networkUrl: '...'),
),
)VPollBubble(
messageId: 'msg_8',
isMeSender: false,
time: '12:37',
pollData: VPollData(
question: 'What is your favorite framework?',
options: [
VPollOption(id: '1', text: 'Flutter', voteCount: 150, percentage: 60),
VPollOption(id: '2', text: 'React Native', voteCount: 75, percentage: 30),
VPollOption(id: '3', text: 'Other', voteCount: 25, percentage: 10),
],
totalVotes: 250,
hasVoted: true,
mode: VPollMode.single,
),
)VCallBubble(
messageId: 'msg_9',
isMeSender: true,
time: '12:38',
callData: VCallData(
type: VCallType.video,
status: VCallStatus.completed,
duration: Duration(minutes: 5, seconds: 30),
),
)VGalleryBubble(
messageId: 'msg_10',
isMeSender: false,
time: '12:39',
items: [
VGalleryItemData(messageId: 'img_1', file: VPlatformFile.fromUrl(...), time: '12:39'),
VGalleryItemData(messageId: 'img_2', file: VPlatformFile.fromUrl(...), time: '12:39'),
VGalleryItemData(messageId: 'img_3', file: VPlatformFile.fromUrl(...), time: '12:39'),
],
collection: VMediaCollectionData(
collectionId: 'trip',
title: 'Summer trip',
totalItemCount: 12,
quality: VMediaQuality.highDefinition,
canAddItems: true,
),
)Use VGifBubble for controlled animated media, VChecklistBubble for shared
tasks, VEventBubble for RSVP workflows, and VLiveLocationBubble for an
expiring location session. All interaction state is controlled through their
data models and VBubbleCallbacks, so server state remains authoritative.
VChecklistBubble(
messageId: 'tasks',
isMeSender: false,
time: '12:40',
checklist: const VChecklistData(
title: 'Release checklist',
items: [
VChecklistItem(id: 'test', text: 'Run tests', isCompleted: true),
VChecklistItem(id: 'notes', text: 'Review release notes'),
],
),
)VSystemBubble(text: 'John joined the group')VDateChip(date: 'Today')VDeletedBubble(
isMeSender: true,
time: '12:40',
)| Format | Syntax | Output |
|---|---|---|
| Bold | *text* |
text |
| Italic | _text_ |
text |
| Strikethrough | ~text~ |
|
| Inline Code | `code` |
code |
| Code Block | ```code``` |
Syntax highlighted block |
| Blockquote | > text |
Indented quote |
| Bullet List | - item |
Bulleted list |
| Numbered List | 1. item |
Numbered list |
VBubbleConfig(
patterns: VPatternConfig(
enableFormatting: true, // Bold, italic, etc.
enableCodeBlocks: true, // ```code```
enableBlockquotes: true, // > quotes
enableBulletLists: true, // - items
enableNumberedLists: true, // 1. items
),
)
// Or use preset
VBubbleConfig(patterns: VPatternConfig.markdown)Create custom bubble widgets using VCustomBubble or by extending BaseBubble.
// 1. Define your data model
@immutable
class VPaymentData extends VCustomBubbleData {
final double amount;
final String currency;
const VPaymentData({required this.amount, this.currency = 'USD'});
@override
String get contentType => 'payment';
}
// 2. Use VCustomBubble with a builder
VCustomBubble<VPaymentData>(
messageId: 'msg_payment_1',
isMeSender: true,
time: '12:30',
data: VPaymentData(amount: 99.99),
builder: (context, data) {
final theme = context.bubbleTheme;
final textColor = theme.outgoingTextColor;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.payment, color: Colors.green),
SizedBox(width: 8),
Text('Payment', style: TextStyle(color: textColor, fontWeight: FontWeight.bold)),
],
),
SizedBox(height: 8),
Text('\$${data.amount}', style: TextStyle(color: textColor, fontSize: 24)),
],
);
},
)class VPaymentBubble extends BaseBubble {
final VPaymentData paymentData;
@override
String get messageType => 'payment';
const VPaymentBubble({
super.key,
required super.messageId,
required super.isMeSender,
required super.time,
required this.paymentData,
super.status,
super.isSameSender,
super.avatar,
super.senderName,
super.senderColor,
});
@override
Widget buildContent(BuildContext context) {
final theme = context.bubbleTheme;
final textColor = selectTextColor(theme);
return buildBubbleContainer(
context: context,
showTail: !isSameSender,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
Icon(Icons.payment, color: textColor),
SizedBox(width: 8),
Text('Payment', style: TextStyle(color: textColor, fontWeight: FontWeight.bold)),
],
),
SizedBox(height: 8),
Text(
'\$${paymentData.amount.toStringAsFixed(2)}',
style: TextStyle(color: textColor, fontSize: 24, fontWeight: FontWeight.w600),
),
SizedBox(height: 8),
buildMeta(context),
],
),
);
}
}| Method | Description |
|---|---|
buildBubbleContainer() |
Wraps content with styled bubble shape |
buildBubbleHeader() |
Forward header + sender name + reply preview |
buildMeta() |
Timestamp + status + flags |
buildTimestamp() |
Just timestamp widget |
buildStatusIcon() |
Just status indicator |
buildReactionsWidget() |
Reaction pills row |
selectTextColor(theme) |
Get correct text color |
selectSecondaryTextColor(theme) |
Get secondary text color |
selectLinkColor(theme) |
Get link/accent color |
Access scope data from any descendant widget:
// Full scope
final scope = context.bubbleScope;
// Individual properties
final theme = context.bubbleTheme;
final config = context.bubbleConfig;
final callbacks = context.bubbleCallbacks;
final style = context.bubbleStyle;
// Expansion state
final expandManager = context.expandStateManager;
final isExpanded = expandManager.isExpanded('msg_123');
// Custom builders
final hasBuilder = context.hasCustomBubbleBuilder('payment');
final builder = context.getCustomBubbleBuilder('payment');Enable multi-selection for bulk operations:
class _ChatState extends State<ChatScreen> {
bool _isSelectionMode = false;
final Set<String> _selectedIds = {};
@override
Widget build(BuildContext context) {
return VBubbleScope(
isSelectionMode: _isSelectionMode,
selectedIds: _selectedIds,
callbacks: VBubbleCallbacks(
onSelectionChanged: (messageId, isSelected) {
setState(() {
if (isSelected) {
_selectedIds.add(messageId);
_isSelectionMode = true;
} else {
_selectedIds.remove(messageId);
if (_selectedIds.isEmpty) _isSelectionMode = false;
}
});
},
),
menuItemsBuilder: (messageId, messageType, isMeSender) {
return [...VDefaultMenuItems.textDefaults]; // Includes select option
},
child: ...,
);
}
}VDefaultMenuItems.reply
VDefaultMenuItems.forward
VDefaultMenuItems.copy
VDefaultMenuItems.delete
VDefaultMenuItems.download
VDefaultMenuItems.pin
VDefaultMenuItems.star
VDefaultMenuItems.select // Triggers selection mode
VDefaultMenuItems.edit
VDefaultMenuItems.report
// Preset lists
VDefaultMenuItems.textDefaults // reply, forward, copy, edit, pin, delete, select
VDefaultMenuItems.mediaDefaults // reply, forward, download, pin, deleteThe package includes several built-in optimizations:
- Span caching: Parsed text spans cached per message
- Pattern caching: Built pattern lists cached per config
- Block widget caching: Block-level widgets cached
- Text direction caching: RTL/LTR detection cached
- shouldRepaint optimization: Bubble painters only repaint when properties change
- IntrinsicWidth wrapping: Bubbles shrink-wrap to content
- Lazy parsing: Block patterns only parsed when enabled
- Fast path detection: Quick checks before expensive regex operations
// Use performance preset for very long lists
VBubbleConfig.performance()
// Disable unused patterns
VPatternConfig(
enableLinks: true,
enableEmails: false, // Disable if not needed
enablePhones: false,
enableFormatting: false,
)
// Use appropriate image sizes
VPlatformFile.fromUrl(
networkUrl: thumbnailUrl, // Use thumbnails, not full images
)MIT License - see LICENSE file for details.
Hatem Ragap π§ Email: hatemragapdev@gmail.com π GitHub: github.com/hatemragab
For issues, feature requests, or questions:
- π§ Email: hatemragapdev@gmail.com
- π GitHub Issues: Create an issue
- β Star us on GitHub if you find this package useful!






