Merge pull request #62 from krawieck/no-more-users-in-accounts-store
This commit is contained in:
commit
1eb3c9e937
|
@ -1,6 +1,8 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
|
||||
/// creates an [AsyncSnapshot] from the Future returned from the valueBuilder.
|
||||
/// [keys] can be used to rebuild the Future
|
||||
AsyncSnapshot<T> useMemoFuture<T>(Future<T> Function() valueBuilder,
|
||||
[List<Object> keys = const <dynamic>[]]) =>
|
||||
useFuture(useMemoized<Future<T>>(valueBuilder, keys), preserveState: false);
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
|
||||
/// Observes MobX observables in [fn] and returns the built value.
|
||||
/// When observable inside have changed, the hook rebuilds the value.
|
||||
/// The returned value can be ignored for a `useEffect(() { autorun(fn); }, [])`
|
||||
/// effect.
|
||||
T useObserved<T>(T Function() fn) {
|
||||
final returnValue = useState(useMemoized(fn));
|
||||
|
||||
useEffect(() {
|
||||
final disposer = autorun((_) {
|
||||
returnValue.value = fn();
|
||||
});
|
||||
|
||||
return disposer;
|
||||
}, []);
|
||||
|
||||
return returnValue.value;
|
||||
}
|
|
@ -11,6 +11,7 @@ import '../widgets/bottom_modal.dart';
|
|||
import '../widgets/fullscreenable_image.dart';
|
||||
import 'add_instance.dart';
|
||||
|
||||
/// A modal where an account can be added for a given instance
|
||||
class AddAccountPage extends HookWidget {
|
||||
final String instanceUrl;
|
||||
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey();
|
||||
|
@ -27,7 +28,7 @@ class AddAccountPage extends HookWidget {
|
|||
useValueListenable(passwordController);
|
||||
final accountsStore = useAccountsStore();
|
||||
|
||||
final loading = useDelayedLoading(Duration(milliseconds: 500));
|
||||
final loading = useDelayedLoading();
|
||||
final selectedInstance = useState(instanceUrl);
|
||||
final icon = useState<String>(null);
|
||||
useEffect(() {
|
||||
|
@ -38,6 +39,7 @@ class AddAccountPage extends HookWidget {
|
|||
return null;
|
||||
}, [selectedInstance.value]);
|
||||
|
||||
/// show a modal with a list of instance checkboxes
|
||||
selectInstance() async {
|
||||
final val = await showModalBottomSheet<String>(
|
||||
backgroundColor: Colors.transparent,
|
||||
|
@ -46,7 +48,7 @@ class AddAccountPage extends HookWidget {
|
|||
builder: (context) => BottomModal(
|
||||
title: 'select instance',
|
||||
child: Column(children: [
|
||||
for (final i in accountsStore.users.keys)
|
||||
for (final i in accountsStore.instances)
|
||||
RadioListTile<String>(
|
||||
value: i,
|
||||
groupValue: selectedInstance.value,
|
||||
|
|
|
@ -146,6 +146,7 @@ class AddInstancePage extends HookWidget {
|
|||
}
|
||||
}
|
||||
|
||||
/// removes protocol and trailing slash
|
||||
String _fixInstanceUrl(String inst) {
|
||||
if (inst.startsWith('https://')) {
|
||||
inst = inst.substring(8);
|
||||
|
@ -155,7 +156,9 @@ String _fixInstanceUrl(String inst) {
|
|||
inst = inst.substring(7);
|
||||
}
|
||||
|
||||
if (inst.endsWith('/')) inst = inst.substring(0, inst.length - 1);
|
||||
if (inst.endsWith('/')) {
|
||||
inst = inst.substring(0, inst.length - 1);
|
||||
}
|
||||
|
||||
return inst;
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import '../util/goto.dart';
|
|||
import '../widgets/markdown_text.dart';
|
||||
import '../widgets/sortable_infinite_list.dart';
|
||||
|
||||
/// Infinite list of Communities fetched by the given fetcher
|
||||
class CommunitiesListPage extends StatelessWidget {
|
||||
final String title;
|
||||
final Future<List<CommunityView>> Function(
|
||||
|
|
|
@ -10,8 +10,10 @@ import '../hooks/delayed_loading.dart';
|
|||
import '../hooks/memo_future.dart';
|
||||
import '../hooks/stores.dart';
|
||||
import '../util/extensions/iterators.dart';
|
||||
import '../util/goto.dart';
|
||||
import '../util/text_color.dart';
|
||||
|
||||
/// List of subscribed communities per instance
|
||||
class CommunitiesTab extends HookWidget {
|
||||
CommunitiesTab();
|
||||
|
||||
|
@ -22,14 +24,13 @@ class CommunitiesTab extends HookWidget {
|
|||
useValueListenable(filterController);
|
||||
final accountsStore = useAccountsStore();
|
||||
|
||||
final amountOfDisplayInstances = useMemoized(() => accountsStore.users.keys
|
||||
.where((e) => !accountsStore.isAnonymousFor(e))
|
||||
.length);
|
||||
final amountOfDisplayInstances =
|
||||
useMemoized(() => accountsStore.loggedInInstances.length);
|
||||
final isCollapsed = useState(List.filled(amountOfDisplayInstances, false));
|
||||
|
||||
// TODO: rebuild when instances/accounts change
|
||||
final instancesSnap = useMemoFuture(() {
|
||||
final futures = accountsStore.users.keys
|
||||
.where((e) => !accountsStore.isAnonymousFor(e))
|
||||
final futures = accountsStore.loggedInInstances
|
||||
.map(
|
||||
(instanceUrl) =>
|
||||
LemmyApi(instanceUrl).v1.getSite().then((e) => e.site),
|
||||
|
@ -39,8 +40,7 @@ class CommunitiesTab extends HookWidget {
|
|||
return Future.wait(futures);
|
||||
});
|
||||
final communitiesSnap = useMemoFuture(() {
|
||||
final futures = accountsStore.users.keys
|
||||
.where((e) => !accountsStore.isAnonymousFor(e))
|
||||
final futures = accountsStore.loggedInInstances
|
||||
.map(
|
||||
(instanceUrl) => LemmyApi(instanceUrl)
|
||||
.v1
|
||||
|
@ -115,7 +115,6 @@ class CommunitiesTab extends HookWidget {
|
|||
toggleCollapse(int i) => isCollapsed.value =
|
||||
isCollapsed.value.mapWithIndex((e, j) => j == i ? !e : e).toList();
|
||||
|
||||
// TODO: add observer
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
actions: [
|
||||
|
@ -141,7 +140,8 @@ class CommunitiesTab extends HookWidget {
|
|||
Column(
|
||||
children: [
|
||||
ListTile(
|
||||
onTap: () {}, // TODO: open instance
|
||||
onTap: () => goToInstance(
|
||||
context, accountsStore.loggedInInstances.elementAt(i)),
|
||||
onLongPress: () => toggleCollapse(i),
|
||||
leading: instances[i].icon != null
|
||||
? CachedNetworkImage(
|
||||
|
@ -174,7 +174,10 @@ class CommunitiesTab extends HookWidget {
|
|||
Padding(
|
||||
padding: const EdgeInsets.only(left: 17),
|
||||
child: ListTile(
|
||||
onTap: () {}, // TODO: open community
|
||||
onTap: () => goToCommunity.byId(
|
||||
context,
|
||||
accountsStore.loggedInInstances.elementAt(i),
|
||||
comm.communityId),
|
||||
dense: true,
|
||||
leading: VerticalDivider(
|
||||
color: theme.hintColor,
|
||||
|
|
|
@ -21,6 +21,7 @@ import '../widgets/fullscreenable_image.dart';
|
|||
import '../widgets/markdown_text.dart';
|
||||
import '../widgets/sortable_infinite_list.dart';
|
||||
|
||||
/// Displays posts, comments, and general info about the given community
|
||||
class CommunityPage extends HookWidget {
|
||||
final CommunityView _community;
|
||||
final String instanceUrl;
|
||||
|
|
|
@ -13,6 +13,7 @@ import '../util/spaced.dart';
|
|||
import '../widgets/markdown_text.dart';
|
||||
import 'full_post.dart';
|
||||
|
||||
/// Fab that triggers the [CreatePost] modal
|
||||
class CreatePostFab extends HookWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
@ -26,6 +27,7 @@ class CreatePostFab extends HookWidget {
|
|||
}
|
||||
}
|
||||
|
||||
/// Modal for creating a post to some community in some instance
|
||||
class CreatePost extends HookWidget {
|
||||
final CommunityView community;
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ import '../widgets/post.dart';
|
|||
import '../widgets/save_post_button.dart';
|
||||
import '../widgets/write_comment.dart';
|
||||
|
||||
/// Displays a post with its comment section
|
||||
class FullPostPage extends HookWidget {
|
||||
final int id;
|
||||
final String instanceUrl;
|
||||
|
|
|
@ -19,6 +19,7 @@ import '../widgets/sortable_infinite_list.dart';
|
|||
import 'communities_list.dart';
|
||||
import 'users_list.dart';
|
||||
|
||||
/// Displays posts, comments, and general info about the given instance
|
||||
class InstancePage extends HookWidget {
|
||||
final String instanceUrl;
|
||||
final Future<FullSiteView> siteFuture;
|
||||
|
|
|
@ -7,6 +7,7 @@ import 'package:photo_view/photo_view.dart';
|
|||
|
||||
import '../widgets/bottom_modal.dart';
|
||||
|
||||
/// View to interact with a media object. Zoom in/out, download, share, etc.
|
||||
class MediaViewPage extends HookWidget {
|
||||
final String url;
|
||||
|
||||
|
|
|
@ -4,12 +4,13 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
|||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
|
||||
import '../hooks/stores.dart';
|
||||
import '../util/extensions/api.dart';
|
||||
import '../util/goto.dart';
|
||||
import '../widgets/bottom_modal.dart';
|
||||
import '../widgets/user_profile.dart';
|
||||
import 'settings.dart';
|
||||
|
||||
/// Profile page for a logged in user. The difference between this and
|
||||
/// UserPage is that here you have access to settings
|
||||
class UserProfileTab extends HookWidget {
|
||||
UserProfileTab();
|
||||
|
||||
|
@ -40,8 +41,6 @@ class UserProfileTab extends HookWidget {
|
|||
);
|
||||
}
|
||||
|
||||
final user = accountsStore.defaultUser;
|
||||
|
||||
return Scaffold(
|
||||
extendBodyBehindAppBar: true,
|
||||
appBar: AppBar(
|
||||
|
@ -54,7 +53,8 @@ class UserProfileTab extends HookWidget {
|
|||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'@${user.name}', // TODO: fix overflow issues
|
||||
// TODO: fix overflow issues
|
||||
'@${accountsStore.defaultUsername}',
|
||||
style: theme.primaryTextTheme.headline6,
|
||||
overflow: TextOverflow.fade,
|
||||
),
|
||||
|
@ -71,37 +71,33 @@ class UserProfileTab extends HookWidget {
|
|||
builder: (_) {
|
||||
final userTags = <String>[];
|
||||
|
||||
accountsStore.users.forEach((instanceUrl, value) {
|
||||
accountsStore.tokens.forEach((instanceUrl, value) {
|
||||
value.forEach((username, _) {
|
||||
userTags.add('$username@$instanceUrl');
|
||||
});
|
||||
});
|
||||
|
||||
return Observer(
|
||||
builder: (ctx) {
|
||||
final user = accountsStore.defaultUser;
|
||||
final instanceUrl = user.instanceUrl;
|
||||
|
||||
return BottomModal(
|
||||
title: 'account',
|
||||
child: Column(
|
||||
children: [
|
||||
for (final tag in userTags)
|
||||
RadioListTile<String>(
|
||||
value: tag,
|
||||
title: Text(tag),
|
||||
groupValue: '${user.name}@$instanceUrl',
|
||||
onChanged: (selected) {
|
||||
final userTag = selected.split('@');
|
||||
accountsStore.setDefaultAccount(
|
||||
userTag[1], userTag[0]);
|
||||
Navigator.of(ctx).pop();
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
builder: (ctx) => BottomModal(
|
||||
title: 'account',
|
||||
child: Column(
|
||||
children: [
|
||||
for (final tag in userTags)
|
||||
RadioListTile<String>(
|
||||
value: tag,
|
||||
title: Text(tag),
|
||||
groupValue: '${accountsStore.defaultUsername}'
|
||||
'@${accountsStore.defaultInstanceUrl}',
|
||||
onChanged: (selected) {
|
||||
final userTag = selected.split('@');
|
||||
accountsStore.setDefaultAccount(
|
||||
userTag[1], userTag[0]);
|
||||
Navigator.of(ctx).pop();
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
@ -117,8 +113,8 @@ class UserProfileTab extends HookWidget {
|
|||
],
|
||||
),
|
||||
body: UserProfile(
|
||||
userId: user.id,
|
||||
instanceUrl: user.instanceUrl,
|
||||
userId: accountsStore.defaultToken.payload.id,
|
||||
instanceUrl: accountsStore.defaultInstanceUrl,
|
||||
),
|
||||
);
|
||||
},
|
||||
|
|
|
@ -10,6 +10,7 @@ import '../util/goto.dart';
|
|||
import 'add_account.dart';
|
||||
import 'add_instance.dart';
|
||||
|
||||
/// Page with a list of different settings sections
|
||||
class SettingsPage extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
@ -48,6 +49,7 @@ class SettingsPage extends StatelessWidget {
|
|||
}
|
||||
}
|
||||
|
||||
/// Settings for theme color, AMOLED switch
|
||||
class AppearanceConfigPage extends HookWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
@ -89,6 +91,7 @@ class AppearanceConfigPage extends HookWidget {
|
|||
}
|
||||
}
|
||||
|
||||
/// Settings for managing accounts
|
||||
class AccountsConfigPage extends HookWidget {
|
||||
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey();
|
||||
|
||||
|
@ -169,7 +172,7 @@ class AccountsConfigPage extends HookWidget {
|
|||
onTap: () => showCupertinoModalPopup(
|
||||
context: context,
|
||||
builder: (_) =>
|
||||
AddAccountPage(instanceUrl: accountsStore.users.keys.last)),
|
||||
AddAccountPage(instanceUrl: accountsStore.instances.last)),
|
||||
),
|
||||
SpeedDialChild(
|
||||
child: Icon(Icons.dns),
|
||||
|
@ -186,7 +189,7 @@ class AccountsConfigPage extends HookWidget {
|
|||
|
||||
return ListView(
|
||||
children: [
|
||||
if (accountsStore.users.isEmpty)
|
||||
if (accountsStore.tokens.isEmpty)
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
|
@ -205,7 +208,7 @@ class AccountsConfigPage extends HookWidget {
|
|||
),
|
||||
],
|
||||
),
|
||||
for (final entry in accountsStore.users.entries) ...[
|
||||
for (final entry in accountsStore.tokens.entries) ...[
|
||||
SizedBox(height: 40),
|
||||
Slidable(
|
||||
actionPane: SlidableBehindActionPane(),
|
||||
|
@ -243,7 +246,7 @@ class AccountsConfigPage extends HookWidget {
|
|||
decoration: BoxDecoration(color: theme.canvasColor),
|
||||
child: ListTile(
|
||||
trailing: username ==
|
||||
accountsStore.defaultUserFor(entry.key).name
|
||||
accountsStore.defaultUsernameFor(entry.key)
|
||||
? Icon(
|
||||
Icons.check_circle_outline,
|
||||
color: theme.accentColor,
|
||||
|
@ -254,7 +257,6 @@ class AccountsConfigPage extends HookWidget {
|
|||
accountsStore.setDefaultAccountFor(
|
||||
entry.key, username);
|
||||
},
|
||||
|
||||
onTap: () {}, // TODO: go to managing account
|
||||
),
|
||||
),
|
||||
|
|
|
@ -5,6 +5,7 @@ import 'package:lemmy_api_client/lemmy_api_client.dart';
|
|||
|
||||
import '../widgets/user_profile.dart';
|
||||
|
||||
/// Page showing posts, comments, and general info about a user.
|
||||
class UserPage extends HookWidget {
|
||||
final int userId;
|
||||
final String instanceUrl;
|
||||
|
|
|
@ -5,6 +5,7 @@ import 'package:lemmy_api_client/lemmy_api_client.dart';
|
|||
import '../util/extensions/api.dart';
|
||||
import '../widgets/markdown_text.dart';
|
||||
|
||||
/// Infinite list of Users fetched by the given fetcher
|
||||
class UsersListPage extends StatelessWidget {
|
||||
final String title;
|
||||
final List<UserView> users;
|
||||
|
@ -13,6 +14,7 @@ class UsersListPage extends StatelessWidget {
|
|||
: assert(users != null),
|
||||
super(key: key);
|
||||
|
||||
// TODO: go to user
|
||||
void goToUser(BuildContext context, int id) {
|
||||
print('GO TO USER $id');
|
||||
}
|
||||
|
@ -20,6 +22,8 @@ class UsersListPage extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
// TODO: change to infinite scroll
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(title ?? '', style: theme.textTheme.headline6),
|
||||
|
|
|
@ -6,6 +6,7 @@ import 'package:shared_preferences/shared_preferences.dart';
|
|||
|
||||
part 'accounts_store.g.dart';
|
||||
|
||||
/// Store that manages all accounts
|
||||
class AccountsStore extends _AccountsStore with _$AccountsStore {}
|
||||
|
||||
abstract class _AccountsStore with Store {
|
||||
|
@ -16,8 +17,6 @@ abstract class _AccountsStore with Store {
|
|||
// persistently save settings each time they are changed
|
||||
_saveReactionDisposer = reaction(
|
||||
(_) => [
|
||||
users.forEach((k, submap) =>
|
||||
MapEntry(k, submap.forEach((k2, v2) => MapEntry(k2, v2)))),
|
||||
tokens.forEach((k, submap) =>
|
||||
MapEntry(k, submap.forEach((k2, v2) => MapEntry(k2, v2)))),
|
||||
_defaultAccount,
|
||||
|
@ -26,11 +25,9 @@ abstract class _AccountsStore with Store {
|
|||
(_) => save(),
|
||||
);
|
||||
|
||||
// check if there's a default profile and if not, select one
|
||||
// automatically set new default accounts when accounts are added/removed
|
||||
_pickDefaultsDisposer = reaction(
|
||||
(_) => [
|
||||
users.forEach((k, submap) =>
|
||||
MapEntry(k, submap.forEach((k2, v2) => MapEntry(k2, v2)))),
|
||||
tokens.forEach((k, submap) =>
|
||||
MapEntry(k, submap.forEach((k2, v2) => MapEntry(k2, v2)))),
|
||||
],
|
||||
|
@ -45,8 +42,8 @@ abstract class _AccountsStore with Store {
|
|||
final instance = dft.key;
|
||||
final username = dft.value;
|
||||
// if instance or username doesn't exist, remove
|
||||
if (!users.containsKey(instance) ||
|
||||
!users[instance].containsKey(username)) {
|
||||
if (!instances.contains(instance) ||
|
||||
!tokens[instance].containsKey(username)) {
|
||||
return instance;
|
||||
}
|
||||
}).forEach(_defaultAccounts.remove);
|
||||
|
@ -54,19 +51,19 @@ abstract class _AccountsStore with Store {
|
|||
final instance = _defaultAccount.split('@')[1];
|
||||
final username = _defaultAccount.split('@')[0];
|
||||
// if instance or username doesn't exist, remove
|
||||
if (!users.containsKey(instance) ||
|
||||
!users[instance].containsKey(username)) {
|
||||
if (!instances.contains(instance) ||
|
||||
!tokens[instance].containsKey(username)) {
|
||||
_defaultAccount = null;
|
||||
}
|
||||
}
|
||||
|
||||
// set local defaults
|
||||
for (final instanceUrl in users.keys) {
|
||||
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, users[instanceUrl].keys.first);
|
||||
setDefaultAccountFor(instanceUrl, tokens[instanceUrl].keys.first);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -74,10 +71,10 @@ abstract class _AccountsStore with Store {
|
|||
// set global default
|
||||
if (_defaultAccount == null) {
|
||||
// select first account of first instance
|
||||
for (final instanceUrl in users.keys) {
|
||||
for (final instanceUrl in instances) {
|
||||
// select first account in this instance, if any
|
||||
if (!isAnonymousFor(instanceUrl)) {
|
||||
setDefaultAccount(instanceUrl, users[instanceUrl].keys.first);
|
||||
setDefaultAccount(instanceUrl, tokens[instanceUrl].keys.first);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -91,6 +88,8 @@ abstract class _AccountsStore with Store {
|
|||
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>(String key, T f(Map<String, dynamic> json)) =>
|
||||
ObservableMap.of(
|
||||
(jsonDecode(prefs.getString(key) ?? '{}') as Map<String, dynamic>)
|
||||
|
@ -108,7 +107,6 @@ abstract class _AccountsStore with Store {
|
|||
);
|
||||
|
||||
// set saved settings or create defaults
|
||||
users = nestedMapsCast('users', (json) => User.fromJson(json));
|
||||
tokens = nestedMapsCast('tokens', (json) => Jwt(json['raw']));
|
||||
_defaultAccount = prefs.getString('defaultAccount');
|
||||
_defaultAccounts = ObservableMap.of(Map.castFrom(
|
||||
|
@ -120,16 +118,12 @@ abstract class _AccountsStore with Store {
|
|||
|
||||
await prefs.setString('defaultAccount', _defaultAccount);
|
||||
await prefs.setString('defaultAccounts', jsonEncode(_defaultAccounts));
|
||||
await prefs.setString('users', jsonEncode(users));
|
||||
await prefs.setString('tokens', jsonEncode(tokens));
|
||||
}
|
||||
|
||||
/// if path to tokens map exists, it exists for users as well
|
||||
/// `users['instanceUrl']['username']`
|
||||
@observable
|
||||
ObservableMap<String, ObservableMap<String, User>> users;
|
||||
|
||||
/// if path to users map exists, it exists for tokens as well
|
||||
/// Map containing JWT tokens of specific users.
|
||||
/// If a token is in this map, the user is considered logged in
|
||||
/// for that account.
|
||||
/// `tokens['instanceUrl']['username']`
|
||||
@observable
|
||||
ObservableMap<String, ObservableMap<String, Jwt>> tokens;
|
||||
|
@ -140,20 +134,36 @@ abstract class _AccountsStore with Store {
|
|||
ObservableMap<String, String> _defaultAccounts;
|
||||
|
||||
/// default account for the app
|
||||
/// username@instanceUrl
|
||||
/// It is in a form of `username@instanceUrl`
|
||||
@observable
|
||||
String _defaultAccount;
|
||||
|
||||
@computed
|
||||
User get defaultUser {
|
||||
String get defaultUsername {
|
||||
if (_defaultAccount == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final userTag = _defaultAccount.split('@');
|
||||
return users[userTag[1]][userTag[0]];
|
||||
return _defaultAccount.split('@')[0];
|
||||
}
|
||||
|
||||
@computed
|
||||
String get defaultInstanceUrl {
|
||||
if (_defaultAccount == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return _defaultAccount.split('@')[1];
|
||||
}
|
||||
|
||||
String defaultUsernameFor(String instanceUrl) => Computed(() {
|
||||
if (isAnonymousFor(instanceUrl)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return _defaultAccounts[instanceUrl];
|
||||
}).value;
|
||||
|
||||
@computed
|
||||
Jwt get defaultToken {
|
||||
if (_defaultAccount == null) {
|
||||
|
@ -164,14 +174,6 @@ abstract class _AccountsStore with Store {
|
|||
return tokens[userTag[1]][userTag[0]];
|
||||
}
|
||||
|
||||
User defaultUserFor(String instanceUrl) => Computed(() {
|
||||
if (isAnonymousFor(instanceUrl)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return users[instanceUrl][_defaultAccounts[instanceUrl]];
|
||||
}).value;
|
||||
|
||||
Jwt defaultTokenFor(String instanceUrl) => Computed(() {
|
||||
if (isAnonymousFor(instanceUrl)) {
|
||||
return null;
|
||||
|
@ -192,19 +194,22 @@ abstract class _AccountsStore with Store {
|
|||
_defaultAccounts[instanceUrl] = username;
|
||||
}
|
||||
|
||||
/// An instance is considered anonymous if it was not
|
||||
/// added or there are no accounts assigned to it.
|
||||
bool isAnonymousFor(String instanceUrl) => Computed(() {
|
||||
if (!instances.contains(instanceUrl)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return users[instanceUrl].isEmpty;
|
||||
return tokens[instanceUrl].isEmpty;
|
||||
}).value;
|
||||
|
||||
/// `true` if no added instance has an account assigned to it
|
||||
@computed
|
||||
bool get hasNoAccount => loggedInInstances.isEmpty;
|
||||
|
||||
@computed
|
||||
Iterable<String> get instances => users.keys;
|
||||
Iterable<String> get instances => tokens.keys;
|
||||
|
||||
@computed
|
||||
Iterable<String> get loggedInInstances =>
|
||||
|
@ -221,7 +226,7 @@ abstract class _AccountsStore with Store {
|
|||
String usernameOrEmail,
|
||||
String password,
|
||||
) async {
|
||||
if (!users.containsKey(instanceUrl)) {
|
||||
if (!instances.contains(instanceUrl)) {
|
||||
throw Exception('No such instance was added');
|
||||
}
|
||||
|
||||
|
@ -234,18 +239,18 @@ abstract class _AccountsStore with Store {
|
|||
final userData =
|
||||
await lemmy.getSite(auth: token.raw).then((value) => value.myUser);
|
||||
|
||||
users[instanceUrl][userData.name] = userData;
|
||||
tokens[instanceUrl][userData.name] = token;
|
||||
}
|
||||
|
||||
/// adds a new instance with no accounts associated with it.
|
||||
/// Additionally makes a test GET /site request to check if the instance exists
|
||||
/// 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 (users.containsKey(instanceUrl)) {
|
||||
if (instances.contains(instanceUrl)) {
|
||||
throw Exception('This instance has already been added');
|
||||
}
|
||||
|
||||
|
@ -258,19 +263,17 @@ abstract class _AccountsStore with Store {
|
|||
}
|
||||
}
|
||||
|
||||
users[instanceUrl] = ObservableMap();
|
||||
tokens[instanceUrl] = ObservableMap();
|
||||
}
|
||||
|
||||
/// This also removes all accounts assigned to this instance
|
||||
@action
|
||||
void removeInstance(String instanceUrl) {
|
||||
users.remove(instanceUrl);
|
||||
tokens.remove(instanceUrl);
|
||||
}
|
||||
|
||||
@action
|
||||
void removeAccount(String instanceUrl, String username) {
|
||||
users[instanceUrl].remove(username);
|
||||
tokens[instanceUrl].remove(username);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,13 +9,20 @@ part of 'accounts_store.dart';
|
|||
// ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic
|
||||
|
||||
mixin _$AccountsStore on _AccountsStore, Store {
|
||||
Computed<User> _$defaultUserComputed;
|
||||
Computed<String> _$defaultUsernameComputed;
|
||||
|
||||
@override
|
||||
User get defaultUser =>
|
||||
(_$defaultUserComputed ??= Computed<User>(() => super.defaultUser,
|
||||
name: '_AccountsStore.defaultUser'))
|
||||
.value;
|
||||
String get defaultUsername => (_$defaultUsernameComputed ??= Computed<String>(
|
||||
() => super.defaultUsername,
|
||||
name: '_AccountsStore.defaultUsername'))
|
||||
.value;
|
||||
Computed<String> _$defaultInstanceUrlComputed;
|
||||
|
||||
@override
|
||||
String get defaultInstanceUrl => (_$defaultInstanceUrlComputed ??=
|
||||
Computed<String>(() => super.defaultInstanceUrl,
|
||||
name: '_AccountsStore.defaultInstanceUrl'))
|
||||
.value;
|
||||
Computed<Jwt> _$defaultTokenComputed;
|
||||
|
||||
@override
|
||||
|
@ -45,21 +52,6 @@ mixin _$AccountsStore on _AccountsStore, Store {
|
|||
name: '_AccountsStore.loggedInInstances'))
|
||||
.value;
|
||||
|
||||
final _$usersAtom = Atom(name: '_AccountsStore.users');
|
||||
|
||||
@override
|
||||
ObservableMap<String, ObservableMap<String, User>> get users {
|
||||
_$usersAtom.reportRead();
|
||||
return super.users;
|
||||
}
|
||||
|
||||
@override
|
||||
set users(ObservableMap<String, ObservableMap<String, User>> value) {
|
||||
_$usersAtom.reportWrite(value, super.users, () {
|
||||
super.users = value;
|
||||
});
|
||||
}
|
||||
|
||||
final _$tokensAtom = Atom(name: '_AccountsStore.tokens');
|
||||
|
||||
@override
|
||||
|
@ -183,9 +175,9 @@ mixin _$AccountsStore on _AccountsStore, Store {
|
|||
@override
|
||||
String toString() {
|
||||
return '''
|
||||
users: ${users},
|
||||
tokens: ${tokens},
|
||||
defaultUser: ${defaultUser},
|
||||
defaultUsername: ${defaultUsername},
|
||||
defaultInstanceUrl: ${defaultInstanceUrl},
|
||||
defaultToken: ${defaultToken},
|
||||
hasNoAccount: ${hasNoAccount},
|
||||
instances: ${instances},
|
||||
|
|
|
@ -5,6 +5,7 @@ import 'package:shared_preferences/shared_preferences.dart';
|
|||
|
||||
part 'config_store.g.dart';
|
||||
|
||||
/// Store managing user-level configuration such as theme or language
|
||||
class ConfigStore extends _ConfigStore with _$ConfigStore {}
|
||||
|
||||
abstract class _ConfigStore with Store {
|
||||
|
@ -42,5 +43,6 @@ abstract class _ConfigStore with Store {
|
|||
bool amoledDarkMode;
|
||||
}
|
||||
|
||||
/// converts string to ThemeMode
|
||||
ThemeMode _themeModeFromString(String theme) =>
|
||||
ThemeMode.values.firstWhere((e) => describeEnum(e) == theme);
|
||||
|
|
|
@ -10,6 +10,8 @@ import 'pages/user.dart';
|
|||
import 'stores/accounts_store.dart';
|
||||
import 'util/goto.dart';
|
||||
|
||||
/// Decides where does a link link to. Either somewhere in-app:
|
||||
/// opens the correct page, or outside of the app: opens in a browser
|
||||
Future<void> linkLauncher({
|
||||
@required BuildContext context,
|
||||
@required String url,
|
||||
|
@ -19,7 +21,7 @@ Future<void> linkLauncher({
|
|||
goTo(context, (c) => builder());
|
||||
}
|
||||
|
||||
final instances = context.read<AccountsStore>().users.keys.toList();
|
||||
final instances = context.read<AccountsStore>().instances;
|
||||
|
||||
final chonks = url.split('/');
|
||||
if (chonks.length == 1) return openInBrowser(url);
|
||||
|
|
|
@ -1,7 +1,12 @@
|
|||
import 'package:lemmy_api_client/lemmy_api_client.dart';
|
||||
|
||||
// Extensions to lemmy api objects which give a [.instanceUrl] getter
|
||||
// allowing for a convenient way of knowing from which instance did this
|
||||
// object come from
|
||||
|
||||
// TODO: change it to something more robust? regex?
|
||||
|
||||
extension GetInstanceCommunityView on CommunityView {
|
||||
// TODO: change it to something more robust? regex?
|
||||
String get instanceUrl => actorId.split('/')[2];
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
extension ExtraIterators<E> on Iterable<E> {
|
||||
/// A `.map` but with an index as the second argument
|
||||
Iterable<T> mapWithIndex<T>(T f(E e, int i)) {
|
||||
var i = 0;
|
||||
return map((e) => f(e, i++));
|
||||
|
|
|
@ -6,6 +6,7 @@ import '../pages/full_post.dart';
|
|||
import '../pages/instance.dart';
|
||||
import '../pages/user.dart';
|
||||
|
||||
/// Pushes onto the navigator stack the given widget
|
||||
Future<dynamic> goTo(
|
||||
BuildContext context,
|
||||
Widget Function(BuildContext context) builder,
|
||||
|
@ -14,6 +15,7 @@ Future<dynamic> goTo(
|
|||
builder: builder,
|
||||
));
|
||||
|
||||
/// Replaces the top of the navigator stack with the given widget
|
||||
Future<dynamic> goToReplace(
|
||||
BuildContext context,
|
||||
Widget Function(BuildContext context) builder,
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:flutter/cupertino.dart';
|
||||
|
||||
/// Creates gaps between given widgets
|
||||
List<Widget> spaced(double gap, Iterable<Widget> children) => children
|
||||
.expand((item) sync* {
|
||||
yield SizedBox(width: gap, height: gap);
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Given the background color, returns a text color
|
||||
/// with a good contrast ratio
|
||||
Color textColorBasedOnBackground(Color color) {
|
||||
if (color.computeLuminance() > 0.5) {
|
||||
return Colors.black;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
/// A badge with accent color as background
|
||||
class Badge extends StatelessWidget {
|
||||
final Widget child;
|
||||
final BorderRadiusGeometry borderRadius;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Should be spawned with a showModalBottomSheet, not routed to.
|
||||
class BottomModal extends StatelessWidget {
|
||||
final Widget child;
|
||||
final String title;
|
||||
|
|
|
@ -20,6 +20,7 @@ import 'bottom_modal.dart';
|
|||
import 'markdown_text.dart';
|
||||
import 'write_comment.dart';
|
||||
|
||||
/// A single comment that renders its replies
|
||||
class Comment extends HookWidget {
|
||||
final int indent;
|
||||
final int postCreatorId;
|
||||
|
|
|
@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
|||
import '../pages/media_view.dart';
|
||||
import '../util/goto.dart';
|
||||
|
||||
/// If the media is pressed, it opens itself in a [MediaViewPage]
|
||||
class FullscreenableImage extends StatelessWidget {
|
||||
final String url;
|
||||
final Widget child;
|
||||
|
|
|
@ -18,6 +18,7 @@ class InfiniteScrollController {
|
|||
}
|
||||
}
|
||||
|
||||
/// `ListView.builder` with asynchronous data fetching
|
||||
class InfiniteScroll<T> extends HookWidget {
|
||||
final int batchSize;
|
||||
final Widget loadingWidget;
|
||||
|
|
|
@ -6,6 +6,7 @@ import 'package:markdown/markdown.dart' as md;
|
|||
import '../url_launcher.dart';
|
||||
import 'fullscreenable_image.dart';
|
||||
|
||||
/// A Markdown renderer with link/image handling
|
||||
class MarkdownText extends StatelessWidget {
|
||||
final String instanceUrl;
|
||||
final String text;
|
||||
|
|
|
@ -44,6 +44,7 @@ MediaType whatType(String url) {
|
|||
return MediaType.other;
|
||||
}
|
||||
|
||||
/// A post overview card
|
||||
class Post extends HookWidget {
|
||||
final PostView post;
|
||||
final String instanceUrl;
|
||||
|
@ -426,6 +427,7 @@ class Post extends HookWidget {
|
|||
else if (post.url != null && post.url.isNotEmpty)
|
||||
linkPreview(),
|
||||
if (post.body != null)
|
||||
// TODO: trim content
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: MarkdownText(post.body, instanceUrl: instanceUrl)),
|
||||
|
|
|
@ -5,6 +5,7 @@ import 'package:lemmy_api_client/lemmy_api_client.dart';
|
|||
|
||||
import 'bottom_modal.dart';
|
||||
|
||||
/// Dropdown filters where you can change sorting or viewing type
|
||||
class PostListOptions extends HookWidget {
|
||||
final void Function(SortType sort) onChange;
|
||||
final SortType defaultSort;
|
||||
|
|
|
@ -11,6 +11,7 @@ import '../util/intl.dart';
|
|||
import '../util/text_color.dart';
|
||||
import 'badge.dart';
|
||||
|
||||
/// Shared widget of UserPage and ProfileTab
|
||||
class UserProfile extends HookWidget {
|
||||
final Future<UserView> _userView;
|
||||
final String instanceUrl;
|
||||
|
|
|
@ -7,6 +7,7 @@ import '../hooks/stores.dart';
|
|||
import '../util/extensions/api.dart';
|
||||
import 'markdown_text.dart';
|
||||
|
||||
/// Modal for writing a comment to a given post/comment (aka reply)
|
||||
/// on submit pops the navigator stack with a [CommentView]
|
||||
/// or `null` if cancelled
|
||||
class WriteComment extends HookWidget {
|
||||
|
|
Loading…
Reference in New Issue