Skip to content

Commit 9366e47

Browse files
committed
ui: Add edited/moved marker.
This adds basic support to displaying a edited/moved marker next to the message content. As of now this is implemented without the swipe gesture control that would expand the marker. Partially addresses #171. Signed-off-by: Zixuan James Li <[email protected]>
1 parent f6bbdc8 commit 9366e47

File tree

5 files changed

+159
-16
lines changed

5 files changed

+159
-16
lines changed

assets/l10n/app_en.arb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,5 +479,13 @@
479479
"senderFullName": {"type": "String", "example": "Alice"},
480480
"numOthers": {"type": "int", "example": "4"}
481481
}
482+
},
483+
"messageIsEdited": "Edited",
484+
"@messageIsEdited": {
485+
"description": "Text that appears on a marker next to an edited message."
486+
},
487+
"messageIsMoved": "Moved",
488+
"@messageIsMoved": {
489+
"description": "Text that appears on a marker next to a moved message."
482490
}
483491
}

lib/widgets/message_list.dart

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import 'page.dart';
2121
import 'profile.dart';
2222
import 'sticky_header.dart';
2323
import 'store.dart';
24+
import 'swipable_message_row.dart';
2425
import 'text.dart';
2526
import 'theme.dart';
2627

@@ -962,22 +963,20 @@ class MessageWithPossibleSender extends StatelessWidget {
962963
if (senderRow != null)
963964
Padding(padding: const EdgeInsets.fromLTRB(16, 2, 16, 0),
964965
child: senderRow),
965-
Row(crossAxisAlignment: CrossAxisAlignment.baseline,
966-
textBaseline: localizedTextBaseline(context),
966+
SwipableMessageRow(
967+
message: message,
967968
children: [
968-
const SizedBox(width: 16),
969-
Expanded(
970-
child: Column(
971-
crossAxisAlignment: CrossAxisAlignment.stretch,
972-
children: [
973-
MessageContent(message: message, content: item.content),
974-
if ((message.reactions?.total ?? 0) > 0)
975-
ReactionChipsList(messageId: message.id, reactions: message.reactions!)
976-
])),
977-
SizedBox(width: 16,
978-
child: message.flags.contains(MessageFlag.starred)
979-
? Icon(ZulipIcons.star_filled, size: 16, color: designVariables.star)
980-
: null),
969+
Expanded(child: Column(
970+
crossAxisAlignment: CrossAxisAlignment.stretch,
971+
children: [
972+
MessageContent(message: message, content: item.content),
973+
if ((message.reactions?.total ?? 0) > 0)
974+
ReactionChipsList(messageId: message.id, reactions: message.reactions!)
975+
])),
976+
SizedBox(width: 16,
977+
child: message.flags.contains(MessageFlag.starred)
978+
? Icon(ZulipIcons.star_filled, size: 16, color: designVariables.star)
979+
: null),
981980
]),
982981
])));
983982
}

lib/widgets/swipable_message_row.dart

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:flutter/rendering.dart';
3+
4+
import '../api/model/model.dart';
5+
import 'icons.dart';
6+
import 'text.dart';
7+
import 'theme.dart';
8+
9+
class SwipableMessageRow extends StatefulWidget {
10+
const SwipableMessageRow({
11+
super.key,
12+
required this.children,
13+
required this.message,
14+
});
15+
16+
final List<Widget> children;
17+
final Message message;
18+
19+
@override
20+
State<StatefulWidget> createState() => _SwipableMessageRowState();
21+
}
22+
23+
class _SwipableMessageRowState extends State<SwipableMessageRow> {
24+
@override
25+
Widget build(BuildContext context) {
26+
final hasMarker = widget.message.editState != MessageEditState.none;
27+
28+
return Row(
29+
crossAxisAlignment: CrossAxisAlignment.baseline,
30+
textBaseline: localizedTextBaseline(context),
31+
children: [
32+
hasMarker
33+
? _EditStateMarker(editState: widget.message.editState)
34+
: const SizedBox(width: 16),
35+
...widget.children,
36+
],
37+
);
38+
}
39+
}
40+
41+
class _EditStateMarker extends StatelessWidget {
42+
/// The minimum width of the marker.
43+
//
44+
// Currently, only the collapsed state of the marker has been implemented,
45+
// where only the marker icon, not the marker text, is visible.
46+
static const double widthCollapsed = 16;
47+
48+
const _EditStateMarker({
49+
required MessageEditState editState,
50+
}) : _editState = editState;
51+
52+
final MessageEditState _editState;
53+
54+
@override
55+
Widget build(BuildContext context) {
56+
final designVariables = DesignVariables.of(context);
57+
58+
final IconData icon;
59+
final double iconSize;
60+
61+
switch (_editState) {
62+
case MessageEditState.none:
63+
return const SizedBox(width: widthCollapsed);
64+
case MessageEditState.edited:
65+
icon = ZulipIcons.edited;
66+
iconSize = 14;
67+
break;
68+
case MessageEditState.moved:
69+
icon = ZulipIcons.message_moved;
70+
iconSize = 8;
71+
break;
72+
}
73+
74+
return ConstrainedBox(
75+
constraints: const BoxConstraints(maxWidth: widthCollapsed),
76+
child: Padding(
77+
padding: const EdgeInsets.only(left: 5, right: 3),
78+
child: OverflowBox(
79+
fit: OverflowBoxFit.deferToChild,
80+
maxWidth: 8,
81+
child: Icon(icon, size: iconSize, color: designVariables.textMarkerCollapsed)),
82+
),
83+
);
84+
}
85+
}

