lemmur-app-android/lib/stores/accounts_store.dart

280 lines
7.9 KiB
Dart
Raw Normal View History

2020-09-02 10:00:08 +02:00
import 'dart:convert';
2020-09-01 13:22:37 +02:00
import 'package:lemmy_api_client/lemmy_api_client.dart';
import 'package:mobx/mobx.dart';
import 'package:shared_preferences/shared_preferences.dart';
part 'accounts_store.g.dart';
2020-09-30 19:05:00 +02:00
/// Store that manages all accounts
2020-09-01 13:22:37 +02:00
class AccountsStore extends _AccountsStore with _$AccountsStore {}
abstract class _AccountsStore with Store {
ReactionDisposer _saveReactionDisposer;
2020-09-23 15:27:24 +02:00
ReactionDisposer _pickDefaultsDisposer;
2020-09-01 13:22:37 +02:00
_AccountsStore() {
2020-09-02 15:16:33 +02:00
// persistently save settings each time they are changed
2020-09-02 10:00:08 +02:00
_saveReactionDisposer = reaction(
(_) => [
tokens.forEach((k, submap) =>
MapEntry(k, submap.forEach((k2, v2) => MapEntry(k2, v2)))),
2020-09-02 10:00:08 +02:00
_defaultAccount,
_defaultAccounts.asObservable(),
],
(_) => save(),
2020-09-02 10:00:08 +02:00
);
2020-09-23 15:27:24 +02:00
2020-09-30 19:05:00 +02:00
// automatically set new default accounts when accounts are added/removed
2020-09-23 15:27:24 +02:00
_pickDefaultsDisposer = reaction(
(_) => [
tokens.forEach((k, submap) =>
MapEntry(k, submap.forEach((k2, v2) => MapEntry(k2, v2)))),
],
(_) => _assignDefaultAccounts(),
);
}
2020-09-23 15:27:24 +02:00
@action
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) ||
!tokens[instance].containsKey(username)) {
return instance;
2020-09-23 15:27:24 +02:00
}
}).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) ||
!tokens[instance].containsKey(username)) {
_defaultAccount = null;
2020-09-23 15:27:24 +02:00
}
}
// set local defaults
for (final instanceUrl in instances) {
// if this instance is not in defaults
if (!_defaultAccounts.containsKey(instanceUrl)) {
// select first account in this instance, if any
if (!isAnonymousFor(instanceUrl)) {
setDefaultAccountFor(instanceUrl, tokens[instanceUrl].keys.first);
2020-09-23 15:27:24 +02:00
}
}
}
2020-09-23 15:27:24 +02:00
// set global default
if (_defaultAccount == null) {
// select first account of first instance
for (final instanceUrl in instances) {
// select first account in this instance, if any
if (!isAnonymousFor(instanceUrl)) {
setDefaultAccount(instanceUrl, tokens[instanceUrl].keys.first);
2020-09-23 15:27:24 +02:00
}
}
}
2020-09-01 13:22:37 +02:00
}
void dispose() {
_saveReactionDisposer();
2020-09-23 15:27:24 +02:00
_pickDefaultsDisposer();
2020-09-01 13:22:37 +02:00
}
void load() async {
2020-09-16 23:22:04 +02:00
final prefs = await SharedPreferences.getInstance();
2020-09-30 20:10:13 +02:00
// I barely understand what I did. Long story short it casts a
2020-09-30 19:05:00 +02:00
// raw json into a nested ObservableMap
nestedMapsCast<T>(String key, T f(Map<String, dynamic> json)) =>
ObservableMap.of(
(jsonDecode(prefs.getString(key) ?? '{}') as Map<String, dynamic>)
?.map(
(k, e) => MapEntry(
k,
ObservableMap.of(
(e as Map<String, dynamic>)?.map(
(k, e) => MapEntry(
k, e == null ? null : f(e as Map<String, dynamic>)),
),
),
),
),
);
2020-09-01 13:22:37 +02:00
// set saved settings or create defaults
tokens = nestedMapsCast('tokens', (json) => Jwt(json['raw']));
2020-09-02 10:00:08 +02:00
_defaultAccount = prefs.getString('defaultAccount');
_defaultAccounts = ObservableMap.of(Map.castFrom(
jsonDecode(prefs.getString('defaultAccounts') ?? 'null') ?? {}));
2020-09-01 13:22:37 +02:00
}
void save() async {
2020-09-16 23:22:04 +02:00
final prefs = await SharedPreferences.getInstance();
2020-09-02 15:16:33 +02:00
2020-09-02 10:00:08 +02:00
await prefs.setString('defaultAccount', _defaultAccount);
await prefs.setString('defaultAccounts', jsonEncode(_defaultAccounts));
2020-09-02 15:16:33 +02:00
await prefs.setString('tokens', jsonEncode(tokens));
2020-09-01 13:22:37 +02:00
}
2020-09-30 19:05:00 +02:00
/// Map containing JWT tokens of specific users.
2020-09-30 19:37:56 +02:00
/// If a token is in this map, the user is considered logged in
2020-09-30 19:05:00 +02:00
/// for that account.
2020-09-01 13:22:37 +02:00
/// `tokens['instanceUrl']['username']`
@observable
ObservableMap<String, ObservableMap<String, Jwt>> tokens;
/// default account for a given instance
/// map where keys are instanceUrls and values are usernames
@observable
ObservableMap<String, String> _defaultAccounts;
/// default account for the app
2020-09-30 19:05:00 +02:00
/// It is in a form of `username@instanceUrl`
2020-09-01 13:22:37 +02:00
@observable
String _defaultAccount;
@computed
String get defaultUsername {
if (_defaultAccount == null) {
return null;
}
return _defaultAccount.split('@')[0];
2020-09-01 13:22:37 +02:00
}
@computed
String get defaultInstanceUrl {
if (_defaultAccount == null) {
return null;
}
return _defaultAccount.split('@')[1];
2020-09-01 13:22:37 +02:00
}
String defaultUsernameFor(String instanceUrl) => Computed(() {
if (isAnonymousFor(instanceUrl)) {
return null;
}
return _defaultAccounts[instanceUrl];
}).value;
@computed
Jwt get defaultToken {
if (_defaultAccount == null) {
return null;
}
final userTag = _defaultAccount.split('@');
return tokens[userTag[1]][userTag[0]];
}
Jwt defaultTokenFor(String instanceUrl) => Computed(() {
if (isAnonymousFor(instanceUrl)) {
return null;
}
return tokens[instanceUrl][_defaultAccounts[instanceUrl]];
}).value;
2020-09-01 13:22:37 +02:00
2020-09-23 15:27:24 +02:00
/// sets globally default account
2020-09-01 13:22:37 +02:00
@action
void setDefaultAccount(String instanceUrl, String username) {
_defaultAccount = '$username@$instanceUrl';
}
2020-09-23 15:27:24 +02:00
/// sets default account for given instance
2020-09-01 13:22:37 +02:00
@action
void setDefaultAccountFor(String instanceUrl, String username) {
_defaultAccounts[instanceUrl] = username;
}
2020-09-30 19:05:00 +02:00
/// An instance is considered anonymous if it was not
/// added or there are no accounts assigned to it.
bool isAnonymousFor(String instanceUrl) => Computed(() {
2020-09-26 12:43:34 +02:00
if (!instances.contains(instanceUrl)) {
return true;
}
return tokens[instanceUrl].isEmpty;
}).value;
2020-09-30 19:05:00 +02:00
/// `true` if no added instance has an account assigned to it
@computed
2020-09-26 12:43:34 +02:00
bool get hasNoAccount => loggedInInstances.isEmpty;
2020-09-22 23:17:02 +02:00
@computed
Iterable<String> get instances => tokens.keys;
2020-09-22 23:17:02 +02:00
2020-09-26 12:43:34 +02:00
@computed
Iterable<String> get loggedInInstances =>
instances.where((e) => !isAnonymousFor(e));
2020-09-01 13:22:37 +02:00
/// 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
@action
Future<void> addAccount(
String instanceUrl,
String usernameOrEmail,
String password,
) async {
if (!instances.contains(instanceUrl)) {
throw Exception('No such instance was added');
}
2020-09-16 23:22:04 +02:00
final lemmy = LemmyApi(instanceUrl).v1;
2020-09-01 13:22:37 +02:00
2020-09-16 23:22:04 +02:00
final token = await lemmy.login(
2020-09-01 13:22:37 +02:00
usernameOrEmail: usernameOrEmail,
password: password,
);
2020-09-16 23:22:04 +02:00
final userData =
2020-09-01 13:22:37 +02:00
await lemmy.getSite(auth: token.raw).then((value) => value.myUser);
tokens[instanceUrl][userData.name] = token;
}
2020-09-08 00:34:09 +02:00
/// adds a new instance with no accounts associated with it.
2020-09-30 19:05:00 +02:00
/// Additionally makes a test `GET /site` request to check if the instance exists.
/// Check is skipped when [assumeValid] is `true`
@action
Future<void> addInstance(
String instanceUrl, {
bool assumeValid = false,
}) async {
if (instances.contains(instanceUrl)) {
throw Exception('This instance has already been added');
}
if (!assumeValid) {
try {
await LemmyApi(instanceUrl).v1.getSite();
// ignore: avoid_catches_without_on_clauses
} catch (_) {
throw Exception('This instance seems to not exist');
}
2020-09-08 00:34:09 +02:00
}
tokens[instanceUrl] = ObservableMap();
}
2020-09-30 19:05:00 +02:00
/// This also removes all accounts assigned to this instance
@action
void removeInstance(String instanceUrl) {
tokens.remove(instanceUrl);
}
@action
void removeAccount(String instanceUrl, String username) {
tokens[instanceUrl].remove(username);
}
2020-09-01 13:22:37 +02:00
}