import 'dart:async'; import 'package:cloud_firestore/cloud_firestore.dart'; import '../map/map_provider_interface.dart'; import 'social_feed_models.dart'; class SocialFeedService { final FirebaseFirestore? firestore; final StreamController> _localFeedController = StreamController>.broadcast(); List _memoryFeed = []; bool _initialized = false; SocialFeedService({this.firestore}) { _initFeed(); } void _initFeed() { if (_initialized) return; _initialized = true; // Populate with Stitch SOC-01 canonical community items _memoryFeed = [ SocialFeedItem( id: 'feed_post_1', authorUid: 'uid_marcus', authorDisplayName: 'Marcus Vance', authorHandle: 'ApexMarcus', authorAvatarPreset: 'helmet_carbon_cyan', authorBadge: 'PRO', clubName: 'Bay Area Twisties Club', locationName: "Alice's Restaurant", timestamp: DateTime.now().subtract(const Duration(minutes: 28)), rideTitle: 'Sunday Skyline & Pescadero Carve', bikeModel: '2024 Ducati Streetfighter V4', distanceMiles: 68.4, durationSeconds: 4440, // 1h 14m maxLeanAngleDeg: 51.0, maxLeanSide: 'RIGHT', paceFactor: '1.32x Spirited', hazardCount: 0, elevationFt: 2140, snapshotType: FeedSnapshotType.routeTrack, isGpsVerified: true, fistBumpCount: 42, fistBumpUserIds: const ['uid_rider_2', 'uid_rider_3'], routePoints: const [ AppLatLng(37.3861, -122.2641), AppLatLng(37.3600, -122.2400), AppLatLng(37.3200, -122.2100), AppLatLng(37.2800, -122.2400), AppLatLng(37.2500, -122.1800), ], comments: [ FeedPostComment( id: 'c1', authorUid: 'uid_sarah', authorDisplayName: 'Sarah Chen', authorHandle: 'SarahSweep', content: 'Clean line through section 3! Pavement was prime today.', createdAt: DateTime.now().subtract(const Duration(minutes: 15)), ), ], ), SocialFeedItem( id: 'feed_post_2', authorUid: 'uid_sarah', authorDisplayName: 'Sarah Chen', authorHandle: 'SarahSweep', authorAvatarPreset: 'helmet_aerodynamic_grey', authorBadge: 'ROAD CAPTAIN', clubName: 'Pacific Coast Riders', locationName: 'Mulholland Hwy', timestamp: DateTime.now().subtract(const Duration(hours: 2)), rideTitle: 'The Snake & Rock Store Pace Lap', bikeModel: 'Yamaha MT-09 SP', distanceMiles: 44.2, durationSeconds: 2900, // 48m 20s maxLeanAngleDeg: 48.0, maxLeanSide: 'PEAK', paceFactor: '1.25x Swift', hazardCount: 0, elevationFt: 1850, snapshotType: FeedSnapshotType.scenicPhoto, photoUrl: 'assets/images/rock_store_lookout.png', photoCaption: 'Rock Store Lookout', isGpsVerified: true, fistBumpCount: 28, fistBumpUserIds: const ['uid_marcus'], comments: const [], ), SocialFeedItem( id: 'feed_post_3', authorUid: 'uid_dave', authorDisplayName: 'Dave Sterling', authorHandle: 'GhostRider99', authorAvatarPreset: 'helmet_matte_stealth', authorBadge: 'FOUNDER', clubName: 'Angeles Crest Squad', locationName: 'Newcomb\'s Ranch', timestamp: DateTime.now().subtract(const Duration(hours: 5)), rideTitle: 'Angeles Crest Mountain Pace Run', bikeModel: 'BMW S1000RR M-Sport', distanceMiles: 82.0, durationSeconds: 5200, maxLeanAngleDeg: 53.5, maxLeanSide: 'LEFT', paceFactor: '1.45x Fast', hazardCount: 1, elevationFt: 5300, snapshotType: FeedSnapshotType.routeTrack, isGpsVerified: true, fistBumpCount: 65, fistBumpUserIds: const ['uid_marcus', 'uid_sarah'], routePoints: const [ AppLatLng(34.2000, -118.1500), AppLatLng(34.2500, -118.0800), AppLatLng(34.3300, -118.0000), ], comments: const [], ), ]; _localFeedController.add(List.unmodifiable(_memoryFeed)); } List get currentFeed => List.unmodifiable(_memoryFeed); List getFilteredItems({ FeedFilter filter = FeedFilter.allActivity, String? userClubName, }) { return _filterItems(_memoryFeed, filter, userClubName); } Stream> getFeedStream({ FeedFilter filter = FeedFilter.allActivity, String? userClubName, }) { late StreamController> controller; StreamSubscription>? sub; controller = StreamController>( onListen: () { controller.add(_filterItems(_memoryFeed, filter, userClubName)); sub = _localFeedController.stream.listen((list) { if (!controller.isClosed) { controller.add(_filterItems(list, filter, userClubName)); } }); }, onCancel: () { sub?.cancel(); }, ); return controller.stream; } List _filterItems( List items, FeedFilter filter, String? userClubName, ) { switch (filter) { case FeedFilter.allActivity: return items; case FeedFilter.clubOnly: if (userClubName == null || userClubName.isEmpty) { return items.where((i) => i.clubName != null && i.clubName!.isNotEmpty).toList(); } return items.where((i) => i.clubName?.toLowerCase() == userClubName.toLowerCase()).toList(); case FeedFilter.nearbyPacks: return items.where((i) => i.distanceMiles < 60.0).toList(); } } /// Toggle fist bump on a post. Future toggleFistBump({ required String postId, required String userUid, }) async { final index = _memoryFeed.indexWhere((p) => p.id == postId); if (index == -1) return; final post = _memoryFeed[index]; final isBumped = post.isFistBumpedBy(userUid); final updatedList = List.from(post.fistBumpUserIds); final updatedCount = isBumped ? post.fistBumpCount - 1 : post.fistBumpCount + 1; if (isBumped) { updatedList.remove(userUid); } else { updatedList.add(userUid); } final updatedPost = post.copyWith( fistBumpCount: updatedCount < 0 ? 0 : updatedCount, fistBumpUserIds: updatedList, ); _memoryFeed[index] = updatedPost; _localFeedController.add(List.unmodifiable(_memoryFeed)); final fs = firestore; if (fs != null) { try { await fs.collection('social_feed').doc(postId).update({ 'fistBumpCount': updatedPost.fistBumpCount, 'fistBumpUserIds': updatedPost.fistBumpUserIds, }); } catch (_) {} } } /// Add a comment to a feed post. Future addComment({ required String postId, required FeedPostComment comment, }) async { final index = _memoryFeed.indexWhere((p) => p.id == postId); if (index == -1) return; final post = _memoryFeed[index]; final updatedComments = List.from(post.comments)..add(comment); final updatedPost = post.copyWith(comments: updatedComments); _memoryFeed[index] = updatedPost; _localFeedController.add(List.unmodifiable(_memoryFeed)); final fs = firestore; if (fs != null) { try { await fs.collection('social_feed').doc(postId).update({ 'comments': updatedComments.map((c) => c.toJson()).toList(), }); } catch (_) {} } } /// Share a recent completed ride session to the community feed. Future shareRecentRide({ required String authorUid, required String authorDisplayName, required String authorHandle, String? authorAvatarPreset, String? authorBadge, String? clubName, required String locationName, required String rideTitle, required String bikeModel, required double distanceMiles, required int durationSeconds, required double maxLeanAngleDeg, String maxLeanSide = 'RIGHT', String paceFactor = '1.2x Spirited', int hazardCount = 0, int elevationFt = 1500, FeedSnapshotType snapshotType = FeedSnapshotType.routeTrack, List routePoints = const [], }) async { final newPost = SocialFeedItem( id: 'feed_post_${DateTime.now().millisecondsSinceEpoch}', authorUid: authorUid, authorDisplayName: authorDisplayName, authorHandle: authorHandle, authorAvatarPreset: authorAvatarPreset, authorBadge: authorBadge ?? 'PRO', clubName: clubName, locationName: locationName, timestamp: DateTime.now(), rideTitle: rideTitle, bikeModel: bikeModel, distanceMiles: distanceMiles, durationSeconds: durationSeconds, maxLeanAngleDeg: maxLeanAngleDeg, maxLeanSide: maxLeanSide, paceFactor: paceFactor, hazardCount: hazardCount, elevationFt: elevationFt, snapshotType: snapshotType, routePoints: routePoints, isGpsVerified: true, fistBumpCount: 1, fistBumpUserIds: [authorUid], comments: const [], ); _memoryFeed.insert(0, newPost); _localFeedController.add(List.unmodifiable(_memoryFeed)); final fs = firestore; if (fs != null) { try { await fs.collection('social_feed').doc(newPost.id).set(newPost.toJson()); } catch (_) {} } return newPost; } void dispose() { _localFeedController.close(); } }