diff --git a/mobile-app/lib/l10n/app_en.arb b/mobile-app/lib/l10n/app_en.arb index 55f78904e..4086cb6ac 100644 --- a/mobile-app/lib/l10n/app_en.arb +++ b/mobile-app/lib/l10n/app_en.arb @@ -671,6 +671,10 @@ "@temporarily_unavailable": { "description": "snackbar shown when a curriculum item is temporarily disabled" }, + "share_challenge": "Share this challenge", + "@share_challenge": { + "description": "tooltip for the button that shares a link to the current challenge" + }, "not_available_web": "Not available use the web version", "@not_available_web": { "description": "snackbar shown when a curriculum item is only available on the web" diff --git a/mobile-app/lib/l10n/app_es.arb b/mobile-app/lib/l10n/app_es.arb index b58cc6763..70b8e702a 100644 --- a/mobile-app/lib/l10n/app_es.arb +++ b/mobile-app/lib/l10n/app_es.arb @@ -613,6 +613,7 @@ "no_blocks_available": "No blocks available right now.", "coming_soon": "Coming Soon", "temporarily_unavailable": "Temporarily unavailable, come back soon.", + "share_challenge": "Share this challenge", "not_available_web": "Not available use the web version", "stage_core": "Recommended curriculum (still in beta):", "stage_english": "Learn English for Developers:", diff --git a/mobile-app/lib/l10n/app_localizations.dart b/mobile-app/lib/l10n/app_localizations.dart index 49c5745b2..8aaa1da8f 100644 --- a/mobile-app/lib/l10n/app_localizations.dart +++ b/mobile-app/lib/l10n/app_localizations.dart @@ -1067,6 +1067,12 @@ abstract class AppLocalizations { /// **'Temporarily unavailable, come back soon.'** String get temporarily_unavailable; + /// tooltip for the button that shares a link to the current challenge + /// + /// In en, this message translates to: + /// **'Share this challenge'** + String get share_challenge; + /// snackbar shown when a curriculum item is only available on the web /// /// In en, this message translates to: diff --git a/mobile-app/lib/l10n/app_localizations_en.dart b/mobile-app/lib/l10n/app_localizations_en.dart index 86795fe27..d4a21746d 100644 --- a/mobile-app/lib/l10n/app_localizations_en.dart +++ b/mobile-app/lib/l10n/app_localizations_en.dart @@ -576,6 +576,9 @@ class AppLocalizationsEn extends AppLocalizations { String get temporarily_unavailable => 'Temporarily unavailable, come back soon.'; + @override + String get share_challenge => 'Share this challenge'; + @override String get not_available_web => 'Not available use the web version'; diff --git a/mobile-app/lib/l10n/app_localizations_es.dart b/mobile-app/lib/l10n/app_localizations_es.dart index 039aad243..5b99fec04 100644 --- a/mobile-app/lib/l10n/app_localizations_es.dart +++ b/mobile-app/lib/l10n/app_localizations_es.dart @@ -576,6 +576,9 @@ class AppLocalizationsEs extends AppLocalizations { String get temporarily_unavailable => 'Temporarily unavailable, come back soon.'; + @override + String get share_challenge => 'Share this challenge'; + @override String get not_available_web => 'Not available use the web version'; diff --git a/mobile-app/lib/l10n/app_localizations_pt.dart b/mobile-app/lib/l10n/app_localizations_pt.dart index 34349f67f..c7c5a66e2 100644 --- a/mobile-app/lib/l10n/app_localizations_pt.dart +++ b/mobile-app/lib/l10n/app_localizations_pt.dart @@ -576,6 +576,9 @@ class AppLocalizationsPt extends AppLocalizations { String get temporarily_unavailable => 'Temporarily unavailable, come back soon.'; + @override + String get share_challenge => 'Share this challenge'; + @override String get not_available_web => 'Not available use the web version'; diff --git a/mobile-app/lib/l10n/app_pt.arb b/mobile-app/lib/l10n/app_pt.arb index c919afdbb..2bb47de2b 100644 --- a/mobile-app/lib/l10n/app_pt.arb +++ b/mobile-app/lib/l10n/app_pt.arb @@ -613,6 +613,7 @@ "no_blocks_available": "No blocks available right now.", "coming_soon": "Coming Soon", "temporarily_unavailable": "Temporarily unavailable, come back soon.", + "share_challenge": "Share this challenge", "not_available_web": "Not available use the web version", "stage_core": "Recommended curriculum (still in beta):", "stage_english": "Learn English for Developers:", diff --git a/mobile-app/lib/ui/views/learn/challenge/challenge_view.dart b/mobile-app/lib/ui/views/learn/challenge/challenge_view.dart index 34e8990a7..4b4b95ba1 100644 --- a/mobile-app/lib/ui/views/learn/challenge/challenge_view.dart +++ b/mobile-app/lib/ui/views/learn/challenge/challenge_view.dart @@ -7,10 +7,12 @@ import 'package:freecodecamp/models/learn/curriculum_model.dart'; import 'package:freecodecamp/models/learn/daily_challenge_model.dart'; import 'package:freecodecamp/ui/theme/fcc_theme.dart'; import 'package:freecodecamp/ui/views/learn/challenge/challenge_viewmodel.dart'; +import 'package:freecodecamp/ui/views/learn/utils/challenge_utils.dart'; import 'package:freecodecamp/ui/views/learn/widgets/challenge_widgets/project_preview.dart'; import 'package:freecodecamp/ui/views/learn/widgets/challenge_widgets/symbol_bar.dart'; import 'package:freecodecamp/ui/views/learn/widgets/console/console_view.dart'; import 'package:freecodecamp/ui/views/news/html_handler/html_handler.dart'; +import 'package:share_plus/share_plus.dart'; import 'package:stacked/stacked.dart'; class ChallengeView extends StatelessWidget { @@ -268,6 +270,7 @@ class ChallengeView extends StatelessWidget { Row( children: [ ..._panelIconButtons( + context, model, challenge, block, @@ -485,6 +488,7 @@ class ChallengeView extends StatelessWidget { required bool isActive, required IconData icon, required VoidCallback? onPressed, + String? tooltip, }) { return Container( margin: const EdgeInsets.symmetric(horizontal: 8), @@ -499,6 +503,7 @@ class ChallengeView extends StatelessWidget { : Colors.white, ), onPressed: onPressed, + tooltip: tooltip, splashColor: Colors.transparent, highlightColor: Colors.transparent, ), @@ -547,6 +552,7 @@ class ChallengeView extends StatelessWidget { } List _panelIconButtons( + BuildContext context, ChallengeViewModel model, Challenge challenge, Block block, @@ -586,6 +592,19 @@ class ChallengeView extends StatelessWidget { model.setMounted = false; }, ), + _panelIconButton( + isActive: false, + icon: Icons.share, + tooltip: context.t.share_challenge, + onPressed: () { + SharePlus.instance.share( + ShareParams( + text: + '${block.name} - ${challenge.title}\n\n${challengeUrl(challenge)}', + ), + ); + }, + ), ]; } diff --git a/mobile-app/lib/ui/views/learn/utils/challenge_utils.dart b/mobile-app/lib/ui/views/learn/utils/challenge_utils.dart index 22f4beca2..2d39abd52 100644 --- a/mobile-app/lib/ui/views/learn/utils/challenge_utils.dart +++ b/mobile-app/lib/ui/views/learn/utils/challenge_utils.dart @@ -1,6 +1,12 @@ import 'package:freecodecamp/models/learn/challenge_model.dart'; import 'package:freecodecamp/models/learn/curriculum_model.dart'; +// Builds the public freeCodeCamp URL of a challenge, so campers can share the +// exact step they are on. +String challengeUrl(Challenge challenge) { + return 'https://www.freecodecamp.org/learn/${challenge.superBlock}/${challenge.block}/${challenge.dashedName}'; +} + String handleChallengeTitle(Challenge challenge, Block block) { if (block.challenges.length == 1 || challenge.title.contains('Dialogue') || diff --git a/mobile-app/lib/ui/views/learn/widgets/hint/hint_widget_view.dart b/mobile-app/lib/ui/views/learn/widgets/hint/hint_widget_view.dart index 577294903..213ee7e34 100644 --- a/mobile-app/lib/ui/views/learn/widgets/hint/hint_widget_view.dart +++ b/mobile-app/lib/ui/views/learn/widgets/hint/hint_widget_view.dart @@ -4,6 +4,7 @@ import 'package:freecodecamp/models/learn/challenge_model.dart'; import 'package:freecodecamp/models/learn/curriculum_model.dart'; import 'package:freecodecamp/service/learn/learn_service.dart'; import 'package:freecodecamp/ui/views/learn/challenge/challenge_viewmodel.dart'; +import 'package:freecodecamp/ui/views/learn/utils/challenge_utils.dart'; import 'package:freecodecamp/ui/views/learn/widgets/hint/hint_widget_model.dart'; import 'package:freecodecamp/ui/views/news/html_handler/html_handler.dart'; import 'package:freecodecamp/utils/helpers.dart'; @@ -26,7 +27,7 @@ Future genForumLink( final titleText = '$blockTitle - ${currChallenge.title}'; final t = context.t; final String endingText = - '**${t.forum_mobile_info}**\n```txt\n$userDeviceInfo\n```\n\n**${t.forum_challenge}** $titleText\n\n**${t.forum_challenge_link}**\nhttps://www.freecodecamp.org/learn/${currChallenge.superBlock}/${currChallenge.block}/${currChallenge.dashedName}'; + '**${t.forum_mobile_info}**\n```txt\n$userDeviceInfo\n```\n\n**${t.forum_challenge}** $titleText\n\n**${t.forum_challenge_link}**\n${challengeUrl(currChallenge)}'; final String userCode = await filesToMarkdown(currChallenge, editorText); diff --git a/mobile-app/test/unit/challenge_utils_test.dart b/mobile-app/test/unit/challenge_utils_test.dart index c07a0f830..7a4d90b90 100644 --- a/mobile-app/test/unit/challenge_utils_test.dart +++ b/mobile-app/test/unit/challenge_utils_test.dart @@ -40,6 +40,17 @@ void main() { ); } + group('challengeUrl', () { + test('should build the public learn URL of a challenge', () { + final challenge = createChallenge(id: '1', title: 'Step 1'); + + expect( + challengeUrl(challenge), + 'https://www.freecodecamp.org/learn/superblock/block1/challenge-1', + ); + }); + }); + group('handleChallengeTitle', () { test('should return empty string if the block contains a single challenge', () {