Skip to content
Merged
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ import 'package:design_system_gallery/components/accessories/stream_file_type_ic
as _design_system_gallery_components_accessories_stream_file_type_icons;
import 'package:design_system_gallery/components/button.dart'
as _design_system_gallery_components_button;
import 'package:design_system_gallery/components/message_composer/message_composer.dart'
as _design_system_gallery_components_message_composer_message_composer;
import 'package:design_system_gallery/components/message_composer/message_composer_attachment_media_file.dart'
as _design_system_gallery_components_message_composer_message_composer_attachment_media_file;
import 'package:design_system_gallery/components/stream_avatar.dart'
as _design_system_gallery_components_stream_avatar;
import 'package:design_system_gallery/components/stream_avatar_group.dart'
Expand Down Expand Up @@ -296,6 +300,39 @@ final directories = <_widgetbook.WidgetbookNode>[
),
],
),
_widgetbook.WidgetbookFolder(
name: 'Message Composer',
children: [
_widgetbook.WidgetbookComponent(
name: 'MessageComposerAttachmentMediaFile',
useCases: [
_widgetbook.WidgetbookUseCase(
name: 'Playground',
builder:
_design_system_gallery_components_message_composer_message_composer_attachment_media_file
.buildMessageComposerAttachmentMediaFilePlayground,
),
],
),
_widgetbook.WidgetbookComponent(
name: 'StreamBaseMessageComposer',
useCases: [
_widgetbook.WidgetbookUseCase(
name: 'Playground',
builder:
_design_system_gallery_components_message_composer_message_composer
.buildStreamMessageComposerPlayground,
),
_widgetbook.WidgetbookUseCase(
name: 'Real-world Example',
builder:
_design_system_gallery_components_message_composer_message_composer
.buildStreamMessageComposerExample,
),
],
),
],
),
],
),
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
import 'package:flutter/material.dart';
import 'package:stream_core_flutter/stream_core_flutter.dart';
import 'package:widgetbook/widgetbook.dart';
import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook;

// =============================================================================
// Playground
// =============================================================================

@widgetbook.UseCase(
name: 'Playground',
type: StreamBaseMessageComposer,
path: '[Components]/Message Composer',
)
Widget buildStreamMessageComposerPlayground(BuildContext context) {
final textEditingController = TextEditingController();

return Center(
child: StreamBaseMessageComposer(
controller: textEditingController,
isFloating: false,
inputTrailing: StreamMessageComposerInputTrailing(
controller: textEditingController,
onSendPressed: () {},
onMicrophonePressed: () {},
),
),
);
}

// =============================================================================
// Real-world Example
// =============================================================================

@widgetbook.UseCase(
name: 'Real-world Example',
type: StreamBaseMessageComposer,
path: '[Components]/Message Composer',
)
Widget buildStreamMessageComposerExample(BuildContext context) {
final theme = StreamTheme.of(context);
final colorScheme = theme.colorScheme;
final textTheme = theme.textTheme;

final isFloating = context.knobs.boolean(
label: 'Floating',
description: 'When true, the composer has no background or border.',
);

// Sample messages for scrollable list
const messages = [
(message: 'Hey! How are you doing today?', isMe: false),
(message: "I'm doing great, thanks for asking!", isMe: true),
(message: 'Did you see the new design updates?', isMe: false),
(message: 'Yes! They look amazing. Great work on the color scheme.', isMe: true),
(message: 'Thanks! We spent a lot of time on the details.', isMe: false),
(message: 'It really shows. The typography is much cleaner now.', isMe: true),
(message: 'Glad you like it! Any feedback?', isMe: false),
(message: 'Maybe we could add more spacing in some areas?', isMe: true),
(message: "Good point, I'll look into that.", isMe: false),
(message: 'Perfect! Let me know if you need any help.', isMe: true),
(message: 'Should be finished by tomorrow.', isMe: false),
(message: 'Great! Thanks for the update.', isMe: true),
(message: "No problem! You're welcome.", isMe: false),
(message: 'I need to go now. See you later!', isMe: false),
(message: 'Bye! Take care.', isMe: true),
(message: 'Thanks! You too!', isMe: false),
(message: 'See you soon!', isMe: true),
(message: 'Bye!', isMe: false),
(message: 'See you soon!', isMe: true),
];

final textEditingController = TextEditingController();

return Scaffold(
appBar: AppBar(
title: Row(
children: [
StreamAvatar(
size: StreamAvatarSize.sm,
placeholder: (context) => const Text('JD'),
),
const SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'John Doe',
style: textTheme.bodyEmphasis.copyWith(
color: colorScheme.textPrimary,
),
),
Text(
'Online',
style: textTheme.captionDefault.copyWith(
color: colorScheme.accentSuccess,
),
),
],
),
],
),
),
body: isFloating
? Stack(
children: [
// Scrollable messages area (with bottom padding for composer)
ListView.builder(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 250),
itemCount: messages.length,
itemBuilder: (context, index) {
final msg = messages[index];
return Padding(
padding: EdgeInsets.only(
bottom: index < messages.length - 1 ? 8 : 0,
),
child: _MessageBubble(
message: msg.message,
isMe: msg.isMe,
),
);
},
),
// Floating composer at bottom
Positioned(
left: 0,
right: 0,
bottom: 0,
child: StreamBaseMessageComposer(
controller: textEditingController,
isFloating: true,
inputTrailing: StreamMessageComposerInputTrailing(
controller: textEditingController,
onSendPressed: () {},
onMicrophonePressed: () {},
),
),
),
],
)
: Column(
children: [
// Scrollable messages area
Expanded(
child: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: messages.length,
itemBuilder: (context, index) {
final msg = messages[index];
return Padding(
padding: EdgeInsets.only(
bottom: index < messages.length - 1 ? 8 : 0,
),
child: _MessageBubble(
message: msg.message,
isMe: msg.isMe,
),
);
},
),
),
// Non-floating composer
StreamBaseMessageComposer(
controller: textEditingController,
isFloating: false,
inputTrailing: StreamMessageComposerInputTrailing(
controller: textEditingController,
onSendPressed: () {},
onMicrophonePressed: () {},
),
),
],
),
);
}

