diff --git a/lib/packages/ui/src/widgets/actions/bottom_sheet_action.dart b/lib/packages/ui/src/widgets/actions/bottom_sheet_action.dart index a5ade259f..8b7faefba 100644 --- a/lib/packages/ui/src/widgets/actions/bottom_sheet_action.dart +++ b/lib/packages/ui/src/widgets/actions/bottom_sheet_action.dart @@ -4,7 +4,15 @@ import 'package:flutter/material.dart'; /// /// When tapped, will call the [onTap] callback. class BottomSheetAction extends StatelessWidget { - const BottomSheetAction({super.key, required this.leading, this.trailing, required this.title, this.subtitle, required this.onTap}); + const BottomSheetAction({ + super.key, + required this.leading, + this.trailing, + required this.title, + this.subtitle, + required this.onTap, + this.onLongPress, + }); /// The leading widget final Widget leading; @@ -21,20 +29,21 @@ class BottomSheetAction extends StatelessWidget { /// Callback function to be called when the category is tapped final Function() onTap; + /// Callback function to be called when the category is long pressed + final Function()? onLongPress; + @override Widget build(BuildContext context) { final theme = Theme.of(context); return InkWell( onTap: onTap, + onLongPress: onLongPress, customBorder: const StadiumBorder(), child: ListTile( leading: leading, trailing: trailing, - title: Text( - title, - style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w500), - ), + title: Text(title, style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w500)), subtitle: subtitle != null ? Text( subtitle ?? '', diff --git a/lib/src/features/comment/presentation/widgets/comment_bottom_sheet/general_comment_action_bottom_sheet.dart b/lib/src/features/comment/presentation/widgets/comment_bottom_sheet/general_comment_action_bottom_sheet.dart index f2d436a9f..38c8fcf30 100644 --- a/lib/src/features/comment/presentation/widgets/comment_bottom_sheet/general_comment_action_bottom_sheet.dart +++ b/lib/src/features/comment/presentation/widgets/comment_bottom_sheet/general_comment_action_bottom_sheet.dart @@ -8,6 +8,7 @@ import 'package:thunder/src/features/post/post.dart'; import 'package:thunder/src/features/settings/api.dart'; import 'package:thunder/src/foundation/config/global_context.dart'; import 'package:thunder/src/features/instance/domain/utils/instance_link_utils.dart'; +import 'package:thunder/src/shared/full_name_copy_utils.dart'; import 'package:thunder/packages/ui/ui.dart' show BottomSheetAction, MultiPickerItem, PickerItemData; /// Defines the general actions that can be taken on a comment @@ -289,6 +290,15 @@ class _GeneralCommentActionBottomSheetPageState extends State widget.onSwitchActivePage(page), + onLongPress: switch (page) { + GeneralCommentAction.user => () => copyActivityPubFullName( + type: ActivityPubFullNameType.user, + name: widget.comment.creator?.name, + displayName: widget.comment.creator?.displayName, + instance: fetchInstanceNameFromUrl(widget.comment.creator?.actorId), + ), + _ => null, + }, ), ), ], diff --git a/lib/src/features/post/presentation/widgets/post_bottom_sheet/general_post_action_bottom_sheet.dart b/lib/src/features/post/presentation/widgets/post_bottom_sheet/general_post_action_bottom_sheet.dart index 8cb116f2e..d0442d118 100644 --- a/lib/src/features/post/presentation/widgets/post_bottom_sheet/general_post_action_bottom_sheet.dart +++ b/lib/src/features/post/presentation/widgets/post_bottom_sheet/general_post_action_bottom_sheet.dart @@ -8,6 +8,7 @@ import 'package:thunder/src/features/post/post.dart'; import 'package:thunder/src/features/settings/api.dart'; import 'package:thunder/src/foundation/config/global_context.dart'; import 'package:thunder/src/features/instance/domain/utils/instance_link_utils.dart'; +import 'package:thunder/src/shared/full_name_copy_utils.dart'; import 'package:thunder/packages/ui/ui.dart' show BottomSheetAction, MultiPickerItem, PickerItemData; /// Defines the general actions that can be taken on a post @@ -302,6 +303,21 @@ class _GeneralPostActionBottomSheetPageState extends State widget.onSwitchActivePage(page), + onLongPress: switch (page) { + GeneralPostAction.user => () => copyActivityPubFullName( + type: ActivityPubFullNameType.user, + name: widget.post.creator?.name, + displayName: widget.post.creator?.displayName, + instance: fetchInstanceNameFromUrl(widget.post.creator?.actorId), + ), + GeneralPostAction.community => () => copyActivityPubFullName( + type: ActivityPubFullNameType.community, + name: widget.post.community?.name, + displayName: widget.post.community?.title, + instance: fetchInstanceNameFromUrl(widget.post.community?.actorId), + ), + _ => null, + }, ), ), ], diff --git a/lib/src/shared/full_name_copy_utils.dart b/lib/src/shared/full_name_copy_utils.dart new file mode 100644 index 000000000..318a95fa7 --- /dev/null +++ b/lib/src/shared/full_name_copy_utils.dart @@ -0,0 +1,63 @@ +import 'package:flutter/services.dart'; + +import 'package:thunder/src/foundation/config/global_context.dart'; +import 'package:thunder/src/features/settings/api.dart'; +import 'package:thunder/packages/ui/ui.dart' show showSnackbar; + +enum ActivityPubFullNameType { + user, + community, +} + +/// Generates the full name of the given type. +/// +/// For users, the full name is !name@instance.tld +/// For communities, the full name is !name@instance.tld +String? generateActivityPubFullName({ + required ActivityPubFullNameType type, + required String? name, + required String? displayName, + required String? instance, +}) { + if (name == null || name.isEmpty || instance == null || instance.isEmpty) return null; + + return switch (type) { + ActivityPubFullNameType.user => generateUserFullName( + null, + name, + displayName, + instance, + userSeparator: FullNameSeparator.lemmy, + useDisplayName: false, + ), + ActivityPubFullNameType.community => generateCommunityFullName( + null, + name, + displayName, + instance, + communitySeparator: FullNameSeparator.lemmy, + useDisplayName: false, + ), + }; +} + +/// Copies the full name of the given type to the clipboard. +Future copyActivityPubFullName({ + required ActivityPubFullNameType type, + required String? name, + required String? displayName, + required String? instance, +}) async { + final fullName = generateActivityPubFullName( + type: type, + name: name, + displayName: displayName, + instance: instance, + ); + + if (fullName == null || fullName.isEmpty) return; + + HapticFeedback.mediumImpact(); + await Clipboard.setData(ClipboardData(text: fullName)); + showSnackbar(GlobalContext.l10n.copiedToClipboard); +} diff --git a/lib/src/shared/widgets/chips/community_chip.dart b/lib/src/shared/widgets/chips/community_chip.dart index e4ccdb724..8bd6c3f27 100644 --- a/lib/src/shared/widgets/chips/community_chip.dart +++ b/lib/src/shared/widgets/chips/community_chip.dart @@ -8,6 +8,7 @@ import 'package:thunder/src/features/identity/presentation/widgets/full_name_wid import 'package:thunder/src/features/settings/api.dart'; import 'package:thunder/src/features/instance/domain/utils/instance_link_utils.dart'; import 'package:thunder/src/app/shell/navigation/navigation_utils.dart'; +import 'package:thunder/src/shared/full_name_copy_utils.dart'; /// A chip which displays the given community and instance information. /// @@ -52,6 +53,13 @@ class CommunityChip extends StatelessWidget { onTap: () => navigateToFeedPage(context, feedType: FeedType.community, communityId: communityId), child: Tooltip( excludeFromSemantics: true, + triggerMode: TooltipTriggerMode.longPress, + onTriggered: () => copyActivityPubFullName( + type: ActivityPubFullNameType.community, + name: communityName, + displayName: communityTitle, + instance: fetchInstanceNameFromUrl(communityUrl), + ), message: generateCommunityFullName( context, communityName, diff --git a/lib/src/shared/widgets/chips/user_chip.dart b/lib/src/shared/widgets/chips/user_chip.dart index 6acf90871..099d07ed4 100644 --- a/lib/src/shared/widgets/chips/user_chip.dart +++ b/lib/src/shared/widgets/chips/user_chip.dart @@ -11,6 +11,7 @@ import 'package:thunder/src/features/settings/api.dart'; import 'package:thunder/src/features/user/api.dart'; import 'package:thunder/src/features/instance/domain/utils/instance_link_utils.dart'; import 'package:thunder/src/app/shell/navigation/navigation_utils.dart'; +import 'package:thunder/src/shared/full_name_copy_utils.dart'; import 'package:thunder/packages/ui/ui.dart' show Thunder; /// A chip which displays the given user and instance information. Additionally, it renders special chips for special users. @@ -60,6 +61,13 @@ class UserChip extends StatelessWidget { ignoring: ignorePointerEvents, child: Tooltip( excludeFromSemantics: true, + triggerMode: TooltipTriggerMode.longPress, + onTriggered: () => copyActivityPubFullName( + type: ActivityPubFullNameType.user, + name: user.name, + displayName: user.displayName, + instance: fetchInstanceNameFromUrl(user.actorId), + ), message: '${generateUserFullName( context, user.name,