Skip to content

Commit

Permalink
Improved user/community link handling (#1200)
Browse files Browse the repository at this point in the history
* Even more link handling!

* Show loading page
  • Loading branch information
micahmo authored Mar 14, 2024
1 parent e5ab0e3 commit 00bb1cb
Show file tree
Hide file tree
Showing 4 changed files with 208 additions and 56 deletions.
54 changes: 30 additions & 24 deletions lib/feed/utils/utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import 'package:thunder/community/bloc/community_bloc.dart';
import 'package:thunder/core/auth/bloc/auth_bloc.dart';
import 'package:thunder/feed/feed.dart';
import 'package:thunder/instance/bloc/instance_bloc.dart';
import 'package:thunder/shared/pages/loading_page.dart';
import 'package:thunder/shared/sort_picker.dart';
import 'package:thunder/community/widgets/community_drawer.dart';
import 'package:thunder/thunder/bloc/thunder_bloc.dart';
Expand Down Expand Up @@ -96,34 +97,39 @@ Future<void> navigateToFeedPage(
);
}

Navigator.of(context).push(
SwipeablePageRoute(
transitionDuration: reduceAnimations ? const Duration(milliseconds: 100) : null,
backGestureDetectionWidth: 45,
canOnlySwipeFromEdge: disableFullPageSwipe(isUserLoggedIn: authBloc.state.isLoggedIn, state: thunderBloc.state, isFeedPage: true) || !thunderState.enableFullScreenSwipeNavigationGesture,
builder: (context) => MultiBlocProvider(
providers: [
BlocProvider.value(value: accountBloc),
BlocProvider.value(value: authBloc),
BlocProvider.value(value: thunderBloc),
BlocProvider.value(value: instanceBloc),
BlocProvider.value(value: anonymousSubscriptionsBloc),
BlocProvider.value(value: communityBloc),
],
child: Material(
child: FeedPage(
feedType: feedType,
sortType: sortType ?? thunderBloc.state.defaultSortType,
communityName: communityName,
communityId: communityId,
userId: userId,
username: username,
postListingType: postListingType,
),
SwipeablePageRoute route = SwipeablePageRoute(
transitionDuration: isLoadingPageShown
? Duration.zero
: reduceAnimations
? const Duration(milliseconds: 100)
: null,
reverseTransitionDuration: reduceAnimations ? const Duration(milliseconds: 100) : const Duration(milliseconds: 500),
backGestureDetectionWidth: 45,
canOnlySwipeFromEdge: disableFullPageSwipe(isUserLoggedIn: authBloc.state.isLoggedIn, state: thunderBloc.state, isFeedPage: true) || !thunderState.enableFullScreenSwipeNavigationGesture,
builder: (context) => MultiBlocProvider(
providers: [
BlocProvider.value(value: accountBloc),
BlocProvider.value(value: authBloc),
BlocProvider.value(value: thunderBloc),
BlocProvider.value(value: instanceBloc),
BlocProvider.value(value: anonymousSubscriptionsBloc),
BlocProvider.value(value: communityBloc),
],
child: Material(
child: FeedPage(
feedType: feedType,
sortType: sortType ?? thunderBloc.state.defaultSortType,
communityName: communityName,
communityId: communityId,
userId: userId,
username: username,
postListingType: postListingType,
),
),
),
);

pushOnTopOfLoadingPage(context, route);
}

Future<void> triggerRefresh(BuildContext context) async {
Expand Down
26 changes: 1 addition & 25 deletions lib/shared/link_preview_card.dart
Original file line number Diff line number Diff line change
Expand Up @@ -267,31 +267,7 @@ class LinkPreviewCard extends StatelessWidget {
}

if (originURL != null) {
String? communityName = await getLemmyCommunity(originURL!);

if (communityName != null) {
try {
await navigateToFeedPage(context, feedType: FeedType.community, communityName: communityName);
return;
} catch (e) {
// Ignore exception, if it's not a valid community we'll perform the next fallback
}
}

String? username = await getLemmyUser(originURL!);

if (username != null) {
try {
await navigateToFeedPage(context, feedType: FeedType.user, username: username);
return;
} catch (e) {
// Ignore exception, if it's not a valid user, we'll perform the next fallback
}
}

if (context.mounted) {
handleLink(context, url: originURL!);
}
handleLink(context, url: originURL!);
}
}

Expand Down
96 changes: 96 additions & 0 deletions lib/shared/pages/loading_page.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import 'dart:io';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:swipeable_page_route/swipeable_page_route.dart';
import 'package:thunder/thunder/bloc/thunder_bloc.dart';

bool isLoadingPageShown = false;

class LoadingPage extends StatelessWidget {
const LoadingPage({super.key});

@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);

return Scaffold(
body: Container(
color: theme.colorScheme.background,
child: SafeArea(
top: false,
child: CustomScrollView(
slivers: [
SliverAppBar(
toolbarHeight: 70.0,
leading: IconButton(
icon: !kIsWeb && Platform.isIOS
? Icon(
Icons.arrow_back_ios_new_rounded,
semanticLabel: MaterialLocalizations.of(context).backButtonTooltip,
)
: Icon(Icons.arrow_back_rounded, semanticLabel: MaterialLocalizations.of(context).backButtonTooltip),
onPressed: () => Navigator.of(context).maybePop(),
)),
const SliverFillRemaining(
child: Center(
child: CircularProgressIndicator(),
),
),
],
),
),
),
);
}
}

void showLoadingPage(BuildContext context) {
if (isLoadingPageShown) return;

isLoadingPageShown = true;

// Immediately push the loading page.
final ThunderBloc thunderBloc = context.read<ThunderBloc>();
final bool reduceAnimations = thunderBloc.state.reduceAnimations;
Navigator.of(context).push(
SwipeablePageRoute(
transitionDuration: reduceAnimations ? const Duration(milliseconds: 100) : null,
backGestureDetectionWidth: 45,
canOnlySwipeFromEdge: !thunderBloc.state.enableFullScreenSwipeNavigationGesture,
builder: (context) => MultiBlocProvider(
providers: [
BlocProvider.value(value: thunderBloc),
],
child: PopScope(
onPopInvoked: (didPop) => isLoadingPageShown = !didPop,
child: const LoadingPage(),
),
),
),
);
}

Future<void> hideLoadingPage(BuildContext context, {bool delay = false}) async {
if (isLoadingPageShown) {
isLoadingPageShown = false;

if (delay) {
await Future.delayed(const Duration(seconds: 1));
}

if (context.mounted) {
Navigator.of(context).maybePop();
}
}
}

void pushOnTopOfLoadingPage(BuildContext context, Route route) {
if (isLoadingPageShown) {
isLoadingPageShown = false;
Navigator.of(context).pushReplacement(route);
} else {
Navigator.of(context).push(route);
}
}
88 changes: 81 additions & 7 deletions lib/utils/links.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:lemmy_api_client/v3.dart';
import 'package:link_preview_generator/link_preview_generator.dart';
import 'package:share_plus/share_plus.dart';
import 'package:swipeable_page_route/swipeable_page_route.dart';
import 'package:thunder/core/enums/browser_mode.dart';
import 'package:thunder/instances.dart';
import 'package:thunder/shared/pages/loading_page.dart';
import 'package:thunder/shared/webview.dart';
import 'package:thunder/utils/bottom_sheet_list_picker.dart';
import 'package:thunder/utils/media/image.dart';
Expand Down Expand Up @@ -70,9 +73,11 @@ void _openLink(BuildContext context, {required String url}) async {
ThunderState state = context.read<ThunderBloc>().state;

if (state.browserMode == BrowserMode.external || (!kIsWeb && !Platform.isAndroid && !Platform.isIOS)) {
hideLoadingPage(context, delay: true);
url_launcher.launchUrl(Uri.parse(url), mode: url_launcher.LaunchMode.externalApplication);
} else if (state.browserMode == BrowserMode.customTabs) {
await launchUrl(
hideLoadingPage(context, delay: true);
launchUrl(
Uri.parse(url),
customTabsOptions: CustomTabsOptions(
browser: const CustomTabsBrowserConfiguration(
Expand Down Expand Up @@ -101,13 +106,24 @@ void _openLink(BuildContext context, {required String url}) async {
if (uri != null && uri.scheme != 'https') {
// Although a non-https scheme is an indication that this link is intended for another app,
// we actually have to change it back to https in order for the intent to be properly passed to another app.
hideLoadingPage(context, delay: true);
url_launcher.launchUrl(uri, mode: url_launcher.LaunchMode.externalApplication);
} else {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => WebView(url: url),
),
final bool reduceAnimations = state.reduceAnimations;

SwipeablePageRoute route = SwipeablePageRoute(
transitionDuration: isLoadingPageShown
? Duration.zero
: reduceAnimations
? const Duration(milliseconds: 100)
: null,
reverseTransitionDuration: reduceAnimations ? const Duration(milliseconds: 100) : const Duration(milliseconds: 500),
backGestureDetectionWidth: 45,
canOnlySwipeFromEdge: true,
builder: (context) => WebView(url: url),
);

pushOnTopOfLoadingPage(context, route);
}
}
}
Expand All @@ -121,7 +137,7 @@ void handleLink(BuildContext context, {required String url}) async {

// Try navigating to community
String? communityName = await getLemmyCommunity(url);
if (communityName != null) {
if (communityName != null && (!context.mounted || await _testValidCommunity(context, url, communityName, communityName.split('@')[1]))) {
try {
if (context.mounted) {
await navigateToFeedPage(context, feedType: FeedType.community, communityName: communityName);
Expand All @@ -134,7 +150,7 @@ void handleLink(BuildContext context, {required String url}) async {

// Try navigating to user
String? username = await getLemmyUser(url);
if (username != null) {
if (username != null && (!context.mounted || await _testValidUser(context, url, username, username.split('@')[1]))) {
try {
if (context.mounted) {
await navigateToFeedPage(context, feedType: FeedType.user, username: username);
Expand Down Expand Up @@ -197,6 +213,64 @@ void handleLink(BuildContext context, {required String url}) async {
}
}

/// This is a helper method which helps [handleLink] determine whether a link refers to a valid Lemmy community.
/// If the passed in link is not a valid URI, then there's no point in doing any fallback, so assume it passes.
/// If the passed in [instance] is a known Lemmy instance, then it passes.
/// If we can retrieve the passed in object, then it passes.
/// Otherwise it fails.
Future<bool> _testValidCommunity(BuildContext context, String link, String communityName, String instance) async {
Uri? uri = Uri.tryParse(link);
if (uri == null || !uri.hasScheme) {
return true;
}

if (instances.contains(instance)) {
return true;
}

try {
// Since this may take a while, show a loading page.
showLoadingPage(context);

Account? account = await fetchActiveProfileAccount();
await LemmyClient.instance.lemmyApiV3.run(GetCommunity(name: communityName, auth: account?.jwt));
return true;
} catch (e) {
// Ignore and return false below.
}

return false;
}

/// This is a helper method which helps [handleLink] determine whether a link refers to a valid Lemmy user.
/// If the passed in link is not a valid URI, then there's no point in doing any fallback, so assume it passes.
/// If the passed in [instance] is a known Lemmy instance, then it passes.
/// If we can retrieve the passed in object, then it passes.
/// Otherwise it fails.
Future<bool> _testValidUser(BuildContext context, String link, String userName, String instance) async {
Uri? uri = Uri.tryParse(link);
if (uri == null || !uri.hasScheme) {
return true;
}

if (instances.contains(instance)) {
return true;
}

try {
// Since this may take a while, show a loading page.
showLoadingPage(context);

Account? account = await fetchActiveProfileAccount();
await LemmyClient.instance.lemmyApiV3.run(GetPersonDetails(username: userName, auth: account?.jwt));
return true;
} catch (e) {
// Ignore and return false below.
}

return false;
}

void handleLinkLongPress(BuildContext context, ThunderState state, String text, String? url) {
final theme = Theme.of(context);
final l10n = AppLocalizations.of(context)!;
Expand Down

0 comments on commit 00bb1cb

Please sign in to comment.