diff --git a/packages/stream_chat_flutter/lib/src/gradient_avatar.dart b/packages/stream_chat_flutter/lib/src/gradient_avatar.dart new file mode 100644 index 00000000..3a6aba10 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/gradient_avatar.dart @@ -0,0 +1,258 @@ +import 'dart:math'; +import 'dart:ui'; +import 'dart:ui' as ui; + +import 'package:flutter/material.dart'; + +/// Fallback user avatar with a polygon gradient overlayed with text +class GradientAvatar extends StatefulWidget { + /// Constructor for [GradientAvatar] + const GradientAvatar({ + Key? key, + required this.name, + required this.userId, + }) : super(key: key); + + /// Name of user to shorten and display + final String name; + + /// ID of user to be used for key + final String userId; + + @override + _GradientAvatarState createState() => _GradientAvatarState(); +} + +class _GradientAvatarState extends State { + @override + Widget build(BuildContext context) => Center( + child: RepaintBoundary( + child: CustomPaint( + painter: DemoPainter( + widget.userId, + getShortenedName(widget.name), + DefaultTextStyle.of(context).style.fontFamily ?? 'Roboto', + ), + child: const SizedBox.expand(), + ), + ), + ); + + String getShortenedName(String name) { + var parts = name.split(' ')..removeWhere((e) => e == ''); + + if (parts.length > 2) { + parts = parts.take(2).toList(); + } + + var result = ''; + + for (var i = 0; i < parts.length; i++) { + result = result + parts[i][0].toUpperCase(); + } + + return result; + } +} + +/// Painter for bg polygon gradient +class DemoPainter extends CustomPainter { + /// Constructor for [DemoPainter] + DemoPainter( + this.userId, + this.username, + this.fontFamily, + ); + + /// Init grid row count + static const int rowCount = 5; + + /// Init 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; + + @override + void paint(Canvas canvas, Size size) { + final rowUnit = size.width / columnCount; + final columnUnit = size.height / rowCount; + final rand = Random(userId.length); + + final squares = []; + final points = {}; + final gradient = colorGradients[rand.nextInt(colorGradients.length)]; + + for (var i = 0; i < rowCount; i++) { + for (var j = 0; j < columnCount; j++) { + final off1 = Offset(rowUnit * j, columnUnit * i); + final off2 = Offset(rowUnit * (j + 1), columnUnit * i); + final off3 = Offset(rowUnit * (j + 1), columnUnit * (i + 1)); + final off4 = Offset(rowUnit * j, columnUnit * (i + 1)); + + points.addAll([off1, off2, off3, off4]); + + final pointsList = points.toList(); + + final p1 = pointsList.indexOf(off1); + final p2 = pointsList.indexOf(off2); + final p3 = pointsList.indexOf(off3); + final p4 = pointsList.indexOf(off4); + + squares.add( + Offset4(p1, p2, p3, p4, i, j, rowCount, columnCount, gradient)); + } + } + + final list = transformPoints(points, size); + squares.forEach((e) => e.draw(canvas, list)); + + final smallerSide = size.width > size.height ? size.width : size.height; + + final textSize = smallerSide / 3; + + final dxShift = (username.length == 2 ? 1.45 : 0.9) * textSize / 2; + final dyShift = (username.length == 2 ? 1.0 : 1.65) * textSize / 2; + + final fontSize = username.length == 2 ? textSize : textSize * 1.5; + + TextPainter( + text: TextSpan( + text: username, + style: TextStyle( + fontFamily: fontFamily, + fontSize: fontSize, + fontWeight: FontWeight.w500, + color: Colors.white.withOpacity(0.7), + ), + ), + textAlign: TextAlign.center, + textDirection: TextDirection.ltr) + ..layout(maxWidth: size.width) + ..paint( + canvas, + Offset( + (size.width / 2) - dxShift, + (size.height / 2) - dyShift, + ), + ); + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => false; + + /// Transforms initial grid into a polygon grid + List transformPoints(Set points, Size size) { + final transformedList = []; + final orgList = points.toList(); + final rand = Random(userId.length); + + for (var i = 0; i < points.length; i++) { + final orgDx = orgList[i].dx; + final orgDy = orgList[i].dy; + + if (orgDx == 0 || + orgDy == 0 || + orgDx == size.width || + orgDy == size.height) { + transformedList.add(Offset(orgDx, orgDy)); + continue; + } + + final sign1 = rand.nextInt(2) == 1 ? 1 : -1; + final sign2 = rand.nextInt(2) == 1 ? 1 : -1; + + final dx = 0.6 * sign1 * rand.nextInt(size.width ~/ columnCount); + final dy = 0.6 * sign2 * rand.nextInt(size.height ~/ rowCount); + + transformedList.add(Offset(orgDx + dx, orgDy + dy)); + } + + return transformedList; + } +} + +/// Class for storing and drawing four points of a polygon +class Offset4 { + /// Constructor for [Offset4] + Offset4( + this.p1, + this.p2, + this.p3, + this.p4, + this.row, + this.column, + this.rowSize, + this.colSize, + this.gradient, + ); + + /// Point 1 + int p1; + + /// Point 2 + int p2; + + /// Point 3 + int p3; + + /// Point 4 + int p4; + + /// Position of polygon on grid + int row; + + /// Position of polygon on grid + int column; + + /// Max row size + int rowSize; + + /// Max col size + int colSize; + + /// Gradient to be applied to polygon + List gradient; + + /// Draw the polygon on canvas + void draw(Canvas canvas, List 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, + ); + + final backgroundPath = Path() + ..moveTo(points[p1].dx, points[p1].dy) + ..lineTo(points[p2].dx, points[p2].dy) + ..lineTo(points[p3].dx, points[p3].dy) + ..lineTo(points[p4].dx, points[p4].dy) + ..lineTo(points[p1].dx, points[p1].dy) + ..close(); + + canvas.drawPath(backgroundPath, paint); + } +} + +/// Gradient list for polygons +const colorGradients = [ + [Color(0xffffafbd), Color(0xffffc3a0)], + [Color(0xff2193b0), Color(0xff6dd5ed)], + [Color(0xffcc2b5e), Color(0xff753a88)], + [Color(0xffee9ca7), Color(0xffffdde1)], + [Color(0xff42275a), Color(0xff734b6d)], + [Color(0xffde6262), Color(0xffffb88c)], + [Color(0xff56ab2f), Color(0xffa8e063)], + [Color(0xff614385), Color(0xff516395)], + [Color(0xffeacda3), Color(0xffd6ae7b)], + [Color(0xff02aab0), Color(0xff00cdac)], +]; diff --git a/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart b/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart index 94ba50e4..9d2df266 100644 --- a/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart +++ b/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart @@ -1,12 +1,11 @@ -import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/channel_header.dart'; import 'package:stream_chat_flutter/src/channel_preview.dart'; import 'package:stream_chat_flutter/src/extension.dart'; +import 'package:stream_chat_flutter/src/gradient_avatar.dart'; import 'package:stream_chat_flutter/src/message_input.dart'; import 'package:stream_chat_flutter/src/reaction_icon.dart'; -import 'package:stream_chat_flutter/src/utils.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; @@ -310,10 +309,9 @@ class StreamChatThemeData { colorTheme: colorTheme, primaryIconTheme: iconTheme, defaultUserImage: (context, user) => Center( - child: CachedNetworkImage( - filterQuality: FilterQuality.high, - imageUrl: getRandomPicUrl(user), - fit: BoxFit.cover, + child: GradientAvatar( + name: user.name, + userId: user.id, ), ), channelPreviewTheme: channelPreviewTheme, diff --git a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart index 39aabb85..12049fc6 100644 --- a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart +++ b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart @@ -14,6 +14,7 @@ export 'src/deleted_message.dart'; export 'src/full_screen_media.dart'; export 'src/gallery_footer.dart'; export 'src/gallery_header.dart'; +export 'src/gradient_avatar.dart'; export 'src/info_tile.dart'; export 'src/mention_tile.dart'; export 'src/message_action.dart'; diff --git a/packages/stream_chat_flutter/test/src/goldens/gradient_avatar_0.png b/packages/stream_chat_flutter/test/src/goldens/gradient_avatar_0.png new file mode 100644 index 00000000..bf15f6af Binary files /dev/null and b/packages/stream_chat_flutter/test/src/goldens/gradient_avatar_0.png differ diff --git a/packages/stream_chat_flutter/test/src/goldens/gradient_avatar_1.png b/packages/stream_chat_flutter/test/src/goldens/gradient_avatar_1.png new file mode 100644 index 00000000..bd466f79 Binary files /dev/null and b/packages/stream_chat_flutter/test/src/goldens/gradient_avatar_1.png differ diff --git a/packages/stream_chat_flutter/test/src/goldens/gradient_avatar_2.png b/packages/stream_chat_flutter/test/src/goldens/gradient_avatar_2.png new file mode 100644 index 00000000..d7d671c4 Binary files /dev/null and b/packages/stream_chat_flutter/test/src/goldens/gradient_avatar_2.png differ diff --git a/packages/stream_chat_flutter/test/src/goldens/gradient_avatar_3.png b/packages/stream_chat_flutter/test/src/goldens/gradient_avatar_3.png new file mode 100644 index 00000000..5381c0f0 Binary files /dev/null and b/packages/stream_chat_flutter/test/src/goldens/gradient_avatar_3.png differ diff --git a/packages/stream_chat_flutter/test/src/goldens/message_text.png b/packages/stream_chat_flutter/test/src/goldens/message_text.png index ecd93721..dda158b2 100644 Binary files a/packages/stream_chat_flutter/test/src/goldens/message_text.png and b/packages/stream_chat_flutter/test/src/goldens/message_text.png differ diff --git a/packages/stream_chat_flutter/test/src/gradient_avatar_test.dart b/packages/stream_chat_flutter/test/src/gradient_avatar_test.dart new file mode 100644 index 00000000..f8decfef --- /dev/null +++ b/packages/stream_chat_flutter/test/src/gradient_avatar_test.dart @@ -0,0 +1,127 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:golden_toolkit/golden_toolkit.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:stream_chat_flutter/src/gradient_avatar.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +import 'mocks.dart'; + +void main() { + testWidgets( + 'control test', + (WidgetTester tester) async { + final client = MockClient(); + final clientState = MockClientState(); + + when(() => client.state).thenReturn(clientState); + when(() => clientState.user).thenReturn(OwnUser(id: 'user-id')); + + await tester.pumpWidget( + MaterialApp( + home: StreamChat( + client: client, + child: const Scaffold( + body: Center( + child: SizedBox( + width: 100, + height: 100, + child: GradientAvatar(name: 'demo user', userId: 'demo123'), + ), + ), + ), + ), + ), + ); + + expect(find.byType(GradientAvatar), findsOneWidget); + }, + ); + + testGoldens( + 'golden test for the name "demo user"', + (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: Center( + child: SizedBox( + width: 100, + height: 100, + child: GradientAvatar(name: 'demo user', userId: 'demo123'), + ), + ), + ), + ), + ); + + await screenMatchesGolden(tester, 'gradient_avatar_0'); + }, + ); + + testGoldens( + 'golden test for the name "demo"', + (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: Center( + child: SizedBox( + width: 100, + height: 100, + child: GradientAvatar(name: 'demo', userId: 'demo1'), + ), + ), + ), + ), + ); + + await screenMatchesGolden(tester, 'gradient_avatar_1'); + }, + ); + + testGoldens( + 'control special character test', + (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: Center( + child: SizedBox( + width: 100, + height: 100, + child: GradientAvatar( + name: 'd123@/d de:\$as', + userId: 'demo123', + ), + ), + ), + ), + ), + ); + + await screenMatchesGolden(tester, 'gradient_avatar_2'); + }, + ); + + testGoldens( + 'control special character test 2', + (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: Center( + child: SizedBox( + width: 100, + height: 100, + child: GradientAvatar(name: '123@/d \$as', userId: 'demo123'), + ), + ), + ), + ), + ); + + await screenMatchesGolden(tester, 'gradient_avatar_3'); + }, + ); +} diff --git a/packages/stream_chat_flutter/test/src/message_text_test.dart b/packages/stream_chat_flutter/test/src/message_text_test.dart index f64e2bf1..765f7f2c 100644 --- a/packages/stream_chat_flutter/test/src/message_text_test.dart +++ b/packages/stream_chat_flutter/test/src/message_text_test.dart @@ -80,7 +80,7 @@ void main() { }); const messageText = ''' - a message. +a message. with multiple lines and a list: - a. okasd diff --git a/packages/stream_chat_flutter/test/src/reaction_bubble_test.dart b/packages/stream_chat_flutter/test/src/reaction_bubble_test.dart index 1e3c9f5e..acf485c0 100644 --- a/packages/stream_chat_flutter/test/src/reaction_bubble_test.dart +++ b/packages/stream_chat_flutter/test/src/reaction_bubble_test.dart @@ -6,32 +6,8 @@ import 'package:stream_chat_flutter/src/reaction_bubble.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'mocks.dart'; -import 'simple_frame.dart'; void main() { - testGoldens( - 'it should show no reactions', - (WidgetTester tester) async { - await tester.pumpWidgetBuilder( - SimpleFrame( - child: StreamChatTheme( - data: StreamChatThemeData(), - child: const SizedBox( - child: ReactionBubble( - reactions: [], - borderColor: Colors.black, - backgroundColor: Colors.white, - maskColor: Colors.white, - ), - ), - ), - ), - surfaceSize: const Size(100, 100), - ); - await screenMatchesGolden(tester, 'reaction_bubble_0'); - }, - ); - testGoldens( 'it should show a like - light theme', (WidgetTester tester) async {