Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -491,7 +491,8 @@ class _StreamAutocompleteState extends State<StreamAutocomplete> {
_focusNode.dispose();
}
_onChangedField.cancel();
closeSuggestions();
_currentQuery = null;
_currentTrigger = null;
super.dispose();
}

Expand Down Expand Up @@ -551,6 +552,72 @@ const _kDefaultStreamAutocompleteOptionsShape = RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(8)),
);

/// Defines the visual style of autocomplete options overlay.
enum AutocompleteOptionsStyle {
/// Flat overlay with no elevation or margin.
///
/// Used for overlays that appear directly above the composer (default).
fixed,

/// Floating card with elevation and rounded corners.
///
/// Used for overlays that appear in open space away from the composer.
floating,
}

/// Resolves visual parameters for a [StreamAutocompleteOptions] widget based
/// on [AutocompleteOptionsStyle].
extension AutocompleteOptionsStyleX on AutocompleteOptionsStyle {
/// Returns the elevation, margin, and shape for [StreamAutocompleteOptions].
///
/// [borderColor] is used for the top border (fixed) or outline (floating).
({double elevation, EdgeInsetsGeometry margin, ShapeBorder shape}) resolve(
Color borderColor,
) {
return switch (this) {
AutocompleteOptionsStyle.fixed => (
elevation: 0.0,
margin: EdgeInsets.zero,
shape: _TopBorderShape(BorderSide(color: borderColor)),
),
AutocompleteOptionsStyle.floating => (
elevation: 4.0,
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
shape: RoundedRectangleBorder(
borderRadius: const BorderRadius.all(Radius.circular(24)),
side: BorderSide(color: borderColor),
),
),
};
}
}

/// A [ShapeBorder] that paints only a top border, with no rounding or sides.
class _TopBorderShape extends ShapeBorder {
const _TopBorderShape(this.top);

final BorderSide top;

@override
EdgeInsetsGeometry get dimensions => EdgeInsets.only(top: top.width);

@override
Path getInnerPath(Rect rect, {TextDirection? textDirection}) => Path()..addRect(rect);

@override
Path getOuterPath(Rect rect, {TextDirection? textDirection}) => Path()..addRect(rect);

@override
void paint(Canvas canvas, Rect rect, {TextDirection? textDirection}) {
final paint = top.toPaint()..strokeCap = StrokeCap.square;
final y = rect.top + top.width / 2;
canvas.drawLine(Offset(rect.left, y), Offset(rect.right, y), paint);
}

@override
ShapeBorder scale(double t) => _TopBorderShape(top.scale(t));
}

