Skip to content

Commit be002d2

Browse files
authored
feat(UI): composer attachment reply (#48)
* Add attachment - reply * Add component for reply * Add GestureDetector on RemoveControl * chore: Update Goldens * formatting * rename chat theme to message theme * rename chat_theme file --------- Co-authored-by: renefloor <15101411+renefloor@users.noreply.github.com>
1 parent 2065478 commit be002d2

23 files changed

Lines changed: 1112 additions & 77 deletions

apps/design_system_gallery/lib/app/gallery_app.directories.g.dart

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import 'package:design_system_gallery/components/message_composer/message_compos
1818
as _design_system_gallery_components_message_composer_message_composer;
1919
import 'package:design_system_gallery/components/message_composer/message_composer_attachment_media_file.dart'
2020
as _design_system_gallery_components_message_composer_message_composer_attachment_media_file;
21+
import 'package:design_system_gallery/components/message_composer/message_composer_attachment_reply.dart'
22+
as _design_system_gallery_components_message_composer_message_composer_attachment_reply;
2123
import 'package:design_system_gallery/components/stream_avatar.dart'
2224
as _design_system_gallery_components_stream_avatar;
2325
import 'package:design_system_gallery/components/stream_avatar_group.dart'
@@ -314,6 +316,17 @@ final directories = <_widgetbook.WidgetbookNode>[
314316
),
315317
],
316318
),
319+
_widgetbook.WidgetbookComponent(
320+
name: 'MessageComposerAttachmentReply',
321+
useCases: [
322+
_widgetbook.WidgetbookUseCase(
323+
name: 'Playground',
324+
builder:
325+
_design_system_gallery_components_message_composer_message_composer_attachment_reply
326+
.buildMessageComposerAttachmentReplyPlayground,
327+
),
328+
],
329+
),
317330
_widgetbook.WidgetbookComponent(
318331
name: 'StreamBaseMessageComposer',
319332
useCases: [
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:stream_core_flutter/stream_core_flutter.dart';
3+
import 'package:widgetbook/widgetbook.dart';
4+
import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook;
5+
6+
// =============================================================================
7+
// Playground
8+
// =============================================================================
9+
10+
@widgetbook.UseCase(
11+
name: 'Playground',
12+
type: MessageComposerAttachmentReply,
13+
path: '[Components]/Message Composer',
14+
)
15+
Widget buildMessageComposerAttachmentReplyPlayground(BuildContext context) {
16+
final style = context.knobs.object.dropdown<ReplyStyle>(
17+
label: 'Style',
18+
options: ReplyStyle.values,
19+
initialOption: ReplyStyle.incoming,
20+
labelBuilder: (option) => option.name,
21+
description: 'Incoming uses left-hand bar and incoming colors; outgoing uses right-hand bar and outgoing colors.',
22+
);
23+
24+
return Center(
25+
child: ConstrainedBox(
26+
constraints: const BoxConstraints(maxWidth: 360),
27+
child: MessageComposerAttachmentReply(
28+
title: 'Reply to John Doe',
29+
subtitle: 'We had a great time during our holiday.',
30+
image: const AssetImage('assets/attachment_image.png'),
31+
onRemovePressed: () {},
32+
style: style,
33+
),
34+
),
35+
);
36+
}

packages/stream_core_flutter/lib/src/components/controls/remove_control.dart

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,22 @@ class RemoveControl extends StatelessWidget {
1414
Widget build(BuildContext context) {
1515
final colorScheme = context.streamColorScheme;
1616

17-
return Container(
18-
decoration: BoxDecoration(
19-
color: colorScheme.accentBlack,
20-
shape: BoxShape.circle,
21-
border: Border.all(color: colorScheme.borderOnDark, width: 2),
22-
),
23-
alignment: Alignment.center,
24-
height: 20,
25-
width: 20,
26-
child: Icon(
27-
context.streamIcons.crossSmall,
28-
color: colorScheme.textInverse,
29-
size: 10,
17+
return GestureDetector(
18+
onTap: onPressed,
19+
child: Container(
20+
decoration: BoxDecoration(
21+
color: colorScheme.accentBlack,
22+
shape: BoxShape.circle,
23+
border: Border.all(color: colorScheme.borderOnDark, width: 2),
24+
),
25+
alignment: Alignment.center,
26+
height: 20,
27+
width: 20,
28+
child: Icon(
29+
context.streamIcons.crossSmall,
30+
color: colorScheme.textOnAccent,
31+
size: 10,
32+
),
3033
),
3134
);
3235
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export 'message_composer/attachment/message_composer_attachment_media_file.dart';
2+
export 'message_composer/attachment/message_composer_attachment_reply.dart';
23
export 'message_composer/message_composer.dart';
34
export 'message_composer/message_composer_input.dart';
45
export 'message_composer/message_composer_input_trailing.dart';
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:flutter/widgets.dart';
3+
4+
import '../../../../stream_core_flutter.dart';
5+
import '../../controls/remove_control.dart';
6+
7+
class MessageComposerAttachmentReply extends StatelessWidget {
8+
const MessageComposerAttachmentReply({
9+
super.key,
10+
required this.title,
11+
required this.subtitle,
12+
this.image,
13+
this.onRemovePressed,
14+
this.style = ReplyStyle.incoming,
15+
});
16+
17+
final String title;
18+
final String subtitle;
19+
final ImageProvider? image;
20+
final VoidCallback? onRemovePressed;
21+
final ReplyStyle style;
22+
23+
@override
24+
Widget build(BuildContext context) {
25+
final messageTheme = context.streamMessageTheme;
26+
final messageDefaults = MessageThemeDefaults(context: context).data;
27+
final indicatorColor = switch (style) {
28+
ReplyStyle.incoming => messageTheme.replyIndicatorIncoming ?? messageDefaults.replyIndicatorIncoming!,
29+
ReplyStyle.outgoing => messageTheme.replyIndicatorOutgoing ?? messageDefaults.replyIndicatorOutgoing!,
30+
};
31+
final backgroundColor = switch (style) {
32+
ReplyStyle.incoming => messageTheme.backgroundIncoming ?? messageDefaults.backgroundIncoming,
33+
ReplyStyle.outgoing => messageTheme.backgroundOutgoing ?? messageDefaults.backgroundOutgoing,
34+
};
35+
36+
final spacing = context.streamSpacing;
37+
return Stack(
38+
children: [
39+
Container(
40+
margin: EdgeInsets.all(spacing.xxs),
41+
padding: EdgeInsets.all(spacing.xs),
42+
decoration: BoxDecoration(
43+
color: backgroundColor,
44+
borderRadius: BorderRadius.all(context.streamRadius.lg),
45+
),
46+
child: IntrinsicHeight(
47+
child: Row(
48+
crossAxisAlignment: CrossAxisAlignment.start,
49+
children: [
50+
Container(
51+
margin: const EdgeInsets.only(top: 2, bottom: 2),
52+
color: indicatorColor,
53+
child: const SizedBox(
54+
width: 2,
55+
height: double.infinity,
56+
),
57+
),
58+
SizedBox(width: spacing.xs),
59+
Expanded(
60+
child: Column(
61+
mainAxisSize: MainAxisSize.min,
62+
crossAxisAlignment: CrossAxisAlignment.start,
63+
children: [
64+
Text(title, style: context.streamTextTheme.metadataEmphasis),
65+
Row(
66+
children: [
67+
if (image != null) ...[
68+
Icon(context.streamIcons.camera1, size: 12),
69+
SizedBox(width: spacing.xxs),
70+
],
71+
Expanded(child: Text(subtitle, style: context.streamTextTheme.metadataDefault)),
72+
],
73+
),
74+
],
75+
),
76+
),
77+
if (image != null) ...[
78+
SizedBox(width: spacing.xs),
79+
Container(
80+
width: 40,
81+
height: 40,
82+
decoration: BoxDecoration(
83+
borderRadius: BorderRadius.all(context.streamRadius.md),
84+
image: DecorationImage(image: image!, fit: BoxFit.cover),
85+
),
86+
),
87+
],
88+
],
89+
),
90+
),
91+
),
92+
if (onRemovePressed case final VoidCallback onRemovePressed?)
93+
Align(
94+
alignment: Alignment.topRight,
95+
child: RemoveControl(onPressed: onRemovePressed),
96+
),
97+
],
98+
);
99+
}
100+
}
101+
102+
enum ReplyStyle {
103+
incoming,
104+
outgoing,
105+
}

packages/stream_core_flutter/lib/src/components/message_composer/message_composer.dart

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import 'dart:math' as math;
22

3+
import 'package:flutter/material.dart';
34
import 'package:flutter/widgets.dart';
45

56
import '../../../stream_core_flutter.dart';
7+
import '../../theme/components/stream_input_theme.dart';
8+
import '../../theme/components/stream_message_theme.dart';
69

710
class StreamBaseMessageComposer extends StatefulWidget {
811
const StreamBaseMessageComposer({
@@ -110,3 +113,47 @@ class _StreamBaseMessageComposerState extends State<StreamBaseMessageComposer> {
110113
}
111114

112115
class MessageData {}
116+
117+
class InputThemeDefaults {
118+
InputThemeDefaults({required this.context}) : _colorScheme = context.streamColorScheme;
119+
120+
final BuildContext context;
121+
final StreamColorScheme _colorScheme;
122+
123+
StreamInputThemeData get data => StreamInputThemeData(
124+
textColor: _colorScheme.textPrimary,
125+
placeholderColor: _colorScheme.textTertiary,
126+
disabledColor: _colorScheme.textDisabled,
127+
iconColor: _colorScheme.textTertiary,
128+
borderColor: _colorScheme.borderDefault,
129+
);
130+
}
131+
132+
class MessageThemeDefaults {
133+
MessageThemeDefaults({required this.context}) : _colorScheme = context.streamColorScheme;
134+
135+
final BuildContext context;
136+
final StreamColorScheme _colorScheme;
137+
138+
StreamMessageThemeData get data => StreamMessageThemeData(
139+
backgroundIncoming: _colorScheme.backgroundSurface,
140+
backgroundOutgoing: _colorScheme.brand.shade100,
141+
backgroundAttachmentIncoming: _colorScheme.backgroundSurfaceStrong,
142+
backgroundAttachmentOutgoing: _colorScheme.brand.shade150,
143+
backgroundTypingIndicator: _colorScheme.accentNeutral,
144+
textIncoming: _colorScheme.textPrimary,
145+
textOutgoing: _colorScheme.brand.shade900,
146+
textUsername: _colorScheme.textSecondary,
147+
textTimestamp: _colorScheme.textTertiary,
148+
textMention: _colorScheme.textLink,
149+
textLink: _colorScheme.textLink,
150+
textReaction: _colorScheme.textSecondary,
151+
textSystem: _colorScheme.textSecondary,
152+
borderIncoming: _colorScheme.borderSubtle,
153+
borderOutgoing: _colorScheme.brand.shade100,
154+
borderOnChatIncoming: _colorScheme.borderOnSurface,
155+
borderOnChatOutgoing: _colorScheme.brand.shade300,
156+
replyIndicatorIncoming: _colorScheme.borderOnSurface,
157+
replyIndicatorOutgoing: _colorScheme.brand.shade400,
158+
);
159+
}

packages/stream_core_flutter/lib/src/components/message_composer/message_composer_input.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ class _MessageComposerInputField extends StatelessWidget {
7474
// TODO: fully implement the input field
7575

7676
final composerBorderRadius = context.streamRadius.xxxl;
77+
final inputTheme = context.streamInputTheme;
78+
final inputDefaults = InputThemeDefaults(context: context).data;
7779

7880
final border = OutlineInputBorder(
7981
borderSide: BorderSide.none,
@@ -83,6 +85,9 @@ class _MessageComposerInputField extends StatelessWidget {
8385
return TextField(
8486
controller: controller,
8587
focusNode: focusNode,
88+
style: TextStyle(
89+
color: inputTheme.textColor ?? inputDefaults.textColor,
90+
),
8691
decoration: InputDecoration(
8792
border: border,
8893
focusedBorder: border,
@@ -92,6 +97,9 @@ class _MessageComposerInputField extends StatelessWidget {
9297
fillColor: Colors.transparent,
9398
contentPadding: const EdgeInsets.fromLTRB(12, 8, 12, 8),
9499
hintText: placeholder,
100+
hintStyle: TextStyle(
101+
color: inputTheme.placeholderColor ?? inputDefaults.placeholderColor,
102+
),
95103
),
96104
);
97105
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import 'package:flutter/widgets.dart';
2+
import 'package:theme_extensions_builder_annotation/theme_extensions_builder_annotation.dart';
3+
4+
import '../../../stream_core_flutter.dart';
5+
6+
part 'stream_input_theme.g.theme.dart';
7+
8+
class StreamInputTheme extends InheritedTheme {
9+
const StreamInputTheme({
10+
super.key,
11+
required this.data,
12+
required super.child,
13+
});
14+
15+
final StreamInputThemeData data;
16+
17+
static StreamInputThemeData of(BuildContext context) {
18+
final localTheme = context.dependOnInheritedWidgetOfExactType<StreamInputTheme>();
19+
return StreamTheme.of(context).inputTheme.merge(localTheme?.data);
20+
}
21+
22+
@override
23+
Widget wrap(BuildContext context, Widget child) {
24+
return StreamInputTheme(data: data, child: child);
25+
}
26+
27+
@override
28+
bool updateShouldNotify(StreamInputTheme oldWidget) => data != oldWidget.data;
29+
}
30+
31+
@themeGen
32+
@immutable
33+
class StreamInputThemeData with _$StreamInputThemeData {
34+
const StreamInputThemeData({
35+
this.textColor,
36+
this.placeholderColor,
37+
this.disabledColor,
38+
this.iconColor,
39+
this.borderColor,
40+
});
41+
42+
final Color? textColor;
43+
final Color? placeholderColor;
44+
final Color? disabledColor;
45+
final Color? iconColor;
46+
47+
final Color? borderColor;
48+
}

0 commit comments

Comments
 (0)