class _MessageBubble extends StatelessWidget {
const _MessageBubble({
required this.message,
required this.isMe,
});

final String message;
final bool isMe;

@override
Widget build(BuildContext context) {
final theme = StreamTheme.of(context);
final colorScheme = theme.colorScheme;
final textTheme = theme.textTheme;

return Align(
alignment: isMe ? Alignment.centerRight : Alignment.centerLeft,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: isMe ? colorScheme.accentPrimary : colorScheme.backgroundApp,
borderRadius: BorderRadius.circular(16),
border: isMe ? null : Border.all(color: colorScheme.borderSubtle),
),
child: Text(
message,
style: textTheme.bodyDefault.copyWith(
color: isMe ? colorScheme.textOnAccent : colorScheme.textPrimary,
),
),
),
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import 'package:flutter/material.dart';
import 'package:stream_core_flutter/stream_core_flutter.dart';
import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook;

// =============================================================================
// Playground
// =============================================================================

@widgetbook.UseCase(
name: 'Playground',
type: MessageComposerAttachmentMediaFile,
path: '[Components]/Message Composer',
)
Widget buildMessageComposerAttachmentMediaFilePlayground(BuildContext context) {
return Center(
child: MessageComposerAttachmentMediaFile(
image: const AssetImage('assets/attachment_image.png'),
onRemovePressed: () {},
),
);
}
3 changes: 2 additions & 1 deletion packages/stream_core_flutter/lib/src/components.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ export 'components/avatar/stream_avatar.dart' hide DefaultStreamAvatar;
export 'components/avatar/stream_avatar_group.dart' hide DefaultStreamAvatarGroup;
export 'components/avatar/stream_avatar_stack.dart' hide DefaultStreamAvatarStack;
export 'components/badge/stream_badge_count.dart' hide DefaultStreamBadgeCount;
export 'components/badge/stream_online_indicator.dart' hide DefaultStreamOnlineIndicator;
export 'components/buttons/stream_button.dart' hide DefaultStreamButton;
export 'components/indicator/stream_online_indicator.dart' hide DefaultStreamOnlineIndicator;
export 'components/message_composer.dart';

export 'factory/stream_component_factory.dart';
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import 'package:flutter/widgets.dart';

import '../../../stream_core_flutter.dart';

class MediaBadge extends StatelessWidget {
const MediaBadge({
super.key,
required this.type,
required this.duration,
});

final MediaBadgeType type;
final Duration duration;

@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: context.streamColorScheme.backgroundInverse,
shape: BoxShape.circle,
),
padding: EdgeInsets.symmetric(
horizontal: context.streamSpacing.xs,
vertical: context.streamSpacing.xxs,
),
child: Column(
children: [
Icon(
switch (type) {
MediaBadgeType.video => context.streamIcons.videoSolid,
MediaBadgeType.audio => context.streamIcons.microphoneSolid,
},
size: 12,
color: context.streamColorScheme.textPrimary,
),

Text(duration.toReadableString()),
],
),
);
}
}

extension on Duration {
String toReadableString() {
if (inSeconds < 60) {
return '${inSeconds}s';
}
if (inSeconds < 3600) {
return '${inMinutes}m';
}
return '${inHours}h';
}
}

enum MediaBadgeType {
video,
audio,
}
Loading