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

297 lines
8.3 KiB
Dart
Raw Normal View History

import 'dart:collection';
2020-09-02 10:00:08 +02:00
import 'dart:convert';
import 'package:flutter/foundation.dart';
2021-02-24 21:54:15 +01:00
import 'package:json_annotation/json_annotation.dart';
2021-04-05 20:14:39 +02:00
import 'package:lemmy_api_client/v3.dart';
2020-09-01 13:22:37 +02:00
import 'package:shared_preferences/shared_preferences.dart';
2021-02-24 21:54:15 +01:00
part 'accounts_store.g.dart';
2020-09-30 19:05:00 +02:00
/// Store that manages all accounts
2021-02-24 21:54:15 +01:00
@JsonSerializable()
class AccountsStore extends ChangeNotifier {
2021-04-11 18:27:22 +02:00
static const prefsKey = 'v4:AccountsStore';
2021-02-24 21:54:15 +01:00
static final _prefs = SharedPreferences.getInstance();
2021-04-11 18:27:22 +02:00
/// Map containing user data (jwt token, userId) of specific accounts.
/// If a token is in this map, the user is considered logged in
/// for that account.
2021-04-11 18:27:22 +02:00
/// `accounts['instanceHost']['username']`
2021-02-24 21:54:15 +01:00
@protected
@JsonKey(defaultValue: {'lemmy.ml': {}})
2021-04-11 18:27:22 +02:00
late Map<String, Map<String, UserData>> accounts;
2020-09-23 15:27:24 +02:00
/// default account for a given instance
/// map where keys are instanceHosts and values are usernames
2021-02-24 21:54:15 +01:00
@protected
@JsonKey(defaultValue: {})
late Map<String, String> defaultAccounts;
/// default account for the app
/// It is in a form of `username@instanceHost`
2021-02-24 21:54:15 +01:00
@protected
String? defaultAccount;
2021-02-24 21:54:15 +01:00
static Future<AccountsStore> load() async {
final prefs = await _prefs;
return _$AccountsStoreFromJson(
jsonDecode(prefs.getString(prefsKey) ?? '{}') as Map<String, dynamic>,
);
}
2020-09-23 15:27:24 +02:00
Future<void> save() async {
2021-02-24 21:54:15 +01:00
final prefs = await _prefs;
2021-02-24 21:54:15 +01:00
await prefs.setString(prefsKey, jsonEncode(_$AccountsStoreToJson(this)));
}
/// automatically sets default accounts
2021-04-05 20:14:39 +02:00
Future<void> _assignDefaultAccounts() async {
// remove dangling defaults
2021-02-24 21:54:15 +01:00
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()
2021-02-24 21:54:15 +01: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) ||
2021-01-17 17:35:47 +01:00
!usernamesFor(instance).contains(username)) {
2021-02-24 21:54:15 +01:00
defaultAccount = null;
2020-09-23 15:27:24 +02:00
}
}
// set local defaults
for (final instanceHost in instances) {
// if this instance is not in defaults
2021-02-24 21:54:15 +01:00
if (!defaultAccounts.containsKey(instanceHost)) {
// select first account in this instance, if any
if (!isAnonymousFor(instanceHost)) {
2021-04-05 20:14:39 +02:00
await setDefaultAccountFor(
instanceHost, usernamesFor(instanceHost).first);
2020-09-23 15:27:24 +02:00
}
}
}
2020-09-23 15:27:24 +02:00
// set global default
2021-02-24 21:54:15 +01:00
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)) {
2021-04-05 20:14:39 +02:00
await setDefaultAccount(
instanceHost, usernamesFor(instanceHost).first);
2020-09-23 15:27:24 +02:00
}
}
}
2020-09-01 13:22:37 +02:00
}
2021-04-11 00:28:33 +02:00
String? get defaultUsername => defaultAccount?.split('@')[0];
2021-04-11 00:28:33 +02:00
String? get defaultInstanceHost => defaultAccount?.split('@')[1];
2020-09-01 13:22:37 +02:00
2021-04-11 18:27:22 +02:00
UserData? get defaultUserData {
2021-04-11 00:28:33 +02:00
if (defaultAccount == null) {
return null;
}
2021-04-11 00:28:33 +02:00
final userTag = defaultAccount!.split('@');
2021-04-11 18:27:22 +02:00
return accounts[userTag[1]]?[userTag[0]];
2020-09-01 13:22:37 +02:00
}
String? defaultUsernameFor(String instanceHost) {
if (isAnonymousFor(instanceHost)) {
return null;
}
2021-02-24 21:54:15 +01:00
return defaultAccounts[instanceHost];
}
2021-04-11 18:27:22 +02:00
UserData? defaultUserDataFor(String instanceHost) {
if (isAnonymousFor(instanceHost)) {
return null;
}
2021-04-11 18:27:22 +02:00
return accounts[instanceHost]?[defaultAccounts[instanceHost]];
2021-01-17 17:35:47 +01:00
}
2021-04-11 18:27:22 +02:00
UserData? userDataFor(String instanceHost, String username) {
2021-01-17 17:35:47 +01:00
if (!usernamesFor(instanceHost).contains(username)) {
return null;
}
2021-04-11 18:27:22 +02:00
return accounts[instanceHost]?[username];
}
2020-09-01 13:22:37 +02:00
2020-09-23 15:27:24 +02:00
/// sets globally default account
2021-04-05 20:14:39 +02:00
Future<void> setDefaultAccount(String instanceHost, String username) {
2021-02-24 21:54:15 +01:00
defaultAccount = '$username@$instanceHost';
notifyListeners();
2021-04-05 20:14:39 +02:00
return save();
2020-09-01 13:22:37 +02:00
}
2020-09-23 15:27:24 +02:00
/// sets default account for given instance
2021-04-05 20:14:39 +02:00
Future<void> setDefaultAccountFor(String instanceHost, String username) {
2021-02-24 21:54:15 +01:00
defaultAccounts[instanceHost] = username;
notifyListeners();
2021-04-05 20:14:39 +02:00
return save();
2020-09-01 13:22:37 +02:00
}
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 instanceHost) {
if (!instances.contains(instanceHost)) {
return true;
}
2021-04-11 18:27:22 +02:00
return accounts[instanceHost]!.isEmpty;
}
2020-09-30 19:05:00 +02:00
/// `true` if no added instance has an account assigned to it
2020-09-26 12:43:34 +02:00
bool get hasNoAccount => loggedInInstances.isEmpty;
2021-04-11 18:27:22 +02:00
Iterable<String> get instances => accounts.keys;
2020-09-22 23:17:02 +02:00
2020-09-26 12:43:34 +02:00
Iterable<String> get loggedInInstances =>
instances.where((e) => !isAnonymousFor(e));
2021-01-17 17:35:47 +01:00
/// Usernames that are assigned to a given instance
Iterable<String> usernamesFor(String instanceHost) =>
2021-04-11 18:27:22 +02:00
accounts[instanceHost]?.keys ?? const Iterable.empty();
2021-01-17 17:35:47 +01:00
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
Future<void> addAccount(
String instanceHost,
2020-09-01 13:22:37 +02:00
String usernameOrEmail,
String password,
) async {
if (!instances.contains(instanceHost)) {
throw Exception('No such instance was added');
}
2021-04-05 20:14:39 +02:00
final lemmy = LemmyApiV3(instanceHost);
2022-01-14 14:25:31 +01:00
final response = await lemmy.run(Login(
2020-09-01 13:22:37 +02:00
usernameOrEmail: usernameOrEmail,
password: password,
2021-01-24 20:01:55 +01:00
));
2022-01-14 14:25:31 +01:00
final jwt = response.jwt;
if (jwt == null) {
if (response.verifyEmailSent) {
throw const VerifyEmailException();
} else if (response.registrationCreated) {
throw const RegistrationApplicationSentException();
}
throw Exception('Unknown error'); // should never happen
}
2021-08-26 00:27:50 +02:00
final userData = await lemmy
.run(GetSite(auth: jwt.raw))
.then((value) => value.myUser!.localUserView.person);
2020-09-01 13:22:37 +02:00
2021-08-26 00:27:50 +02:00
accounts[instanceHost]![userData.name] = UserData(
2021-04-11 18:27:22 +02:00
jwt: jwt,
2021-08-26 00:27:50 +02:00
userId: userData.id,
);
2021-04-05 20:14:39 +02:00
await _assignDefaultAccounts();
notifyListeners();
2021-04-05 20:14:39 +02:00
return save();
2020-09-01 13:22:37 +02:00
}
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`
Future<void> addInstance(
String instanceHost, {
bool assumeValid = false,
}) async {
if (instances.contains(instanceHost)) {
throw Exception('This instance has already been added');
}
if (!assumeValid) {
try {
2021-04-05 20:14:39 +02:00
await LemmyApiV3(instanceHost).run(const GetSite());
} catch (_) {
throw Exception('This instance seems to not exist');
}
2020-09-08 00:34:09 +02:00
}
2021-04-11 18:27:22 +02:00
accounts[instanceHost] = HashMap();
2021-04-05 20:14:39 +02:00
await _assignDefaultAccounts();
notifyListeners();
2021-04-05 20:14:39 +02:00
return save();
}
2020-09-30 19:05:00 +02:00
/// This also removes all accounts assigned to this instance
2021-04-05 20:14:39 +02:00
Future<void> removeInstance(String instanceHost) async {
2021-04-11 18:27:22 +02:00
accounts.remove(instanceHost);
2021-04-05 20:14:39 +02:00
await _assignDefaultAccounts();
notifyListeners();
2021-04-05 20:14:39 +02:00
return save();
}
2021-04-05 20:14:39 +02:00
Future<void> removeAccount(String instanceHost, String username) async {
2021-04-11 18:27:22 +02:00
if (!accounts.containsKey(instanceHost)) {
throw Exception("instance doesn't exist");
}
2021-04-11 18:27:22 +02:00
accounts[instanceHost]!.remove(username);
2021-04-05 20:14:39 +02:00
await _assignDefaultAccounts();
notifyListeners();
2021-04-05 20:14:39 +02:00
return save();
}
2020-09-01 13:22:37 +02:00
}
2021-04-11 18:27:22 +02:00
/// Stores data associated with a logged in user
@JsonSerializable()
class UserData {
final Jwt jwt;
final int userId;
const UserData({
required this.jwt,
required this.userId,
});
factory UserData.fromJson(Map<String, dynamic> json) =>
_$UserDataFromJson(json);
Map<String, dynamic> toJson() => _$UserDataToJson(this);
}
2022-01-14 14:25:31 +01:00
//if (data.verify_email_sent) {
// toast(i18n.t("verify_email_sent"));
// }
// if (data.registration_created) {
// toast(i18n.t("registration_application_sent"));
// }
class VerifyEmailException implements Exception {
final message = 'verify_email_sent';
const VerifyEmailException();
}
class RegistrationApplicationSentException implements Exception {
final message = 'registration_application_sent';
const RegistrationApplicationSentException();
}