[READ ONLY MIRROR] Open Source TikTok alternative built on AT Protocol github.com/sprksocial/client
flutter atproto video dart
10
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: add share post page for deeplinks

+101 -29
+1
lib/src/core/routing/app_router.dart
··· 134 134 ), 135 135 136 136 // Deep linking routes or routes that will be pushed on top of everything 137 + AutoRoute(page: SharedPostRoute.page, path: '/post/:did/:rkey'), 137 138 AutoRoute(page: StandalonePostRoute.page, path: '/post/:postUri'), 138 139 AutoRoute(page: ProfileRoute.page, path: '/profile/:did'), 139 140 AutoRoute(
+1
lib/src/core/routing/pages.dart
··· 8 8 export 'package:spark/src/features/comments/ui/pages/replies_page.dart'; 9 9 export 'package:spark/src/features/feed/ui/pages/feed_page.dart'; 10 10 export 'package:spark/src/features/feed/ui/pages/feeds_page.dart'; 11 + export 'package:spark/src/features/feed/ui/pages/shared_post_page.dart'; 11 12 export 'package:spark/src/features/feed/ui/pages/standalone_post_page.dart'; 12 13 export 'package:spark/src/features/home/ui/pages/empty_page.dart'; 13 14 export 'package:spark/src/features/home/ui/pages/main_page.dart';
+16 -10
lib/src/core/utils/share_urls.dart
··· 19 19 return null; 20 20 } 21 21 22 - final canonicalMatch = _parseCanonicalSparkUri(trimmedPostUri); 22 + final canonicalMatch = parseCanonicalSparkPostUri(trimmedPostUri); 23 23 if (canonicalMatch != null) { 24 - return trimmedPostUri; 24 + return 'at://${canonicalMatch.did}/so.sprk.feed.post/${canonicalMatch.rkey}'; 25 25 } 26 26 27 27 final shortUriMatch = RegExp( ··· 31 31 return null; 32 32 } 33 33 34 - final did = shortUriMatch.group(1)!; 35 - final rkey = shortUriMatch.group(2)!; 34 + final did = Uri.decodeComponent(shortUriMatch.group(1)!); 35 + final rkey = Uri.decodeComponent(shortUriMatch.group(2)!); 36 36 return 'at://$did/so.sprk.feed.post/$rkey'; 37 37 } 38 38 39 - ({String did, String rkey})? _parseCanonicalSparkUri(String uri) { 39 + ({String did, String rkey})? parseCanonicalSparkPostUri(String uri) { 40 40 final match = RegExp( 41 41 r'^at://([^/]+)/so\.sprk\.feed\.post/([^/?#]+)$', 42 42 ).firstMatch(uri); 43 43 if (match == null) return null; 44 - return (did: match.group(1)!, rkey: match.group(2)!); 44 + return ( 45 + did: Uri.decodeComponent(match.group(1)!), 46 + rkey: Uri.decodeComponent(match.group(2)!), 47 + ); 45 48 } 46 49 47 50 String buildSparkShareUrl(String postUri) { 48 - final normalizedPostUri = normalizeSparkPostUri(postUri); 49 - final parts = _parseCanonicalSparkUri(normalizedPostUri); 51 + final canonicalPostUri = canonicalizeSparkPostUri(postUri); 52 + final parts = canonicalPostUri == null 53 + ? null 54 + : parseCanonicalSparkPostUri(canonicalPostUri); 50 55 if (parts == null) { 56 + final normalizedPostUri = normalizeSparkPostUri(postUri); 51 57 return Uri.https(_sparkShareHost, '/watch', { 52 58 'uri': normalizedPostUri, 53 59 }).toString(); ··· 79 85 r'^/post/([^/]+)/([^/?#]+)$', 80 86 ).firstMatch(uri.path); 81 87 if (postPathMatch != null) { 82 - final identifier = postPathMatch.group(1)!; 83 - final rkey = postPathMatch.group(2)!; 88 + final identifier = Uri.decodeComponent(postPathMatch.group(1)!); 89 + final rkey = Uri.decodeComponent(postPathMatch.group(2)!); 84 90 final did = identifier.startsWith('did:') 85 91 ? identifier 86 92 : 'did:plc:$identifier';
+24
lib/src/features/feed/ui/pages/shared_post_page.dart
··· 1 + import 'package:auto_route/auto_route.dart'; 2 + import 'package:flutter/material.dart'; 3 + import 'package:spark/src/features/feed/ui/pages/standalone_post_page.dart'; 4 + 5 + @RoutePage() 6 + class SharedPostPage extends StatelessWidget { 7 + const SharedPostPage({ 8 + @PathParam('did') required this.did, 9 + @PathParam('rkey') required this.rkey, 10 + super.key, 11 + }); 12 + 13 + final String did; 14 + final String rkey; 15 + 16 + @override 17 + Widget build(BuildContext context) { 18 + final canonicalPostUri = 19 + 'at://${Uri.decodeComponent(did)}/so.sprk.feed.post/' 20 + '${Uri.decodeComponent(rkey)}'; 21 + 22 + return StandalonePostPage(postUri: canonicalPostUri); 23 + } 24 + }
+1 -19
lib/src/sprk_app.dart
··· 1 - import 'package:auto_route/auto_route.dart'; 2 1 import 'package:flutter/material.dart'; 3 2 import 'package:flutter/services.dart'; 4 3 import 'package:flutter_riverpod/flutter_riverpod.dart'; ··· 7 6 import 'package:spark/src/core/l10n/app_localizations.dart'; 8 7 import 'package:spark/src/core/routing/app_router.dart'; 9 8 import 'package:spark/src/core/ui/theme/providers/theme_provider.dart'; 10 - import 'package:spark/src/core/utils/share_urls.dart'; 11 9 import 'package:spark/src/core/utils/logging/log_service.dart'; 12 10 import 'package:spark/src/core/utils/logging/logger.dart'; 13 11 import 'package:spark/src/features/feed/providers/feed_provider.dart'; ··· 91 89 } 92 90 return supportedLocales.first; 93 91 }, 94 - routerConfig: _appRouter.config(deepLinkBuilder: _buildIncomingDeepLink), 95 - ); 96 - } 97 - 98 - Future<DeepLink> _buildIncomingDeepLink(PlatformDeepLink deepLink) async { 99 - final canonicalPostUri = extractCanonicalSparkPostUri( 100 - deepLink.uri.toString(), 101 - ); 102 - if (canonicalPostUri == null) { 103 - return deepLink; 104 - } 105 - 106 - _logger.i( 107 - 'Resolved incoming post deep link ${deepLink.uri} ' 108 - 'to canonical URI $canonicalPostUri', 92 + routerConfig: _appRouter.config(), 109 93 ); 110 - 111 - return DeepLink.single(StandalonePostRoute(postUri: canonicalPostUri)); 112 94 } 113 95 }
+58
test/src/core/utils/share_urls_test.dart
··· 1 + import 'package:flutter_test/flutter_test.dart'; 2 + import 'package:spark/src/core/utils/share_urls.dart'; 3 + 4 + void main() { 5 + group('buildSparkShareUrl', () { 6 + test('builds post URLs from canonical Spark post URIs', () { 7 + expect( 8 + buildSparkShareUrl('at://did:plc:abc123/so.sprk.feed.post/postkey'), 9 + 'https://sprk.so/post/did:plc:abc123/postkey', 10 + ); 11 + }); 12 + 13 + test('builds post URLs from short Spark post URIs', () { 14 + expect( 15 + buildSparkShareUrl('did:plc:abc123/postkey'), 16 + 'https://sprk.so/post/did:plc:abc123/postkey', 17 + ); 18 + }); 19 + }); 20 + 21 + group('extractCanonicalSparkPostUri', () { 22 + test('parses canonical Spark post URLs', () { 23 + expect( 24 + extractCanonicalSparkPostUri( 25 + 'https://sprk.so/post/did:plc:abc123/postkey', 26 + ), 27 + 'at://did:plc:abc123/so.sprk.feed.post/postkey', 28 + ); 29 + }); 30 + 31 + test('parses encoded DIDs from universal-link paths', () { 32 + expect( 33 + extractCanonicalSparkPostUri( 34 + 'https://sprk.so/post/did%3Aplc%3Aabc123/postkey', 35 + ), 36 + 'at://did:plc:abc123/so.sprk.feed.post/postkey', 37 + ); 38 + }); 39 + 40 + test('parses legacy watch URLs', () { 41 + expect( 42 + extractCanonicalSparkPostUri( 43 + 'https://sprk.so/watch?uri=at%3A%2F%2Fdid%3Aplc%3Aabc123%2Fso.sprk.feed.post%2Fpostkey', 44 + ), 45 + 'at://did:plc:abc123/so.sprk.feed.post/postkey', 46 + ); 47 + }); 48 + }); 49 + 50 + test('parseCanonicalSparkPostUri returns did and rkey', () { 51 + expect( 52 + parseCanonicalSparkPostUri( 53 + 'at://did:plc:abc123/so.sprk.feed.post/postkey', 54 + ), 55 + (did: 'did:plc:abc123', rkey: 'postkey'), 56 + ); 57 + }); 58 + }