Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 19 additions & 29 deletions lib/src/services/solid_profile_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ import 'dart:typed_data';
import 'package:flutter/foundation.dart' show debugPrint;

import 'package:rdflib/rdflib.dart' show Literal, Namespace, URIRef;
import 'package:shared_preferences/shared_preferences.dart';
import 'package:solidpod/solidpod.dart';

import 'package:solidui/src/services/solid_profile_notifier.dart';
Expand All @@ -47,10 +46,6 @@ const int maxProfilePictureBytes = 2 * 1024 * 1024;

const Set<String> allowedProfileExtensions = {'.png', '.jpg', '.jpeg'};

// SharedPreferences key prefix for the per-WebID privacy preference.

const String _privacyPrefKey = 'solidui_profile_privacy_';

/// Manages reading, writing, and deleting profile data on the user's POD.

class SolidProfileService {
Expand Down Expand Up @@ -98,8 +93,6 @@ class SolidProfileService {
if (_initialised) return;
if (!await isUserLoggedIn()) return;

await _loadPrivacyPreference();

final dirUrl = await _profileDirUrl();

// Create the folder if it is missing. We deliberately do not swallow
Expand Down Expand Up @@ -139,6 +132,9 @@ class SolidProfileService {

try {
await ensureProfileFolder();
// Privacy must be resolved before avatar/display-name because it
// determines the decryption mode used by readPod.
await _loadPrivacyFromAcl();
await Future.wait([_loadAvatar(), _loadDisplayName()]);
} catch (e) {
debugPrint('SolidProfileService.loadProfile: $e');
Expand Down Expand Up @@ -256,7 +252,6 @@ class SolidProfileService {
final currentName = solidProfileNotifier.displayName;

solidProfileNotifier.setPrivacy(mode);
await _persistPrivacyPreference(mode);

if (currentAvatar != null) {
await saveAvatar(currentAvatar);
Expand Down Expand Up @@ -294,30 +289,25 @@ class SolidProfileService {
await createResource(aclUrl, content: aclTurtle, replaceIfExist: true);
}

Future<void> _loadPrivacyPreference() async {
try {
final webId = await getWebId();
if (webId == null) return;
final prefs = await SharedPreferences.getInstance();
final stored = prefs.getString('$_privacyPrefKey$webId');
if (stored == SolidProfilePrivacy.public.name) {
solidProfileNotifier.setPrivacy(SolidProfilePrivacy.public);
} else {
solidProfileNotifier.setPrivacy(SolidProfilePrivacy.private);
}
} catch (e) {
debugPrint('SolidProfileService._loadPrivacyPreference: $e');
}
}
/// Reads the profile folder ACL from the POD and updates
/// [solidProfileNotifier] with the derived [SolidProfilePrivacy].
/// Public mode is inferred from the presence of a public-read grant
/// (`foaf:Agent` + `acl:Read`) in the ACL turtle.

Future<void> _persistPrivacyPreference(SolidProfilePrivacy mode) async {
Future<void> _loadPrivacyFromAcl() async {
try {
final webId = await getWebId();
if (webId == null) return;
final prefs = await SharedPreferences.getInstance();
await prefs.setString('$_privacyPrefKey$webId', mode.name);
final dirUrl = await _profileDirUrl();
final aclUrl = '$dirUrl.acl';
if (await checkResourceStatus(aclUrl) != ResourceStatus.exist) return;

final content = await readPod(aclUrl, pathType: PathType.absoluteUrl);
final isPublic =
content.contains('foaf:Agent') && content.contains('acl:Read');
solidProfileNotifier.setPrivacy(
isPublic ? SolidProfilePrivacy.public : SolidProfilePrivacy.private,
);
} catch (e) {
debugPrint('SolidProfileService._persistPrivacyPreference: $e');
debugPrint('SolidProfileService._loadPrivacyFromAcl: $e');
}
}

Expand Down
26 changes: 25 additions & 1 deletion lib/src/widgets/solid_profile_editor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ class _SolidProfileEditorState extends State<SolidProfileEditor> {
bool _isSaving = false;
late SolidProfilePrivacy _pendingPrivacy;

/// True once the user explicitly toggles the privacy selector.
/// Prevents async [loadProfile] completions from overwriting a local edit.

bool _privacyUserEdited = false;

@override
void initState() {
super.initState();
Expand All @@ -74,14 +79,30 @@ class _SolidProfileEditorState extends State<SolidProfileEditor> {
);
_pendingAvatar = solidProfileNotifier.avatarBytes;
_pendingPrivacy = solidProfileNotifier.privacy;
solidProfileNotifier.addListener(_onProfileNotifierChanged);
}

@override
void dispose() {
solidProfileNotifier.removeListener(_onProfileNotifierChanged);
_nameController.dispose();
super.dispose();
}

/// Keeps [_pendingPrivacy] in sync if [solidProfileNotifier] is updated
/// asynchronously (e.g. [loadProfile] completes after the editor opens).
/// Only updates the pending value when the user has not already explicitly
/// toggled the privacy selector.

void _onProfileNotifierChanged() {
if (!mounted || _privacyUserEdited) return;
if (_pendingPrivacy != solidProfileNotifier.privacy) {
setState(() {
_pendingPrivacy = solidProfileNotifier.privacy;
});
}
}

bool get _hasChanges {
final nameChanged =
_nameController.text.trim() != (solidProfileNotifier.displayName ?? '');
Expand Down Expand Up @@ -224,7 +245,10 @@ class _SolidProfileEditorState extends State<SolidProfileEditor> {
selected: {_pendingPrivacy},
onSelectionChanged: _isSaving
? null
: (values) => setState(() => _pendingPrivacy = values.first),
: (values) => setState(() {
_pendingPrivacy = values.first;
_privacyUserEdited = true;
}),
),
const SizedBox(height: 6),
Text(
Expand Down
1 change: 1 addition & 0 deletions lib/src/widgets/solid_scaffold.dart
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import 'package:flutter/material.dart';

import 'package:solidui/src/constants/navigation.dart';
import 'package:solidui/src/handlers/solid_auth_handler.dart';
import 'package:solidui/src/services/solid_login_status_notifier.dart';
import 'package:solidui/src/services/solid_profile_service.dart';
import 'package:solidui/src/services/solid_security_key_notifier.dart';
import 'package:solidui/src/services/solid_security_key_service.dart';
Expand Down
19 changes: 19 additions & 0 deletions lib/src/widgets/solid_scaffold_state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,13 @@ class SolidScaffoldState extends State<SolidScaffold> {
}
});
_loadCurrentWebId();
// Profile is loaded reactively via _onLoginStatusChangedForProfile when
// solidLoginStatusNotifier confirms the user is logged in. This avoids
// the race condition where isUserLoggedIn() returns false immediately
// after a page refresh before auth state has been restored from storage.
// As a best-effort fallback we also attempt an eager load here; if auth
// is not yet ready loadProfile() will be a no-op and the listener above
// will retry once the login status is resolved.
if (widget.enableProfile) {
SolidProfileService.instance.loadProfile();
}
Expand Down Expand Up @@ -83,6 +90,9 @@ class SolidScaffoldState extends State<SolidScaffold> {
}
solidPreferencesNotifier.addListener(_onPreferencesChanged);
widget.controller?.addListener(_onControllerChanged);
if (widget.enableProfile) {
solidLoginStatusNotifier.addListener(_onLoginStatusChangedForProfile);
}
}

Future<void> _initializeNotifiers() async {
Expand Down Expand Up @@ -114,6 +124,9 @@ class SolidScaffoldState extends State<SolidScaffold> {
}
solidPreferencesNotifier.removeListener(_onPreferencesChanged);
widget.controller?.removeListener(_onControllerChanged);
if (widget.enableProfile) {
solidLoginStatusNotifier.removeListener(_onLoginStatusChangedForProfile);
}
super.dispose();
}

Expand Down Expand Up @@ -143,6 +156,12 @@ class SolidScaffoldState extends State<SolidScaffold> {
widget.statusBar?.securityKeyStatus?.onKeyStatusChanged,
);

void _onLoginStatusChangedForProfile() {
if (solidLoginStatusNotifier.isLoggedIn) {
SolidProfileService.instance.loadProfile();
}
}

Future<void> refreshSecurityKeyStatus() async =>
await _securityKeyHelper?.refresh(
_isKeySaved,
Expand Down
Loading