Skip to content

Commit a57b315

Browse files
committed
fix(ui): use userId.hashCode for GradientAvatar colors
This commit fixes an issue where GradientAvatars for users with same-length IDs would have identical colors. The `GradientAvatarPainter` now uses `Random(userId.hashCode)` instead of `Random(userId.length)` to generate colors and transform points. This ensures that different user IDs, even if they have the same length, will produce visually distinct avatars. A regression test (`gradient_avatar_issue_2369`) has been added to verify this fix, including the specific scenario reported in GitHub issue #2369. Additionally, the golden test theme in `flutter_test_config.dart` has been updated with a new background color, border color, name text style, and padding for improved visual clarity in golden tests.
1 parent ee78069 commit a57b315

File tree

7 files changed

+200
-35
lines changed

7 files changed

+200
-35
lines changed

packages/stream_chat_flutter/lib/src/avatars/gradient_avatar.dart

Lines changed: 25 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -69,30 +69,31 @@ class PolygonGradientPainter extends CustomPainter {
6969
this.fontFamily,
7070
);
7171

72+
/// User ID used for key
73+
final String userId;
74+
75+
/// User name to display
76+
final String username;
77+
78+
/// Font family to use
79+
final String fontFamily;
80+
7281
/// Initial grid row count
7382
static const int rowCount = 5;
7483

7584
/// Initial grid column count
7685
static const int columnCount = 5;
7786

78-
/// User ID used for key
79-
String userId;
80-
81-
/// User name to display
82-
String username;
83-
84-
/// Font family to use
85-
String fontFamily;
87+
late final Random _rand = Random(userId.hashCode);
8688

