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

Breaking upgrades to atproto & codegen deps (#99)

* upgrade codegen and atproto libs

* add util to replace jsonEncode(jsonDecode(...))

* small tweaks

* fix logout

* enforce lockfile and do full analysis

* oh thats not fair

* Update flutter_lint.yml

* one more

* fix lint for widgetbook

* Update flutter_lint.yml

* Update flutter_lint.yml

* implement suggestions

* back to lint only changed files

authored by

Roscoe Rubin-Rottenberg and committed by
GitHub
e4ad2f20 72290c12

+865 -742
+39 -72
.github/workflows/flutter_lint.yml
··· 3 3 on: 4 4 pull_request: 5 5 branches: [main] 6 - types: [opened, synchronize, reopened] # Run when PR is opened, updated, or reopened 6 + types: [opened, synchronize, reopened] 7 + 8 + permissions: 9 + contents: read 10 + pull-requests: write 7 11 8 12 jobs: 9 13 lint: ··· 22 26 files: | 23 27 **/*.dart 24 28 29 + - name: Get changed widgetbook Dart files 30 + id: widgetbook_changed_files 31 + uses: tj-actions/changed-files@v46 32 + with: 33 + files: | 34 + widgetbook/**/*.dart 35 + 25 36 - name: Set up Flutter 26 - if: steps.changed_files.outputs.all_changed_files != '' 37 + if: steps.changed_files.outputs.any_changed == 'true' 27 38 uses: subosito/flutter-action@v2 28 39 with: 29 40 channel: "stable" 30 - flutter-version: 3.35.7 41 + flutter-version: 3.38.4 31 42 cache: true 32 43 33 - - name: Generated code setup 34 - if: steps.changed_files.outputs.all_changed_files != '' 44 + - name: Codegen (app) 45 + if: steps.changed_files.outputs.any_changed == 'true' 35 46 run: | 36 - flutter pub get 47 + touch .env 48 + flutter pub get --enforce-lockfile 37 49 dart run build_runner build --delete-conflicting-outputs 38 50 39 - - name: Run Flutter Analyze 40 - id: analyze_step 41 - if: steps.changed_files.outputs.all_changed_files != '' 51 + - name: Codegen (widgetbook) 52 + if: steps.widgetbook_changed_files.outputs.any_changed == 'true' 42 53 run: | 43 - echo "Changed Dart files:" 44 - echo "${{ steps.changed_files.outputs.all_changed_files }}" 45 - 46 - FILTERED_FILES=$(printf "%s\n" "${{ steps.changed_files.outputs.all_changed_files }}" | tr ' ' '\n' | grep -v '^widgetbook/' | tr '\n' ' ' | sed 's/[[:space:]]*$//') 47 - 48 - if [ -z "$FILTERED_FILES" ]; then 49 - echo "Only widgetbook files changed. Skipping flutter analyze." 50 - exit 0 51 - fi 52 - 53 - echo "Analyzing:" 54 - echo "$FILTERED_FILES" 55 - # Run analyze and save output. Redirect stderr to stdout for capture. 56 - # Continue with '|| true' so the workflow doesn't stop if issues are found, 57 - # as we want to report them as annotations. 58 - flutter analyze $FILTERED_FILES > flutter_analyze_output.txt 2>&1 || true 59 - 60 - echo "--- Flutter Analyze Output (raw) ---" 61 - cat flutter_analyze_output.txt 62 - echo "------------------------------------" 54 + cd widgetbook 55 + dart run build_runner build --delete-conflicting-outputs 63 56 64 - - name: Process Analyze Output, Annotate & Check Errors 65 - if: steps.changed_files.outputs.all_changed_files != '' 57 + - name: Determine analyze targets 58 + id: analyze_targets 59 + if: steps.changed_files.outputs.any_changed == 'true' 66 60 run: | 67 - if [ ! -s flutter_analyze_output.txt ]; then 68 - echo "::debug::flutter_analyze_output.txt not found or is empty. No annotations to create or errors to check." 69 - exit 0 61 + if [ "${{ steps.widgetbook_changed_files.outputs.any_changed }}" = "true" ]; then 62 + echo "targets=." >> "$GITHUB_OUTPUT" 63 + else 64 + echo "targets=lib test" >> "$GITHUB_OUTPUT" 70 65 fi 71 66 72 - echo "::debug::Starting annotation processing and error checking from flutter_analyze_output.txt" 73 - found_any_issue="false" # Flag to track if any issues (info, warning, error) are found 67 + - name: Run Flutter Analyze 68 + id: analyze_step 69 + if: steps.changed_files.outputs.any_changed == 'true' 70 + run: flutter analyze --write=flutter_analyze.log ${{ steps.analyze_targets.outputs.targets }} 74 71 75 - while IFS= read -r line; do 76 - if [[ "$line" =~ ^[[:space:]]*(info|warning|error)[[:space:]]+•[[:space:]]+(.+)[[:space:]]+•[[:space:]]+([^:]+):([0-9]+):([0-9]+)[[:space:]]+•[[:space:]]+(.+)$ ]]; then 77 - found_any_issue="true" # Set flag if any issue is found 78 - type="${BASH_REMATCH[1]}" 79 - message_body="${BASH_REMATCH[2]}" 80 - file_path="${BASH_REMATCH[3]}" 81 - line_num="${BASH_REMATCH[4]}" 82 - col_num="${BASH_REMATCH[5]}" 83 - rule_id="${BASH_REMATCH[6]}" 84 - 85 - github_level="notice" # Default for 'info' 86 - if [ "$type" = "warning" ]; then 87 - github_level="warning" 88 - elif [ "$type" = "error" ]; then 89 - github_level="error" 90 - fi 91 - 92 - message_body_escaped="${message_body//'%'/'%25'}" 93 - message_body_escaped="${message_body_escaped//$'\r'/'%0D'}" 94 - message_body_escaped="${message_body_escaped//$'\n'/'%0A'}" 95 - 96 - echo "::$github_level file=$file_path,line=$line_num,col=$col_num,title=$rule_id::$message_body_escaped" 97 - elif [[ "$line" =~ issues\ found\.|\ Analyzing\ |^$|No\ issues\ found!|Looking\ for\ direct\ dependencies\ of|Running\ \"flutter\ pub\ get\"\ in ]]; then 98 - echo "::debug::Skipping known non-issue line: $line" 99 - else 100 - echo "::debug::Skipping unparseable line from flutter_analyze_output.txt: $line" 101 - fi 102 - done < flutter_analyze_output.txt 103 - echo "::debug::Finished annotation processing." 104 - 105 - if [ "$found_any_issue" = "true" ]; then 106 - echo "::error::Flutter analyze reported issues. See annotations for details. Failing workflow." 107 - exit 1 108 - else 109 - echo "::debug::No flutter analyze issues found. Workflow will pass." 110 - fi 72 + - name: Flutter Analyze Commenter 73 + if: ${{ !cancelled() && steps.changed_files.outputs.any_changed == 'true' }} # Run only when analyze executed 74 + uses: yorifuji/flutter-analyze-commenter@v1 75 + with: 76 + analyze-log: flutter_analyze.log 77 + verbose: false
+1
.gitignore
··· 43 43 /android/app/debug 44 44 /android/app/profile 45 45 /android/app/release 46 + /android/build/ 46 47 47 48 # Environment variables 48 49 .env
+1 -1
lib/src/core/auth/data/models/identity_info.dart
··· 5 5 6 6 /// Represents identity information in the AT Protocol 7 7 @freezed 8 - class IdentityInfo with _$IdentityInfo { 8 + abstract class IdentityInfo with _$IdentityInfo { 9 9 const factory IdentityInfo({ 10 10 /// Decentralized Identifier (DID) 11 11 required String did,
+1 -1
lib/src/core/auth/data/models/login_result.dart
··· 5 5 6 6 /// Result of a login attempt 7 7 @freezed 8 - class LoginResult with _$LoginResult { 8 + abstract class LoginResult with _$LoginResult { 9 9 const factory LoginResult({ 10 10 required LoginStatus status, 11 11 String? error,
+3 -3
lib/src/core/auth/data/models/onboarding_screen_state.dart
··· 1 1 import 'dart:typed_data'; 2 2 3 - import 'package:bluesky/bluesky.dart'; 3 + import 'package:bluesky/app_bsky_actor_profile.dart'; 4 4 import 'package:freezed_annotation/freezed_annotation.dart'; 5 5 6 6 part 'onboarding_screen_state.freezed.dart'; 7 7 8 8 @freezed 9 - class OnboardingScreenState with _$OnboardingScreenState { 9 + abstract class OnboardingScreenState with _$OnboardingScreenState { 10 10 const factory OnboardingScreenState({ 11 11 @Default(true) bool isLoading, 12 - ProfileRecord? bskyProfileRecord, 12 + ActorProfileRecord? bskyProfileRecord, 13 13 String? initialAvatarCid, 14 14 String? initialAvatarUrl, 15 15 Uint8List? localAvatarBytes,
+2 -2
lib/src/core/auth/data/repositories/onboarding_repository.dart
··· 1 - import 'package:bluesky/bluesky.dart'; 1 + import 'package:bluesky/app_bsky_actor_profile.dart'; 2 2 import 'package:sparksocial/src/core/network/atproto/data/models/graph_models.dart'; 3 3 4 4 abstract class OnboardingRepository { ··· 6 6 Future<bool> hasSparkProfile(); 7 7 8 8 /// Retrieves the Bluesky profile for import 9 - Future<ProfileRecord?> getBskyProfile(); 9 + Future<ActorProfileRecord?> getBskyProfile(); 10 10 11 11 /// Creates a Spark actor profile with custom values 12 12 Future<void> createSparkProfile({required String displayName, required String description, dynamic avatar});
+7 -6
lib/src/core/auth/data/repositories/onboarding_repository_impl.dart
··· 2 2 3 3 import 'package:atproto/atproto.dart'; 4 4 import 'package:atproto/core.dart'; 5 + import 'package:bluesky/app_bsky_actor_profile.dart'; 5 6 import 'package:bluesky/bluesky.dart' as bs; 6 7 import 'package:get_it/get_it.dart'; 7 8 import 'package:sparksocial/src/core/auth/data/repositories/auth_repository.dart'; ··· 30 31 final uri = AtUri.parse('at://${_session!.did}/so.sprk.actor.profile/self'); 31 32 try { 32 33 final response = await _repoRepository.getRecord(uri: uri); 33 - _logger.i('Spark profile found: ${response.record.value}'); 34 - return response.record.value.isNotEmpty; 34 + _logger.i('Spark profile found: ${response.record.toJson()}'); 35 + return response.record.toJson().isNotEmpty; 35 36 } catch (e) { 36 37 // Treat 404 and 'Could not locate record' 400 errors as no profile 37 38 final msg = e.toString().toLowerCase(); ··· 44 45 } 45 46 46 47 @override 47 - Future<bs.ProfileRecord?> getBskyProfile() async { 48 + Future<ActorProfileRecord?> getBskyProfile() async { 48 49 if (_session == null) return null; 49 50 50 51 try { 51 52 final uri = AtUri.parse('at://${_session!.did}/app.bsky.actor.profile/self'); 52 53 final response = await _repoRepository.getRecord(uri: uri); 53 - return bs.ProfileRecord.fromJson(response.record.value); 54 + return ActorProfileRecord.fromJson(response.record.toJson()); 54 55 } catch (e) { 55 56 _logger.i('Bluesky profile not found', error: e); 56 57 return null; ··· 93 94 }; 94 95 95 96 await _repoRepository.createRecord( 96 - collection: NSID.parse('so.sprk.actor.profile'), 97 + collection: 'so.sprk.actor.profile', 97 98 record: record, 98 99 rkey: 'self', 99 100 ); ··· 130 131 'createdAt': DateTime.now().toUtc().toIso8601String(), 131 132 }; 132 133 133 - final response = await _repoRepository.createRecord(collection: NSID.parse('so.sprk.graph.follow'), record: record); 134 + final response = await _repoRepository.createRecord(collection: 'so.sprk.graph.follow', record: record); 134 135 135 136 if (response.uri.toString().isEmpty) { 136 137 throw Exception('Failed to create Spark follow');
+1 -5
lib/src/core/media/create_media_actions.dart
··· 35 35 final result = await GetIt.I<ProVideoEditorRepository>().openVideoEditor(context, editorVideo); 36 36 if (result != null && context.mounted) { 37 37 await context.router.push( 38 - VideoReviewRoute( 39 - videoPath: result.video.path, 40 - storyMode: storyMode, 41 - soundRef: result.soundRef, 42 - ), 38 + VideoReviewRoute(videoPath: result.video.path, storyMode: storyMode, soundRef: result.soundRef), 43 39 ); 44 40 } 45 41 }
+68 -35
lib/src/core/network/atproto/data/adapters/bsky/feed_adapter.dart
··· 1 - import 'dart:convert'; 2 - 3 1 import 'package:atproto_core/atproto_core.dart'; 4 - import 'package:bluesky/bluesky.dart' as bsky; 2 + import 'package:bluesky/app_bsky_embed_images.dart'; 3 + import 'package:bluesky/app_bsky_feed_getpostthread.dart'; 4 + import 'package:bluesky/app_bsky_feed_post.dart'; 5 + import 'package:bluesky/app_bsky_richtext_facet.dart'; 5 6 // ignore: implementation_imports 6 - import 'package:bluesky/src/services/entities/converter/embed_converter.dart'; 7 - import 'package:sparksocial/src/core/network/atproto/data/models/models.dart'; 7 + import 'package:sparksocial/src/core/network/atproto/data/models/models.dart' hide ReplyRef; 8 + import 'package:sparksocial/src/core/utils/json_utils.dart'; 8 9 9 10 /// Adapter for Bluesky feed models <-> Spark feed models 10 11 /// ··· 103 104 if (rootType == 'com.atproto.repo.strongRef') { 104 105 // Leave as is 105 106 } else if (rootType == 'app.bsky.feed.defs#postView') { 106 - final postViewData = jsonDecode(jsonEncode(root)) as Map<String, dynamic>; 107 - postViewData.remove(r'$type'); 107 + final postViewData = deepCopyJson(root)..remove(r'$type'); 108 108 convertPostViewJson(postViewData, isNestedReply: true); 109 - root.removeWhere((key, value) => key != r'$type'); 110 - root['post'] = postViewData; 109 + root 110 + ..clear() 111 + ..addAll({r'$type': rootType, 'post': postViewData}); 111 112 } else if (rootType == 'app.bsky.feed.defs#notFoundPost' || rootType == 'app.bsky.feed.defs#blockedPost') { 112 113 // Already in correct format 113 114 } else if (root.containsKey('post')) { ··· 125 126 if (parentType == 'com.atproto.repo.strongRef') { 126 127 // Leave as is 127 128 } else if (parentType == 'app.bsky.feed.defs#postView') { 128 - final postViewData = jsonDecode(jsonEncode(parent)) as Map<String, dynamic>; 129 - postViewData.remove(r'$type'); 129 + final postViewData = deepCopyJson(parent)..remove(r'$type'); 130 130 convertPostViewJson(postViewData, isNestedReply: true); 131 - parent.removeWhere((key, value) => key != r'$type'); 132 - parent['post'] = postViewData; 131 + parent 132 + ..clear() 133 + ..addAll({r'$type': parentType, 'post': postViewData}); 133 134 } else if (parentType == 'app.bsky.feed.defs#notFoundPost' || parentType == 'app.bsky.feed.defs#blockedPost') { 134 135 // Already in correct format 135 136 } else if (parent.containsKey('post')) { ··· 165 166 // ============================================================================ 166 167 167 168 /// Convert Spark images to Bluesky images 168 - List<bsky.Image> convertImages(List<Image> sparkImages) { 169 + List<EmbedImagesImage> convertImages(List<Image> sparkImages) { 169 170 return sparkImages.map((sparkImage) { 170 - return bsky.Image( 171 + return EmbedImagesImage( 171 172 alt: sparkImage.alt ?? '', 172 173 image: sparkImage.image, 173 174 ); 174 175 }).toList(); 175 176 } 176 177 178 + /// Convert Spark Media JSON to Bluesky embed format 179 + /// Returns null if media is null or not supported for Bluesky 180 + UFeedPostEmbed? convertJsonToBskyEmbed(Map<String, dynamic> mediaJson) { 181 + final media = Media.fromJson(mediaJson); 182 + 183 + switch (media) { 184 + case MediaImage(:final image, :final alt): 185 + // Convert single Spark image to Bluesky embed images 186 + final bskyImage = EmbedImagesImage( 187 + alt: alt ?? '', 188 + image: image, 189 + ); 190 + return UFeedPostEmbed.embedImages(data: EmbedImages(images: [bskyImage])); 191 + 192 + case MediaImages(:final images): 193 + // Convert multiple Spark images to Bluesky embed images 194 + final bskyImages = convertImages(images); 195 + return UFeedPostEmbed.embedImages(data: EmbedImages(images: bskyImages)); 196 + 197 + case MediaBskyImages(:final images): 198 + // Already in Bluesky format, convert to embed 199 + final bskyImages = convertImages(images); 200 + return UFeedPostEmbed.embedImages(data: EmbedImages(images: bskyImages)); 201 + 202 + case MediaVideo(): 203 + case MediaBskyVideo(): 204 + case MediaBskyRecord(): 205 + case MediaBskyRecordWithMedia(): 206 + case MediaBskyExternal(): 207 + // Videos and other embed types are not supported for comments/replies 208 + return null; 209 + } 210 + } 211 + 177 212 /// Create a Bluesky post record 178 - bsky.PostRecord createPostRecord({ 213 + FeedPostRecord createPostRecord({ 179 214 required String text, 180 215 required DateTime createdAt, 181 - List<bsky.Image>? images, 182 - List<bsky.Facet>? facets, 216 + List<EmbedImagesImage>? images, 217 + List<RichtextFacet>? facets, 183 218 }) { 184 - return bsky.PostRecord( 219 + return FeedPostRecord( 185 220 text: text, 186 221 createdAt: createdAt, 187 - embed: images != null && images.isNotEmpty ? bsky.Embed.images(data: bsky.EmbedImages(images: images)) : null, 222 + embed: images != null && images.isNotEmpty ? UFeedPostEmbed.embedImages(data: EmbedImages(images: images)) : null, 188 223 facets: facets, 189 224 ); 190 225 } 191 226 192 227 /// Create Bluesky comment/reply record 193 - bsky.PostRecord createCommentRecord({ 228 + FeedPostRecord createCommentRecord({ 194 229 required String text, 195 230 required DateTime createdAt, 196 - required bsky.ReplyRef reply, 197 - bsky.Embed? embed, 231 + required RecordReplyRef reply, 232 + UFeedPostEmbed? embed, 198 233 }) { 199 - return bsky.PostRecord( 234 + return FeedPostRecord( 200 235 text: text, 201 236 createdAt: createdAt, 202 - reply: reply, 237 + reply: ReplyRef( 238 + root: reply.root, 239 + parent: reply.parent, 240 + ), 203 241 embed: embed, 204 242 ); 205 243 } 206 244 207 245 /// Create a link facet for Bluesky posts 208 - bsky.Facet createLinkFacet({ 246 + RichtextFacet createLinkFacet({ 209 247 required String linkUrl, 210 248 required int byteStart, 211 249 }) { 212 - return bsky.Facet( 213 - index: bsky.ByteSlice(byteStart: byteStart, byteEnd: byteStart + linkUrl.length), 214 - features: [bsky.FacetFeature.link(data: bsky.FacetLink(uri: linkUrl))], 250 + return RichtextFacet( 251 + index: RichtextFacetByteSlice(byteStart: byteStart, byteEnd: byteStart + linkUrl.length), 252 + features: [URichtextFacetFeatures.richtextFacetLink(data: RichtextFacetLink(uri: linkUrl))], 215 253 ); 216 254 } 217 255 218 256 /// Convert Bluesky thread to Spark thread 219 257 Thread convertBskyThreadToSparkThread({ 220 - required bsky.PostThreadView thread, 258 + required UFeedGetPostThreadThread thread, 221 259 required AtUri uri, 222 260 }) { 223 261 return Thread.fromBsky(thread: thread, uri: uri); 224 - } 225 - 226 - /// Convert media JSON to Bluesky embed 227 - bsky.Embed? convertJsonToBskyEmbed(Map<String, dynamic> mediaJson) { 228 - return embedConverter.fromJson(mediaJson); 229 262 } 230 263 } 231 264
+10 -9
lib/src/core/network/atproto/data/models/actor_models.dart
··· 1 - import 'package:atproto/atproto.dart'; 1 + import 'package:atproto/com_atproto_label_defs.dart'; 2 + import 'package:atproto/com_atproto_repo_strongref.dart'; 2 3 import 'package:atproto_core/atproto_core.dart'; 3 4 import 'package:freezed_annotation/freezed_annotation.dart'; 4 5 import 'package:sparksocial/src/core/utils/uri_converter.dart'; ··· 7 8 part 'actor_models.g.dart'; 8 9 9 10 @freezed 10 - class ActorViewer with _$ActorViewer { 11 + abstract class ActorViewer with _$ActorViewer { 11 12 @JsonSerializable(explicitToJson: true) 12 13 const factory ActorViewer({ 13 14 bool? muted, ··· 25 26 } 26 27 27 28 @freezed 28 - class KnownFollowers with _$KnownFollowers { 29 + abstract class KnownFollowers with _$KnownFollowers { 29 30 @JsonSerializable(explicitToJson: true) 30 31 const factory KnownFollowers({ 31 32 required int count, ··· 43 44 } 44 45 45 46 @freezed 46 - class ProfileViewBasic with _$ProfileViewBasic { 47 + abstract class ProfileViewBasic with _$ProfileViewBasic { 47 48 @JsonSerializable(explicitToJson: true) 48 49 const factory ProfileViewBasic({ 49 50 required String did, ··· 52 53 @UriConverter() Uri? avatar, 53 54 // associated: lists, feedgens, starterpacks, labelers, chat?? not needed for now 54 55 ActorViewer? viewer, 55 - List<StrongRef>? stories, 56 + List<RepoStrongRef>? stories, 56 57 }) = _ProfileViewBasic; 57 58 const ProfileViewBasic._(); 58 59 ··· 60 61 } 61 62 62 63 @freezed 63 - class ProfileView with _$ProfileView { 64 + abstract class ProfileView with _$ProfileView { 64 65 @JsonSerializable(explicitToJson: true) 65 66 const factory ProfileView({ 66 67 required String did, ··· 103 104 } 104 105 105 106 @freezed 106 - class ProfileViewDetailed with _$ProfileViewDetailed { 107 + abstract class ProfileViewDetailed with _$ProfileViewDetailed { 107 108 @JsonSerializable(explicitToJson: true) 108 109 const factory ProfileViewDetailed({ 109 110 required String did, ··· 120 121 // indexedAt and createdAt 121 122 ActorViewer? viewer, 122 123 List<Label>? labels, 123 - StrongRef? pinnedPost, // this is a list if the backend implements https://github.com/sprksocial/spark-back-end/issues/13 124 - List<StrongRef>? stories, 124 + RepoStrongRef? pinnedPost, // this is a list if the backend implements https://github.com/sprksocial/spark-back-end/issues/13 125 + List<RepoStrongRef>? stories, 125 126 }) = _ProfileViewDetailed; 126 127 const ProfileViewDetailed._(); 127 128
+88 -41
lib/src/core/network/atproto/data/models/feed_models.dart
··· 1 - import 'package:atproto/atproto.dart'; 1 + import 'package:atproto/com_atproto_label_defs.dart'; 2 2 import 'package:atproto_core/atproto_core.dart'; 3 - import 'package:bluesky/bluesky.dart' as bsky; 3 + import 'package:bluesky/app_bsky_feed_defs.dart' as bsky_defs; 4 + import 'package:bluesky/app_bsky_feed_getpostthread.dart'; 4 5 import 'package:flutter/foundation.dart'; 5 6 import 'package:freezed_annotation/freezed_annotation.dart'; 6 7 import 'package:sparksocial/src/core/network/atproto/data/adapters/bsky/feed_adapter.dart'; 7 8 import 'package:sparksocial/src/core/network/atproto/data/models/models.dart'; 9 + import 'package:sparksocial/src/core/utils/json_utils.dart'; 8 10 import 'package:sparksocial/src/core/utils/uri_converter.dart'; 9 11 10 12 part 'feed_models.freezed.dart'; 11 13 part 'feed_models.g.dart'; 12 14 15 + PostRecord _postRecordFromJson(dynamic json) { 16 + if (json is! Map<String, dynamic>) { 17 + throw Exception('Expected Map<String, dynamic> but got ${json.runtimeType}'); 18 + } 19 + final record = Record.fromJson(json); 20 + if (record is PostRecord) { 21 + return record; 22 + } 23 + throw Exception('Expected PostRecord but got ${record.runtimeType}'); 24 + } 25 + 26 + Map<String, dynamic> _postRecordToJson(PostRecord record) => record.toJson(); 27 + 28 + StoryRecord _storyRecordFromJson(dynamic json) { 29 + if (json is! Map<String, dynamic>) { 30 + throw Exception('Expected Map<String, dynamic> but got ${json.runtimeType}'); 31 + } 32 + final record = Record.fromJson(json); 33 + if (record is StoryRecord) { 34 + return record; 35 + } 36 + throw Exception('Expected StoryRecord but got ${record.runtimeType}'); 37 + } 38 + 39 + Map<String, dynamic> _storyRecordToJson(StoryRecord record) => record.toJson(); 40 + 13 41 /// https://pub.dev/packages/freezed#union-types <= read this to know how to use pattern matching to know the type of the object 14 42 @freezed 15 - class GeneratorViewerState with _$GeneratorViewerState { 43 + abstract class GeneratorViewerState with _$GeneratorViewerState { 16 44 @JsonSerializable(explicitToJson: true) 17 45 const factory GeneratorViewerState({ 18 46 @AtUriConverter() AtUri? like, ··· 23 51 } 24 52 25 53 @freezed 26 - class GeneratorView with _$GeneratorView { 54 + abstract class GeneratorView with _$GeneratorView { 27 55 @JsonSerializable(explicitToJson: true) 28 56 const factory GeneratorView({ 29 57 @AtUriConverter() required AtUri uri, ··· 47 75 48 76 /// The feeds that are actually used in the app 49 77 @freezed 50 - class Feed with _$Feed { 78 + abstract class Feed with _$Feed { 51 79 @JsonSerializable(explicitToJson: true) 52 80 factory Feed({ 53 81 required String type, ··· 61 89 62 90 /// Skeleton of a FeedView. Needs to be hydrated. 63 91 @freezed 64 - class SkeletonFeedPost with _$SkeletonFeedPost { 92 + abstract class SkeletonFeedPost with _$SkeletonFeedPost { 65 93 @JsonSerializable(explicitToJson: true) 66 94 const factory SkeletonFeedPost({ 67 95 @AtUriConverter() required AtUri uri, ··· 143 171 } 144 172 145 173 @freezed 146 - class FeedView with _$FeedView { 174 + abstract class FeedView with _$FeedView { 147 175 @JsonSerializable(explicitToJson: true) 148 176 const factory FeedView({ 149 177 required List<FeedViewPost> feed, ··· 155 183 } 156 184 157 185 @freezed 158 - class ReplyRef with _$ReplyRef { 186 + abstract class ReplyRef with _$ReplyRef { 159 187 @JsonSerializable(explicitToJson: true) 160 188 const factory ReplyRef({ 161 189 required ReplyRefPostReference root, // post, not found or blocked ··· 200 228 } 201 229 202 230 @freezed 203 - class BlockedAuthor with _$BlockedAuthor { 231 + abstract class BlockedAuthor with _$BlockedAuthor { 204 232 @JsonSerializable(explicitToJson: true) 205 233 const factory BlockedAuthor({required String did, Viewer? viewer}) = _BlockedAuthor; 206 234 const BlockedAuthor._(); ··· 209 237 } 210 238 211 239 @freezed 212 - class PostThread with _$PostThread { 240 + abstract class PostThread with _$PostThread { 213 241 @JsonSerializable(explicitToJson: true) 214 242 const factory PostThread({required PostView post, List<PostView>? parent, List<PostView>? replies}) = _PostThread; 215 243 const PostThread._(); ··· 218 246 } 219 247 220 248 @freezed 221 - class Viewer with _$Viewer { 249 + abstract class Viewer with _$Viewer { 222 250 @JsonSerializable(explicitToJson: true) 223 251 const factory Viewer({ 224 252 @AtUriConverter() AtUri? repost, ··· 234 262 } 235 263 236 264 @freezed 237 - class PostView with _$PostView { 265 + abstract class PostView with _$PostView { 238 266 @JsonSerializable(explicitToJson: true) 239 267 const factory PostView({ 240 268 @AtUriConverter() required AtUri uri, 241 269 required String cid, 242 270 required ProfileViewBasic author, 243 - required PostRecord record, 271 + @JsonKey(fromJson: _postRecordFromJson, toJson: _postRecordToJson) required PostRecord record, 244 272 required DateTime indexedAt, 245 273 @Default(false) bool isRepost, 246 274 int? likeCount, ··· 463 491 } 464 492 465 493 @freezed 466 - class EmbedViewExternal with _$EmbedViewExternal { 494 + abstract class EmbedViewExternal with _$EmbedViewExternal { 467 495 @JsonSerializable(explicitToJson: true) 468 496 const factory EmbedViewExternal({ 469 497 required String uri, ··· 518 546 } 519 547 520 548 @freezed 521 - class FeedSkeleton with _$FeedSkeleton { 549 + abstract class FeedSkeleton with _$FeedSkeleton { 522 550 @JsonSerializable(explicitToJson: true) 523 551 const factory FeedSkeleton({required List<SkeletonFeedPost> feed, String? cursor}) = _FeedSkeleton; 524 552 const FeedSkeleton._(); ··· 527 555 } 528 556 529 557 @freezed 530 - class ImageUploadResult with _$ImageUploadResult { 558 + abstract class ImageUploadResult with _$ImageUploadResult { 531 559 @JsonSerializable(explicitToJson: true) 532 560 const factory ImageUploadResult({required String fullsize, required String alt, required Map<String, dynamic> image}) = 533 561 _ImageUploadResult; ··· 537 565 } 538 566 539 567 @freezed 540 - class CaptionRef with _$CaptionRef { 568 + abstract class CaptionRef with _$CaptionRef { 541 569 @JsonSerializable(explicitToJson: true) 542 570 const factory CaptionRef({ 543 571 required String text, ··· 550 578 551 579 /// Represents the index range for a facet in the text 552 580 @freezed 553 - class FacetIndex with _$FacetIndex { 581 + abstract class FacetIndex with _$FacetIndex { 554 582 @JsonSerializable(explicitToJson: true) 555 583 const factory FacetIndex({ 556 584 /// Start index (inclusive) ··· 567 595 568 596 /// Represents a feature of a facet (mention, link, hashtag, etc.) 569 597 @Freezed(unionKey: r'$type') 570 - class FacetFeature with _$FacetFeature { 598 + abstract class FacetFeature with _$FacetFeature { 571 599 const FacetFeature._(); 572 600 573 601 // Spark facet feature types ··· 608 636 609 637 /// Represents a richtext facet for text formatting, mentions, links, etc. 610 638 @freezed 611 - class Facet with _$Facet { 639 + abstract class Facet with _$Facet { 612 640 @JsonSerializable(explicitToJson: true) 613 641 const factory Facet({ 614 642 /// Index range for the facet in the text ··· 624 652 } 625 653 626 654 @freezed 627 - class ViewImage with _$ViewImage { 655 + abstract class ViewImage with _$ViewImage { 628 656 @JsonSerializable(explicitToJson: true) 629 657 const factory ViewImage({ 630 658 @AtUriConverter() required Uri thumb, ··· 727 755 } 728 756 729 757 @Freezed(unionKey: r'$type') 730 - class Thread with _$Thread { 758 + sealed class Thread with _$Thread { 731 759 const Thread._(); 732 760 733 761 // NORMAL POST ··· 749 777 750 778 factory Thread.fromJson(Map<String, dynamic> json) => _$ThreadFromJson(json); 751 779 752 - factory Thread.fromBsky({required bsky.PostThreadView thread, required AtUri uri}) { 780 + static Thread? _convertParentToThread(bsky_defs.UThreadViewPostParent parent, AtUri uri) { 781 + switch (parent) { 782 + case bsky_defs.UThreadViewPostParentThreadViewPost(:final data): 783 + return Thread.fromBsky( 784 + thread: UFeedGetPostThreadThread.threadViewPost(data: data), 785 + uri: uri, 786 + ); 787 + case bsky_defs.UThreadViewPostParentNotFoundPost(:final data): 788 + return Thread.notFoundPost(uri: data.uri, notFound: true); 789 + case bsky_defs.UThreadViewPostParentBlockedPost(:final data): 790 + return Thread.blockedPost(uri: data.uri, blocked: true, author: BlockedAuthor.fromJson(data.author.toJson())); 791 + case bsky_defs.UThreadViewPostParentUnknown(): 792 + return null; 793 + } 794 + } 795 + 796 + factory Thread.fromBsky({required UFeedGetPostThreadThread thread, required AtUri uri}) { 753 797 switch (thread) { 754 - case bsky.UPostThreadViewRecord(:final data): 798 + case UFeedGetPostThreadThreadThreadViewPost(:final data): 755 799 try { 756 800 var embed = data.post.embed; 757 - if (data.post.embed is bsky.UEmbedViewExternal) { 801 + if (data.post.embed is bsky_defs.UPostViewEmbedEmbedExternalView) { 758 802 embed = null; 759 803 } 760 804 final postJson = data.post.copyWith(embed: embed); 761 805 762 - // Create PostView with safer parsing 763 - final postViewJson = postJson.toJson(); 806 + // Create PostView with deep copy - required because we modify nested structures like embeds 807 + final postViewJson = deepCopyJson(postJson.toJson()); 764 808 765 809 // Ensure required fields are not null 766 810 if (postViewJson['cid'] == null) { ··· 903 947 904 948 final thread = Thread.threadViewPost( 905 949 post: ThreadPost.post(post: PostView.fromJson(postViewJson)), 906 - parent: data.parent != null ? Thread.fromBsky(thread: data.parent!, uri: uri) : null, 950 + parent: data.parent != null ? _convertParentToThread(data.parent!, uri) : null, 907 951 replies: data.replies 908 952 ?.map((reply) { 909 953 switch (reply) { 910 - case bsky.UPostThreadViewRecord(:final data): 911 - return Thread.fromBsky(thread: reply, uri: data.post.uri); 912 - case bsky.UPostThreadViewNotFound(:final data): 954 + case bsky_defs.UThreadViewPostRepliesThreadViewPost(:final data): 955 + return Thread.fromBsky( 956 + thread: UFeedGetPostThreadThread.threadViewPost(data: data), 957 + uri: data.post.uri, 958 + ); 959 + case bsky_defs.UThreadViewPostRepliesNotFoundPost(:final data): 913 960 return Thread.notFoundPost(uri: data.uri, notFound: true); 914 - case bsky.UPostThreadViewBlocked(:final data): 961 + case bsky_defs.UThreadViewPostRepliesBlockedPost(:final data): 915 962 return Thread.blockedPost( 916 963 uri: data.uri, 917 964 blocked: true, 918 965 author: BlockedAuthor.fromJson(data.author.toJson()), 919 966 ); 920 - case bsky.UPostThreadViewUnknown(): 967 + case bsky_defs.UThreadViewPostRepliesUnknown(): 921 968 // Skip unknown reply types by returning null 922 969 return null; 923 970 } ··· 929 976 } catch (e) { 930 977 rethrow; 931 978 } 932 - case bsky.UPostThreadViewNotFound(): 933 - return Thread.notFoundPost(uri: uri, notFound: true); 934 - case bsky.UPostThreadViewBlocked(:final data): 935 - return Thread.blockedPost(uri: uri, blocked: true, author: BlockedAuthor.fromJson(data.author.toJson())); 979 + case UFeedGetPostThreadThreadNotFoundPost(:final data): 980 + return Thread.notFoundPost(uri: data.uri, notFound: true); 981 + case UFeedGetPostThreadThreadBlockedPost(:final data): 982 + return Thread.blockedPost(uri: data.uri, blocked: true, author: BlockedAuthor.fromJson(data.author.toJson())); 936 983 default: 937 984 throw Exception('Unsupported thread type: ${thread.runtimeType}'); 938 985 } ··· 1118 1165 } 1119 1166 1120 1167 @freezed 1121 - class ThreadContext with _$ThreadContext { 1168 + abstract class ThreadContext with _$ThreadContext { 1122 1169 @JsonSerializable(explicitToJson: true) 1123 1170 const factory ThreadContext({@AtUriConverter() AtUri? rootAuthorLike}) = _ThreadContext; 1124 1171 const ThreadContext._(); ··· 1127 1174 } 1128 1175 1129 1176 @freezed 1130 - class ReplyView with _$ReplyView { 1177 + abstract class ReplyView with _$ReplyView { 1131 1178 @JsonSerializable(explicitToJson: true) 1132 1179 const factory ReplyView({ 1133 1180 @AtUriConverter() required AtUri uri, ··· 1229 1276 } 1230 1277 1231 1278 @freezed 1232 - class StoryView with _$StoryView { 1279 + abstract class StoryView with _$StoryView { 1233 1280 @JsonSerializable(explicitToJson: true) 1234 1281 const factory StoryView({ 1235 1282 required String cid, 1236 1283 @AtUriConverter() required AtUri uri, 1237 1284 required ProfileViewBasic author, 1238 - required StoryRecord record, 1285 + @JsonKey(fromJson: _storyRecordFromJson, toJson: _storyRecordToJson) required StoryRecord record, 1239 1286 required DateTime indexedAt, 1240 1287 MediaView? media, 1241 1288 // viewer eventually i think
+3 -3
lib/src/core/network/atproto/data/models/graph_models.dart
··· 5 5 part 'graph_models.g.dart'; 6 6 7 7 @freezed 8 - class FollowersResponse with _$FollowersResponse { 8 + abstract class FollowersResponse with _$FollowersResponse { 9 9 const factory FollowersResponse({required List<ProfileView> followers, String? cursor}) = _FollowersResponse; 10 10 11 11 factory FollowersResponse.fromJson(Map<String, dynamic> json) => _$FollowersResponseFromJson(json); 12 12 } 13 13 14 14 @freezed 15 - class FollowsResponse with _$FollowsResponse { 15 + abstract class FollowsResponse with _$FollowsResponse { 16 16 const factory FollowsResponse({required List<ProfileView> follows, String? cursor}) = _FollowsResponse; 17 17 18 18 factory FollowsResponse.fromJson(Map<String, dynamic> json) => _$FollowsResponseFromJson(json); 19 19 } 20 20 21 21 @freezed 22 - class FollowUserResponse with _$FollowUserResponse { 22 + abstract class FollowUserResponse with _$FollowUserResponse { 23 23 const factory FollowUserResponse({required String uri, required String cid}) = _FollowUserResponse; 24 24 25 25 factory FollowUserResponse.fromJson(Map<String, dynamic> json) => _$FollowUserResponseFromJson(json);
+1 -1
lib/src/core/network/atproto/data/models/labeler_models.dart
··· 1 - import 'package:atproto/atproto.dart'; 1 + import 'package:atproto/com_atproto_label_defs.dart'; 2 2 import 'package:atproto_core/atproto_core.dart'; 3 3 import 'package:freezed_annotation/freezed_annotation.dart'; 4 4 import 'package:sparksocial/src/core/network/atproto/data/models/actor_models.dart';
+1 -1
lib/src/core/network/atproto/data/models/models.dart
··· 3 3 export 'package:sparksocial/src/core/network/atproto/data/models/graph_models.dart'; 4 4 export 'package:sparksocial/src/core/network/atproto/data/models/labeler_models.dart'; 5 5 export 'package:sparksocial/src/core/network/atproto/data/models/pref_models.dart'; 6 - export 'package:sparksocial/src/core/network/atproto/data/models/records.dart'; 6 + export 'package:sparksocial/src/core/network/atproto/data/models/record_models.dart'; 7 7 export 'package:sparksocial/src/core/network/atproto/data/models/sound_models.dart';
+7 -7
lib/src/core/network/atproto/data/models/pref_models.dart
··· 5 5 part 'pref_models.g.dart'; 6 6 7 7 @freezed 8 - class Preferences with _$Preferences { 8 + abstract class Preferences with _$Preferences { 9 9 @JsonSerializable(explicitToJson: true) 10 10 factory Preferences({ 11 11 required List<Preference> preferences, ··· 92 92 }) = _Preferences; 93 93 const Preferences._(); 94 94 95 - factory Preferences.fromJson(Map<String, dynamic> json) => _$$PreferencesImplFromJson(json); 95 + factory Preferences.fromJson(Map<String, dynamic> json) => _$PreferencesFromJson(json); 96 96 } 97 97 98 98 @Freezed(unionKey: r'$type') 99 - class Preference with _$Preference { 99 + abstract class Preference with _$Preference { 100 100 const Preference._(); 101 101 102 102 @FreezedUnionValue('so.sprk.actor.defs#contentLabelPref') ··· 182 182 } 183 183 184 184 @freezed 185 - class SavedFeed with _$SavedFeed { 185 + abstract class SavedFeed with _$SavedFeed { 186 186 factory SavedFeed({ 187 187 required String type, 188 188 required String value, ··· 212 212 } 213 213 214 214 @freezed 215 - class MutedWord with _$MutedWord { 215 + abstract class MutedWord with _$MutedWord { 216 216 @JsonSerializable(explicitToJson: true) 217 217 const factory MutedWord({ 218 218 required String value, ··· 225 225 } 226 226 227 227 @freezed 228 - class LabelerPrefItem with _$LabelerPrefItem { 228 + abstract class LabelerPrefItem with _$LabelerPrefItem { 229 229 @JsonSerializable(explicitToJson: true) 230 230 const factory LabelerPrefItem({ 231 231 required String did, ··· 236 236 } 237 237 238 238 @freezed 239 - class ContentLabelPref with _$ContentLabelPref { 239 + abstract class ContentLabelPref with _$ContentLabelPref { 240 240 @JsonSerializable(explicitToJson: true) 241 241 const factory ContentLabelPref({ 242 242 required String labelerDid,
+15 -14
lib/src/core/network/atproto/data/models/records.dart lib/src/core/network/atproto/data/models/record_models.dart
··· 1 - import 'package:atproto/atproto.dart'; 1 + import 'package:atproto/com_atproto_label_defs.dart'; 2 + import 'package:atproto/com_atproto_repo_strongref.dart'; 2 3 import 'package:atproto/core.dart'; 3 4 import 'package:freezed_annotation/freezed_annotation.dart'; 4 5 import 'package:sparksocial/src/core/network/atproto/data/models/models.dart'; 5 6 6 - part 'records.freezed.dart'; 7 - part 'records.g.dart'; 7 + part 'record_models.freezed.dart'; 8 + part 'record_models.g.dart'; 8 9 9 10 @Freezed(unionKey: r'$type') 10 - class Record with _$Record { 11 + abstract class Record with _$Record { 11 12 factory Record.fromJson(Map<String, dynamic> json) => _$RecordFromJson(json); 12 13 const Record._(); 13 14 @JsonSerializable(explicitToJson: true) ··· 20 21 List<String>? tags, 21 22 List<SelfLabel>? selfLabels, 22 23 Media? media, 23 - StrongRef? sound, 24 + RepoStrongRef? sound, 24 25 }) = PostRecord; 25 26 26 27 @JsonSerializable(explicitToJson: true) ··· 39 40 const factory Record.story({ 40 41 required Media media, 41 42 required DateTime createdAt, 42 - StrongRef? sound, 43 + RepoStrongRef? sound, 43 44 List<SelfLabel>? labels, 44 45 List<String>? tags, 45 46 }) = StoryRecord; ··· 52 53 Blob? avatar, 53 54 Blob? banner, 54 55 List<SelfLabel>? selfLabels, 55 - StrongRef? joinedViaStarterPack, 56 - StrongRef? pinnedPost, 56 + RepoStrongRef? joinedViaStarterPack, 57 + RepoStrongRef? pinnedPost, 57 58 DateTime? createdAt, 58 59 }) = ProfileRecord; 59 60 ··· 63 64 required Blob sound, 64 65 required String title, 65 66 required DateTime createdAt, 66 - StrongRef? origin, 67 + RepoStrongRef? origin, 67 68 AudioDetails? details, 68 69 List<SelfLabel>? labels, 69 70 }) = AudioRecord; ··· 103 104 104 105 /// Skeleton of a ReplyRef. Needs to be hydrated. 105 106 @freezed 106 - class RecordReplyRef with _$RecordReplyRef { 107 + abstract class RecordReplyRef with _$RecordReplyRef { 107 108 @JsonSerializable(explicitToJson: true) 108 - const factory RecordReplyRef({required StrongRef root, required StrongRef parent}) = _RecordReplyRef; 109 + const factory RecordReplyRef({required RepoStrongRef root, required RepoStrongRef parent}) = _RecordReplyRef; 109 110 const RecordReplyRef._(); 110 111 111 112 factory RecordReplyRef.fromJson(Map<String, dynamic> json) => _$RecordReplyRefFromJson(json); ··· 148 149 149 150 @FreezedUnionValue('app.bsky.embed.record') 150 151 @JsonSerializable(explicitToJson: true) 151 - const factory Media.bskyRecord({required StrongRef record}) = MediaBskyRecord; 152 + const factory Media.bskyRecord({required RepoStrongRef record}) = MediaBskyRecord; 152 153 153 154 @FreezedUnionValue('app.bsky.embed.recordWithMedia') 154 155 @JsonSerializable(explicitToJson: true) ··· 167 168 } 168 169 169 170 @freezed 170 - class EmbedExternal with _$EmbedExternal { 171 + abstract class EmbedExternal with _$EmbedExternal { 171 172 @JsonSerializable(explicitToJson: true) 172 173 const factory EmbedExternal({ 173 174 required String uri, ··· 181 182 } 182 183 183 184 @freezed 184 - class Image with _$Image { 185 + abstract class Image with _$Image { 185 186 @JsonSerializable(explicitToJson: true) 186 187 const factory Image({ 187 188 required Blob image,
+19 -6
lib/src/core/network/atproto/data/models/sound_models.dart
··· 1 - import 'package:atproto/atproto.dart'; 1 + import 'package:atproto/com_atproto_label_defs.dart'; 2 2 import 'package:atproto_core/atproto_core.dart'; 3 3 import 'package:freezed_annotation/freezed_annotation.dart'; 4 4 import 'package:sparksocial/src/core/network/atproto/data/models/models.dart'; ··· 7 7 part 'sound_models.freezed.dart'; 8 8 part 'sound_models.g.dart'; 9 9 10 + AudioRecord _audioRecordFromJson(dynamic json) { 11 + if (json is! Map<String, dynamic>) { 12 + throw Exception('Expected Map<String, dynamic> but got ${json.runtimeType}'); 13 + } 14 + final record = Record.fromJson(json); 15 + if (record is AudioRecord) { 16 + return record; 17 + } 18 + throw Exception('Expected AudioRecord but got ${record.runtimeType}'); 19 + } 20 + 21 + Map<String, dynamic> _audioRecordToJson(AudioRecord record) => record.toJson(); 22 + 10 23 @freezed 11 - class AudioDetails with _$AudioDetails { 24 + abstract class AudioDetails with _$AudioDetails { 12 25 @JsonSerializable(explicitToJson: true) 13 26 const factory AudioDetails({ 14 27 String? artist, ··· 19 32 } 20 33 21 34 @freezed 22 - class AudioView with _$AudioView { 35 + abstract class AudioView with _$AudioView { 23 36 @JsonSerializable(explicitToJson: true) 24 37 const factory AudioView({ 25 38 @AtUriConverter() required AtUri uri, 26 39 required String cid, 27 40 required ProfileViewBasic author, 28 - required AudioRecord record, 41 + @JsonKey(fromJson: _audioRecordFromJson, toJson: _audioRecordToJson) required AudioRecord record, 29 42 required String title, 30 43 @UriConverter() required Uri coverArt, 31 44 required DateTime indexedAt, ··· 40 53 } 41 54 42 55 @freezed 43 - class AudioPostsResponse with _$AudioPostsResponse { 56 + abstract class AudioPostsResponse with _$AudioPostsResponse { 44 57 @JsonSerializable(explicitToJson: true) 45 58 const factory AudioPostsResponse({ 46 59 required List<PostView> posts, ··· 52 65 } 53 66 54 67 @freezed 55 - class TrendingAudiosResponse with _$TrendingAudiosResponse { 68 + abstract class TrendingAudiosResponse with _$TrendingAudiosResponse { 56 69 @JsonSerializable(explicitToJson: true) 57 70 const factory TrendingAudiosResponse({ 58 71 required List<AudioView> audios,
+3 -1
lib/src/core/network/atproto/data/repositories/actor_repository_impl.dart
··· 107 107 } 108 108 109 109 await atproto.repo.putRecord( 110 - uri: AtUri.parse('at://${_client.authRepository.session!.did}/so.sprk.actor.profile/self'), 110 + repo: _client.authRepository.session!.did, 111 + collection: 'so.sprk.actor.profile', 112 + rkey: 'self', 111 113 record: record.toJson(), 112 114 ); 113 115 }
+11 -5
lib/src/core/network/atproto/data/repositories/feed_repository.dart
··· 1 - import 'package:atproto/atproto.dart'; 1 + import 'package:atproto/com_atproto_label_defs.dart'; 2 + import 'package:atproto/com_atproto_repo_strongref.dart'; 2 3 import 'package:atproto_core/atproto_core.dart'; 3 4 import 'package:image_picker/image_picker.dart'; 4 5 import 'package:sparksocial/src/core/network/atproto/data/models/models.dart'; ··· 73 74 /// 74 75 /// [postCid] The String of the post to like 75 76 /// [postUri] The URI of the post to like 76 - Future<StrongRef> likePost(String postCid, AtUri postUri); 77 + Future<RepoStrongRef> likePost(String postCid, AtUri postUri); 77 78 78 79 /// Unlike a post 79 80 /// ··· 95 96 /// [rootUri] The URI of the root post (optional, defaults to parent if not provided) 96 97 /// [imageFiles] List of image files to attach (optional) 97 98 /// [altTexts] Map of file paths to alt texts (optional) 98 - Future<StrongRef> postComment( 99 + Future<RepoStrongRef> postComment( 99 100 String text, 100 101 String parentCid, 101 102 AtUri parentUri, { ··· 111 112 /// [imageFiles] List of image files to attach 112 113 /// [altTexts] Map of file paths to alt texts 113 114 /// [crosspostToBsky] Whether to also post to Bluesky 114 - Future<StrongRef> postImages(String text, List<XFile> imageFiles, Map<String, String> altTexts, {bool crosspostToBsky = false}); 115 + Future<RepoStrongRef> postImages( 116 + String text, 117 + List<XFile> imageFiles, 118 + Map<String, String> altTexts, { 119 + bool crosspostToBsky = false, 120 + }); 115 121 116 122 /// Upload images to the server 117 123 /// ··· 133 139 /// [tags] The tags of the video 134 140 /// [langs] The languages of the video 135 141 /// [selfLabels] The self labels of the video 136 - Future<StrongRef> postVideo( 142 + Future<RepoStrongRef> postVideo( 137 143 Blob blob, { 138 144 String text = '', 139 145 String alt = '',
+66 -36
lib/src/core/network/atproto/data/repositories/feed_repository_impl.dart
··· 2 2 import 'dart:io'; 3 3 import 'dart:typed_data'; 4 4 5 - import 'package:atproto/atproto.dart'; 5 + import 'package:atproto/com_atproto_label_defs.dart'; 6 + import 'package:atproto/com_atproto_repo_strongref.dart'; 6 7 import 'package:atproto/core.dart'; 8 + import 'package:bluesky/app_bsky_feed_getauthorfeed.dart'; 9 + import 'package:bluesky/app_bsky_richtext_facet.dart'; 7 10 import 'package:bluesky/bluesky.dart' as bsky; 8 11 import 'package:get_it/get_it.dart'; 9 12 import 'package:http/http.dart' as http; ··· 15 18 import 'package:sparksocial/src/core/network/atproto/data/models/models.dart'; 16 19 import 'package:sparksocial/src/core/network/atproto/data/repositories/feed_repository.dart'; 17 20 import 'package:sparksocial/src/core/network/atproto/data/repositories/sprk_repository.dart'; 21 + import 'package:sparksocial/src/core/utils/json_utils.dart'; 18 22 import 'package:sparksocial/src/core/utils/logging/log_service.dart'; 19 23 import 'package:sparksocial/src/core/utils/logging/logger.dart'; 20 24 ··· 115 119 final posts = await blueskyClient.feed.getPosts(uris: uris); 116 120 117 121 // Convert Bluesky posts using adapter 118 - final rawPosts = posts.data.posts.map((post) => post.toJson()).toList(); 122 + // Create mutable copies to avoid "Cannot modify unmodifiable map" errors 123 + final rawPosts = posts.data.posts.map((post) { 124 + final json = post.toJson(); 125 + return deepCopyJson(json); 126 + }).toList(); 119 127 120 128 for (final rawPost in rawPosts) { 121 129 bskyFeedAdapter.convertPostViewJson(rawPost); ··· 255 263 actor: actorUri.hostname, 256 264 limit: limit, 257 265 cursor: cursor, 258 - filter: videosOnly ? bsky.FeedFilter.postsWithVideo : bsky.FeedFilter.postsWithMedia, 266 + filter: FeedGetAuthorFeedFilter.valueOf(videosOnly ? 'posts_with_video' : 'posts_with_media'), 259 267 ); 260 268 261 - final rawFeed = resultBsky.data.feed.map((feedView) => feedView.toJson()).toList(); 269 + // Create mutable copies of the JSON maps since toJson() returns unmodifiable maps 270 + final rawFeed = resultBsky.data.feed.map((feedView) { 271 + final json = feedView.toJson(); 272 + // Deep copy to make it mutable 273 + return deepCopyJson(json); 274 + }).toList(); 262 275 263 276 // Use adapter to convert Bluesky JSON to Spark structure 264 277 final feedPosts = <FeedViewPost>[]; ··· 672 685 } 673 686 674 687 @override 675 - Future<StrongRef> likePost(String postCid, AtUri postUri) async { 688 + Future<RepoStrongRef> likePost(String postCid, AtUri postUri) async { 676 689 _logger.d('Liking post with String: $postCid, URI: $postUri'); 677 690 return _client.executeWithRetry(() async { 678 691 if (!_client.authRepository.isAuthenticated) { ··· 689 702 // Determine if this is a Bluesky post or Spark post 690 703 final isBskyPost = postUri.collection.toString().startsWith('app.bsky.feed.post'); 691 704 final likeType = isBskyPost ? 'app.bsky.feed.like' : 'so.sprk.feed.like'; 692 - final likeCollection = NSID.parse(likeType); 705 + final likeCollection = likeType; 693 706 694 707 _logger.d('Post type: ${isBskyPost ? 'Bluesky' : 'Spark'}, using collection: $likeType'); 695 708 ··· 699 712 'createdAt': DateTime.now().toUtc().toIso8601String(), 700 713 }; 701 714 702 - final result = await atproto.repo.createRecord(collection: likeCollection, record: likeRecord); 715 + final result = await atproto.repo.createRecord(repo: _client.sprkDid, collection: likeCollection, record: likeRecord); 703 716 704 717 _logger.i('Post liked successfully: ${result.data.uri}'); 705 718 706 - return result.data; 719 + return result.data as RepoStrongRef; 707 720 }); 708 721 } 709 722 ··· 722 735 throw Exception('AtProto not initialized'); 723 736 } 724 737 725 - await atproto.repo.deleteRecord(uri: likeUri); 738 + await atproto.repo.deleteRecord(repo: _client.sprkDid, collection: likeUri.collection.toString(), rkey: likeUri.rkey); 726 739 _logger.i('Post unliked successfully'); 727 740 }); 728 741 } 729 742 730 743 @override 731 - Future<StrongRef> postComment( 744 + Future<RepoStrongRef> postComment( 732 745 String text, 733 746 String parentCid, 734 747 AtUri parentUri, { ··· 783 796 final sprkRecord = ReplyRecord( 784 797 caption: CaptionRef(text: text, facets: []), 785 798 reply: RecordReplyRef( 786 - root: StrongRef(uri: effectiveRootUri, cid: effectiveRootCid), 787 - parent: StrongRef(uri: parentUri, cid: parentCid), 799 + root: RepoStrongRef(uri: effectiveRootUri, cid: effectiveRootCid), 800 + parent: RepoStrongRef(uri: parentUri, cid: parentCid), 788 801 ), 789 802 createdAt: DateTime.now().toUtc(), 790 803 media: media, ··· 793 806 collection = NSID.parse('so.sprk.feed.reply'); 794 807 } else { 795 808 // Bluesky comment - use adapter to create Bluesky-specific models 796 - final bskyMedia = mediaJson != null ? bskyFeedAdapter.convertJsonToBskyEmbed(mediaJson) : null; 797 - 798 - // Validate that videos are not allowed in replies 799 - if (bskyMedia != null && bskyMedia is bsky.UEmbedVideo) { 800 - _logger.e('Videos are not allowed in replies'); 801 - throw Exception('Videos are not allowed in replies'); 809 + // Validate that videos are not allowed in replies before conversion 810 + if (mediaJson != null) { 811 + final media = Media.fromJson(mediaJson); 812 + if (media is MediaVideo || media is MediaBskyVideo) { 813 + _logger.e('Videos are not allowed in replies'); 814 + throw Exception('Videos are not allowed in replies'); 815 + } 802 816 } 803 817 818 + final bskyMedia = mediaJson != null ? bskyFeedAdapter.convertJsonToBskyEmbed(mediaJson) : null; 819 + 804 820 final bskyRecord = bskyFeedAdapter.createCommentRecord( 805 821 text: text, 806 822 createdAt: DateTime.now().toUtc(), 807 - reply: bsky.ReplyRef( 808 - root: StrongRef(uri: effectiveRootUri, cid: effectiveRootCid), 809 - parent: StrongRef(uri: parentUri, cid: parentCid), 823 + reply: RecordReplyRef( 824 + root: RepoStrongRef(uri: effectiveRootUri, cid: effectiveRootCid), 825 + parent: RepoStrongRef(uri: parentUri, cid: parentCid), 810 826 ), 811 827 embed: bskyMedia, 812 828 ); ··· 814 830 collection = NSID.parse('app.bsky.feed.post'); 815 831 } 816 832 817 - final result = await atproto.repo.createRecord(collection: collection, record: recordJson); 833 + final result = await atproto.repo.createRecord( 834 + repo: _client.sprkDid, 835 + collection: collection.toString(), 836 + record: recordJson, 837 + ); 818 838 819 839 _logger.i('Comment posted successfully: ${result.data.uri}'); 820 840 821 - return result.data; 841 + return result.data as RepoStrongRef; 822 842 } 823 843 }); 824 844 } 825 845 826 846 @override 827 - Future<StrongRef> postImages( 847 + Future<RepoStrongRef> postImages( 828 848 String text, 829 849 List<XFile> imageFiles, 830 850 Map<String, String> altTexts, { ··· 851 871 createdAt: DateTime.now().toUtc(), 852 872 ); 853 873 854 - final result = await atproto.repo.createRecord(collection: NSID.parse('so.sprk.feed.post'), record: record.toJson()); 874 + final result = await atproto.repo.createRecord( 875 + repo: _client.sprkDid, 876 + collection: NSID.parse('so.sprk.feed.post').toString(), 877 + record: record.toJson(), 878 + ); 855 879 856 880 _logger.i('Image post created successfully: ${result.data.uri}'); 857 881 858 882 // Crosspost to Bluesky if enabled 859 883 if (crosspostToBsky) { 860 884 try { 861 - await _crosspostToBlueSky(text, uploadedImageMaps, result.data, altTexts); 885 + await _crosspostToBlueSky(text, uploadedImageMaps, result.data as RepoStrongRef, altTexts); 862 886 } catch (e) { 863 887 _logger.w('Failed to crosspost to Bluesky: $e'); 864 888 // Don't fail the entire operation if Bluesky crossposting fails 865 889 } 866 890 } 867 891 868 - return result.data; 892 + return result.data as RepoStrongRef; 869 893 } else { 870 894 _logger.e('AtProto not initialized'); 871 895 throw Exception('AtProto not initialized'); ··· 901 925 _logger.e('AtProto not initialized'); 902 926 throw Exception('AtProto not initialized'); 903 927 case final atproto: 904 - final response = await atproto.repo.uploadBlob(processedBytes); 928 + final response = await atproto.repo.uploadBlob(bytes: processedBytes); 905 929 906 930 switch (response.status.code) { 907 931 case 200: ··· 960 984 final pdsService = authAtProto.service; 961 985 final serviceTokenRes = await authAtProto.server.getServiceAuth( 962 986 aud: 'did:web:$pdsService', 963 - lxm: NSID.parse('com.atproto.repo.uploadBlob'), 987 + lxm: 'com.atproto.repo.uploadBlob', 964 988 exp: DateTime.now().toUtc().add(const Duration(minutes: 5)).millisecondsSinceEpoch ~/ 1000, 965 989 ); 966 990 ··· 1049 1073 Future<void> _crosspostToBlueSky( 1050 1074 String text, 1051 1075 List<Image> sparkImages, 1052 - StrongRef sparkPostData, 1076 + RepoStrongRef sparkPostData, 1053 1077 Map<String, String> altTexts, 1054 1078 ) async { 1055 1079 _logger.d('Crossposting to Bluesky with ${sparkImages.length} images'); ··· 1062 1086 1063 1087 // Determine if we need to add a link to the Spark post 1064 1088 String? linkUrl; 1065 - List<bsky.Facet>? facets; 1089 + List<RichtextFacet>? facets; 1066 1090 1067 1091 if (sparkImages.length > maxBskyImages) { 1068 1092 final sparkRkey = sparkPostData.uri.rkey; ··· 1092 1116 1093 1117 final bskyAtProto = _client.authRepository.atproto!; 1094 1118 final bskyResult = await bskyAtProto.repo.createRecord( 1095 - collection: NSID.parse('app.bsky.feed.post'), 1119 + repo: _client.sprkDid, 1120 + collection: 'app.bsky.feed.post', 1096 1121 record: bskyPost.toJson(), 1097 1122 rkey: sparkPostData.uri.rkey, 1098 1123 ); ··· 1144 1169 } 1145 1170 1146 1171 try { 1147 - final response = await atproto.repo.deleteRecord(uri: postUri); 1172 + final response = await atproto.repo.deleteRecord( 1173 + repo: _client.sprkDid, 1174 + collection: postUri.collection.toString(), 1175 + rkey: postUri.rkey, 1176 + ); 1148 1177 1149 1178 switch (response.status.code) { 1150 1179 case 200: ··· 1162 1191 } 1163 1192 1164 1193 @override 1165 - Future<StrongRef> postVideo( 1194 + Future<RepoStrongRef> postVideo( 1166 1195 Blob blob, { 1167 1196 String text = '', 1168 1197 String alt = '', ··· 1189 1218 1190 1219 // Create the post record 1191 1220 final response = await _client.authRepository.atproto!.repo.createRecord( 1192 - collection: NSID.parse('so.sprk.feed.post'), 1221 + repo: _client.sprkDid, 1222 + collection: 'so.sprk.feed.post', 1193 1223 record: record.toJson(), 1194 1224 ); 1195 1225 1196 1226 if (response.status == HttpStatus.ok) { 1197 1227 _logger.i('Video posted successfully: ${response.data.uri}'); 1198 - return response.data; 1228 + return response.data as RepoStrongRef; 1199 1229 } else { 1200 1230 _logger.e('Failed to post video: ${response.status} ${response.data}'); 1201 1231 throw Exception('Failed to post video: ${response.status} ${response.data}');
+9 -6
lib/src/core/network/atproto/data/repositories/graph_repository_impl.dart
··· 107 107 throw Exception('Session DID not available'); 108 108 } 109 109 110 - final collection = NSID.parse('so.sprk.graph.follow'); 111 - const recordType = 'so.sprk.graph.follow'; 110 + const collection = 'so.sprk.graph.follow'; 112 111 113 112 try { 114 113 _logger.d('Checking if already following user: $did'); ··· 121 120 throw Exception('Already following this user'); 122 121 } 123 122 124 - final followRecord = {r'$type': recordType, 'subject': did, 'createdAt': DateTime.now().toUtc().toIso8601String()}; 123 + final followRecord = {r'$type': collection, 'subject': did, 'createdAt': DateTime.now().toUtc().toIso8601String()}; 125 124 126 - final result = await atproto.repo.createRecord(collection: collection, record: followRecord); 125 + final result = await atproto.repo.createRecord(repo: sessionDid, collection: collection, record: followRecord); 127 126 128 - _logger.i('User followed successfully with $recordType: ${result.data.uri}'); 127 + _logger.i('User followed successfully with $collection: ${result.data.uri}'); 129 128 130 129 return FollowUserResponse(uri: result.data.uri.toString(), cid: result.data.cid); 131 130 } catch (e) { ··· 150 149 throw Exception('AtProto not initialized'); 151 150 } 152 151 153 - await atproto.repo.deleteRecord(uri: followUri); 152 + await atproto.repo.deleteRecord( 153 + repo: followUri.hostname, 154 + collection: followUri.collection.toString(), 155 + rkey: followUri.rkey, 156 + ); 154 157 _logger.i('User unfollowed successfully'); 155 158 }); 156 159 }
+9 -11
lib/src/core/network/atproto/data/repositories/repo_repository.dart
··· 1 1 import 'dart:typed_data'; 2 - import 'package:atproto/atproto.dart'; 2 + import 'package:atproto/com_atproto_moderation_createreport.dart'; 3 + import 'package:atproto/com_atproto_repo_strongref.dart'; 4 + import 'package:atproto/com_atproto_services.dart'; 3 5 import 'package:atproto/core.dart'; 6 + import 'package:sparksocial/src/core/network/atproto/data/models/record_models.dart'; 4 7 5 8 /// Interface for Repository-related API endpoints 6 9 abstract class RepoRepository { 7 10 /// Get a record from the repository 8 - Future<({Record record, StrongRef strongRef})> getRecord({required AtUri uri}); 11 + Future<({Record record, RepoStrongRef strongRef})> getRecord({required AtUri uri}); 9 12 10 13 /// Edit a record in the repository 11 14 /// 12 15 /// [uri] The URI of the record to edit 13 16 /// [record] The record data to edit 14 - Future<StrongRef> editRecord({required AtUri uri, required Record record}); 17 + Future<RepoStrongRef> editRecord({required AtUri uri, required Record record}); 15 18 16 19 /// Create a record in the repository 17 20 /// 18 21 /// [collection] The NSID of the collection to create the record in 19 22 /// [record] The record data to create 20 - Future<StrongRef> createRecord({required NSID collection, required Map<String, dynamic> record, String? rkey}); 23 + Future<RepoStrongRef> createRecord({required String collection, required Map<String, dynamic> record, String? rkey}); 21 24 22 25 /// Delete a record from the repository 23 26 /// ··· 35 38 /// [collection] The NSID of the collection to list records from 36 39 Future<List<Record>> listRecords({ 37 40 required String repo, 38 - required NSID collection, 41 + required String collection, 39 42 String? cursor, 40 43 int? limit, 41 44 bool? reverse, ··· 49 52 /// [service] Optional moderation service to use 50 53 /// 51 54 /// Returns true if the report was successfully created 52 - Future<bool> createReport({ 53 - required ReportSubject subject, 54 - required ModerationReasonType reasonType, 55 - String? reason, 56 - ModerationService? service, 57 - }); 55 + Future<bool> createReport({required ModerationCreateReportInput input, ModerationService? service}); 58 56 }
+48 -45
lib/src/core/network/atproto/data/repositories/repo_repository_impl.dart
··· 1 1 import 'dart:convert'; 2 2 import 'dart:typed_data'; 3 3 4 - import 'package:atproto/atproto.dart'; 4 + import 'package:atproto/com_atproto_moderation_createreport.dart'; 5 + import 'package:atproto/com_atproto_repo_strongref.dart'; 6 + import 'package:atproto/com_atproto_services.dart'; 5 7 import 'package:atproto/core.dart'; 6 8 import 'package:get_it/get_it.dart'; 7 - import 'package:http/http.dart' as http; 9 + import 'package:sparksocial/src/core/network/atproto/data/models/record_models.dart'; 8 10 import 'package:sparksocial/src/core/network/atproto/data/repositories/repo_repository.dart'; 9 11 import 'package:sparksocial/src/core/network/atproto/data/repositories/sprk_repository_impl.dart'; 10 12 import 'package:sparksocial/src/core/utils/logging/log_service.dart'; ··· 19 21 final SparkLogger _logger = GetIt.instance<LogService>().getLogger('RepoAPI'); 20 22 21 23 @override 22 - Future<({Record record, StrongRef strongRef})> getRecord({required AtUri uri}) async { 24 + Future<({Record record, RepoStrongRef strongRef})> getRecord({required AtUri uri}) async { 23 25 _logger.d('Getting record for URI: $uri'); 24 26 return _client.executeWithRetry(() async { 25 27 if (!_client.authRepository.isAuthenticated) { ··· 31 33 _logger.e('AtProto not initialized'); 32 34 throw Exception('AtProto not initialized'); 33 35 } 34 - final result = await atproto.repo.getRecord(uri: uri); 36 + final result = await atproto.repo.getRecord(repo: uri.hostname, collection: uri.collection.toString(), rkey: uri.rkey); 35 37 _logger.d('Record retrieved successfully'); 36 - return (record: result.data, strongRef: StrongRef(uri: result.data.uri, cid: result.data.cid ?? '')); 38 + return ( 39 + record: Record.fromJson(result.data.value), 40 + strongRef: RepoStrongRef(uri: result.data.uri, cid: result.data.cid ?? ''), 41 + ); 37 42 }); 38 43 } 39 44 40 45 @override 41 - Future<StrongRef> editRecord({required AtUri uri, required Record record}) async { 46 + Future<RepoStrongRef> editRecord({required AtUri uri, required Record record}) async { 42 47 _logger.d('Editing record at URI: $uri'); 43 48 return _client.executeWithRetry(() async { 44 49 if (!_client.authRepository.isAuthenticated) { ··· 50 55 _logger.e('AtProto not initialized'); 51 56 throw Exception('AtProto not initialized'); 52 57 } 53 - final result = await atproto.repo.putRecord(uri: uri, record: record.toJson()); 58 + final result = await atproto.repo.putRecord( 59 + repo: uri.hostname, 60 + collection: uri.collection.toString(), 61 + rkey: uri.rkey, 62 + record: record.toJson(), 63 + ); 54 64 _logger.d('Record edited successfully'); 55 - return StrongRef(uri: result.data.uri, cid: result.data.cid); 65 + return RepoStrongRef(uri: result.data.uri, cid: result.data.cid); 56 66 }); 57 67 } 58 68 59 69 @override 60 - Future<StrongRef> createRecord({required NSID collection, required Map<String, dynamic> record, String? rkey}) async { 70 + Future<RepoStrongRef> createRecord({required String collection, required Map<String, dynamic> record, String? rkey}) async { 61 71 _logger.d('Creating record in collection: $collection'); 62 72 return _client.executeWithRetry(() async { 63 73 if (!_client.authRepository.isAuthenticated) { ··· 71 81 throw Exception('AtProto not initialized'); 72 82 } 73 83 74 - final result = await atproto.repo.createRecord(collection: collection, record: record, rkey: rkey); 84 + final result = await atproto.repo.createRecord(repo: _client.sprkDid, collection: collection, record: record, rkey: rkey); 75 85 _logger.d('Record created successfully'); 76 - return StrongRef(uri: result.data.uri, cid: result.data.cid); 86 + return RepoStrongRef(uri: result.data.uri, cid: result.data.cid); 77 87 }); 78 88 } 79 89 ··· 92 102 throw Exception('AtProto not initialized'); 93 103 } 94 104 95 - await atproto.repo.deleteRecord(uri: uri); 105 + await atproto.repo.deleteRecord(repo: uri.hostname, collection: uri.collection.toString(), rkey: uri.rkey); 96 106 _logger.d('Record deleted successfully'); 97 107 98 108 // Delete cross-posted Bluesky counterpart if it exists ··· 104 114 _logger.d('Attempting to delete Bluesky counterpart post: $blueskyUri'); 105 115 106 116 try { 107 - await atproto.repo.deleteRecord(uri: blueskyUri); 117 + await atproto.repo.deleteRecord( 118 + repo: blueskyUri.hostname, 119 + collection: blueskyUri.collection.toString(), 120 + rkey: blueskyUri.rkey, 121 + ); 108 122 _logger.d('Bluesky counterpart post deleted successfully'); 109 123 } catch (e) { 110 124 // Ignore errors like 404 – it simply means the counterpart does not exist. ··· 132 146 throw Exception('AtProto not initialized'); 133 147 } 134 148 135 - final result = await atproto.repo.uploadBlob(data); 149 + final result = await atproto.repo.uploadBlob(bytes: data); 136 150 _logger.d('Blob uploaded successfully'); 137 151 138 152 return result.data.blob; ··· 142 156 @override 143 157 Future<List<Record>> listRecords({ 144 158 required String repo, 145 - required NSID collection, 159 + required String collection, 146 160 String? cursor, 147 161 int? limit, 148 162 bool? reverse, ··· 177 191 } 178 192 179 193 @override 180 - Future<bool> createReport({ 181 - required ReportSubject subject, 182 - required ModerationReasonType reasonType, 183 - String? reason, 184 - ModerationService? service, 185 - }) async { 186 - _logger.i('Creating moderation report for reason: ${reasonType.value}'); 194 + Future<bool> createReport({required ModerationCreateReportInput input, ModerationService? service}) async { 195 + _logger.i('Creating moderation report for reason: ${input.reasonType}'); 187 196 188 197 return _client.executeWithRetry(() async { 189 198 if (!_client.authRepository.isAuthenticated) { ··· 198 207 } else if (service != null) { 199 208 _logger.d('Using provided moderation service'); 200 209 try { 201 - final report = await service.createReport(subject: subject, reasonType: reasonType, reason: reason); 210 + final report = await service.createReport(subject: input.subject, reasonType: input.reasonType, reason: input.reason); 202 211 return report.status.code == 200; 203 212 } catch (e) { 204 213 _logger.e('Error creating report with service', error: e); ··· 207 216 } else { 208 217 _logger.d('Using direct API call for moderation report'); 209 218 final endpoint = NSID.parse('com.atproto.moderation.createReport'); 210 - final subjectData = subject.data; 219 + final subjectData = input.subject.data; 211 220 212 221 Map<String, dynamic> body; 213 222 214 - if (subjectData is StrongRef) { 215 - final strongRef = subjectData.toJson(); 223 + if (subjectData is RepoStrongRef) { 216 224 body = { 217 - 'subject': {r'$type': 'com.atproto.repo.strongRef', 'uri': strongRef['uri'], 'cid': strongRef['cid']}, 218 - 'reasonType': reasonType.value, 219 - }; 220 - } else if (subjectData is RepoRef) { 221 - body = { 222 - 'subject': {r'$type': 'com.atproto.admin.defs.repoRef', 'did': subjectData.did}, 223 - 'reasonType': reasonType.value, 225 + 'subject': {r'$type': 'com.atproto.repo.strongRef', 'uri': subjectData.uri, 'cid': subjectData.cid}, 226 + 'reasonType': input.reasonType.data.toString(), 224 227 }; 225 228 } else { 226 229 _logger.e('Invalid subject data type: ${subjectData.runtimeType}'); 227 230 throw Exception('Invalid subject data'); 228 231 } 229 232 230 - if (reason != null) { 231 - body['reason'] = reason; 233 + if (input.reason != null) { 234 + body['reason'] = input.reason; 232 235 } 233 236 234 - // Send to Spark's PDS (don't use the user's PDS as it might be different) 235 - // TODO: send to a chosen labeler's PDS 236 - final uri = Uri.parse('https://pds.sprk.so/xrpc/$endpoint'); 237 - final headers = {'Authorization': 'Bearer ${atproto.session!.accessJwt}', 'Content-Type': 'application/json'}; 238 - 239 - _logger.d('Sending report to: $uri'); 237 + // Send to Spark's Mod service 238 + final headers = { 239 + 'Authorization': 'Bearer ${atproto.session!.accessJwt}', 240 + 'Content-Type': 'application/json', 241 + 'atproto-proxy': _client.modDid, 242 + }; 240 243 241 244 try { 242 - final response = await http.post(uri, headers: headers, body: jsonEncode(body)); 245 + final response = await atproto.post(endpoint, headers: headers, body: jsonEncode(body)); 243 246 244 - if (response.statusCode != 200) { 245 - _logger.e('Failed to create report: ${response.body}', error: 'HTTP ${response.statusCode}'); 246 - throw Exception('Failed to create report: ${response.body}'); 247 + if (response.status != HttpStatus.ok) { 248 + _logger.e('Failed to create report: ${response.data}', error: 'HTTP ${response.status}'); 249 + throw Exception('Failed to create report: ${response.data}'); 247 250 } 248 251 249 252 _logger.i('Report created successfully');
+2 -2
lib/src/core/network/atproto/data/repositories/sound_repository.dart
··· 1 - import 'package:atproto/atproto.dart'; 1 + import 'package:atproto/com_atproto_repo_strongref.dart'; 2 2 import 'package:atproto_core/atproto_core.dart'; 3 3 import 'package:sparksocial/src/core/network/atproto/data/models/models.dart'; 4 4 ··· 9 9 /// [title] The title of the sound. 10 10 /// [details] Optional audio details (artist, title metadata). 11 11 /// Returns a [StrongRef] to the created sound record. 12 - Future<StrongRef> createSound({ 12 + Future<RepoStrongRef> createSound({ 13 13 required Blob sound, 14 14 required String title, 15 15 AudioDetails? details,
+5 -4
lib/src/core/network/atproto/data/repositories/sound_repository_impl.dart
··· 1 1 import 'dart:convert'; 2 2 3 - import 'package:atproto/atproto.dart'; 3 + import 'package:atproto/com_atproto_repo_strongref.dart'; 4 4 import 'package:atproto/core.dart'; 5 5 import 'package:get_it/get_it.dart'; 6 6 import 'package:sparksocial/src/core/network/atproto/data/models/models.dart'; ··· 17 17 final SparkLogger _logger = GetIt.instance<LogService>().getLogger('SoundRepository'); 18 18 19 19 @override 20 - Future<StrongRef> createSound({ 20 + Future<RepoStrongRef> createSound({ 21 21 required Blob sound, 22 22 required String title, 23 23 AudioDetails? details, ··· 43 43 ); 44 44 45 45 final result = await atproto.repo.createRecord( 46 - collection: NSID.parse('so.sprk.sound.audio'), 46 + repo: _client.sprkDid, 47 + collection: 'so.sprk.sound.audio', 47 48 record: audioRecord.toJson(), 48 49 ); 49 50 50 51 _logger.i('Sound record created successfully: ${result.data.uri}'); 51 - return result.data; 52 + return result.data as RepoStrongRef; 52 53 }); 53 54 } 54 55
+2 -1
lib/src/core/network/atproto/data/repositories/sprk_repository.dart
··· 20 20 21 21 /// Get the Sprk DID 22 22 String get sprkDid; 23 - String get bskyDid => 'did:web:api.bsky.app'; 23 + String get bskyDid => 'did:web:api.bsky.app#bsky_appview'; 24 + String get modDid => 'did:web:mod.sprk.so#atproto_label'; 24 25 25 26 ActorRepository get actor; 26 27 RepoRepository get repo;
+3
lib/src/core/network/atproto/data/repositories/sprk_repository_impl.dart
··· 40 40 @override 41 41 String get bskyDid => 'did:web:api.bsky.app#bsky_appview'; 42 42 43 + @override 44 + String get modDid => 'did:web:mod.sprk.so#atproto_label'; 45 + 43 46 static String _getSprkDid() { 44 47 final sprkAppView = Uri.parse(AppConfig.appViewUrl); 45 48 return 'did:web:${sprkAppView.host}#sprk_appview';
+3 -2
lib/src/core/network/atproto/data/repositories/story_repository.dart
··· 1 - import 'package:atproto/atproto.dart'; 1 + import 'package:atproto/com_atproto_label_defs.dart'; 2 + import 'package:atproto/com_atproto_repo_strongref.dart'; 2 3 import 'package:atproto_core/atproto_core.dart'; 3 4 import 'package:sparksocial/src/core/network/atproto/data/models/models.dart'; 4 5 ··· 9 10 /// [media] The media of the story to post 10 11 /// [selfLabels] The self labels of the story 11 12 /// [tags] The tags of the story 12 - Future<StrongRef> postStory(Media media, {List<SelfLabel>? selfLabels, List<String>? tags}); 13 + Future<RepoStrongRef> postStory(Media media, {List<SelfLabel>? selfLabels, List<String>? tags}); 13 14 14 15 /// Get stories timeline 15 16 ///
+12 -14
lib/src/core/network/atproto/data/repositories/story_repository_impl.dart
··· 1 - import 'package:atproto/atproto.dart'; 1 + import 'package:atproto/com_atproto_label_defs.dart'; 2 + import 'package:atproto/com_atproto_repo_strongref.dart'; 2 3 import 'package:atproto_core/atproto_core.dart'; 3 4 import 'package:sparksocial/src/core/network/atproto/data/models/models.dart'; 4 5 import 'package:sparksocial/src/core/network/atproto/data/repositories/sprk_repository.dart'; ··· 157 158 } 158 159 159 160 @override 160 - Future<StrongRef> postStory(Media media, {List<SelfLabel>? selfLabels, List<String>? tags}) { 161 + Future<RepoStrongRef> postStory(Media media, {List<SelfLabel>? selfLabels, List<String>? tags}) { 161 162 return _client.executeWithRetry(() async { 162 163 if (!_client.authRepository.isAuthenticated) { 163 164 throw Exception('Not authenticated'); ··· 165 166 166 167 final record = StoryRecord(createdAt: DateTime.now().toUtc(), media: media, tags: tags, labels: selfLabels); 167 168 168 - try { 169 - final response = await _client.authRepository.atproto!.repo.createRecord( 170 - collection: NSID.parse('so.sprk.story.post'), 171 - record: record.toJson(), 172 - ); 169 + final response = await _client.authRepository.atproto!.repo.createRecord( 170 + repo: _client.sprkDid, 171 + collection: 'so.sprk.story.post', 172 + record: record.toJson(), 173 + ); 173 174 174 - if (response.status.code == 200) { 175 - return response.data; 176 - } else { 177 - throw Exception('Failed to post story: ${response.status} ${response.data}'); 178 - } 179 - } catch (e) { 180 - rethrow; 175 + if (response.status.code != 200) { 176 + throw Exception('Failed to post story: ${response.status} ${response.data}'); 181 177 } 178 + 179 + return response.data as RepoStrongRef; 182 180 }); 183 181 } 184 182 }
+7 -7
lib/src/core/network/messages/data/models/message_models.dart
··· 4 4 part 'message_models.g.dart'; 5 5 6 6 @freezed 7 - class Embed with _$Embed { 7 + abstract class Embed with _$Embed { 8 8 @JsonSerializable(explicitToJson: true) 9 9 const factory Embed({String? url, String? type, String? preview}) = _Embed; 10 10 const Embed._(); ··· 16 16 } 17 17 18 18 @freezed 19 - class Message with _$Message { 19 + abstract class Message with _$Message { 20 20 @JsonSerializable(explicitToJson: true) 21 21 const factory Message({ 22 22 required int id, ··· 34 34 // XRPC Models for chat service 35 35 36 36 @freezed 37 - class SenderView with _$SenderView { 37 + abstract class SenderView with _$SenderView { 38 38 @JsonSerializable(explicitToJson: true) 39 39 const factory SenderView({ 40 40 required String did, ··· 45 45 } 46 46 47 47 @freezed 48 - class ReactionView with _$ReactionView { 48 + abstract class ReactionView with _$ReactionView { 49 49 @JsonSerializable(explicitToJson: true) 50 50 const factory ReactionView({ 51 51 required String value, ··· 58 58 } 59 59 60 60 @freezed 61 - class MessageView with _$MessageView { 61 + abstract class MessageView with _$MessageView { 62 62 @JsonSerializable(explicitToJson: true) 63 63 const factory MessageView({ 64 64 required String id, ··· 75 75 } 76 76 77 77 @freezed 78 - class DeletedMessageView with _$DeletedMessageView { 78 + abstract class DeletedMessageView with _$DeletedMessageView { 79 79 @JsonSerializable(explicitToJson: true) 80 80 const factory DeletedMessageView({ 81 81 required String id, ··· 89 89 } 90 90 91 91 @freezed 92 - class ConvoView with _$ConvoView { 92 + abstract class ConvoView with _$ConvoView { 93 93 @JsonSerializable(explicitToJson: true) 94 94 const factory ConvoView({ 95 95 required String id,
+1 -2
lib/src/core/network/xrpc/service_auth_helper.dart
··· 1 - import 'package:atproto/core.dart'; 2 1 import 'package:get_it/get_it.dart'; 3 2 import 'package:sparksocial/src/core/auth/data/repositories/auth_repository.dart'; 4 3 import 'package:sparksocial/src/core/config/app_config.dart'; ··· 45 44 // Use official atproto API to request service auth 46 45 final res = await atproto.server.getServiceAuth( 47 46 aud: serviceDid, 48 - lxm: NSID.parse(nsid), 47 + lxm: nsid, 49 48 exp: exp, 50 49 ); 51 50
+2 -2
lib/src/core/pro_video_editor/models/video_editor_result.dart
··· 1 - import 'package:atproto/atproto.dart'; 1 + import 'package:atproto/com_atproto_repo_strongref.dart'; 2 2 import 'package:image_picker/image_picker.dart'; 3 3 4 4 /// Result returned from the video editor containing the edited video ··· 13 13 final XFile video; 14 14 15 15 /// Reference to the audio track used, if any. 16 - final StrongRef? soundRef; 16 + final RepoStrongRef? soundRef; 17 17 }
+4 -4
lib/src/core/pro_video_editor/ui/video_editor_grounded_page.dart
··· 1 1 import 'dart:async'; 2 2 import 'dart:convert'; 3 3 4 - import 'package:atproto/atproto.dart'; 4 + import 'package:atproto/com_atproto_repo_strongref.dart'; 5 5 import 'package:atproto/core.dart'; 6 6 import 'package:auto_route/auto_route.dart'; 7 7 import 'package:flutter/foundation.dart'; ··· 77 77 late EditorVideo _video; 78 78 79 79 String? _outputPath; 80 - StrongRef? _selectedSoundRef; 80 + RepoStrongRef? _selectedSoundRef; 81 81 82 82 late VideoPlayerController _videoController; 83 83 ··· 272 272 String _encodeTrackId(String uri, String cid, {String? authorAvatar}) => 273 273 jsonEncode({'uri': uri, 'cid': cid, 'authorAvatar': authorAvatar}); 274 274 275 - StrongRef? _decodeStrongRef(String? encoded) { 275 + RepoStrongRef? _decodeStrongRef(String? encoded) { 276 276 if (encoded == null) return null; 277 277 try { 278 278 final map = jsonDecode(encoded) as Map<String, dynamic>; 279 - return StrongRef( 279 + return RepoStrongRef( 280 280 uri: AtUri.parse(map['uri'] as String), 281 281 cid: map['cid'] as String, 282 282 );
+1 -1
lib/src/core/routing/app_router.dart
··· 1 - import 'package:atproto/atproto.dart'; 1 + import 'package:atproto/com_atproto_repo_strongref.dart'; 2 2 import 'package:auto_route/auto_route.dart'; 3 3 import 'package:collection/collection.dart'; 4 4 import 'package:flutter/material.dart';
-1
lib/src/core/ui/theme/providers/theme_provider.dart
··· 1 1 import 'package:flutter/material.dart'; 2 - import 'package:flutter_riverpod/flutter_riverpod.dart'; 3 2 import 'package:get_it/get_it.dart'; 4 3 import 'package:riverpod_annotation/riverpod_annotation.dart'; 5 4 import 'package:sparksocial/src/core/ui/theme/data/repositories/theme_repository.dart';
+1 -1
lib/src/core/ui/theme/providers/theme_state.dart
··· 4 4 part 'theme_state.freezed.dart'; 5 5 6 6 @freezed 7 - class ThemeState with _$ThemeState { 7 + abstract class ThemeState with _$ThemeState { 8 8 const factory ThemeState({required ThemeMode themeMode}) = _ThemeState; 9 9 }
+37 -18
lib/src/core/ui/widgets/report_dialog.dart
··· 1 - import 'package:atproto/atproto.dart'; 1 + import 'package:atproto/com_atproto_moderation_createreport.dart'; 2 + import 'package:atproto/com_atproto_moderation_defs.dart'; 3 + import 'package:atproto/com_atproto_services.dart'; 2 4 import 'package:atproto/core.dart'; 3 5 import 'package:auto_route/auto_route.dart'; 6 + import 'package:bluesky/com_atproto_repo_strongref.dart'; 4 7 import 'package:flutter/material.dart'; 5 8 import 'package:flutter_riverpod/flutter_riverpod.dart'; 6 9 import 'package:get_it/get_it.dart'; ··· 12 15 const ReportDialog({required this.postUri, required this.postCid, super.key, this.onSubmit}); 13 16 final String postUri; 14 17 final String postCid; 15 - final Function(ReportSubject subject, ModerationReasonType reasonType, String? reason, ModerationService? service)? onSubmit; 18 + final Function(UModerationCreateReportSubject subject, KnownReasonType reasonType, String? reason, ModerationService? service)? 19 + onSubmit; 16 20 17 21 @override 18 22 ConsumerState<ReportDialog> createState() => _ReportDialogState(); ··· 20 24 21 25 class _ReportDialogState extends ConsumerState<ReportDialog> { 22 26 final SparkLogger _logger = GetIt.instance<LogService>().getLogger('ReportDialog'); 23 - ModerationReasonType _selectedReason = ModerationReasonType.spam; 27 + KnownReasonType _selectedReason = KnownReasonType.comAtprotoModerationDefsReasonSpam; 24 28 final TextEditingController _additionalInfoController = TextEditingController(); 25 29 bool _isSubmitting = false; 26 30 String? _errorMessage; 27 31 28 32 // Map of user-friendly names and descriptions for each reason type 29 - final Map<ModerationReasonType, Map<String, String>> _reasonDescriptions = { 30 - ModerationReasonType.spam: {'name': 'Spam', 'description': 'Unwanted or repetitive content'}, 31 - ModerationReasonType.violation: {'name': 'Terms Violation', 'description': 'Violates platform terms'}, 32 - ModerationReasonType.misleading: {'name': 'Misleading Info', 'description': 'False or deceptive content'}, 33 - ModerationReasonType.sexual: {'name': 'Sexual Content', 'description': 'Inappropriate explicit material'}, 34 - ModerationReasonType.rude: {'name': 'Harassment', 'description': 'Abusive or threatening behavior'}, 35 - ModerationReasonType.other: {'name': 'Other', 'description': 'Other issues not listed above'}, 33 + final Map<KnownReasonType, Map<String, String>> _reasonDescriptions = { 34 + KnownReasonType.comAtprotoModerationDefsReasonSpam: {'name': 'Spam', 'description': 'Unwanted or repetitive content'}, 35 + KnownReasonType.comAtprotoModerationDefsReasonViolation: { 36 + 'name': 'Terms Violation', 37 + 'description': 'Violates platform terms', 38 + }, 39 + KnownReasonType.comAtprotoModerationDefsReasonMisleading: { 40 + 'name': 'Misleading Info', 41 + 'description': 'False or deceptive content', 42 + }, 43 + KnownReasonType.comAtprotoModerationDefsReasonSexual: { 44 + 'name': 'Sexual Content', 45 + 'description': 'Inappropriate explicit material', 46 + }, 47 + KnownReasonType.comAtprotoModerationDefsReasonRude: {'name': 'Harassment', 'description': 'Abusive or threatening behavior'}, 48 + KnownReasonType.comAtprotoModerationDefsReasonOther: {'name': 'Other', 'description': 'Other issues not listed above'}, 36 49 }; 37 50 38 51 @override ··· 42 55 } 43 56 44 57 Future<void> _submitReport() async { 45 - final subject = ReportSubject.strongRef( 46 - data: StrongRef(cid: widget.postCid, uri: AtUri.parse(widget.postUri)), 58 + final subject = UModerationCreateReportSubject.repoStrongRef( 59 + data: RepoStrongRef(cid: widget.postCid, uri: AtUri.parse(widget.postUri)), 47 60 ); 48 61 final reason = _additionalInfoController.text.isNotEmpty ? _additionalInfoController.text : null; 49 62 ··· 64 77 final repoRepository = GetIt.instance<SprkRepository>().repo; 65 78 _logger.d('Creating report with reason: ${_selectedReason.value}'); 66 79 67 - final success = await repoRepository.createReport(subject: subject, reasonType: _selectedReason, reason: reason); 80 + final success = await repoRepository.createReport( 81 + input: ModerationCreateReportInput( 82 + subject: subject, 83 + reasonType: ReasonType.knownValue(data: _selectedReason), 84 + reason: reason, 85 + ), 86 + ); 68 87 69 88 if (success && mounted) { 70 89 context.router.maybePop(); ··· 100 119 mainAxisSize: MainAxisSize.min, 101 120 crossAxisAlignment: CrossAxisAlignment.start, 102 121 children: [ 103 - for (final reason in ModerationReasonType.values) 122 + for (final reason in KnownReasonType.values) 104 123 _ReasonTile( 105 124 reason: reason, 106 125 selectedReason: _selectedReason, ··· 172 191 required this.reasonDescription, 173 192 required this.onChanged, 174 193 }); 175 - final ModerationReasonType reason; 176 - final ModerationReasonType selectedReason; 194 + final KnownReasonType reason; 195 + final KnownReasonType selectedReason; 177 196 final Map<String, String> reasonDescription; 178 - final ValueChanged<ModerationReasonType?> onChanged; 197 + final ValueChanged<KnownReasonType?> onChanged; 179 198 180 199 @override 181 200 Widget build(BuildContext context) { ··· 184 203 final friendlyName = reasonDescription['name'] ?? reason.value; 185 204 final description = reasonDescription['description'] ?? ''; 186 205 187 - return RadioListTile<ModerationReasonType>( 206 + return RadioListTile<KnownReasonType>( 188 207 title: Text( 189 208 friendlyName, 190 209 style: theme.textTheme.bodyMedium?.copyWith(color: textColor, fontWeight: FontWeight.w500, fontSize: 13),
+16
lib/src/core/utils/json_utils.dart
··· 1 + /// Creates a deep mutable copy of a JSON-like map structure. 2 + /// 3 + /// Use when you need to modify a JSON map without affecting the original, 4 + /// especially when the map contains nested maps or lists that will be modified. 5 + Map<String, dynamic> deepCopyJson(Map<String, dynamic> source) { 6 + return source.map((key, value) => MapEntry(key, _deepCopyValue(value))); 7 + } 8 + 9 + dynamic _deepCopyValue(dynamic value) { 10 + if (value is Map<String, dynamic>) { 11 + return deepCopyJson(value); 12 + } else if (value is List) { 13 + return value.map(_deepCopyValue).toList(); 14 + } 15 + return value; 16 + }
+8 -8
lib/src/core/utils/label_utils.dart
··· 1 - import 'package:atproto/atproto.dart'; 1 + import 'package:atproto/com_atproto_label_defs.dart'; 2 2 import 'package:get_it/get_it.dart'; 3 3 import 'package:sparksocial/src/core/network/atproto/data/models/labeler_models.dart'; 4 4 import 'package:sparksocial/src/core/network/atproto/data/repositories/pref_repository.dart'; ··· 75 75 76 76 for (final label in labels) { 77 77 try { 78 - final preference = await _getLabelPreference(label.value); 78 + final preference = await _getLabelPreference(label.val); 79 79 if (preference.severity == Severity.alert && preference.setting == Setting.warn) { 80 80 return true; 81 81 } ··· 93 93 94 94 for (final label in labels) { 95 95 try { 96 - final preference = await _getLabelPreference(label.value); 96 + final preference = await _getLabelPreference(label.val); 97 97 if (preference.blurs == Blurs.content || preference.blurs == Blurs.media && preference.setting == Setting.warn) { 98 98 return true; 99 99 } ··· 113 113 114 114 for (final label in labels) { 115 115 try { 116 - final preference = await _getLabelPreference(label.value); 116 + final preference = await _getLabelPreference(label.val); 117 117 if (preference.severity == Severity.alert && preference.setting == Setting.warn) { 118 - warningLabels.add(label.value); 118 + warningLabels.add(label.val); 119 119 } 120 120 } catch (e) { 121 121 // If no preference found, continue checking other labels ··· 133 133 134 134 for (final label in labels) { 135 135 try { 136 - final preference = await _getLabelPreference(label.value); 136 + final preference = await _getLabelPreference(label.val); 137 137 if (preference.severity == Severity.inform && preference.setting == Setting.warn) { 138 - informLabels.add(label.value); 138 + informLabels.add(label.val); 139 139 } 140 140 } catch (e) { 141 141 // If no preference found, continue checking other labels ··· 151 151 152 152 for (final label in labels) { 153 153 try { 154 - final preference = await _getLabelPreference(label.value); 154 + final preference = await _getLabelPreference(label.val); 155 155 if (preference.setting == Setting.hide || preference.adultOnly) { 156 156 return true; 157 157 }
+7 -7
lib/src/core/utils/logging/riverpod_logger.dart
··· 4 4 import 'package:sparksocial/src/core/utils/logging/logging.dart'; 5 5 6 6 /// A ProviderObserver that logs provider changes 7 - class SparkRiverpodLogger extends ProviderObserver { 7 + final class SparkRiverpodLogger extends ProviderObserver { 8 8 /// Constructor 9 9 SparkRiverpodLogger({LogService? logService}) : _logService = logService ?? GetIt.instance<LogService>() { 10 10 _logger = _logService.getLogger('Riverpod'); ··· 13 13 late final SparkLogger _logger; 14 14 15 15 @override 16 - void didAddProvider(ProviderBase<Object?> provider, Object? value, ProviderContainer container) { 17 - _logger.d('${provider.name ?? provider.runtimeType} added: ${_truncateIfNeeded(value.toString())}'); 16 + void didAddProvider(ProviderObserverContext context, Object? value) { 17 + _logger.d('${context.provider.name ?? context.provider.runtimeType} added: ${_truncateIfNeeded(value.toString())}'); 18 18 } 19 19 20 20 @override 21 - void didDisposeProvider(ProviderBase<Object?> provider, ProviderContainer container) { 22 - _logger.d('${provider.name ?? provider.runtimeType} was disposed.'); 21 + void didDisposeProvider(ProviderObserverContext context) { 22 + _logger.d('${context.provider.name ?? context.provider.runtimeType} was disposed.'); 23 23 } 24 24 25 25 @override 26 - void didUpdateProvider(ProviderBase<Object?> provider, Object? previousValue, Object? newValue, ProviderContainer container) { 26 + void didUpdateProvider(ProviderObserverContext context, Object? previousValue, Object? newValue) { 27 27 // Skip logging if previous and new values are identical 28 28 if (previousValue == newValue) return; 29 29 ··· 32 32 33 33 final diff = _generateDiff(previousValue, newValue); 34 34 if (diff.isNotEmpty && diff != 'No changes') { 35 - _logger.d('${provider.name ?? provider.runtimeType} changed: $diff'); 35 + _logger.d('${context.provider.name ?? context.provider.runtimeType} changed: $diff'); 36 36 } 37 37 } 38 38
+1
lib/src/core/utils/utils.dart
··· 1 + export 'json_utils.dart'; 1 2 export 'logging/logging.dart'; 2 3 export 'text_formatter.dart';
-1
lib/src/features/auth/providers/auth_providers.dart
··· 1 1 import 'package:atproto/atproto.dart'; 2 2 import 'package:atproto/core.dart'; 3 - import 'package:flutter_riverpod/flutter_riverpod.dart'; 4 3 import 'package:get_it/get_it.dart'; 5 4 import 'package:riverpod_annotation/riverpod_annotation.dart'; 6 5 import 'package:sparksocial/src/core/auth/data/models/login_result.dart';
+1 -1
lib/src/features/auth/providers/auth_state.dart
··· 6 6 7 7 /// Authentication state for the application 8 8 @freezed 9 - class AuthState with _$AuthState { 9 + abstract class AuthState with _$AuthState { 10 10 const factory AuthState({ 11 11 @Default(false) bool isAuthenticated, 12 12 Session? session,
+4 -10
lib/src/features/auth/providers/onboarding_providers.dart
··· 1 - import 'package:bluesky/bluesky.dart'; 2 - import 'package:flutter_riverpod/flutter_riverpod.dart'; 1 + import 'package:bluesky/app_bsky_actor_profile.dart'; 3 2 import 'package:get_it/get_it.dart'; 4 3 import 'package:riverpod_annotation/riverpod_annotation.dart'; 5 4 import 'package:sparksocial/src/core/auth/data/repositories/auth_repository.dart'; ··· 28 27 29 28 /// Provider to get the user's Bluesky profile for import 30 29 @riverpod 31 - Future<ProfileRecord?> bskyProfile(Ref ref) async { 30 + Future<ActorProfileRecord?> bskyProfile(Ref ref) async { 32 31 final repository = ref.watch(onboardingRepositoryProvider); 33 - final profileData = await repository.getBskyProfile(); 34 - 35 - if (profileData == null) return null; 36 - 37 - return profileData; 32 + return repository.getBskyProfile(); 38 33 } 39 34 40 35 /// Provider to get Bluesky follows ··· 50 45 @override 51 46 Future<void> build() async { 52 47 // Initial build does nothing 53 - return; 54 48 } 55 49 56 50 /// Import Bluesky profile to create a Spark profile 57 - Future<void> importProfile(ProfileRecord bskyProfile) async { 51 + Future<void> importProfile(ActorProfileRecord bskyProfile) async { 58 52 state = const AsyncLoading(); 59 53 60 54 try {
+25 -26
lib/src/features/auth/ui/pages/onboarding_page.dart
··· 1 1 import 'package:auto_route/auto_route.dart'; 2 2 import 'package:flutter/material.dart'; 3 3 import 'package:flutter_riverpod/flutter_riverpod.dart'; 4 - import 'package:sparksocial/src/core/auth/data/models/onboarding_screen_state.dart'; // Import for OnboardingScreenState 4 + import 'package:sparksocial/src/core/auth/data/models/onboarding_screen_state.dart'; 5 5 import 'package:sparksocial/src/core/routing/app_router.dart'; 6 - import 'package:sparksocial/src/core/ui/widgets/custom_text_field.dart'; // Corrected path 6 + import 'package:sparksocial/src/core/ui/widgets/custom_text_field.dart'; 7 7 import 'package:sparksocial/src/features/auth/providers/onboarding_notifier.dart'; 8 8 import 'package:sparksocial/src/features/auth/providers/onboarding_providers.dart'; 9 9 import 'package:sparksocial/src/features/settings/providers/settings_provider.dart'; ··· 27 27 super.initState(); 28 28 _displayNameController = TextEditingController(); 29 29 _descriptionController = TextEditingController(); 30 + _displayNameController.addListener(_updateDisplayName); 31 + _descriptionController.addListener(_updateDescription); 32 + } 30 33 31 - final initialState = ref.read(onboardingNotifierProvider); 32 - if (initialState.hasValue && initialState.value != null) { 33 - _displayNameController.text = initialState.value!.displayName; 34 - _descriptionController.text = initialState.value!.description; 34 + void _updateDisplayName() { 35 + final currentProviderState = ref.read(onboardingProvider).value; 36 + if (currentProviderState != null && _displayNameController.text != currentProviderState.displayName) { 37 + ref.read(onboardingProvider.notifier).updateDisplayName(_displayNameController.text); 35 38 } 39 + } 36 40 37 - _displayNameController.addListener(() { 38 - final currentProviderState = ref.read(onboardingNotifierProvider).value; 39 - if (currentProviderState != null && _displayNameController.text != currentProviderState.displayName) { 40 - ref.read(onboardingNotifierProvider.notifier).updateDisplayName(_displayNameController.text); 41 - } 42 - }); 43 - 44 - _descriptionController.addListener(() { 45 - final currentProviderState = ref.read(onboardingNotifierProvider).value; 46 - if (currentProviderState != null && _descriptionController.text != currentProviderState.description) { 47 - ref.read(onboardingNotifierProvider.notifier).updateDescription(_descriptionController.text); 48 - } 49 - }); 41 + void _updateDescription() { 42 + final currentProviderState = ref.read(onboardingProvider).value; 43 + if (currentProviderState != null && _descriptionController.text != currentProviderState.description) { 44 + ref.read(onboardingProvider.notifier).updateDescription(_descriptionController.text); 45 + } 50 46 } 51 47 52 48 @override 53 49 void dispose() { 50 + _displayNameController.removeListener(_updateDisplayName); 51 + _descriptionController.removeListener(_updateDescription); 54 52 _displayNameController.dispose(); 55 53 _descriptionController.dispose(); 56 54 super.dispose(); ··· 68 66 final onboardingState = ref.read(onboardingStateProvider.notifier); 69 67 70 68 // Determine avatar to use 71 - dynamic avatarToUse; 72 - final currentState = ref.read(onboardingNotifierProvider).value; 69 + Object? avatarToUse; 70 + final currentState = ref.read(onboardingProvider).value; 73 71 if (currentState?.localAvatarBytes != null) { 74 72 avatarToUse = currentState!.localAvatarBytes; 75 73 } else if (currentState?.bskyProfileRecord?.avatar != null) { ··· 106 104 107 105 @override 108 106 Widget build(BuildContext context) { 109 - final onboardingStateAsync = ref.watch(onboardingNotifierProvider); 110 - final notifier = ref.read(onboardingNotifierProvider.notifier); 107 + final onboardingStateAsync = ref.watch(onboardingProvider); 108 + final notifier = ref.read(onboardingProvider.notifier); 111 109 112 - ref.listen<AsyncValue<OnboardingScreenState>>(onboardingNotifierProvider, (_, next) { 110 + // Initialize controllers from state if not already set 111 + ref.listen<AsyncValue<OnboardingScreenState>>(onboardingProvider, (previous, next) { 113 112 if (next.hasValue && next.value != null) { 114 113 final stateValue = next.value!; 115 - if (_displayNameController.text != stateValue.displayName) { 114 + if (_displayNameController.text.isEmpty && stateValue.displayName.isNotEmpty) { 116 115 _displayNameController.text = stateValue.displayName; 117 116 } 118 - if (_descriptionController.text != stateValue.description) { 117 + if (_descriptionController.text.isEmpty && stateValue.description.isNotEmpty) { 119 118 _descriptionController.text = stateValue.description; 120 119 } 121 120 }
+1 -1
lib/src/features/comments/providers/comment_input_state.dart
··· 5 5 part 'comment_input_state.freezed.dart'; 6 6 7 7 @freezed 8 - class CommentInputState with _$CommentInputState { 8 + abstract class CommentInputState with _$CommentInputState { 9 9 const factory CommentInputState({ 10 10 required TextEditingController textController, 11 11 required ImagePicker imagePicker,
+4 -10
lib/src/features/comments/providers/comment_provider.dart
··· 1 - import 'package:atproto/atproto.dart'; 2 1 import 'package:atproto_core/atproto_core.dart'; 2 + import 'package:bluesky/com_atproto_repo_strongref.dart'; 3 3 import 'package:cached_network_image/cached_network_image.dart'; 4 4 import 'package:flutter/material.dart'; 5 5 import 'package:get_it/get_it.dart'; ··· 23 23 throw Exception('Post not found'); 24 24 case BlockedPost(): 25 25 throw Exception('Post is blocked'); 26 - default: 27 - throw Exception('Unknown thread type'); 28 26 } 29 27 } 30 28 ··· 93 91 final revertedPost = switch (state.thread.post) { 94 92 ThreadPostView(:final post) => ThreadPostView( 95 93 post: post.copyWith( 96 - viewer: wasLiked 97 - ? post.viewer?.copyWith(like: post.viewer?.like) 98 - : post.viewer?.copyWith(like: null), 94 + viewer: wasLiked ? post.viewer?.copyWith(like: post.viewer?.like) : post.viewer?.copyWith(like: null), 99 95 likeCount: currentLikeCount, 100 96 ), 101 97 ), 102 98 ThreadReplyView(:final reply) => ThreadReplyView( 103 99 reply: reply.copyWith( 104 - viewer: wasLiked 105 - ? reply.viewer?.copyWith(like: reply.viewer?.like) 106 - : reply.viewer?.copyWith(like: null), 100 + viewer: wasLiked ? reply.viewer?.copyWith(like: reply.viewer?.like) : reply.viewer?.copyWith(like: null), 107 101 likeCount: currentLikeCount, 108 102 ), 109 103 ), ··· 123 117 } 124 118 } 125 119 126 - Future<StrongRef> postComment( 120 + Future<RepoStrongRef> postComment( 127 121 String text, 128 122 String parentCid, 129 123 String parentUri, {
+1 -1
lib/src/features/comments/providers/comment_state.dart
··· 4 4 part 'comment_state.freezed.dart'; 5 5 6 6 @freezed 7 - class CommentState with _$CommentState { 7 + abstract class CommentState with _$CommentState { 8 8 const factory CommentState({ 9 9 required ThreadViewPost thread, 10 10 @Default(false) bool isVideoInitialized,
-2
lib/src/features/comments/providers/comments_page_provider.dart
··· 27 27 case BlockedPost(): 28 28 throw Exception('Post is blocked'); 29 29 } 30 - throw Exception('Post not found'); 31 30 } catch (e) { 32 31 final networkPost = await feedRepository.getPosts([postUri], bluesky: isBlueskyPost, filter: false); 33 32 if (networkPost.isEmpty) { ··· 43 42 case BlockedPost(): 44 43 throw Exception('Post is blocked'); 45 44 } 46 - throw Exception('Post not found'); 47 45 } 48 46 } 49 47
+1 -1
lib/src/features/comments/providers/comments_page_state.dart
··· 4 4 part 'comments_page_state.freezed.dart'; 5 5 6 6 @freezed 7 - class CommentsPageState with _$CommentsPageState { 7 + abstract class CommentsPageState with _$CommentsPageState { 8 8 const factory CommentsPageState({ 9 9 required ThreadViewPost thread, 10 10 }) = _CommentsPageState;
+1 -1
lib/src/features/comments/ui/widgets/comment_input.dart
··· 73 73 children: [ 74 74 UserAvatar( 75 75 imageUrl: ref 76 - .read(profileNotifierProvider(did: session?.did ?? '')) 76 + .read(profileProvider(did: session?.did ?? '')) 77 77 .when( 78 78 data: (profileData) => profileData.profile?.avatar?.toString() ?? '', 79 79 error: (error, stackTrace) => '',
+5 -6
lib/src/features/comments/ui/widgets/comment_item.dart
··· 1 + import 'package:atproto/com_atproto_moderation_createreport.dart'; 2 + import 'package:atproto/com_atproto_moderation_defs.dart'; 1 3 import 'package:atproto_core/atproto_core.dart'; 2 4 import 'package:auto_route/auto_route.dart'; 3 5 import 'package:fluentui_system_icons/fluentui_system_icons.dart'; ··· 48 50 onSubmit: (subject, reasonType, reason, service) async { 49 51 try { 50 52 final result = await sprkRepository.repo.createReport( 51 - subject: subject, 52 - reasonType: reasonType, 53 - reason: reason, 54 - service: service, 53 + input: ModerationCreateReportInput(subject: subject, reasonType: reasonType as ReasonType, reason: reason), 55 54 ); 56 55 57 56 if (result) { ··· 101 100 102 101 @override 103 102 Widget build(BuildContext context) { 104 - commentState = ref.watch(commentNotifierProvider(widget.thread)); 103 + commentState = ref.watch(commentProvider(widget.thread)); 105 104 const double thumbnailSize = 120; 106 105 107 106 final borderRadius = BorderRadius.circular(8); ··· 247 246 248 247 @override 249 248 Widget build(BuildContext context) { 250 - final notifier = ref.read(commentNotifierProvider(commentState.thread).notifier); 249 + final notifier = ref.read(commentProvider(commentState.thread).notifier); 251 250 return Row( 252 251 children: [ 253 252 TextButton(
+7 -7
lib/src/features/feed/providers/feed_provider.dart
··· 1 1 import 'dart:async'; 2 2 import 'dart:collection'; 3 3 4 - import 'package:atproto/atproto.dart'; 4 + import 'package:atproto/com_atproto_label_defs.dart'; 5 5 import 'package:atproto/core.dart'; 6 6 import 'package:get_it/get_it.dart'; 7 7 import 'package:riverpod_annotation/riverpod_annotation.dart'; ··· 181 181 if (post.record.selfLabels != null) { 182 182 for (final selfLabel in post.record.selfLabels!) { 183 183 postLabels.add( 184 - Label(uri: key, value: selfLabel.value, src: key, createdAt: post.indexedAt), 184 + Label(uri: key, val: selfLabel.val, src: key, cts: post.indexedAt), 185 185 ); 186 186 } 187 187 } ··· 201 201 final existingLabels = value.postLabels; 202 202 203 203 // if the new label is already in the existing labels, check if it should replace the existing one 204 - if (existingLabels.any((label) => label.value == newLabel.value)) { 205 - final existingLabel = existingLabels.firstWhere((label) => label.value == newLabel.value); 204 + if (existingLabels.any((label) => label.val == newLabel.val)) { 205 + final existingLabel = existingLabels.firstWhere((label) => label.val == newLabel.val); 206 206 207 207 // if the new label says that the existing one is negated or expired, replace the existing one 208 - if (((newLabel.ver ?? 0) > (existingLabel.ver ?? 0) && newLabel.isNegate) || 208 + if (((newLabel.ver ?? 0) > (existingLabel.ver ?? 0) && newLabel.isNeg) || 209 209 existingLabel.exp != null && existingLabel.exp!.isBefore(DateTime.now())) { 210 210 existingLabels.remove(existingLabel); 211 211 return ( ··· 391 391 final settings = ref.read(settingsProvider.notifier); 392 392 for (final label in postLabels) { 393 393 try { 394 - final labelPreference = await settings.getLabelPreference(label.value); 394 + final labelPreference = await settings.getLabelPreference(label.val); 395 395 if (labelPreference.setting == Setting.hide || labelPreference.adultOnly) { 396 - _logger.d('Hiding post $uri due to label: ${label.value}'); 396 + _logger.d('Hiding post $uri due to label: ${label.val}'); 397 397 return true; 398 398 } 399 399 } catch (e) {
+1 -1
lib/src/features/feed/providers/feed_refresh_trigger_provider.dart
··· 1 - import 'package:flutter_riverpod/flutter_riverpod.dart'; 1 + import 'package:flutter_riverpod/legacy.dart'; 2 2 import 'package:sparksocial/src/core/network/atproto/data/models/feed_models.dart'; 3 3 4 4 class FeedRefreshTrigger extends StateNotifier<int> {
+1 -1
lib/src/features/feed/providers/feed_state.dart
··· 1 1 import 'dart:collection'; 2 2 3 - import 'package:atproto/atproto.dart'; 3 + import 'package:atproto/com_atproto_label_defs.dart'; 4 4 import 'package:atproto/core.dart'; 5 5 import 'package:freezed_annotation/freezed_annotation.dart'; 6 6 import 'package:sparksocial/src/core/network/atproto/data/models/feed_models.dart';
+2 -3
lib/src/features/feed/providers/like_post.dart
··· 1 - import 'package:atproto/atproto.dart'; 1 + import 'package:atproto/com_atproto_repo_strongref.dart'; 2 2 import 'package:atproto_core/atproto_core.dart'; 3 - import 'package:flutter_riverpod/flutter_riverpod.dart'; 4 3 import 'package:get_it/get_it.dart'; 5 4 import 'package:riverpod_annotation/riverpod_annotation.dart'; 6 5 import 'package:sparksocial/src/core/network/atproto/atproto.dart'; ··· 8 7 part 'like_post.g.dart'; 9 8 10 9 @riverpod 11 - Future<StrongRef> likePost(Ref ref, String postCid, AtUri postUri) async { 10 + Future<RepoStrongRef> likePost(Ref ref, String postCid, AtUri postUri) async { 12 11 try { 13 12 // like post 14 13 return await GetIt.I<SprkRepository>().feed.likePost(postCid, postUri);
+1 -1
lib/src/features/feed/providers/post_updates.dart
··· 1 1 // Provider to track post updates by URI - when a post gets updated, this gets incremented 2 - import 'package:flutter_riverpod/flutter_riverpod.dart'; 2 + import 'package:flutter_riverpod/legacy.dart'; 3 3 4 4 final StateProviderFamily<int, String> postUpdateProvider = StateProvider.family<int, String>((ref, postUri) => 0);
+2 -2
lib/src/features/feed/ui/pages/feed_page.dart
··· 44 44 Widget build(BuildContext context) { 45 45 super.build(context); // Required for AutomaticKeepAliveClientMixin 46 46 47 - final state = ref.watch(feedNotifierProvider(widget.feed)); 48 - final notifier = ref.read(feedNotifierProvider(widget.feed).notifier); 47 + final state = ref.watch(feedProvider(widget.feed)); 48 + final notifier = ref.read(feedProvider(widget.feed).notifier); 49 49 final shouldBeActive = ref.watch(settingsProvider.select((settings) => settings.activeFeed == widget.feed)); 50 50 51 51 ref.listen(feedRefreshTriggerProvider(widget.feed), (previous, next) {
+2 -2
lib/src/features/feed/ui/pages/feeds_page.dart
··· 71 71 // for the debug overlay to update properly 72 72 final feedStates = <Feed, FeedState>{}; 73 73 for (final feed in feeds) { 74 - feedStates[feed] = ref.watch(feedNotifierProvider(feed)); 74 + feedStates[feed] = ref.watch(feedProvider(feed)); 75 75 } 76 76 77 77 // Initialize feeds that haven't been loaded yet 78 78 WidgetsBinding.instance.addPostFrameCallback((_) { 79 79 for (final feed in feeds) { 80 80 final state = feedStates[feed]!; 81 - final notifier = ref.read(feedNotifierProvider(feed).notifier); 81 + final notifier = ref.read(feedProvider(feed).notifier); 82 82 83 83 // Only load if the feed is empty and not already loading and active 84 84 if (state.length == 0 && !state.loadingFirstLoad && !state.isEndOfNetworkFeed && feed == activeFeed) {
+2 -2
lib/src/features/feed/ui/widgets/action_buttons/side_action_bar.dart
··· 89 89 ); 90 90 91 91 if (widget.feed != null) { 92 - ref.read(feedNotifierProvider(widget.feed!).notifier).replacePost(updatedPost); 92 + ref.read(feedProvider(widget.feed!).notifier).replacePost(updatedPost); 93 93 } 94 94 95 95 _currentPost = updatedPost; ··· 104 104 ); 105 105 106 106 if (widget.feed != null) { 107 - ref.read(feedNotifierProvider(widget.feed!).notifier).replacePost(updatedPost); 107 + ref.read(feedProvider(widget.feed!).notifier).replacePost(updatedPost); 108 108 } 109 109 110 110 _currentPost = updatedPost;
+7 -7
lib/src/features/feed/ui/widgets/post/feed_post_widget.dart
··· 1 - import 'package:atproto/atproto.dart'; 1 + import 'package:atproto/com_atproto_label_defs.dart'; 2 2 import 'package:atproto/core.dart'; 3 3 import 'package:auto_route/auto_route.dart'; 4 4 import 'package:flutter/material.dart'; ··· 46 46 } 47 47 48 48 void _loadPost() { 49 - final feedState = ref.read(feedNotifierProvider(widget.feed)); 49 + final feedState = ref.read(feedProvider(widget.feed)); 50 50 if (widget.index < feedState.loadedPosts.length) { 51 51 final post = feedState.loadedPosts[widget.index]; 52 52 final currentUri = post.uri.toString(); ··· 87 87 viewer: postData.viewer?.copyWith(like: newLike.uri) ?? Viewer(like: newLike.uri, repost: postData.viewer?.repost), 88 88 ); 89 89 90 - ref.read(feedNotifierProvider(widget.feed).notifier).replacePost(updatedPost); 90 + ref.read(feedProvider(widget.feed).notifier).replacePost(updatedPost); 91 91 if (mounted) { 92 92 setState(() { 93 93 _overrideIsLiked = true; ··· 99 99 } 100 100 101 101 Future<void> _checkContentWarning(String postUri) async { 102 - final feedState = ref.read(feedNotifierProvider(widget.feed)); 102 + final feedState = ref.read(feedProvider(widget.feed)); 103 103 if (widget.index < feedState.loadedPosts.length) { 104 104 final post = feedState.loadedPosts[widget.index]; 105 105 if (post.uri.toString() != postUri) { ··· 133 133 @override 134 134 Widget build(BuildContext context) { 135 135 // Check if we need to reload post due to state changes 136 - final feedState = ref.watch(feedNotifierProvider(widget.feed)); 136 + final feedState = ref.watch(feedProvider(widget.feed)); 137 137 final navigationState = ref.watch(navigationProvider); 138 138 139 139 // Check if user is not on feeds tab (index 0) ··· 150 150 _lastUpdateCount = updateCount; 151 151 WidgetsBinding.instance.addPostFrameCallback((_) { 152 152 if (mounted) { 153 - ref.read(feedNotifierProvider(widget.feed).notifier).refreshPost(AtUri.parse(currentUri)); 153 + ref.read(feedProvider(widget.feed).notifier).refreshPost(AtUri.parse(currentUri)); 154 154 setState(_loadPost); 155 155 _checkContentWarning(currentUri); 156 156 } ··· 182 182 183 183 // Get labels for the overlay 184 184 var labels = <Label>[]; 185 - final feedState = ref.read(feedNotifierProvider(widget.feed)); 185 + final feedState = ref.read(feedProvider(widget.feed)); 186 186 if (widget.index < feedState.loadedPosts.length) { 187 187 final post = feedState.loadedPosts[widget.index]; 188 188 final extraInfo = feedState.extraInfo[post.uri];
+1 -1
lib/src/features/feed/ui/widgets/post/post_overlay.dart
··· 1 - import 'package:atproto/atproto.dart'; 1 + import 'package:atproto/com_atproto_label_defs.dart'; 2 2 import 'package:flutter/material.dart'; 3 3 import 'package:sparksocial/src/core/network/atproto/data/models/feed_models.dart'; 4 4 import 'package:sparksocial/src/core/utils/label_utils.dart';
+2 -2
lib/src/features/feed/ui/widgets/videos/video_player.dart
··· 151 151 final navigationState = ref.watch(navigationProvider); 152 152 final isOnFeedsTab = navigationState.currentIndex == 0; 153 153 154 - final feedState = widget.feed != null ? ref.watch(feedNotifierProvider(widget.feed!)) : null; 154 + final feedState = widget.feed != null ? ref.watch(feedProvider(widget.feed!)) : null; 155 155 156 156 if (_lastNavigationIndex != navigationState.currentIndex) { 157 157 _lastNavigationIndex = navigationState.currentIndex; ··· 177 177 178 178 final videoAspectRatio = videoController?.videoPlayerController?.value.aspectRatio; 179 179 final videoSize = videoController?.videoPlayerController?.value.size; 180 - 180 + 181 181 final shouldFillScreen = videoAspectRatio != null && videoAspectRatio > 0.5 && videoAspectRatio < 0.7; 182 182 final fitMode = shouldFillScreen ? BoxFit.cover : BoxFit.contain; 183 183
+1 -1
lib/src/features/home/providers/navigation_state.dart
··· 3 3 part 'navigation_state.freezed.dart'; 4 4 5 5 @freezed 6 - class NavigationState with _$NavigationState { 6 + abstract class NavigationState with _$NavigationState { 7 7 const factory NavigationState({ 8 8 @Default(0) int currentIndex, 9 9 }) = _NavigationState;
+1 -1
lib/src/features/home/ui/pages/main_page.dart
··· 78 78 } 79 79 }); 80 80 81 - final profileAsync = userDid != null ? ref.watch(profileNotifierProvider(did: userDid)) : null; 81 + final profileAsync = userDid != null ? ref.watch(profileProvider(did: userDid)) : null; 82 82 final userAvatar = profileAsync?.asData?.value.profile?.avatar?.toString(); 83 83 84 84 final avatarProvider = userAvatar != null && userAvatar.isNotEmpty
-1
lib/src/features/messages/providers/polling_timer.dart
··· 1 1 import 'dart:async'; 2 2 3 - import 'package:flutter_riverpod/flutter_riverpod.dart'; 4 3 import 'package:riverpod_annotation/riverpod_annotation.dart'; 5 4 import 'package:sparksocial/src/features/messages/providers/conversation_provider.dart'; 6 5
+1 -1
lib/src/features/messages/ui/widgets/message_input.dart
··· 39 39 children: [ 40 40 UserAvatar( 41 41 imageUrl: ref 42 - .read(profileNotifierProvider(did: session?.did ?? '')) 42 + .read(profileProvider(did: session?.did ?? '')) 43 43 .when( 44 44 data: (profileData) => profileData.profile?.avatar?.toString() ?? '', 45 45 error: (error, stackTrace) => '',
+1 -1
lib/src/features/posting/providers/camera_state.dart
··· 4 4 part 'camera_state.freezed.dart'; 5 5 6 6 @freezed 7 - class CameraState with _$CameraState { 7 + abstract class CameraState with _$CameraState { 8 8 const factory CameraState({ 9 9 CameraController? controller, 10 10 @Default([]) List<CameraDescription> cameras,
+8 -3
lib/src/features/posting/providers/post_story.dart
··· 1 - import 'package:atproto/atproto.dart'; 2 - import 'package:flutter_riverpod/flutter_riverpod.dart'; 1 + import 'package:atproto/com_atproto_label_defs.dart'; 2 + import 'package:atproto/com_atproto_repo_strongref.dart'; 3 3 import 'package:get_it/get_it.dart'; 4 4 import 'package:riverpod_annotation/riverpod_annotation.dart'; 5 5 import 'package:sparksocial/src/core/network/atproto/atproto.dart'; ··· 7 7 part 'post_story.g.dart'; 8 8 9 9 @riverpod 10 - FutureOr<StrongRef?> postStory(Ref ref, Media media, {List<SelfLabel>? selfLabels, List<String>? tags}) async { 10 + FutureOr<RepoStrongRef?> postStory( 11 + Ref ref, 12 + Media media, { 13 + List<SelfLabel>? selfLabels, 14 + List<String>? tags, 15 + }) async { 11 16 final storyRepository = GetIt.I<StoryRepository>(); 12 17 return await storyRepository.postStory(media, selfLabels: selfLabels, tags: tags); 13 18 }
+1 -1
lib/src/features/posting/providers/recording_state.dart
··· 3 3 part 'recording_state.freezed.dart'; 4 4 5 5 @freezed 6 - class RecordingState with _$RecordingState { 6 + abstract class RecordingState with _$RecordingState { 7 7 const factory RecordingState({ 8 8 @Default(false) bool isRecording, 9 9 @Default(Duration.zero) Duration elapsedDuration,
+10 -9
lib/src/features/posting/providers/video_upload_provider.dart
··· 1 - import 'package:atproto/atproto.dart'; 2 1 import 'package:atproto/core.dart'; 3 - import 'package:flutter_riverpod/flutter_riverpod.dart'; 2 + import 'package:bluesky/com_atproto_repo_strongref.dart'; 4 3 import 'package:get_it/get_it.dart'; 5 4 import 'package:riverpod_annotation/riverpod_annotation.dart'; 6 5 import 'package:sparksocial/src/core/auth/data/repositories/auth_repository.dart'; ··· 30 29 31 30 /// Post a video to the feed using the processed blob reference 32 31 @riverpod 33 - Future<StrongRef?> postVideo( 32 + Future<RepoStrongRef?> postVideo( 34 33 Ref ref, { 35 34 required Blob blob, 36 35 String description = '', 37 36 String altText = '', 38 37 String? videoPath, 39 38 bool crosspostToBsky = false, 40 - StrongRef? soundRef, 39 + RepoStrongRef? soundRef, 41 40 }) async { 42 41 final logger = GetIt.I<LogService>().getLogger('Posting Video'); 43 42 try { ··· 56 55 ); 57 56 58 57 final recordRes = await authAtProto.repo.createRecord( 59 - collection: NSID.parse('so.sprk.feed.post'), 58 + repo: authAtProto.session!.did, 59 + collection: 'so.sprk.feed.post', 60 60 record: postRecord.toJson(), 61 61 ); 62 62 ··· 72 72 } 73 73 } 74 74 logger.i('Video posted successfully: ${recordRes.data.uri}'); 75 - return recordRes.data; 75 + return recordRes.data as RepoStrongRef; 76 76 } catch (error, stackTrace) { 77 77 logger.e('Error posting video', error: error, stackTrace: stackTrace); 78 78 } ··· 81 81 82 82 /// Process video and post it in one step 83 83 @riverpod 84 - Future<StrongRef?> processAndPostVideo( 84 + Future<RepoStrongRef?> processAndPostVideo( 85 85 Ref ref, { 86 86 required String videoPath, 87 87 String description = '', 88 88 String altText = '', 89 89 bool crosspostToBsky = false, 90 90 bool storyMode = false, 91 - StrongRef? soundRef, 91 + RepoStrongRef? soundRef, 92 92 }) async { 93 93 final logger = GetIt.I<LogService>().getLogger('Process/Post Video'); 94 94 logger.d('Processing then posting video: $videoPath (storyMode=$storyMode, sound=${soundRef?.uri})'); ··· 172 172 try { 173 173 final bskyAtProto = authRepository.atproto!; 174 174 final bskyResult = await bskyAtProto.repo.createRecord( 175 - collection: NSID.parse('app.bsky.feed.post'), 175 + repo: session.did, 176 + collection: 'app.bsky.feed.post', 176 177 record: bskyPostRecord, 177 178 rkey: rkey, 178 179 );
+4 -4
lib/src/features/posting/ui/pages/image_review_page.dart
··· 1 - import 'package:atproto/atproto.dart'; 1 + import 'package:atproto/com_atproto_repo_strongref.dart'; 2 2 import 'package:atproto_core/atproto_core.dart'; 3 3 import 'package:auto_route/auto_route.dart'; 4 4 import 'package:flutter/material.dart' hide Image; ··· 100 100 } 101 101 } 102 102 103 - Future<StrongRef?> _uploadImagesAndPost() async { 103 + Future<RepoStrongRef?> _uploadImagesAndPost() async { 104 104 if (_isPosting) return null; 105 105 setState(() { 106 106 _isPosting = true; ··· 108 108 try { 109 109 final crosspostEnabled = widget.storyMode ? false : _crosspostToBsky; 110 110 final description = _descriptionController.text; 111 - StrongRef result; 111 + RepoStrongRef result; 112 112 if (widget.storyMode) { 113 113 final uploadedImage = await _feedRepository.uploadImages( 114 114 imageFiles: _imageFiles, ··· 123 123 ); 124 124 final asyncResult = await ref.read(storyProvider.future); 125 125 if (asyncResult == null) { 126 - throw Exception('Story post returned null StrongRef'); 126 + throw Exception('Story post returned null RepoStrongRef'); 127 127 } 128 128 result = asyncResult; 129 129 } else {
+1 -5
lib/src/features/posting/ui/pages/recording_page.dart
··· 121 121 } 122 122 123 123 await context.router.push( 124 - VideoReviewRoute( 125 - videoPath: result.video.path, 126 - storyMode: widget.storyMode, 127 - soundRef: result.soundRef, 128 - ), 124 + VideoReviewRoute(videoPath: result.video.path, storyMode: widget.storyMode, soundRef: result.soundRef), 129 125 ); 130 126 131 127 if (mounted) {
+3 -2
lib/src/features/posting/ui/pages/video_review_page.dart
··· 1 1 import 'dart:io'; 2 2 3 - import 'package:atproto/atproto.dart'; 3 + import 'package:atproto/com_atproto_repo_strongref.dart'; 4 4 import 'package:atproto/core.dart'; 5 5 import 'package:auto_route/auto_route.dart'; 6 6 import 'package:flutter/material.dart'; ··· 29 29 final bool storyMode; 30 30 31 31 /// Reference to the audio track used in the video, if any. 32 - final StrongRef? soundRef; 32 + /// Stored as JSON string for route serialization. 33 + final RepoStrongRef? soundRef; 33 34 34 35 @override 35 36 ConsumerState<VideoReviewPage> createState() => _VideoReviewPageState();
+7 -3
lib/src/features/profile/providers/edit_profile_provider.dart
··· 79 79 80 80 if (state.localAvatar is Uint8List) { 81 81 // A new avatar image was picked, upload it as a blob. 82 - final respBlob = await atprotoClient.repo.uploadBlob(state.localAvatar as Uint8List); 82 + final respBlob = await atprotoClient.repo.uploadBlob(bytes: state.localAvatar as Uint8List); 83 83 if (respBlob.status.code != 200) { 84 84 throw Exception('Failed to upload avatar blob'); 85 85 } ··· 93 93 // and we need to maintain the existing one by fetching its Blob from the record. 94 94 logger.d('Maintaining existing avatar from record ${state.profile.did}'); 95 95 final uri = AtUri.parse('at://${state.profile.did}/so.sprk.actor.profile/self'); 96 - final recRes = await atprotoClient.repo.getRecord(uri: uri); 96 + final recRes = await atprotoClient.repo.getRecord( 97 + collection: uri.collection.toString(), 98 + repo: uri.hostname, 99 + rkey: uri.rkey, 100 + ); 97 101 final recordData = recRes.data.value; 98 102 99 103 // Ensure the 'avatar' field exists and is a Map before converting to Blob. ··· 119 123 120 124 // Invalidate the main profile provider to trigger a refresh 121 125 if (state.profile.did == _authRepository.session?.did) { 122 - ref.invalidate(profileNotifierProvider(did: state.profile.did)); 126 + ref.invalidate(profileProvider(did: state.profile.did)); 123 127 } 124 128 125 129 state = state.copyWith(isSaving: false);
+1 -1
lib/src/features/profile/providers/edit_profile_state.dart
··· 4 4 part 'edit_profile_state.freezed.dart'; 5 5 6 6 @freezed 7 - class EditProfileState with _$EditProfileState { 7 + abstract class EditProfileState with _$EditProfileState { 8 8 const factory EditProfileState({ 9 9 required ProfileViewDetailed profile, 10 10 required String displayName,
+4 -4
lib/src/features/profile/providers/profile_feed_provider.dart
··· 1 1 import 'dart:collection'; 2 2 3 - import 'package:atproto/atproto.dart'; 3 + import 'package:atproto/com_atproto_label_defs.dart'; 4 4 import 'package:atproto_core/atproto_core.dart'; 5 5 import 'package:get_it/get_it.dart'; 6 6 import 'package:riverpod_annotation/riverpod_annotation.dart'; ··· 215 215 final settings = ref.read(settingsProvider.notifier); 216 216 for (final label in postLabels) { 217 217 try { 218 - final labelPreference = await settings.getLabelPreference(label.value); 218 + final labelPreference = await settings.getLabelPreference(label.val); 219 219 if (labelPreference.setting == Setting.hide || labelPreference.adultOnly) { 220 220 return true; 221 221 } ··· 251 251 postLabels.add( 252 252 Label( 253 253 uri: postView.uri.toString(), 254 - value: selfLabel.value, 254 + val: selfLabel.val, 255 255 src: postView.uri.toString(), 256 - createdAt: postView.indexedAt, 256 + cts: postView.indexedAt, 257 257 ), 258 258 ); 259 259 }
+1 -1
lib/src/features/profile/providers/profile_feed_state.dart
··· 1 1 import 'dart:collection'; 2 2 3 - import 'package:atproto/atproto.dart'; 3 + import 'package:atproto/com_atproto_label_defs.dart'; 4 4 import 'package:atproto_core/atproto_core.dart'; 5 5 import 'package:freezed_annotation/freezed_annotation.dart'; 6 6 import 'package:sparksocial/src/core/network/atproto/data/models/feed_models.dart';
+8 -4
lib/src/features/profile/providers/profile_provider.dart
··· 1 1 import 'dart:async'; 2 2 3 - import 'package:atproto/atproto.dart' as atp; 3 + import 'package:atproto/com_atproto_admin_defs.dart'; 4 + import 'package:atproto/com_atproto_moderation_createreport.dart'; 5 + import 'package:atproto/com_atproto_moderation_defs.dart'; 4 6 import 'package:atproto_core/atproto_core.dart'; 5 7 import 'package:get_it/get_it.dart'; 6 8 import 'package:riverpod_annotation/riverpod_annotation.dart'; ··· 178 180 } 179 181 } 180 182 181 - Future<bool> createReport({required String did, required atp.ModerationReasonType reasonType, String? reason}) async { 183 + Future<bool> createReport({required String did, required KnownReasonType reasonType, String? reason}) async { 182 184 if (!authRepository.isAuthenticated) { 183 185 logger.w('Cannot create report, user not authenticated'); 184 186 final currentData = state.asData?.value; ··· 190 192 191 193 try { 192 194 logger.d('Creating report for DID: $did with reason: $reasonType'); 193 - final subject = atp.ReportSubject.repoRef(data: atp.RepoRef(did: did)); 194 - final result = await sprkRepository.repo.createReport(subject: subject, reasonType: reasonType, reason: reason); 195 + final subject = UModerationCreateReportSubject.repoRef(data: RepoRef(did: did)); 196 + final result = await sprkRepository.repo.createReport( 197 + input: ModerationCreateReportInput(subject: subject, reasonType: reasonType as ReasonType, reason: reason), 198 + ); 195 199 logger.i('Report created successfully for $did'); 196 200 return result; 197 201 } catch (e, s) {
+1 -1
lib/src/features/profile/providers/profile_state.dart
··· 4 4 part 'profile_state.freezed.dart'; 5 5 6 6 @freezed 7 - class ProfileState with _$ProfileState { 7 + abstract class ProfileState with _$ProfileState { 8 8 const factory ProfileState({ 9 9 ProfileViewDetailed? profile, 10 10 @Default(false) bool isEarlySupporter,
+5 -5
lib/src/features/profile/providers/user_list_provider.dart
··· 72 72 final session = _authRepository.session; 73 73 if (session != null) { 74 74 final bskyClient = bsky.Bluesky.fromSession(session); 75 - final fetchedProfiles = <bsky.ActorProfile>[]; 75 + final fetchedProfiles = <dynamic>[]; 76 76 77 77 for (var i = 0; i < didsToFetch.length; i += 25) { 78 78 final batch = didsToFetch.sublist(i, i + 25 > didsToFetch.length ? didsToFetch.length : i + 25); ··· 86 86 if (profilesMap.containsKey(profile.did)) { 87 87 final fetchedProfile = profilesMap[profile.did]!; 88 88 profiles[i] = profile.copyWith( 89 - displayName: fetchedProfile.displayName, 90 - description: fetchedProfile.description, 91 - handle: fetchedProfile.handle, 92 - avatar: fetchedProfile.avatar != null ? Uri.parse(fetchedProfile.avatar!) : null, 89 + displayName: fetchedProfile.displayName as String?, 90 + description: fetchedProfile.description as String?, 91 + handle: fetchedProfile.handle as String, 92 + avatar: fetchedProfile.avatar != null ? Uri.parse(fetchedProfile.avatar as String) : null, 93 93 ); 94 94 } 95 95 }
+4 -4
lib/src/features/profile/ui/pages/profile_page.dart
··· 81 81 82 82 @override 83 83 Widget build(BuildContext context) { 84 - final profileStateAsync = ref.watch(profileNotifierProvider(did: widget.did)); 85 - final notifier = ref.read(profileNotifierProvider(did: widget.did).notifier); 84 + final profileStateAsync = ref.watch(profileProvider(did: widget.did)); 85 + final notifier = ref.read(profileProvider(did: widget.did).notifier); 86 86 final theme = Theme.of(context); 87 87 final colorScheme = theme.colorScheme; 88 88 ··· 145 145 onFollowTap: () async { 146 146 try { 147 147 await notifier.toggleFollow(); 148 - final latestProfileState = ref.read(profileNotifierProvider(did: widget.did)).asData?.value; 148 + final latestProfileState = ref.read(profileProvider(did: widget.did)).asData?.value; 149 149 150 150 if (latestProfileState != null && !latestProfileState.showAuthPrompt) { 151 151 if (context.mounted) { ··· 168 168 onUnfollowTap: () async { 169 169 try { 170 170 await notifier.toggleFollow(); 171 - final latestProfileState = ref.read(profileNotifierProvider(did: widget.did)).asData?.value; 171 + final latestProfileState = ref.read(profileProvider(did: widget.did)).asData?.value; 172 172 173 173 if (latestProfileState != null && !latestProfileState.showAuthPrompt) { 174 174 if (context.mounted) {
+8 -3
lib/src/features/search/providers/post_search_provider.dart
··· 1 1 import 'dart:async'; 2 2 3 3 import 'package:atproto_core/atproto_core.dart'; 4 + import 'package:bluesky/app_bsky_feed_searchposts.dart'; 4 5 import 'package:bluesky/bluesky.dart' as bsky; 5 6 import 'package:get_it/get_it.dart'; 6 7 import 'package:riverpod_annotation/riverpod_annotation.dart'; ··· 64 65 65 66 final bskyApi = bsky.Bluesky.fromSession(bskySession); 66 67 final sprkSearch = _feedRepository.searchPosts(query); 67 - final bskySearch = bskyApi.feed.searchPosts(query, sort: 'top'); 68 + final bskySearch = bskyApi.feed.searchPosts(q: query, sort: KnownFeedSearchPostsSort.top as FeedSearchPostsSort); 68 69 69 70 final results = await Future.wait([sprkSearch, bskySearch]); 70 71 71 72 final sprkResponse = results[0] as ({String? cursor, List<PostView> posts}); 72 - final bskyResponse = results[1] as XRPCResponse<bsky.PostsByQuery>; 73 + final bskyResponse = results[1] as XRPCResponse<FeedSearchPostsOutput>; 73 74 74 75 final bskyPosts = bskyResponse.data.posts 75 76 .asMap() ··· 168 169 return; 169 170 } 170 171 final bskyApi = bsky.Bluesky.fromSession(bskySession); 171 - final response = await bskyApi.feed.searchPosts(state.query, sort: 'latest', cursor: bskyCursor); 172 + final response = await bskyApi.feed.searchPosts( 173 + q: state.query, 174 + sort: KnownFeedSearchPostsSort.latest as FeedSearchPostsSort, 175 + cursor: bskyCursor, 176 + ); 172 177 173 178 final bskyPosts = response.data.posts 174 179 .asMap()
+1 -1
lib/src/features/search/providers/post_search_state.dart
··· 6 6 7 7 /// Represents the state of the post search 8 8 @freezed 9 - class PostSearchState with _$PostSearchState { 9 + abstract class PostSearchState with _$PostSearchState { 10 10 /// Creates a new post search state 11 11 const factory PostSearchState({ 12 12 /// Whether search results are loading
+1 -1
lib/src/features/search/providers/search_state.dart
··· 6 6 7 7 /// Represents the state of the search screen 8 8 @freezed 9 - class SearchState with _$SearchState { 9 + abstract class SearchState with _$SearchState { 10 10 /// Creates a new search state 11 11 const factory SearchState({ 12 12 /// Whether search results are loading
-1
lib/src/features/settings/providers/settings_provider.dart
··· 1 - import 'package:flutter_riverpod/flutter_riverpod.dart'; 2 1 import 'package:get_it/get_it.dart'; 3 2 import 'package:riverpod_annotation/riverpod_annotation.dart'; 4 3 import 'package:sparksocial/src/core/network/atproto/data/models/models.dart';
+1 -1
lib/src/features/settings/providers/settings_state.dart
··· 5 5 6 6 // Settings currently loaded 7 7 @freezed 8 - class SettingsState with _$SettingsState { 8 + abstract class SettingsState with _$SettingsState { 9 9 const factory SettingsState({ 10 10 required Feed activeFeed, 11 11 @Default([]) List<Feed> feeds,
+10 -7
lib/src/features/settings/ui/pages/profile_settings_page.dart
··· 8 8 import 'package:sparksocial/src/core/routing/app_router.dart'; 9 9 import 'package:sparksocial/src/core/utils/logging/log_service.dart'; 10 10 import 'package:sparksocial/src/features/auth/auth.dart'; 11 - import 'package:sparksocial/src/features/profile/providers/profile_provider.dart'; 11 + import 'package:sparksocial/src/features/auth/providers/auth_providers.dart'; 12 12 13 13 @RoutePage() 14 14 class ProfileSettingsPage extends ConsumerStatefulWidget { ··· 30 30 ), 31 31 ); 32 32 33 - // Get the profile notifier and call logout 34 - final profileNotifier = ref.read(profileNotifierProvider().notifier); 35 - await profileNotifier.logout(); 33 + // Call logout on the auth provider 34 + await ref.read(authProvider.notifier).logout(); 36 35 37 - // Close loading dialog 38 36 if (mounted) { 37 + // Close loading dialog first 38 + Navigator.of(context).pop(); 39 + 39 40 // Navigate to login screen 40 41 context.router.replaceAll([const RegisterRoute()]); 41 42 } ··· 85 86 logger.i('Fetching Spark posts for DID: $did'); 86 87 87 88 // Fetch Spark posts directly from atproto to get raw records with URIs 88 - final collection = NSID.parse('so.sprk.feed.post'); 89 + const collection = 'so.sprk.feed.post'; 89 90 logger.d('Fetching records from collection: $collection'); 90 91 91 92 final result = await atproto.repo.listRecords( ··· 163 164 try { 164 165 // Update the record in the PDS with the converted value 165 166 final result = await atproto.repo.putRecord( 166 - uri: post.uri, 167 + repo: did, 168 + collection: collection, 169 + rkey: post.uri.rkey, 167 170 record: post.convertedValue, 168 171 ); 169 172
-1
lib/src/features/stories/providers/stories_by_author.dart
··· 1 - import 'package:flutter_riverpod/flutter_riverpod.dart'; 2 1 import 'package:get_it/get_it.dart'; 3 2 import 'package:riverpod_annotation/riverpod_annotation.dart'; 4 3 import 'package:sparksocial/src/core/network/atproto/atproto.dart';
+1 -2
lib/src/features/stories/providers/story_auto_delete_provider.dart
··· 1 1 import 'package:atproto_core/atproto_core.dart'; 2 - import 'package:flutter_riverpod/flutter_riverpod.dart'; 3 2 import 'package:get_it/get_it.dart'; 4 3 import 'package:riverpod_annotation/riverpod_annotation.dart'; 5 4 import 'package:sparksocial/src/core/network/atproto/data/repositories/sprk_repository.dart'; ··· 61 60 do { 62 61 final page = await atproto.repo.listRecords( 63 62 repo: did, 64 - collection: NSID.parse(collection), 63 + collection: collection, 65 64 cursor: cursor, 66 65 limit: 100, 67 66 );
+2 -2
lib/src/features/stories/providers/story_manager_provider.dart
··· 56 56 do { 57 57 final result = await atproto.repo.listRecords( 58 58 repo: did, 59 - collection: NSID.parse(collection), 59 + collection: collection, 60 60 cursor: cursor, 61 61 limit: 100, 62 62 ); ··· 85 85 } 86 86 87 87 Future<void> deleteStory(StoryView story) async { 88 - final current = state.valueOrNull; 88 + final current = state.value; 89 89 if (current == null) return; 90 90 try { 91 91 // Optimistic update
+2 -2
lib/src/features/stories/ui/pages/story_manager_page.dart
··· 12 12 const StoryManagerPage({super.key}); 13 13 14 14 void _openStoryViewer(BuildContext context, WidgetRef ref, int index) { 15 - final state = ref.read(storyManagerProvider).valueOrNull; 15 + final state = ref.read(storyManagerProvider).value; 16 16 if (state == null) return; 17 17 // Build map required by AllStoriesRoute (single author -> list) 18 18 if (state.stories.isEmpty) return; ··· 27 27 28 28 Future<void> _deleteStory(BuildContext context, WidgetRef ref, int index) async { 29 29 final notifier = ref.read(storyManagerProvider.notifier); 30 - final stories = ref.read(storyManagerProvider).valueOrNull?.stories ?? []; 30 + final stories = ref.read(storyManagerProvider).value?.stories ?? []; 31 31 if (index >= stories.length) return; 32 32 final story = stories[index]; 33 33 final shouldDelete =
+1 -1
lib/src/features/stories/ui/widgets/stories_list.dart
··· 72 72 // First item is always the create button 73 73 if (index == 0) { 74 74 final userAvatarUrl = ref 75 - .read(profileNotifierProvider(did: currentUserDid!)) 75 + .read(profileProvider(did: currentUserDid!)) 76 76 .when( 77 77 data: (profileData) => profileData.profile!.avatar.toString(), 78 78 error: (error, stackTrace) => '',
+1 -1
lib/src/sprk_app.dart
··· 44 44 final activeFeed = ref.read(settingsProvider).activeFeed; 45 45 _logger.d('Active feed: ${activeFeed.config.value}'); 46 46 47 - final feedNotifier = ref.read(feedNotifierProvider(activeFeed).notifier); 47 + final feedNotifier = ref.read(feedProvider(activeFeed).notifier); 48 48 feedNotifier.loadAndUpdateFirstLoad(); 49 49 _logger.d('Feed loading started'); 50 50 } catch (e) {
+154 -114
pubspec.lock
··· 5 5 dependency: transitive 6 6 description: 7 7 name: _fe_analyzer_shared 8 - sha256: da0d9209ca76bde579f2da330aeb9df62b6319c834fa7baae052021b0462401f 8 + sha256: c209688d9f5a5f26b2fb47a188131a6fb9e876ae9e47af3737c0b4f58a93470d 9 9 url: "https://pub.dev" 10 10 source: hosted 11 - version: "85.0.0" 11 + version: "91.0.0" 12 12 accessibility_tools: 13 13 dependency: transitive 14 14 description: ··· 21 21 dependency: transitive 22 22 description: 23 23 name: analyzer 24 - sha256: f4ad0fea5f102201015c9aae9d93bc02f75dd9491529a8c21f88d17a8523d44c 24 + sha256: a40a0cee526a7e1f387c6847bd8a5ccbf510a75952ef8a28338e989558072cb0 25 + url: "https://pub.dev" 26 + source: hosted 27 + version: "8.4.0" 28 + analyzer_buffer: 29 + dependency: transitive 30 + description: 31 + name: analyzer_buffer 32 + sha256: aba2f75e63b3135fd1efaa8b6abefe1aa6e41b6bd9806221620fa48f98156033 25 33 url: "https://pub.dev" 26 34 source: hosted 27 - version: "7.6.0" 35 + version: "0.1.11" 28 36 analyzer_plugin: 29 37 dependency: transitive 30 38 description: 31 39 name: analyzer_plugin 32 - sha256: a5ab7590c27b779f3d4de67f31c4109dbe13dd7339f86461a6f2a8ab2594d8ce 40 + sha256: "08cfefa90b4f4dd3b447bda831cecf644029f9f8e22820f6ee310213ebe2dd53" 33 41 url: "https://pub.dev" 34 42 source: hosted 35 - version: "0.13.4" 43 + version: "0.13.10" 36 44 ansicolor: 37 45 dependency: transitive 38 46 description: ··· 73 81 url: "https://pub.dev" 74 82 source: hosted 75 83 version: "2.13.0" 76 - at_identifier: 84 + at_primitives: 77 85 dependency: transitive 78 86 description: 79 - name: at_identifier 80 - sha256: "7c8778202d17ec4e63b38a6a58480503fbf0d7fc1d62e0d64580a9b6cbe142f7" 87 + name: at_primitives 88 + sha256: "016ffaa419b01befe6f5c6869c84d570060a5388655354a25668dbb1ad8ade3f" 81 89 url: "https://pub.dev" 82 90 source: hosted 83 - version: "0.2.2" 84 - at_uri: 85 - dependency: transitive 86 - description: 87 - name: at_uri 88 - sha256: "1156d9d70460fcfcb30e744d7f8c7d544eff073b3142b772f0d02aca10dd064f" 89 - url: "https://pub.dev" 90 - source: hosted 91 - version: "0.4.0" 91 + version: "1.0.0" 92 92 atproto: 93 93 dependency: "direct main" 94 94 description: 95 95 name: atproto 96 - sha256: "0f3d342c4d629e9994d58dbadd4281074641ac75a18cd514b212a3b15f86019e" 96 + sha256: "33a8355a7eee37c87ac08242911693a6037ebb0a39cde8f6a7cc52e09324c4f9" 97 97 url: "https://pub.dev" 98 98 source: hosted 99 - version: "0.13.3" 99 + version: "1.2.4" 100 100 atproto_core: 101 101 dependency: "direct main" 102 102 description: 103 103 name: atproto_core 104 - sha256: "13e7f5f0f3d9e5be59eefd5f427adf45ffdeaa59001d4ea7c91764ba21f1e9ba" 104 + sha256: "0f060b31745d01bcf73e8a25fc1c24ecf14dc78039d59a526ae9d7653f4f8fe7" 105 105 url: "https://pub.dev" 106 106 source: hosted 107 - version: "0.11.2" 107 + version: "1.0.7" 108 108 atproto_oauth: 109 109 dependency: transitive 110 110 description: 111 111 name: atproto_oauth 112 - sha256: "8a0c64455c38c45773ebab5fdd55bf214541461f3a97fe0e6184a5eeb8222f03" 112 + sha256: "3d51fec3a73382d816751afdcf3e7b9cc2f092e4ce6b6b1433d2a128b93dcb29" 113 113 url: "https://pub.dev" 114 114 source: hosted 115 - version: "0.1.0" 115 + version: "0.1.2" 116 116 audio_waveforms: 117 117 dependency: "direct main" 118 118 description: ··· 189 189 dependency: "direct dev" 190 190 description: 191 191 name: auto_route_generator 192 - sha256: "2a5b5bf9c55d4a2098931037dac90921a4663808aed494bb4f134d82d46cb8ec" 192 + sha256: a84dcd972e3e38c8925cca2669faa8112a79db9b5d726e0fb8d4ea15ced095fb 193 193 url: "https://pub.dev" 194 194 source: hosted 195 - version: "10.2.3" 195 + version: "10.3.1" 196 196 base_codecs: 197 197 dependency: transitive 198 198 description: ··· 213 213 dependency: "direct main" 214 214 description: 215 215 name: bluesky 216 - sha256: "207135e189278936dfc6bad0d59835a359f06b97ecd73eee1bccf6b993969428" 216 + sha256: eea4944e15fca6c72838cdfcd1ed440a4da450e3f97d0f420c80c63229bcdddf 217 217 url: "https://pub.dev" 218 218 source: hosted 219 - version: "0.18.10" 219 + version: "1.2.6" 220 220 boolean_selector: 221 221 dependency: transitive 222 222 description: ··· 237 237 dependency: transitive 238 238 description: 239 239 name: build 240 - sha256: "51dc711996cbf609b90cbe5b335bbce83143875a9d58e4b5c6d3c4f684d3dda7" 240 + sha256: c1668065e9ba04752570ad7e038288559d1e2ca5c6d0131c0f5f55e39e777413 241 241 url: "https://pub.dev" 242 242 source: hosted 243 - version: "2.5.4" 243 + version: "4.0.3" 244 244 build_config: 245 245 dependency: transitive 246 246 description: 247 247 name: build_config 248 - sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33" 248 + sha256: "4f64382b97504dc2fcdf487d5aae33418e08b4703fc21249e4db6d804a4d0187" 249 249 url: "https://pub.dev" 250 250 source: hosted 251 - version: "1.1.2" 251 + version: "1.2.0" 252 252 build_daemon: 253 253 dependency: transitive 254 254 description: ··· 257 257 url: "https://pub.dev" 258 258 source: hosted 259 259 version: "4.1.1" 260 - build_resolvers: 261 - dependency: transitive 262 - description: 263 - name: build_resolvers 264 - sha256: ee4257b3f20c0c90e72ed2b57ad637f694ccba48839a821e87db762548c22a62 265 - url: "https://pub.dev" 266 - source: hosted 267 - version: "2.5.4" 268 260 build_runner: 269 261 dependency: "direct dev" 270 262 description: 271 263 name: build_runner 272 - sha256: "382a4d649addbfb7ba71a3631df0ec6a45d5ab9b098638144faf27f02778eb53" 264 + sha256: "110c56ef29b5eb367b4d17fc79375fa8c18a6cd7acd92c05bb3986c17a079057" 273 265 url: "https://pub.dev" 274 266 source: hosted 275 - version: "2.5.4" 276 - build_runner_core: 277 - dependency: transitive 278 - description: 279 - name: build_runner_core 280 - sha256: "85fbbb1036d576d966332a3f5ce83f2ce66a40bea1a94ad2d5fc29a19a0d3792" 281 - url: "https://pub.dev" 282 - source: hosted 283 - version: "9.1.2" 267 + version: "2.10.4" 284 268 built_collection: 285 269 dependency: transitive 286 270 description: ··· 333 317 dependency: transitive 334 318 description: 335 319 name: camera_android_camerax 336 - sha256: "360a9436980590e7268e0ff9eff482ff73ac3e0f66fffdd203cd93d1bfed0fc4" 320 + sha256: "474d8355961658d43f1c976e2fa1ca715505bea1adbd56df34c581aaa70ec41f" 337 321 url: "https://pub.dev" 338 322 source: hosted 339 - version: "0.6.25+1" 323 + version: "0.6.26+2" 340 324 camera_avfoundation: 341 325 dependency: transitive 342 326 description: ··· 393 377 url: "https://pub.dev" 394 378 source: hosted 395 379 version: "2.0.4" 380 + cli_config: 381 + dependency: transitive 382 + description: 383 + name: cli_config 384 + sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec 385 + url: "https://pub.dev" 386 + source: hosted 387 + version: "0.2.0" 396 388 cli_util: 397 389 dependency: transitive 398 390 description: ··· 433 425 url: "https://pub.dev" 434 426 source: hosted 435 427 version: "3.1.2" 428 + coverage: 429 + dependency: transitive 430 + description: 431 + name: coverage 432 + sha256: "5da775aa218eaf2151c721b16c01c7676fbfdd99cebba2bf64e8b807a28ff94d" 433 + url: "https://pub.dev" 434 + source: hosted 435 + version: "1.15.0" 436 436 cross_file: 437 437 dependency: transitive 438 438 description: ··· 469 469 dependency: transitive 470 470 description: 471 471 name: custom_lint_core 472 - sha256: "31110af3dde9d29fb10828ca33f1dce24d2798477b167675543ce3d208dee8be" 472 + sha256: "85b339346154d5646952d44d682965dfe9e12cae5febd706f0db3aa5010d6423" 473 473 url: "https://pub.dev" 474 474 source: hosted 475 - version: "0.7.5" 475 + version: "0.8.1" 476 476 custom_lint_visitor: 477 477 dependency: transitive 478 478 description: 479 479 name: custom_lint_visitor 480 - sha256: "4a86a0d8415a91fbb8298d6ef03e9034dc8e323a599ddc4120a0e36c433983a2" 480 + sha256: "91f2a81e9f0abb4b9f3bb529f78b6227ce6050300d1ae5b1e2c69c66c7a566d8" 481 481 url: "https://pub.dev" 482 482 source: hosted 483 - version: "1.0.0+7.7.0" 483 + version: "1.0.0+8.4.0" 484 484 dart_multihash: 485 485 dependency: transitive 486 486 description: ··· 493 493 dependency: transitive 494 494 description: 495 495 name: dart_style 496 - sha256: "8a0e5fba27e8ee025d2ffb4ee820b4e6e2cf5e4246a6b1a477eb66866947e0bb" 496 + sha256: a9c30492da18ff84efe2422ba2d319a89942d93e58eb0b73d32abe822ef54b7b 497 497 url: "https://pub.dev" 498 498 source: hosted 499 - version: "3.1.1" 499 + version: "3.1.3" 500 500 dbus: 501 501 dependency: transitive 502 502 description: ··· 647 647 dependency: "direct main" 648 648 description: 649 649 name: flutter_riverpod 650 - sha256: "9532ee6db4a943a1ed8383072a2e3eeda041db5657cdf6d2acecf3c21ecbe7e1" 650 + sha256: "9e2d6907f12cc7d23a846847615941bddee8709bf2bfd274acdf5e80bcf22fde" 651 651 url: "https://pub.dev" 652 652 source: hosted 653 - version: "2.6.1" 653 + version: "3.0.3" 654 654 flutter_secure_storage: 655 655 dependency: "direct main" 656 656 description: ··· 729 729 dependency: "direct main" 730 730 description: 731 731 name: freezed 732 - sha256: "59a584c24b3acdc5250bb856d0d3e9c0b798ed14a4af1ddb7dc1c7b41df91c9c" 732 + sha256: "13065f10e135263a4f5a4391b79a8efc5fb8106f8dd555a9e49b750b45393d77" 733 733 url: "https://pub.dev" 734 734 source: hosted 735 - version: "2.5.8" 735 + version: "3.2.3" 736 736 freezed_annotation: 737 737 dependency: "direct main" 738 738 description: 739 739 name: freezed_annotation 740 - sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2 740 + sha256: "7294967ff0a6d98638e7acb774aac3af2550777accd8149c90af5b014e6d44d8" 741 741 url: "https://pub.dev" 742 742 source: hosted 743 - version: "2.4.4" 743 + version: "3.1.0" 744 744 frontend_server_client: 745 745 dependency: transitive 746 746 description: ··· 969 969 dependency: "direct dev" 970 970 description: 971 971 name: json_serializable 972 - sha256: c50ef5fc083d5b5e12eef489503ba3bf5ccc899e487d691584699b4bdefeea8c 972 + sha256: c5b2ee75210a0f263c6c7b9eeea80553dbae96ea1bf57f02484e806a3ffdffa3 973 973 url: "https://pub.dev" 974 974 source: hosted 975 - version: "6.9.5" 975 + version: "6.11.2" 976 976 leak_tracker: 977 977 dependency: transitive 978 978 description: ··· 1001 1001 dependency: transitive 1002 1002 description: 1003 1003 name: lean_builder 1004 - sha256: ef5cd5f907157eb7aa87d1704504b5a6386d2cbff88a3c2b3344477bab323ee9 1004 + sha256: f3ddb3b2c29285a726d739f3eaa4877a2478068f975b51231d032711efbc02d8 1005 1005 url: "https://pub.dev" 1006 1006 source: hosted 1007 - version: "0.1.2" 1007 + version: "0.1.4" 1008 1008 lints: 1009 1009 dependency: transitive 1010 1010 description: ··· 1049 1049 dependency: transitive 1050 1050 description: 1051 1051 name: mime 1052 - sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a" 1052 + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" 1053 1053 url: "https://pub.dev" 1054 1054 source: hosted 1055 - version: "1.0.6" 1056 - multiformats: 1055 + version: "2.0.0" 1056 + mockito: 1057 1057 dependency: transitive 1058 1058 description: 1059 - name: multiformats 1060 - sha256: aa2fa36d2e4d0069dac993b35ee52e5165d67f15b995d68f797466065a6d05a5 1059 + name: mockito 1060 + sha256: dac24d461418d363778d53198d9ac0510b9d073869f078450f195766ec48d05e 1061 1061 url: "https://pub.dev" 1062 1062 source: hosted 1063 - version: "0.2.3" 1064 - nanoid: 1063 + version: "5.6.1" 1064 + multiformats: 1065 1065 dependency: transitive 1066 1066 description: 1067 - name: nanoid 1068 - sha256: be3f8752d9046c825df2f3914195151eb876f3ad64b9d833dd0b799b77b8759e 1067 + name: multiformats 1068 + sha256: a6e85f37aa228e82b5644fc68d28f76622fe894d9191ebd1f994296325a89482 1069 1069 url: "https://pub.dev" 1070 1070 source: hosted 1071 - version: "1.0.0" 1071 + version: "1.0.2" 1072 1072 nested: 1073 1073 dependency: transitive 1074 1074 description: ··· 1077 1077 url: "https://pub.dev" 1078 1078 source: hosted 1079 1079 version: "1.0.0" 1080 - nsid: 1080 + node_preamble: 1081 1081 dependency: transitive 1082 1082 description: 1083 - name: nsid 1084 - sha256: f0e58c3899f7c224a7c9fb991be5bb2c18de0f920bec4e807ae2d3572cb718c1 1083 + name: node_preamble 1084 + sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" 1085 1085 url: "https://pub.dev" 1086 1086 source: hosted 1087 - version: "0.4.1" 1087 + version: "2.0.2" 1088 1088 octo_image: 1089 1089 dependency: transitive 1090 1090 description: ··· 1233 1233 dependency: "direct main" 1234 1234 description: 1235 1235 name: posthog_flutter 1236 - sha256: "98ee15a00b1d963462e65f13531d03f67033795181b25a53c56a0e83cd9d2cdd" 1236 + sha256: "93d26162898320a21c444732bab21be8f53690e70fd991e4fbbd90ac6f69b753" 1237 1237 url: "https://pub.dev" 1238 1238 source: hosted 1239 - version: "5.9.0" 1239 + version: "5.9.1" 1240 1240 pro_image_editor: 1241 1241 dependency: "direct main" 1242 1242 description: ··· 1282 1282 dependency: transitive 1283 1283 description: 1284 1284 name: riverpod 1285 - sha256: "59062512288d3056b2321804332a13ffdd1bf16df70dcc8e506e411280a72959" 1285 + sha256: c406de02bff19d920b832bddfb8283548bfa05ce41c59afba57ce643e116aa59 1286 1286 url: "https://pub.dev" 1287 1287 source: hosted 1288 - version: "2.6.1" 1288 + version: "3.0.3" 1289 1289 riverpod_analyzer_utils: 1290 1290 dependency: transitive 1291 1291 description: 1292 1292 name: riverpod_analyzer_utils 1293 - sha256: "837a6dc33f490706c7f4632c516bcd10804ee4d9ccc8046124ca56388715fdf3" 1293 + sha256: a0f68adb078b790faa3c655110a017f9a7b7b079a57bbd40f540e80dce5fcd29 1294 1294 url: "https://pub.dev" 1295 1295 source: hosted 1296 - version: "0.5.9" 1296 + version: "1.0.0-dev.7" 1297 1297 riverpod_annotation: 1298 1298 dependency: "direct main" 1299 1299 description: 1300 1300 name: riverpod_annotation 1301 - sha256: e14b0bf45b71326654e2705d462f21b958f987087be850afd60578fcd502d1b8 1301 + sha256: "7230014155777fc31ba3351bc2cb5a3b5717b11bfafe52b1553cb47d385f8897" 1302 1302 url: "https://pub.dev" 1303 1303 source: hosted 1304 - version: "2.6.1" 1304 + version: "3.0.3" 1305 1305 riverpod_generator: 1306 1306 dependency: "direct dev" 1307 1307 description: 1308 1308 name: riverpod_generator 1309 - sha256: "120d3310f687f43e7011bb213b90a436f1bbc300f0e4b251a72c39bccb017a4f" 1309 + sha256: "49894543a42cf7a9954fc4e7366b6d3cb2e6ec0fa07775f660afcdd92d097702" 1310 1310 url: "https://pub.dev" 1311 1311 source: hosted 1312 - version: "2.6.4" 1312 + version: "3.0.3" 1313 1313 rxdart: 1314 1314 dependency: transitive 1315 1315 description: ··· 1322 1322 dependency: "direct main" 1323 1323 description: 1324 1324 name: shared_preferences 1325 - sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" 1325 + sha256: "2939ae520c9024cb197fc20dee269cd8cdbf564c8b5746374ec6cacdc5169e64" 1326 1326 url: "https://pub.dev" 1327 1327 source: hosted 1328 - version: "2.5.3" 1328 + version: "2.5.4" 1329 1329 shared_preferences_android: 1330 1330 dependency: transitive 1331 1331 description: ··· 1382 1382 url: "https://pub.dev" 1383 1383 source: hosted 1384 1384 version: "1.4.2" 1385 + shelf_packages_handler: 1386 + dependency: transitive 1387 + description: 1388 + name: shelf_packages_handler 1389 + sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" 1390 + url: "https://pub.dev" 1391 + source: hosted 1392 + version: "3.0.2" 1393 + shelf_static: 1394 + dependency: transitive 1395 + description: 1396 + name: shelf_static 1397 + sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3 1398 + url: "https://pub.dev" 1399 + source: hosted 1400 + version: "1.1.3" 1385 1401 shelf_web_socket: 1386 1402 dependency: transitive 1387 1403 description: ··· 1399 1415 dependency: transitive 1400 1416 description: 1401 1417 name: source_gen 1402 - sha256: "35c8150ece9e8c8d263337a265153c3329667640850b9304861faea59fc98f6b" 1418 + sha256: "07b277b67e0096c45196cbddddf2d8c6ffc49342e88bf31d460ce04605ddac75" 1403 1419 url: "https://pub.dev" 1404 1420 source: hosted 1405 - version: "2.0.0" 1421 + version: "4.1.1" 1406 1422 source_helper: 1407 1423 dependency: transitive 1408 1424 description: 1409 1425 name: source_helper 1410 - sha256: a447acb083d3a5ef17f983dd36201aeea33fedadb3228fa831f2f0c92f0f3aca 1426 + sha256: "6a3c6cc82073a8797f8c4dc4572146114a39652851c157db37e964d9c7038723" 1427 + url: "https://pub.dev" 1428 + source: hosted 1429 + version: "1.3.8" 1430 + source_map_stack_trace: 1431 + dependency: transitive 1432 + description: 1433 + name: source_map_stack_trace 1434 + sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b 1435 + url: "https://pub.dev" 1436 + source: hosted 1437 + version: "2.1.2" 1438 + source_maps: 1439 + dependency: transitive 1440 + description: 1441 + name: source_maps 1442 + sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812" 1411 1443 url: "https://pub.dev" 1412 1444 source: hosted 1413 - version: "1.3.7" 1445 + version: "0.10.13" 1414 1446 source_span: 1415 1447 dependency: transitive 1416 1448 description: ··· 1523 1555 url: "https://pub.dev" 1524 1556 source: hosted 1525 1557 version: "1.2.2" 1558 + test: 1559 + dependency: transitive 1560 + description: 1561 + name: test 1562 + sha256: "75906bf273541b676716d1ca7627a17e4c4070a3a16272b7a3dc7da3b9f3f6b7" 1563 + url: "https://pub.dev" 1564 + source: hosted 1565 + version: "1.26.3" 1526 1566 test_api: 1527 1567 dependency: transitive 1528 1568 description: ··· 1531 1571 url: "https://pub.dev" 1532 1572 source: hosted 1533 1573 version: "0.7.7" 1534 - timing: 1574 + test_core: 1535 1575 dependency: transitive 1536 1576 description: 1537 - name: timing 1538 - sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe" 1577 + name: test_core 1578 + sha256: "0cc24b5ff94b38d2ae73e1eb43cc302b77964fbf67abad1e296025b78deb53d0" 1539 1579 url: "https://pub.dev" 1540 1580 source: hosted 1541 - version: "1.0.2" 1581 + version: "0.6.12" 1542 1582 typed_data: 1543 1583 dependency: transitive 1544 1584 description: ··· 1547 1587 url: "https://pub.dev" 1548 1588 source: hosted 1549 1589 version: "1.4.0" 1550 - universal_io: 1551 - dependency: transitive 1552 - description: 1553 - name: universal_io 1554 - sha256: f63cbc48103236abf48e345e07a03ce5757ea86285ed313a6a032596ed9301e2 1555 - url: "https://pub.dev" 1556 - source: hosted 1557 - version: "2.3.1" 1558 1590 url_launcher: 1559 1591 dependency: "direct main" 1560 1592 description: ··· 1771 1803 url: "https://pub.dev" 1772 1804 source: hosted 1773 1805 version: "3.0.3" 1806 + webkit_inspection_protocol: 1807 + dependency: transitive 1808 + description: 1809 + name: webkit_inspection_protocol 1810 + sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" 1811 + url: "https://pub.dev" 1812 + source: hosted 1813 + version: "1.2.1" 1774 1814 widgetbook: 1775 1815 dependency: transitive 1776 1816 description: ··· 1791 1831 dependency: transitive 1792 1832 description: 1793 1833 name: widgetbook_generator 1794 - sha256: "361f28ae26a4eb9c74625ff1e53eb6a783c8f0e6032baed80de4489da778f955" 1834 + sha256: d4fd5989abeac63825746e52e4497b3bac7c59dff3565c261040b07581d22f8e 1795 1835 url: "https://pub.dev" 1796 1836 source: hosted 1797 - version: "3.14.0" 1837 + version: "3.20.1" 1798 1838 win32: 1799 1839 dependency: transitive 1800 1840 description: ··· 1823 1863 dependency: transitive 1824 1864 description: 1825 1865 name: xrpc 1826 - sha256: bacfa0f6824fdeaa631aad1a5fd064c3f140c771fed94cbd04df3b7d1e008709 1866 + sha256: ccaaf5224d1037a196bdfd03059027961bed3980718c9ce956be39d6bce152df 1827 1867 url: "https://pub.dev" 1828 1868 source: hosted 1829 - version: "0.6.1" 1869 + version: "1.0.3" 1830 1870 xxh3: 1831 1871 dependency: transitive 1832 1872 description:
+8 -8
pubspec.yaml
··· 21 21 camera: ^0.11.1 22 22 path_provider: ^2.1.2 23 23 flutter_svg: ^2.2.0 24 - atproto: ^0.13.3 25 - bluesky: ^0.18.10 24 + atproto: ^1.2.4 25 + bluesky: ^1.2.6 26 26 http: ^1.2.0 27 27 url_launcher: ^6.2.5 28 28 shared_preferences: ^2.5.3 ··· 33 33 image: ^4.5.4 34 34 flutter_cache_manager: ^3.3.1 35 35 synchronized: ^3.1.0 36 - flutter_riverpod: ^2.4.9 37 - riverpod_annotation: ^2.3.3 38 - freezed: ^2.4.6 39 - freezed_annotation: ^2.4.1 36 + flutter_riverpod: ^3.0.3 37 + riverpod_annotation: ^3.0.3 38 + freezed: ^3.2.3 39 + freezed_annotation: ^3.1.0 40 40 json_annotation: ^4.9.0 41 41 get_it: ^8.0.3 42 42 auto_route: ^10.1.0+1 43 43 flutter_secure_storage: ^9.2.4 44 - atproto_core: ^0.11.2 44 + atproto_core: ^1.0.7 45 45 sqflite: ^2.4.2 46 46 pool: ^1.5.0 47 47 collection: ^1.19.1 ··· 73 73 sdk: flutter 74 74 flutter_lints: ^6.0.0 75 75 build_runner: ^2.5.4 76 - riverpod_generator: ^2.3.9 76 + riverpod_generator: ^3.0.3 77 77 json_serializable: ^6.7.1 78 78 auto_route_generator: ^10.2.3 79 79 flutter_launcher_icons: ^0.14.3
+16 -70
widgetbook/lib/molecules/glass_input.dart
··· 1 1 import 'package:flutter/material.dart'; 2 2 import 'package:flutter_riverpod/flutter_riverpod.dart'; 3 + import 'package:flutter_riverpod/legacy.dart'; 3 4 import 'package:sparksocial/src/core/design_system/components/atoms/icons.dart'; 4 5 import 'package:sparksocial/src/core/design_system/components/molecules/glass_input.dart'; 5 6 import 'package:widgetbook/widgetbook.dart'; 6 7 import 'package:widgetbook_annotation/widgetbook_annotation.dart'; 7 8 8 - final _chatControllerProvider = Provider.autoDispose<TextEditingController>(( 9 - ref, 10 - ) { 9 + final _chatControllerProvider = Provider.autoDispose<TextEditingController>((ref) { 11 10 final c = TextEditingController(); 12 11 ref.onDispose(c.dispose); 13 12 return c; 14 13 }); 15 - final _chatMessagesProvider = StateProvider.autoDispose<List<String>>( 16 - (_) => const [], 17 - ); 14 + final _chatMessagesProvider = StateProvider.autoDispose<List<String>>((_) => const []); 18 15 19 16 @UseCase(name: 'comment', type: GlassInput) 20 17 Widget buildGlassInputCommentUseCase(BuildContext context) { 21 18 return Center( 22 19 child: Container( 23 - constraints: BoxConstraints( 24 - maxWidth: context.knobs.double.slider( 25 - label: 'width', 26 - initialValue: 210, 27 - min: 160, 28 - max: 400, 29 - divisions: 24, 30 - ), 31 - ), 20 + constraints: BoxConstraints(maxWidth: context.knobs.double.slider(label: 'width', initialValue: 210, min: 160, max: 400, divisions: 24)), 32 21 child: GlassInput.comment( 33 - hintText: context.knobs.string( 34 - label: 'hint', 35 - initialValue: 'Add a comment...', 36 - ), 37 - leadingWidgets: [ 38 - if (context.knobs.boolean(label: 'show_avatar', initialValue: true)) 39 - const CircleAvatar(radius: 10), 40 - ], 41 - actionWidgets: [ 42 - if (context.knobs.boolean( 43 - label: 'show_action_icon', 44 - initialValue: true, 45 - )) 46 - AppIcons.smiley(), 47 - ], 22 + hintText: context.knobs.string(label: 'hint', initialValue: 'Add a comment...'), 23 + leadingWidgets: [if (context.knobs.boolean(label: 'show_avatar', initialValue: true)) const CircleAvatar(radius: 10)], 24 + actionWidgets: [if (context.knobs.boolean(label: 'show_action_icon', initialValue: true)) AppIcons.smiley()], 48 25 ), 49 26 ), 50 27 ); ··· 54 31 Widget buildGlassInputSearchUseCase(BuildContext context) { 55 32 return Center( 56 33 child: SizedBox( 57 - width: context.knobs.double.slider( 58 - label: 'width', 59 - initialValue: 280, 60 - min: 160, 61 - max: 400, 62 - divisions: 24, 63 - ), 34 + width: context.knobs.double.slider(label: 'width', initialValue: 280, min: 160, max: 400, divisions: 24), 64 35 child: GlassInput.search( 65 - hintText: context.knobs.string( 66 - label: 'hint', 67 - initialValue: 'Search...', 68 - ), 69 - leadingWidgets: [ 70 - const Icon(Icons.search, size: 18, color: Colors.white70), 71 - ], 36 + hintText: context.knobs.string(label: 'hint', initialValue: 'Search...'), 37 + leadingWidgets: [const Icon(Icons.search, size: 18, color: Colors.white70)], 72 38 actionWidgets: [ 73 39 if (context.knobs.boolean(label: 'show_clear', initialValue: true)) 74 40 GestureDetector( ··· 87 53 child: Center( 88 54 child: _ChatDemo( 89 55 showSend: context.knobs.boolean(label: 'show_send', initialValue: true), 90 - placeholder: context.knobs.string( 91 - label: 'hint', 92 - initialValue: 'Message...', 93 - ), 56 + placeholder: context.knobs.string(label: 'hint', initialValue: 'Message...'), 94 57 ), 95 58 ), 96 59 ); ··· 127 90 child: Padding( 128 91 padding: const EdgeInsets.symmetric(vertical: 2), 129 92 child: DecoratedBox( 130 - decoration: BoxDecoration( 131 - color: Colors.white.withAlpha(30), 132 - borderRadius: BorderRadius.circular(8), 133 - ), 93 + decoration: BoxDecoration(color: Colors.white.withAlpha(30), borderRadius: BorderRadius.circular(8)), 134 94 child: Padding( 135 - padding: const EdgeInsets.symmetric( 136 - horizontal: 8, 137 - vertical: 4, 138 - ), 139 - child: Text( 140 - msg, 141 - style: const TextStyle( 142 - fontSize: 11, 143 - color: Colors.white, 144 - ), 145 - ), 95 + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), 96 + child: Text(msg, style: const TextStyle(fontSize: 11, color: Colors.white)), 146 97 ), 147 98 ), 148 99 ), ··· 157 108 child: GlassInput.chat( 158 109 controller: controller, 159 110 hintText: placeholder, 160 - leadingWidgets: const [ 161 - Icon(Icons.chat_bubble_outline, size: 18, color: Colors.white70), 162 - ], 111 + leadingWidgets: const [Icon(Icons.chat_bubble_outline, size: 18, color: Colors.white70)], 163 112 onSendMessage: showSend 164 113 ? () { 165 114 final text = controller.text.trim(); 166 115 if (text.isEmpty) return; 167 - ref.read(_chatMessagesProvider.notifier).state = [ 168 - ...messages, 169 - text, 170 - ]; 116 + ref.read(_chatMessagesProvider.notifier).state = [...messages, text]; 171 117 controller.clear(); 172 118 } 173 119 : null,
+1 -1
widgetbook/pubspec.yaml
··· 18 18 path: ../fonts 19 19 assets: 20 20 path: ../assets 21 - flutter_riverpod: ^2.6.1 21 + flutter_riverpod: ^3.0.3 22 22 fluentui_system_icons: ^1.1.273 23 23 24 24 dev_dependencies: