import 'dart:collection'; import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:lemmy_api_client/v2.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../util/unawaited.dart'; import 'shared_pref_keys.dart'; /// Store that manages all accounts class AccountsStore extends ChangeNotifier { /// Map containing JWT tokens of specific users. /// If a token is in this map, the user is considered logged in /// for that account. /// `tokens['instanceHost']['username']` HashMap> _tokens; /// default account for a given instance /// map where keys are instanceHosts and values are usernames HashMap _defaultAccounts; /// default account for the app /// It is in a form of `username@instanceHost` String _defaultAccount; Future load() async { final prefs = await SharedPreferences.getInstance(); // I barely understand what I did. Long story short it casts a // raw json into a nested ObservableMap nestedMapsCast(T f(Map json)) => HashMap.of( (jsonDecode(prefs.getString(SharedPrefKeys.tokens) ?? '{"lemmy.ml":{}}') as Map) ?.map( (k, e) => MapEntry( k, HashMap.of( (e as Map)?.map( (k, e) => MapEntry( k, e == null ? null : f(e as Map)), ), ), ), ), ); // set saved settings or create defaults _tokens = nestedMapsCast((json) => Jwt(json['raw'] as String)); _defaultAccount = prefs.getString(SharedPrefKeys.defaultAccount); _defaultAccounts = HashMap.of(Map.castFrom( jsonDecode(prefs.getString(SharedPrefKeys.defaultAccounts) ?? 'null') as Map ?? {}, )); notifyListeners(); } Future save() async { final prefs = await SharedPreferences.getInstance(); await prefs.setString(SharedPrefKeys.defaultAccount, _defaultAccount); await prefs.setString( SharedPrefKeys.defaultAccounts, jsonEncode(_defaultAccounts)); await prefs.setString(SharedPrefKeys.tokens, jsonEncode(_tokens)); } /// automatically sets default accounts void _assignDefaultAccounts() { // remove dangling defaults _defaultAccounts.entries .map((dft) { final instance = dft.key; final username = dft.value; // if instance or username doesn't exist, remove if (!instances.contains(instance) || !usernamesFor(instance).contains(username)) { return instance; } }) .toList() .forEach(_defaultAccounts.remove); if (_defaultAccount != null) { final instance = _defaultAccount.split('@')[1]; final username = _defaultAccount.split('@')[0]; // if instance or username doesn't exist, remove if (!instances.contains(instance) || !usernamesFor(instance).contains(username)) { _defaultAccount = null; } } // set local defaults for (final instanceHost in instances) { // if this instance is not in defaults if (!_defaultAccounts.containsKey(instanceHost)) { // select first account in this instance, if any if (!isAnonymousFor(instanceHost)) { setDefaultAccountFor(instanceHost, usernamesFor(instanceHost).first); } } } // set global default if (_defaultAccount == null) { // select first account of first instance for (final instanceHost in instances) { // select first account in this instance, if any if (!isAnonymousFor(instanceHost)) { setDefaultAccount(instanceHost, usernamesFor(instanceHost).first); } } } } String get defaultUsername { if (_defaultAccount == null) { return null; } return _defaultAccount.split('@')[0]; } String get defaultInstanceHost { if (_defaultAccount == null) { return null; } return _defaultAccount.split('@')[1]; } String defaultUsernameFor(String instanceHost) { if (isAnonymousFor(instanceHost)) { return null; } return _defaultAccounts[instanceHost]; } Jwt get defaultToken { if (_defaultAccount == null) { return null; } final userTag = _defaultAccount.split('@'); return _tokens[userTag[1]][userTag[0]]; } Jwt defaultTokenFor(String instanceHost) { if (isAnonymousFor(instanceHost)) { return null; } return _tokens[instanceHost][_defaultAccounts[instanceHost]]; } Jwt tokenFor(String instanceHost, String username) { if (!usernamesFor(instanceHost).contains(username)) { return null; } return _tokens[instanceHost][username]; } /// sets globally default account void setDefaultAccount(String instanceHost, String username) { _defaultAccount = '$username@$instanceHost'; notifyListeners(); save(); } /// sets default account for given instance void setDefaultAccountFor(String instanceHost, String username) { _defaultAccounts[instanceHost] = username; notifyListeners(); save(); } /// An instance is considered anonymous if it was not /// added or there are no accounts assigned to it. bool isAnonymousFor(String instanceHost) { if (!instances.contains(instanceHost)) { return true; } return _tokens[instanceHost].isEmpty; } /// `true` if no added instance has an account assigned to it bool get hasNoAccount => loggedInInstances.isEmpty; Iterable get instances => _tokens.keys; Iterable get loggedInInstances => instances.where((e) => !isAnonymousFor(e)); /// Usernames that are assigned to a given instance Iterable usernamesFor(String instanceHost) => _tokens[instanceHost].keys; /// adds a new account /// if it's the first account ever the account is /// set as default for the app /// if it's the first account for an instance the account is /// set as default for that instance Future addAccount( String instanceHost, String usernameOrEmail, String password, ) async { if (!instances.contains(instanceHost)) { throw Exception('No such instance was added'); } final lemmy = LemmyApiV2(instanceHost); final token = await lemmy.run(Login( usernameOrEmail: usernameOrEmail, password: password, )); final userData = await lemmy.run(GetSite(auth: token.raw)).then((value) => value.myUser); _tokens[instanceHost][userData.name] = token; _assignDefaultAccounts(); notifyListeners(); unawaited(save()); } /// adds a new instance with no accounts associated with it. /// Additionally makes a test `GET /site` request to check if the instance exists. /// Check is skipped when [assumeValid] is `true` Future addInstance( String instanceHost, { bool assumeValid = false, }) async { if (instances.contains(instanceHost)) { throw Exception('This instance has already been added'); } if (!assumeValid) { try { await LemmyApiV2(instanceHost).run(GetSite()); // ignore: avoid_catches_without_on_clauses } catch (_) { throw Exception('This instance seems to not exist'); } } _tokens[instanceHost] = HashMap(); _assignDefaultAccounts(); notifyListeners(); unawaited(save()); } /// This also removes all accounts assigned to this instance void removeInstance(String instanceHost) { _tokens.remove(instanceHost); _assignDefaultAccounts(); notifyListeners(); save(); } void removeAccount(String instanceHost, String username) { _tokens[instanceHost].remove(username); _assignDefaultAccounts(); notifyListeners(); save(); } }