Skip to content

Commit 2610fa3

Browse files
message_list: added pressedTint feature
It allows to see a tint color on the message when it is pressed. Moved `MessageWithPossibleSender` to `StatefulWidget` and used `ModalStatus` return type of `showMessageActionSheet` to check whether BottomSheet is open or not. Added `pressedTint` to `DesignVariables` for using it in `MessageWithPossibleSender`. Added tests too in `message_list_test.dart`. Fixes: zulip#1142
1 parent ac1c602 commit 2610fa3

File tree

4 files changed

+151
-40
lines changed

4 files changed

+151
-40
lines changed

lib/widgets/action_sheet.dart

+5-4
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,11 @@ import 'store.dart';
2929
import 'text.dart';
3030
import 'theme.dart';
3131

32-
void _showActionSheet(
32+
ModalStatus _showActionSheet(
3333
BuildContext context, {
3434
required List<Widget> optionButtons,
3535
}) {
36-
showModalBottomSheet<void>(
36+
final future = showModalBottomSheet<void>(
3737
context: context,
3838
// Clip.hardEdge looks bad; Clip.antiAliasWithSaveLayer looks pixel-perfect
3939
// on my iPhone 13 Pro but is marked as "much slower":
@@ -63,6 +63,7 @@ void _showActionSheet(
6363
const ActionSheetCancelButton(),
6464
])));
6565
});
66+
return ModalStatus(future);
6667
}
6768

