diff --git a/lib/src/core/design_system/components/molecules/profile_avatar.dart b/lib/src/core/design_system/components/molecules/profile_avatar.dart index 615df354..0a86472c 100644 --- a/lib/src/core/design_system/components/molecules/profile_avatar.dart +++ b/lib/src/core/design_system/components/molecules/profile_avatar.dart @@ -14,6 +14,7 @@ class ProfileAvatar extends StatelessWidget { this.onTap, this.showAddButton = false, this.onAddTap, + this.avatarBuilder, }); final String? avatarUrl; @@ -23,6 +24,7 @@ class ProfileAvatar extends StatelessWidget { final VoidCallback? onTap; final bool showAddButton; final VoidCallback? onAddTap; + final Widget Function(Widget avatar)? avatarBuilder; @override Widget build(BuildContext context) { @@ -111,24 +113,22 @@ class ProfileAvatar extends StatelessWidget { required bool isDarkMode, required double avatarSize, }) { - if (avatarUrl != null && avatarUrl!.isNotEmpty) { - return ClipOval( - child: CachedNetworkImage( - fadeInDuration: Duration.zero, - fadeOutDuration: Duration.zero, - imageUrl: avatarUrl!, - width: avatarSize, - height: avatarSize, - fit: BoxFit.cover, - placeholder: (context, url) => - _buildPlaceholder(context, isDarkMode, avatarSize), - errorWidget: (context, url, error) => - _buildPlaceholder(context, isDarkMode, avatarSize), - ), - ); - } + final avatar = avatarUrl != null && avatarUrl!.isNotEmpty + ? CachedNetworkImage( + fadeInDuration: Duration.zero, + fadeOutDuration: Duration.zero, + imageUrl: avatarUrl!, + width: avatarSize, + height: avatarSize, + fit: BoxFit.cover, + placeholder: (context, url) => + _buildPlaceholder(context, isDarkMode, avatarSize), + errorWidget: (context, url, error) => + _buildPlaceholder(context, isDarkMode, avatarSize), + ) + : _buildPlaceholder(context, isDarkMode, avatarSize); - return _buildPlaceholder(context, isDarkMode, avatarSize); + return ClipOval(child: avatarBuilder?.call(avatar) ?? avatar); } Widget _buildPlaceholder( diff --git a/lib/src/core/design_system/components/molecules/profile_card.dart b/lib/src/core/design_system/components/molecules/profile_card.dart index f3c2fc82..9cd4eaa3 100644 --- a/lib/src/core/design_system/components/molecules/profile_card.dart +++ b/lib/src/core/design_system/components/molecules/profile_card.dart @@ -20,6 +20,7 @@ class ProfileCard extends StatelessWidget { this.onTap, this.hasStories = false, this.onAvatarTap, + this.avatarBuilder, super.key, }); @@ -37,6 +38,7 @@ class ProfileCard extends StatelessWidget { VoidCallback? onTap, bool hasStories = false, VoidCallback? onAvatarTap, + Widget Function(Widget avatar)? avatarBuilder, Key? key, }) : this( imageUrl: imageUrl, @@ -51,6 +53,7 @@ class ProfileCard extends StatelessWidget { onTap: onTap, hasStories: hasStories, onAvatarTap: onAvatarTap, + avatarBuilder: avatarBuilder, key: key, ); @@ -66,6 +69,7 @@ class ProfileCard extends StatelessWidget { final VoidCallback? onTap; final bool hasStories; final VoidCallback? onAvatarTap; + final Widget Function(Widget avatar)? avatarBuilder; @override Widget build(BuildContext context) { @@ -73,6 +77,14 @@ class ProfileCard extends StatelessWidget { final isDark = Theme.of(context).brightness == Brightness.dark; final radius = BorderRadius.circular(AppShapes.squircleRadius); final borderColor = isDark ? AppColors.grey800 : AppColors.grey200; + final avatar = ProfileAvatar( + avatarUrl: imageUrl.isNotEmpty ? imageUrl : null, + displayName: userName, + size: 36, + hasStories: hasStories, + onTap: onAvatarTap ?? onTap, + avatarBuilder: avatarBuilder, + ); final Widget content = ConstrainedBox( constraints: const BoxConstraints(minHeight: 60), @@ -97,13 +109,7 @@ class ProfileCard extends StatelessWidget { child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - ProfileAvatar( - avatarUrl: imageUrl.isNotEmpty ? imageUrl : null, - displayName: userName, - size: 36, - hasStories: hasStories, - onTap: onAvatarTap ?? onTap, - ), + avatar, const SizedBox(width: 10), Expanded( child: Column( diff --git a/lib/src/core/design_system/components/molecules/story_circle.dart b/lib/src/core/design_system/components/molecules/story_circle.dart index 0344fec0..00105048 100644 --- a/lib/src/core/design_system/components/molecules/story_circle.dart +++ b/lib/src/core/design_system/components/molecules/story_circle.dart @@ -12,24 +12,28 @@ class StoryCircle extends StatelessWidget { final String userName; final String imageUrl; final String live; + final Widget Function(Widget avatar)? avatarBuilder; const StoryCircle._({ required this.type, required this.userName, required this.imageUrl, required this.live, + this.avatarBuilder, }); /// Constructor variant for an unread story with an accent border. factory StoryCircle.story({ required String userName, required String imageUrl, + Widget Function(Widget avatar)? avatarBuilder, }) { return StoryCircle._( type: StoryType.story, userName: userName, imageUrl: imageUrl, live: '', + avatarBuilder: avatarBuilder, ); } @@ -38,22 +42,29 @@ class StoryCircle extends StatelessWidget { required String userName, required String imageUrl, required String live, + Widget Function(Widget avatar)? avatarBuilder, }) { return StoryCircle._( type: StoryType.live, userName: userName, imageUrl: imageUrl, live: live, + avatarBuilder: avatarBuilder, ); } /// Constructor variant for a "Close Friends" story with a green border. - factory StoryCircle.cf({required String userName, required String imageUrl}) { + factory StoryCircle.cf({ + required String userName, + required String imageUrl, + Widget Function(Widget avatar)? avatarBuilder, + }) { return StoryCircle._( type: StoryType.cf, userName: userName, imageUrl: imageUrl, live: '', + avatarBuilder: avatarBuilder, ); } @@ -62,12 +73,14 @@ class StoryCircle extends StatelessWidget { factory StoryCircle.create({ required String userName, required String imageUrl, + Widget Function(Widget avatar)? avatarBuilder, }) { return StoryCircle._( type: StoryType.create, userName: userName, imageUrl: imageUrl, live: '', + avatarBuilder: avatarBuilder, ); } @@ -111,20 +124,9 @@ class StoryCircle extends StatelessWidget { child: Padding( padding: const EdgeInsets.all(_ringGap), child: ClipOval( - child: imageUrl.isNotEmpty - ? CachedNetworkImage( - fadeInDuration: Duration.zero, - fadeOutDuration: Duration.zero, - imageUrl: imageUrl, - width: _imageSize, - height: _imageSize, - fit: BoxFit.cover, - errorWidget: (context, url, error) => - const DefaultProfileAvatar( - size: _imageSize, - ), - ) - : const DefaultProfileAvatar(size: _imageSize), + child: + avatarBuilder?.call(_buildAvatar()) ?? + _buildAvatar(), ), ), ), @@ -160,6 +162,22 @@ class StoryCircle extends StatelessWidget { return null; } } + + Widget _buildAvatar() { + if (imageUrl.isEmpty) { + return const DefaultProfileAvatar(size: _imageSize); + } + return CachedNetworkImage( + fadeInDuration: Duration.zero, + fadeOutDuration: Duration.zero, + imageUrl: imageUrl, + width: _imageSize, + height: _imageSize, + fit: BoxFit.cover, + errorWidget: (context, url, error) => + const DefaultProfileAvatar(size: _imageSize), + ); + } } class _LiveBadge extends StatelessWidget { diff --git a/lib/src/core/design_system/templates/chat_list_page_template.dart b/lib/src/core/design_system/templates/chat_list_page_template.dart index b5a03b0b..0ab11854 100644 --- a/lib/src/core/design_system/templates/chat_list_page_template.dart +++ b/lib/src/core/design_system/templates/chat_list_page_template.dart @@ -13,6 +13,7 @@ class ChatListItemData { this.avatarUrl, this.verified = false, this.unread = false, + this.avatarBuilder, }); final String? avatarUrl; @@ -22,6 +23,7 @@ class ChatListItemData { final String preview; final bool verified; final bool unread; + final Widget Function(Widget avatar)? avatarBuilder; } class ChatListPageTemplate extends StatelessWidget { @@ -117,16 +119,17 @@ class _ChatTile extends StatelessWidget { Widget build(BuildContext context) { final theme = Theme.of(context); final onSurface = theme.colorScheme.onSurface; + final avatar = UserAvatar( + imageUrl: data.avatarUrl ?? '', + username: data.handle, + size: 50.45, + ); return ListTile( onTap: onTap, horizontalTitleGap: 12, contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), - leading: UserAvatar( - imageUrl: data.avatarUrl ?? '', - username: data.handle, - size: 50.45, - ), + leading: data.avatarBuilder?.call(avatar) ?? avatar, title: Row( children: [ Expanded( diff --git a/lib/src/core/design_system/templates/info_bar_template.dart b/lib/src/core/design_system/templates/info_bar_template.dart index 02683340..f5848df5 100644 --- a/lib/src/core/design_system/templates/info_bar_template.dart +++ b/lib/src/core/design_system/templates/info_bar_template.dart @@ -23,6 +23,7 @@ class InfoBarTemplate extends StatefulWidget { this.altAvailable = false, this.onAltTap, this.avatarUrl, + this.avatarBuilder, }); /// Display name @@ -57,6 +58,7 @@ class InfoBarTemplate extends StatefulWidget { /// Avatar shown on left of the name/handle. final String? avatarUrl; + final Widget Function(Widget avatar)? avatarBuilder; @override State createState() => _InfoBarTemplateState(); @@ -95,6 +97,7 @@ class _InfoBarTemplateState extends State displayName: widget.displayName, size: 32, onTap: widget.onAvatarTap ?? widget.onTitleTap, + avatarBuilder: widget.avatarBuilder, ), ), diff --git a/lib/src/core/design_system/templates/profile_page_template.dart b/lib/src/core/design_system/templates/profile_page_template.dart index e25ef529..98930153 100644 --- a/lib/src/core/design_system/templates/profile_page_template.dart +++ b/lib/src/core/design_system/templates/profile_page_template.dart @@ -24,6 +24,7 @@ class ProfilePageTemplate extends StatelessWidget { required this.tabsWidget, super.key, this.avatarUrl, + this.avatarBuilder, this.description, this.links, this.knownFollowers, @@ -58,6 +59,7 @@ class ProfilePageTemplate extends StatelessWidget { final String followersCount; final String followingCount; final String? avatarUrl; + final Widget Function(Widget avatar)? avatarBuilder; final String? description; final List? links; final KnownFollowers? knownFollowers; @@ -119,6 +121,7 @@ class ProfilePageTemplate extends StatelessWidget { followersCount: followersCount, followingCount: followingCount, avatarUrl: avatarUrl, + avatarBuilder: avatarBuilder, description: description, links: links, knownFollowers: knownFollowers, @@ -169,6 +172,7 @@ class _ProfileHeaderSection extends StatelessWidget { required this.isBlocking, required this.isEarlySupporter, this.avatarUrl, + this.avatarBuilder, this.description, this.links, this.knownFollowers, @@ -190,6 +194,7 @@ class _ProfileHeaderSection extends StatelessWidget { final String followersCount; final String followingCount; final String? avatarUrl; + final Widget Function(Widget avatar)? avatarBuilder; final String? description; final List? links; final KnownFollowers? knownFollowers; @@ -211,6 +216,17 @@ class _ProfileHeaderSection extends StatelessWidget { @override Widget build(BuildContext context) { + final avatar = ProfileAvatar( + avatarUrl: avatarUrl, + displayName: displayName, + hasStories: hasStories, + size: 80, + onTap: onAvatarTap, + showAddButton: isCurrentUser, + onAddTap: onAddStoryTap, + avatarBuilder: avatarBuilder, + ); + return Padding( padding: const EdgeInsets.all(16), child: Column( @@ -219,17 +235,7 @@ class _ProfileHeaderSection extends StatelessWidget { Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Skeleton.keep( - child: ProfileAvatar( - avatarUrl: avatarUrl, - displayName: displayName, - hasStories: hasStories, - size: 80, - onTap: onAvatarTap, - showAddButton: isCurrentUser, - onAddTap: onAddStoryTap, - ), - ), + Skeleton.keep(child: avatar), const SizedBox(width: 16), Expanded( child: Column( diff --git a/lib/src/core/moderation/moderated_content.dart b/lib/src/core/moderation/moderated_content.dart index 7751dd5b..404c5d16 100644 --- a/lib/src/core/moderation/moderated_content.dart +++ b/lib/src/core/moderation/moderated_content.dart @@ -47,6 +47,33 @@ class ModeratedContent extends ConsumerStatefulWidget { ConsumerState createState() => _ModeratedContentState(); } +class ModeratedProfileAvatar extends StatelessWidget { + const ModeratedProfileAvatar({ + required this.labels, + required this.subjectDid, + required this.child, + super.key, + }); + + final List