Skip to content

Commit 0156275

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 zulip#171. Signed-off-by: Zixuan James Li <[email protected]>
1 parent bbadf28 commit 0156275

File tree

4 files changed

+146
-14
lines changed

4 files changed

+146
-14
lines changed

lib/widgets/message_list.dart

+11-13
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,23 +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-
])),
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+
])),
977976
SizedBox(width: 16,
978977
child: message.flags.contains(MessageFlag.starred)
979978
? Icon(ZulipIcons.star_filled, size: 16, color: designVariables.star)
980-
: null),
981-
]),
979+
: null)]),
982980
])));
983981
}
984982
}

lib/widgets/swipable_message_row.dart

+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
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+
// Currently, only the collapsed state of the marker has been implemented,
44+
// where only the marker icon, not the marker text, is visible.
45+
static const double widthCollapsed = 16;
46+
47+
const _EditStateMarker({
48+
required MessageEditState editState,
49+
}) : _editState = editState;
50+
51+
final MessageEditState _editState;
52+
53+
@override
54+
Widget build(BuildContext context) {
55+
final designVariables = DesignVariables.of(context);
56+
57+
final IconData icon;
58+
final double iconSize;
59+
60+
switch (_editState) {
61+
case MessageEditState.none:
62+
return const SizedBox(width: widthCollapsed);
63+
case MessageEditState.edited:
64+
icon = ZulipIcons.edited;
65+
iconSize = 14;
66+
break;
67+
case MessageEditState.moved:
68+
icon = ZulipIcons.message_moved;
69+
iconSize = 8;
70+
break;
71+
}
72+
73+
return ConstrainedBox(
74+
constraints: const BoxConstraints(maxWidth: widthCollapsed),
75+
child: Padding(
76+
padding: const EdgeInsetsDirectional.only(start: 5, end: 3),
77+
child: Icon(icon, size: iconSize,
78+
color: designVariables.editedMovedMarkerCollapsed)));
79+
}
80+
}

lib/widgets/theme.dart

+22-1
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,13 @@ 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+
// TODO(#95) need dark-theme color
88+
editedMovedMarkerBg = const Color(0xffddecf6),
89+
// TODO(#95) need dark-theme color
90+
editedMovedMarkerExpanded = const Color(0xff26516e),
91+
// TODO(#95) need dark-theme color
92+
editedMovedMarkerCollapsed = const Color.fromARGB(128, 146, 167, 182);
8793

8894
DesignVariables._({
8995
required this.bgMain,
@@ -93,6 +99,9 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
9399
required this.title,
94100
required this.streamColorSwatches,
95101
required this.star,
102+
required this.editedMovedMarkerBg,
103+
required this.editedMovedMarkerExpanded,
104+
required this.editedMovedMarkerCollapsed,
96105
});
97106

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

117126
// Not named variables in Figma; taken from older Figma drafts, or elsewhere.
118127
final Color star;
128+
final Color editedMovedMarkerBg;
129+
final Color editedMovedMarkerExpanded;
130+
final Color editedMovedMarkerCollapsed;
119131

120132
@override
121133
DesignVariables copyWith({
@@ -126,6 +138,9 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
126138
Color? title,
127139
StreamColorSwatches? streamColorSwatches,
128140
Color? star,
141+
Color? editedMovedMarkerBg,
142+
Color? editedMovedMarkerExpanded,
143+
Color? editedMovedMarkerCollapsed,
129144
}) {
130145
return DesignVariables._(
131146
bgMain: bgMain ?? this.bgMain,
@@ -135,6 +150,9 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
135150
title: title ?? this.title,
136151
streamColorSwatches: streamColorSwatches ?? this.streamColorSwatches,
137152
star: star ?? this.star,
153+
editedMovedMarkerBg: editedMovedMarkerBg ?? this.editedMovedMarkerBg,
154+
editedMovedMarkerExpanded: editedMovedMarkerExpanded ?? this.editedMovedMarkerExpanded,
155+
editedMovedMarkerCollapsed: editedMovedMarkerCollapsed ?? this.editedMovedMarkerCollapsed,
138156
);
139157
}
140158

@@ -151,6 +169,9 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
151169
title: Color.lerp(title, other.title, t)!,
152170
streamColorSwatches: StreamColorSwatches.lerp(streamColorSwatches, other.streamColorSwatches, t),
153171
star: Color.lerp(star, other.star, t)!,
172+
editedMovedMarkerBg: Color.lerp(editedMovedMarkerBg, other.editedMovedMarkerBg, t)!,
173+
editedMovedMarkerExpanded: Color.lerp(editedMovedMarkerExpanded, other.editedMovedMarkerExpanded, t)!,
174+
editedMovedMarkerCollapsed: Color.lerp(editedMovedMarkerCollapsed, other.editedMovedMarkerCollapsed, t)!,
154175
);
155176
}
156177
}

test/widgets/message_list_test.dart

+33
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)