-
Notifications
You must be signed in to change notification settings - Fork 306
new_dm: Add UI for starting new DM conversations #1322
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
base: main
Are you sure you want to change the base?
Conversation
b407738
to
d8818a2
Compare
Thanks for working on this @chimnayajith! I took a quick look at the implementation and checked the design. There are places where it currently does not match the Figma, for example: Container(
width: MediaQuery.of(context).size.width,
constraints: const BoxConstraints(
minHeight: 44,
maxHeight: 124,
),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: designVariables.bgSearchInput,
),
child: SingleChildScrollView(
controller: scrollController,
child: Row(
children: [
Expanded(
child: Wrap(
spacing: 4,
runSpacing: 4, where the horizontal padding should be 14px; and the spacing between the pills should be 6px. I think there might be more places like this, so please sure to check your PR with the design and make sure that they match exactly. While working on the next revision, please also try to break down the sheet into reasonable widgets, instead of a single one that encompasses the entire new DM page. There are also things like collapsing the closing brackets into a single line; you can find examples of that throughout the codebase: // [...]
child: SafeArea(
child: SizedBox(height: 48,
child: Center(
child: ConstrainedBox(
// TODO(design): determine a suitable max width for bottom nav bar
constraints: const BoxConstraints(maxWidth: 600),
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
for (final navigationBarButton in navigationBarButtons)
Expanded(child: navigationBarButton),
]))))))); All those changes will help make your PR more reviewable. At the high-level, I think the user filtering code should re-use the |
0bc3e12
to
a6cc695
Compare
@PIG208 Thanks for the review! I’ve made the necessary changes to match the Figma, and refactored the sheet into smaller widgets. I also followed the code style guidelines you mentioned. I’ve pushed the revision—PTAL. |
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.
Thanks for the update! I went through the implementation of the design (mainly layout but not the colors) and left some comments. I haven't read the tests yet, but it should be a good amount of feedback to work on a new revision for.
width: 137, | ||
height: 48, |
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.
icon: Icon(Icons.add, size: 24), | ||
label: Text(zulipLocalizations.newDmFabButtonLabel, style: TextStyle(fontSize: 20, fontWeight: FontWeight.w500)), |
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.
Do we have a way to control/verify the spacing between the icon and the label? In Figma it's 8px but from this code it is not obvious if it complies to the design.
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.
The extendedIconLabelSpacing
field for extended floating action buttons has a default value of 8.0. Should it still be specified?
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.
Yeah. Because the default value can change (though unlikely), but we have a specific value in mind.
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.
Have given it in the revision pushed.
lib/widgets/new_dm_sheet.dart
Outdated
|
||
void showNewDmSheet(BuildContext context) { | ||
final store = PerAccountStoreWidget.of(context); | ||
showModalBottomSheet<dynamic>( |
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.
Let's avoid dynamic
. We in general follow Flutter's style guide.
lib/widgets/new_dm_sheet.dart
Outdated
@override | ||
Widget build(BuildContext context) { | ||
return SizedBox( | ||
height: MediaQuery.of(context).size.height * 0.95, |
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.
It doesn't seem right to size it this way. If the goal is to avoid the top of the bottom sheet overlapping with the device's top inset, see useSafeArea
on showModalBottomSheet
.
lib/widgets/new_dm_sheet.dart
Outdated
} | ||
} | ||
|
||
class NewDmHeader extends StatelessWidget { |
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.
For helper widgets like this, let's make them private (_NewDmHeader
) unless it is otherwise necessary.
lib/widgets/new_dm_sheet.dart
Outdated
hintStyle: TextStyle( | ||
fontSize: 17, | ||
height: 1.0, | ||
color: designVariables.textInput.withFadedAlpha(0.5)), |
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.
Looks like we do have a Figma variable for this: label-search-prompt
.
lib/widgets/new_dm_sheet.dart
Outdated
children: [ | ||
Avatar(userId: userId, size: 22, borderRadius: 3), | ||
Padding( | ||
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 3), |
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.
lib/widgets/new_dm_sheet.dart
Outdated
|
||
final List<User> filteredUsers; | ||
final Set<int> selectedUserIds; | ||
final void Function(int) onUserSelected; |
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.
The name of this is a bit misleading, because "selected" seems to indicate that the user is added to the list of selected user, when in reality it is more like toggling the selection of the user.
How about something like void Function(int userId) onUserTapped
, with the parameter name.
Widget build(BuildContext context) { | ||
final designVariables = DesignVariables.of(context); | ||
|
||
return ListView.builder( |
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.
lib/widgets/new_dm_sheet.dart
Outdated
child: Padding( | ||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), | ||
child: Container( | ||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), |
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.
Is the outer Padding
redundant?
lib/widgets/new_dm_sheet.dart
Outdated
}); | ||
} | ||
|
||
// Scroll to the search field when the user selects a user |
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.
Hmm, is this part of the UX specified in the design? It would be good to link to the relevant discussion/Figma if that's the case. Either way, I feel that it might be the best to leave this out from the initial implementation, to simplify things as we work it out.
lib/widgets/new_dm_sheet.dart
Outdated
size: 24, | ||
color: selectedUserIds.isEmpty | ||
? designVariables.icon.withFadedAlpha(0.5) | ||
: designVariables.icon)])); |
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.
Square brackets are usually not wrapped, because they help indicate the end of a list and that the items before them are parallel.
lib/widgets/new_dm_sheet.dart
Outdated
super.key, | ||
required this.searchController, | ||
required this.scrollController, | ||
required this.selectedUserIds}); |
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.
Similarly, curly brackets are not wrapped, because they indicate the end of a parameter list, function, etc.
9439a4a
to
e033db5
Compare
@PIG208 Pushed a revision. PTAL! |
@chimnayajith Are the screenshots in the PR description up to date? Please update them if not. |
I've updated the screenshots. Please take a look! |
Let's change "Add Person" to "Add user", which will be more consistent with the web app. I'm not sure why your screenshots have varied capitalization, but in any case, only the first word should be capitalized. |
Actually, it would probably be better to match the web app more fully, if it doesn't feel too long in the mobile context.
|
Why is there no "Add Person" in your last screenshot? What is different vs. the other ones? |
Continuing the discussion in CZO |
e033db5
to
126c4cc
Compare
126c4cc
to
a8bcda8
Compare
@PIG208 Pushed a revision with the mentioned changes in UI. PTAL Note: Uses |
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.
Thanks @chimnayajith!
This is still in maintainer review with @PIG208, but I took a quick skim just now and also tried playing with this in the running app. Here's one high-level comment on the code, and a couple of things that jumped out at me in the UX.
return SafeArea( | ||
return Scaffold( | ||
body: SafeArea( |
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.
This will mean a Scaffold
nested inside a Scaffold
. See the Scaffold docs on that:
https://main-api.flutter.dev/flutter/material/Scaffold-class.html#nested-scaffolds
Have you tried alternatives to nesting the Scaffolds?
zulipLocalizations.newDmFabButtonLabel, | ||
style: const TextStyle(fontSize: 20, height: 24 / 20) | ||
.merge(weightVariableTextStyle(context, wght: 500))), | ||
backgroundColor: designVariables.fabBg, |
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.
Trying out the PR, this color doesn't look right.
It looks like the screenshots in the PR description agree with what I see running it (good), but don't agree on this color with what's in Figma.
floatingActionButton: Container( | ||
constraints: const BoxConstraints( | ||
minWidth: 137, | ||
minHeight: 48), |
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.
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.
Yeah, most of the time, as long as we get the paddings right, the size of the button should match the Figma design with normal UI scaling setting, so the constraints
is not needed.
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.
Thanks for the update! I went through the implementation but haven't looked at the tests in this round. Most of the colors look good to me. The font height/size/weight also looks good. Once all the comments are addressed, this should match the design quite well. Using devtool will be helpful to confirm the actual sizes/paddings.
child: NewDmPicker(pageContext: context)))); | ||
} | ||
|
||
class NewDmPicker extends StatefulWidget { |
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.
nit: for NewDmPicker
, we can add a @visibleForTesting
annotation since we only use it for testing externally.
lib/widgets/new_dm_sheet.dart
Outdated
padding: const EdgeInsets.all(8), | ||
child: Icon( | ||
isSelected ? Icons.check_circle : Icons.circle_outlined, | ||
color: isSelected ? designVariables.radioFillSelected : designVariables.radioBorder, |
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.
This line is too wide (>80 characters); consider breaking this into multiple lines.
lib/widgets/new_dm_sheet.dart
Outdated
]))); | ||
}); | ||
} | ||
} |
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.
nit: add a newline at the end of the file
lib/widgets/new_dm_sheet.dart
Outdated
class NewDmPicker extends StatefulWidget { | ||
const NewDmPicker({ | ||
super.key, | ||
required this.pageContext |
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.
nit: need a trailing comma here
lib/widgets/new_dm_sheet.dart
Outdated
const _NewDmUserList({ | ||
required this.filteredUsers, | ||
required this.selectedUserIds, | ||
required this.onUserTapped |
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.
nit: similar issue with missing trailing comma
lib/widgets/new_dm_sheet.dart
Outdated
Widget build(BuildContext context) { | ||
final designVariables = DesignVariables.of(context); | ||
final store = PerAccountStoreWidget.of(context); | ||
final user = store.getUser(userId)!; |
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.
Hmm, I don't feel confident that the userId
will always present. If it does, we need a comment justifying why it is always present; if it doesn't, we should be more defensive and find a way to not throw.
lib/widgets/new_dm_sheet.dart
Outdated
final user = store.getUser(userId)!; | ||
|
||
return Container( | ||
decoration: BoxDecoration(color: designVariables.bgMenuButtonSelected, borderRadius: BorderRadius.circular(3)), |
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.
nit: missing indentation; this line should also be wrapped because it is too long (>80 characters)
lib/widgets/new_dm_sheet.dart
Outdated
children: [ | ||
_buildBackButton(context), | ||
Text(zulipLocalizations.newDmSheetScreenTitle, | ||
style: TextStyle(color: designVariables.title, | ||
fontSize: 20, height: 30 / 20) | ||
.merge(weightVariableTextStyle(context, wght: 600))), | ||
_buildNextButton(context), |
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.
final isSelected = selectedUserIds.contains(user.userId); | ||
|
||
return InkWell( | ||
onTap: () => onUserTapped(user.userId), |
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.
Let's use splashFactory: NoSplash.splashFactory,
here; in general we avoid having Material's splash effect in the app.
lib/widgets/new_dm_sheet.dart
Outdated
Avatar(userId: user.userId, size: 32, borderRadius: 3), | ||
Expanded( | ||
child: Padding( | ||
padding: const EdgeInsetsDirectional.fromSTEB(8, 6.5, 0, 6.5), | ||
child: Text(user.fullName, | ||
style: TextStyle( | ||
fontSize: 17, | ||
height: 19 / 17, | ||
color: designVariables.textMessage) | ||
.merge(weightVariableTextStyle(context, wght: 500))))), | ||
]))); |
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.
Hii @chimnayajith, Since already a lot of reviews and updates have gone through the process. If you are not able to take this further or need some help, I will be happy to join this and proceed further. |
@Gaurav-Kushwaha-1225 Will be sending in a revision today, Got occupied with exams. |
49a7fbe
to
30b556d
Compare
@PIG208 Pushed a revision. PTAL. |
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.
Thanks! This looks a lot closer to the design now and I left some small comments on the implementation. This time, I also reviewed the tests.
lib/widgets/new_dm_sheet.dart
Outdated
child: Text(zulipLocalizations.newDmSheetScreenTitle, | ||
style: TextStyle(color: designVariables.title, | ||
fontSize: 20, height: 30 / 20) | ||
.merge(weightVariableTextStyle(context, wght: 600)), |
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.
nit: indentation
lib/widgets/new_dm_sheet.dart
Outdated
} | ||
|
||
class _SelectedUserChip extends StatelessWidget { | ||
const _SelectedUserChip({ required this.userId }); |
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.
nit: remove spaces around the parameter
lib/widgets/theme.dart
Outdated
@@ -197,6 +203,9 @@ class DesignVariables extends ThemeExtension<DesignVariables> { | |||
contextMenuItemMeta: const Color(0xff9194a3), | |||
contextMenuItemText: const Color(0xff9398fd), | |||
editorButtonPressedBg: Colors.white.withValues(alpha: 0.06), | |||
fabBg: const Color(0xff4f42c9), | |||
fabBgPressed: const Color(0xff6159e1), |
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.
@@ -197,6 +203,9 @@ class DesignVariables extends ThemeExtension<DesignVariables> { | |||
contextMenuItemMeta: const Color(0xff9194a3), | |||
contextMenuItemText: const Color(0xff9398fd), | |||
editorButtonPressedBg: Colors.white.withValues(alpha: 0.06), | |||
fabBg: const Color(0xff4f42c9), | |||
fabBgPressed: const Color(0xff6159e1), | |||
fabLabel: const Color(0xffeceefc), |
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.
test/widgets/new_dm_sheet_test.dart
Outdated
await runAndCheck(tester, user: user); | ||
}); | ||
}); | ||
} |
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.
nit: need a newline after this
test/widgets/new_dm_sheet_test.dart
Outdated
} | ||
|
||
testWidgets('navigates to message list on Next', (tester) async { | ||
final user = eg.user(userId: 1, fullName: 'Test User'); |
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.
We can skip specifying userId
because we don't rely on it being this particular value for the test.
test/widgets/new_dm_sheet_test.dart
Outdated
eg.user(userId: 1, fullName: 'Alice Anderson'), | ||
eg.user(userId: 2, fullName: 'Bob Brown'), | ||
eg.user(userId: 3, fullName: 'Alice Carter'), |
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.
nit: we can skip specifying user IDs here
test/widgets/new_dm_sheet_test.dart
Outdated
for (final user in users) { | ||
await store.addUser(user); | ||
} |
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.
nit: use store.addUsers
instead
test/widgets/new_dm_sheet_test.dart
Outdated
check(nextButton.onTap).isNull(); | ||
}); | ||
|
||
testWidgets('shows filtered users based on search', (tester) async { |
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.
I think there are some other interesting filtering logic that can be tested. Such as checking that the self user is excluded from the list, the search is case-insensitive. Another useful test would be checking if unknown users are handled correctly.
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.
I wasn't able to write tests specifically for handling unknown users, but the logic relies on userDisplayName
, which is already covered in user_test.dart
. I also didn’t find any other tests that handle this specifically.
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.
I realized that it's a product question as to whether we want to allow selecting the self user. I think we should, since that's allowed on web and the legacy app.
lib/widgets/new_dm_sheet.dart
Outdated
}); | ||
|
||
final BuildContext pageContext; | ||
final PerAccountStore store; |
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.
I think we can skip passing this by replacing the _updateFilteredUsers
call with an overridden didChangeDependencies
.
@override
void didChangeDependencies() {
super.didChangeDependencies();
_handleSearchUpdate();
}
Hi @chimnayajith! Thanks for working on this. Since this issue is a launch blocker, we would prioritize reviewing this/getting this done first; the other issue that you are working on (#1344) is a launch goal, which means that it will likely receive less attention until we clear the blockers. |
61ede77
to
82b1b66
Compare
@PIG208 Pushed a revision. PTAL. I had missed the "No users found" screen earlier; just added it and included the screenshots in the PR description. There aren’t any design references for it, but it follows how it’s done in the RN app |
return Center( | ||
child: Padding( | ||
padding: const EdgeInsets.all(24), | ||
child: Text( | ||
zulipLocalizations.newDmSheetNoUsersFound, | ||
style: TextStyle( | ||
color: designVariables.labelMenuButton, | ||
fontSize: 16)))); |
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.
This will need a TODO(design)
comment since it is not a part of the Figma design.
lib/widgets/new_dm_sheet.dart
Outdated
decoration: isSelected ? BoxDecoration( | ||
color: designVariables.bgMenuButtonSelected, | ||
borderRadius: BorderRadius.circular(10)) : null, |
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.
nit: Container
shouldn't be necessary; we can use DecoratedBox
another nit: I think we usually have the null
case first, for reasons similar to why early returns are preferred.
lib/widgets/new_dm_sheet.dart
Outdated
color: designVariables.textMessage) | ||
.merge(weightVariableTextStyle(context, wght: 500))))), |
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.
nit:
color: designVariables.textMessage) | |
.merge(weightVariableTextStyle(context, wght: 500))))), | |
color: designVariables.textMessage, | |
).merge(weightVariableTextStyle(context, wght: 500))))), |
This way the end boundary of TextStyle
is clear.
test/widgets/new_dm_sheet_test.dart
Outdated
check(nextButton.onTap).isNull(); | ||
}); | ||
|
||
testWidgets('shows filtered users based on search', (tester) async { |
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.
I realized that it's a product question as to whether we want to allow selecting the self user. I think we should, since that's allowed on web and the legacy app.
test/widgets/new_dm_sheet_test.dart
Outdated
if (users.length == 1) { | ||
check(find.descendant( | ||
of: find.byType(ZulipAppBar), | ||
matching: find.text('DMs with ${users[0].fullName}'))).findsOne(); | ||
} else { | ||
final names = users.map((user) => user.fullName).join(', '); | ||
check(find.descendant( | ||
of: find.byType(ZulipAppBar), | ||
matching: find.text('DMs with $names'))).findsOne(); | ||
} |
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.
It will probably be easier to pass the expected app bar title from the tests.
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.
Thanks for the update! We have gone through the design in previous rounds. In this review I focused mostly on new changes like _NewDmButton
and the tests.
onTapUp: (_) => setState(() => _pressed = false), | ||
onTapCancel: () => setState(() => _pressed = false), | ||
borderRadius: BorderRadius.circular(28), | ||
child: Container( |
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.
Let's use the animated version AnimatedContainer
here. The Figma design has specified the duration and curve for transitions.
Looks like it also handles the box shadow animation properly, so we can just complete the TODO from #1322 (comment), now that we do have a AnimatedScaleOnTap style widget.
zulipLocalizations.newDmFabButtonLabel, | ||
style: TextStyle(fontSize: 20, height: 24 / 20) | ||
.merge(weightVariableTextStyle(context, wght: 500)) | ||
.copyWith(color: fabLabelColor)), |
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.
It should be fine to just specify the color when constructing TextStyle
, right?
@@ -138,3 +149,55 @@ class RecentDmConversationsItem extends StatelessWidget { | |||
])))); | |||
} | |||
} | |||
|
|||
class _NewDmButton extends StatefulWidget { | |||
final VoidCallback onPressed; |
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.
nit: inline the callback in the _NewDmButtonState.build
so that we can remove this parameter; it seems fitting that this widget is responsible for calling showNewDmSheet
final fabBgColor = _pressed ? designVariables.fabBgPressed : designVariables.fabBg; | ||
final fabLabelColor = _pressed ? designVariables.fabLabelPressed : designVariables.fabLabel; |
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.
nit: these lines are too long; wrap them so that they stay under 80 characters in width
test/widgets/new_dm_sheet_test.dart
Outdated
import 'test_app.dart'; | ||
|
||
Future<void> setupSheet(WidgetTester tester, { | ||
required List<User> users |
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.
nit: missing trailing comma
test/widgets/new_dm_sheet_test.dart
Outdated
testWidgets('selecting a user', (tester) async { | ||
final user = eg.user(fullName: 'Test User'); | ||
await setupSheet(tester, users: [user]); | ||
Finder findNextButton() => find.widgetWithText(GestureDetector, 'Next'); | ||
|
||
check(find.byIcon(Icons.circle_outlined)).findsOne(); | ||
check(find.byIcon(Icons.check_circle)).findsNothing(); | ||
|
||
var nextButton = tester.widget<GestureDetector>(findNextButton()); | ||
check(nextButton.onTap).isNull(); | ||
final userTile = find.ancestor( | ||
of: find.text(user.fullName), | ||
matching: find.byType(InkWell)); | ||
await tester.tap(userTile); | ||
await tester.pump(); | ||
check(find.byIcon(Icons.check_circle)).findsOne(); | ||
check(find.byIcon(Icons.circle_outlined)).findsNothing(); | ||
nextButton = tester.widget<GestureDetector>(findNextButton()); | ||
check(nextButton.onTap).isNotNull(); | ||
}); |
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.
nit:
testWidgets('selecting a user', (tester) async { | |
final user = eg.user(fullName: 'Test User'); | |
await setupSheet(tester, users: [user]); | |
Finder findNextButton() => find.widgetWithText(GestureDetector, 'Next'); | |
check(find.byIcon(Icons.circle_outlined)).findsOne(); | |
check(find.byIcon(Icons.check_circle)).findsNothing(); | |
var nextButton = tester.widget<GestureDetector>(findNextButton()); | |
check(nextButton.onTap).isNull(); | |
final userTile = find.ancestor( | |
of: find.text(user.fullName), | |
matching: find.byType(InkWell)); | |
await tester.tap(userTile); | |
await tester.pump(); | |
check(find.byIcon(Icons.check_circle)).findsOne(); | |
check(find.byIcon(Icons.circle_outlined)).findsNothing(); | |
nextButton = tester.widget<GestureDetector>(findNextButton()); | |
check(nextButton.onTap).isNotNull(); | |
}); | |
testWidgets('selecting a user', (tester) async { | |
final user = eg.user(fullName: 'Test User'); | |
final userTileFinder = find.ancestor( | |
of: find.text(user.fullName), | |
matching: find.byType(InkWell)); | |
await setupSheet(tester, users: [user]); | |
final nextButton = tester.widget<GestureDetector>( | |
find.widgetWithText(GestureDetector, 'Next')); | |
check(find.byIcon(Icons.circle_outlined)).findsOne(); | |
check(find.byIcon(Icons.check_circle)).findsNothing(); | |
check(nextButton.onTap).isNull(); | |
await tester.tap(userTileFinder); | |
await tester.pump(); | |
check(find.byIcon(Icons.circle_outlined)).findsNothing(); | |
check(find.byIcon(Icons.check_circle)).findsOne(); | |
check(nextButton.onTap).isNotNull(); | |
}); |
By breaking the checks into two stanzas, we can make this a bit more readable. (We don't need to call tester.widget()
twice to see updates to nextButton.onTap
.)
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.
We don't need to call
tester.widget()
twice to see updates tonextButton.onTap
.
I tried implementing this change, but I'm running into some issues - the tests are failing when I remove the second call.
When I only get the button once before the tap and then check its onTap
property after the UI update, the test fails as if the property isn't being updated.
Could you help me understand the correct approach here?
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.
I see. Yeah, it looks like the return values of the tester.widget
call before and after the pump are different. Let's keep calling them again then.
|
||
testWidgets('opens new DM sheet on FAB tap', (tester) async { | ||
await setupPage(tester, users: [], dmMessages: []); | ||
final fab = find.ancestor( |
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.
nit: Let's avoid abbreviations like this (here and in the test name) when possible, unless it comes from an outside source like the Figma design. In this case, we are just referring to the new DM button widget.
82b1b66
to
161e5c5
Compare
d221599
to
7eb7984
Compare
@PIG208 Pushed a revision. PTAL! |
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.
Thanks for the update! Left some comments, most are small, except for ones on adding more test cases.
lib/widgets/new_dm_sheet.dart
Outdated
bool shouldIncludeSelfUser(User user) { | ||
final shouldExcludeSelfUser = selectedUserIds.isNotEmpty | ||
&& !selectedUserIds.contains(store.selfUserId); | ||
|
||
if (user.userId != store.selfUserId) return true; | ||
return !shouldExcludeSelfUser; | ||
} |
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.
This seems a bit complicated. From reading this, it seems that if user
is selfUserId
, this will only return false
when selectedUserIds
is non-empty and contains selfUserId
.
However, I think the shouldIncludeUser(user)
check can just be removed from the .where
condition, since the intention is to just not special-case self-user when filtering by user name.
lib/widgets/new_dm_sheet.dart
Outdated
if (userId != store.selfUserId | ||
&& selectedUserIds.contains(store.selfUserId)) { | ||
selectedUserIds.remove(store.selfUserId); | ||
} |
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.
Similarly, it will probably be simpler if we don't special-case self user ID. The UX here seems a bit confusing when I tried it out: if myself is selected, then I go on to select another user, myself is deselected. This seemed a bit surprising.
lib/widgets/new_dm_sheet.dart
Outdated
decoration: !isSelected | ||
? const BoxDecoration() | ||
: BoxDecoration(color: designVariables.bgMenuButtonSelected, | ||
borderRadius: BorderRadius.circular(10)), |
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.
nit: indentation
lib/widgets/new_dm_sheet.dart
Outdated
borderRadius: BorderRadius.circular(10)), | ||
child: Padding( | ||
padding: const EdgeInsets.fromLTRB(0, 6, 12, 6), | ||
child:Row( |
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.
nit: missing space
lib/widgets/new_dm_sheet.dart
Outdated
final narrow = DmNarrow.withOtherUsers( | ||
selectedUserIds, | ||
selfUserId: store.selfUserId); |
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.
Because now selectedUserIds
can contain selfUserId, this needs to change. See the dartdoc of DmNarrow.withOtherUsers
.
|
||
check(find.byType(ComposeBox)).findsOne(); | ||
} | ||
|
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.
Let's also test the cases when only the selfUser is selected, and when the selfUser is selected with some other users.
test/widgets/new_dm_sheet_test.dart
Outdated
testWidgets('selecting a user', (tester) async { | ||
final user = eg.user(fullName: 'Test User'); | ||
await setupSheet(tester, users: [user]); | ||
Finder findNextButton() => find.widgetWithText(GestureDetector, 'Next'); | ||
|
||
check(find.byIcon(Icons.circle_outlined)).findsOne(); | ||
check(find.byIcon(Icons.check_circle)).findsNothing(); | ||
|
||
var nextButton = tester.widget<GestureDetector>(findNextButton()); | ||
check(nextButton.onTap).isNull(); | ||
final userTile = find.ancestor( | ||
of: find.text(user.fullName), | ||
matching: find.byType(InkWell)); | ||
await tester.tap(userTile); | ||
await tester.pump(); | ||
check(find.byIcon(Icons.check_circle)).findsOne(); | ||
check(find.byIcon(Icons.circle_outlined)).findsNothing(); | ||
nextButton = tester.widget<GestureDetector>(findNextButton()); | ||
check(nextButton.onTap).isNotNull(); | ||
}); |
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.
I see. Yeah, it looks like the return values of the tester.widget
call before and after the pump are different. Let's keep calling them again then.
lib/widgets/new_dm_sheet.dart
Outdated
filteredUsers = store.allUsers | ||
.where((user) => | ||
shouldIncludeSelfUser(user) && | ||
!user.isBot && |
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.
I think we shouldn't exclude bot users. Some bots might actually utilize DMs as a way of receiving commands from the user.
lib/widgets/new_dm_sheet.dart
Outdated
textAlign: TextAlign.center)), | ||
_buildNextButton(context), | ||
]) | ||
); |
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.
nit: move trailing parenthesis to the previous line
? const Color(0xff2B0E8A).withValues(alpha: 0.3) | ||
: const Color(0xff2B0E8A).withValues(alpha: 0.4), |
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.
Hmm, so these are not named variables in the Figma design. In dark mode, the shadow is barely visible. Let's also have a TODO(design) comment here, mentioning dark mode colors. Perhaps it is also helpful to discussion in #mobile-design on whether we want the shadows in dark mode or not.
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.
Opened a design thread for this in CZO here
Started a CZO thread to discuss the filtering logic. |
Add a modal bottom sheet UI for starting direct messages: - Search and select users from global list - Support single and group DMs - Navigate to message list after selection Design reference: https://www.figma.com/design/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=4903-31879&p=f&t=pQP4QcxpccllCF7g-0 Fixes: zulip#127
@PIG208 Pushed a revision. PTAL! |
Pull Request
Description
This pull request adds the UI to starting new DM conversations.. A floating action button has been added to the
RecentDmConversationsPage
, which opens a bottom modal sheet, allowing users to select conversation participants and proceed to theMessageListPage
.Design reference: Figma
Related Issues
Screenshots
Light mode
Dark mode
Additional Notes
The user list is currently sorted based on the recency of direct messages.