[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.

feat: universal links

+140 -10
+24
android/app/src/main/AndroidManifest.xml
··· 27 27 android:icon="@mipmap/ic_launcher" 28 28 android:roundIcon="@mipmap/ic_launcher_round" 29 29 > 30 + <meta-data 31 + android:name="asset_statements" 32 + android:resource="@string/asset_statements" 33 + /> 30 34 <activity 31 35 android:name=".MainActivity" 32 36 android:exported="true" ··· 47 51 <intent-filter> 48 52 <action android:name="android.intent.action.MAIN" /> 49 53 <category android:name="android.intent.category.LAUNCHER" /> 54 + </intent-filter> 55 + <intent-filter android:autoVerify="true"> 56 + <action android:name="android.intent.action.VIEW" /> 57 + <category android:name="android.intent.category.DEFAULT" /> 58 + <category android:name="android.intent.category.BROWSABLE" /> 59 + <data 60 + android:scheme="https" 61 + android:host="sprk.so" 62 + android:pathPrefix="/post" 63 + /> 64 + <data 65 + android:scheme="https" 66 + android:host="sprk.so" 67 + android:pathPrefix="/profile" 68 + /> 69 + <data 70 + android:scheme="https" 71 + android:host="sprk.so" 72 + android:pathPrefix="/watch" 73 + /> 50 74 </intent-filter> 51 75 </activity> 52 76 <!-- OAuth callback activity for flutter_web_auth_2 -->
+12
android/app/src/main/res/values/strings.xml
··· 1 + <?xml version="1.0" encoding="utf-8"?> 2 + <resources> 3 + <string name="asset_statements"><![CDATA[ 4 + [{ 5 + "relation": ["delegate_permission/common.handle_all_urls"], 6 + "target": { 7 + "namespace": "web", 8 + "site": "https://sprk.so" 9 + } 10 + }] 11 + ]]></string> 12 + </resources>
+4
ios/Runner/Runner.entitlements
··· 4 4 <dict> 5 5 <key>aps-environment</key> 6 6 <string>development</string> 7 + <key>com.apple.developer.associated-domains</key> 8 + <array> 9 + <string>applinks:sprk.so</string> 10 + </array> 7 11 </dict> 8 12 </plist>
+83 -9
lib/src/core/utils/share_urls.dart
··· 13 13 return normalizedPostUri; 14 14 } 15 15 16 + String? canonicalizeSparkPostUri(String postUri) { 17 + final trimmedPostUri = postUri.trim(); 18 + if (trimmedPostUri.isEmpty) { 19 + return null; 20 + } 21 + 22 + final canonicalMatch = _parseCanonicalSparkUri(trimmedPostUri); 23 + if (canonicalMatch != null) { 24 + return trimmedPostUri; 25 + } 26 + 27 + final shortUriMatch = RegExp( 28 + r'^(did:[^/]+)/([^/?#]+)$', 29 + ).firstMatch(normalizeSparkPostUri(trimmedPostUri)); 30 + if (shortUriMatch == null) { 31 + return null; 32 + } 33 + 34 + final did = shortUriMatch.group(1)!; 35 + final rkey = shortUriMatch.group(2)!; 36 + return 'at://$did/so.sprk.feed.post/$rkey'; 37 + } 38 + 39 + ({String did, String rkey})? _parseCanonicalSparkUri(String uri) { 40 + final match = RegExp( 41 + r'^at://([^/]+)/so\.sprk\.feed\.post/([^/?#]+)$', 42 + ).firstMatch(uri); 43 + if (match == null) return null; 44 + return (did: match.group(1)!, rkey: match.group(2)!); 45 + } 46 + 16 47 String buildSparkShareUrl(String postUri) { 17 48 final normalizedPostUri = normalizeSparkPostUri(postUri); 49 + final parts = _parseCanonicalSparkUri(normalizedPostUri); 50 + if (parts == null) { 51 + return Uri.https(_sparkShareHost, '/watch', { 52 + 'uri': normalizedPostUri, 53 + }).toString(); 54 + } 18 55 19 - return Uri.https(_sparkShareHost, '/watch', { 20 - 'uri': normalizedPostUri, 21 - }).toString(); 56 + return Uri.https( 57 + _sparkShareHost, 58 + '/post/${parts.did}/${parts.rkey}', 59 + ).toString(); 22 60 } 23 61 24 62 String? extractSparkPostUri(String url) { 63 + final canonicalPostUri = extractCanonicalSparkPostUri(url); 64 + if (canonicalPostUri == null) { 65 + return null; 66 + } 67 + 68 + return normalizeSparkPostUri(canonicalPostUri); 69 + } 70 + 71 + String? extractCanonicalSparkPostUri(String url) { 25 72 try { 26 73 final uri = Uri.parse(url); 27 - final postUri = uri.queryParameters['uri']; 28 - final isSparkHost = uri.host == _sparkShareHost && uri.path == '/watch'; 74 + final isSparkHost = uri.host == _sparkShareHost; 29 75 final isLegacySparkHost = uri.host == _legacySparkShareHost; 30 76 31 - if ((isSparkHost || isLegacySparkHost) && 32 - postUri != null && 33 - postUri.isNotEmpty) { 34 - return normalizeSparkPostUri(postUri); 77 + if (isSparkHost || isLegacySparkHost) { 78 + final postPathMatch = RegExp( 79 + r'^/post/([^/]+)/([^/?#]+)$', 80 + ).firstMatch(uri.path); 81 + if (postPathMatch != null) { 82 + final identifier = postPathMatch.group(1)!; 83 + final rkey = postPathMatch.group(2)!; 84 + final did = identifier.startsWith('did:') 85 + ? identifier 86 + : 'did:plc:$identifier'; 87 + return 'at://$did/so.sprk.feed.post/$rkey'; 88 + } 89 + 90 + final postUri = uri.queryParameters['uri']; 91 + if ((uri.path == '/watch' || isLegacySparkHost) && 92 + postUri != null && 93 + postUri.isNotEmpty) { 94 + return canonicalizeSparkPostUri(postUri); 95 + } 96 + 97 + final shortDid = uri.queryParameters['u']; 98 + final rkey = uri.queryParameters['p']; 99 + if ((uri.path == '/watch' || isLegacySparkHost) && 100 + shortDid != null && 101 + shortDid.isNotEmpty && 102 + rkey != null && 103 + rkey.isNotEmpty) { 104 + final did = shortDid.startsWith('did:') 105 + ? shortDid 106 + : 'did:plc:$shortDid'; 107 + return 'at://$did/so.sprk.feed.post/$rkey'; 108 + } 35 109 } 36 110 } catch (_) { 37 111 // Ignore malformed URLs.
+17 -1
lib/src/sprk_app.dart
··· 6 6 import 'package:spark/src/core/l10n/app_localizations.dart'; 7 7 import 'package:spark/src/core/routing/app_router.dart'; 8 8 import 'package:spark/src/core/ui/theme/providers/theme_provider.dart'; 9 + import 'package:spark/src/core/utils/share_urls.dart'; 9 10 import 'package:spark/src/core/utils/logging/log_service.dart'; 10 11 import 'package:spark/src/core/utils/logging/logger.dart'; 11 12 import 'package:spark/src/features/feed/providers/feed_provider.dart'; ··· 89 90 } 90 91 return supportedLocales.first; 91 92 }, 92 - routerConfig: _appRouter.config(), 93 + routerConfig: _appRouter.config( 94 + deepLinkTransformer: _transformIncomingDeepLink, 95 + ), 96 + ); 97 + } 98 + 99 + Future<Uri> _transformIncomingDeepLink(Uri uri) async { 100 + final canonicalPostUri = extractCanonicalSparkPostUri(uri.toString()); 101 + if (canonicalPostUri == null) { 102 + return uri; 103 + } 104 + 105 + final transformedUri = Uri( 106 + path: '/post/${Uri.encodeComponent(canonicalPostUri)}', 93 107 ); 108 + _logger.i('Transformed incoming deep link from $uri to $transformedUri'); 109 + return transformedUri; 94 110 } 95 111 }