/// A helper widget used to show the options of a [StreamAutocomplete].
class StreamAutocompleteOptions<T extends Object> extends StatelessWidget {
/// Creates a [StreamAutocompleteOptions] widget.
Expand Down Expand Up @@ -617,10 +684,7 @@ class StreamAutocompleteOptions<T extends Object> extends StatelessWidget {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (headerBuilder != null) ...[
headerBuilder!(context),
Divider(height: 0, color: colorTheme.borders),
],
if (headerBuilder != null) headerBuilder!(context),
LimitedBox(
maxHeight: maxHeight ?? height * 0.5,
child: ListView.builder(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/message_input/attachment_picker/options/stream_command_icon.dart';
import 'package:stream_chat_flutter/src/misc/empty_widget.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';

Expand All @@ -12,6 +13,7 @@ class StreamCommandAutocompleteOptions extends StatelessWidget {
required this.query,
required this.channel,
this.onCommandSelected,
this.style = AutocompleteOptionsStyle.fixed,
super.key,
});

Expand All @@ -24,6 +26,11 @@ class StreamCommandAutocompleteOptions extends StatelessWidget {
/// Callback called when a command is selected.
final ValueSetter<Command>? onCommandSelected;

/// The visual style of the autocomplete options overlay.
///
/// Defaults to [AutocompleteOptionsStyle.fixed].
final AutocompleteOptionsStyle style;

@override
Widget build(BuildContext context) {
final commands = channel.config?.commands.where((it) {
Expand All @@ -34,47 +41,51 @@ class StreamCommandAutocompleteOptions extends StatelessWidget {

if (commands == null || commands.isEmpty) return const Empty();

final streamChatTheme = StreamChatTheme.of(context);
final colorTheme = streamChatTheme.colorTheme;
final textTheme = streamChatTheme.textTheme;
final colorScheme = context.streamColorScheme;
final textTheme = context.streamTextTheme;

final (:elevation, :margin, :shape) = style.resolve(colorScheme.borderDefault);

return StreamAutocompleteOptions<Command>(
options: commands,
elevation: elevation,
margin: margin,
shape: shape,
headerBuilder: (context) {
return ListTile(
dense: true,
horizontalTitleGap: 0,
leading: Icon(
context.streamIcons.thunder,
color: colorTheme.accentPrimary,
size: 28,
return Padding(
padding: EdgeInsets.only(
left: context.streamSpacing.sm,
right: context.streamSpacing.sm,
top: context.streamSpacing.md,
bottom: context.streamSpacing.xs,
),
title: Text(
context.translations.instantCommandsLabel,
style: textTheme.body.copyWith(
color: colorTheme.textLowEmphasis,
child: Align(
alignment: .centerStart,
child: Text(
context.translations.instantCommandsLabel,
style: textTheme.headingXs.copyWith(color: colorScheme.textTertiary),
),
),
);
},
optionBuilder: (context, command) {
return ListTile(
dense: true,
horizontalTitleGap: 8,
leading: _CommandIcon(command: command),
title: Row(
horizontalTitleGap: context.streamSpacing.sm,
leading: StreamCommandIcon(command: command),
title: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
command.name.sentenceCase,
style: textTheme.bodyBold.copyWith(
color: colorTheme.textHighEmphasis,
),
style: textTheme.bodyDefault,
),
const SizedBox(width: 8),
SizedBox(height: context.streamSpacing.xxs),
Text(
'/${command.name} ${command.args}',
style: textTheme.body.copyWith(
color: colorTheme.textLowEmphasis,
command.description,
style: textTheme.captionDefault.copyWith(
color: colorScheme.textTertiary,
),
),
],
Expand All @@ -85,95 +96,3 @@ class StreamCommandAutocompleteOptions extends StatelessWidget {
);
}
}

class _CommandIcon extends StatelessWidget {
const _CommandIcon({required this.command});

final Command command;

@override
Widget build(BuildContext context) {
final _streamChatTheme = StreamChatTheme.of(context);
switch (command.name) {
case 'giphy':
return const CircleAvatar(
radius: 12,
child: StreamSvgIcon(
size: 24,
icon: StreamSvgIcons.giphy,
),
);
case 'ban':
return CircleAvatar(
backgroundColor: _streamChatTheme.colorTheme.accentPrimary,
radius: 12,
child: Icon(
context.streamIcons.peopleRemove,
size: 16,
color: Colors.white,
),
);
case 'flag':
return CircleAvatar(
backgroundColor: _streamChatTheme.colorTheme.accentPrimary,
radius: 12,
child: Icon(
context.streamIcons.flag2,
size: 14,
color: Colors.white,
),
);
case 'imgur':
return CircleAvatar(
backgroundColor: _streamChatTheme.colorTheme.accentPrimary,
radius: 12,
child: const ClipOval(
child: StreamSvgIcon(
size: 24,
icon: StreamSvgIcons.imgur,
),
),
);
case 'mute':
return CircleAvatar(
backgroundColor: _streamChatTheme.colorTheme.accentPrimary,
radius: 12,
child: Icon(
context.streamIcons.mute,
size: 16,
color: Colors.white,
),
);
case 'unban':
return CircleAvatar(
backgroundColor: _streamChatTheme.colorTheme.accentPrimary,
radius: 12,
child: Icon(
context.streamIcons.peopleAdd,
size: 16,
color: Colors.white,
),
);
case 'unmute':
return CircleAvatar(
backgroundColor: _streamChatTheme.colorTheme.accentPrimary,
radius: 12,
child: Icon(
context.streamIcons.volumeFull,
size: 16,
color: Colors.white,
),
);
default:
return CircleAvatar(
backgroundColor: _streamChatTheme.colorTheme.accentPrimary,
radius: 12,
child: Icon(
context.streamIcons.thunder,
size: 16,
color: Colors.white,
),
);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class StreamMentionAutocompleteOptions extends StatefulWidget {
this.mentionAllAppUsers = false,
this.mentionsTileBuilder,
this.onMentionUserTap,
this.style = AutocompleteOptionsStyle.fixed,
}) : assert(
channel.state != null,
'Channel ${channel.cid} is not yet initialized',
Expand Down Expand Up @@ -48,6 +49,11 @@ class StreamMentionAutocompleteOptions extends StatefulWidget {
/// Callback called when a user is selected.
final ValueSetter<User>? onMentionUserTap;

/// The visual style of the autocomplete options overlay.
///
/// Defaults to [AutocompleteOptionsStyle.fixed].
final AutocompleteOptionsStyle style;

@override
_StreamMentionAutocompleteOptionsState createState() => _StreamMentionAutocompleteOptionsState();
}
Expand Down Expand Up @@ -81,15 +87,48 @@ class _StreamMentionAutocompleteOptionsState extends State<StreamMentionAutocomp
if (!snapshot.hasData) return const Empty();
final users = snapshot.data!;

final colorScheme = context.streamColorScheme;
final spacing = context.streamSpacing;
final (:elevation, :margin, :shape) = widget.style.resolve(colorScheme.borderDefault);

return StreamAutocompleteOptions<User>(
options: users,
elevation: elevation,
margin: margin,
shape: shape,
optionBuilder: (context, user) {
final colorTheme = StreamChatTheme.of(context).colorTheme;
return Material(
color: colorTheme.barsBg,
final mentionsTileBuilder = widget.mentionsTileBuilder;
if (mentionsTileBuilder != null) {
final colorTheme = StreamChatTheme.of(context).colorTheme;
return Material(
color: colorTheme.barsBg,
child: InkWell(
onTap: widget.onMentionUserTap == null ? null : () => widget.onMentionUserTap!(user),
child: mentionsTileBuilder(context, user),
),
);
}

return Padding(
padding: EdgeInsets.symmetric(horizontal: spacing.xxs),
child: InkWell(
borderRadius: BorderRadius.circular(12),
onTap: widget.onMentionUserTap == null ? null : () => widget.onMentionUserTap!(user),
child: widget.mentionsTileBuilder?.call(context, user) ?? StreamUserMentionTile(user),
child: Padding(
padding: EdgeInsets.symmetric(horizontal: spacing.sm, vertical: spacing.xs),
child: Row(
spacing: spacing.sm,
children: [
StreamUserAvatar(size: .md, user: user),
Text(
user.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: context.streamTextTheme.bodyDefault,
),
],
),
),
),
);
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export 'stream_command_picker.dart';
export 'stream_file_picker.dart';
export 'stream_gallery_picker.dart';
export 'stream_image_picker.dart';
Expand Down
Loading
Loading