8789
@override
8890
void paint(Canvas canvas, Size size) {
8991
final rowUnit = size.width / columnCount;
9092
final columnUnit = size.height / rowCount;
91-
final rand = Random(userId.length);
9293

9394
final squares = <Offset4>[];
9495
final points = <Offset>{};
95-
final gradient = colorGradients[rand.nextInt(colorGradients.length)];
96+
final gradient = colorGradients[_rand.nextInt(colorGradients.length)];
9697

9798
for (var i = 0; i < rowCount; i++) {
9899
for (var j = 0; j < columnCount; j++) {
@@ -159,7 +160,6 @@ class PolygonGradientPainter extends CustomPainter {
159160
List<Offset> transformPoints(Set<Offset> points, Size size) {
160161
final transformedList = <Offset>[];
161162
final orgList = points.toList();
162-
final rand = Random(userId.length);
163163

164164
for (var i = 0; i < points.length; i++) {
165165
final orgDx = orgList[i].dx;
@@ -173,11 +173,11 @@ class PolygonGradientPainter extends CustomPainter {
173173
continue;
174174
}
175175

176-
final sign1 = rand.nextInt(2) == 1 ? 1 : -1;
177-
final sign2 = rand.nextInt(2) == 1 ? 1 : -1;
176+
final sign1 = _rand.nextInt(2) == 1 ? 1 : -1;
177+
final sign2 = _rand.nextInt(2) == 1 ? 1 : -1;
178178

179-
final dx = sign1 * 0.6 * rand.nextInt(size.width ~/ columnCount);
180-
final dy = sign2 * 0.6 * rand.nextInt(size.height ~/ rowCount);
179+
final dx = sign1 * 0.6 * _rand.nextInt(size.width ~/ columnCount);
180+
final dy = sign2 * 0.6 * _rand.nextInt(size.height ~/ rowCount);
181181

182182
transformedList.add(Offset(orgDx + dx, orgDy + dy));
183183
}
@@ -204,46 +204,36 @@ class Offset4 {
204204
);
205205

206206
/// Point 1
207-
int p1;
207+
final int p1;
208208

209209
/// Point 2
210-
int p2;
210+
final int p2;
211211

212212
/// Point 3
213-
int p3;
213+
final int p3;
214214

215215
/// Point 4
216-
int p4;
216+
final int p4;
217217

218218
/// Position of polygon on grid
219-
int row;
219+
final int row;
220220

221221
/// Position of polygon on grid
222-
int column;
222+
final int column;
223223

224224
/// Max row size
225-
int rowSize;
225+
final int rowSize;
226226

227227
/// Max col size
228-
int colSize;
228+
final int colSize;
229229

230230
/// Gradient to be applied to polygon
231-
List<Color> gradient;
231+
final List<Color> gradient;
232232

233233
/// Draw the polygon on canvas
234234
void draw(Canvas canvas, List<Offset> points) {
235235
final paint = Paint()
236-
..color = Color.fromARGB(
237-
255,
238-
Random().nextInt(255),
239-
Random().nextInt(255),
240-
Random().nextInt(255),
241-
)
242-
..shader = ui.Gradient.linear(
243-
points[p1],
244-
points[p3],
245-
gradient,
246-
);
236+
..shader = ui.Gradient.linear(points[p1], points[p3], gradient);
247237

248238
final backgroundPath = Path()
249239
..moveTo(points[p1].dx, points[p1].dy)

packages/stream_chat_flutter/test/flutter_test_config.dart

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import 'dart:async';
22
import 'dart:io';
33

44
import 'package:alchemist/alchemist.dart';
5+
import 'package:flutter/material.dart';
56

67
Future<void> testExecutable(FutureOr<void> Function() testMain) async {
78
final isRunningInCi = Platform.environment.containsKey('CI') ||
@@ -12,6 +13,16 @@ Future<void> testExecutable(FutureOr<void> Function() testMain) async {
1213
platformGoldensConfig: PlatformGoldensConfig(
1314
enabled: !isRunningInCi,
1415
),
16+
goldenTestTheme: GoldenTestTheme(
17+
backgroundColor: const Color(0xFFF8F9FA), // Light neutral background
18+
borderColor: const Color(0xFFE9ECEF), // Subtle border
19+
nameTextStyle: const TextStyle(
20+
fontSize: 16,
21+
fontWeight: FontWeight.w600,
22+
color: Color(0xFF343A40), // Dark text for good contrast
23+
),
24+
padding: const EdgeInsets.all(16), // More generous padding
25+
),
1526
),
1627
run: testMain,
1728
);
1.79 KB
Loading
753 Bytes
Loading
1.79 KB
Loading
1.81 KB
Loading

packages/stream_chat_flutter/test/src/avatars/gradient_avatar_test.dart

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
// ignore_for_file: lines_longer_than_80_chars
2+
13
import 'package:alchemist/alchemist.dart';
24
import 'package:flutter/material.dart';
35
import 'package:flutter_test/flutter_test.dart';
@@ -112,4 +114,166 @@ void main() {
112114
),
113115
),
114116
);
117+
118+
// Regression test for GitHub issue #2369
119+
// https://github.com/GetStream/stream-chat-flutter/issues/2369
120+
//
121+
// Issue: All Users have the same Gradient Avatar color
122+
// Problem: Users with same-length IDs were getting identical gradient colors
123+
// Solution: Use userId.hashCode instead of length-based randomization
124+
goldenTest(
125+
'GitHub issue #2369 - same-length user IDs should have different colors',
126+
fileName: 'gradient_avatar_issue_2369',
127+
builder: () => GoldenTestGroup(
128+
children: [
129+
// Test case from GitHub issue #2369 - these numeric IDs have same length
130+
// but should produce different gradient colors after the fix
131+
GoldenTestScenario(
132+
name: 'Numeric IDs (5 chars) - Should show different colors',
133+
constraints: const BoxConstraints.tightFor(width: 450, height: 180),
134+
child: const AvatarComparisonRow(
135+
users: [
136+
('12133', 'User One'), // Example IDs from the issue
137+
('12134', 'User Two'), // These were showing same colors
138+
('12135', 'User Three'), // before the hashCode fix
139+
],
140+
),
141+
),
142+
// Additional test with alphabetic IDs of same length
143+
GoldenTestScenario(
144+
name: 'Alphabetic IDs (5 chars) - Should show different colors',
145+
constraints: const BoxConstraints.tightFor(width: 450, height: 180),
146+
child: const AvatarComparisonRow(
147+
users: [
148+
('abcde', 'User Alpha'),
149+
('fghij', 'User Beta'),
150+
('klmno', 'User Gamma'),
151+
],
152+
),
153+
),
154+
GoldenTestScenario(
155+
name: 'Mixed length IDs - For reference (should be different)',
156+
constraints: const BoxConstraints.tightFor(width: 450, height: 180),
157+
child: const AvatarComparisonRow(
158+
users: [
159+
('a', 'Short'),
160+
('medium123', 'Medium'),
161+
('verylonguser456', 'Long'),
162+
],
163+
),
164+
),
165+
GoldenTestScenario(
166+
name: 'Same user ID - Should be identical',
167+
constraints: const BoxConstraints.tightFor(width: 450, height: 180),
168+
child: const AvatarComparisonRow(
169+
users: [
170+
('test123', 'Same User'),
171+
('test123', 'Same User'),
172+
('test123', 'Same User'),
173+
],
174+
),
175+
),
176+
],
177+
),
178+
);
179+
}
180+
181+
/// A widget that displays a row of gradient avatars for comparison testing.
182+
///
183+
/// This widget is specifically designed for testing gradient avatar color
184+
/// variations, particularly for verifying fixes to GitHub issue #2369 where
185+
/// users with same-length IDs were getting identical colors.
186+
///
187+
/// See: https://github.com/GetStream/stream-chat-flutter/issues/2369
188+
class AvatarComparisonRow extends StatelessWidget {
189+
/// Creates an [AvatarComparisonRow] with the given list of users.
190+
///
191+
/// The [users] parameter should contain tuples of (userId, userName) pairs
192+
/// to be displayed as gradient avatars for visual comparison.
193+
const AvatarComparisonRow({
194+
super.key,
195+
required this.users,
196+
this.avatarSize = 100.0,
197+
this.spacing = 8.0,
198+
});
199+
200+
/// List of users to display as (userId, userName) tuples
201+
final List<(String, String)> users;
202+
203+
/// Size of each avatar in logical pixels
204+
final double avatarSize;
205+
206+
/// Horizontal spacing between avatars in logical pixels
207+
final double spacing;
208+
209+
@override
210+
Widget build(BuildContext context) {
211+
return Row(
212+
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
213+
children: users.map((userData) {
214+
final (userId, userName) = userData;
215+
return Expanded(
216+
child: Padding(
217+
padding: EdgeInsets.symmetric(horizontal: spacing / 2),
218+
child: _AvatarItem(
219+
userId: userId,
220+
userName: userName,
221+
avatarSize: avatarSize,
222+
),
223+
),
224+
);
225+
}).toList(),
226+
);
227+
}
228+
}
229+
230+
/// Individual avatar item with labels
231+
class _AvatarItem extends StatelessWidget {
232+
const _AvatarItem({
233+
required this.userId,
234+
required this.userName,
235+
required this.avatarSize,
236+
});
237+
238+
final String userId;
239+
final String userName;
240+
final double avatarSize;
241+
242+
@override
243+
Widget build(BuildContext context) {
244+
return Column(
245+
mainAxisSize: MainAxisSize.min,
246+
mainAxisAlignment: MainAxisAlignment.center,
247+
children: [
248+
SizedBox(
249+
width: avatarSize,
250+
height: avatarSize,
251+
child: StreamGradientAvatar(
252+
name: userName,
253+
userId: userId,
254+
),
255+
),
256+
const SizedBox(height: 8),
257+
Text(
258+
userId,
259+
style: const TextStyle(
260+
fontSize: 12,
261+
fontWeight: FontWeight.w600,
262+
),
263+
textAlign: TextAlign.center,
264+
),
265+
const SizedBox(height: 2),
266+
Text(
267+
userName,
268+
style: TextStyle(
269+
fontSize: 10,
270+
color: Colors.grey.shade600,
271+
),
272+
maxLines: 1,
273+
overflow: TextOverflow.ellipsis,
274+
textAlign: TextAlign.center,
275+
),
276+
],
277+
);
278+
}
115279
}

0 commit comments

Comments
 (0)