Skip to content

Commit 7fb60a8

Browse files
committed
ui [nfc]: 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. This partially addresses #171. Signed-off-by: Zixuan James Li <[email protected]>
1 parent 251d29b commit 7fb60a8

File tree

4 files changed

+170
-26
lines changed

4 files changed

+170
-26
lines changed

assets/l10n/app_en.arb

+8
Original file line numberDiff line numberDiff line change
@@ -471,5 +471,13 @@
471471
"senderFullName": {"type": "String", "example": "Alice"},
472472
"numOthers": {"type": "int", "example": "4"}
473473
}
474+
},
475+
"messageIsEdited": "Edited",
476+
"@messageIsEdited": {
477+
"description": "Text that appears on a marker next to an edited message."
478+
},
479+
"messageIsMoved": "Moved",
480+
"@messageIsMoved": {
481+
"description": "Text that appears on a marker next to an moved message."
474482
}
475483
}

lib/widgets/edit_state_marker.dart

+127
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:flutter/rendering.dart';
3+
import 'package:flutter_gen/gen_l10n/zulip_localizations.dart';
4+
5+
import '../api/model/model.dart';
6+
import 'icons.dart';
7+
import 'theme.dart';
8+
9+
class SwipableMessageRow extends StatefulWidget {
10+
const SwipableMessageRow({
11+
super.key,
12+
required this.child,
13+
required this.message,
14+
});
15+
16+
final Widget child;
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 theme = Theme.of(context).extension<DesignVariables>()!;
27+
28+
// TODO(#157): fix how star marker aligns with message content
29+
// Design from Figma at:
30+
// https://www.figma.com/file/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=813%3A28817&mode=dev .
31+
var star = Padding(padding: const EdgeInsets.only(top: 5.5),
32+
child: Icon(ZulipIcons.star_filled, size: 16, color: theme.starColor));
33+
final hasMarker = widget.message.editState != MessageEditState.none;
34+
35+
return Stack(
36+
children: [
37+
if (hasMarker) Positioned(
38+
left: 0,
39+
child: EditStateMarker(editState: widget.message.editState)),
40+
Padding(
41+
padding: const EdgeInsets.only(left: 16, right: 16),
42+
child: widget.child),
43+
if (widget.message.flags.contains(MessageFlag.starred))
44+
Positioned(
45+
right: 0,
46+
child: star),
47+
],
48+
);
49+
}
50+
}
51+
52+
class EditStateMarker extends StatelessWidget {
53+
/// The minimum width of the marker.
54+
///
55+
/// Currently, only the collapsed state of the marker has been implemented,
56+
/// where only the marker icon, not the marker text, is visible.
57+
static const double widthCollapsed = 16;
58+
59+
const EditStateMarker({
60+
super.key,
61+
required MessageEditState editState,
62+
}) : _editState = editState;
63+
64+
final MessageEditState _editState;
65+
66+
@override
67+
Widget build(BuildContext context) {
68+
final theme = DesignVariables.of(context);
69+
final zulipLocalizations = ZulipLocalizations.of(context);
70+
71+
final IconData icon;
72+
final double iconSize;
73+
final String markerText;
74+
75+
switch (_editState) {
76+
case MessageEditState.none:
77+
return const SizedBox(width: widthCollapsed);
78+
case MessageEditState.edited:
79+
icon = ZulipIcons.edited;
80+
iconSize = 14;
81+
markerText = zulipLocalizations.messageIsEdited;
82+
break;
83+
case MessageEditState.moved:
84+
icon = ZulipIcons.message_moved;
85+
iconSize = 8;
86+
markerText = zulipLocalizations.messageIsMoved;
87+
break;
88+
}
89+
90+
var marker = Row(
91+
mainAxisAlignment: MainAxisAlignment.end,
92+
mainAxisSize: MainAxisSize.min,
93+
children: [
94+
Flexible(
95+
fit: FlexFit.loose,
96+
// For now, [markerText] is not displayed because it is transparent and
97+
// there is not enough space in the parent ConstrainedBox.
98+
child: Text('$markerText ',
99+
overflow: TextOverflow.clip,
100+
softWrap: false,
101+
textAlign: TextAlign.center,
102+
style: TextStyle(fontSize: 15, color: theme.textMarker.withAlpha(0)))),
103+
// To match the Figma design, we cannot make the collapsed width of the
104+
// marker larger. We need to explicitly allow the icon to overflow.
105+
OverflowBox(
106+
fit: OverflowBoxFit.deferToChild,
107+
maxWidth: 10,
108+
child: Icon(icon, size: iconSize, color: theme.textMarkerLight),
109+
),
110+
],
111+
);
112+
113+
return ConstrainedBox(
114+
constraints: const BoxConstraints(maxWidth: widthCollapsed),
115+
child: Container(
116+
margin: const EdgeInsets.only(top: 4, left: 0),
117+
clipBehavior: Clip.hardEdge,
118+
decoration: BoxDecoration(
119+
borderRadius: BorderRadius.circular(2),
120+
color: theme.bgMarker.withAlpha(0)),
121+
child: Padding(
122+
padding: const EdgeInsets.all(1.0),
123+
child: marker),
124+
),
125+
);
126+
}
127+
}

lib/widgets/message_list.dart

