···
import '../config/environment_config.dart';
import '../models/coves_session.dart';
6
+
import '../providers/vote_provider.dart' show VoteState;
import 'api_exceptions.dart';
···
/// 1. Unseals the token to get the actual access/refresh tokens
/// 2. Uses stored DPoP keys to sign requests
/// 3. Writes to the user's PDS on their behalf
23
+
/// 4. Handles toggle logic (creating, deleting, or switching vote direction)
23
-
/// TODO: Backend vote endpoints need to be implemented:
24
-
/// - POST /xrpc/social.coves.feed.vote.create
25
-
/// - POST /xrpc/social.coves.feed.vote.delete
26
-
/// - GET /xrpc/social.coves.feed.vote.list (or included in feed response)
25
+
/// **Backend Endpoints**:
26
+
/// - POST /xrpc/social.coves.feed.vote.create - Creates, toggles, or switches votes
Future<CovesSession?> Function()? sessionGetter,
···
/// Collection name for vote records
static const String voteCollection = 'social.coves.feed.vote';
183
-
/// Get all votes for the current user
185
-
/// TODO: This needs a backend endpoint to list user's votes.
186
-
/// For now, returns empty map - votes will be fetched with feed data.
189
-
/// - `Map<String, VoteInfo>` where key is the post URI
190
-
/// - Empty map if not authenticated or no votes found
191
-
Future<Map<String, VoteInfo>> getUserVotes() async {
193
-
final userDid = _didGetter?.call();
194
-
if (userDid == null || userDid.isEmpty) {
198
-
final session = await _sessionGetter?.call();
199
-
if (session == null) {
203
-
// TODO: Implement backend endpoint for listing user votes
204
-
// For now, vote state should come from feed responses
207
-
'⚠️ getUserVotes: Backend endpoint not yet implemented. '
208
-
'Vote state should come from feed responses.',
213
-
} on Exception catch (e) {
215
-
debugPrint('⚠️ Failed to load user votes: $e');
/// Create or toggle vote
223
-
/// Sends vote request to the Coves backend, which proxies to the user's PDS.
185
+
/// Sends vote request to the Coves backend, which handles toggle logic.
186
+
/// The backend will create a vote if none exists, or toggle it off if
187
+
/// voting the same direction again.
/// - [postUri]: AT-URI of the post
/// - [postCid]: Content ID of the post (for strong reference)
/// - [direction]: Vote direction - "up" for like/upvote, "down" for downvote
229
-
/// - [existingVoteRkey]: Optional rkey from cached state
230
-
/// - [existingVoteDirection]: Optional direction from cached state
233
-
/// - VoteResponse with uri/cid/rkey if created
234
-
/// - VoteResponse with deleted=true if toggled off
195
+
/// - VoteResponse with uri/cid/rkey if vote was created
196
+
/// - VoteResponse with deleted=true if vote was toggled off (empty uri/cid)
/// - ApiException for API errors
···
242
-
String? existingVoteRkey,
243
-
String? existingVoteDirection,
final userDid = _didGetter?.call();
···
debugPrint(' Direction: $direction');
263
-
// Determine if this is a toggle (delete) or create
264
-
final isToggleOff =
265
-
existingVoteRkey != null && existingVoteDirection == direction;
268
-
// Delete existing vote
269
-
return _deleteVote(session: session, rkey: existingVoteRkey);
272
-
// If switching direction, delete old vote first
273
-
if (existingVoteRkey != null && existingVoteDirection != null) {
275
-
debugPrint(' Switching vote direction - deleting old vote first');
277
-
await _deleteVote(session: session, rkey: existingVoteRkey);
280
-
// Create new vote via backend
223
+
// Send vote request to backend
// Note: Authorization header is added by the interceptor
final response = await _dio.post<Map<String, dynamic>>(
'/xrpc/social.coves.feed.vote.create',
···
final uri = data['uri'] as String?;
final cid = data['cid'] as String?;
298
-
if (uri == null || cid == null) {
299
-
throw ApiException('Invalid response from server - missing uri or cid');
241
+
// If uri/cid are empty, the backend toggled off an existing vote
242
+
if (uri == null || uri.isEmpty || cid == null || cid.isEmpty) {
244
+
debugPrint('✅ Vote toggled off (deleted)');
246
+
return const VoteResponse(deleted: true);
302
-
// Extract rkey from URI
303
-
final rkey = uri.split('/').last;
249
+
// Extract rkey from URI using shared utility
250
+
final rkey = VoteState.extractRkeyFromUri(uri);
debugPrint('✅ Vote created: $uri');
···
throw ApiException('Failed to create vote: $e');
334
-
/// Delete vote via backend
335
-
Future<VoteResponse> _deleteVote({
336
-
required CovesSession session,
337
-
required String rkey,
340
-
// Note: Authorization header is added by the interceptor
341
-
await _dio.post<void>(
342
-
'/xrpc/social.coves.feed.vote.delete',
343
-
data: {'rkey': rkey},
347
-
debugPrint('✅ Vote deleted');
350
-
return const VoteResponse(deleted: true);
351
-
} on DioException catch (e) {
353
-
debugPrint('❌ Delete vote failed: ${e.message}');
356
-
throw ApiException(
357
-
'Failed to delete vote: ${e.message}',
358
-
statusCode: e.response?.statusCode,
···
/// Whether the vote was deleted (toggled off)
386
-
/// Represents a vote that already exists on the PDS.
387
-
class ExistingVote {
388
-
const ExistingVote({required this.direction, required this.rkey});
390
-
/// Vote direction ("up" or "down")
391
-
final String direction;
393
-
/// Record key for deletion
399
-
/// Information about a user's vote on a post, returned from getUserVotes().
402
-
required this.direction,
403
-
required this.voteUri,
404
-
required this.rkey,
407
-
/// Vote direction ("up" or "down")
408
-
final String direction;
410
-
/// AT-URI of the vote record
411
-
final String voteUri;
413
-
/// Record key (rkey) - last segment of URI