Skip to content

Commit 92274ad

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: #1142
1 parent 4fe4b2e commit 92274ad

File tree

5 files changed

+162
-42
lines changed

5 files changed

+162
-42
lines changed

lib/widgets/action_sheet.dart

Lines changed: 5 additions & 4 deletions
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/content.dart

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1319,7 +1319,6 @@ class MessageTableCell extends StatelessWidget {
13191319

13201320
void _launchUrl(BuildContext context, String urlString) async {
13211321
ModalStatus showError(BuildContext context, String? message) {
1322-
final zulipLocalizations = ZulipLocalizations.of(context);
13231322
return showErrorDialog(context: context,
13241323
title: 'Unable to open link',
13251324
message: [

lib/widgets/message_list.dart

Lines changed: 78 additions & 37 deletions
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(
@@ -1389,40 +1413,57 @@ class MessageWithPossibleSender extends StatelessWidget {
13891413

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

lib/widgets/theme.dart

Lines changed: 7 additions & 0 deletions
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

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import 'package:zulip/widgets/message_list.dart';
2424
import 'package:zulip/widgets/page.dart';
2525
import 'package:zulip/widgets/store.dart';
2626
import 'package:zulip/widgets/channel_colors.dart';
27+
import 'package:zulip/widgets/theme.dart';
2728

2829
import '../api/fake_api.dart';
2930
import '../example_data.dart' as eg;
@@ -1188,6 +1189,77 @@ void main() {
11881189

11891190
debugNetworkImageHttpClientProvider = null;
11901191
});
1192+
1193+
1194+
group('action sheet visual feedback', () {
1195+
late Message message;
1196+
1197+
setUp(() {
1198+
message = eg.streamMessage();
1199+
});
1200+
1201+
Color? getBackgroundColor(WidgetTester tester) {
1202+
final decoratedBox = tester.widget<DecoratedBox>(
1203+
find.descendant(
1204+
of: find.byType(MessageWithPossibleSender),
1205+
matching: find.byType(DecoratedBox),
1206+
),
1207+
);
1208+
return (decoratedBox.decoration as BoxDecoration).color;
1209+
}
1210+
1211+
testWidgets('starts with transparent background', (tester) async {
1212+
await setupMessageListPage(tester, messages: [message]);
1213+
1214+
check(getBackgroundColor(tester),
1215+
because: 'Message should start with transparent background',
1216+
).equals(Colors.transparent);
1217+
});
1218+
1219+
testWidgets('shows tint color when long pressed', (tester) async {
1220+
await setupMessageListPage(tester, messages: [message]);
1221+
1222+
await tester.longPress(find.byType(MessageWithPossibleSender));
1223+
await tester.pump();
1224+
1225+
final expectedTint = DesignVariables.of(tester.element(find.byType(MessageWithPossibleSender)))
1226+
.pressedTint;
1227+
1228+
check(getBackgroundColor(tester),
1229+
because: 'Message should show tint color during long press',
1230+
).equals(expectedTint);
1231+
});
1232+
1233+
testWidgets('returns to transparent after action sheet dismissal', (tester) async {
1234+
await setupMessageListPage(tester, messages: [message]);
1235+
1236+
await tester.longPress(find.byType(MessageWithPossibleSender));
1237+
await tester.pump();
1238+
1239+
await tester.tapAt(const Offset(0, 0));
1240+
await tester.pumpAndSettle();
1241+
1242+
check(getBackgroundColor(tester),
1243+
because: 'Message should return to transparent after dismissal',
1244+
).equals(Colors.transparent);
1245+
});
1246+
1247+
testWidgets('maintains tint color while action sheet is open', (tester) async {
1248+
await setupMessageListPage(tester, messages: [message]);
1249+
1250+
await tester.longPress(find.byType(MessageWithPossibleSender));
1251+
await tester.pump();
1252+
1253+
final expectedTint = DesignVariables.of(tester.element(find.byType(MessageWithPossibleSender)))
1254+
.pressedTint;
1255+
1256+
await tester.pump(const Duration(milliseconds: 500));
1257+
1258+
check(getBackgroundColor(tester),
1259+
because: 'Message should continue to show tint color while action sheet is visible',
1260+
).equals(expectedTint);
1261+
});
1262+
});
11911263
});
11921264

11931265
group('Starred messages', () {

0 commit comments

Comments
 (0)