[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: push notifications

+939 -38
+3
.github/workflows/android-internal-release.yml
··· 43 43 flutter pub get --enforce-lockfile 44 44 dart run build_runner build --delete-conflicting-outputs 45 45 46 + - name: Setup Firebase config 47 + run: echo "${{ secrets.GOOGLE_SERVICES_JSON_BASE64 }}" | base64 -d > android/app/google-services.json 48 + 46 49 - name: Setup keys 47 50 uses: timheuer/base64-to-file@v1 48 51 with:
+4
.gitignore
··· 48 48 # Environment variables 49 49 .env 50 50 51 + # Firebase config files (stored as secrets in CI) 52 + android/app/google-services.json 53 + ios/Runner/GoogleService-Info.plist 54 + 51 55 .vscode/ 52 56 53 57 # Generated files
+3
android/app/build.gradle.kts
··· 4 4 5 5 plugins { 6 6 id("com.android.application") 7 + // START: FlutterFire Configuration 8 + id("com.google.gms.google-services") 9 + // END: FlutterFire Configuration 7 10 id("kotlin-android") 8 11 id("org.jetbrains.kotlin.plugin.compose") 9 12 id("dev.flutter.flutter-gradle-plugin")
+3
android/settings.gradle.kts
··· 19 19 plugins { 20 20 id("dev.flutter.flutter-plugin-loader") version "1.0.0" 21 21 id("com.android.application") version "8.13.2" apply false 22 + // START: FlutterFire Configuration 23 + id("com.google.gms.google-services") version("4.3.15") apply false 24 + // END: FlutterFire Configuration 22 25 id("org.jetbrains.kotlin.android") version "2.2.21" apply false 23 26 id("org.jetbrains.kotlin.plugin.compose") version "2.2.21" apply false 24 27 id("com.google.devtools.ksp") version "2.2.21-2.0.4" apply false
+15 -9
ios/Runner.xcodeproj/project.pbxproj
··· 13 13 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 14 14 59C723F32EA9C6BF002F18BF /* AppIcon.icon in Resources */ = {isa = PBXBuildFile; fileRef = 59C723F22EA9C6BF002F18BF /* AppIcon.icon */; }; 15 15 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 16 + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; 16 17 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 17 18 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 18 19 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; 20 + 9B6EB3EAD1043C920A48987A /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 622B2DC1E9685418514D2FC4 /* GoogleService-Info.plist */; }; 19 21 C788453B52B48F95B80D3F19 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4E62B4B048ACF350C4CFD2DF /* Pods_RunnerTests.framework */; }; 20 - 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; 21 22 /* End PBXBuildFile section */ 22 23 23 24 /* Begin PBXContainerItemProxy section */ ··· 57 58 4E62B4B048ACF350C4CFD2DF /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 58 59 59C723F22EA9C6BF002F18BF /* AppIcon.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; path = AppIcon.icon; sourceTree = "<group>"; }; 59 60 59DA3B292DA45BAD00C44421 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; }; 61 + 622B2DC1E9685418514D2FC4 /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = "<group>"; }; 60 62 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; }; 61 63 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; }; 64 + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = "<group>"; }; 62 65 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; }; 63 66 86577EB0EB28471AE1014B20 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; }; 64 67 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; }; ··· 69 72 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; }; 70 73 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; 71 74 E84053FC439008C2EBB39FE0 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; }; 72 - 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = "<group>"; }; 73 75 /* End PBXFileReference section */ 74 76 75 77 /* Begin PBXFrameworksBuildPhase section */ ··· 123 125 331C8082294A63A400263BE5 /* RunnerTests */, 124 126 BA71D3C7868C6E4326FA2D8D /* Pods */, 125 127 A72872E4BD53F060F12B36CF /* Frameworks */, 128 + 622B2DC1E9685418514D2FC4 /* GoogleService-Info.plist */, 126 129 ); 127 130 sourceTree = "<group>"; 128 131 }; ··· 196 199 productType = "com.apple.product-type.bundle.unit-test"; 197 200 }; 198 201 97C146ED1CF9000F007C117D /* Runner */ = { 199 - packageProductDependencies = ( 200 - 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, 201 - ); 202 202 isa = PBXNativeTarget; 203 203 buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; 204 204 buildPhases = ( ··· 216 216 dependencies = ( 217 217 ); 218 218 name = Runner; 219 + packageProductDependencies = ( 220 + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, 221 + ); 219 222 productName = Runner; 220 223 productReference = 97C146EE1CF9000F007C117D /* Runner.app */; 221 224 productType = "com.apple.product-type.application"; ··· 224 227 225 228 /* Begin PBXProject section */ 226 229 97C146E61CF9000F007C117D /* Project object */ = { 227 - packageReferences = ( 228 - 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */, 229 - ); 230 230 isa = PBXProject; 231 231 attributes = { 232 232 BuildIndependentTargetsInParallel = YES; ··· 252 252 Base, 253 253 ); 254 254 mainGroup = 97C146E51CF9000F007C117D; 255 + packageReferences = ( 256 + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */, 257 + ); 255 258 productRefGroup = 97C146EF1CF9000F007C117D /* Products */; 256 259 projectDirPath = ""; 257 260 projectRoot = ""; ··· 279 282 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 280 283 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 281 284 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, 285 + 9B6EB3EAD1043C920A48987A /* GoogleService-Info.plist in Resources */, 282 286 ); 283 287 runOnlyForDeploymentPostprocessing = 0; 284 288 }; ··· 756 760 defaultConfigurationName = Release; 757 761 }; 758 762 /* End XCConfigurationList section */ 763 + 759 764 /* Begin XCLocalSwiftPackageReference section */ 760 - 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = { 765 + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = { 761 766 isa = XCLocalSwiftPackageReference; 762 767 relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; 763 768 }; 764 769 /* End XCLocalSwiftPackageReference section */ 770 + 765 771 /* Begin XCSwiftPackageProductDependency section */ 766 772 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { 767 773 isa = XCSwiftPackageProductDependency;
+140
ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
··· 1 + { 2 + "pins" : [ 3 + { 4 + "identity" : "abseil-cpp-binary", 5 + "kind" : "remoteSourceControl", 6 + "location" : "https://github.com/google/abseil-cpp-binary.git", 7 + "state" : { 8 + "revision" : "bbe8b69694d7873315fd3a4ad41efe043e1c07c5", 9 + "version" : "1.2024072200.0" 10 + } 11 + }, 12 + { 13 + "identity" : "app-check", 14 + "kind" : "remoteSourceControl", 15 + "location" : "https://github.com/google/app-check.git", 16 + "state" : { 17 + "revision" : "61b85103a1aeed8218f17c794687781505fbbef5", 18 + "version" : "11.2.0" 19 + } 20 + }, 21 + { 22 + "identity" : "firebase-ios-sdk", 23 + "kind" : "remoteSourceControl", 24 + "location" : "https://github.com/firebase/firebase-ios-sdk", 25 + "state" : { 26 + "revision" : "fdc352fabaf5916e7faa1f96ad02b1957e93e5a5", 27 + "version" : "11.15.0" 28 + } 29 + }, 30 + { 31 + "identity" : "flutterfire", 32 + "kind" : "remoteSourceControl", 33 + "location" : "https://github.com/firebase/flutterfire", 34 + "state" : { 35 + "revision" : "dadb0fd27bc9afe4dee4f23326a4a9ba238258ac", 36 + "version" : "3.15.2-firebase-core-swift" 37 + } 38 + }, 39 + { 40 + "identity" : "google-ads-on-device-conversion-ios-sdk", 41 + "kind" : "remoteSourceControl", 42 + "location" : "https://github.com/googleads/google-ads-on-device-conversion-ios-sdk", 43 + "state" : { 44 + "revision" : "a2d0f1f1666de591eb1a811f40b1706f5c63a2ed", 45 + "version" : "2.3.0" 46 + } 47 + }, 48 + { 49 + "identity" : "googleappmeasurement", 50 + "kind" : "remoteSourceControl", 51 + "location" : "https://github.com/google/GoogleAppMeasurement.git", 52 + "state" : { 53 + "revision" : "45ce435e9406d3c674dd249a042b932bee006f60", 54 + "version" : "11.15.0" 55 + } 56 + }, 57 + { 58 + "identity" : "googledatatransport", 59 + "kind" : "remoteSourceControl", 60 + "location" : "https://github.com/google/GoogleDataTransport.git", 61 + "state" : { 62 + "revision" : "617af071af9aa1d6a091d59a202910ac482128f9", 63 + "version" : "10.1.0" 64 + } 65 + }, 66 + { 67 + "identity" : "googleutilities", 68 + "kind" : "remoteSourceControl", 69 + "location" : "https://github.com/google/GoogleUtilities.git", 70 + "state" : { 71 + "revision" : "60da361632d0de02786f709bdc0c4df340f7613e", 72 + "version" : "8.1.0" 73 + } 74 + }, 75 + { 76 + "identity" : "grpc-binary", 77 + "kind" : "remoteSourceControl", 78 + "location" : "https://github.com/google/grpc-binary.git", 79 + "state" : { 80 + "revision" : "75b31c842f664a0f46a2e590a570e370249fd8f6", 81 + "version" : "1.69.1" 82 + } 83 + }, 84 + { 85 + "identity" : "gtm-session-fetcher", 86 + "kind" : "remoteSourceControl", 87 + "location" : "https://github.com/google/gtm-session-fetcher.git", 88 + "state" : { 89 + "revision" : "c756a29784521063b6a1202907e2cc47f41b667c", 90 + "version" : "4.5.0" 91 + } 92 + }, 93 + { 94 + "identity" : "interop-ios-for-google-sdks", 95 + "kind" : "remoteSourceControl", 96 + "location" : "https://github.com/google/interop-ios-for-google-sdks.git", 97 + "state" : { 98 + "revision" : "040d087ac2267d2ddd4cca36c757d1c6a05fdbfe", 99 + "version" : "101.0.0" 100 + } 101 + }, 102 + { 103 + "identity" : "leveldb", 104 + "kind" : "remoteSourceControl", 105 + "location" : "https://github.com/firebase/leveldb.git", 106 + "state" : { 107 + "revision" : "a0bc79961d7be727d258d33d5a6b2f1023270ba1", 108 + "version" : "1.22.5" 109 + } 110 + }, 111 + { 112 + "identity" : "nanopb", 113 + "kind" : "remoteSourceControl", 114 + "location" : "https://github.com/firebase/nanopb.git", 115 + "state" : { 116 + "revision" : "b7e1104502eca3a213b46303391ca4d3bc8ddec1", 117 + "version" : "2.30910.0" 118 + } 119 + }, 120 + { 121 + "identity" : "promises", 122 + "kind" : "remoteSourceControl", 123 + "location" : "https://github.com/google/promises.git", 124 + "state" : { 125 + "revision" : "540318ecedd63d883069ae7f1ed811a2df00b6ac", 126 + "version" : "2.4.0" 127 + } 128 + }, 129 + { 130 + "identity" : "swift-protobuf", 131 + "kind" : "remoteSourceControl", 132 + "location" : "https://github.com/apple/swift-protobuf.git", 133 + "state" : { 134 + "revision" : "c169a5744230951031770e27e475ff6eefe51f9d", 135 + "version" : "1.33.3" 136 + } 137 + } 138 + ], 139 + "version" : 2 140 + }
+140
ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved
··· 1 + { 2 + "pins" : [ 3 + { 4 + "identity" : "abseil-cpp-binary", 5 + "kind" : "remoteSourceControl", 6 + "location" : "https://github.com/google/abseil-cpp-binary.git", 7 + "state" : { 8 + "revision" : "bbe8b69694d7873315fd3a4ad41efe043e1c07c5", 9 + "version" : "1.2024072200.0" 10 + } 11 + }, 12 + { 13 + "identity" : "app-check", 14 + "kind" : "remoteSourceControl", 15 + "location" : "https://github.com/google/app-check.git", 16 + "state" : { 17 + "revision" : "61b85103a1aeed8218f17c794687781505fbbef5", 18 + "version" : "11.2.0" 19 + } 20 + }, 21 + { 22 + "identity" : "firebase-ios-sdk", 23 + "kind" : "remoteSourceControl", 24 + "location" : "https://github.com/firebase/firebase-ios-sdk", 25 + "state" : { 26 + "revision" : "fdc352fabaf5916e7faa1f96ad02b1957e93e5a5", 27 + "version" : "11.15.0" 28 + } 29 + }, 30 + { 31 + "identity" : "flutterfire", 32 + "kind" : "remoteSourceControl", 33 + "location" : "https://github.com/firebase/flutterfire", 34 + "state" : { 35 + "revision" : "dadb0fd27bc9afe4dee4f23326a4a9ba238258ac", 36 + "version" : "3.15.2-firebase-core-swift" 37 + } 38 + }, 39 + { 40 + "identity" : "google-ads-on-device-conversion-ios-sdk", 41 + "kind" : "remoteSourceControl", 42 + "location" : "https://github.com/googleads/google-ads-on-device-conversion-ios-sdk", 43 + "state" : { 44 + "revision" : "a2d0f1f1666de591eb1a811f40b1706f5c63a2ed", 45 + "version" : "2.3.0" 46 + } 47 + }, 48 + { 49 + "identity" : "googleappmeasurement", 50 + "kind" : "remoteSourceControl", 51 + "location" : "https://github.com/google/GoogleAppMeasurement.git", 52 + "state" : { 53 + "revision" : "45ce435e9406d3c674dd249a042b932bee006f60", 54 + "version" : "11.15.0" 55 + } 56 + }, 57 + { 58 + "identity" : "googledatatransport", 59 + "kind" : "remoteSourceControl", 60 + "location" : "https://github.com/google/GoogleDataTransport.git", 61 + "state" : { 62 + "revision" : "617af071af9aa1d6a091d59a202910ac482128f9", 63 + "version" : "10.1.0" 64 + } 65 + }, 66 + { 67 + "identity" : "googleutilities", 68 + "kind" : "remoteSourceControl", 69 + "location" : "https://github.com/google/GoogleUtilities.git", 70 + "state" : { 71 + "revision" : "60da361632d0de02786f709bdc0c4df340f7613e", 72 + "version" : "8.1.0" 73 + } 74 + }, 75 + { 76 + "identity" : "grpc-binary", 77 + "kind" : "remoteSourceControl", 78 + "location" : "https://github.com/google/grpc-binary.git", 79 + "state" : { 80 + "revision" : "75b31c842f664a0f46a2e590a570e370249fd8f6", 81 + "version" : "1.69.1" 82 + } 83 + }, 84 + { 85 + "identity" : "gtm-session-fetcher", 86 + "kind" : "remoteSourceControl", 87 + "location" : "https://github.com/google/gtm-session-fetcher.git", 88 + "state" : { 89 + "revision" : "c756a29784521063b6a1202907e2cc47f41b667c", 90 + "version" : "4.5.0" 91 + } 92 + }, 93 + { 94 + "identity" : "interop-ios-for-google-sdks", 95 + "kind" : "remoteSourceControl", 96 + "location" : "https://github.com/google/interop-ios-for-google-sdks.git", 97 + "state" : { 98 + "revision" : "040d087ac2267d2ddd4cca36c757d1c6a05fdbfe", 99 + "version" : "101.0.0" 100 + } 101 + }, 102 + { 103 + "identity" : "leveldb", 104 + "kind" : "remoteSourceControl", 105 + "location" : "https://github.com/firebase/leveldb.git", 106 + "state" : { 107 + "revision" : "a0bc79961d7be727d258d33d5a6b2f1023270ba1", 108 + "version" : "1.22.5" 109 + } 110 + }, 111 + { 112 + "identity" : "nanopb", 113 + "kind" : "remoteSourceControl", 114 + "location" : "https://github.com/firebase/nanopb.git", 115 + "state" : { 116 + "revision" : "b7e1104502eca3a213b46303391ca4d3bc8ddec1", 117 + "version" : "2.30910.0" 118 + } 119 + }, 120 + { 121 + "identity" : "promises", 122 + "kind" : "remoteSourceControl", 123 + "location" : "https://github.com/google/promises.git", 124 + "state" : { 125 + "revision" : "540318ecedd63d883069ae7f1ed811a2df00b6ac", 126 + "version" : "2.4.0" 127 + } 128 + }, 129 + { 130 + "identity" : "swift-protobuf", 131 + "kind" : "remoteSourceControl", 132 + "location" : "https://github.com/apple/swift-protobuf.git", 133 + "state" : { 134 + "revision" : "c169a5744230951031770e27e475ff6eefe51f9d", 135 + "version" : "1.33.3" 136 + } 137 + } 138 + ], 139 + "version" : 2 140 + }
+8
ios/ci_scripts/ci_post_clone.sh
··· 24 24 SHOWCASES_LICENSE_FLUTTER=$SHOWCASES_LICENSE_FLUTTER 25 25 EOL 26 26 27 + # Decode Firebase config from base64 environment variable 28 + if [ -n "$GOOGLE_SERVICE_INFO_PLIST_BASE64" ]; then 29 + echo "Decoding GoogleService-Info.plist..." 30 + echo "$GOOGLE_SERVICE_INFO_PLIST_BASE64" | base64 -d > ios/Runner/GoogleService-Info.plist 31 + else 32 + echo "Warning: GOOGLE_SERVICE_INFO_PLIST_BASE64 not set" 33 + fi 34 + 27 35 # Install CocoaPods using Homebrew. 28 36 HOMEBREW_NO_AUTO_UPDATE=1 # disable homebrew's automatic updates. 29 37 brew install cocoapods
+68
lib/firebase_options.dart
··· 1 + // File generated by FlutterFire CLI. 2 + // ignore_for_file: type=lint 3 + import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; 4 + import 'package:flutter/foundation.dart' 5 + show defaultTargetPlatform, kIsWeb, TargetPlatform; 6 + 7 + /// Default [FirebaseOptions] for use with your Firebase apps. 8 + /// 9 + /// Example: 10 + /// ```dart 11 + /// import 'firebase_options.dart'; 12 + /// // ... 13 + /// await Firebase.initializeApp( 14 + /// options: DefaultFirebaseOptions.currentPlatform, 15 + /// ); 16 + /// ``` 17 + class DefaultFirebaseOptions { 18 + static FirebaseOptions get currentPlatform { 19 + if (kIsWeb) { 20 + throw UnsupportedError( 21 + 'DefaultFirebaseOptions have not been configured for web - ' 22 + 'you can reconfigure this by running the FlutterFire CLI again.', 23 + ); 24 + } 25 + switch (defaultTargetPlatform) { 26 + case TargetPlatform.android: 27 + return android; 28 + case TargetPlatform.iOS: 29 + return ios; 30 + case TargetPlatform.macOS: 31 + throw UnsupportedError( 32 + 'DefaultFirebaseOptions have not been configured for macos - ' 33 + 'you can reconfigure this by running the FlutterFire CLI again.', 34 + ); 35 + case TargetPlatform.windows: 36 + throw UnsupportedError( 37 + 'DefaultFirebaseOptions have not been configured for windows - ' 38 + 'you can reconfigure this by running the FlutterFire CLI again.', 39 + ); 40 + case TargetPlatform.linux: 41 + throw UnsupportedError( 42 + 'DefaultFirebaseOptions have not been configured for linux - ' 43 + 'you can reconfigure this by running the FlutterFire CLI again.', 44 + ); 45 + default: 46 + throw UnsupportedError( 47 + 'DefaultFirebaseOptions are not supported for this platform.', 48 + ); 49 + } 50 + } 51 + 52 + static const FirebaseOptions android = FirebaseOptions( 53 + apiKey: 'AIzaSyCZUyZLcOZkt5FohWAIdDIA4ubnTkEJtKI', 54 + appId: '1:244807562864:android:04ab7d3cda19185e2e66b5', 55 + messagingSenderId: '244807562864', 56 + projectId: 'sprk-social', 57 + storageBucket: 'sprk-social.firebasestorage.app', 58 + ); 59 + 60 + static const FirebaseOptions ios = FirebaseOptions( 61 + apiKey: 'AIzaSyBYBZLhGhUkObNbwLCXO1PmtPuEa8zthdI', 62 + appId: '1:244807562864:ios:e5004433a6cff5ab2e66b5', 63 + messagingSenderId: '244807562864', 64 + projectId: 'sprk-social', 65 + storageBucket: 'sprk-social.firebasestorage.app', 66 + iosBundleId: 'so.sprk.app', 67 + ); 68 + }
-2
lib/src/core/design_system/components/molecules/feed_tag_list.dart
··· 204 204 1.0, 205 205 ); 206 206 return LinearGradient( 207 - begin: Alignment.centerLeft, 208 - end: Alignment.centerRight, 209 207 colors: const [ 210 208 Colors.white, 211 209 Colors.white,
+11
lib/src/core/di/service_locator.dart
··· 5 5 import 'package:spark/src/core/network/atproto/atproto.dart'; 6 6 import 'package:spark/src/core/network/atproto/data/repositories/actor_repository_impl.dart'; 7 7 import 'package:spark/src/core/network/atproto/data/repositories/graph_repository_impl.dart'; 8 + import 'package:spark/src/core/network/atproto/data/repositories/notification_repository.dart'; 9 + import 'package:spark/src/core/network/atproto/data/repositories/notification_repository_impl.dart'; 8 10 import 'package:spark/src/core/network/atproto/data/repositories/pref_repository.dart'; 9 11 import 'package:spark/src/core/network/atproto/data/repositories/pref_repository_impl.dart'; 10 12 import 'package:spark/src/core/network/atproto/data/repositories/sound_repository.dart'; ··· 14 16 import 'package:spark/src/core/network/messages/data/repository/messages_repository.dart'; 15 17 import 'package:spark/src/core/network/messages/data/repository/messages_repository_xrpc.dart'; 16 18 import 'package:spark/src/core/network/xrpc/service_auth_helper.dart'; 19 + import 'package:spark/src/core/notifications/push_notification_service.dart'; 17 20 import 'package:spark/src/core/pro_video_editor/pro_video_editor_repository.dart'; 18 21 import 'package:spark/src/core/pro_video_editor/pro_video_editor_repository_impl.dart'; 19 22 import 'package:spark/src/core/storage/cache/download_manager_interface.dart'; ··· 87 90 ) 88 91 ..registerSingleton<ProVideoEditorRepository>( 89 92 const ProVideoEditorRepositoryImpl(), 93 + ) 94 + ..registerSingleton<NotificationRepository>( 95 + NotificationRepositoryImpl(sl<SprkRepository>()), 90 96 ); 97 + 98 + // Initialize push notification service asynchronously 99 + final pushService = PushNotificationService(); 100 + await pushService.initialize(); 101 + sl.registerSingleton<PushNotificationService>(pushService); 91 102 }
+22
lib/src/core/network/atproto/data/repositories/notification_repository.dart
··· 24 24 /// 25 25 /// [seenAt] The timestamp to mark notifications as seen at 26 26 Future<void> updateSeen(DateTime seenAt); 27 + 28 + /// Register device for push notifications 29 + /// 30 + /// [token] The FCM/APNs device token 31 + /// [platform] The platform identifier ('ios' or 'android') 32 + /// [appId] The application identifier (e.g., 'so.sprk.app') 33 + Future<void> registerPush({ 34 + required String token, 35 + required String platform, 36 + required String appId, 37 + }); 38 + 39 + /// Unregister device from push notifications 40 + /// 41 + /// [token] The FCM/APNs device token to unregister 42 + /// [platform] The platform identifier ('ios' or 'android') 43 + /// [appId] The application identifier (e.g., 'so.sprk.app') 44 + Future<void> unregisterPush({ 45 + required String token, 46 + required String platform, 47 + required String appId, 48 + }); 27 49 }
+86
lib/src/core/network/atproto/data/repositories/notification_repository_impl.dart
··· 5 5 import 'package:spark/src/core/network/atproto/data/models/notification_models.dart'; 6 6 import 'package:spark/src/core/network/atproto/data/repositories/notification_repository.dart'; 7 7 import 'package:spark/src/core/network/atproto/data/repositories/sprk_repository.dart'; 8 + import 'package:spark/src/core/notifications/push_notification_service.dart'; 8 9 import 'package:spark/src/core/utils/logging/log_service.dart'; 9 10 import 'package:spark/src/core/utils/logging/logger.dart'; 10 11 ··· 128 129 headers: {'atproto-proxy': _client.sprkDid}, 129 130 ); 130 131 132 + // Clear the app badge locally (server also sends silent push for background) 133 + try { 134 + await GetIt.instance<PushNotificationService>().clearBadge(); 135 + } catch (e) { 136 + _logger.w('Failed to clear badge: $e'); 137 + } 138 + 131 139 _logger.d('Seen timestamp updated successfully'); 140 + }); 141 + } 142 + 143 + @override 144 + Future<void> registerPush({ 145 + required String token, 146 + required String platform, 147 + required String appId, 148 + }) async { 149 + _logger.d('Registering push token: platform=$platform, appId=$appId'); 150 + return _client.executeWithRetry(() async { 151 + if (!_client.authRepository.isAuthenticated) { 152 + _logger.w('Not authenticated'); 153 + throw Exception('Not authenticated'); 154 + } 155 + 156 + final atproto = _client.authRepository.atproto; 157 + if (atproto == null) { 158 + _logger.e('AtProto not initialized'); 159 + throw Exception('AtProto not initialized'); 160 + } 161 + 162 + // serviceDid needs just the DID without fragment (format validation) 163 + final serviceDid = _client.sprkDid.split('#').first; 164 + 165 + final body = { 166 + 'serviceDid': serviceDid, 167 + 'token': token, 168 + 'platform': platform, 169 + 'appId': appId, 170 + }; 171 + 172 + await atproto.post( 173 + NSID.parse('so.sprk.notification.registerPush'), 174 + body: body, 175 + headers: {'atproto-proxy': _client.sprkDid}, 176 + ); 177 + 178 + _logger.i('Push token registered successfully'); 179 + }); 180 + } 181 + 182 + @override 183 + Future<void> unregisterPush({ 184 + required String token, 185 + required String platform, 186 + required String appId, 187 + }) async { 188 + _logger.d('Unregistering push token: platform=$platform, appId=$appId'); 189 + return _client.executeWithRetry(() async { 190 + if (!_client.authRepository.isAuthenticated) { 191 + _logger.w('Not authenticated'); 192 + throw Exception('Not authenticated'); 193 + } 194 + 195 + final atproto = _client.authRepository.atproto; 196 + if (atproto == null) { 197 + _logger.e('AtProto not initialized'); 198 + throw Exception('AtProto not initialized'); 199 + } 200 + 201 + // serviceDid needs just the DID without fragment (format validation) 202 + final serviceDid = _client.sprkDid.split('#').first; 203 + 204 + final body = { 205 + 'serviceDid': serviceDid, 206 + 'token': token, 207 + 'platform': platform, 208 + 'appId': appId, 209 + }; 210 + 211 + await atproto.post( 212 + NSID.parse('so.sprk.notification.unregisterPush'), 213 + body: body, 214 + headers: {'atproto-proxy': _client.sprkDid}, 215 + ); 216 + 217 + _logger.i('Push token unregistered successfully'); 132 218 }); 133 219 } 134 220 }
+167
lib/src/core/notifications/push_notification_service.dart
··· 1 + import 'dart:io'; 2 + 3 + import 'package:app_badge_plus/app_badge_plus.dart'; 4 + import 'package:firebase_core/firebase_core.dart'; 5 + import 'package:firebase_messaging/firebase_messaging.dart'; 6 + import 'package:get_it/get_it.dart'; 7 + import 'package:spark/src/core/utils/logging/log_service.dart'; 8 + import 'package:spark/src/core/utils/logging/logger.dart'; 9 + 10 + /// Service for managing push notifications via Firebase Cloud Messaging 11 + class PushNotificationService { 12 + PushNotificationService() { 13 + _logger.v('PushNotificationService initialized'); 14 + } 15 + 16 + late final FirebaseMessaging _messaging; 17 + final SparkLogger _logger = GetIt.instance<LogService>().getLogger( 18 + 'PushNotificationService', 19 + ); 20 + 21 + String? _currentToken; 22 + bool _badgeSupported = false; 23 + bool _initialized = false; 24 + 25 + /// Initializes Firebase without requesting permissions 26 + /// Permissions should be requested separately via [requestPermissionAndGetToken] 27 + Future<void> initialize() async { 28 + _logger.i('Initializing push notification service'); 29 + 30 + try { 31 + await Firebase.initializeApp(); 32 + _messaging = FirebaseMessaging.instance; 33 + _logger.d('Firebase initialized'); 34 + 35 + // Listen for token refresh 36 + _messaging.onTokenRefresh.listen(_onTokenRefresh); 37 + 38 + // Check if badge is supported on this device 39 + _badgeSupported = await AppBadgePlus.isSupported(); 40 + _logger.d('Badge support: $_badgeSupported'); 41 + 42 + _initialized = true; 43 + _logger.i('Push notification service initialized successfully'); 44 + } catch (e, stackTrace) { 45 + _logger.e( 46 + 'Failed to initialize push notifications', 47 + error: e, 48 + stackTrace: stackTrace, 49 + ); 50 + } 51 + } 52 + 53 + /// Returns true if notification permissions are already granted 54 + Future<bool> hasPermission() async { 55 + if (!_initialized) return false; 56 + 57 + try { 58 + final settings = await _messaging.getNotificationSettings(); 59 + return settings.authorizationStatus == AuthorizationStatus.authorized || 60 + settings.authorizationStatus == AuthorizationStatus.provisional; 61 + } catch (e) { 62 + _logger.e('Failed to check permission status', error: e); 63 + return false; 64 + } 65 + } 66 + 67 + /// Requests notification permissions from the user 68 + /// Returns true if permission was granted 69 + Future<bool> requestPermission() async { 70 + if (!_initialized) return false; 71 + 72 + _logger.d('Requesting notification permissions'); 73 + 74 + try { 75 + final settings = await _messaging.requestPermission(); 76 + 77 + if (settings.authorizationStatus == AuthorizationStatus.authorized) { 78 + _logger.i('User granted notification permission'); 79 + return true; 80 + } else if (settings.authorizationStatus == 81 + AuthorizationStatus.provisional) { 82 + _logger.i('User granted provisional notification permission'); 83 + return true; 84 + } else { 85 + _logger.w('User denied notification permission'); 86 + return false; 87 + } 88 + } catch (e, stackTrace) { 89 + _logger.e( 90 + 'Failed to request permission', 91 + error: e, 92 + stackTrace: stackTrace, 93 + ); 94 + return false; 95 + } 96 + } 97 + 98 + /// Requests permission (if needed) and returns the FCM token 99 + /// Returns null if permission denied or error occurs 100 + Future<String?> requestPermissionAndGetToken() async { 101 + if (!_initialized) return null; 102 + 103 + final hasExistingPermission = await hasPermission(); 104 + 105 + if (!hasExistingPermission) { 106 + final granted = await requestPermission(); 107 + if (!granted) return null; 108 + } 109 + 110 + return getToken(); 111 + } 112 + 113 + /// Handles FCM token refresh 114 + void _onTokenRefresh(String token) { 115 + _logger.d('FCM token refreshed: ${token.substring(0, 10)}...'); 116 + _currentToken = token; 117 + // Token refresh registration is handled by the auth flow 118 + // which will call registerPush with the new token 119 + } 120 + 121 + /// Returns the current FCM token, or null if not available 122 + /// Note: This requires permission to be granted first 123 + Future<String?> getToken() async { 124 + if (!_initialized) return null; 125 + 126 + try { 127 + _currentToken ??= await _messaging.getToken(); 128 + if (_currentToken != null) { 129 + _logger.d('FCM token obtained: ${_currentToken!.substring(0, 10)}...'); 130 + } 131 + return _currentToken; 132 + } catch (e) { 133 + _logger.e('Failed to get FCM token', error: e); 134 + return null; 135 + } 136 + } 137 + 138 + /// Returns the platform identifier ('ios' or 'android') 139 + String get platform => Platform.isIOS ? 'ios' : 'android'; 140 + 141 + /// Callback for when token is refreshed - allows external registration 142 + Stream<String> get onTokenRefresh => _messaging.onTokenRefresh; 143 + 144 + /// Clears the app badge count (iOS only, no-op on Android) 145 + Future<void> clearBadge() async { 146 + if (!_badgeSupported) return; 147 + 148 + try { 149 + await AppBadgePlus.updateBadge(0); 150 + _logger.d('Badge cleared'); 151 + } catch (e, stackTrace) { 152 + _logger.e('Failed to clear badge', error: e, stackTrace: stackTrace); 153 + } 154 + } 155 + 156 + /// Updates the app badge count (iOS only, no-op on Android) 157 + Future<void> updateBadge(int count) async { 158 + if (!_badgeSupported) return; 159 + 160 + try { 161 + await AppBadgePlus.updateBadge(count); 162 + _logger.d('Badge updated to $count'); 163 + } catch (e, stackTrace) { 164 + _logger.e('Failed to update badge', error: e, stackTrace: stackTrace); 165 + } 166 + } 167 + }
+160
lib/src/features/auth/providers/auth_providers.dart
··· 1 + import 'dart:async'; 2 + 1 3 import 'package:atproto/atproto.dart'; 2 4 import 'package:get_it/get_it.dart'; 3 5 import 'package:riverpod_annotation/riverpod_annotation.dart'; 4 6 import 'package:spark/src/core/auth/data/models/login_result.dart'; 5 7 import 'package:spark/src/core/auth/data/repositories/auth_repository.dart'; 8 + import 'package:spark/src/core/network/atproto/data/repositories/notification_repository.dart'; 9 + import 'package:spark/src/core/notifications/push_notification_service.dart'; 6 10 import 'package:spark/src/core/utils/logging/log_service.dart'; 7 11 import 'package:spark/src/core/utils/logging/logger.dart'; 8 12 import 'package:spark/src/features/auth/providers/auth_state.dart'; ··· 21 25 class Auth extends _$Auth { 22 26 late final AuthRepository _authRepository; 23 27 late final LogService _logService; 28 + StreamSubscription<String>? _tokenRefreshSubscription; 29 + bool _pendingPushRegistration = false; 24 30 25 31 @override 26 32 AuthState build() { ··· 133 139 } else { 134 140 _updateState(); 135 141 state = state.copyWith(isLoading: false); 142 + 143 + // Register for push notifications after successful login 144 + await _registerPushNotifications(); 136 145 } 137 146 138 147 return result; ··· 152 161 state = state.copyWith(isLoading: true, error: null); 153 162 154 163 try { 164 + // Unregister push notifications before logout 165 + await _unregisterPushNotifications(); 166 + 155 167 await _authRepository.logout(); 156 168 _updateState(); 157 169 state = state.copyWith(isLoading: false); ··· 190 202 _logger.e('Token refresh error', error: e, stackTrace: stackTrace); 191 203 state = state.copyWith(error: 'Token refresh failed: $e'); 192 204 return false; 205 + } 206 + } 207 + 208 + /// Registers the device for push notifications 209 + /// Only registers if permission is already granted, otherwise defers 210 + Future<void> _registerPushNotifications() async { 211 + try { 212 + final pushService = GetIt.instance<PushNotificationService>(); 213 + 214 + // Check if we already have permission 215 + final hasPermission = await pushService.hasPermission(); 216 + 217 + if (hasPermission) { 218 + // Permission already granted, register immediately 219 + await _doRegisterPush(pushService); 220 + } else { 221 + // Permission not granted yet, defer until main screen 222 + _pendingPushRegistration = true; 223 + _logger.i( 224 + 'Push permission not granted, deferring registration to main screen', 225 + ); 226 + } 227 + } catch (e, stackTrace) { 228 + // Don't fail login if push registration fails 229 + _logger.e( 230 + 'Failed to register push notifications', 231 + error: e, 232 + stackTrace: stackTrace, 233 + ); 234 + } 235 + } 236 + 237 + /// Actually performs push registration (called when we have permission) 238 + Future<void> _doRegisterPush(PushNotificationService pushService) async { 239 + final token = await pushService.getToken(); 240 + 241 + if (token != null) { 242 + final notificationRepo = GetIt.instance<NotificationRepository>(); 243 + await notificationRepo.registerPush( 244 + token: token, 245 + platform: pushService.platform, 246 + appId: 'so.sprk.app', 247 + ); 248 + _logger.i('Push notifications registered successfully'); 249 + 250 + // Set up listener for token refresh 251 + await _setupTokenRefreshListener(pushService, notificationRepo); 252 + _pendingPushRegistration = false; 253 + } else { 254 + _logger.w('No push token available'); 255 + } 256 + } 257 + 258 + /// Returns true if push registration is pending (permission not yet requested) 259 + bool get hasPendingPushRegistration => _pendingPushRegistration; 260 + 261 + /// Requests push notification permission and registers if granted 262 + /// Call this from the main screen after login 263 + Future<bool> requestPushPermissionAndRegister() async { 264 + if (!_pendingPushRegistration) { 265 + _logger.d('No pending push registration'); 266 + return true; 267 + } 268 + 269 + try { 270 + final pushService = GetIt.instance<PushNotificationService>(); 271 + final granted = await pushService.requestPermission(); 272 + 273 + if (granted) { 274 + await _doRegisterPush(pushService); 275 + return true; 276 + } else { 277 + _logger.w('User denied push notification permission'); 278 + _pendingPushRegistration = false; 279 + return false; 280 + } 281 + } catch (e, stackTrace) { 282 + _logger.e( 283 + 'Failed to request push permission', 284 + error: e, 285 + stackTrace: stackTrace, 286 + ); 287 + return false; 288 + } 289 + } 290 + 291 + /// Sets up a listener for FCM token refresh to re-register 292 + Future<void> _setupTokenRefreshListener( 293 + PushNotificationService pushService, 294 + NotificationRepository notificationRepo, 295 + ) async { 296 + // Cancel any existing subscription and wait for it to complete 297 + await _tokenRefreshSubscription?.cancel(); 298 + 299 + _tokenRefreshSubscription = pushService.onTokenRefresh.listen( 300 + (newToken) async { 301 + _logger.i('FCM token refreshed, re-registering push notifications'); 302 + try { 303 + await notificationRepo.registerPush( 304 + token: newToken, 305 + platform: pushService.platform, 306 + appId: 'so.sprk.app', 307 + ); 308 + _logger.i('Push notifications re-registered with new token'); 309 + } catch (e, stackTrace) { 310 + _logger.e( 311 + 'Failed to re-register push notifications after token refresh', 312 + error: e, 313 + stackTrace: stackTrace, 314 + ); 315 + } 316 + }, 317 + onError: (Object error, StackTrace stackTrace) { 318 + _logger.e( 319 + 'Error in token refresh stream', 320 + error: error, 321 + stackTrace: stackTrace, 322 + ); 323 + }, 324 + ); 325 + } 326 + 327 + /// Unregisters the device from push notifications 328 + Future<void> _unregisterPushNotifications() async { 329 + // Cancel token refresh listener 330 + await _tokenRefreshSubscription?.cancel(); 331 + _tokenRefreshSubscription = null; 332 + 333 + try { 334 + final pushService = GetIt.instance<PushNotificationService>(); 335 + final token = await pushService.getToken(); 336 + 337 + if (token != null) { 338 + final notificationRepo = GetIt.instance<NotificationRepository>(); 339 + await notificationRepo.unregisterPush( 340 + token: token, 341 + platform: pushService.platform, 342 + appId: 'so.sprk.app', 343 + ); 344 + _logger.i('Push notifications unregistered successfully'); 345 + } 346 + } catch (e, stackTrace) { 347 + // Don't fail logout if push unregistration fails 348 + _logger.e( 349 + 'Failed to unregister push notifications', 350 + error: e, 351 + stackTrace: stackTrace, 352 + ); 193 353 } 194 354 } 195 355 }
+9 -9
lib/src/features/feed/ui/widgets/feed/feeds_bar.dart
··· 63 63 height: 4, 64 64 margin: const EdgeInsets.only(bottom: 16), 65 65 decoration: BoxDecoration( 66 - color: Theme.of(context) 67 - .colorScheme 68 - .onSurface 69 - .withAlpha(50), 66 + color: Theme.of( 67 + context, 68 + ).colorScheme.onSurface.withAlpha(50), 70 69 borderRadius: BorderRadius.circular(2), 71 70 ), 72 71 ), ··· 79 78 child: Text( 80 79 feed.view?.displayName ?? 'Following', 81 80 style: Theme.of(context).textTheme.titleMedium?.copyWith( 82 - fontWeight: FontWeight.bold, 83 - ), 81 + fontWeight: FontWeight.bold, 82 + ), 84 83 ), 85 84 ), 86 85 const Divider(), ··· 142 141 ], 143 142 ), 144 143 ); 145 - if (confirmed == true) { 144 + if (confirmed ?? false) { 146 145 await ref 147 146 .read(settingsProvider.notifier) 148 147 .removeFeed(feed); ··· 168 167 final settings = ref.watch(settingsProvider); 169 168 170 169 // Only show pinned feeds in the home view 171 - final pinnedFeeds = 172 - settings.feeds.where((feed) => feed.config.pinned).toList(); 170 + final pinnedFeeds = settings.feeds 171 + .where((feed) => feed.config.pinned) 172 + .toList(); 173 173 174 174 final tags = pinnedFeeds.map((feed) { 175 175 final isTimeline =
+12
lib/src/features/home/ui/pages/main_page.dart
··· 26 26 @override 27 27 void initState() { 28 28 super.initState(); 29 + // Request push notification permission if pending (after login) 30 + WidgetsBinding.instance.addPostFrameCallback((_) { 31 + _requestPushPermissionIfNeeded(); 32 + }); 33 + } 34 + 35 + /// Requests push notification permission if it was deferred during login 36 + Future<void> _requestPushPermissionIfNeeded() async { 37 + final auth = ref.read(authProvider.notifier); 38 + if (auth.hasPendingPushRegistration) { 39 + await auth.requestPushPermissionAndRegister(); 40 + } 29 41 } 30 42 31 43 void _updateSystemUIOverlayStyle(int activeIndex, BuildContext context) {
+2 -2
lib/src/features/notifications/providers/notification_provider.dart
··· 150 150 final mostRecent = state.notifications.first; 151 151 await _notificationRepository.updateSeen(mostRecent.indexedAt); 152 152 // Refresh the unread count 153 - ref.invalidate(unreadCountProvider); 153 + await ref.read(unreadCountProvider().notifier).refresh(); 154 154 } catch (e, stackTrace) { 155 155 _logger.e( 156 156 'Error marking notifications as seen: $e', ··· 174 174 // Use this notification's indexedAt as the seenAt timestamp 175 175 await _notificationRepository.updateSeen(notification.indexedAt); 176 176 // Refresh the unread count 177 - ref.invalidate(unreadCountProvider); 177 + await ref.read(unreadCountProvider().notifier).refresh(); 178 178 } catch (e, stackTrace) { 179 179 _logger.e( 180 180 'Error marking notification as viewed: $e',
+13 -16
lib/src/features/notifications/ui/pages/notifications_page.dart
··· 4 4 import 'package:spark/src/core/ui/foundation/colors.dart'; 5 5 import 'package:spark/src/features/notifications/providers/notification_provider.dart' 6 6 show notificationProvider; 7 - import 'package:spark/src/features/notifications/providers/unread_count_provider.dart' 8 - show unreadCountProvider; 9 7 import 'package:spark/src/features/notifications/ui/widgets/notifications_list.dart'; 10 8 11 9 @RoutePage() ··· 17 15 } 18 16 19 17 class _NotificationsPageState extends ConsumerState<NotificationsPage> { 20 - @override 21 - void initState() { 22 - super.initState(); 23 - // Mark notifications as seen when page is viewed 24 - WidgetsBinding.instance.addPostFrameCallback((_) { 25 - ref 26 - .read( 27 - notificationProvider().notifier, 28 - ) 29 - .markAsSeen(); 30 - // Refresh unread count after marking as seen 31 - ref.read(unreadCountProvider().notifier).refresh(); 32 - }); 33 - } 18 + bool _hasMarkedAsSeen = false; 34 19 35 20 @override 36 21 Widget build(BuildContext context) { 22 + final notificationState = ref.watch(notificationProvider()); 23 + 24 + // Mark notifications as seen once they're loaded 25 + if (!_hasMarkedAsSeen && 26 + !notificationState.isLoading && 27 + notificationState.notifications.isNotEmpty) { 28 + _hasMarkedAsSeen = true; 29 + WidgetsBinding.instance.addPostFrameCallback((_) { 30 + ref.read(notificationProvider().notifier).markAsSeen(); 31 + }); 32 + } 33 + 37 34 return Scaffold( 38 35 backgroundColor: AppColors.black, 39 36 appBar: AppBar(
+64
pubspec.lock
··· 9 9 url: "https://pub.dev" 10 10 source: hosted 11 11 version: "91.0.0" 12 + _flutterfire_internals: 13 + dependency: transitive 14 + description: 15 + name: _flutterfire_internals 16 + sha256: ff0a84a2734d9e1089f8aedd5c0af0061b82fb94e95260d943404e0ef2134b11 17 + url: "https://pub.dev" 18 + source: hosted 19 + version: "1.3.59" 12 20 accessibility_tools: 13 21 dependency: transitive 14 22 description: ··· 57 65 url: "https://pub.dev" 58 66 source: hosted 59 67 version: "3.0.3" 68 + app_badge_plus: 69 + dependency: "direct main" 70 + description: 71 + name: app_badge_plus 72 + sha256: "152c22866f4b6d0d4eb3c8fe6916a77bbc0e3939da11b51cce38a4a26738dd60" 73 + url: "https://pub.dev" 74 + source: hosted 75 + version: "1.2.6" 60 76 archive: 61 77 dependency: transitive 62 78 description: ··· 571 587 url: "https://pub.dev" 572 588 source: hosted 573 589 version: "0.9.3+5" 590 + firebase_core: 591 + dependency: "direct main" 592 + description: 593 + name: firebase_core 594 + sha256: "7be63a3f841fc9663342f7f3a011a42aef6a61066943c90b1c434d79d5c995c5" 595 + url: "https://pub.dev" 596 + source: hosted 597 + version: "3.15.2" 598 + firebase_core_platform_interface: 599 + dependency: transitive 600 + description: 601 + name: firebase_core_platform_interface 602 + sha256: cccb4f572325dc14904c02fcc7db6323ad62ba02536833dddb5c02cac7341c64 603 + url: "https://pub.dev" 604 + source: hosted 605 + version: "6.0.2" 606 + firebase_core_web: 607 + dependency: transitive 608 + description: 609 + name: firebase_core_web 610 + sha256: "0ed0dc292e8f9ac50992e2394e9d336a0275b6ae400d64163fdf0a8a8b556c37" 611 + url: "https://pub.dev" 612 + source: hosted 613 + version: "2.24.1" 614 + firebase_messaging: 615 + dependency: "direct main" 616 + description: 617 + name: firebase_messaging 618 + sha256: "60be38574f8b5658e2f22b7e311ff2064bea835c248424a383783464e8e02fcc" 619 + url: "https://pub.dev" 620 + source: hosted 621 + version: "15.2.10" 622 + firebase_messaging_platform_interface: 623 + dependency: transitive 624 + description: 625 + name: firebase_messaging_platform_interface 626 + sha256: "685e1771b3d1f9c8502771ccc9f91485b376ffe16d553533f335b9183ea99754" 627 + url: "https://pub.dev" 628 + source: hosted 629 + version: "4.6.10" 630 + firebase_messaging_web: 631 + dependency: transitive 632 + description: 633 + name: firebase_messaging_web 634 + sha256: "0d1be17bc89ed3ff5001789c92df678b2e963a51b6fa2bdb467532cc9dbed390" 635 + url: "https://pub.dev" 636 + source: hosted 637 + version: "3.10.10" 574 638 fixnum: 575 639 dependency: transitive 576 640 description:
+3
pubspec.yaml
··· 12 12 13 13 dependencies: 14 14 any_link_preview: ^3.0.3 15 + app_badge_plus: ^1.1.5 15 16 assets: 16 17 path: assets 17 18 atproto: ^1.3.0 ··· 24 25 cached_network_image: ^3.3.1 25 26 camera: ^0.11.3 26 27 collection: ^1.19.1 28 + firebase_core: ^3.8.1 29 + firebase_messaging: ^15.1.6 27 30 fluentui_system_icons: ^1.1.273 28 31 flutter: 29 32 sdk: flutter
+6
widgetbook/macos/Flutter/GeneratedPluginRegistrant.swift
··· 5 5 import FlutterMacOS 6 6 import Foundation 7 7 8 + import app_badge_plus 8 9 import audioplayers_darwin 9 10 import desktop_webview_window 10 11 import file_selector_macos 12 + import firebase_core 13 + import firebase_messaging 11 14 import flutter_secure_storage_darwin 12 15 import flutter_web_auth_2 13 16 import fvp ··· 22 25 import window_to_front 23 26 24 27 func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { 28 + AppBadgePlusPlugin.register(with: registry.registrar(forPlugin: "AppBadgePlusPlugin")) 25 29 AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin")) 26 30 DesktopWebviewWindowPlugin.register(with: registry.registrar(forPlugin: "DesktopWebviewWindowPlugin")) 27 31 FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) 32 + FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) 33 + FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin")) 28 34 FlutterSecureStorageDarwinPlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStorageDarwinPlugin")) 29 35 FlutterWebAuth2Plugin.register(with: registry.registrar(forPlugin: "FlutterWebAuth2Plugin")) 30 36 FvpPlugin.register(with: registry.registrar(forPlugin: "FvpPlugin"))