diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 45b1fb7..7154cc7 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,147 +1,11 @@ PODS: - - connectivity_plus (0.0.1): - - Flutter - - Firebase/CoreOnly (12.12.0): - - FirebaseCore (~> 12.12.0) - - Firebase/Crashlytics (12.12.0): - - Firebase/CoreOnly - - FirebaseCrashlytics (~> 12.12.0) - - Firebase/Messaging (12.12.0): - - Firebase/CoreOnly - - FirebaseMessaging (~> 12.12.0) - - firebase_core (4.7.0): - - Firebase/CoreOnly (= 12.12.0) - - Flutter - - firebase_crashlytics (5.2.0): - - Firebase/Crashlytics (= 12.12.0) - - firebase_core - - Flutter - - firebase_messaging (16.2.0): - - Firebase/Messaging (= 12.12.0) - - firebase_core - - Flutter - - FirebaseCore (12.12.1): - - FirebaseCoreInternal (~> 12.12.0) - - GoogleUtilities/Environment (~> 8.1) - - GoogleUtilities/Logger (~> 8.1) - - FirebaseCoreExtension (12.12.0): - - FirebaseCore (~> 12.12.0) - - FirebaseCoreInternal (12.12.0): - - "GoogleUtilities/NSData+zlib (~> 8.1)" - - FirebaseCrashlytics (12.12.1): - - FirebaseCore (~> 12.12.0) - - FirebaseInstallations (~> 12.12.0) - - FirebaseRemoteConfigInterop (~> 12.12.0) - - FirebaseSessions (~> 12.12.0) - - GoogleDataTransport (~> 10.1) - - GoogleUtilities/Environment (~> 8.1) - - nanopb (~> 3.30910.0) - - PromisesObjC (~> 2.4) - - FirebaseInstallations (12.12.0): - - FirebaseCore (~> 12.12.0) - - GoogleUtilities/Environment (~> 8.1) - - GoogleUtilities/UserDefaults (~> 8.1) - - PromisesObjC (~> 2.4) - - FirebaseMessaging (12.12.0): - - FirebaseCore (~> 12.12.0) - - FirebaseInstallations (~> 12.12.0) - - GoogleDataTransport (~> 10.1) - - GoogleUtilities/AppDelegateSwizzler (~> 8.1) - - GoogleUtilities/Environment (~> 8.1) - - GoogleUtilities/Reachability (~> 8.1) - - GoogleUtilities/UserDefaults (~> 8.1) - - nanopb (~> 3.30910.0) - - FirebaseRemoteConfigInterop (12.12.0) - - FirebaseSessions (12.12.0): - - FirebaseCore (~> 12.12.0) - - FirebaseCoreExtension (~> 12.12.0) - - FirebaseInstallations (~> 12.12.0) - - GoogleDataTransport (~> 10.1) - - GoogleUtilities/Environment (~> 8.1) - - GoogleUtilities/UserDefaults (~> 8.1) - - nanopb (~> 3.30910.0) - - PromisesSwift (~> 2.1) - Flutter (1.0.0) - - flutter_local_notifications (0.0.1): - - Flutter - - gal (1.0.0): - - Flutter - - FlutterMacOS - - GoogleDataTransport (10.1.0): - - nanopb (~> 3.30910.0) - - PromisesObjC (~> 2.4) - - GoogleUtilities/AppDelegateSwizzler (8.1.0): - - GoogleUtilities/Environment - - GoogleUtilities/Logger - - GoogleUtilities/Network - - GoogleUtilities/Privacy - - GoogleUtilities/Environment (8.1.0): - - GoogleUtilities/Privacy - - GoogleUtilities/Logger (8.1.0): - - GoogleUtilities/Environment - - GoogleUtilities/Privacy - - GoogleUtilities/Network (8.1.0): - - GoogleUtilities/Logger - - "GoogleUtilities/NSData+zlib" - - GoogleUtilities/Privacy - - GoogleUtilities/Reachability - - "GoogleUtilities/NSData+zlib (8.1.0)": - - GoogleUtilities/Privacy - - GoogleUtilities/Privacy (8.1.0) - - GoogleUtilities/Reachability (8.1.0): - - GoogleUtilities/Logger - - GoogleUtilities/Privacy - - GoogleUtilities/UserDefaults (8.1.0): - - GoogleUtilities/Logger - - GoogleUtilities/Privacy - - image_picker_ios (0.0.1): - - Flutter - - nanopb (3.30910.0): - - nanopb/decode (= 3.30910.0) - - nanopb/encode (= 3.30910.0) - - nanopb/decode (3.30910.0) - - nanopb/encode (3.30910.0) - ObjectBox (5.3.0-beta.4) - objectbox_flutter_libs (0.0.1): - Flutter - ObjectBox (= 5.3.0-beta.4) - - package_info_plus (0.4.5): - - Flutter - permission_handler_apple (9.3.0): - Flutter - - PromisesObjC (2.4.0) - - PromisesSwift (2.4.0): - - PromisesObjC (= 2.4.0) - - share_plus (0.0.1): - - Flutter - - sqflite_darwin (0.0.4): - - Flutter - - FlutterMacOS - - sqlite3 (3.52.0): - - sqlite3/common (= 3.52.0) - - sqlite3/common (3.52.0) - - sqlite3/dbstatvtab (3.52.0): - - sqlite3/common - - sqlite3/fts5 (3.52.0): - - sqlite3/common - - sqlite3/math (3.52.0): - - sqlite3/common - - sqlite3/perf-threadsafe (3.52.0): - - sqlite3/common - - sqlite3/rtree (3.52.0): - - sqlite3/common - - sqlite3/session (3.52.0): - - sqlite3/common - - sqlite3_flutter_libs (0.0.1): - - Flutter - - FlutterMacOS - - sqlite3 (~> 3.52.0) - - sqlite3/dbstatvtab - - sqlite3/fts5 - - sqlite3/math - - sqlite3/perf-threadsafe - - sqlite3/rtree - - sqlite3/session - TensorFlowLiteC (2.12.0): - TensorFlowLiteC/Core (= 2.12.0) - TensorFlowLiteC/Core (2.12.0) @@ -164,135 +28,42 @@ PODS: - TensorFlowLiteSwift (= 2.12.0) - TensorFlowLiteSwift/CoreML (= 2.12.0) - TensorFlowLiteSwift/Metal (= 2.12.0) - - url_launcher_ios (0.0.1): - - Flutter - - video_player_avfoundation (0.0.1): - - Flutter - - FlutterMacOS - - wakelock_plus (0.0.1): - - Flutter - workmanager_apple (0.0.1): - Flutter DEPENDENCIES: - - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) - - firebase_core (from `.symlinks/plugins/firebase_core/ios`) - - firebase_crashlytics (from `.symlinks/plugins/firebase_crashlytics/ios`) - - firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`) - Flutter (from `Flutter`) - - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`) - - gal (from `.symlinks/plugins/gal/darwin`) - - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) - objectbox_flutter_libs (from `.symlinks/plugins/objectbox_flutter_libs/ios`) - - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - - share_plus (from `.symlinks/plugins/share_plus/ios`) - - sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`) - - sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/darwin`) - tflite_flutter (from `.symlinks/plugins/tflite_flutter/ios`) - - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - - video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`) - - wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`) - workmanager_apple (from `.symlinks/plugins/workmanager_apple/ios`) SPEC REPOS: trunk: - - Firebase - - FirebaseCore - - FirebaseCoreExtension - - FirebaseCoreInternal - - FirebaseCrashlytics - - FirebaseInstallations - - FirebaseMessaging - - FirebaseRemoteConfigInterop - - FirebaseSessions - - GoogleDataTransport - - GoogleUtilities - - nanopb - ObjectBox - - PromisesObjC - - PromisesSwift - - sqlite3 - TensorFlowLiteC - TensorFlowLiteSwift EXTERNAL SOURCES: - connectivity_plus: - :path: ".symlinks/plugins/connectivity_plus/ios" - firebase_core: - :path: ".symlinks/plugins/firebase_core/ios" - firebase_crashlytics: - :path: ".symlinks/plugins/firebase_crashlytics/ios" - firebase_messaging: - :path: ".symlinks/plugins/firebase_messaging/ios" Flutter: :path: Flutter - flutter_local_notifications: - :path: ".symlinks/plugins/flutter_local_notifications/ios" - gal: - :path: ".symlinks/plugins/gal/darwin" - image_picker_ios: - :path: ".symlinks/plugins/image_picker_ios/ios" objectbox_flutter_libs: :path: ".symlinks/plugins/objectbox_flutter_libs/ios" - package_info_plus: - :path: ".symlinks/plugins/package_info_plus/ios" permission_handler_apple: :path: ".symlinks/plugins/permission_handler_apple/ios" - share_plus: - :path: ".symlinks/plugins/share_plus/ios" - sqflite_darwin: - :path: ".symlinks/plugins/sqflite_darwin/darwin" - sqlite3_flutter_libs: - :path: ".symlinks/plugins/sqlite3_flutter_libs/darwin" tflite_flutter: :path: ".symlinks/plugins/tflite_flutter/ios" - url_launcher_ios: - :path: ".symlinks/plugins/url_launcher_ios/ios" - video_player_avfoundation: - :path: ".symlinks/plugins/video_player_avfoundation/darwin" - wakelock_plus: - :path: ".symlinks/plugins/wakelock_plus/ios" workmanager_apple: :path: ".symlinks/plugins/workmanager_apple/ios" SPEC CHECKSUMS: - connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd - Firebase: aa154fee4e9b8eac17aa42344988865b3e857d33 - firebase_core: 9156a152117c843440b0b990c785aa0259bc5447 - firebase_crashlytics: e24acd48861c5edf6e0f6c134d6a0b28593c76d7 - firebase_messaging: 0d962ab44ff24ed36deb8fa2ee043c4671858269 - FirebaseCore: 86241206e656f5c80c995e370e6c975913b9b284 - FirebaseCoreExtension: ff6fd42eb5287e71d3e160450de6509733d9ead7 - FirebaseCoreInternal: 7c12fc3011d889085e765e317d7b9fd1cef97af9 - FirebaseCrashlytics: 03f4e20d0c9b7fd6338cb9066f4bfb69d3f42fd0 - FirebaseInstallations: 4e6e162aa4abaaeeeb01dd00179dfc5ad9c2194e - FirebaseMessaging: 341004946fa7ffc741344b20f1b667514fc93e31 - FirebaseRemoteConfigInterop: 23996ab7397494722df4fdd1fd398024389d5da8 - FirebaseSessions: 804bd321f2d2f2ddafe74ef7856062aa19f179c2 Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 - flutter_local_notifications: a5a732f069baa862e728d839dd2ebb904737effb - gal: baecd024ebfd13c441269ca7404792a7152fde89 - GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 - GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1 - image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326 - nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 ObjectBox: eccb95ea2054c39d81dfa2d4ccc5f1e31187228a objectbox_flutter_libs: ed1510f71602e4a0d3f2a721324e468d066fdbb9 - package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d - PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 - PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851 - share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a - sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 - sqlite3: a51c07cf16e023d6c48abd5e5791a61a47354921 - sqlite3_flutter_libs: b3e120efe9a82017e5552a620f696589ed4f62ab TensorFlowLiteC: 20785a69299185a379ba9852b6625f00afd7984a TensorFlowLiteSwift: 3a4928286e9e35bdd3e17970f48e53c80d25e793 tflite_flutter: 64b192e11352fe36943ab6656e1d49207f1a5595 - url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b - video_player_avfoundation: dd410b52df6d2466a42d28550e33e4146928280a - wakelock_plus: e29112ab3ef0b318e58be66b556 workmanager_apple: 904529ae31e97fc5be632cf628507652294a0778 PODFILE CHECKSUM: eacb6976ee55d09dd7976888367ed4eee5f3a1bd diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 36b989c..e8d6560 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -17,6 +17,7 @@ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -68,6 +69,7 @@ AA7C2CE85592DCD41249030A /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; D8BAC14F436233DB60FD42F5 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; E85E7FAF00CB12EDA75D52D5 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -83,6 +85,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, 3681913A96A36188ECBC1602 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -101,6 +104,7 @@ 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */, 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, @@ -192,6 +196,9 @@ productType = "com.apple.product-type.bundle.unit-test"; }; 97C146ED1CF9000F007C117D /* Runner */ = { + packageProductDependencies = ( + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, + ); isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( @@ -220,6 +227,9 @@ /* Begin PBXProject section */ 97C146E61CF9000F007C117D /* Project object */ = { + packageReferences = ( + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */, + ); isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; @@ -820,6 +830,18 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ +/* Begin XCLocalSwiftPackageReference section */ + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; + }; +/* End XCLocalSwiftPackageReference section */ +/* Begin XCSwiftPackageProductDependency section */ + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { + isa = XCSwiftPackageProductDependency; + productName = FlutterGeneratedPluginSwiftPackage; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 97C146E61CF9000F007C117D /* Project object */; } diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..2607354 --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,130 @@ +{ + "pins" : [ + { + "identity" : "abseil-cpp-binary", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/abseil-cpp-binary.git", + "state" : { + "revision" : "bbe8b69694d7873315fd3a4ad41efe043e1c07c5", + "version" : "1.2024072200.0" + } + }, + { + "identity" : "app-check", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/app-check.git", + "state" : { + "revision" : "61b85103a1aeed8218f17c794687781505fbbef5", + "version" : "11.2.0" + } + }, + { + "identity" : "csqlite", + "kind" : "remoteSourceControl", + "location" : "https://github.com/simolus3/CSQLite.git", + "state" : { + "revision" : "1ee46d19a4f451a7aa64ffc64fc99b4748131e62" + } + }, + { + "identity" : "firebase-ios-sdk", + "kind" : "remoteSourceControl", + "location" : "https://github.com/firebase/firebase-ios-sdk", + "state" : { + "revision" : "d10045cace0b4c335c4efa8f7df7e9a9fc5a7c60", + "version" : "12.13.0" + } + }, + { + "identity" : "google-ads-on-device-conversion-ios-sdk", + "kind" : "remoteSourceControl", + "location" : "https://github.com/googleads/google-ads-on-device-conversion-ios-sdk", + "state" : { + "revision" : "19dffda9a9caf8d86570ff846535902d8509d7bf", + "version" : "3.5.0" + } + }, + { + "identity" : "googleappmeasurement", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleAppMeasurement.git", + "state" : { + "revision" : "c2c76bebcfbb90d90ea10599f934f9af160e1604", + "version" : "12.13.0" + } + }, + { + "identity" : "googledatatransport", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleDataTransport.git", + "state" : { + "revision" : "617af071af9aa1d6a091d59a202910ac482128f9", + "version" : "10.1.0" + } + }, + { + "identity" : "googleutilities", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleUtilities.git", + "state" : { + "revision" : "60da361632d0de02786f709bdc0c4df340f7613e", + "version" : "8.1.0" + } + }, + { + "identity" : "grpc-binary", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/grpc-binary.git", + "state" : { + "revision" : "75b31c842f664a0f46a2e590a570e370249fd8f6", + "version" : "1.69.1" + } + }, + { + "identity" : "gtm-session-fetcher", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/gtm-session-fetcher.git", + "state" : { + "revision" : "c0ac7575d70050c2973ba2318bd5af47f8e8153a", + "version" : "5.3.0" + } + }, + { + "identity" : "interop-ios-for-google-sdks", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/interop-ios-for-google-sdks.git", + "state" : { + "revision" : "040d087ac2267d2ddd4cca36c757d1c6a05fdbfe", + "version" : "101.0.0" + } + }, + { + "identity" : "leveldb", + "kind" : "remoteSourceControl", + "location" : "https://github.com/firebase/leveldb.git", + "state" : { + "revision" : "a0bc79961d7be727d258d33d5a6b2f1023270ba1", + "version" : "1.22.5" + } + }, + { + "identity" : "nanopb", + "kind" : "remoteSourceControl", + "location" : "https://github.com/firebase/nanopb.git", + "state" : { + "revision" : "b7e1104502eca3a213b46303391ca4d3bc8ddec1", + "version" : "2.30910.0" + } + }, + { + "identity" : "promises", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/promises.git", + "state" : { + "revision" : "540318ecedd63d883069ae7f1ed811a2df00b6ac", + "version" : "2.4.0" + } + } + ], + "version" : 2 +} diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index e3773d4..c3fedb2 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -5,6 +5,24 @@ + + + + + + + + + + images, {required String heroNamespace}) { @@ -286,209 +284,29 @@ class PostEmbedView extends StatelessWidget { required String heroNamespace, required int quoteDepth, }) { - final record = recordView.record; - final theme = Theme.of(context); - final colorScheme = theme.colorScheme; - - if (record.isEmbedRecordViewRecord) { - final quoted = record.embedRecordViewRecord!; - final quotedRecord = tryParseRecord(quoted.value); - final nestedHeroNamespace = '$heroNamespace/quote:${quoted.uri}'; - final nestedEmbed = quoteDepth >= maxQuoteDepth - ? null - : _buildQuotedEmbeds( - context, - quoted.embeds, - heroNamespace: '$nestedHeroNamespace/embeds', - quoteDepth: quoteDepth + 1, - ); - - return Container( - decoration: BoxDecoration( - border: Border.all(color: colorScheme.outlineVariant), - borderRadius: BorderRadius.circular(12), - color: colorScheme.surfaceContainerLow, - ), - child: InkWell( - borderRadius: BorderRadius.circular(12), - onTap: () { - GoRouter.maybeOf(context)?.push('/post?uri=${Uri.encodeComponent(quoted.uri.toString())}'); - }, - child: Padding( - padding: const EdgeInsets.all(12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ProfileAvatar( - size: 28, - imageUrl: quoted.author.avatar, - fallbackText: quoted.author.displayName ?? quoted.author.handle, - shape: BoxShape.rectangle, - border: Border.all(color: context.colorScheme.outlineVariant), - ), - const SizedBox(width: 8), - Expanded( - child: ActorNameWidget( - displayName: quoted.author.displayName, - handle: quoted.author.handle, - displayNameStyle: theme.textTheme.bodyMedium?.copyWith( - fontWeight: FontWeight.w600, - color: colorScheme.onSurface, - ), - handleStyle: theme.textTheme.bodySmall?.copyWith(color: colorScheme.onSurfaceVariant), - uppercaseHandle: false, - ), - ), - ], - ), - if (quotedRecord != null && quotedRecord.text.isNotEmpty) ...[ - const SizedBox(height: 8), - FacetText( - text: quotedRecord.text, - facets: quotedRecord.facets, - style: feedPostBodyTextStyle(context, compact: compact, nested: true), - maxLines: compact ? 6 : null, - overflow: compact ? TextOverflow.ellipsis : TextOverflow.visible, - ), - ], - if (nestedEmbed != null) ...[const SizedBox(height: 8), nestedEmbed], - if (quoteDepth == maxQuoteDepth && _hasQuotedRecordEmbed(quoted.embeds)) ...[ - const SizedBox(height: 8), - _buildShallowQuote(context, _nestedQuotedAuthor(quoted.embeds) ?? quoted.author), - ], - ], - ), - ), - ), - ); - } - - if (record.isEmbedRecordViewNotFound) { - return _buildUnavailableQuote(context, context.l10n.messageQuotedPostNotFound); - } - if (record.isEmbedRecordViewBlocked) { - return _buildUnavailableQuote(context, context.l10n.messageQuotedPostBlocked); - } - if (record.isEmbedRecordViewDetached) { - return _buildUnavailableQuote(context, context.l10n.messageQuotedPostUnavailable); - } - - return const SizedBox.shrink(); - } - - Widget _buildShallowQuote(BuildContext context, ProfileViewBasic author) { - final colorScheme = context.colorScheme; - return Container( - width: double.infinity, - padding: EdgeInsets.all(compact ? 8 : 10), - decoration: BoxDecoration( - border: Border.all(color: colorScheme.outlineVariant), - borderRadius: BorderRadius.circular(10), - color: colorScheme.surfaceContainerLow, - ), - child: Row( - children: [ - ProfileAvatar( - size: compact ? 20 : 24, - imageUrl: author.avatar, - fallbackText: author.displayName ?? author.handle, - shape: BoxShape.rectangle, - border: Border.all(color: colorScheme.outlineVariant), - ), - SizedBox(width: compact ? 6 : 8), - Expanded( - child: Text( - '${author.displayName ?? author.handle} @${author.handle} ...', - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: context.textTheme.bodySmall?.copyWith(color: colorScheme.onSurfaceVariant), - ), - ), - ], - ), + return PostRecordEmbed( + recordView: recordView, + heroNamespace: heroNamespace, + quoteDepth: quoteDepth, + maxQuoteDepth: maxQuoteDepth, + compact: compact, + buildImagesEmbed: _buildImagesEmbed, + buildExternalEmbed: _buildExternalEmbed, + buildVideoEmbed: _buildVideoEmbed, + buildUnknownEmbed: _buildUnknownEmbed, ); } - Widget _buildUnavailableQuote(BuildContext context, String label) { - return Container( - width: double.infinity, - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: context.colorScheme.surfaceContainerLow, - borderRadius: BorderRadius.circular(12), - ), - child: Text(label, style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceVariant)), + Widget _buildUnknownEmbed(BuildContext context) { + return PostRecordResourceCard( + icon: Icons.extension_outlined, + fallbackText: context.l10n.labelUnknown, + label: context.l10n.labelUnknown, + title: context.l10n.labelUnknown, + compact: compact, ); } - Widget? _buildQuotedEmbeds( - BuildContext context, - List? embeds, { - required String heroNamespace, - required int quoteDepth, - }) { - if (embeds == null || embeds.isEmpty) return null; - - final embed = embeds.first; - - if (embed.isEmbedImagesView) { - return _buildImagesEmbed(context, embed.embedImagesView!.images, heroNamespace: '$heroNamespace/images'); - } - if (embed.isEmbedExternalView) { - return _buildExternalEmbed(context, embed.embedExternalView!.external); - } - if (embed.isEmbedVideoView) { - return _buildVideoEmbed(context, embed.embedVideoView!); - } - if (embed.isEmbedRecordView) { - return _buildQuotedRecord( - context, - embed.embedRecordView!, - heroNamespace: '$heroNamespace/record', - quoteDepth: quoteDepth, - ); - } - if (embed.isEmbedRecordWithMediaView) { - final recordWithMedia = embed.embedRecordWithMediaView!; - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildRecordWithMediaMedia(context, recordWithMedia.media, heroNamespace: '$heroNamespace/rwm-media'), - const SizedBox(height: 8), - _buildQuotedRecord( - context, - recordWithMedia.record, - heroNamespace: '$heroNamespace/rwm-record', - quoteDepth: quoteDepth, - ), - ], - ); - } - - return null; - } - - bool _hasQuotedRecordEmbed(List? embeds) { - if (embeds == null || embeds.isEmpty) return false; - final embed = embeds.first; - return embed.isEmbedRecordView || embed.isEmbedRecordWithMediaView; - } - - ProfileViewBasic? _nestedQuotedAuthor(List? embeds) { - if (embeds == null || embeds.isEmpty) return null; - final embed = embeds.first; - final recordView = embed.isEmbedRecordView - ? embed.embedRecordView - : embed.isEmbedRecordWithMediaView - ? embed.embedRecordWithMediaView?.record - : null; - final record = recordView?.record; - return record?.isEmbedRecordViewRecord == true ? record!.embedRecordViewRecord!.author : null; - } - void _openImageViewer( BuildContext context, List images, { diff --git a/lib/features/feed/presentation/widgets/post_record_embed.dart b/lib/features/feed/presentation/widgets/post_record_embed.dart new file mode 100644 index 0000000..01ccd9d --- /dev/null +++ b/lib/features/feed/presentation/widgets/post_record_embed.dart @@ -0,0 +1,488 @@ +import 'package:bluesky_poptart/app/bsky/actor/defs.dart'; +import 'package:bluesky_poptart/app/bsky/embed/external.dart'; +import 'package:bluesky_poptart/app/bsky/embed/images.dart'; +import 'package:bluesky_poptart/app/bsky/embed/record.dart'; +import 'package:bluesky_poptart/app/bsky/embed/record_with_media.dart'; +import 'package:bluesky_poptart/app/bsky/embed/video.dart'; +import 'package:bluesky_poptart/app/bsky/feed/defs.dart'; +import 'package:bluesky_poptart/app/bsky/graph/defs.dart' as bsky_graph; +import 'package:bluesky_poptart/app/bsky/labeler/defs.dart' as bsky_labeler; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:lazurite/core/l10n/l10n.dart'; +import 'package:lazurite/core/theme/theme_extensions.dart'; +import 'package:lazurite/features/feed/presentation/widgets/facet_text.dart'; +import 'package:lazurite/features/feed/presentation/widgets/post_text_styles.dart'; +import 'package:lazurite/shared/presentation/widgets/actor_name_widget.dart'; +import 'package:lazurite/shared/presentation/widgets/profile_avatar.dart'; +import 'package:lazurite/shared/utils/format_utils.dart'; +import 'package:lazurite/shared/utils/parse_utils.dart'; + +typedef PostImagesEmbedBuilder = + Widget Function(BuildContext context, List images, {required String heroNamespace}); +typedef PostExternalEmbedBuilder = Widget Function(BuildContext context, EmbedExternalViewExternal external); +typedef PostVideoEmbedBuilder = Widget Function(BuildContext context, EmbedVideoView video); +typedef PostUnknownEmbedBuilder = Widget Function(BuildContext context); + +enum PostRecordEmbedKind { post, notFound, blocked, detached, generator, list, labeler, starterPack, unknown } + +extension PostRecordEmbedUnionKind on UEmbedRecordViewRecord { + PostRecordEmbedKind get postRecordEmbedKind { + if (isEmbedRecordViewRecord) return PostRecordEmbedKind.post; + if (isEmbedRecordViewNotFound) return PostRecordEmbedKind.notFound; + if (isEmbedRecordViewBlocked) return PostRecordEmbedKind.blocked; + if (isEmbedRecordViewDetached) return PostRecordEmbedKind.detached; + if (isGeneratorView) return PostRecordEmbedKind.generator; + if (isListView) return PostRecordEmbedKind.list; + if (isLabelerView) return PostRecordEmbedKind.labeler; + if (isStarterPackViewBasic) return PostRecordEmbedKind.starterPack; + return PostRecordEmbedKind.unknown; + } +} + +class PostRecordEmbed extends StatelessWidget { + const PostRecordEmbed({ + super.key, + required this.recordView, + required this.heroNamespace, + required this.quoteDepth, + required this.maxQuoteDepth, + required this.compact, + required this.buildImagesEmbed, + required this.buildExternalEmbed, + required this.buildVideoEmbed, + required this.buildUnknownEmbed, + }); + + final EmbedRecordView recordView; + final String heroNamespace; + final int quoteDepth; + final int maxQuoteDepth; + final bool compact; + final PostImagesEmbedBuilder buildImagesEmbed; + final PostExternalEmbedBuilder buildExternalEmbed; + final PostVideoEmbedBuilder buildVideoEmbed; + final PostUnknownEmbedBuilder buildUnknownEmbed; + + @override + Widget build(BuildContext context) { + final record = recordView.record; + + return switch (record.postRecordEmbedKind) { + PostRecordEmbedKind.post => _buildQuotedPost(context, record.embedRecordViewRecord!), + PostRecordEmbedKind.notFound => _buildUnavailableQuote(context, context.l10n.messageQuotedPostNotFound), + PostRecordEmbedKind.blocked => _buildUnavailableQuote(context, context.l10n.messageQuotedPostBlocked), + PostRecordEmbedKind.detached => _buildUnavailableQuote(context, context.l10n.messageQuotedPostUnavailable), + PostRecordEmbedKind.generator => _buildGeneratorRecord(context, record.generatorView!), + PostRecordEmbedKind.list => _buildListRecord(context, record.listView!), + PostRecordEmbedKind.labeler => _buildLabelerRecord(context, record.labelerView!), + PostRecordEmbedKind.starterPack => _buildStarterPackRecord(context, record.starterPackViewBasic!), + PostRecordEmbedKind.unknown => _buildUnknownRecord(context), + }; + } + + Widget _buildQuotedPost(BuildContext context, EmbedRecordViewRecord quoted) { + final quotedRecord = tryParseRecord(quoted.value); + final nestedHeroNamespace = '$heroNamespace/quote:${quoted.uri}'; + final nestedEmbed = quoteDepth >= maxQuoteDepth + ? null + : _buildQuotedEmbeds( + context, + quoted.embeds, + heroNamespace: '$nestedHeroNamespace/embeds', + quoteDepth: quoteDepth + 1, + ); + + return Container( + decoration: BoxDecoration( + border: Border.all(color: context.colorScheme.outlineVariant), + borderRadius: BorderRadius.circular(12), + color: context.colorScheme.surfaceContainerLow, + ), + child: InkWell( + borderRadius: BorderRadius.circular(12), + onTap: () { + GoRouter.maybeOf(context)?.push('/post?uri=${Uri.encodeComponent(quoted.uri.toString())}'); + }, + child: Padding( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ProfileAvatar( + size: 28, + imageUrl: quoted.author.avatar, + fallbackText: quoted.author.displayName ?? quoted.author.handle, + shape: BoxShape.rectangle, + border: Border.all(color: context.colorScheme.outlineVariant), + ), + const SizedBox(width: 8), + Expanded( + child: ActorNameWidget( + displayName: quoted.author.displayName, + handle: quoted.author.handle, + displayNameStyle: context.textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w600, + color: context.colorScheme.onSurface, + ), + handleStyle: context.textTheme.bodySmall?.copyWith(color: context.colorScheme.onSurfaceVariant), + uppercaseHandle: false, + ), + ), + ], + ), + if (quotedRecord != null && quotedRecord.text.isNotEmpty) ...[ + const SizedBox(height: 8), + FacetText( + text: quotedRecord.text, + facets: quotedRecord.facets, + style: feedPostBodyTextStyle(context, compact: compact, nested: true), + maxLines: compact ? 6 : null, + overflow: compact ? TextOverflow.ellipsis : TextOverflow.visible, + ), + ], + if (nestedEmbed != null) ...[const SizedBox(height: 8), nestedEmbed], + if (quoteDepth == maxQuoteDepth && _hasQuotedRecordEmbed(quoted.embeds)) ...[ + const SizedBox(height: 8), + _buildShallowQuote(context, _nestedQuotedAuthor(quoted.embeds) ?? quoted.author), + ], + ], + ), + ), + ), + ); + } + + Widget _buildGeneratorRecord(BuildContext context, GeneratorView generator) => PostRecordResourceCard( + icon: Icons.dynamic_feed_outlined, + avatarUrl: generator.avatar, + fallbackText: generator.displayName, + label: context.l10n.labelFeed, + title: generator.displayName, + subtitle: _byline(generator.creator.displayName, generator.creator.handle), + description: generator.description, + metadata: generator.likeCount == null ? null : '${formatCount(generator.likeCount!)} likes', + compact: compact, + onTap: () => GoRouter.maybeOf(context)?.push('/feed?uri=${Uri.encodeComponent(generator.uri.toString())}'), + ); + + Widget _buildListRecord(BuildContext context, bsky_graph.ListView list) { + final isMod = list.purpose.knownValue == bsky_graph.KnownListPurpose.appBskyGraphDefsModlist; + return PostRecordResourceCard( + icon: isMod ? Icons.shield_outlined : Icons.list_alt_outlined, + avatarUrl: list.avatar, + fallbackText: list.name, + label: isMod ? context.l10n.labelModerationShort : context.l10n.labelList, + title: list.name, + subtitle: _byline(list.creator.displayName, list.creator.handle), + description: list.description, + metadata: list.listItemCount == null ? null : '${formatCount(list.listItemCount!)} members', + compact: compact, + onTap: () => GoRouter.maybeOf(context)?.push('/list?uri=${Uri.encodeComponent(list.uri.toString())}'), + ); + } + + Widget _buildLabelerRecord(BuildContext context, bsky_labeler.LabelerView labeler) => PostRecordResourceCard( + icon: Icons.verified_user_outlined, + avatarUrl: labeler.creator.avatar, + fallbackText: labeler.creator.displayName ?? labeler.creator.handle, + label: context.l10n.labelLabeler, + title: labeler.creator.displayName ?? labeler.creator.handle, + subtitle: '@${labeler.creator.handle}', + metadata: labeler.likeCount == null ? null : '${formatCount(labeler.likeCount!)} likes', + compact: compact, + onTap: () => + GoRouter.maybeOf(context)?.push('/settings/moderation/detail?did=${Uri.encodeComponent(labeler.creator.did)}'), + ); + + Widget _buildStarterPackRecord(BuildContext context, bsky_graph.StarterPackViewBasic pack) { + final name = (pack.record['name'] as String?)?.trim(); + final description = (pack.record['description'] as String?)?.trim(); + final title = name == null || name.isEmpty ? context.l10n.labelStarterPack : name; + return PostRecordResourceCard( + icon: Icons.group_add_outlined, + fallbackText: title, + label: context.l10n.labelStarterPack, + title: title, + subtitle: _byline(pack.creator.displayName, pack.creator.handle), + description: description, + metadata: pack.listItemCount == null ? null : '${formatCount(pack.listItemCount!)} members', + compact: compact, + onTap: () => GoRouter.maybeOf(context)?.push('/starter-pack?uri=${Uri.encodeComponent(pack.uri.toString())}'), + ); + } + + Widget _buildUnknownRecord(BuildContext context) => PostRecordResourceCard( + icon: Icons.article_outlined, + fallbackText: context.l10n.labelUnknown, + label: context.l10n.labelUnknown, + title: context.l10n.messageQuotedPostUnavailable, + compact: compact, + ); + + Widget _buildShallowQuote(BuildContext context, ProfileViewBasic author) => Container( + width: double.infinity, + padding: EdgeInsets.all(compact ? 8 : 10), + decoration: BoxDecoration( + border: Border.all(color: context.colorScheme.outlineVariant), + borderRadius: BorderRadius.circular(10), + color: context.colorScheme.surfaceContainerLow, + ), + child: Row( + children: [ + ProfileAvatar( + size: compact ? 20 : 24, + imageUrl: author.avatar, + fallbackText: author.displayName ?? author.handle, + shape: BoxShape.rectangle, + border: Border.all(color: context.colorScheme.outlineVariant), + ), + SizedBox(width: compact ? 6 : 8), + Expanded( + child: Text( + '${author.displayName ?? author.handle} @${author.handle} ...', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: context.textTheme.bodySmall?.copyWith(color: context.colorScheme.onSurfaceVariant), + ), + ), + ], + ), + ); + + Widget _buildUnavailableQuote(BuildContext context, String label) => Container( + width: double.infinity, + padding: const EdgeInsets.all(12), + decoration: BoxDecoration(color: context.colorScheme.surfaceContainerLow, borderRadius: BorderRadius.circular(12)), + child: Text(label, style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceVariant)), + ); + + Widget? _buildQuotedEmbeds( + BuildContext context, + List? embeds, { + required String heroNamespace, + required int quoteDepth, + }) { + if (embeds == null || embeds.isEmpty) return null; + + final embed = embeds.first; + + if (embed.isEmbedImagesView) { + return buildImagesEmbed(context, embed.embedImagesView!.images, heroNamespace: '$heroNamespace/images'); + } + if (embed.isEmbedExternalView) { + return buildExternalEmbed(context, embed.embedExternalView!.external); + } + if (embed.isEmbedVideoView) { + return buildVideoEmbed(context, embed.embedVideoView!); + } + if (embed.isEmbedRecordView) { + return PostRecordEmbed( + recordView: embed.embedRecordView!, + heroNamespace: '$heroNamespace/record', + quoteDepth: quoteDepth, + maxQuoteDepth: maxQuoteDepth, + compact: compact, + buildImagesEmbed: buildImagesEmbed, + buildExternalEmbed: buildExternalEmbed, + buildVideoEmbed: buildVideoEmbed, + buildUnknownEmbed: buildUnknownEmbed, + ); + } + if (embed.isEmbedRecordWithMediaView) { + final recordWithMedia = embed.embedRecordWithMediaView!; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildRecordWithMediaMedia(context, recordWithMedia.media, heroNamespace: '$heroNamespace/rwm-media'), + const SizedBox(height: 8), + PostRecordEmbed( + recordView: recordWithMedia.record, + heroNamespace: '$heroNamespace/rwm-record', + quoteDepth: quoteDepth, + maxQuoteDepth: maxQuoteDepth, + compact: compact, + buildImagesEmbed: buildImagesEmbed, + buildExternalEmbed: buildExternalEmbed, + buildVideoEmbed: buildVideoEmbed, + buildUnknownEmbed: buildUnknownEmbed, + ), + ], + ); + } + if (embed.isUnknown) { + return buildUnknownEmbed(context); + } + + return null; + } + + Widget _buildRecordWithMediaMedia( + BuildContext context, + UEmbedRecordWithMediaViewMedia media, { + required String heroNamespace, + }) { + if (media.isEmbedImagesView) { + return buildImagesEmbed(context, media.embedImagesView!.images, heroNamespace: '$heroNamespace/images'); + } + if (media.isEmbedExternalView) { + return buildExternalEmbed(context, media.embedExternalView!.external); + } + if (media.isEmbedVideoView) { + return buildVideoEmbed(context, media.embedVideoView!); + } + return buildUnknownEmbed(context); + } + + bool _hasQuotedRecordEmbed(List? embeds) { + if (embeds == null || embeds.isEmpty) return false; + final embed = embeds.first; + return embed.isEmbedRecordView || embed.isEmbedRecordWithMediaView; + } + + ProfileViewBasic? _nestedQuotedAuthor(List? embeds) { + if (embeds == null || embeds.isEmpty) return null; + final embed = embeds.first; + final recordView = embed.isEmbedRecordView + ? embed.embedRecordView + : embed.isEmbedRecordWithMediaView + ? embed.embedRecordWithMediaView?.record + : null; + final record = recordView?.record; + return record?.isEmbedRecordViewRecord == true ? record!.embedRecordViewRecord!.author : null; + } + + String _byline(String? displayName, String handle) { + final name = displayName?.trim(); + return name == null || name.isEmpty ? '@$handle' : 'by $name @$handle'; + } +} + +class PostRecordResourceCard extends StatelessWidget { + const PostRecordResourceCard({ + super.key, + required this.icon, + required this.fallbackText, + required this.label, + required this.title, + required this.compact, + this.avatarUrl, + this.subtitle, + this.description, + this.metadata, + this.onTap, + }); + + final IconData icon; + final String? avatarUrl; + final String fallbackText; + final String label; + final String title; + final String? subtitle; + final String? description; + final String? metadata; + final bool compact; + final VoidCallback? onTap; + + @override + Widget build(BuildContext context) { + final descriptionText = description?.trim(); + final metadataText = metadata?.trim(); + final subtitleText = subtitle?.trim(); + + return Container( + width: double.infinity, + decoration: BoxDecoration( + border: Border.all(color: context.colorScheme.outlineVariant), + borderRadius: BorderRadius.circular(12), + color: context.colorScheme.surfaceContainerLow, + ), + child: InkWell( + borderRadius: BorderRadius.circular(12), + onTap: onTap, + child: Padding( + padding: EdgeInsets.all(compact ? 10 : 12), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ProfileAvatar( + size: compact ? 32 : 40, + imageUrl: avatarUrl, + fallbackText: fallbackText, + fallbackBuilder: (_) => + Icon(icon, color: context.colorScheme.onSurfaceVariant, size: compact ? 18 : 22), + shape: BoxShape.rectangle, + border: Border.all(color: context.colorScheme.outlineVariant), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label.toUpperCase(), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: context.textTheme.labelSmall?.copyWith( + color: context.colorScheme.primary, + fontWeight: FontWeight.w800, + letterSpacing: 0.6, + ), + ), + const SizedBox(height: 3), + Text( + title, + maxLines: compact ? 1 : 2, + overflow: TextOverflow.ellipsis, + style: context.textTheme.titleSmall?.copyWith( + color: context.colorScheme.onSurface, + fontWeight: FontWeight.w700, + ), + ), + if (subtitleText != null && subtitleText.isNotEmpty) ...[ + const SizedBox(height: 2), + Text( + subtitleText, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: context.textTheme.bodySmall?.copyWith(color: context.colorScheme.onSurfaceVariant), + ), + ], + if (!compact && descriptionText != null && descriptionText.isNotEmpty) ...[ + const SizedBox(height: 8), + Text( + descriptionText, + maxLines: 3, + overflow: TextOverflow.ellipsis, + style: context.textTheme.bodySmall?.copyWith(color: context.colorScheme.onSurface), + ), + ], + if (metadataText != null && metadataText.isNotEmpty) ...[ + const SizedBox(height: 8), + Text( + metadataText, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: context.textTheme.labelSmall?.copyWith( + color: context.colorScheme.onSurfaceVariant, + fontWeight: FontWeight.w700, + ), + ), + ], + ], + ), + ), + if (onTap != null) ...[ + const SizedBox(width: 6), + Icon(Icons.chevron_right, size: 20, color: context.colorScheme.onSurfaceVariant), + ], + ], + ), + ), + ), + ); + } +} diff --git a/lib/features/starter_packs/presentation/widgets/starter_pack_card.dart b/lib/features/starter_packs/presentation/widgets/starter_pack_card.dart index 446bcc8..63bcdac 100644 --- a/lib/features/starter_packs/presentation/widgets/starter_pack_card.dart +++ b/lib/features/starter_packs/presentation/widgets/starter_pack_card.dart @@ -13,9 +13,6 @@ class StarterPackCard extends StatelessWidget { @override Widget build(BuildContext context) { - final colorScheme = context.colorScheme; - final textTheme = context.textTheme; - final name = (pack.record['name'] as String?) ?? context.l10n.labelStarterPack; final memberCount = pack.listItemCount; final joinedWeek = pack.joinedWeekCount; @@ -36,8 +33,9 @@ class StarterPackCard extends StatelessWidget { ProfileAvatar( size: 40, fallbackText: name, - backgroundColor: colorScheme.primaryContainer, - fallbackBuilder: (_) => Icon(Icons.group_outlined, color: colorScheme.onPrimaryContainer, size: 20), + backgroundColor: context.colorScheme.primaryContainer, + fallbackBuilder: (_) => + Icon(Icons.group_outlined, color: context.colorScheme.onPrimaryContainer, size: 20), ), const SizedBox(width: 12), Expanded( @@ -46,13 +44,13 @@ class StarterPackCard extends StatelessWidget { children: [ Text( name, - style: textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600), + style: context.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600), maxLines: 1, overflow: TextOverflow.ellipsis, ), Text( context.l10n.formatListByHandle(pack.creator.handle), - style: textTheme.bodySmall?.copyWith(color: colorScheme.onSurfaceVariant), + style: context.textTheme.bodySmall?.copyWith(color: context.colorScheme.onSurfaceVariant), ), ], ), @@ -76,13 +74,11 @@ class StarterPackCard extends StatelessWidget { ); } - Widget _buildStat(BuildContext context, int count, String label) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(formatCount(count), style: context.textTheme.labelLarge?.copyWith(fontWeight: FontWeight.w700)), - Text(label, style: context.textTheme.labelSmall?.copyWith(color: context.colorScheme.onSurfaceVariant)), - ], - ); - } + Widget _buildStat(BuildContext context, int count, String label) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(formatCount(count), style: context.textTheme.labelLarge?.copyWith(fontWeight: FontWeight.w700)), + Text(label, style: context.textTheme.labelSmall?.copyWith(color: context.colorScheme.onSurfaceVariant)), + ], + ); } diff --git a/test/features/feed/presentation/post_card_test.dart b/test/features/feed/presentation/post_card_test.dart index ecad0a0..d07d70e 100644 --- a/test/features/feed/presentation/post_card_test.dart +++ b/test/features/feed/presentation/post_card_test.dart @@ -7,6 +7,8 @@ import 'package:bluesky_poptart/app/bsky/embed/record.dart'; import 'package:bluesky_poptart/app/bsky/embed/record_with_media.dart'; import 'package:bluesky_poptart/app/bsky/feed/defs.dart'; import 'package:bluesky_poptart/app/bsky/feed/post.dart'; +import 'package:bluesky_poptart/app/bsky/graph/defs.dart' as bsky_graph; +import 'package:bluesky_poptart/app/bsky/labeler/defs.dart' as bsky_labeler; import 'package:bluesky_poptart/app/bsky/richtext/facet.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -625,4 +627,281 @@ void main() { expect(heroTags.length, greaterThanOrEqualTo(2)); expect(heroTags.toSet().length, heroTags.length); }); + + testWidgets('renders starter pack record embeds and navigates to detail', (tester) async { + final packUri = AtUri.parse('at://did:plc:creator/app.bsky.graph.starterpack/pack'); + final post = _makePostWithRecordEmbed( + UEmbedRecordViewRecord.starterPackViewBasic( + data: bsky_graph.StarterPackViewBasic( + uri: packUri, + cid: 'cid-pack', + record: const {'name': 'Starter Pack Picks', 'description': 'People worth following'}, + creator: const ProfileViewBasic( + did: 'did:plc:creator', + handle: 'creator.bsky.social', + displayName: 'Creator', + ), + listItemCount: 12, + indexedAt: DateTime.utc(2026, 3, 15), + ), + ), + ); + String? pushedRoute; + + final router = GoRouter( + routes: [ + GoRoute( + path: '/', + builder: (context, state) => Scaffold(body: PostCard(feedViewPost: post)), + ), + GoRoute( + path: '/starter-pack', + builder: (context, state) { + pushedRoute = state.uri.toString(); + return const Scaffold(body: Text('starter pack detail')); + }, + ), + ], + ); + + await tester.pumpWidget(MaterialApp.router(routerConfig: router)); + await tester.pumpAndSettle(); + + expect(find.text('STARTER PACK'), findsOneWidget); + expect(find.text('Starter Pack Picks'), findsOneWidget); + expect(find.text('People worth following'), findsOneWidget); + expect(find.text('12 members'), findsOneWidget); + + await tester.tap(find.text('Starter Pack Picks')); + await tester.pumpAndSettle(); + + expect(pushedRoute, isNotNull); + expect(Uri.parse(pushedRoute!).path, '/starter-pack'); + expect(Uri.decodeComponent(Uri.parse(pushedRoute!).queryParameters['uri']!), packUri.toString()); + }); + + testWidgets('renders feed, list, labeler, and unknown record embeds', (tester) async { + const creator = ProfileView(did: 'did:plc:creator', handle: 'creator.bsky.social', displayName: 'Creator'); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: SingleChildScrollView( + child: Column( + children: [ + PostCard( + feedViewPost: _makePostWithRecordEmbed( + UEmbedRecordViewRecord.generatorView( + data: GeneratorView( + uri: AtUri.parse('at://did:plc:creator/app.bsky.feed.generator/feed'), + cid: 'cid-feed', + did: 'did:web:feed.example', + creator: creator, + displayName: 'News Feed', + description: 'Fresh posts', + likeCount: 1200, + indexedAt: DateTime.utc(2026, 3, 15), + ), + ), + rkey: 'feed', + ), + ), + PostCard( + feedViewPost: _makePostWithRecordEmbed( + UEmbedRecordViewRecord.listView( + data: bsky_graph.ListView( + uri: AtUri.parse('at://did:plc:creator/app.bsky.graph.list/list'), + cid: 'cid-list', + creator: creator, + name: 'Good Accounts', + purpose: const bsky_graph.ListPurpose.knownValue( + data: bsky_graph.KnownListPurpose.appBskyGraphDefsCuratelist, + ), + description: 'Useful people', + listItemCount: 42, + indexedAt: DateTime.utc(2026, 3, 15), + ), + ), + rkey: 'list', + ), + ), + PostCard( + feedViewPost: _makePostWithRecordEmbed( + UEmbedRecordViewRecord.labelerView( + data: bsky_labeler.LabelerView( + uri: AtUri.parse('at://did:plc:labeler/app.bsky.labeler.service/self'), + cid: 'cid-labeler', + creator: const ProfileView( + did: 'did:plc:labeler', + handle: 'labeler.bsky.social', + displayName: 'Labeler Service', + ), + likeCount: 7, + indexedAt: DateTime.utc(2026, 3, 15), + ), + ), + rkey: 'labeler', + ), + ), + PostCard( + feedViewPost: _makePostWithRecordEmbed( + const UEmbedRecordViewRecord.unknown(data: {r'$type': 'app.example.record'}), + rkey: 'unknown', + ), + ), + ], + ), + ), + ), + ), + ); + + expect(find.text('FEED'), findsOneWidget); + expect(find.text('News Feed'), findsOneWidget); + expect(find.text('Fresh posts'), findsOneWidget); + expect(find.text('1.2K likes'), findsOneWidget); + expect(find.text('LIST'), findsOneWidget); + expect(find.text('Good Accounts'), findsOneWidget); + expect(find.text('42 members'), findsOneWidget); + expect(find.text('LABELER'), findsOneWidget); + expect(find.text('Labeler Service'), findsOneWidget); + expect(find.text('7 likes'), findsOneWidget); + expect(find.text('UNKNOWN'), findsOneWidget); + expect(find.text('Quoted post is unavailable'), findsOneWidget); + }); + + testWidgets('feed, list, and labeler record embeds navigate through internal routes', (tester) async { + const creator = ProfileView(did: 'did:plc:creator', handle: 'creator.bsky.social', displayName: 'Creator'); + final feedUri = AtUri.parse('at://did:plc:creator/app.bsky.feed.generator/feed'); + final listUri = AtUri.parse('at://did:plc:creator/app.bsky.graph.list/list'); + String? pushedRoute; + + await _pumpRecordEmbedRouter( + tester, + post: _makePostWithRecordEmbed( + UEmbedRecordViewRecord.generatorView( + data: GeneratorView( + uri: feedUri, + cid: 'cid-feed', + did: 'did:web:feed.example', + creator: creator, + displayName: 'News Feed', + indexedAt: DateTime.utc(2026, 3, 15), + ), + ), + ), + routePath: '/feed', + onRoute: (uri) => pushedRoute = uri.toString(), + ); + + await tester.tap(find.text('News Feed')); + await tester.pumpAndSettle(); + + expect(Uri.parse(pushedRoute!).path, '/feed'); + expect(Uri.decodeComponent(Uri.parse(pushedRoute!).queryParameters['uri']!), feedUri.toString()); + + pushedRoute = null; + await _pumpRecordEmbedRouter( + tester, + post: _makePostWithRecordEmbed( + UEmbedRecordViewRecord.listView( + data: bsky_graph.ListView( + uri: listUri, + cid: 'cid-list', + creator: creator, + name: 'Good Accounts', + purpose: const bsky_graph.ListPurpose.knownValue( + data: bsky_graph.KnownListPurpose.appBskyGraphDefsCuratelist, + ), + indexedAt: DateTime.utc(2026, 3, 15), + ), + ), + ), + routePath: '/list', + onRoute: (uri) => pushedRoute = uri.toString(), + ); + + await tester.tap(find.text('Good Accounts')); + await tester.pumpAndSettle(); + + expect(Uri.parse(pushedRoute!).path, '/list'); + expect(Uri.decodeComponent(Uri.parse(pushedRoute!).queryParameters['uri']!), listUri.toString()); + + pushedRoute = null; + await _pumpRecordEmbedRouter( + tester, + post: _makePostWithRecordEmbed( + UEmbedRecordViewRecord.labelerView( + data: bsky_labeler.LabelerView( + uri: AtUri.parse('at://did:plc:labeler/app.bsky.labeler.service/self'), + cid: 'cid-labeler', + creator: const ProfileView( + did: 'did:plc:labeler', + handle: 'labeler.bsky.social', + displayName: 'Labeler Service', + ), + indexedAt: DateTime.utc(2026, 3, 15), + ), + ), + ), + routePath: '/settings/moderation/detail', + onRoute: (uri) => pushedRoute = uri.toString(), + ); + + await tester.tap(find.text('Labeler Service')); + await tester.pumpAndSettle(); + + expect(Uri.parse(pushedRoute!).path, '/settings/moderation/detail'); + expect(Uri.parse(pushedRoute!).queryParameters['did'], 'did:plc:labeler'); + }); + + testWidgets('renders unknown top-level embed fallback', (tester) async { + final post = _makePostWithEmbed(const UPostViewEmbed.unknown(data: {r'$type': 'com.example.embed'})); + + await tester.pumpWidget(buildSubject(post)); + + expect(find.text('UNKNOWN'), findsOneWidget); + }); +} + +FeedViewPost _makePostWithRecordEmbed(UEmbedRecordViewRecord record, {String rkey = 'record'}) => _makePostWithEmbed( + UPostViewEmbed.embedRecordView(data: EmbedRecordView(record: record)), + rkey: rkey, +); + +FeedViewPost _makePostWithEmbed(UPostViewEmbed embed, {String rkey = 'record'}) => FeedViewPost( + post: PostView( + uri: AtUri.parse('at://did:plc:test/app.bsky.feed.post/$rkey'), + cid: 'cid-$rkey', + author: const ProfileViewBasic(did: 'did:plc:test', handle: 'test.bsky.social'), + record: FeedPostRecord(text: 'Main post', createdAt: DateTime.utc(2026, 3, 16)).toJson(), + indexedAt: DateTime.utc(2026, 3, 16), + embed: embed, + ), +); + +Future _pumpRecordEmbedRouter( + WidgetTester tester, { + required FeedViewPost post, + required String routePath, + required ValueChanged onRoute, +}) async { + final router = GoRouter( + routes: [ + GoRoute( + path: '/', + builder: (context, state) => Scaffold(body: PostCard(feedViewPost: post)), + ), + GoRoute( + path: routePath, + builder: (context, state) { + onRoute(state.uri); + return const Scaffold(body: Text('destination')); + }, + ), + ], + ); + + await tester.pumpWidget(MaterialApp.router(routerConfig: router)); + await tester.pumpAndSettle(); }