[READ ONLY MIRROR] Open Source TikTok alternative built on AT Protocol github.com/sprksocial/client
flutter atproto video dart
10
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: bluesky profile in onboarding

+121 -13
+1
lib/src/core/auth/data/models/onboarding_screen_state.dart
··· 13 13 String? initialAvatarCid, 14 14 String? initialAvatarUrl, 15 15 Uint8List? localAvatarBytes, 16 + @Default(false) bool removeInitialAvatar, 16 17 @Default('') String displayName, 17 18 @Default('') String description, 18 19 String? errorMessage,
+3
lib/src/core/auth/data/repositories/onboarding_repository.dart
··· 8 8 /// Retrieves the Bluesky profile for import 9 9 Future<ActorProfileRecord?> getBskyProfile(); 10 10 11 + /// Retrieves the resolved Bluesky avatar URL for the current user. 12 + Future<String?> getBskyAvatarUrl(); 13 + 11 14 /// Creates a Spark actor profile with custom values 12 15 Future<void> createSparkProfile({ 13 16 required String displayName,
+49 -3
lib/src/core/auth/data/repositories/onboarding_repository_impl.dart
··· 56 56 57 57 @override 58 58 Future<ActorProfileRecord?> getBskyProfile() async { 59 - if (_did == null) return null; 59 + await _authRepository.initializationComplete; 60 + 61 + if (_did == null || _did!.isEmpty) return null; 60 62 61 63 try { 64 + final atproto = _atproto; 65 + if (atproto == null) { 66 + _logger.w('AtProto not initialized while fetching Bluesky profile'); 67 + return null; 68 + } 69 + 62 70 final uri = AtUri.parse('at://$_did/app.bsky.actor.profile/self'); 63 - final response = await _repoRepository.getRecord(uri: uri); 64 - return ActorProfileRecord.fromJson(response.record.toJson()); 71 + final response = await atproto.repo.getRecord( 72 + repo: uri.hostname, 73 + collection: uri.collection.toString(), 74 + rkey: uri.rkey, 75 + ); 76 + 77 + return ActorProfileRecord.fromJson(response.data.value); 65 78 } catch (e) { 66 79 _logger.i('Bluesky profile not found', error: e); 80 + return null; 81 + } 82 + } 83 + 84 + @override 85 + Future<String?> getBskyAvatarUrl() async { 86 + await _authRepository.initializationComplete; 87 + 88 + if (_did == null || _did!.isEmpty) return null; 89 + 90 + try { 91 + final atproto = _atproto; 92 + if (atproto == null) { 93 + _logger.w('AtProto not initialized while fetching Bluesky avatar URL'); 94 + return null; 95 + } 96 + 97 + final oauthSession = atproto.oAuthSession; 98 + if (oauthSession == null) { 99 + _logger.w('OAuth session missing while fetching Bluesky avatar URL'); 100 + return null; 101 + } 102 + 103 + final bluesky = bs.Bluesky.fromOAuthSession(oauthSession); 104 + final profile = await bluesky.actor.getProfile(actor: _did!); 105 + 106 + return profile.data.avatar; 107 + } catch (e, s) { 108 + _logger.i( 109 + 'Failed to resolve Bluesky avatar URL', 110 + error: e, 111 + stackTrace: s, 112 + ); 67 113 return null; 68 114 } 69 115 }
+6 -2
lib/src/core/utils/logging/logger_factory.dart
··· 1 + import 'dart:io'; 2 + 1 3 import 'package:flutter/foundation.dart'; 2 4 3 5 import 'package:spark/src/core/utils/logging/console_output.dart'; ··· 11 13 /// Global minimum log level 12 14 static LogLevel _globalMinLevel = LogLevel.warning; 13 15 16 + static bool get _supportsFileLogging => !kIsWeb && !Platform.isIOS; 17 + 14 18 /// List of default outputs 15 19 static final List<LogOutput> _defaultOutputs = [ 16 20 ConsoleOutput(), 17 - if (!kIsWeb) FileOutput(), 21 + if (_supportsFileLogging) FileOutput(), 18 22 ]; 19 23 20 24 /// Map of logger instances by name ··· 69 73 _defaultOutputs 70 74 ..clear() 71 75 ..add(ConsoleOutput()); 72 - if (!kIsWeb) { 76 + if (_supportsFileLogging) { 73 77 _defaultOutputs.add(FileOutput()); 74 78 } 75 79 _globalMinLevel = LogLevel.warning;
+30 -7
lib/src/features/auth/providers/onboarding_notifier.dart
··· 38 38 } 39 39 40 40 final profileDataMap = await _onboardingRepository.getBskyProfile(); 41 - 42 41 final avatarCid = profileDataMap?.avatar?.ref.link; 43 - final avatarUrl = avatarCid != null && avatarCid.isNotEmpty 44 - ? 'https://cdn.bsky.app/img/avatar/plain/$userDid/$avatarCid@jpeg' 45 - : null; 42 + final avatarUrl = await _onboardingRepository.getBskyAvatarUrl(); 46 43 47 44 return OnboardingScreenState( 48 45 isLoading: false, ··· 91 88 final pickedFile = await picker.pickImage(source: ImageSource.gallery); 92 89 if (pickedFile != null && state.hasValue) { 93 90 final bytes = await pickedFile.readAsBytes(); 94 - state = AsyncValue.data(state.value!.copyWith(localAvatarBytes: bytes)); 91 + state = AsyncValue.data( 92 + state.value!.copyWith( 93 + localAvatarBytes: bytes, 94 + removeInitialAvatar: false, 95 + ), 96 + ); 95 97 } 96 98 } 97 99 ··· 117 119 118 120 void revertAvatarToInitial() { 119 121 if (state.hasValue) { 120 - state = AsyncValue.data(state.value!.copyWith(localAvatarBytes: null)); 122 + state = AsyncValue.data( 123 + state.value!.copyWith( 124 + localAvatarBytes: null, 125 + removeInitialAvatar: false, 126 + ), 127 + ); 121 128 } 122 129 } 123 130 124 131 void clearAvatarSelection() { 125 132 if (state.hasValue) { 126 - state = AsyncValue.data(state.value!.copyWith(localAvatarBytes: null)); 133 + final current = state.value!; 134 + final hasInitialAvatar = 135 + (current.initialAvatarUrl?.isNotEmpty ?? false) || 136 + (current.initialAvatarCid?.isNotEmpty ?? false); 137 + 138 + state = AsyncValue.data( 139 + current.copyWith( 140 + localAvatarBytes: null, 141 + removeInitialAvatar: hasInitialAvatar, 142 + ), 143 + ); 127 144 } 128 145 } 129 146 ··· 135 152 return null; 136 153 } 137 154 155 + if (currentVal.removeInitialAvatar) { 156 + return null; 157 + } 158 + 138 159 if (currentVal.initialAvatarUrl != null && 139 160 currentVal.initialAvatarUrl!.isNotEmpty) { 140 161 return currentVal.initialAvatarUrl; ··· 156 177 Uint8List? avatarBytes, 157 178 String? initialAvatarUrl, 158 179 String? initialAvatarCid, 180 + bool removeInitialAvatar, 159 181 })? 160 182 getOnboardingDataForNextStep() { 161 183 if (!state.hasValue) return null; ··· 166 188 avatarBytes: current.localAvatarBytes, 167 189 initialAvatarUrl: current.initialAvatarUrl, 168 190 initialAvatarCid: current.initialAvatarCid, 191 + removeInitialAvatar: current.removeInitialAvatar, 169 192 ); 170 193 } 171 194 }
+32 -1
lib/src/features/auth/ui/pages/onboarding_page.dart
··· 77 77 final currentState = ref.read(onboardingProvider).value; 78 78 if (currentState?.localAvatarBytes != null) { 79 79 avatarToUse = currentState!.localAvatarBytes; 80 - } else if (currentState?.bskyProfileRecord?.avatar != null) { 80 + } else if (currentState?.removeInitialAvatar != true && 81 + currentState?.bskyProfileRecord?.avatar != null) { 81 82 avatarToUse = currentState!.bskyProfileRecord!.avatar; 82 83 } 83 84 ··· 159 160 ), 160 161 ), 161 162 data: (state) { 163 + final hasImportedBskyProfile = state.bskyProfileRecord != null; 162 164 ImageProvider<Object>? avatarImageProvider; 163 165 if (state.localAvatarBytes != null) { 164 166 avatarImageProvider = MemoryImage(state.localAvatarBytes!); ··· 169 171 } 170 172 171 173 final hasLocalAvatar = state.localAvatarBytes != null; 174 + final hasInitialAvatar = 175 + (state.initialAvatarUrl?.isNotEmpty ?? false) || 176 + (state.initialAvatarCid?.isNotEmpty ?? false); 172 177 final isAvatarActive = 173 178 hasLocalAvatar || notifier.currentAvatarDisplayUrl != null; 174 179 ··· 180 185 mainAxisSize: MainAxisSize.min, 181 186 crossAxisAlignment: CrossAxisAlignment.stretch, 182 187 children: [ 188 + if (hasImportedBskyProfile) ...[ 189 + Container( 190 + padding: const EdgeInsets.all(16), 191 + decoration: BoxDecoration( 192 + color: colorScheme.surfaceContainerHighest, 193 + borderRadius: BorderRadius.circular(16), 194 + ), 195 + child: Text( 196 + 'We found your Bluesky profile in your repo and used it to autofill these details. You can change anything here before continuing, and this profile only appears in Spark, not on Bluesky.', 197 + style: theme.textTheme.bodyMedium?.copyWith( 198 + color: colorScheme.onSurfaceVariant, 199 + ), 200 + ), 201 + ), 202 + const SizedBox(height: 20), 203 + ], 183 204 Center( 184 205 child: Stack( 185 206 alignment: Alignment.bottomRight, ··· 250 271 ], 251 272 ), 252 273 ), 274 + if (state.removeInitialAvatar && hasInitialAvatar) ...[ 275 + const SizedBox(height: 8), 276 + Center( 277 + child: TextButton.icon( 278 + onPressed: notifier.revertAvatarToInitial, 279 + icon: const Icon(Icons.undo), 280 + label: const Text('Use Bluesky avatar'), 281 + ), 282 + ), 283 + ], 253 284 const SizedBox(height: 16), 254 285 Form( 255 286 key: _formKey,