Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Even more link handling! #1200

Merged
merged 2 commits into from
Mar 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading