Skip to content
Merged
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
28 changes: 14 additions & 14 deletions ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -84,21 +84,21 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/sqflite_darwin/darwin"

SPEC CHECKSUMS:
audio_session: 9bb7f6c970f21241b19f5a3658097ae459681ba0
emoji_picker_flutter: ece213fc274bdddefb77d502d33080dc54e616cc
audio_session: 19e9480dbdd4e5f6c4543826b2e8b0e4ab6145fe
emoji_picker_flutter: 8e50ec5caac456a23a78637e02c6293ea0ac8771
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13
image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e
just_audio: 4e391f57b79cad2b0674030a00453ca5ce817eed
mobile_scanner: 9157936403f5a0644ca3779a38ff8404c5434a93
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
rust_lib_whitenoise: 22de658398f8e36a1a396d35b6b6547a0732e6bb
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
flutter_native_splash: df59bb2e1421aa0282cb2e95618af4dcb0c56c29
flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573
just_audio: a42c63806f16995daf5b219ae1d679deb76e6a79
mobile_scanner: 77265f3dc8d580810e91849d4a0811a90467ed5e
package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
rust_lib_whitenoise: 69ef24b69b2aba78a7ebabc09a504b5a39177d21
share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d

PODFILE CHECKSUM: 251cb053df7158f337c0712f2ab29f4e0fa474ce

