Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 25 additions & 35 deletions packages/stream_chat_flutter/lib/src/avatars/gradient_avatar.dart
Original file line number Diff line number Diff line change
Expand Up @@ -69,30 +69,31 @@ class PolygonGradientPainter extends CustomPainter {
this.fontFamily,
);

/// User ID used for key
final String userId;

Comment thread
xsahil03x marked this conversation as resolved.
/// User name to display
final String username;

/// Font family to use
final String fontFamily;

/// Initial grid row count
static const int rowCount = 5;

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

/// User ID used for key
String userId;

/// User name to display
String username;

/// Font family to use
String fontFamily;
late final Random _rand = Random(userId.hashCode);
Comment thread
xsahil03x marked this conversation as resolved.
Outdated

@override
void paint(Canvas canvas, Size size) {
final rowUnit = size.width / columnCount;
final columnUnit = size.height / rowCount;
final rand = Random(userId.length);

final squares = <Offset4>[];
final points = <Offset>{};
final gradient = colorGradients[rand.nextInt(colorGradients.length)];
final gradient = colorGradients[_rand.nextInt(colorGradients.length)];
Comment thread
xsahil03x marked this conversation as resolved.
Outdated

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

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

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

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

transformedList.add(Offset(orgDx + dx, orgDy + dy));
}
Expand All @@ -204,46 +204,36 @@ class Offset4 {
);

/// Point 1
int p1;
final int p1;

/// Point 2
int p2;
final int p2;

/// Point 3
int p3;
final int p3;

/// Point 4
int p4;
final int p4;

/// Position of polygon on grid
int row;
final int row;

/// Position of polygon on grid
int column;
final int column;

/// Max row size
int rowSize;
final int rowSize;

/// Max col size
int colSize;
final int colSize;

/// Gradient to be applied to polygon
List<Color> gradient;
final List<Color> gradient;

/// Draw the polygon on canvas
void draw(Canvas canvas, List<Offset> points) {
final paint = Paint()
..color = Color.fromARGB(
255,
Random().nextInt(255),
Random().nextInt(255),
Random().nextInt(255),
)
..shader = ui.Gradient.linear(
points[p1],
points[p3],
gradient,
);
..shader = ui.Gradient.linear(points[p1], points[p3], gradient);

final backgroundPath = Path()
..moveTo(points[p1].dx, points[p1].dy)
Expand Down
11 changes: 11 additions & 0 deletions packages/stream_chat_flutter/test/flutter_test_config.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:io';

import 'package:alchemist/alchemist.dart';
import 'package:flutter/material.dart';

Future<void> testExecutable(FutureOr<void> Function() testMain) async {
final isRunningInCi = Platform.environment.containsKey('CI') ||
Expand All @@ -12,6 +13,16 @@ Future<void> testExecutable(FutureOr<void> Function() testMain) async {
platformGoldensConfig: PlatformGoldensConfig(
enabled: !isRunningInCi,
),
goldenTestTheme: GoldenTestTheme(
backgroundColor: const Color(0xFFF8F9FA), // Light neutral background
borderColor: const Color(0xFFE9ECEF), // Subtle border
nameTextStyle: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Color(0xFF343A40), // Dark text for good contrast
),
padding: const EdgeInsets.all(16), // More generous padding
),
),
run: testMain,
);
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// ignore_for_file: lines_longer_than_80_chars

import 'package:alchemist/alchemist.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
Expand Down Expand Up @@ -112,4 +114,166 @@ void main() {
),
),
);

// Regression test for GitHub issue #2369
// https://github.com/GetStream/stream-chat-flutter/issues/2369
//
// Issue: All Users have the same Gradient Avatar color
// Problem: Users with same-length IDs were getting identical gradient colors
// Solution: Use userId.hashCode instead of length-based randomization
goldenTest(
'GitHub issue #2369 - same-length user IDs should have different colors',
fileName: 'gradient_avatar_issue_2369',
builder: () => GoldenTestGroup(
children: [
// Test case from GitHub issue #2369 - these numeric IDs have same length
// but should produce different gradient colors after the fix
GoldenTestScenario(
name: 'Numeric IDs (5 chars) - Should show different colors',
constraints: const BoxConstraints.tightFor(width: 450, height: 180),
child: const AvatarComparisonRow(
users: [
('12133', 'User One'), // Example IDs from the issue
('12134', 'User Two'), // These were showing same colors
('12135', 'User Three'), // before the hashCode fix
],
),
Comment thread
xsahil03x marked this conversation as resolved.
),
// Additional test with alphabetic IDs of same length
GoldenTestScenario(
name: 'Alphabetic IDs (5 chars) - Should show different colors',
constraints: const BoxConstraints.tightFor(width: 450, height: 180),
child: const AvatarComparisonRow(
users: [
('abcde', 'User Alpha'),
('fghij', 'User Beta'),
('klmno', 'User Gamma'),
],
),
),
GoldenTestScenario(
name: 'Mixed length IDs - For reference (should be different)',
constraints: const BoxConstraints.tightFor(width: 450, height: 180),
child: const AvatarComparisonRow(
users: [
('a', 'Short'),
('medium123', 'Medium'),
('verylonguser456', 'Long'),
],
),
),
GoldenTestScenario(
name: 'Same user ID - Should be identical',
constraints: const BoxConstraints.tightFor(width: 450, height: 180),
child: const AvatarComparisonRow(
users: [
('test123', 'Same User'),
('test123', 'Same User'),
('test123', 'Same User'),
],
),
),
],
),
);
}

/// A widget that displays a row of gradient avatars for comparison testing.
///
/// This widget is specifically designed for testing gradient avatar color
/// variations, particularly for verifying fixes to GitHub issue #2369 where
/// users with same-length IDs were getting identical colors.
///
/// See: https://github.com/GetStream/stream-chat-flutter/issues/2369
class AvatarComparisonRow extends StatelessWidget {
/// Creates an [AvatarComparisonRow] with the given list of users.
///
/// The [users] parameter should contain tuples of (userId, userName) pairs
/// to be displayed as gradient avatars for visual comparison.
const AvatarComparisonRow({
super.key,
required this.users,
this.avatarSize = 100.0,
this.spacing = 8.0,
});

/// List of users to display as (userId, userName) tuples
final List<(String, String)> users;

/// Size of each avatar in logical pixels
final double avatarSize;

/// Horizontal spacing between avatars in logical pixels
final double spacing;

@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: users.map((userData) {
final (userId, userName) = userData;
return Expanded(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: spacing / 2),
child: _AvatarItem(
userId: userId,
userName: userName,
avatarSize: avatarSize,
),
),
);
}).toList(),
);
}
}

/// Individual avatar item with labels
class _AvatarItem extends StatelessWidget {
const _AvatarItem({
required this.userId,
required this.userName,
required this.avatarSize,
});

final String userId;
final String userName;
final double avatarSize;

@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: avatarSize,
height: avatarSize,
child: StreamGradientAvatar(
name: userName,
userId: userId,
),
),
const SizedBox(height: 8),
Text(
userId,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 2),
Text(
userName,
style: TextStyle(
fontSize: 10,
color: Colors.grey.shade600,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
),
],
);
}
}
Loading