Skip to content

Commit afd4add

Browse files
committed
theme: Extract starColor to DesignVariables.
Signed-off-by: Zixuan James Li <[email protected]>
1 parent 575d978 commit afd4add

File tree

4 files changed

+260
-28
lines changed

4 files changed

+260
-28
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

+211
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
import 'dart:ui';
2+
3+
import 'package:flutter/material.dart';
4+
import 'package:flutter/rendering.dart';
5+
import 'package:flutter_gen/gen_l10n/zulip_localizations.dart';
6+
7+
import '../api/model/model.dart';
8+
import 'icons.dart';
9+
import 'theme.dart';
10+
11+
class SwipableMessageRow extends StatefulWidget {
12+
const SwipableMessageRow({
13+
super.key,
14+
required this.child,
15+
required this.message,
16+
this.movementDuration = const Duration(milliseconds: 200),
17+
});
18+
19+
final Duration movementDuration;
20+
final Widget child;
21+
final Message message;
22+
23+
@override
24+
State<StatefulWidget> createState() => _SwipableMessageRowState();
25+
}
26+
27+
class _SwipableMessageRowState extends State<SwipableMessageRow> with TickerProviderStateMixin {
28+
@override
29+
void initState() {
30+
super.initState();
31+
_controller = AnimationController(
32+
duration: widget.movementDuration,
33+
vsync: this);
34+
_animation = Tween<double>(begin: 0, end: 1).animate(_controller)
35+
..addListener(() {
36+
setState(() {});
37+
});
38+
}
39+
40+
@override
41+
void dispose() {
42+
_controller.dispose();
43+
super.dispose();
44+
}
45+
46+
late AnimationController _controller;
47+
late Animation<double> _animation;
48+
49+
double _dragExtent = 0;
50+
double get _overallDragExtent => context.size!.width / 2;
51+
52+
void _handleDragStart(DragStartDetails details) {
53+
_dragExtent = 0;
54+
}
55+
56+
void _handleDragUpdate(DragUpdateDetails details) {
57+
_dragExtent += details.delta.dx;
58+
_controller.value = _dragExtent / _overallDragExtent;
59+
}
60+
61+
void _handleDragEnd(DragEndDetails details) {
62+
_controller.reverse();
63+
}
64+
65+
@override
66+
Widget build(BuildContext context) {
67+
// on start:
68+
// remember drag start position
69+
// calculate relative position x
70+
//
71+
// on update:
72+
// the marker rect follows the pointer until it becomes a bit wider
73+
// than the maximum width (>
74+
75+
// Initial state
76+
// No text, no background, lighter colored icon
77+
// Final state
78+
// Text, light blue background, darker colored icon, text pushed to the right
79+
final theme = Theme.of(context).extension<DesignVariables>()!;
80+
81+
var marker = EditStateMarker(editState: widget.message.editState, animation: _animation);
82+
83+
final content = Stack(
84+
children: [
85+
marker,
86+
Row(
87+
crossAxisAlignment: CrossAxisAlignment.start,
88+
children: [
89+
SizedBox(width: marker.marginLeft + marker.marginRight + marker.width),
90+
Expanded(child: widget.child),
91+
SizedBox(width: 16,
92+
child: widget.message.flags.contains(MessageFlag.starred)
93+
// TODO(#157): fix how star marker aligns with message content
94+
// Design from Figma at:
95+
// https://www.figma.com/file/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=813%3A28817&mode=dev .
96+
? Padding(padding: const EdgeInsets.only(top: 5.5),
97+
child: Icon(ZulipIcons.star_filled, size: 16, color: theme.starColor))
98+
: null),
99+
]),
100+
],
101+
);
102+
103+
if (widget.message.editState == MessageEditState.none) return content;
104+
105+
return GestureDetector(
106+
onHorizontalDragStart: _handleDragStart,
107+
onHorizontalDragEnd: _handleDragEnd,
108+
onHorizontalDragUpdate: _handleDragUpdate,
109+
child: content,
110+
);
111+
}
112+
}
113+
114+
class EditStateMarker extends StatelessWidget {
115+
const EditStateMarker({
116+
super.key,
117+
required this.editState,
118+
required Animation<double> animation,
119+
}) : _animation = animation;
120+
121+
final Animation<double> _animation;
122+
final MessageEditState editState;
123+
124+
static const double _marginLeftCollapsed = 5;
125+
static const double _marignRightCollapsed = 0;
126+
static const double _widthCollapsed = 10;
127+
128+
static const double _marginLeftExpanded = 9;
129+
static const double _marginRightExpanded = 3;
130+
static const double _widthExpanded = 60;
131+
static const double _markerFontSize = 15;
132+
133+
double get marginLeft => lerpDouble(
134+
_marginLeftCollapsed,
135+
_marginLeftExpanded,
136+
_animation.value)!;
137+
double get marginRight => lerpDouble(
138+
_marignRightCollapsed,
139+
_marginRightExpanded,
140+
_animation.value)!;
141+
double get width => lerpDouble(
142+
_widthCollapsed,
143+
_widthExpanded,
144+
_animation.value)!;
145+
146+
@override
147+
Widget build(BuildContext context) {
148+
final DesignVariables theme = DesignVariables.of(context);
149+
final IconData icon;
150+
final double iconSize;
151+
final String markerText;
152+
final double iconRightMargin;
153+
final zulipLocalizations = ZulipLocalizations.of(context);
154+
155+
switch (editState) {
156+
case MessageEditState.none:
157+
return const SizedBox(width: _marginLeftCollapsed + _widthCollapsed + _marignRightCollapsed);
158+
case MessageEditState.edited:
159+
icon = ZulipIcons.edited;
160+
iconSize = 14;
161+
iconRightMargin = 0;
162+
markerText = zulipLocalizations.messageIsEdited;
163+
break;
164+
case MessageEditState.moved:
165+
icon = ZulipIcons.message_moved;
166+
iconSize = 8;
167+
iconRightMargin = 1;
168+
markerText = zulipLocalizations.messageIsMoved;
169+
break;
170+
}
171+
172+
var marker = Row(
173+
mainAxisAlignment: MainAxisAlignment.end,
174+
children: [
175+
// Apply animation for both the text and the icon
176+
Expanded(child: Text(markerText,
177+
overflow: TextOverflow.clip,
178+
softWrap: false,
179+
textAlign: TextAlign.center,
180+
style: TextStyle(fontSize: _markerFontSize, color: Color.lerp(
181+
theme.textMarker.withAlpha(0),
182+
theme.textMarker,
183+
_animation.value)))),
184+
OverflowBox(
185+
fit: OverflowBoxFit.deferToChild,
186+
maxWidth: _widthCollapsed,
187+
child: Icon(icon, size: iconSize, color: Color.lerp(
188+
theme.textMarkerLight,
189+
theme.textMarker,
190+
_animation.value)),
191+
),
192+
SizedBox(width: iconRightMargin * _animation.value),
193+
],
194+
);
195+
196+
return Padding(
197+
padding: EdgeInsets.only(top: 4, left: marginLeft),
198+
child: Container(
199+
height: _markerFontSize * 1.5,
200+
width: width,
201+
clipBehavior: Clip.hardEdge,
202+
decoration: BoxDecoration(
203+
borderRadius: BorderRadius.circular(2),
204+
color: Color.lerp(
205+
theme.bgMarker.withAlpha(0),
206+
theme.bgMarker,
207+
_animation.value)),
208+
child: marker,
209+
));
210+
}
211+
}

