-
Notifications
You must be signed in to change notification settings - Fork 310
Show marked/edited markers, without swipe gesture #762
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
import 'package:flutter/material.dart'; | ||
|
||
import '../api/model/model.dart'; | ||
import 'icons.dart'; | ||
import 'text.dart'; | ||
import 'theme.dart'; | ||
|
||
class EditStateMarker extends StatelessWidget { | ||
const EditStateMarker({ | ||
super.key, | ||
required this.editState, | ||
required this.children, | ||
}); | ||
|
||
final MessageEditState editState; | ||
final List<Widget> children; | ||
|
||
@override | ||
Widget build(BuildContext context) { | ||
final hasMarker = editState != MessageEditState.none; | ||
|
||
return Row( | ||
crossAxisAlignment: CrossAxisAlignment.baseline, | ||
textBaseline: localizedTextBaseline(context), | ||
children: [ | ||
hasMarker | ||
? _EditStateMarkerPill(editState: editState) | ||
: const SizedBox(width: _EditStateMarkerPill.widthCollapsed), | ||
...children, | ||
], | ||
); | ||
} | ||
} | ||
|
||
class _EditStateMarkerPill extends StatelessWidget { | ||
const _EditStateMarkerPill({required this.editState}); | ||
|
||
final MessageEditState editState; | ||
|
||
/// The minimum width of the marker. | ||
// Currently, only the collapsed state of the marker has been implemented, | ||
// where only the marker icon, not the marker text, is visible. | ||
static const double widthCollapsed = 16; | ||
|
||
@override | ||
Widget build(BuildContext context) { | ||
final designVariables = DesignVariables.of(context); | ||
|
||
final IconData icon; | ||
final Offset offset; | ||
switch (editState) { | ||
case MessageEditState.none: | ||
assert(false); | ||
return const SizedBox(width: widthCollapsed); | ||
case MessageEditState.edited: | ||
icon = ZulipIcons.edited; | ||
// These offsets are chosen ad hoc, but give a good vertical alignment | ||
// of the icons with the first line of the message, when the message | ||
// begins with a paragraph, at default text scaling. See: | ||
// https://github.com/zulip/zulip-flutter/pull/762#issuecomment-2232041922 | ||
offset = const Offset(0, 2); | ||
case MessageEditState.moved: | ||
icon = ZulipIcons.message_moved; | ||
offset = const Offset(0, 3); | ||
} | ||
|
||
return ConstrainedBox( | ||
constraints: const BoxConstraints(maxWidth: widthCollapsed), | ||
child: Transform.translate( | ||
offset: offset, | ||
child: Icon( | ||
icon, size: 16, color: designVariables.editedMovedMarkerCollapsed))); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,6 +21,7 @@ import 'page.dart'; | |
import 'profile.dart'; | ||
import 'sticky_header.dart'; | ||
import 'store.dart'; | ||
import 'edit_state_marker.dart'; | ||
import 'text.dart'; | ||
import 'theme.dart'; | ||
|
||
|
@@ -896,12 +897,10 @@ class MessageWithPossibleSender extends StatelessWidget { | |
|
||
final MessageListMessageItem item; | ||
|
||
// TODO(#95) unchanged in dark theme? | ||
static final _starColor = const HSLColor.fromAHSL(0.5, 47, 1, 0.41).toColor(); | ||
PIG208 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
@override | ||
Widget build(BuildContext context) { | ||
final store = PerAccountStoreWidget.of(context); | ||
final designVariables = DesignVariables.of(context); | ||
|
||
final message = item.message; | ||
final sender = store.users[message.senderId]; | ||
|
@@ -964,25 +963,21 @@ class MessageWithPossibleSender extends StatelessWidget { | |
if (senderRow != null) | ||
Padding(padding: const EdgeInsets.fromLTRB(16, 2, 16, 0), | ||
child: senderRow), | ||
Row(crossAxisAlignment: CrossAxisAlignment.start, children: [ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Neat! |
||
const SizedBox(width: 16), | ||
Expanded( | ||
child: Column( | ||
EditStateMarker( | ||
editState: message.editState, | ||
children: [ | ||
Expanded(child: Column( | ||
crossAxisAlignment: CrossAxisAlignment.stretch, | ||
children: [ | ||
MessageContent(message: message, content: item.content), | ||
if ((message.reactions?.total ?? 0) > 0) | ||
ReactionChipsList(messageId: message.id, reactions: message.reactions!) | ||
])), | ||
SizedBox(width: 16, | ||
child: message.flags.contains(MessageFlag.starred) | ||
// TODO(#157): fix how star marker aligns with message content | ||
// Design from Figma at: | ||
// https://www.figma.com/file/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=813%3A28817&mode=dev . | ||
? Padding(padding: const EdgeInsets.only(top: 4), | ||
child: Icon(ZulipIcons.star_filled, size: 16, color: _starColor)) | ||
: null), | ||
]), | ||
SizedBox(width: 16, | ||
child: message.flags.contains(MessageFlag.starred) | ||
? Icon(ZulipIcons.star_filled, size: 16, color: designVariables.star) | ||
: null), | ||
]), | ||
]))); | ||
} | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,6 +16,7 @@ import 'package:zulip/model/localizations.dart'; | |
import 'package:zulip/model/narrow.dart'; | ||
import 'package:zulip/model/store.dart'; | ||
import 'package:zulip/widgets/content.dart'; | ||
import 'package:zulip/widgets/emoji_reaction.dart'; | ||
import 'package:zulip/widgets/icons.dart'; | ||
import 'package:zulip/widgets/message_list.dart'; | ||
import 'package:zulip/widgets/store.dart'; | ||
|
@@ -576,6 +577,99 @@ void main() { | |
}); | ||
}); | ||
|
||
group('EditStateMarker', () { | ||
void checkMarkersCount({required int edited, required int moved}) { | ||
check(find.byIcon(ZulipIcons.edited).evaluate()).length.equals(edited); | ||
check(find.byIcon(ZulipIcons.message_moved).evaluate()).length.equals(moved); | ||
} | ||
|
||
testWidgets('no edited or moved messages', (WidgetTester tester) async { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (The explicit type here is fine, but FWIW we most often leave it out in this context — it gets inferred just fine from the OTOH in the upstream Flutter tree you'll see these types always, because they use a lint rule to always have explicit types.) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see. I added those in the previous revision because I saw we doing that in the surrounding test cases. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, matching the surrounding code is a good instinct. Like I said, having the explicit type here is pretty harmless. But maybe I'll do a quick sweep to remove them for |
||
final message = eg.streamMessage(); | ||
await setupMessageListPage(tester, messages: [message]); | ||
checkMarkersCount(edited: 0, moved: 0); | ||
}); | ||
|
||
testWidgets('edited and moved messages from events', (WidgetTester tester) async { | ||
final message = eg.streamMessage(); | ||
final message2 = eg.streamMessage(); | ||
await setupMessageListPage(tester, messages: [message, message2]); | ||
checkMarkersCount(edited: 0, moved: 0); | ||
|
||
await store.handleEvent(eg.updateMessageEditEvent(message, renderedContent: 'edited')); | ||
await tester.pump(); | ||
checkMarkersCount(edited: 1, moved: 0); | ||
|
||
await store.handleEvent(eg.updateMessageMoveEvent( | ||
[message, message2], origTopic: 'old', newTopic: 'new')); | ||
await tester.pump(); | ||
checkMarkersCount(edited: 1, moved: 1); | ||
|
||
await store.handleEvent(eg.updateMessageEditEvent(message2, renderedContent: 'edited')); | ||
await tester.pump(); | ||
checkMarkersCount(edited: 2, moved: 0); | ||
}); | ||
|
||
List<List<(String, Rect)>> captureMessageRects( | ||
WidgetTester tester, | ||
List<Message> messages, | ||
Message targetMessage, | ||
) { | ||
assert(messages.contains(targetMessage)); | ||
final List<List<(String, Rect)>> result = []; | ||
for (final message in messages) { | ||
final baseFinder = find.byWidgetPredicate( | ||
(widget) => widget is MessageWithPossibleSender | ||
&& widget.item.message.id == message.id); | ||
|
||
Rect getRectMatching(Finder matching) { | ||
final finder = find.descendant( | ||
of: baseFinder, matching: matching)..tryEvaluate(); | ||
check(finder.found, because: '${message.content}: $matching') | ||
.length.equals(1); | ||
return tester.getRect(finder.first); | ||
} | ||
|
||
result.add([ | ||
('MessageWithPossibleSender', tester.getRect(baseFinder)), | ||
('MessageContent', getRectMatching(find.byType(MessageContent))), | ||
('Paragraph', getRectMatching(find.byType(Paragraph))), | ||
if (message.id == targetMessage.id) ...[ | ||
('Star', getRectMatching(find.byIcon(ZulipIcons.star_filled))), | ||
('ReactionChipsList', getRectMatching(find.byType(ReactionChipsList))), | ||
], | ||
]); | ||
} | ||
return result; | ||
} | ||
|
||
testWidgets('edit state updates do not affect layout', (WidgetTester tester) async { | ||
final messages = [ | ||
eg.streamMessage(), | ||
eg.streamMessage( | ||
reactions: [eg.unicodeEmojiReaction, eg.realmEmojiReaction], | ||
flags: [MessageFlag.starred]), | ||
eg.streamMessage(), | ||
]; | ||
final StreamMessage messageWithMarker = messages[1]; | ||
await setupMessageListPage(tester, messages: messages); | ||
final rectsBefore = captureMessageRects(tester, messages, messageWithMarker); | ||
checkMarkersCount(edited: 0, moved: 0); | ||
|
||
await store.handleEvent(eg.updateMessageMoveEvent( | ||
[messageWithMarker], origTopic: 'old', newTopic: messageWithMarker.topic)); | ||
await tester.pump(); | ||
check(captureMessageRects(tester, messages, messageWithMarker)) | ||
.deepEquals(rectsBefore); | ||
checkMarkersCount(edited: 0, moved: 1); | ||
|
||
await store.handleEvent(eg.updateMessageEditEvent(messageWithMarker)); | ||
await tester.pump(); | ||
check(captureMessageRects(tester, messages, messageWithMarker)) | ||
.deepEquals(rectsBefore); | ||
checkMarkersCount(edited: 1, moved: 0); | ||
}); | ||
}); | ||
|
||
group('_UnreadMarker animations', () { | ||
// TODO: Improve animation state testing so it is less tied to | ||
// implementation details and more focused on output, see: | ||
|
Uh oh!
There was an error while loading. Please reload this page.