this repo has no description

feat: add description facets to profile model and update profile fetching logic

+114 -48
+1 -3
lib/main.dart
··· 74 74 } 75 75 76 76 void handleSignIn() async { 77 - final container = ProviderScope.containerOf(context, listen: false); 78 - 79 77 setState(() { 80 78 isSignedIn = true; 81 79 }); ··· 87 85 void handleSignOut(BuildContext context) async { 88 86 final container = ProviderScope.containerOf(context, listen: false); 89 87 await auth.clearSession(); // Clear session data 90 - // Invalidate Riverpod providers for profile and gallery state 88 + // Invalidate Riverpod providers for profile state 91 89 container.invalidate(profileNotifierProvider); 92 90 // Add any other providers you want to invalidate here 93 91 setState(() {
+2
lib/models/profile.dart
··· 18 18 int? followsCount, 19 19 int? galleryCount, 20 20 ProfileViewer? viewer, 21 + // Added field for description facets used on profile page 22 + List<Map<String, dynamic>>? descriptionFacets, 21 23 }) = _Profile; 22 24 23 25 factory Profile.fromJson(Map<String, dynamic> json) => _$ProfileFromJson(json);
+41 -5
lib/models/profile.freezed.dart
··· 30 30 int? get followersCount => throw _privateConstructorUsedError; 31 31 int? get followsCount => throw _privateConstructorUsedError; 32 32 int? get galleryCount => throw _privateConstructorUsedError; 33 - ProfileViewer? get viewer => throw _privateConstructorUsedError; 33 + ProfileViewer? get viewer => 34 + throw _privateConstructorUsedError; // Added field for description facets used on profile page 35 + List<Map<String, dynamic>>? get descriptionFacets => 36 + throw _privateConstructorUsedError; 34 37 35 38 /// Serializes this Profile to a JSON map. 36 39 Map<String, dynamic> toJson() => throw _privateConstructorUsedError; ··· 57 60 int? followsCount, 58 61 int? galleryCount, 59 62 ProfileViewer? viewer, 63 + List<Map<String, dynamic>>? descriptionFacets, 60 64 }); 61 65 62 66 $ProfileViewerCopyWith<$Res>? get viewer; ··· 87 91 Object? followsCount = freezed, 88 92 Object? galleryCount = freezed, 89 93 Object? viewer = freezed, 94 + Object? descriptionFacets = freezed, 90 95 }) { 91 96 return _then( 92 97 _value.copyWith( ··· 130 135 ? _value.viewer 131 136 : viewer // ignore: cast_nullable_to_non_nullable 132 137 as ProfileViewer?, 138 + descriptionFacets: freezed == descriptionFacets 139 + ? _value.descriptionFacets 140 + : descriptionFacets // ignore: cast_nullable_to_non_nullable 141 + as List<Map<String, dynamic>>?, 133 142 ) 134 143 as $Val, 135 144 ); ··· 169 178 int? followsCount, 170 179 int? galleryCount, 171 180 ProfileViewer? viewer, 181 + List<Map<String, dynamic>>? descriptionFacets, 172 182 }); 173 183 174 184 @override ··· 199 209 Object? followsCount = freezed, 200 210 Object? galleryCount = freezed, 201 211 Object? viewer = freezed, 212 + Object? descriptionFacets = freezed, 202 213 }) { 203 214 return _then( 204 215 _$ProfileImpl( ··· 242 253 ? _value.viewer 243 254 : viewer // ignore: cast_nullable_to_non_nullable 244 255 as ProfileViewer?, 256 + descriptionFacets: freezed == descriptionFacets 257 + ? _value._descriptionFacets 258 + : descriptionFacets // ignore: cast_nullable_to_non_nullable 259 + as List<Map<String, dynamic>>?, 245 260 ), 246 261 ); 247 262 } ··· 261 276 this.followsCount, 262 277 this.galleryCount, 263 278 this.viewer, 264 - }); 279 + final List<Map<String, dynamic>>? descriptionFacets, 280 + }) : _descriptionFacets = descriptionFacets; 265 281 266 282 factory _$ProfileImpl.fromJson(Map<String, dynamic> json) => 267 283 _$$ProfileImplFromJson(json); ··· 286 302 final int? galleryCount; 287 303 @override 288 304 final ProfileViewer? viewer; 305 + // Added field for description facets used on profile page 306 + final List<Map<String, dynamic>>? _descriptionFacets; 307 + // Added field for description facets used on profile page 308 + @override 309 + List<Map<String, dynamic>>? get descriptionFacets { 310 + final value = _descriptionFacets; 311 + if (value == null) return null; 312 + if (_descriptionFacets is EqualUnmodifiableListView) 313 + return _descriptionFacets; 314 + // ignore: implicit_dynamic_type 315 + return EqualUnmodifiableListView(value); 316 + } 289 317 290 318 @override 291 319 String toString() { 292 - return 'Profile(cid: $cid, did: $did, handle: $handle, displayName: $displayName, description: $description, avatar: $avatar, followersCount: $followersCount, followsCount: $followsCount, galleryCount: $galleryCount, viewer: $viewer)'; 320 + return 'Profile(cid: $cid, did: $did, handle: $handle, displayName: $displayName, description: $description, avatar: $avatar, followersCount: $followersCount, followsCount: $followsCount, galleryCount: $galleryCount, viewer: $viewer, descriptionFacets: $descriptionFacets)'; 293 321 } 294 322 295 323 @override ··· 311 339 other.followsCount == followsCount) && 312 340 (identical(other.galleryCount, galleryCount) || 313 341 other.galleryCount == galleryCount) && 314 - (identical(other.viewer, viewer) || other.viewer == viewer)); 342 + (identical(other.viewer, viewer) || other.viewer == viewer) && 343 + const DeepCollectionEquality().equals( 344 + other._descriptionFacets, 345 + _descriptionFacets, 346 + )); 315 347 } 316 348 317 349 @JsonKey(includeFromJson: false, includeToJson: false) ··· 328 360 followsCount, 329 361 galleryCount, 330 362 viewer, 363 + const DeepCollectionEquality().hash(_descriptionFacets), 331 364 ); 332 365 333 366 /// Create a copy of Profile ··· 356 389 final int? followsCount, 357 390 final int? galleryCount, 358 391 final ProfileViewer? viewer, 392 + final List<Map<String, dynamic>>? descriptionFacets, 359 393 }) = _$ProfileImpl; 360 394 361 395 factory _Profile.fromJson(Map<String, dynamic> json) = _$ProfileImpl.fromJson; ··· 379 413 @override 380 414 int? get galleryCount; 381 415 @override 382 - ProfileViewer? get viewer; 416 + ProfileViewer? get viewer; // Added field for description facets used on profile page 417 + @override 418 + List<Map<String, dynamic>>? get descriptionFacets; 383 419 384 420 /// Create a copy of Profile 385 421 /// with the given fields replaced by the non-null parameter values.
+4
lib/models/profile.g.dart
··· 20 20 viewer: json['viewer'] == null 21 21 ? null 22 22 : ProfileViewer.fromJson(json['viewer'] as Map<String, dynamic>), 23 + descriptionFacets: (json['descriptionFacets'] as List<dynamic>?) 24 + ?.map((e) => e as Map<String, dynamic>) 25 + .toList(), 23 26 ); 24 27 25 28 Map<String, dynamic> _$$ProfileImplToJson(_$ProfileImpl instance) => ··· 34 37 'followsCount': instance.followsCount, 35 38 'galleryCount': instance.galleryCount, 36 39 'viewer': instance.viewer, 40 + 'descriptionFacets': instance.descriptionFacets, 37 41 };
+42 -1
lib/providers/profile_provider.dart
··· 1 1 import 'dart:io'; 2 2 3 + import 'package:bluesky_text/bluesky_text.dart'; 3 4 import 'package:grain/api.dart'; 4 5 import 'package:grain/models/profile.dart'; 5 6 import 'package:grain/models/profile_with_galleries.dart'; ··· 16 17 return _fetchProfile(did); 17 18 } 18 19 20 + // @TODO: Facets don't always render correctly. 21 + List<Map<String, dynamic>>? _filterValidFacets( 22 + List<Map<String, dynamic>>? computedFacets, 23 + String desc, 24 + ) { 25 + if (computedFacets == null) return null; 26 + return computedFacets.where((facet) { 27 + final index = facet['index']; 28 + if (index is Map) { 29 + final start = index['byteStart'] ?? 0; 30 + final end = index['byteEnd'] ?? 0; 31 + return start is int && end is int && start >= 0 && end > start && end <= desc.length; 32 + } 33 + final start = facet['index'] ?? facet['offset'] ?? 0; 34 + final end = facet['end']; 35 + final length = facet['length']; 36 + if (end is int && start is int) { 37 + return start >= 0 && end > start && end <= desc.length; 38 + } else if (length is int && start is int) { 39 + return start >= 0 && length > 0 && start + length <= desc.length; 40 + } 41 + return false; 42 + }).toList(); 43 + } 44 + 19 45 Future<ProfileWithGalleries?> _fetchProfile(String did) async { 20 46 final profile = await apiService.fetchProfile(did: did); 21 47 final galleries = await apiService.fetchActorGalleries(did: did); 22 48 if (profile != null) { 23 - return ProfileWithGalleries(profile: profile, galleries: galleries); 49 + List<Map<String, dynamic>>? facets; 50 + final desc = profile.description ?? ''; 51 + if (desc.isNotEmpty) { 52 + try { 53 + final blueskyText = BlueskyText(desc); 54 + final entities = blueskyText.entities; 55 + final computedFacets = await entities.toFacets(); 56 + facets = _filterValidFacets(computedFacets, desc); 57 + } catch (_) { 58 + facets = null; 59 + } 60 + } 61 + return ProfileWithGalleries( 62 + profile: profile.copyWith(descriptionFacets: facets), 63 + galleries: galleries, 64 + ); 24 65 } 25 66 return null; 26 67 }
+1 -1
lib/providers/profile_provider.g.dart
··· 6 6 // RiverpodGenerator 7 7 // ************************************************************************** 8 8 9 - String _$profileNotifierHash() => r'0a4d40043196309ae74bdb3b88d7d63d25b8b4f6'; 9 + String _$profileNotifierHash() => r'a955b4d4a22d864fc88f77dad30b5d3019ba8dfe'; 10 10 11 11 /// Copied from Dart SDK 12 12 class _SystemHash {
+2
lib/screens/home_page.dart
··· 1 1 import 'package:flutter/material.dart'; 2 + import 'package:flutter/services.dart'; 2 3 import 'package:flutter_riverpod/flutter_riverpod.dart'; 3 4 import 'package:font_awesome_flutter/font_awesome_flutter.dart'; 4 5 import 'package:grain/api.dart'; ··· 397 398 ? FloatingActionButton( 398 399 shape: const CircleBorder(), 399 400 onPressed: () { 401 + HapticFeedback.mediumImpact(); 400 402 showModalBottomSheet( 401 403 context: context, 402 404 isScrollControlled: true,
+21 -38
lib/screens/profile_page.dart
··· 1 - import 'package:bluesky_text/bluesky_text.dart'; 2 1 import 'package:flutter/material.dart'; 3 2 import 'package:flutter_riverpod/flutter_riverpod.dart'; 4 3 import 'package:grain/api.dart'; ··· 133 132 } 134 133 final profile = profileWithGalleries.profile; 135 134 final galleries = profileWithGalleries.galleries; 136 - Future<List<Map<String, dynamic>>?> getDescriptionFacets() async { 137 - final desc = profile.description ?? ''; 138 - if (desc.isEmpty) return null; 139 - try { 140 - final blueskyText = BlueskyText(desc); 141 - final entities = blueskyText.entities; 142 - return await entities.toFacets(); 143 - } catch (_) { 144 - return null; 145 - } 146 - } 147 135 148 136 return Scaffold( 149 137 backgroundColor: theme.scaffoldBackgroundColor, ··· 298 286 ), 299 287 if ((profile.description ?? '').isNotEmpty) ...[ 300 288 const SizedBox(height: 16), 301 - FutureBuilder<List<Map<String, dynamic>>?>( 302 - future: getDescriptionFacets(), 303 - builder: (context, snapshot) { 304 - return FacetedText( 305 - text: profile.description ?? '', 306 - facets: snapshot.data, 307 - onMentionTap: (didOrHandle) { 308 - Navigator.of(context).push( 309 - MaterialPageRoute( 310 - builder: (context) => 311 - ProfilePage(did: didOrHandle, showAppBar: true), 312 - ), 313 - ); 314 - }, 315 - onLinkTap: (url) { 316 - // TODO: Implement WebViewPage navigation 317 - }, 318 - onTagTap: (tag) => Navigator.push( 319 - context, 320 - MaterialPageRoute( 321 - builder: (_) => HashtagPage(hashtag: tag), 322 - ), 323 - ), 324 - linkStyle: TextStyle( 325 - color: Theme.of(context).colorScheme.primary, 326 - fontWeight: FontWeight.w600, 289 + FacetedText( 290 + text: profile.description ?? '', 291 + facets: profile.descriptionFacets, 292 + onMentionTap: (didOrHandle) { 293 + Navigator.of(context).push( 294 + MaterialPageRoute( 295 + builder: (context) => 296 + ProfilePage(did: didOrHandle, showAppBar: true), 327 297 ), 328 298 ); 329 299 }, 300 + onLinkTap: (url) { 301 + // TODO: Implement WebViewPage navigation 302 + }, 303 + onTagTap: (tag) => Navigator.push( 304 + context, 305 + MaterialPageRoute( 306 + builder: (_) => HashtagPage(hashtag: tag), 307 + ), 308 + ), 309 + linkStyle: TextStyle( 310 + color: Theme.of(context).colorScheme.primary, 311 + fontWeight: FontWeight.w600, 312 + ), 330 313 ), 331 314 ], 332 315 const SizedBox(height: 24),