lib/widgets/theme.dart

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,10 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
8383
title = const Color(0xff1a1a1a),
8484
streamColorSwatches = StreamColorSwatches.light,
8585
// TODO(#95) unchanged in dark theme?
86-
star = const HSLColor.fromAHSL(0.5, 47, 1, 0.41).toColor();
86+
star = const HSLColor.fromAHSL(0.5, 47, 1, 0.41).toColor(),
87+
bgMarker = const Color(0xffddecf6),
88+
textMarkerExpanded = const Color(0xff26516e),
89+
textMarkerCollapsed = const Color(0xff92a7b6);
8790

8891
DesignVariables._({
8992
required this.bgMain,
@@ -93,6 +96,9 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
9396
required this.title,
9497
required this.streamColorSwatches,
9598
required this.star,
99+
required this.bgMarker,
100+
required this.textMarkerExpanded,
101+
required this.textMarkerCollapsed,
96102
});
97103

98104
/// The [DesignVariables] from the context's active theme.
@@ -116,6 +122,9 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
116122

117123
// Not named variables in Figma; taken from older Figma drafts, or elsewhere.
118124
final Color star;
125+
final Color bgMarker;
126+
final Color textMarkerExpanded;
127+
final Color textMarkerCollapsed;
119128

120129
@override
121130
DesignVariables copyWith({
@@ -126,6 +135,9 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
126135
Color? title,
127136
StreamColorSwatches? streamColorSwatches,
128137
Color? star,
138+
Color? bgMarker,
139+
Color? textMarkerCollapsed,
140+
Color? textMarkerExpanded,
129141
}) {
130142
return DesignVariables._(
131143
bgMain: bgMain ?? this.bgMain,
@@ -135,6 +147,9 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
135147
title: title ?? this.title,
136148
streamColorSwatches: streamColorSwatches ?? this.streamColorSwatches,
137149
star: star ?? this.star,
150+
bgMarker: bgMarker ?? this.bgMarker,
151+
textMarkerExpanded: textMarkerCollapsed ?? this.textMarkerExpanded,
152+
textMarkerCollapsed: textMarkerExpanded ?? this.textMarkerCollapsed,
138153
);
139154
}
140155

@@ -151,6 +166,9 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
151166
title: Color.lerp(title, other.title, t)!,
152167
streamColorSwatches: StreamColorSwatches.lerp(streamColorSwatches, other.streamColorSwatches, t),
153168
star: Color.lerp(star, other.star, t)!,
169+
bgMarker: Color.lerp(bgMarker, other.bgMarker, t)!,
170+
textMarkerExpanded: Color.lerp(textMarkerExpanded, other.textMarkerExpanded, t)!,
171+
textMarkerCollapsed: Color.lerp(textMarkerCollapsed, other.textMarkerCollapsed, t)!,
154172
);
155173
}
156174
}

test/widgets/message_list_test.dart

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -576,6 +576,39 @@ void main() {
576576
});
577577
});
578578

579+
group('EditStateMarker', () {
580+
void checkMarkersCount({required int edited, required int moved}) {
581+
check(find.byIcon(ZulipIcons.edited).evaluate()).length.equals(edited);
582+
check(find.byIcon(ZulipIcons.message_moved).evaluate()).length.equals(moved);
583+
}
584+
585+
testWidgets('no edited or moved messages', (tester) async {
586+
final message = eg.streamMessage();
587+
await setupMessageListPage(tester, messages: [message]);
588+
checkMarkersCount(edited: 0, moved: 0);
589+
});
590+
591+
testWidgets('edited and moved messages from events', (tester) async {
592+
final message = eg.streamMessage();
593+
final message2 = eg.streamMessage();
594+
await setupMessageListPage(tester, messages: [message, message2]);
595+
checkMarkersCount(edited: 0, moved: 0);
596+
597+
await store.handleEvent(eg.updateMessageEditEvent(message, renderedContent: "edited"));
598+
await tester.pump();
599+
checkMarkersCount(edited: 1, moved: 0);
600+
601+
await store.handleEvent(eg.updateMessageMoveEvent(
602+
[message, message2], origTopic: 'old', newTopic: 'new'));
603+
await tester.pump();
604+
checkMarkersCount(edited: 1, moved: 1);
605+
606+
await store.handleEvent(eg.updateMessageEditEvent(message2, renderedContent: "edited"));
607+
await tester.pump();
608+
checkMarkersCount(edited: 2, moved: 0);
609+
});
610+
});
611+
579612
group('_UnreadMarker animations', () {
580613
// TODO: Improve animation state testing so it is less tied to
581614
// implementation details and more focused on output, see:

0 commit comments

Comments
 (0)