+16-25
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import 'action_sheet.dart';
1515
import 'compose_box.dart';
1616
import 'content.dart';
1717
import 'dialog.dart';
18+
import 'edit_state_marker.dart';
1819
import 'emoji_reaction.dart';
1920
import 'icons.dart';
2021
import 'page.dart';
@@ -586,11 +587,11 @@ class MessageItem extends StatelessWidget {
586587
header: header,
587588
child: _UnreadMarker(
588589
isRead: message.flags.contains(MessageFlag.read),
589-
child: ColoredBox(
590-
color: Colors.white,
591-
child: Column(children: [
592-
MessageWithPossibleSender(item: item),
593-
if (trailingWhitespace != null && item.isLastInBlock) SizedBox(height: trailingWhitespace!),
590+
child: ColoredBox(
591+
color: Colors.white,
592+
child: Column(children: [
593+
MessageWithPossibleSender(item: item),
594+
if (trailingWhitespace != null && item.isLastInBlock) SizedBox(height: trailingWhitespace!),
594595
]))));
595596
}
596597
}
@@ -899,7 +900,6 @@ class MessageWithPossibleSender extends StatelessWidget {
899900
@override
900901
Widget build(BuildContext context) {
901902
final store = PerAccountStoreWidget.of(context);
902-
final theme = DesignVariables.of(context);
903903

904904
final message = item.message;
905905
final sender = store.users[message.senderId];
@@ -962,25 +962,16 @@ class MessageWithPossibleSender extends StatelessWidget {
962962
if (senderRow != null)
963963
Padding(padding: const EdgeInsets.fromLTRB(16, 2, 16, 0),
964964
child: senderRow),
965-
Row(crossAxisAlignment: CrossAxisAlignment.start, children: [
966-
const SizedBox(width: 16),
967-
Expanded(
968-
child: Column(
969-
crossAxisAlignment: CrossAxisAlignment.stretch,
970-
children: [
971-
MessageContent(message: message, content: item.content),
972-
if ((message.reactions?.total ?? 0) > 0)
973-
ReactionChipsList(messageId: message.id, reactions: message.reactions!)
974-
])),
975-
SizedBox(width: 16,
976-
child: message.flags.contains(MessageFlag.starred)
977-
// TODO(#157): fix how star marker aligns with message content
978-
// Design from Figma at:
979-
// https://www.figma.com/file/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=813%3A28817&mode=dev .
980-
? Padding(padding: const EdgeInsets.only(top: 4),
981-
child: Icon(ZulipIcons.star_filled, size: 16, color: theme.starColor))
982-
: null),
983-
]),
965+
SwipableMessageRow(
966+
message: message,
967+
child: Column(
968+
crossAxisAlignment: CrossAxisAlignment.stretch,
969+
children: [
970+
MessageContent(message: message, content: item.content),
971+
if ((message.reactions?.total ?? 0) > 0)
972+
ReactionChipsList(messageId: message.id, reactions: message.reactions!)
973+
])
974+
),
984975
])));
985976
}
986977
}

lib/widgets/theme.dart

+19-1
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,10 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
8282
icon = const Color(0xff666699),
8383
title = const Color(0xff1a1a1a),
8484
streamColorSwatches = StreamColorSwatches.light,
85-
starColor = const HSLColor.fromAHSL(0.5, 47, 1, 0.41).toColor();
85+
starColor = const HSLColor.fromAHSL(0.5, 47, 1, 0.41).toColor(),
86+
bgMarker = const Color(0xffddecf6),
87+
textMarker = const Color(0xff26516e),
88+
textMarkerLight = const Color(0xff92a7b6);
8689

8790
DesignVariables._({
8891
required this.bgMain,
@@ -92,6 +95,9 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
9295
required this.title,
9396
required this.streamColorSwatches,
9497
required this.starColor,
98+
required this.bgMarker,
99+
required this.textMarker,
100+
required this.textMarkerLight,
95101
});
96102

97103
/// The [DesignVariables] from the context's active theme.
@@ -113,6 +119,9 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
113119
// Not exactly from the Figma design, but from Vlad anyway.
114120
final StreamColorSwatches streamColorSwatches;
115121
final Color starColor;
122+
final Color bgMarker;
123+
final Color textMarker;
124+
final Color textMarkerLight;
116125

117126
@override
118127
DesignVariables copyWith({
@@ -123,6 +132,9 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
123132
Color? title,
124133
StreamColorSwatches? streamColorSwatches,
125134
Color? starColor,
135+
Color? bgMarker,
136+
Color? textMarker,
137+
Color? textMarkerLight,
126138
}) {
127139
return DesignVariables._(
128140
bgMain: bgMain ?? this.bgMain,
@@ -132,6 +144,9 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
132144
title: title ?? this.title,
133145
streamColorSwatches: streamColorSwatches ?? this.streamColorSwatches,
134146
starColor: starColor ?? this.starColor,
147+
bgMarker: bgMarker ?? this.bgMarker,
148+
textMarker: textMarker ?? this.textMarker,
149+
textMarkerLight: textMarkerLight ?? this.textMarkerLight,
135150
);
136151
}
137152

@@ -148,6 +163,9 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
148163
title: Color.lerp(title, other.title, t)!,
149164
streamColorSwatches: StreamColorSwatches.lerp(streamColorSwatches, other.streamColorSwatches, t),
150165
starColor: Color.lerp(starColor, other.starColor, t)!,
166+
bgMarker: Color.lerp(bgMarker, other.bgMarker, t)!,
167+
textMarker: Color.lerp(textMarker, other.textMarker, t)!,
168+
textMarkerLight: Color.lerp(textMarkerLight, other.textMarkerLight, t)!,
151169
);
152170
}
153171
}

0 commit comments

Comments
 (0)