···66import '../config/app_config.dart';
77import '../services/auth_service.dart';
88import '../services/onboarding_service.dart';
99+import '../services/settings_service.dart';
910import '../utils/app_colors.dart';
1011import '../utils/app_theme.dart';
1112import '../widgets/ataccount_dialog.dart';
···5859 });
59606061 if (success) {
6262+ if (!mounted) return;
6363+ final settingsService = Provider.of<SettingsService>(context, listen: false);
6464+ await settingsService.syncFollowModeFromServer();
6565+6166 final onboardingService = OnboardingService(authService);
6267 final hasSpark = await onboardingService.hasSparkProfile();
6868+6369 if (!mounted) return;
6470 Navigator.of(context).pushReplacementNamed(hasSpark ? '/home' : '/onboarding');
6571 } else {
+2-11
lib/screens/splash_screen.dart
···44import 'package:provider/provider.dart';
5566import '../services/auth_service.dart';
77-import '../services/onboarding_service.dart';
8798class SplashScreen extends StatefulWidget {
109 const SplashScreen({super.key});
···4443 final bool isSessionValid = await authService.validateSession();
45444645 if (!mounted) return;
4747- if (!isSessionValid) {
4848- Navigator.of(context).pushReplacementNamed('/auth');
4949- return;
5050- }
5151- // Check if Spark profile exists
5252- final onboardingService = OnboardingService(authService);
5353- final hasSpark = await onboardingService.hasSparkProfile();
5454- if (!mounted) return;
5555- final nextRoute = hasSpark ? '/home' : '/onboarding';
5656- Navigator.of(context).pushReplacementNamed(nextRoute);
4646+4747+ Navigator.of(context).pushReplacementNamed(isSessionValid ? '/home' : '/auth');
5748 }
58495950 @override
+47-28
lib/services/actions_service.dart
···5566import '../models/feed_post.dart';
77import 'auth_service.dart';
88+import 'settings_service.dart';
89import 'sprk_client.dart';
9101011class ActionsService extends ChangeNotifier {
1112 final AuthService _authService;
1313+ final SettingsService _settingsService;
1214 late final SprkClient _client;
13151414- ActionsService(this._authService) {
1616+ ActionsService(this._authService, this._settingsService) {
1517 _client = SprkClient(_authService);
1618 }
1719···205207 }
206208207209 Future<dynamic> followUser(String did) async {
208208- // Check if already following
209210 try {
210210- // Query existing follow records
211211- final existingFollows = await _client.repo.listRecords(
212212- repo: _authService.session!.did,
213213- collection: NSID.parse('so.sprk.graph.follow'),
214214- );
215215-216216- // Check if we're already following this specific user
217217- for (final record in existingFollows.data.records) {
218218- if (record.value['subject'] == did) {
219219- throw Exception('Already following this user');
211211+ final mode = _settingsService.followMode;
212212+ if (mode == FollowMode.sprk) {
213213+ // Check if already following in Spark
214214+ final existingFollows = await _client.repo.listRecords(
215215+ repo: _authService.session!.did,
216216+ collection: NSID.parse('so.sprk.graph.follow'),
217217+ );
218218+ for (final record in existingFollows.data.records) {
219219+ if (record.value['subject'] == did) {
220220+ throw Exception('Already following this user');
221221+ }
222222+ }
223223+ final followRecord = {
224224+ "\$type": "so.sprk.graph.follow",
225225+ "subject": did,
226226+ "createdAt": DateTime.now().toUtc().toIso8601String(),
227227+ };
228228+ final response = await _client.repo.createRecord(collection: NSID.parse('so.sprk.graph.follow'), record: followRecord);
229229+ if (response.status.code != 200) {
230230+ throw Exception('Failed to follow user: \\${response.status.code} \\${response.data}');
231231+ }
232232+ notifyListeners();
233233+ return response;
234234+ } else {
235235+ // Bluesky mode
236236+ final session = _authService.session;
237237+ if (session == null) throw Exception('Not authenticated');
238238+ // Check if already following in Bluesky
239239+ final followsRes = await _client.repo.listRecords(repo: session.did, collection: NSID.parse('app.bsky.graph.follow'));
240240+ for (final record in followsRes.data.records) {
241241+ if (record.value['subject'] == did) {
242242+ throw Exception('Already following this user');
243243+ }
244244+ }
245245+ final followRecord = {
246246+ "\$type": "app.bsky.graph.follow",
247247+ "subject": did,
248248+ "createdAt": DateTime.now().toUtc().toIso8601String(),
249249+ };
250250+ final response = await _client.repo.createRecord(collection: NSID.parse('app.bsky.graph.follow'), record: followRecord);
251251+ if (response.status.code != 200) {
252252+ throw Exception('Failed to follow user (bsky): \\${response.status.code} \\${response.data}');
220253 }
254254+ notifyListeners();
255255+ return response;
221256 }
222222-223223- // If not already following, create new follow record
224224- final followRecord = {
225225- "\$type": "so.sprk.graph.follow",
226226- "subject": did,
227227- "createdAt": DateTime.now().toUtc().toIso8601String(),
228228- };
229229-230230- final response = await _client.repo.createRecord(collection: NSID.parse('so.sprk.graph.follow'), record: followRecord);
231231-232232- if (response.status.code != 200) {
233233- throw Exception('Failed to follow user: ${response.status.code} ${response.data}');
234234- }
235235-236236- notifyListeners();
237237- return response;
238257 } catch (e) {
239258 debugPrint('Error in followUser: $e');
240259 rethrow;
+180
lib/services/onboarding_service.dart
···11+import 'dart:typed_data';
22+13import 'package:atproto/core.dart';
24import 'package:bluesky/bluesky.dart' as bs;
35···810class OnboardingService {
911 final AuthService _authService;
1012 final SprkClient _sprkClient;
1313+1414+ /// Maximum number of writes allowed in a single applyWrites request
1515+ static const int _maxWritesPerRequest = 200;
11161217 OnboardingService(this._authService) : _sprkClient = SprkClient(_authService);
1318···6065 if (response.status.code != 200) {
6166 throw Exception('Failed to create Spark profile: ${response.status.code} ${response.data}');
6267 }
6868+ }
6969+7070+ /// Finalizes the profile creation process including avatar upload.
7171+ Future<void> finalizeProfileCreation({
7272+ required String displayName,
7373+ required String description,
7474+ dynamic avatar, // This can be Uint8List or existing profile data
7575+ }) async {
7676+ dynamic avatarToSend = avatar;
7777+ if (avatar is List<int>) {
7878+ final resp = await _sprkClient.repo.uploadBlob(avatar as Uint8List);
7979+ if (resp.status.code != 200) {
8080+ throw Exception('Failed to upload avatar blob: ${resp.status.code}');
8181+ }
8282+ avatarToSend = resp.data.blob.toJson();
8383+ }
8484+8585+ await importCustomProfile(displayName: displayName, description: description, avatar: avatarToSend);
6386 }
64876588 /// Fetches the list of DIDs that the user follows on Bluesky
···86109 if (response.status.code != 200) {
87110 throw Exception('Failed to create Spark follow: ${response.status.code}');
88111 }
112112+ }
113113+114114+ /// Helper function to apply writes in chunks of 200 (max allowed per request)
115115+ Future<void> _applyWritesInChunks(List<Map<String, dynamic>> writes, String did) async {
116116+ final atproto = _authService.atproto;
117117+ if (atproto == null) throw Exception('Not authenticated');
118118+119119+ // Split writes into chunks of max 200 items
120120+ for (int i = 0; i < writes.length; i += _maxWritesPerRequest) {
121121+ final end = (i + _maxWritesPerRequest < writes.length) ? i + _maxWritesPerRequest : writes.length;
122122+ final chunk = writes.sublist(i, end);
123123+124124+ final response = await atproto.post(NSID.parse('com.atproto.repo.applyWrites'), body: {'repo': did, 'writes': chunk});
125125+126126+ if (response.status.code != 200) {
127127+ throw Exception('Failed to apply writes: ${response.status.code}');
128128+ }
129129+ }
130130+ }
131131+132132+ /// Creates multiple follow records in chunks using applyWrites
133133+ /// Returns a list of DIDs that were successfully followed
134134+ Future<List<String>> createBatchFollows(List<String> subjects) async {
135135+ if (subjects.isEmpty) return [];
136136+137137+ final session = _authService.session;
138138+ if (session == null) throw Exception('Not authenticated');
139139+140140+ // Create write operations for each follow
141141+ final writes =
142142+ subjects.map((subject) {
143143+ return {
144144+ '\$type': 'com.atproto.repo.applyWrites#create',
145145+ 'collection': 'so.sprk.graph.follow',
146146+ 'value': {
147147+ '\$type': 'so.sprk.graph.follow',
148148+ 'subject': subject,
149149+ 'createdAt': DateTime.now().toUtc().toIso8601String(),
150150+ },
151151+ };
152152+ }).toList();
153153+154154+ // Apply writes in chunks
155155+ await _applyWritesInChunks(writes, session.did);
156156+157157+ return subjects;
158158+ }
159159+160160+ /// Fetches the user's current Spark follows from their PDS
161161+ /// Returns a set of DIDs that the user follows
162162+ Future<Set<String>> getCurrentSparkFollows() async {
163163+ final session = _authService.session;
164164+ final atproto = _authService.atproto;
165165+166166+ if (session == null || atproto == null) throw Exception('Not authenticated');
167167+168168+ final followedDids = <String>{};
169169+ String? cursor;
170170+171171+ do {
172172+ final response = await atproto.repo.listRecords(
173173+ repo: session.did,
174174+ collection: NSID.parse('so.sprk.graph.follow'),
175175+ cursor: cursor,
176176+ limit: 100,
177177+ );
178178+179179+ if (response.status.code != 200) {
180180+ throw Exception('Failed to list Spark follows: ${response.status.code}');
181181+ }
182182+183183+ for (final record in response.data.records) {
184184+ // Convert to a Map to access the value field
185185+ final recordMap = record.toJson();
186186+ final value = recordMap['value'] as Map<String, dynamic>;
187187+ final subject = value['subject'] as String;
188188+ followedDids.add(subject);
189189+ }
190190+191191+ cursor = response.data.cursor;
192192+ } while (cursor != null);
193193+194194+ return followedDids;
195195+ }
196196+197197+ /// Cleanup duplicate follow records to ensure unique subject values
198198+ /// This function detects and removes duplicate follow records from the user's PDS
199199+ Future<int> gambiarraFixDuplicates() async {
200200+ final session = _authService.session;
201201+ final atproto = _authService.atproto;
202202+203203+ if (session == null || atproto == null) throw Exception('Not authenticated');
204204+205205+ // Fetch all follow records
206206+ final allFollows = <Map<String, dynamic>>[];
207207+ String? cursor;
208208+209209+ do {
210210+ final response = await atproto.repo.listRecords(
211211+ repo: session.did,
212212+ collection: NSID.parse('so.sprk.graph.follow'),
213213+ cursor: cursor,
214214+ limit: 100,
215215+ );
216216+217217+ if (response.status.code != 200) {
218218+ throw Exception('Failed to list follow records: ${response.status.code}');
219219+ }
220220+221221+ for (final record in response.data.records) {
222222+ allFollows.add(record.toJson());
223223+ }
224224+225225+ cursor = response.data.cursor;
226226+ } while (cursor != null);
227227+228228+ // Find duplicates: group by subject and keep track of records to delete
229229+ final subjectToRecords = <String, List<Map<String, dynamic>>>{};
230230+231231+ for (final record in allFollows) {
232232+ final subject = record['value']['subject'] as String;
233233+ subjectToRecords[subject] ??= [];
234234+ subjectToRecords[subject]!.add(record);
235235+ }
236236+237237+ // Prepare delete operations for duplicate records
238238+ // For each subject, sort by createdAt (oldest first) and keep the oldest record
239239+ final deleteWrites = <Map<String, dynamic>>[];
240240+241241+ for (final entry in subjectToRecords.entries) {
242242+ final records = entry.value;
243243+ if (records.length > 1) {
244244+ // Sort records by createdAt timestamp (oldest first)
245245+ records.sort((a, b) {
246246+ final aTimestamp = a['value']['createdAt'] as String;
247247+ final bTimestamp = b['value']['createdAt'] as String;
248248+ return aTimestamp.compareTo(bTimestamp);
249249+ });
250250+251251+ // Keep the oldest one (first after sorting) and mark others for deletion
252252+ for (int i = 1; i < records.length; i++) {
253253+ deleteWrites.add({
254254+ '\$type': 'com.atproto.repo.applyWrites#delete',
255255+ 'collection': 'so.sprk.graph.follow',
256256+ 'rkey': records[i]['uri'].toString().split('/').last,
257257+ });
258258+ }
259259+ }
260260+ }
261261+262262+ // If no duplicates found, return early
263263+ if (deleteWrites.isEmpty) return 0;
264264+265265+ // Apply delete operations in chunks
266266+ await _applyWritesInChunks(deleteWrites, session.did);
267267+268268+ return deleteWrites.length;
89269 }
90270}
+3-26
lib/services/profile_service.dart
···3030 try {
3131 final sprkProfile = await getProfileFullSprk(did, forceRefresh: forceRefresh);
3232 if (sprkProfile != null) {
3333- final viewer = sprkProfile['viewer'] as Map<dynamic, dynamic>?;
3433 return Profile.fromSparkProfile({
3534 'actor': sprkProfile,
3636- 'viewer': {...?viewer, 'following': existingFollowUri},
3535+ 'viewer': sprkProfile['viewer'] as Map<dynamic, dynamic>? ?? {},
3736 'source': 'spark',
3837 });
3938 }
···5352 final profile = Profile.fromBlueskyActor(bskyProfile);
5453 final counts = bskyProfile.toJson();
55545656- var followersCount = counts['followersCount'] as int? ?? 0;
5757- var followingCount = counts['followsCount'] as int? ?? 0;
5858-5959- // Try to enhance with Spark data, but don't fail if these calls fail
6060- final client = SprkClient(_authService);
6161-6262- try {
6363- final sparkFollowers = await client.graph.getFollowers(did);
6464-6565- final followers = sparkFollowers.data['followers'] as List;
6666- followersCount += followers.length;
6767- } catch (e) {
6868- debugPrint('Error fetching Spark followers: $e');
6969- // Continue anyway
7070- }
7171-7272- try {
7373- final sparkFollows = await client.graph.getFollows(did);
7474-7575- final follows = sparkFollows.data['follows'] as List;
7676- followingCount += follows.length;
7777- } catch (e) {
7878- debugPrint('Error fetching Spark follows: $e');
7979- }
5555+ final followersCount = counts['followersCount'] as int? ?? 0;
5656+ final followingCount = counts['followsCount'] as int? ?? 0;
80578158 return profile.withCounts({
8259 'followersCount': followersCount,
+122-70
lib/services/settings_service.dart
···11+import 'dart:convert';
22+13import 'package:flutter/material.dart';
24import 'package:shared_preferences/shared_preferences.dart';
33-import 'dart:convert';
55+66+import 'auth_service.dart';
77+import 'sprk_client.dart';
4859/// Enum to represent the user's preference for a specific label
66-enum LabelPreference {
77- show,
88- warn,
99- hide,
1010+enum LabelPreference { show, warn, hide }
1111+1212+/// Enum to represent follow mode options
1313+enum FollowMode {
1414+ sprk,
1515+ bsky;
1616+1717+ @override
1818+ String toString() => name;
1019}
11201221class SettingsService extends ChangeNotifier {
···1423 static const String _followedLabelersKey = 'followed_labelers';
1524 static const String _labelerPreferencesKey = 'labeler_preferences';
1625 static const String _hideAdultContentKey = 'hide_adult_content';
1717-2626+ static const String _keyFollowMode = 'profile_follow_mode';
2727+1828 SharedPreferences? _prefs;
1929 bool _isLoading = true;
2030 bool _feedBlurEnabled = false;
2121- bool _hideAdultContent = true; // On by default
3131+ bool _hideAdultContent = true;
2232 List<String> _followedLabelers = [];
2323-3333+ FollowMode _followMode = FollowMode.sprk;
3434+ final AuthService _authService;
3535+ final SprkClient _sprkClient;
3636+2437 /// Stores label preferences for each labeler
2538 /// Format: {labelerDid: {labelValue: preferenceValue}}
2639 Map<String, Map<String, String>> _labelPreferences = {};
27402828- SettingsService() {
4141+ SettingsService({required AuthService authService}) : _authService = authService, _sprkClient = SprkClient(authService) {
2942 _loadSettings();
4343+ // Listen for auth state changes to clear cached values on logout
4444+ _authService.addListener(_handleAuthStateChange);
4545+ }
4646+4747+ @override
4848+ void dispose() {
4949+ _authService.removeListener(_handleAuthStateChange);
5050+ super.dispose();
5151+ }
5252+5353+ void _handleAuthStateChange() {
5454+ if (!_authService.isAuthenticated) {
5555+ _clearCachedFollowMode();
5656+ }
5757+ }
5858+5959+ void _clearCachedFollowMode() {
6060+ _prefs?.remove(_keyFollowMode);
3061 }
31623263 bool get isLoading => _isLoading;
3364 bool get feedBlurEnabled => _feedBlurEnabled;
3465 bool get hideAdultContent => _hideAdultContent;
3566 List<String> get followedLabelers => List.unmodifiable(_followedLabelers);
3636-6767+ FollowMode get followMode => _followMode;
6868+3769 /// Returns an immutable copy of all labeler preferences
3838- Map<String, Map<String, String>> get labelPreferences =>
3939- Map.unmodifiable(_labelPreferences);
7070+ Map<String, Map<String, String>> get labelPreferences => Map.unmodifiable(_labelPreferences);
40714172 Future<void> _loadSettings() async {
4273 _prefs = await SharedPreferences.getInstance();
4374 _feedBlurEnabled = _prefs?.getBool(_feedBlurKey) ?? false;
4444- _hideAdultContent = _prefs?.getBool(_hideAdultContentKey) ?? true; // Default to true if not set
7575+ _hideAdultContent = _prefs?.getBool(_hideAdultContentKey) ?? true; // Default to true if not set
4576 _followedLabelers = _prefs?.getStringList(_followedLabelersKey) ?? [];
4646-7777+7878+ // Load the cached follow mode as a temporary value
7979+ final savedMode = _prefs?.getString(_keyFollowMode) ?? 'sprk';
8080+ _followMode = savedMode == 'bsky' ? FollowMode.bsky : FollowMode.sprk;
8181+4782 // Load label preferences
4883 final prefsJson = _prefs?.getString(_labelerPreferencesKey);
4984 if (prefsJson != null) {
5085 final Map<String, dynamic> decoded = jsonDecode(prefsJson);
5151- _labelPreferences = decoded.map((key, value) => MapEntry(
5252- key,
5353- (value as Map<String, dynamic>).map((k, v) => MapEntry(k, v.toString())),
5454- ));
8686+ _labelPreferences = decoded.map(
8787+ (key, value) => MapEntry(key, (value as Map<String, dynamic>).map((k, v) => MapEntry(k, v.toString()))),
8888+ );
5589 }
5656-9090+5791 _isLoading = false;
5892 notifyListeners();
5993 }
···6498 await _prefs?.setBool(_feedBlurKey, value);
6599 notifyListeners();
66100 }
6767-101101+68102 Future<void> setHideAdultContent(bool value) async {
69103 if (_isLoading) await _loadSettings();
70104 _hideAdultContent = value;
71105 await _prefs?.setBool(_hideAdultContentKey, value);
72106 notifyListeners();
73107 }
7474-108108+75109 Future<void> setFollowedLabelers(List<String> labelerDids) async {
76110 if (_isLoading) await _loadSettings();
77111 _followedLabelers = List<String>.from(labelerDids);
78112 await _prefs?.setStringList(_followedLabelersKey, _followedLabelers);
79113 notifyListeners();
80114 }
8181-115115+82116 Future<void> addFollowedLabeler(String labelerDid) async {
83117 if (_isLoading) await _loadSettings();
84118 if (!_followedLabelers.contains(labelerDid)) {
···87121 notifyListeners();
88122 }
89123 }
9090-124124+91125 Future<void> removeFollowedLabeler(String labelerDid) async {
92126 if (_isLoading) await _loadSettings();
93127 if (_followedLabelers.contains(labelerDid)) {
94128 _followedLabelers.remove(labelerDid);
95129 await _prefs?.setStringList(_followedLabelersKey, _followedLabelers);
9696-130130+97131 // Also remove preferences for this labeler
98132 _labelPreferences.remove(labelerDid);
99133 await _saveLabelPreferences();
100100-134134+101135 notifyListeners();
102136 }
103137 }
104104-138138+105139 /// Saves label preferences to SharedPreferences
106140 Future<void> _saveLabelPreferences() async {
107141 if (_prefs == null) return;
108108-142142+109143 final jsonStr = jsonEncode(_labelPreferences);
110144 await _prefs!.setString(_labelerPreferencesKey, jsonStr);
111145 }
112112-146146+113147 /// Sets a preference for a specific label from a labeler
114114- Future<void> setLabelPreference(
115115- String labelerDid,
116116- String labelValue,
117117- LabelPreference preference
118118- ) async {
148148+ Future<void> setLabelPreference(String labelerDid, String labelValue, LabelPreference preference) async {
119149 if (_isLoading) await _loadSettings();
120120-150150+121151 // Ensure the labeler is initialized in the map
122152 _labelPreferences[labelerDid] ??= {};
123123-153153+124154 // Set the preference
125155 _labelPreferences[labelerDid]![labelValue] = preference.name;
126126-156156+127157 // Save to SharedPreferences
128158 await _saveLabelPreferences();
129159 notifyListeners();
130160 }
131131-161161+132162 /// Removes a preference for a specific label, reverting to the default
133133- Future<void> removeLabelPreference(
134134- String labelerDid,
135135- String labelValue
136136- ) async {
163163+ Future<void> removeLabelPreference(String labelerDid, String labelValue) async {
137164 if (_isLoading) await _loadSettings();
138138-165165+139166 // Check if the labeler and preference exist
140167 if (_labelPreferences.containsKey(labelerDid)) {
141168 // Remove the specific preference
142169 _labelPreferences[labelerDid]?.remove(labelValue);
143143-170170+144171 // Save to SharedPreferences
145172 await _saveLabelPreferences();
146173 notifyListeners();
147174 }
148175 }
149149-176176+150177 /// Gets the preference for a specific label from a labeler
151178 /// Returns null if no preference is defined
152179 LabelPreference? getLabelPreference(String labelerDid, String labelValue) {
153180 if (_isLoading || !_labelPreferences.containsKey(labelerDid)) {
154181 return null;
155182 }
156156-183183+157184 final prefValue = _labelPreferences[labelerDid]?[labelValue];
158185 if (prefValue == null) return null;
159159-186186+160187 return LabelPreference.values.firstWhere(
161161- (e) => e.name == prefValue,
162162- orElse: () => LabelPreference.warn // default
188188+ (e) => e.name == prefValue,
189189+ orElse: () => LabelPreference.warn, // default
163190 );
164191 }
165165-192192+166193 /// Gets the preference for a specific label, or returns the default setting from the label definition
167167- LabelPreference getLabelPreferenceOrDefault(
168168- String labelerDid,
169169- String labelValue,
170170- Map<String, dynamic>? labelDefinition
171171- ) {
194194+ LabelPreference getLabelPreferenceOrDefault(String labelerDid, String labelValue, Map<String, dynamic>? labelDefinition) {
172195 // First try to get user's explicit preference
173196 final userPreference = getLabelPreference(labelerDid, labelValue);
174197 if (userPreference != null) {
175198 return userPreference;
176199 }
177177-200200+178201 // If no user preference and we have a label definition with defaultSetting
179202 if (labelDefinition != null && labelDefinition.containsKey('defaultSetting')) {
180203 final defaultSetting = labelDefinition['defaultSetting'] as String;
181181-204204+182205 // Map the defaultSetting string to LabelPreference
183206 switch (defaultSetting) {
184207 case 'show':
···191214 return LabelPreference.warn; // Fallback default
192215 }
193216 }
194194-217217+195218 // Final fallback
196219 return LabelPreference.warn;
197220 }
198198-221221+199222 /// Sets preferences in bulk for all labels from a labeler
200200- Future<void> setLabelerPreferences(
201201- String labelerDid,
202202- Map<String, LabelPreference> preferences
203203- ) async {
223223+ Future<void> setLabelerPreferences(String labelerDid, Map<String, LabelPreference> preferences) async {
204224 if (_isLoading) await _loadSettings();
205205-225225+206226 // Convert the map of enums to strings
207207- final stringPrefs = preferences.map(
208208- (key, value) => MapEntry(key, value.name)
209209- );
210210-227227+ final stringPrefs = preferences.map((key, value) => MapEntry(key, value.name));
228228+211229 _labelPreferences[labelerDid] = stringPrefs;
212230 await _saveLabelPreferences();
213231 notifyListeners();
214232 }
215215-233233+216234 /// Removes all preferences for a specific labeler
217235 Future<void> clearLabelerPreferences(String labelerDid) async {
218236 if (_isLoading) await _loadSettings();
219219-237237+220238 _labelPreferences.remove(labelerDid);
221239 await _saveLabelPreferences();
222240 notifyListeners();
223241 }
224224-} 242242+243243+ /// Fetch and sync the follow mode from the backend, store locally, and notify listeners if changed.
244244+ Future<void> syncFollowModeFromServer() async {
245245+ if (_isLoading) await _loadSettings();
246246+ try {
247247+ final response = await _sprkClient.actor.getPreferences();
248248+ final serverMode = response.data['followMode'] ?? 'sprk';
249249+ final mode = serverMode == 'bsky' ? FollowMode.bsky : FollowMode.sprk;
250250+ if (_followMode != mode) {
251251+ _followMode = mode;
252252+ await _prefs?.setString(_keyFollowMode, mode.name);
253253+ notifyListeners();
254254+ }
255255+ } catch (e) {
256256+ debugPrint('Failed to sync follow mode from server: $e');
257257+ }
258258+ }
259259+260260+ /// Sets the profile follow mode, saves it, and notifies listeners.
261261+ Future<void> setFollowMode(FollowMode mode) async {
262262+ if (_isLoading) await _loadSettings();
263263+264264+ _followMode = mode;
265265+ await _prefs?.setString(_keyFollowMode, mode.name);
266266+267267+ // Call the API to update the server-side preference
268268+ try {
269269+ await _sprkClient.actor.putPreferences(followMode: mode);
270270+ } catch (e) {
271271+ debugPrint('Failed to update server preference: $e');
272272+ }
273273+274274+ notifyListeners();
275275+ }
276276+}
+46
lib/services/sprk_client.dart
···55import 'package:sparksocial/config/app_config.dart';
6677import 'auth_service.dart';
88+import 'settings_service.dart';
89910/// Client for interacting with Spark API endpoints
1011class SprkClient {
···205206 return await atproto.get(
206207 NSID.parse('so.sprk.actor.searchActors'),
207208 parameters: {'q': query},
209209+ headers: {'atproto-proxy': _client._sprkDid},
210210+ to: (jsonMap) => jsonMap,
211211+ adaptor: (uint8) => jsonDecode(utf8.decode(uint8)),
212212+ );
213213+ });
214214+ }
215215+216216+ /// Set user preferences
217217+ ///
218218+ /// [followMode] The follow mode to set (FollowMode.bsky or FollowMode.sprk)
219219+ Future<dynamic> putPreferences({required FollowMode followMode}) async {
220220+ return _client._executeWithRetry(() async {
221221+ if (!_client._authService.isAuthenticated) {
222222+ throw Exception('Not authenticated');
223223+ }
224224+225225+ final atproto = _client._authService.atproto;
226226+ if (atproto == null) {
227227+ throw Exception('AtProto not initialized');
228228+ }
229229+230230+ return await atproto.post(
231231+ NSID.parse('so.sprk.actor.putPreferences'),
232232+ body: {'followMode': followMode.name},
233233+ headers: {'atproto-proxy': _client._sprkDid},
234234+ );
235235+ });
236236+ }
237237+238238+ /// Get user preferences
239239+ ///
240240+ /// Returns the user's preferences, including followMode
241241+ Future<dynamic> getPreferences() async {
242242+ return _client._executeWithRetry(() async {
243243+ if (!_client._authService.isAuthenticated) {
244244+ throw Exception('Not authenticated');
245245+ }
246246+247247+ final atproto = _client._authService.atproto;
248248+ if (atproto == null) {
249249+ throw Exception('AtProto not initialized');
250250+ }
251251+252252+ return await atproto.get(
253253+ NSID.parse('so.sprk.actor.getPreferences'),
208254 headers: {'atproto-proxy': _client._sprkDid},
209255 to: (jsonMap) => jsonMap,
210256 adaptor: (uint8) => jsonDecode(utf8.decode(uint8)),