6869
/// A button in an action sheet.
@@ -464,7 +465,7 @@ class ResolveUnresolveButton extends ActionSheetMenuItemButton {
464465
/// Show a sheet of actions you can take on a message in the message list.
465466
///
466467
/// Must have a [MessageListPage] ancestor.
467-
void showMessageActionSheet({required BuildContext context, required Message message}) {
468+
ModalStatus showMessageActionSheet({required BuildContext context, required Message message}) {
468469
final pageContext = PageRoot.contextOf(context);
469470
final store = PerAccountStoreWidget.of(pageContext);
470471

@@ -492,7 +493,7 @@ void showMessageActionSheet({required BuildContext context, required Message mes
492493
ShareButton(message: message, pageContext: pageContext),
493494
];
494495

495-
_showActionSheet(pageContext, optionButtons: optionButtons);
496+
return _showActionSheet(pageContext, optionButtons: optionButtons);
496497
}
497498

498499
abstract class MessageActionSheetMenuItemButton extends ActionSheetMenuItemButton {

lib/widgets/message_list.dart

+76-35
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import 'actions.dart';
1616
import 'app_bar.dart';
1717
import 'compose_box.dart';
1818
import 'content.dart';
19+
import 'dialog.dart';
1920
import 'emoji_reaction.dart';
2021
import 'icons.dart';
2122
import 'page.dart';
@@ -1316,22 +1317,45 @@ String formatHeaderDate(
13161317
// Design referenced from:
13171318
// - https://github.com/zulip/zulip-mobile/issues/5511
13181319
// - https://www.figma.com/file/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=538%3A20849&mode=dev
1319-
class MessageWithPossibleSender extends StatelessWidget {
1320+
class MessageWithPossibleSender extends StatefulWidget {
13201321
const MessageWithPossibleSender({super.key, required this.item});
13211322

13221323
final MessageListMessageItem item;
13231324

1325+
@override
1326+
State<MessageWithPossibleSender> createState() => _MessageWithPossibleSenderState();
1327+
}
1328+
1329+
class _MessageWithPossibleSenderState extends State<MessageWithPossibleSender> {
1330+
final WidgetStatesController statesController = WidgetStatesController();
1331+
1332+
@override
1333+
void initState() {
1334+
super.initState();
1335+
statesController.addListener(() {
1336+
setState(() {
1337+
// Force a rebuild to resolve background color
1338+
});
1339+
});
1340+
}
1341+
1342+
@override
1343+
void dispose() {
1344+
statesController.dispose();
1345+
super.dispose();
1346+
}
1347+
13241348
@override
13251349
Widget build(BuildContext context) {
13261350
final store = PerAccountStoreWidget.of(context);
13271351
final messageListTheme = MessageListTheme.of(context);
13281352
final designVariables = DesignVariables.of(context);
13291353

1330-
final message = item.message;
1354+
final message = widget.item.message;
13311355
final sender = store.users[message.senderId];
13321356

13331357
Widget? senderRow;
1334-
if (item.showSender) {
1358+
if (widget.item.showSender) {
13351359
final time = _kMessageTimestampFormat
13361360
.format(DateTime.fromMillisecondsSinceEpoch(1000 * message.timestamp));
13371361
senderRow = Row(
@@ -1400,38 +1424,55 @@ class MessageWithPossibleSender extends StatelessWidget {
14001424

14011425
return GestureDetector(
14021426
behavior: HitTestBehavior.translucent,
1403-
onLongPress: () => showMessageActionSheet(context: context, message: message),
1404-
child: Padding(
1405-
padding: const EdgeInsets.symmetric(vertical: 4),
1406-
child: Column(children: [
1407-
if (senderRow != null)
1408-
Padding(padding: const EdgeInsets.fromLTRB(16, 2, 16, 0),
1409-
child: senderRow),
1410-
Row(
1411-
crossAxisAlignment: CrossAxisAlignment.baseline,
1412-
textBaseline: localizedTextBaseline(context),
1413-
children: [
1414-
const SizedBox(width: 16),
1415-
Expanded(child: Column(
1416-
crossAxisAlignment: CrossAxisAlignment.stretch,
1417-
children: [
1418-
MessageContent(message: message, content: item.content),
1419-
if ((message.reactions?.total ?? 0) > 0)
1420-
ReactionChipsList(messageId: message.id, reactions: message.reactions!),
1421-
if (editStateText != null)
1422-
Text(editStateText,
1423-
textAlign: TextAlign.end,
1424-
style: TextStyle(
1425-
color: designVariables.labelEdited,
1426-
fontSize: 12,
1427-
height: (12 / 12),
1428-
letterSpacing: proportionalLetterSpacing(
1429-
context, 0.05, baseFontSize: 12))),
1430-
])),
1431-
SizedBox(width: 16,
1432-
child: star),
1433-
]),
1434-
])));
1427+
onLongPress: () async {
1428+
statesController.update(WidgetState.selected, true);
1429+
ModalStatus status = showMessageActionSheet(context: context,
1430+
message: message);
1431+
await status.closed;
1432+
statesController.update(WidgetState.selected, false);
1433+
},
1434+
onLongPressDown: (_) => statesController.update(WidgetState.pressed, true),
1435+
onLongPressCancel: () => statesController.update(WidgetState.pressed, false),
1436+
onLongPressUp: () => statesController.update(WidgetState.pressed, false),
1437+
child: DecoratedBox(
1438+
decoration: BoxDecoration(
1439+
color: WidgetStateColor.fromMap({
1440+
WidgetState.pressed: designVariables.pressedTint,
1441+
WidgetState.selected: designVariables.pressedTint,
1442+
WidgetState.any: Colors.transparent,
1443+
}).resolve(statesController.value)
1444+
),
1445+
child: Padding(
1446+
padding: const EdgeInsets.symmetric(vertical: 4),
1447+
child: Column(children: [
1448+
if (senderRow != null)
1449+
Padding(padding: const EdgeInsets.fromLTRB(16, 2, 16, 0),
1450+
child: senderRow),
1451+
Row(
1452+
crossAxisAlignment: CrossAxisAlignment.baseline,
1453+
textBaseline: localizedTextBaseline(context),
1454+
children: [
1455+
const SizedBox(width: 16),
1456+
Expanded(child: Column(
1457+
crossAxisAlignment: CrossAxisAlignment.stretch,
1458+
children: [
1459+
MessageContent(message: message, content: widget.item.content),
1460+
if ((message.reactions?.total ?? 0) > 0)
1461+
ReactionChipsList(messageId: message.id, reactions: message.reactions!),
1462+
if (editStateText != null)
1463+
Text(editStateText,
1464+
textAlign: TextAlign.end,
1465+
style: TextStyle(
1466+
color: designVariables.labelEdited,
1467+
fontSize: 12,
1468+
height: (12 / 12),
1469+
letterSpacing: proportionalLetterSpacing(
1470+
context, 0.05, baseFontSize: 12))),
1471+
])),
1472+
SizedBox(width: 16,
1473+
child: star),
1474+
]),
1475+
]))));
14351476
}
14361477
}
14371478

lib/widgets/theme.dart

+7
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
145145
labelEdited: const HSLColor.fromAHSL(0.35, 0, 0, 0).toColor(),
146146
labelMenuButton: const Color(0xff222222),
147147
mainBackground: const Color(0xfff0f0f0),
148+
pressedTint: const HSLColor.fromAHSL(0.04, 0, 0, 0).toColor(),
148149
textInput: const Color(0xff000000),
149150
title: const Color(0xff1a1a1a),
150151
bgSearchInput: const Color(0xffe3e3e3),
@@ -194,6 +195,7 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
194195
labelEdited: const HSLColor.fromAHSL(0.35, 0, 0, 1).toColor(),
195196
labelMenuButton: const Color(0xffffffff).withValues(alpha: 0.85),
196197
mainBackground: const Color(0xff1d1d1d),
198+
pressedTint: const HSLColor.fromAHSL(0.04, 0, 0, 1).toColor(),
197199
textInput: const Color(0xffffffff).withValues(alpha: 0.9),
198200
title: const Color(0xffffffff),
199201
bgSearchInput: const Color(0xff313131),
@@ -251,6 +253,7 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
251253
required this.labelEdited,
252254
required this.labelMenuButton,
253255
required this.mainBackground,
256+
required this.pressedTint,
254257
required this.textInput,
255258
required this.title,
256259
required this.bgSearchInput,
@@ -309,6 +312,7 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
309312
final Color labelEdited;
310313
final Color labelMenuButton;
311314
final Color mainBackground;
315+
final Color pressedTint;
312316
final Color textInput;
313317
final Color title;
314318
final Color bgSearchInput;
@@ -362,6 +366,7 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
362366
Color? labelEdited,
363367
Color? labelMenuButton,
364368
Color? mainBackground,
369+
Color? pressedTint,
365370
Color? textInput,
366371
Color? title,
367372
Color? bgSearchInput,
@@ -410,6 +415,7 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
410415
labelEdited: labelEdited ?? this.labelEdited,
411416
labelMenuButton: labelMenuButton ?? this.labelMenuButton,
412417
mainBackground: mainBackground ?? this.mainBackground,
418+
pressedTint: pressedTint ?? this.pressedTint,
413419
textInput: textInput ?? this.textInput,
414420
title: title ?? this.title,
415421
bgSearchInput: bgSearchInput ?? this.bgSearchInput,
@@ -465,6 +471,7 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
465471
labelEdited: Color.lerp(labelEdited, other.labelEdited, t)!,
466472
labelMenuButton: Color.lerp(labelMenuButton, other.labelMenuButton, t)!,
467473
mainBackground: Color.lerp(mainBackground, other.mainBackground, t)!,
474+
pressedTint: Color.lerp(pressedTint, other.pressedTint, t)!,
468475
textInput: Color.lerp(textInput, other.textInput, t)!,
469476
title: Color.lerp(title, other.title, t)!,
470477
bgSearchInput: Color.lerp(bgSearchInput, other.bgSearchInput, t)!,

test/widgets/message_list_test.dart

+63-1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import 'package:zulip/widgets/message_list.dart';
2525
import 'package:zulip/widgets/page.dart';
2626
import 'package:zulip/widgets/store.dart';
2727
import 'package:zulip/widgets/channel_colors.dart';
28+
import 'package:zulip/widgets/theme.dart';
2829

2930
import '../api/fake_api.dart';
3031
import '../example_data.dart' as eg;
@@ -1100,7 +1101,7 @@ void main() {
11001101
.initNarrow.equals(DmNarrow.withUser(eg.otherUser.userId, selfUserId: eg.selfUser.userId));
11011102
await tester.pumpAndSettle();
11021103
});
1103-
1104+
11041105
testWidgets('does not navigate on tapping recipient header in DmNarrow', (tester) async {
11051106
final pushedRoutes = <Route<void>>[];
11061107
final navObserver = TestNavigatorObserver()
@@ -1226,6 +1227,67 @@ void main() {
12261227

12271228
debugNetworkImageHttpClientProvider = null;
12281229
});
1230+
1231+
group('action sheet visual feedback', () {
1232+
late Message message;
1233+
1234+
setUp(() {
1235+
message = eg.streamMessage();
1236+
});
1237+
1238+
Color? getBackgroundColor(WidgetTester tester) {
1239+
final decoratedBox = tester.widget<DecoratedBox>(
1240+
find.descendant(
1241+
of: find.byType(MessageWithPossibleSender),
1242+
matching: find.byType(DecoratedBox),
1243+
),
1244+
);
1245+
return (decoratedBox.decoration as BoxDecoration).color;
1246+
}
1247+
1248+
testWidgets('long-press opens action sheet', (tester) async {
1249+
await setupMessageListPage(tester, messages: [message]);
1250+
1251+
check(getBackgroundColor(tester)).equals(Colors.transparent);
1252+
1253+
await tester.longPress(find.byType(MessageWithPossibleSender));
1254+
await tester.pump();
1255+
1256+
final expectedTint = DesignVariables.of(tester.element(find.byType(MessageWithPossibleSender)))
1257+
.pressedTint;
1258+
1259+
check(getBackgroundColor(tester)).equals(expectedTint);
1260+
1261+
await tester.pumpAndSettle();
1262+
1263+
check(getBackgroundColor(tester)).equals(expectedTint);
1264+
1265+
await tester.tapAt(const Offset(0, 0));
1266+
await tester.pumpAndSettle();
1267+
1268+
check(getBackgroundColor(tester)).equals(Colors.transparent);
1269+
});
1270+
1271+
testWidgets('long-press canceled', (tester) async {
1272+
await setupMessageListPage(tester, messages: [message]);
1273+
1274+
check(getBackgroundColor(tester)).equals(Colors.transparent);
1275+
1276+
final gesture = await tester.startGesture(tester.getCenter(find.byType(MessageWithPossibleSender)));
1277+
await tester.pump();
1278+
1279+
final expectedTint = DesignVariables.of(tester.element(find.byType(MessageWithPossibleSender)))
1280+
.pressedTint;
1281+
1282+
check(getBackgroundColor(tester)).equals(expectedTint);
1283+
1284+
await gesture.moveBy(const Offset(0, 50));
1285+
await gesture.up();
1286+
await tester.pumpAndSettle();
1287+
1288+
check(getBackgroundColor(tester)).equals(Colors.transparent);
1289+
});
1290+
});
12291291
});
12301292

12311293
group('Starred messages', () {

0 commit comments

Comments
 (0)