Repository navigation
fix(ui): iPhone Duo polish: picker height, list states, full-width suggestions #3021
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
78b449c
fb0aea2
1d98505
a78f940
d2e6a78
91dd54d
4191616
c33f86f
c91ba57
dd48cd2
a552d1d
a5f716e
ad01254
d9e226a
516a738
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -77,6 +77,13 @@ class StreamSystemAttachmentPicker extends StatelessWidget { | |
| // The height of the attachment picker when using the tabbed interface. | ||
| const _kTabbedAttachmentPickerHeight = 260.0; | ||
|
|
||
| // The height of the attachment picker when using the tabbed interface on | ||
| // compact-height screens. | ||
| const _kCompactTabbedAttachmentPickerHeight = 158.0; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think 158 is too short for every tab except the gallery. The file, camera, video and poll tabs wrap their content in So on a phone in landscape, switching to Files/Camera/Video/Poll should still hit a ~38px RenderFlex overflow, which is the overflow this PR is fixing. Only the gallery tab (
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Confirmed: on the iPhone Duo in landscape the file, camera, video and poll tabs overflowed by 45px at 158. Fixed in d9e226a by giving those four tabs only the side margin. Their prompt is wrapped in a |
||
|
|
||
| // Screens shorter than this have a compact height, such as a phone in landscape. | ||
| const _kCompactHeightBreakpoint = 480.0; | ||
|
|
||
| /// Inline widget for the tabbed attachment picker interface. | ||
| /// | ||
| /// Displays a tabbed interface with horizontal tabs for different attachment | ||
|
|
@@ -143,6 +150,11 @@ class _StreamTabbedAttachmentPickerState extends State<StreamTabbedAttachmentPic | |
| return ValueListenableBuilder<AttachmentPickerValue>( | ||
| valueListenable: widget.controller, | ||
| builder: (context, value, _) { | ||
| final pickerHeight = switch (MediaQuery.sizeOf(context)) { | ||
| Size(height: < _kCompactHeightBreakpoint) => _kCompactTabbedAttachmentPickerHeight, | ||
| _ => _kTabbedAttachmentPickerHeight, | ||
| }; | ||
|
|
||
| return Column( | ||
| crossAxisAlignment: CrossAxisAlignment.start, | ||
| mainAxisSize: MainAxisSize.min, | ||
|
|
@@ -158,7 +170,7 @@ class _StreamTabbedAttachmentPickerState extends State<StreamTabbedAttachmentPic | |
| Semantics( | ||
| role: SemanticsRole.tabPanel, | ||
| child: SizedBox( | ||
| height: _kTabbedAttachmentPickerHeight, | ||
| height: pickerHeight, | ||
| child: _currentOption.optionViewBuilder( | ||
| context, | ||
| widget.controller, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -861,38 +861,13 @@ class DefaultStreamMessageComposerState extends State<DefaultStreamMessageCompos | |
| if (enabled) return messageInput; | ||
|
|
||
| // Otherwise, show the no permission message. | ||
| return _buildNoPermissionMessage(context); | ||
| return _insetContent(context, _buildNoPermissionMessage(context)); | ||
| }, | ||
| ), | ||
| final messageInput => messageInput, | ||
| }; | ||
|
|
||
| final spacing = context.streamSpacing; | ||
| final platform = Theme.of(context).platform; | ||
| final padding = MediaQuery.paddingOf(context); | ||
| final hasBottomInset = padding.bottom > 0; | ||
|
|
||
| // Apple platforms rest on the bottom inset; elsewhere a margin clears it, | ||
| // and stands in when there is none. | ||
| final bottomSafeAreaMargin = switch (platform) { | ||
| .iOS || .macOS when hasBottomInset => spacing.none, | ||
| _ => spacing.md, | ||
| }; | ||
|
|
||
| final content = Material( | ||
| type: .transparency, | ||
| child: switch (widget.props.enableSafeArea) { | ||
| false => Center(heightFactor: 1, child: messageInput), | ||
| _ => StreamSafeArea.driven( | ||
| top: false, | ||
| listenable: _pickerAnimation, | ||
| // Only the bottom inset gives way to the picker; the side insets stay. | ||
| to: .only(left: padding.left, right: padding.right), | ||
| margin: .only(bottom: bottomSafeAreaMargin), | ||
| child: Center(heightFactor: 1, child: messageInput), | ||
| ), | ||
| }, | ||
| ); | ||
| final content = Material(type: .transparency, child: messageInput); | ||
|
|
||
| final colorScheme = context.streamColorScheme; | ||
| final effectiveSurfaceStyle = _resolveSurfaceStyle(context); | ||
|
|
@@ -926,11 +901,41 @@ class DefaultStreamMessageComposerState extends State<DefaultStreamMessageCompos | |
| ); | ||
| } | ||
|
|
||
| Widget _insetContent(BuildContext context, Widget child) { | ||
| final spacing = context.streamSpacing; | ||
| final platform = Theme.of(context).platform; | ||
| final padding = MediaQuery.paddingOf(context); | ||
| final hasBottomInset = padding.bottom > 0; | ||
|
|
||
| // Apple platforms rest on the bottom inset; elsewhere a margin clears it, | ||
| // and stands in when there is none. | ||
| final bottomSafeAreaMargin = switch (platform) { | ||
| .iOS || .macOS when hasBottomInset => spacing.none, | ||
| _ => spacing.md, | ||
| }; | ||
|
|
||
| return switch (widget.props.enableSafeArea) { | ||
| false => Center(heightFactor: 1, child: child), | ||
| _ => StreamSafeArea.driven( | ||
| top: false, | ||
| listenable: _pickerAnimation, | ||
| // Only the bottom inset gives way to the picker; the side insets stay. | ||
| to: .only(left: padding.left, right: padding.right), | ||
| margin: .only(bottom: bottomSafeAreaMargin), | ||
| child: Center(heightFactor: 1, child: child), | ||
| ), | ||
| }; | ||
| } | ||
|
|
||
| Widget _buildAutocompleteMessageInput(BuildContext context) { | ||
| return StreamAutocomplete( | ||
| focusNode: _effectiveFocusNode, | ||
| messageComposerController: _effectiveController, | ||
| fieldViewBuilder: _buildMessageInput, | ||
| // Inset inside the autocomplete so its suggestions span the full width. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Moving the inset into
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Intentional. The suggestions area now matches the composer and app bar: the background runs under the side insets, and our own |
||
| fieldViewBuilder: (context, controller, focusNode) { | ||
| final messageInput = _buildMessageInput(context, controller, focusNode); | ||
| return _insetContent(context, messageInput); | ||
| }, | ||
| autocompleteTriggers: [ | ||
| ...widget.props.customAutocompleteTriggers, | ||
| StreamAutocompleteTrigger( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| import 'package:flutter/material.dart'; | ||
|
|
||
| import '../../stream_chat_flutter.dart'; | ||
| import '../utils/network_error_text.dart'; | ||
|
|
||
| /// A widget that is used to display the error state of the message list. | ||
| /// | ||
| /// Scrolls instead of overflowing when it has less room than it needs. | ||
| class StreamMessageListErrorState extends StatelessWidget { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: this isn't exported (consistent with the empty/skeleton states), but the
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Leaving it internal for now. It matches |
||
| /// Creates a new instance of the [StreamMessageListErrorState]. | ||
| const StreamMessageListErrorState({ | ||
| super.key, | ||
| required this.error, | ||
| required this.onRetryPressed, | ||
| }); | ||
|
|
||
| /// The error that stopped the messages from loading. | ||
| final Object error; | ||
|
|
||
| /// Called when the retry button is pressed. | ||
| final VoidCallback onRetryPressed; | ||
|
|
||
| @override | ||
| Widget build(BuildContext context) { | ||
| final translations = context.translations; | ||
| final text = resolveNetworkErrorText(context, error, fallbackTitle: translations.loadingMessagesError); | ||
|
|
||
| final content = Center( | ||
| child: StreamScrollViewErrorWidget( | ||
| errorTitle: Text(text.title), | ||
| errorSubtitle: Text(text.description), | ||
| retryButtonText: Text(translations.tryAgainLabel), | ||
| onRetryPressed: onRetryPressed, | ||
| ), | ||
| ); | ||
|
|
||
| return CustomScrollView( | ||
| slivers: [SliverFillRemaining(hasScrollBody: false, child: content)], | ||
| ); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
StreamAutocompleteOptionsis exported, and it now always adds side padding, which changes any custom overlay built on it. This isn't in the changelog; could it get its own🔄 Changedline?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added in a552d1d: the 🔄 Changed line now names
StreamAutocompleteOptionsand how other options views clear the insets. a5f716e checks its content stays clear of them.