sojorn/sojorn_app/lib/utils/link_handler.dart
Patrick Britton 3c4680bdd7 Initial commit: Complete threaded conversation system with inline replies
**Major Features Added:**
- **Inline Reply System**: Replace compose screen with inline reply boxes
- **Thread Navigation**: Parent/child navigation with jump functionality
- **Chain Flow UI**: Reply counts, expand/collapse animations, visual hierarchy
- **Enhanced Animations**: Smooth transitions, hover effects, micro-interactions

 **Frontend Changes:**
- **ThreadedCommentWidget**: Complete rewrite with animations and navigation
- **ThreadNode Model**: Added parent references and descendant counting
- **ThreadedConversationScreen**: Integrated navigation handlers
- **PostDetailScreen**: Replaced with threaded conversation view
- **ComposeScreen**: Added reply indicators and context
- **PostActions**: Fixed visibility checks for chain buttons

 **Backend Changes:**
- **API Route**: Added /posts/:id/thread endpoint
- **Post Repository**: Include allow_chain and visibility fields in feed
- **Thread Handler**: Support for fetching post chains

 **UI/UX Improvements:**
- **Reply Context**: Clear indication when replying to specific posts
- **Character Counting**: 500 character limit with live counter
- **Visual Hierarchy**: Depth-based indentation and styling
- **Smooth Animations**: SizeTransition, FadeTransition, hover states
- **Chain Navigation**: Parent/child buttons with visual feedback

 **Technical Enhancements:**
- **Animation Controllers**: Proper lifecycle management
- **State Management**: Clean separation of concerns
- **Navigation Callbacks**: Reusable navigation system
- **Error Handling**: Graceful fallbacks and user feedback

This creates a Reddit-style threaded conversation experience with smooth
animations, inline replies, and intuitive navigation between posts in a chain.
2026-01-30 07:40:19 -06:00

97 lines
3.2 KiB
Dart

import 'package:flutter/material.dart';
import 'package:latlong2/latlong.dart';
import 'package:url_launcher/url_launcher.dart';
import 'external_link_controller.dart';
import '../routes/app_routes.dart';
/// Utility for safely launching URLs and links.
///
/// Provides a clean interface for opening external links from anywhere
/// in the app, with automatic scheme detection and error handling.
///
/// NOTE: For external URLs, prefer using [ExternalLinkController.handleUrl]
/// which includes safety checks against the domain whitelist.
class LinkHandler {
/// Launches a URL string, handling common edge cases.
///
/// [context] - BuildContext for showing error SnackBars
/// [url] - The URL to open (can be with or without scheme)
///
/// If the URL doesn't have a scheme (http/https), https is assumed.
/// Uses the [ExternalLinkController] for safety checks on external links.
/// Handles sojorn:// deep links by navigating to the Beacon screen.
static Future<void> launchLink(BuildContext context, String url) async {
if (url.trim().isEmpty) return;
// Handle sojorn:// deep links - navigate within app to Beacon screen
if (url.startsWith('sojorn://')) {
Uri? uri = Uri.tryParse(url.replaceFirst('sojorn://', 'sojorn://'));
// Normalize to https for query parsing if needed
uri ??=
Uri.tryParse(url.replaceFirst('sojorn://', 'https://gosojorn.com/'));
final latParam = uri?.queryParameters['lat'];
final longParam = uri?.queryParameters['long'];
if (latParam != null && longParam != null) {
final lat = double.tryParse(latParam);
final long = double.tryParse(longParam);
if (lat != null && long != null) {
AppRoutes.navigateToBeacon(context, LatLng(lat, long));
return;
}
}
// If no valid coordinates, just open beacon screen at current/default
AppRoutes.navigateToBeacon(context, LatLng(37.7749, -122.4194));
return;
}
// Ensure scheme exists
final Uri? uri = Uri.tryParse(url);
final Uri effectiveUri;
if (uri != null && !uri.hasScheme) {
effectiveUri = Uri.parse('https://$url');
} else {
effectiveUri = uri ?? Uri.parse(url);
}
// Use ExternalLinkController for safety checks
await ExternalLinkController.handleUrl(context, effectiveUri.toString());
}
/// Quick launcher for safe/trusted URLs without safety prompts.
/// Use this only for URLs you know are safe.
///
/// [context] - BuildContext for showing error SnackBars
/// [url] - The URL to open
static Future<void> launchSafeUrl(BuildContext context, String url) async {
if (url.trim().isEmpty) return;
final Uri? uri = Uri.tryParse(url);
if (uri == null) return;
try {
if (await canLaunchUrl(uri)) {
await launchUrl(
uri,
mode: LaunchMode.externalApplication,
);
} else {
_showError(context, 'Could not open link.');
}
} catch (e) {
_showError(context, 'Error opening link: $e');
}
}
static void _showError(BuildContext context, String message) {
final messenger = ScaffoldMessenger.of(context);
messenger.showSnackBar(
SnackBar(content: Text(message)),
);
}
}