Expand Down
202 changes: 173 additions & 29 deletions lib/ui/settings/general_settings_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -37,20 +37,37 @@ class GeneralSettingsScreen extends ConsumerStatefulWidget {
class _GeneralSettingsScreenState extends ConsumerState<GeneralSettingsScreen> {
List<Account> _accounts = [];
Account? _currentAccount;
Map<String, ContactModel> _accountContactModels = {}; // Cache for contact models
final Map<String, ContactModel> _accountContactModels = {}; // Cache for contact models
ProviderSubscription<AsyncValue<ActiveAccountState>>? _activeAccountSubscription;
PackageInfo? _packageInfo;

// Loading states
bool _isLoadingAccounts = false;
String? _loadingError;
bool _isLoadingInProgress = false; // Prevent multiple simultaneous loads

@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
_loadAccounts();
_loadFromProviders();
_loadPackageInfo();
_activeAccountSubscription = ref.listenManual(
Comment on lines +53 to 55
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Prevent the initial “No accounts found” flash on first frame

Because loading starts in a post-frame callback, the first build shows “No accounts found” until _isLoadingAccounts flips. Start in loading=true and clear it as soon as provider data sets _currentAccount.

Apply this diff:

@@
-  bool _isLoadingAccounts = false;
+  bool _isLoadingAccounts = true;
@@
-      setState(() {
-        _currentAccount = activeAccount;
-      });
+      setState(() {
+        _currentAccount = activeAccount;
+        _isLoadingAccounts = false;
+      });

Also applies to: 449-454

activeAccountProvider,
(previous, next) {
if (next is AsyncData) {
_loadAccounts();
// Reload when account data changes (including metadata updates)
if (next is AsyncData && !_isLoadingInProgress) {
final newAccount = next.value?.account;
final newMetadata = next.value?.metadata;

// Check if account changed OR metadata updated
final accountChanged = newAccount?.pubkey != _currentAccount?.pubkey;
final metadataChanged =
previous is AsyncData<ActiveAccountState> && previous.value.metadata != newMetadata;

if (accountChanged || metadataChanged) {
_refreshAccountData();
}
}
},
);
Expand All @@ -63,29 +80,156 @@ class _GeneralSettingsScreenState extends ConsumerState<GeneralSettingsScreen> {
super.dispose();
}

Future<void> _loadAccounts() async {
try {
final accounts = await getAccounts();
final contactModels = <String, ContactModel>{};
final userProfileDataNotifier = ref.read(userProfileDataProvider.notifier);
for (final account in accounts) {
final userProfileData = await userProfileDataNotifier.getUserProfileData(account.pubkey);
contactModels[account.pubkey] = userProfileData;
}
// Load from existing providers instead of making new bridge calls
Future<void> _loadFromProviders() async {
if (!mounted) return;

final activeAccountState = await ref.read(activeAccountProvider.future);
final activeAccount = activeAccountState.account;
// Get active account from provider (already loaded at app startup)
final activeAccountState = ref.read(activeAccountProvider);
final activeAccount = activeAccountState.value?.account;
final activeMetadata = activeAccountState.value?.metadata;

if (activeAccount != null) {
setState(() {
_accounts = accounts;
_currentAccount = activeAccount;
_accountContactModels = contactModels;
});

// If we have metadata, create ContactModel immediately
if (activeMetadata != null) {
try {
final contactModel = ContactModel.fromMetadata(
pubkey: activeAccount.pubkey,
metadata: activeMetadata,
);
setState(() {
_accountContactModels[activeAccount.pubkey] = contactModel;
});
} catch (e) {
debugPrint('Failed to create ContactModel from metadata: $e');
}
}

// Only load additional accounts if we need the full list for switching
_loadAccountsLazilyIfNeeded();
} else {
// Fallback: If no account in provider, load from API
_loadAccountsFromApi();
}
}

// Lazy load full account list only when needed
Future<void> _loadAccountsLazilyIfNeeded() async {
if (_accounts.isEmpty) {
try {
final accounts = await getAccounts();
if (mounted) {
setState(() {
_accounts = accounts;
});

// Load contact models for non-active accounts using userProfileDataProvider
_loadContactModelsForOtherAccounts(accounts);
}
} catch (e) {
// Silently handle - account switching might not work but current account will show
debugPrint('Failed to load accounts list: $e');
}
}
}

// Load contact models for accounts that don't have metadata yet
Future<void> _loadContactModelsForOtherAccounts(List<Account> accounts) async {
final userProfileDataNotifier = ref.read(userProfileDataProvider.notifier);

for (final account in accounts) {
// Skip if we already have this account's contact model
if (!_accountContactModels.containsKey(account.pubkey)) {
// Load in background, don't wait for all
userProfileDataNotifier
.getUserProfileData(account.pubkey)
.then((contactModel) {
if (mounted) {
setState(() {
_accountContactModels[account.pubkey] = contactModel;
});
}
})
.catchError((e) {
debugPrint('Failed to load profile data for ${account.pubkey}: $e');
// Will use fallback in _accountToContactModel
});
}
}
}

// Fallback method - only used if providers don't have data
Future<void> _loadAccountsFromApi() async {
if (!mounted || _isLoadingInProgress) return;

_isLoadingInProgress = true;

setState(() {
_isLoadingAccounts = true;
_loadingError = null;
});

try {
final accounts = await getAccounts();
final activeAccountState = ref.read(activeAccountProvider);
final activeAccount = activeAccountState.value?.account;

if (mounted) {
setState(() {
_accounts = accounts;
_currentAccount = activeAccount;
_isLoadingAccounts = false;
});
}
} catch (e) {
if (mounted) {
setState(() {
_loadingError = e.toString();
_isLoadingAccounts = false;
});
ref.showErrorToast('Failed to load accounts: $e');
}
} finally {}
} finally {
_isLoadingInProgress = false;
}
}
Comment on lines +166 to +199
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Fallback should still show a valid account instead of “No accounts found”

If the provider hasn’t resolved yet, _currentAccount stays null even when the accounts list exists, which reintroduces the “No accounts found” UI. Default _currentAccount to the first account as a temporary selection until the provider catches up.

@@
-        setState(() {
-          _accounts = accounts;
-          _currentAccount = activeAccount;
-          _isLoadingAccounts = false;
-        });
+        setState(() {
+          _accounts = accounts;
+          _currentAccount = activeAccount ?? (accounts.isNotEmpty ? accounts.first : null);
+          _isLoadingAccounts = false;
+        });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Future<void> _loadAccountsFromApi() async {
if (!mounted || _isLoadingInProgress) return;
_isLoadingInProgress = true;
setState(() {
_isLoadingAccounts = true;
_loadingError = null;
});
try {
final accounts = await getAccounts();
final activeAccountState = ref.read(activeAccountProvider);
final activeAccount = activeAccountState.value?.account;
if (mounted) {
setState(() {
_accounts = accounts;
_currentAccount = activeAccount;
_isLoadingAccounts = false;
});
}
} catch (e) {
if (mounted) {
setState(() {
_loadingError = e.toString();
_isLoadingAccounts = false;
});
ref.showErrorToast('Failed to load accounts: $e');
}
} finally {}
} finally {
_isLoadingInProgress = false;
}
}
Future<void> _loadAccountsFromApi() async {
if (!mounted || _isLoadingInProgress) return;
_isLoadingInProgress = true;
setState(() {
_isLoadingAccounts = true;
_loadingError = null;
});
try {
final accounts = await getAccounts();
final activeAccountState = ref.read(activeAccountProvider);
final activeAccount = activeAccountState.value?.account;
if (mounted) {
setState(() {
_accounts = accounts;
_currentAccount = activeAccount ?? (accounts.isNotEmpty ? accounts.first : null);
_isLoadingAccounts = false;
});
}
} catch (e) {
if (mounted) {
setState(() {
_loadingError = e.toString();
_isLoadingAccounts = false;
});
ref.showErrorToast('Failed to load accounts: $e');
}
} finally {
_isLoadingInProgress = false;
}
}
🤖 Prompt for AI Agents
In lib/ui/settings/general_settings_screen.dart around lines 166 to 199, the
current implementation leaves _currentAccount null when the provider hasn’t
resolved yet, causing the UI to show “No accounts found” even though accounts
were fetched; update the success path after fetching accounts so that if
activeAccount is null but the returned accounts list is non-empty, set
_currentAccount = accounts.first as a temporary/default selection (and keep
updating when the provider resolves), ensure this assignment happens inside the
mounted setState block and only when _currentAccount is null to avoid
overwriting a user selection.


// Refresh account data including contact models (for profile updates)
void _refreshAccountData() async {
if (!mounted || _isLoadingInProgress) return;

final activeAccountState = ref.read(activeAccountProvider);
final activeAccount = activeAccountState.value?.account;
final activeMetadata = activeAccountState.value?.metadata;

if (activeAccount != null) {
// Update current account immediately
setState(() {
_currentAccount = activeAccount;
});

// If provider has fresh metadata, use it immediately
if (activeMetadata != null) {
try {
final contactModel = ContactModel.fromMetadata(
pubkey: activeAccount.pubkey,
metadata: activeMetadata,
);
setState(() {
_accountContactModels[activeAccount.pubkey] = contactModel;
});
return;
} catch (e) {
debugPrint('Failed to create ContactModel from fresh metadata: $e');
}
}

// No additional fallback needed - use basic account info
}
}

Future<void> _loadPackageInfo() async {
Expand Down Expand Up @@ -169,16 +313,13 @@ class _GeneralSettingsScreenState extends ConsumerState<GeneralSettingsScreen> {
await _switchAccount(selectedAccount);
// Don't close the sheet - stay on settings screen after account switch
} else {
// Account not found, reload accounts and show error
// Just show error, don't reload to prevent flickering
if (mounted) {
try {
ref.showErrorToast('Account not found. Refreshing account list...');
ref.showErrorToast('Account not found. Please try switching account again.');
} catch (e) {
// Fallback if toast fails - just reload accounts silently
debugPrint('Toast error: $e');
}
_loadAccounts();
// Don't close the sheet - stay on settings screen
}
}
},
Expand Down Expand Up @@ -229,9 +370,8 @@ class _GeneralSettingsScreenState extends ConsumerState<GeneralSettingsScreen> {

final authNotifier = ref.read(authProvider.notifier);

// Check if there are multiple accounts before logout
final accounts = await getAccounts();
final hasMultipleAccounts = accounts.length > 2;
// Use cached accounts instead of making another bridge call
final hasMultipleAccounts = _accounts.length > 2;

if (!mounted) return;

Expand All @@ -252,14 +392,14 @@ class _GeneralSettingsScreenState extends ConsumerState<GeneralSettingsScreen> {

if (finalAuthState.isAuthenticated) {
if (hasMultipleAccounts) {
await _loadAccounts();
await _loadFromProviders();

if (mounted) {
_showAccountSwitcher(isDismissible: false, showSuccessToast: true);
}
} else {
ref.showSuccessToast('Account signed out. Switched to the other available account.');
await _loadAccounts();
await _loadFromProviders();
}
} else {
ref.showSuccessToast('Signed out successfully.');
Expand Down Expand Up @@ -306,7 +446,11 @@ class _GeneralSettingsScreenState extends ConsumerState<GeneralSettingsScreen> {
padding: EdgeInsets.symmetric(horizontal: 16.w),
child: Column(
children: [
if (_currentAccount != null)
if (_isLoadingAccounts)
const Center(child: CircularProgressIndicator())
else if (_loadingError != null)
const Center(child: Text('Error loading account'))
else if (_currentAccount != null)
ContactListTile(
contact: _accountToContactModel(_currentAccount!),
trailingIcon: WnImage(
Expand Down