470 lines
13 KiB
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>[];
|
|
});
|
|
}
|
|
}
|