lib/widgets/message_list.dart

+16-27
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
}
@@ -896,9 +897,6 @@ class MessageWithPossibleSender extends StatelessWidget {
896897

897898
final MessageListMessageItem item;
898899

899-
// TODO(#95) unchanged in dark theme?
900-
static final _starColor = const HSLColor.fromAHSL(0.5, 47, 1, 0.41).toColor();
901-
902900
@override
903901
Widget build(BuildContext context) {
904902
final store = PerAccountStoreWidget.of(context);
@@ -964,25 +962,16 @@ class MessageWithPossibleSender extends StatelessWidget {
964962
if (senderRow != null)
965963
Padding(padding: const EdgeInsets.fromLTRB(16, 2, 16, 0),
966964
child: senderRow),
967-
Row(crossAxisAlignment: CrossAxisAlignment.start, 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-
// TODO(#157): fix how star marker aligns with message content
980-
// Design from Figma at:
981-
// https://www.figma.com/file/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=813%3A28817&mode=dev .
982-
? Padding(padding: const EdgeInsets.only(top: 4),
983-
child: Icon(ZulipIcons.star_filled, size: 16, color: _starColor))
984-
: null),
985-
]),
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+
),
986975
])));
987976
}
988977
}

lib/widgets/theme.dart

+25-1
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,11 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
8181
borderBar = const Color(0x33000000),
8282
icon = const Color(0xff666699),
8383
title = const Color(0xff1a1a1a),
84-
streamColorSwatches = StreamColorSwatches.light;
84+
streamColorSwatches = StreamColorSwatches.light,
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);
8589

8690
DesignVariables._({
8791
required this.bgMain,
@@ -90,6 +94,10 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
9094
required this.icon,
9195
required this.title,
9296
required this.streamColorSwatches,
97+
required this.starColor,
98+
required this.bgMarker,
99+
required this.textMarker,
100+
required this.textMarkerLight,
93101
});
94102

95103
/// The [DesignVariables] from the context's active theme.
@@ -110,6 +118,10 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
110118

111119
// Not exactly from the Figma design, but from Vlad anyway.
112120
final StreamColorSwatches streamColorSwatches;
121+
final Color starColor;
122+
final Color bgMarker;
123+
final Color textMarker;
124+
final Color textMarkerLight;
113125

114126
@override
115127
DesignVariables copyWith({
@@ -119,6 +131,10 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
119131
Color? icon,
120132
Color? title,
121133
StreamColorSwatches? streamColorSwatches,
134+
Color? starColor,
135+
Color? bgMarker,
136+
Color? textMarker,
137+
Color? textMarkerLight,
122138
}) {
123139
return DesignVariables._(
124140
bgMain: bgMain ?? this.bgMain,
@@ -127,6 +143,10 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
127143
icon: icon ?? this.icon,
128144
title: title ?? this.title,
129145
streamColorSwatches: streamColorSwatches ?? this.streamColorSwatches,
146+
starColor: starColor ?? this.starColor,
147+
bgMarker: bgMarker ?? this.bgMarker,
148+
textMarker: textMarker ?? this.textMarker,
149+
textMarkerLight: textMarkerLight ?? this.textMarkerLight,
130150
);
131151
}
132152

@@ -142,6 +162,10 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
142162
icon: Color.lerp(icon, other?.icon, t)!,
143163
title: Color.lerp(title, other?.title, t)!,
144164
streamColorSwatches: streamColorSwatches.lerp(other?.streamColorSwatches, t),
165+
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)!,
145169
);
146170
}
147171
}

0 commit comments

Comments
 (0)