277 lines
7.8 KiB
Dart
277 lines
7.8 KiB
Dart
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<String, HashMap<String, Jwt>> _tokens;
|
|
|
|
/// default account for a given instance
|
|
/// map where keys are instanceHosts and values are usernames
|
|
HashMap<String, String> _defaultAccounts;
|
|
|
|
/// default account for the app
|
|
/// It is in a form of `username@instanceHost`
|
|
String _defaultAccount;
|
|
|
|
Future<void> 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>(T f(Map<String, dynamic> json)) => HashMap.of(
|
|
(jsonDecode(prefs.getString(SharedPrefKeys.tokens) ??
|
|
'{"lemmy.ml":{}}') as Map<String, dynamic>)
|
|
?.map(
|
|
(k, e) => MapEntry(
|
|
k,
|
|
HashMap.of(
|
|
(e as Map<String, dynamic>)?.map(
|
|
(k, e) => MapEntry(
|
|
k, e == null ? null : f(e as Map<String, dynamic>)),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
// 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<dynamic, dynamic> ??
|
|
{},
|
|
));
|
|
|
|
notifyListeners();
|
|
}
|
|
|
|
Future<void> 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<String> get instances => _tokens.keys;
|
|
|
|
Iterable<String> get loggedInInstances =>
|
|
instances.where((e) => !isAnonymousFor(e));
|
|
|
|
/// Usernames that are assigned to a given instance
|
|
Iterable<String> 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<void> 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<void> 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();
|
|
}
|
|
}
|