297 lines
8.3 KiB
Dart
297 lines
8.3 KiB
Dart
import 'dart:collection';
|
|
import 'dart:convert';
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:json_annotation/json_annotation.dart';
|
|
import 'package:lemmy_api_client/v3.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
|
|
part 'accounts_store.g.dart';
|
|
|
|
/// Store that manages all accounts
|
|
@JsonSerializable()
|
|
class AccountsStore extends ChangeNotifier {
|
|
static const prefsKey = 'v4:AccountsStore';
|
|
static final _prefs = SharedPreferences.getInstance();
|
|
|
|
/// 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.
|
|
/// `accounts['instanceHost']['username']`
|
|
@protected
|
|
@JsonKey(defaultValue: {'lemmy.ml': {}})
|
|
late Map<String, Map<String, UserData>> accounts;
|
|
|
|
/// default account for a given instance
|
|
/// map where keys are instanceHosts and values are usernames
|
|
@protected
|
|
@JsonKey(defaultValue: {})
|
|
late Map<String, String> defaultAccounts;
|
|
|
|
/// default account for the app
|
|
/// It is in a form of `username@instanceHost`
|
|
@protected
|
|
String? defaultAccount;
|
|
|
|
static Future<AccountsStore> load() async {
|
|
final prefs = await _prefs;
|
|
|
|
return _$AccountsStoreFromJson(
|
|
jsonDecode(prefs.getString(prefsKey) ?? '{}') as Map<String, dynamic>,
|
|
);
|
|
}
|
|
|
|
Future<void> save() async {
|
|
final prefs = await _prefs;
|
|
|
|
await prefs.setString(prefsKey, jsonEncode(_$AccountsStoreToJson(this)));
|
|
}
|
|
|
|
/// automatically sets default accounts
|
|
Future<void> _assignDefaultAccounts() async {
|
|
// 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)) {
|
|
await 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)) {
|
|
await setDefaultAccount(
|
|
instanceHost, usernamesFor(instanceHost).first);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
String? get defaultUsername => defaultAccount?.split('@')[0];
|
|
|
|
String? get defaultInstanceHost => defaultAccount?.split('@')[1];
|
|
|
|
UserData? get defaultUserData {
|
|
if (defaultAccount == null) {
|
|
return null;
|
|
}
|
|
|
|
final userTag = defaultAccount!.split('@');
|
|
return accounts[userTag[1]]?[userTag[0]];
|
|
}
|
|
|
|
String? defaultUsernameFor(String instanceHost) {
|
|
if (isAnonymousFor(instanceHost)) {
|
|
return null;
|
|
}
|
|
|
|
return defaultAccounts[instanceHost];
|
|
}
|
|
|
|
UserData? defaultUserDataFor(String instanceHost) {
|
|
if (isAnonymousFor(instanceHost)) {
|
|
return null;
|
|
}
|
|
|
|
return accounts[instanceHost]?[defaultAccounts[instanceHost]];
|
|
}
|
|
|
|
UserData? userDataFor(String instanceHost, String username) {
|
|
if (!usernamesFor(instanceHost).contains(username)) {
|
|
return null;
|
|
}
|
|
|
|
return accounts[instanceHost]?[username];
|
|
}
|
|
|
|
/// sets globally default account
|
|
Future<void> setDefaultAccount(String instanceHost, String username) {
|
|
defaultAccount = '$username@$instanceHost';
|
|
|
|
notifyListeners();
|
|
return save();
|
|
}
|
|
|
|
/// sets default account for given instance
|
|
Future<void> setDefaultAccountFor(String instanceHost, String username) {
|
|
defaultAccounts[instanceHost] = username;
|
|
|
|
notifyListeners();
|
|
return 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 accounts[instanceHost]!.isEmpty;
|
|
}
|
|
|
|
/// `true` if no added instance has an account assigned to it
|
|
bool get hasNoAccount => loggedInInstances.isEmpty;
|
|
|
|
Iterable<String> get instances => accounts.keys;
|
|
|
|
Iterable<String> get loggedInInstances =>
|
|
instances.where((e) => !isAnonymousFor(e));
|
|
|
|
/// Usernames that are assigned to a given instance
|
|
Iterable<String> usernamesFor(String instanceHost) =>
|
|
accounts[instanceHost]?.keys ?? const Iterable.empty();
|
|
|
|
/// 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 = LemmyApiV3(instanceHost);
|
|
final response = await lemmy.run(Login(
|
|
usernameOrEmail: usernameOrEmail,
|
|
password: password,
|
|
));
|
|
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
|
|
}
|
|
final userData = await lemmy
|
|
.run(GetSite(auth: jwt.raw))
|
|
.then((value) => value.myUser!.localUserView.person);
|
|
|
|
accounts[instanceHost]![userData.name] = UserData(
|
|
jwt: jwt,
|
|
userId: userData.id,
|
|
);
|
|
|
|
await _assignDefaultAccounts();
|
|
notifyListeners();
|
|
return 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 LemmyApiV3(instanceHost).run(const GetSite());
|
|
} catch (_) {
|
|
throw Exception('This instance seems to not exist');
|
|
}
|
|
}
|
|
|
|
accounts[instanceHost] = HashMap();
|
|
|
|
await _assignDefaultAccounts();
|
|
notifyListeners();
|
|
return save();
|
|
}
|
|
|
|
/// This also removes all accounts assigned to this instance
|
|
Future<void> removeInstance(String instanceHost) async {
|
|
accounts.remove(instanceHost);
|
|
|
|
await _assignDefaultAccounts();
|
|
notifyListeners();
|
|
return save();
|
|
}
|
|
|
|
Future<void> removeAccount(String instanceHost, String username) async {
|
|
if (!accounts.containsKey(instanceHost)) {
|
|
throw Exception("instance doesn't exist");
|
|
}
|
|
|
|
accounts[instanceHost]!.remove(username);
|
|
|
|
await _assignDefaultAccounts();
|
|
notifyListeners();
|
|
return save();
|
|
}
|
|
}
|
|
|
|
/// 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);
|
|
}
|
|
//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();
|
|
}
|