MotorSignal-App/lib/services/social/friendship_service.dart

470 lines
13 KiB
Dart

import 'dart:async';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/foundation.dart';
import 'rider_profile_models.dart';
/// Manages mutual friendships, friend requests, blocking, and social boundaries.
class FriendshipService {
final FirebaseFirestore? _firestore;
FriendshipService({FirebaseFirestore? firestore})
: _firestore = firestore ?? _resolveFirestore();
static FirebaseFirestore? _resolveFirestore() {
try {
if (Firebase.apps.isNotEmpty) return FirebaseFirestore.instance;
} catch (_) {}
return null;
}
bool get isAvailable => _firestore != null;
/// Determines relationship status between [myUid] and [otherUid].
Future<FriendshipStatus> getRelationshipStatus(
String myUid,
String otherUid,
) async {
if (myUid == otherUid) return FriendshipStatus.self;
final firestore = _firestore;
if (firestore == null) return FriendshipStatus.none;
try {
final userRef = firestore.collection('users').doc(myUid);
final otherRef = firestore.collection('users').doc(otherUid);
// Check if user has blocked other or other has blocked user
final myBlockDoc = await userRef.collection('blocks').doc(otherUid).get();
if (myBlockDoc.exists) return FriendshipStatus.blocked;
final otherBlockDoc =
await otherRef.collection('blocks').doc(myUid).get();
if (otherBlockDoc.exists) return FriendshipStatus.blocked;
// Check mutual friend
final friendDoc =
await userRef.collection('friends').doc(otherUid).get();
if (friendDoc.exists) return FriendshipStatus.friend;
// Check incoming request
final incomingDoc = await userRef
.collection('friend_requests_incoming')
.doc(otherUid)
.get();
if (incomingDoc.exists) return FriendshipStatus.requestIncoming;
// Check outgoing request
final outgoingDoc = await userRef
.collection('friend_requests_outgoing')
.doc(otherUid)
.get();
if (outgoingDoc.exists) return FriendshipStatus.requestOutgoing;
return FriendshipStatus.none;
} catch (e) {
debugPrint('[FriendshipService] Error checking relationship status: $e');
return FriendshipStatus.none;
}
}
/// Sends a friend request from [sender] to [receiverUid].
Future<void> sendFriendRequest({
required RiderProfile sender,
required String receiverUid,
}) async {
if (sender.uid == receiverUid) {
throw ArgumentError('Cannot send a friend request to yourself.');
}
final firestore = _firestore;
if (firestore == null) {
throw StateError('Social service is offline.');
}
final receiverRef = firestore.collection('users').doc(receiverUid);
final receiverDoc = await receiverRef.get();
if (!receiverDoc.exists || receiverDoc.data() == null) {
throw StateError('Rider not found.');
}
final receiverProfile = RiderProfile.fromJson(receiverDoc.data()!);
if (!receiverProfile.privacySettings.allowFriendRequests) {
throw StateError('This rider is not accepting friend requests.');
}
// Check existing relationship
final status = await getRelationshipStatus(sender.uid, receiverUid);
if (status == FriendshipStatus.friend) {
throw StateError('You are already friends with this rider.');
}
if (status == FriendshipStatus.requestOutgoing) {
throw StateError('Friend request already sent.');
}
if (status == FriendshipStatus.requestIncoming) {
throw StateError('This rider already sent you a friend request.');
}
if (status == FriendshipStatus.blocked) {
throw StateError('Unable to send request to this rider.');
}
final requestId = '${sender.uid}_$receiverUid';
final now = DateTime.now();
final request = FriendRequest(
requestId: requestId,
senderUid: sender.uid,
senderHandle: sender.handle,
senderDisplayName: sender.displayName,
senderAvatarPreset: sender.avatarPreset,
senderAvatarUrl: sender.avatarUrl,
receiverUid: receiverUid,
sentAt: now,
);
final batch = firestore.batch();
// 1. Add to receiver's incoming requests
final incomingRef = receiverRef
.collection('friend_requests_incoming')
.doc(sender.uid);
batch.set(incomingRef, request.toJson());
// 2. Add to sender's outgoing requests
final outgoingRef = firestore
.collection('users')
.doc(sender.uid)
.collection('friend_requests_outgoing')
.doc(receiverUid);
batch.set(outgoingRef, request.toJson());
await batch.commit();
}
/// Accepts an incoming friend request and establishes mutual friendship.
Future<void> acceptFriendRequest({
required RiderProfile me,
required FriendRequest request,
}) async {
final firestore = _firestore;
if (firestore == null) {
throw StateError('Social service is offline.');
}
final senderUid = request.senderUid;
final now = DateTime.now();
final batch = firestore.batch();
// 1. Remove from receiver's incoming requests
final myIncomingRef = firestore
.collection('users')
.doc(me.uid)
.collection('friend_requests_incoming')
.doc(senderUid);
batch.delete(myIncomingRef);
// 2. Remove from sender's outgoing requests
final senderOutgoingRef = firestore
.collection('users')
.doc(senderUid)
.collection('friend_requests_outgoing')
.doc(me.uid);
batch.delete(senderOutgoingRef);
// 3. Add to receiver's friends
final myFriendRef = firestore
.collection('users')
.doc(me.uid)
.collection('friends')
.doc(senderUid);
final myFriendSummary = FriendSummary(
uid: senderUid,
handle: request.senderHandle,
displayName: request.senderDisplayName,
avatarPreset: request.senderAvatarPreset,
avatarUrl: request.senderAvatarUrl,
friendedAt: now,
);
batch.set(myFriendRef, myFriendSummary.toJson());
// 4. Add to sender's friends
final senderFriendRef = firestore
.collection('users')
.doc(senderUid)
.collection('friends')
.doc(me.uid);
final senderFriendSummary = FriendSummary(
uid: me.uid,
handle: me.handle,
displayName: me.displayName,
avatarPreset: me.avatarPreset,
avatarUrl: me.avatarUrl,
friendedAt: now,
);
batch.set(senderFriendRef, senderFriendSummary.toJson());
await batch.commit();
}
/// Declines an incoming friend request.
Future<void> declineFriendRequest({
required String myUid,
required String senderUid,
}) async {
final firestore = _firestore;
if (firestore == null) return;
final batch = firestore.batch();
final myIncomingRef = firestore
.collection('users')
.doc(myUid)
.collection('friend_requests_incoming')
.doc(senderUid);
batch.delete(myIncomingRef);
final senderOutgoingRef = firestore
.collection('users')
.doc(senderUid)
.collection('friend_requests_outgoing')
.doc(myUid);
batch.delete(senderOutgoingRef);
await batch.commit();
}
/// Cancels an outgoing friend request.
Future<void> cancelFriendRequest({
required String myUid,
required String receiverUid,
}) async {
final firestore = _firestore;
if (firestore == null) return;
final batch = firestore.batch();
final myOutgoingRef = firestore
.collection('users')
.doc(myUid)
.collection('friend_requests_outgoing')
.doc(receiverUid);
batch.delete(myOutgoingRef);
final receiverIncomingRef = firestore
.collection('users')
.doc(receiverUid)
.collection('friend_requests_incoming')
.doc(myUid);
batch.delete(receiverIncomingRef);
await batch.commit();
}
/// Removes a mutual friend.
Future<void> removeFriend({
required String myUid,
required String friendUid,
}) async {
final firestore = _firestore;
if (firestore == null) return;
final batch = firestore.batch();
final myFriendRef = firestore
.collection('users')
.doc(myUid)
.collection('friends')
.doc(friendUid);
batch.delete(myFriendRef);
final theirFriendRef = firestore
.collection('users')
.doc(friendUid)
.collection('friends')
.doc(myUid);
batch.delete(theirFriendRef);
await batch.commit();
}
/// Blocks another rider, removing any friendship or pending requests.
Future<void> blockRider({
required String myUid,
required String targetUid,
String? targetHandle,
String? targetDisplayName,
}) async {
if (myUid == targetUid) return;
final firestore = _firestore;
if (firestore == null) return;
final batch = firestore.batch();
// Remove any mutual friendship
batch.delete(
firestore
.collection('users')
.doc(myUid)
.collection('friends')
.doc(targetUid),
);
batch.delete(
firestore
.collection('users')
.doc(targetUid)
.collection('friends')
.doc(myUid),
);
// Remove any pending requests
batch.delete(
firestore
.collection('users')
.doc(myUid)
.collection('friend_requests_incoming')
.doc(targetUid),
);
batch.delete(
firestore
.collection('users')
.doc(myUid)
.collection('friend_requests_outgoing')
.doc(targetUid),
);
batch.delete(
firestore
.collection('users')
.doc(targetUid)
.collection('friend_requests_incoming')
.doc(myUid),
);
batch.delete(
firestore
.collection('users')
.doc(targetUid)
.collection('friend_requests_outgoing')
.doc(myUid),
);
// Add block record
final block = BlockedRider(
blockedUid: targetUid,
blockedHandle: targetHandle,
blockedDisplayName: targetDisplayName,
blockedAt: DateTime.now(),
);
final blockRef = firestore
.collection('users')
.doc(myUid)
.collection('blocks')
.doc(targetUid);
batch.set(blockRef, block.toJson());
await batch.commit();
}
/// Unblocks a previously blocked rider.
Future<void> unblockRider({
required String myUid,
required String targetUid,
}) async {
final firestore = _firestore;
if (firestore == null) return;
await firestore
.collection('users')
.doc(myUid)
.collection('blocks')
.doc(targetUid)
.delete();
}
/// Stream of current friends.
Stream<List<FriendSummary>> friendsStream(String uid) {
final firestore = _firestore;
if (firestore == null) return Stream.value([]);
return firestore
.collection('users')
.doc(uid)
.collection('friends')
.snapshots()
.map((snapshot) {
return snapshot.docs
.map((doc) => FriendSummary.fromJson(doc.data()))
.toList()
..sort((a, b) => a.displayName.compareTo(b.displayName));
})
.handleError((e) {
debugPrint('[FriendshipService] Error in friends stream: $e');
return <FriendSummary>[];
});
}
/// Stream of incoming friend requests.
Stream<List<FriendRequest>> incomingRequestsStream(String uid) {
final firestore = _firestore;
if (firestore == null) return Stream.value([]);
return firestore
.collection('users')
.doc(uid)
.collection('friend_requests_incoming')
.snapshots()
.map((snapshot) {
return snapshot.docs
.map((doc) => FriendRequest.fromJson(doc.data()))
.toList()
..sort((a, b) => b.sentAt.compareTo(a.sentAt));
})
.handleError((e) {
debugPrint('[FriendshipService] Error in incoming requests stream: $e');
return <FriendRequest>[];
});
}
/// Stream of outgoing friend requests.
Stream<List<FriendRequest>> outgoingRequestsStream(String uid) {
final firestore = _firestore;
if (firestore == null) return Stream.value([]);
return firestore
.collection('users')
.doc(uid)
.collection('friend_requests_outgoing')
.snapshots()
.map((snapshot) {
return snapshot.docs
.map((doc) => FriendRequest.fromJson(doc.data()))
.toList()
..sort((a, b) => b.sentAt.compareTo(a.sentAt));
})
.handleError((e) {
debugPrint('[FriendshipService] Error in outgoing requests stream: $e');
return <FriendRequest>[];
});
}
/// Stream of blocked riders.
Stream<List<BlockedRider>> blockedRidersStream(String uid) {
final firestore = _firestore;
if (firestore == null) return Stream.value([]);
return firestore
.collection('users')
.doc(uid)
.collection('blocks')
.snapshots()
.map((snapshot) {
return snapshot.docs
.map((doc) => BlockedRider.fromJson(doc.data()))
.toList()
..sort((a, b) => b.blockedAt.compareTo(a.blockedAt));
})
.handleError((e) {
debugPrint('[FriendshipService] Error in blocked riders stream: $e');
return <BlockedRider>[];
});
}
}