Skip to content

Commit 8605d56

Browse files
committed
msglist: Add animated marker for unread messages
Adds a new _UnreadMarker that renders as a 4px solid border on the left edge of messages. Fixes: zulip#79
1 parent 28f1bd5 commit 8605d56

File tree

2 files changed

+84
-4
lines changed

2 files changed

+84
-4
lines changed

lib/widgets/message_list.dart

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -355,10 +355,45 @@ class MessageItem extends StatelessWidget {
355355
return StickyHeaderItem(
356356
allowOverflow: !item.isLastInBlock,
357357
header: RecipientHeader(message: message),
358-
child: Column(children: [
359-
MessageWithPossibleSender(item: item),
360-
if (trailing != null && item.isLastInBlock) trailing!,
361-
]));
358+
child: _UnreadMarker(
359+
isRead: message.flags.contains(MessageFlag.read),
360+
child: Column(children: [
361+
MessageWithPossibleSender(item: item),
362+
if (trailing != null && item.isLastInBlock) trailing!,
363+
])));
364+
}
365+
}
366+
367+
/// Widget responsible for showing the read status of a message.
368+
class _UnreadMarker extends StatelessWidget {
369+
const _UnreadMarker({required this.isRead, required this.child});
370+
371+
final bool isRead;
372+
final Widget child;
373+
374+
@override
375+
Widget build(BuildContext context) {
376+
return Stack(
377+
children: [
378+
child,
379+
Positioned(
380+
top: 0,
381+
left: 0,
382+
bottom: 0,
383+
width: 4,
384+
child: AnimatedOpacity(
385+
opacity: isRead ? 0 : 1,
386+
// Web uses 2s and 0.3s durations, and a CSS ease-out curve.
387+
// See zulip:web/styles/message_row.css .
388+
duration: Duration(milliseconds: isRead ? 2000 : 300),
389+
curve: Curves.easeOut,
390+
child: ColoredBox(
391+
// The color hsl(227deg 78% 59%) comes from the Figma mockup at:
392+
// https://www.figma.com/file/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=132-9684
393+
// See discussion about design at:
394+
// https://chat.zulip.org/#narrow/stream/243-mobile-team/topic/flutter.3A.20unread.20marker/near/1658008
395+
color: const HSLColor.fromAHSL(1, 227, 0.78, 0.59).toColor()))),
396+
]);
362397
}
363398
}
364399

test/widgets/message_list_test.dart

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,4 +300,49 @@ void main() {
300300
debugNetworkImageHttpClientProvider = null;
301301
});
302302
});
303+
304+
group('_UnreadMarker animations', () {
305+
Animation getAnimation(WidgetTester tester) {
306+
final widget = tester.widget<FadeTransition>(find.descendant(
307+
of: find.byType(MessageItem),
308+
matching: find.byType(FadeTransition)));
309+
return widget.opacity;
310+
}
311+
312+
testWidgets('from read to unread', (WidgetTester tester) async {
313+
final message = eg.streamMessage(flags: [MessageFlag.read]);
314+
await setupMessageListPage(tester, messages: [message]);
315+
316+
check(getAnimation(tester).value).equals(0);
317+
318+
store.handleEvent(eg.updateMessageFlagsRemoveEvent(
319+
MessageFlag.read, [message]));
320+
await tester.pump(); // process handleEvent
321+
check(getAnimation(tester).status).equals(AnimationStatus.forward);
322+
323+
await tester.pumpAndSettle();
324+
325+
check(getAnimation(tester).value).equals(1.0);
326+
});
327+
328+
testWidgets('from unread to read', (WidgetTester tester) async {
329+
final message = eg.streamMessage(flags: []);
330+
await setupMessageListPage(tester, messages: [message]);
331+
332+
check(getAnimation(tester).value).equals(1.0);
333+
334+
store.handleEvent(UpdateMessageFlagsAddEvent(
335+
id: 1,
336+
flag: MessageFlag.read,
337+
messages: [message.id],
338+
all: false,
339+
));
340+
await tester.pump(); // process handleEvent
341+
check(getAnimation(tester).status).equals(AnimationStatus.forward);
342+
343+
await tester.pumpAndSettle();
344+
345+
check(getAnimation(tester).value).equals(0);
346+
});
347+
});
303348
}

0 commit comments

Comments
 (0)