Skip to content

Commit 6b606a8

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 a14c640 commit 6b606a8

File tree

4 files changed

+159
-40
lines changed

4 files changed

+159
-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

+84-36
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
import 'dart:async';
12
import 'dart:math';
23

34
import 'package:collection/collection.dart';
5+
import 'package:flutter/gestures.dart';
46
import 'package:flutter/material.dart';
57
import 'package:flutter_color_models/flutter_color_models.dart';
68
import 'package:intl/intl.dart' hide TextDirection;
@@ -16,6 +18,7 @@ import 'actions.dart';
1618
import 'app_bar.dart';
1719
import 'compose_box.dart';
1820
import 'content.dart';
21+
import 'dialog.dart';
1922
import 'emoji_reaction.dart';
2023
import 'icons.dart';
2124
import 'page.dart';
@@ -1318,22 +1321,45 @@ String formatHeaderDate(
13181321
// Design referenced from:
13191322
// - https://github.com/zulip/zulip-mobile/issues/5511
13201323
// - https://www.figma.com/file/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=538%3A20849&mode=dev
1321-
class MessageWithPossibleSender extends StatelessWidget {
1324+
class MessageWithPossibleSender extends StatefulWidget {
13221325
const MessageWithPossibleSender({super.key, required this.item});
13231326

13241327
final MessageListMessageItem item;
13251328

1329+
@override
1330+
State<MessageWithPossibleSender> createState() => _MessageWithPossibleSenderState();
1331+
}
1332+
1333+
class _MessageWithPossibleSenderState extends State<MessageWithPossibleSender> {
1334+
final WidgetStatesController statesController = WidgetStatesController();
1335+
1336+
@override
1337+
void initState() {
1338+
super.initState();
1339+
statesController.addListener(() {
1340+
setState(() {
1341+
// Force a rebuild to resolve background color
1342+
});
1343+
});
1344+
}
1345+
1346+
@override
1347+
void dispose() {
1348+
statesController.dispose();
1349+
super.dispose();
1350+
}
1351+
13261352
@override
13271353
Widget build(BuildContext context) {
13281354
final store = PerAccountStoreWidget.of(context);
13291355
final messageListTheme = MessageListTheme.of(context);
13301356
final designVariables = DesignVariables.of(context);
13311357

1332-
final message = item.message;
1358+
final message = widget.item.message;
13331359
final sender = store.getUser(message.senderId);
13341360

13351361
Widget? senderRow;
1336-
if (item.showSender) {
1362+
if (widget.item.showSender) {
13371363
final time = _kMessageTimestampFormat
13381364
.format(DateTime.fromMillisecondsSinceEpoch(1000 * message.timestamp));
13391365
senderRow = Row(
@@ -1400,40 +1426,62 @@ class MessageWithPossibleSender extends StatelessWidget {
14001426
child: Icon(ZulipIcons.star_filled, size: 16, color: designVariables.star));
14011427
}
14021428

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

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
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import 'package:zulip/widgets/message_list.dart';
2626
import 'package:zulip/widgets/page.dart';
2727
import 'package:zulip/widgets/store.dart';
2828
import 'package:zulip/widgets/channel_colors.dart';
29+
import 'package:zulip/widgets/theme.dart';
2930

3031
import '../api/fake_api.dart';
3132
import '../example_data.dart' as eg;
@@ -1314,6 +1315,68 @@ void main() {
13141315

13151316
debugNetworkImageHttpClientProvider = null;
13161317
});
1318+
1319+
group('background-color tint', () {
1320+
late Message message;
1321+
1322+
setUp(() {
1323+
message = eg.streamMessage();
1324+
});
1325+
1326+
Color? getBackgroundColor(WidgetTester tester) {
1327+
final decoratedBox = tester.widget<DecoratedBox>(
1328+
find.descendant(
1329+
of: find.byType(MessageWithPossibleSender),
1330+
matching: find.byType(DecoratedBox)));
1331+
return (decoratedBox.decoration as BoxDecoration).color;
1332+
}
1333+
1334+
testWidgets('long-press opens action sheet', (tester) async {
1335+
await setupMessageListPage(tester, messages: [message]);
1336+
1337+
check(getBackgroundColor(tester)).equals(Colors.transparent);
1338+
1339+
final gesture = await tester.startGesture(tester.getCenter(find.byType(MessageWithPossibleSender)));
1340+
await tester.pump(const Duration(milliseconds: 300));
1341+
1342+
final expectedTint = DesignVariables.of(tester.element(find.byType(MessageWithPossibleSender)))
1343+
.pressedTint;
1344+
1345+
check(getBackgroundColor(tester)).equals(expectedTint);
1346+
1347+
await tester.pump(const Duration(milliseconds: 300));
1348+
1349+
await gesture.up();
1350+
await tester.pump();
1351+
check(getBackgroundColor(tester)).equals(expectedTint);
1352+
1353+
await tester.pump(const Duration(milliseconds: 250));
1354+
1355+
await tester.tapAt(const Offset(0, 0));
1356+
await tester.pumpAndSettle();
1357+
check(getBackgroundColor(tester)).equals(Colors.transparent);
1358+
});
1359+
1360+
testWidgets('long-press canceled', (tester) async {
1361+
await setupMessageListPage(tester, messages: [message]);
1362+
1363+
check(getBackgroundColor(tester)).equals(Colors.transparent);
1364+
1365+
final gesture = await tester.startGesture(tester.getCenter(find.byType(MessageWithPossibleSender)));
1366+
await tester.pump();
1367+
1368+
final expectedTint = DesignVariables.of(tester.element(find.byType(MessageWithPossibleSender)))
1369+
.pressedTint;
1370+
1371+
check(getBackgroundColor(tester)).equals(expectedTint);
1372+
1373+
await gesture.moveBy(const Offset(0, 50));
1374+
await tester.pump();
1375+
1376+
check(getBackgroundColor(tester)).equals(Colors.transparent);
1377+
await gesture.up();
1378+
});
1379+
});
13171380
});
13181381

13191382
group('Starred messages', () {

0 commit comments

Comments
 (0)