code
Clone this repository
https://tangled.org/bretton.dev/coves-mobile
git@knot.bretton.dev:bretton.dev/coves-mobile
For self-hosted knots, clone URLs may differ based on your setup.
Complete voting system improvements including lexicon migration,
optimistic UI updates, initial vote state loading, and performance
optimizations from PR review.
All 119 tests passing ✅
This PR implements several improvements to the voting system:
**1. Vote Lexicon Migration**
- Migrate from social.coves.interaction.vote to social.coves.feed.vote
- Aligns with backend migration (commit 7a87d6b)
- Follows atProto conventions (like app.bsky.feed.like)
**2. Optimistic Score Updates**
- Vote counts update immediately when users vote
- Score adjustments tracked per post:
- Create upvote: +1
- Remove upvote: -1
- Create downvote: -1
- Remove downvote: +1
- Switch up→down: -2
- Switch down→up: +2
- Automatic rollback on API errors
- 7 new tests covering all scenarios
**3. Initial Vote State Loading**
- Added VoteService.getUserVotes() to query PDS for user's votes
- Added VoteProvider.loadInitialVotes() to bulk-load vote state
- FeedProvider loads vote state after fetching posts
- Hearts now fill correctly on app reload
**4. Performance Optimization** (PR Review)
- Added vote state cache to avoid O(n) PDS lookups
- VoteProvider passes cached state (rkey + direction) to VoteService
- Eliminates 5-10 API calls for users with many votes
- Performance: O(1) instead of O(n)
**5. Code Quality Improvements** (PR Review)
- Fix: Unsafe force unwrap in Provider initialization (main.dart:57)
- Fix: Added specific catch types (on Exception catch)
- Fix: Reset score adjustments when loading votes (prevents double-counting)
- All 119 tests passing ✅
**Breaking Changes:** None
**Migration Required:** None (backward compatible)
**Test Results:**
- 119/119 tests passing
- 0 errors, 0 warnings
- Updated test mocks for new optional parameters
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Fixes critical bug where users could not unlike posts after voting on
more than 100 posts, leading to duplicate vote records accumulating on
their PDS.
## Problem
The _findExistingVote method only checked the first 100 vote records
(limit=100). Once a user voted on >100 posts, older votes fell off the
first page, causing:
- _findExistingVote to return null for older votes
- Creation of duplicate vote records instead of deleting originals
- Users unable to unlike older posts
## Solution
Implemented cursor-based pagination to search through ALL vote records:
- Added do-while loop to fetch pages until cursor is null
- Constructs URL with cursor parameter for subsequent pages
- Searches each page for matching vote before moving to next
- Early exit when vote is found for efficiency
## Changes
- lib/services/vote_service.dart:
- _findExistingVote now uses cursor-based pagination (lines 198-273)
- Added pageSize constant (100 records per page)
- Loop continues while cursor != null
- Updated doc comment to explain pagination
- test/services/vote_service_test.dart:
- Added 3 comprehensive pagination tests:
1. Vote found in first page
2. Vote found on second page (validates cursor following)
3. Vote not found after exhausting all pages
- Uses mockito to mock OAuthSession.fetchHandler
- Verifies correct number of listRecords calls
## Testing
All 112 tests pass (up from 109, +3 new pagination tests).
## Impact
✅ Users can now unlike posts regardless of vote count
✅ No duplicate vote records created
✅ Proper atProto cursor-based pagination pattern
✅ Early exit optimization when vote found
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Implemented full DPoP (Demonstrating Proof of Possession) authentication
for voting records using the local atproto_oauth_flutter package's built-in
capabilities.
**DPoP Implementation**:
- Uses OAuthSession.fetchHandler for all PDS requests
- Automatic token refresh on expiry
- Nonce management for replay protection
- Proper Authorization: DPoP <access_token> headers
- DPoP: <proof> signed JWT headers
**VoteService Changes**:
- Removed manual Dio HTTP client and interceptors
- Now uses session.fetchHandler for all XRPC calls:
- com.atproto.repo.createRecord
- com.atproto.repo.deleteRecord
- com.atproto.repo.listRecords
- Simplified authentication - all handled by OAuthSession
**Testing**:
- All 109 tests passing
- Successfully tested on real PDS (localhost:3001)
- Vote records properly created with correct schema:
- $type: social.coves.interaction.vote
- Strong references (uri + cid)
- Proper timestamps and direction
**Production Ready**:
✅ DPoP authentication working
✅ Direct-to-PDS writes successful
✅ Records match backend lexicon
✅ Ready for Jetstream integration testing
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Enables authenticated writes to user PDS by adding DPoP (Demonstrating
Proof-of-Possession) support to OAuthSession.fetchHandler().
Changes:
- Replace http.Client with Dio + DPoP interceptor
- Automatically add DPoP headers with JWT proofs and token binding (ath)
- Handle nonce management and automatic retry on nonce errors
- Add proper DioException handling for network/timeout errors
- Remove deprecated _makeRequest method and _httpClient field
This unblocks voting functionality by matching the Expo oauth-client-expo
DPoP implementation, allowing the Flutter app to write records to the PDS.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>