diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d45d7a..2cf1b0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## [unreleased] + +- Fix: Show Parent even if we fallback for the main avatar + ### [1.0.0+4] - bump package dependencies. diff --git a/example/lib/main.dart b/example/lib/main.dart index 7905776..b1a10ef 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -210,7 +210,7 @@ class _MyHomePageState extends State { height: 20, ), Text( - 'Rectangular Acter Avatars ', + 'Space Acter Avatars ', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w500), ), Padding( @@ -308,7 +308,7 @@ class _MyHomePageState extends State { ), const SizedBox(height: 20), Text( - 'Rectangular Acter Avatars With Parent Badge', + 'Space Acter Avatars With Parent Badge', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w500), ), Padding( @@ -356,7 +356,10 @@ class _MyHomePageState extends State { ), avatarsInfo: [ AvatarInfo( - uniqueId: uuid.v4(), displayName: 'Lorem Ipsum') + displayName: "C-Space", + uniqueId: uuid.v4(), + avatar: AssetImage('assets/images/space-3.jpg'), + ), ], ), SizedBox( @@ -409,6 +412,13 @@ class _MyHomePageState extends State { uniqueId: uuid.v4(), avatar: AssetImage('assets/images/space-3.jpg'), ), + avatarsInfo: [ + AvatarInfo( + displayName: "C-Space", + uniqueId: uuid.v4(), + avatar: AssetImage('assets/images/space-3.jpg'), + ), + ], ), ], ), diff --git a/example/pubspec.lock b/example/pubspec.lock index e7505ec..2fa1692 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -7,7 +7,7 @@ packages: path: ".." relative: true source: path - version: "1.0.0+3" + version: "1.0.0+4" args: dependency: transitive description: diff --git a/lib/src/acter_avatar.dart b/lib/src/acter_avatar.dart index 8f79943..d505f3a 100644 --- a/lib/src/acter_avatar.dart +++ b/lib/src/acter_avatar.dart @@ -42,18 +42,18 @@ class ActerAvatar extends StatefulWidget { /// Avatar gesture tap for parent badge of `DisplayMode.Space`. final void Function()? onParentBadgeTap; - ActerAvatar( - {Key? key, - required this.avatarInfo, - required this.mode, - this.onAvatarTap, - this.onParentBadgeTap, - this.avatarsInfo, - this.tooltip = TooltipStyle.Combined, - this.secondaryToolTip = TooltipStyle.Combined, - this.size, - this.badgeSize}) - : super(key: key ?? Key('avatar-${avatarInfo.uniqueId}-$size')); + ActerAvatar({ + Key? key, + required this.avatarInfo, + required this.mode, + this.onAvatarTap, + this.onParentBadgeTap, + this.avatarsInfo, + this.tooltip = TooltipStyle.Combined, + this.secondaryToolTip = TooltipStyle.Combined, + this.size, + this.badgeSize = 20, + }) : super(key: key ?? Key('avatar-${avatarInfo.uniqueId}-$size')); @override _ActerAvatar createState() => _ActerAvatar(); @@ -61,34 +61,37 @@ class ActerAvatar extends StatefulWidget { class _ActerAvatar extends State { bool imgSuccess = false; - bool secondaryImgSuccess = false; ImageProvider? avatar; - ImageProvider? secondaryAvatar; @override void initState() { super.initState(); + _refreshAvatar(); + } + + @override + void didUpdateWidget(ActerAvatar oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.avatarInfo.avatar != oldWidget.avatarInfo.avatar || + widget.avatarInfo.avatarFuture != oldWidget.avatarInfo.avatarFuture) { + _refreshAvatar(); + } + } + + void _refreshAvatar() { ImageStreamListener listener = ImageStreamListener(setImage, onError: setImageError); - ImageStreamListener secondaryListener = - ImageStreamListener(setSecondaryImage, onError: setSecondaryImageError); + + // reset + avatar = null; + imgSuccess = false; if (widget.avatarInfo.avatar != null) { widget.avatarInfo.avatar! .resolve(ImageConfiguration()) .addListener(listener); - if (widget.avatarsInfo != null && widget.avatarsInfo!.isNotEmpty) { - if (widget.avatarsInfo![0].avatar != null) { - widget.avatarsInfo![0].avatar! - .resolve(ImageConfiguration()) - .addListener(secondaryListener); - } - } } else if (widget.avatarInfo.avatarFuture != null) { fetchImageProvider(listener); - if (widget.avatarsInfo != null && widget.avatarsInfo!.isNotEmpty) { - fetchSecondaryImageProvider(secondaryListener); - } } } @@ -98,36 +101,18 @@ class _ActerAvatar extends State { avatar = res; } - void fetchSecondaryImageProvider(ImageStreamListener listener) async { - var res = await widget.avatarsInfo![0].avatarFuture!; - res!.resolve(ImageConfiguration()).addListener(listener); - secondaryAvatar = res; - } - void setImage(ImageInfo image, bool sync) { if (mounted) { setState(() => imgSuccess = true); } } - void setSecondaryImage(ImageInfo image, bool sync) { - if (mounted) { - setState(() => secondaryImgSuccess = true); - } - } - void setImageError(Object obj, StackTrace? st) { if (mounted) { setState(() => imgSuccess = false); } } - void setSecondaryImageError(Object obj, StackTrace? st) { - if (mounted) { - setState(() => secondaryImgSuccess = false); - } - } - @override Widget build(BuildContext context) { final child = @@ -177,20 +162,6 @@ class _ActerAvatar extends State { } } - void secondaryAvatarError(Object error, StackTrace? stackTrace) { - log.warning( - 'Error loading avatar for ${widget.avatarsInfo![0].uniqueId}. Returning to fallback.', - error, - stackTrace, - ); - if (mounted) { - setState(() { - secondaryAvatar = null; - secondaryImgSuccess = false; - }); - } - } - String? secTooltipMsg() { if (widget.avatarsInfo != null || widget.avatarsInfo!.isNotEmpty) { switch (widget.secondaryToolTip) { @@ -224,7 +195,6 @@ class _ActerAvatar extends State { radius: widget.size ?? 24, ); case DisplayMode.Space: - double badgeOverflow = badgeSize / 5; return Stack( clipBehavior: Clip.none, children: [ @@ -240,50 +210,7 @@ class _ActerAvatar extends State { ), ), ), - Positioned( - bottom: -badgeOverflow, - right: -badgeOverflow, - child: (widget.avatarsInfo == null || - widget.avatarsInfo!.isEmpty || - widget.avatarsInfo![0].avatar == null) - ? SizedBox(height: badgeSize + badgeOverflow) - : GestureDetector( - onTap: widget.onParentBadgeTap, - child: Column( - children: [ - SizedBox( - height: widget.badgeSize ?? badgeSize, - width: widget.badgeSize ?? badgeSize, - child: widget.secondaryToolTip != TooltipStyle.None - ? Tooltip( - message: secTooltipMsg(), - child: Container( - decoration: BoxDecoration( - borderRadius: - BorderRadius.circular(6.0), - image: DecorationImage( - fit: BoxFit.cover, - image: widget.avatarsInfo![0].avatar!, - onError: secondaryAvatarError, - ), - ), - ), - ) - : Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(6.0), - image: DecorationImage( - fit: BoxFit.cover, - image: widget.avatarsInfo![0].avatar!, - onError: secondaryAvatarError, - ), - ), - ), - ), - ], - ), - ), - ), + renderSpaceParent(context), ], ); case DisplayMode.GroupChat: @@ -318,13 +245,11 @@ class _ActerAvatar extends State { message: secTooltipMsg(), child: CircleAvatar( foregroundImage: widget.avatarsInfo![0].avatar, - onForegroundImageError: secondaryAvatarError, radius: widget.size ?? 24, ), ) : CircleAvatar( foregroundImage: widget.avatarsInfo![0].avatar, - onForegroundImageError: secondaryAvatarError, radius: widget.size ?? 24, ), ), @@ -359,9 +284,39 @@ class _ActerAvatar extends State { } } - Widget renderFallback(BuildContext context) { + Widget renderSpaceParent(BuildContext context) { + if (widget.badgeSize == null) { + // nothing. ignore + return SizedBox.shrink(); + } + final badgeSize = widget.badgeSize ?? 20; + if (widget.avatarsInfo == null || widget.avatarsInfo!.isEmpty) { + return SizedBox( + height: badgeSize, + width: badgeSize, + ); + } + + final parentInfo = widget.avatarsInfo![0]; double badgeOverflow = badgeSize / 5; - double textFallbackSize = widget.size == null ? 48 : widget.size!; + + return Positioned( + bottom: -badgeOverflow, + right: -badgeOverflow, + child: GestureDetector( + onTap: widget.onParentBadgeTap, + child: ActerAvatar( + avatarInfo: parentInfo, + mode: DisplayMode.Space, + size: badgeSize, + badgeSize: null, + ), + ), + ); + } + + Widget renderFallback(BuildContext context) { + double textFallbackSize = widget.size ?? 48; double multiFallbackSize = widget.size == null ? 48 : widget.size! * 2.0; /// Fallback @@ -435,43 +390,7 @@ class _ActerAvatar extends State { size: textFallbackSize, shape: Shape.Rectangle, ), - Positioned( - bottom: -badgeOverflow, - right: -badgeOverflow, - child: widget.avatarsInfo == null || widget.avatarsInfo!.isEmpty - ? SizedBox(height: badgeSize + badgeOverflow) - : GestureDetector( - onTap: widget.onParentBadgeTap, - child: Column( - children: [ - SizedBox( - height: widget.badgeSize ?? badgeSize, - width: widget.badgeSize ?? badgeSize, - child: widget.secondaryToolTip != TooltipStyle.None - ? Tooltip( - message: secTooltipMsg(), - child: TextAvatar( - text: - widget.avatarsInfo![0].displayName ?? - widget.avatarsInfo![0].uniqueId, - sourceText: - widget.avatarsInfo![0].uniqueId, - fontSize: 6, - shape: Shape.Rectangle, - ), - ) - : TextAvatar( - text: widget.avatarsInfo![0].displayName ?? - widget.avatarsInfo![0].uniqueId, - sourceText: widget.avatarsInfo![0].uniqueId, - fontSize: 6, - shape: Shape.Rectangle, - ), - ), - ], - ), - ), - ), + renderSpaceParent(context), ], ); case DisplayMode.GroupChat: diff --git a/lib/src/constants/keys.dart b/lib/src/constants/keys.dart index 44bee82..4226038 100644 --- a/lib/src/constants/keys.dart +++ b/lib/src/constants/keys.dart @@ -6,6 +6,6 @@ class TestKeys { static const multiAvatarKey = Key('Multi-avatar'); static const textAvatarKey = Key('Text-Avatar'); static const circleAvatarKey = Key('Circle-Avatar'); - static const rectangleAvatarKey = Key('Rectangle-Avatar'); + static const spaceAvatarKey = Key('Space-Avatar'); static const stackedAvatarKey = Key('Stacked-Avatar'); } diff --git a/test/acter_avatar_test.dart b/test/acter_avatar_test.dart index f92f4a1..bdac4a2 100644 --- a/test/acter_avatar_test.dart +++ b/test/acter_avatar_test.dart @@ -76,13 +76,13 @@ void main() { }); }); - group('Rectangular Avatar tests', () { - testWidgets('Rectangular Avatar with specified size', + group('Space Avatar tests', () { + testWidgets('Space Avatar with specified size', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( home: Scaffold( body: ActerAvatar( - key: TestKeys.rectangleAvatarKey, + key: TestKeys.spaceAvatarKey, avatarInfo: AvatarInfo(uniqueId: 'test:acter.org'), mode: DisplayMode.Space, size: 36, @@ -91,18 +91,17 @@ void main() { )); await tester.pumpAndSettle(); final Size avatarSize = - tester.getSize(find.byKey(TestKeys.rectangleAvatarKey)); + tester.getSize(find.byKey(TestKeys.spaceAvatarKey)); // should expect specified fallback size expect(avatarSize.height, equals(36)); expect(avatarSize.width, equals(36)); }); - testWidgets('Rectangular Avatar with fallback size', - (WidgetTester tester) async { + testWidgets('Space Avatar with fallback size', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( home: Scaffold( body: ActerAvatar( - key: TestKeys.rectangleAvatarKey, + key: TestKeys.spaceAvatarKey, avatarInfo: AvatarInfo(uniqueId: 'test:acter.org'), mode: DisplayMode.Space, ), @@ -110,13 +109,13 @@ void main() { )); await tester.pumpAndSettle(); final Size avatarSize = - tester.getSize(find.byKey(TestKeys.rectangleAvatarKey)); + tester.getSize(find.byKey(TestKeys.spaceAvatarKey)); // should expect default fallback avatar size expect(avatarSize.height, equals(48)); expect(avatarSize.width, equals(48)); }); - testWidgets('Rectangular Avatar with NetworkImage render', + testWidgets('Space Avatar with NetworkImage render', (WidgetTester tester) async { final String imagePath = 'https://st5.depositphotos.com/38460822/63964/i/600/depositphotos_639649504-stock-photo-mail-sign-sign-alphabet-made.jpg'; @@ -124,14 +123,14 @@ void main() { await tester.pumpWidget(MaterialApp( home: Scaffold( body: ActerAvatar( - key: TestKeys.rectangleAvatarKey, + key: TestKeys.spaceAvatarKey, avatarInfo: AvatarInfo(uniqueId: '@test:acter.org', avatar: image), mode: DisplayMode.Space, ), ), )); await tester.pumpAndSettle(); - final avatarFinder = find.byKey(TestKeys.rectangleAvatarKey); + final avatarFinder = find.byKey(TestKeys.spaceAvatarKey); // should expect `ActerAvatar` is present expect(avatarFinder, findsOneWidget); final avatar = avatarFinder.evaluate().first.widget as ActerAvatar; @@ -139,12 +138,12 @@ void main() { expect(avatar.avatarInfo.avatar, NetworkImage(imagePath)); }); - testWidgets('Rectangular Avatar Parent Badge specified size', + testWidgets('Space Avatar Parent Badge specified size', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( home: Scaffold( body: ActerAvatar( - key: TestKeys.rectangleAvatarKey, + key: TestKeys.spaceAvatarKey, avatarInfo: AvatarInfo(uniqueId: '@test:acter.org'), avatarsInfo: [AvatarInfo(uniqueId: 'Acter-Global')], mode: DisplayMode.Space, @@ -153,20 +152,21 @@ void main() { ), )); await tester.pumpAndSettle(); - final avatarFinder = find.byKey(TestKeys.rectangleAvatarKey); + final avatarFinder = find.byKey(TestKeys.spaceAvatarKey); // should expect `ActerAvatar` is present expect(avatarFinder, findsOneWidget); - final sizedBoxSize = tester.getSize(find.byType(SizedBox)); + final innerAvatar = tester.getSize(find.descendant( + of: avatarFinder, matching: find.byType(ActerAvatar))); // expect parent badge specified size. - expect(sizedBoxSize.height, equals(35)); - expect(sizedBoxSize.width, equals(35)); + expect(innerAvatar.height, equals(35)); + expect(innerAvatar.width, equals(35)); }); - testWidgets('Rectangular Avatar Parent Badge fallback size', + testWidgets('Space Avatar Parent Badge fallback size', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( home: Scaffold( body: ActerAvatar( - key: TestKeys.rectangleAvatarKey, + key: TestKeys.spaceAvatarKey, avatarInfo: AvatarInfo(uniqueId: '@test:acter.org'), avatarsInfo: [AvatarInfo(uniqueId: 'Acter-Global')], mode: DisplayMode.Space, @@ -174,15 +174,16 @@ void main() { ), )); await tester.pumpAndSettle(); - final avatarFinder = find.byKey(TestKeys.rectangleAvatarKey); + final avatarFinder = find.byKey(TestKeys.spaceAvatarKey); // should expect `ActerAvatar` is present expect(avatarFinder, findsOneWidget); - final sizedBoxSize = tester.getSize(find.byType(SizedBox)); + final innerAvatar = tester.getSize(find.descendant( + of: avatarFinder, matching: find.byType(ActerAvatar))); // expect parent badge fallback size. - expect(sizedBoxSize.height, equals(20)); - expect(sizedBoxSize.width, equals(20)); + expect(innerAvatar.height, equals(20)); + expect(innerAvatar.width, equals(20)); }); - testWidgets('Rectangular Avatar Parent badge with NetworkImage render', + testWidgets('Space Avatar Parent badge with NetworkImage render', (WidgetTester tester) async { final String imagePath = 'https://st5.depositphotos.com/38460822/63964/i/600/depositphotos_639649504-stock-photo-mail-sign-sign-alphabet-made.jpg'; @@ -190,7 +191,7 @@ void main() { await tester.pumpWidget(MaterialApp( home: Scaffold( body: ActerAvatar( - key: TestKeys.rectangleAvatarKey, + key: TestKeys.spaceAvatarKey, avatarInfo: AvatarInfo(uniqueId: '@test:acter.org'), avatarsInfo: [AvatarInfo(uniqueId: 'Acter-Global', avatar: image)], mode: DisplayMode.Space, @@ -198,7 +199,7 @@ void main() { ), )); await tester.pumpAndSettle(); - final avatarFinder = find.byKey(TestKeys.rectangleAvatarKey); + final avatarFinder = find.byKey(TestKeys.spaceAvatarKey); // should expect `ActerAvatar` is present expect(avatarFinder, findsOneWidget); final avatar = avatarFinder.evaluate().first.widget as ActerAvatar; @@ -339,7 +340,7 @@ void main() { onTapped(context, 'Group Chat Avatar tapped'), ), ActerAvatar( - key: TestKeys.rectangleAvatarKey, + key: TestKeys.spaceAvatarKey, avatarInfo: AvatarInfo(uniqueId: '@test:acter.org'), mode: DisplayMode.Space, onAvatarTap: () => onTapped(context, 'Space Avatar tapped'), @@ -368,7 +369,7 @@ void main() { expect(groupChatGestureFinder, findsOneWidget); final spaceGestureFinder = find.descendant( - of: find.byKey(TestKeys.rectangleAvatarKey), + of: find.byKey(TestKeys.spaceAvatarKey), matching: find.byType(GestureDetector)); // we have found the Gesture Detector, proceed with tester operation expect(spaceGestureFinder, findsOneWidget);