Migrate modlog to mobx (#303)
* Migrate modlog to mobx * Remove column * Add MobxProvider and DisposableStore * Add modlog store tests
This commit is contained in:
parent
85f1ab8f99
commit
d4d4a5b999
|
@ -1,5 +1,6 @@
|
||||||
linter:
|
linter:
|
||||||
rules:
|
rules:
|
||||||
|
- annotate_overrides
|
||||||
- avoid_bool_literals_in_conditional_expressions
|
- avoid_bool_literals_in_conditional_expressions
|
||||||
- avoid_catching_errors
|
- avoid_catching_errors
|
||||||
- avoid_equals_and_hash_code_on_mutable_classes
|
- avoid_equals_and_hash_code_on_mutable_classes
|
||||||
|
|
|
@ -12,6 +12,7 @@ import 'l10n/timeago/pl.dart';
|
||||||
import 'pages/log_console/log_console_page_store.dart';
|
import 'pages/log_console/log_console_page_store.dart';
|
||||||
import 'stores/accounts_store.dart';
|
import 'stores/accounts_store.dart';
|
||||||
import 'stores/config_store.dart';
|
import 'stores/config_store.dart';
|
||||||
|
import 'util/mobx_provider.dart';
|
||||||
|
|
||||||
Future<void> mainCommon(AppConfig appConfig) async {
|
Future<void> mainCommon(AppConfig appConfig) async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
@ -22,15 +23,14 @@ Future<void> mainCommon(AppConfig appConfig) async {
|
||||||
_setupLogger(appConfig, logConsoleStore);
|
_setupLogger(appConfig, logConsoleStore);
|
||||||
_setupTimeago();
|
_setupTimeago();
|
||||||
|
|
||||||
final configStore = ConfigStore.load(sharedPrefs);
|
|
||||||
final accountsStore = await AccountsStore.load();
|
final accountsStore = await AccountsStore.load();
|
||||||
|
|
||||||
runApp(
|
runApp(
|
||||||
MultiProvider(
|
MultiProvider(
|
||||||
providers: [
|
providers: [
|
||||||
Provider.value(value: configStore),
|
MobxProvider(create: (context) => ConfigStore.load(sharedPrefs)),
|
||||||
ChangeNotifierProvider.value(value: accountsStore),
|
ChangeNotifierProvider.value(value: accountsStore),
|
||||||
Provider.value(value: logConsoleStore),
|
MobxProvider.value(value: logConsoleStore),
|
||||||
],
|
],
|
||||||
child: const MyApp(),
|
child: const MyApp(),
|
||||||
),
|
),
|
||||||
|
|
|
@ -10,6 +10,7 @@ import '../../util/async_store.dart';
|
||||||
import '../../util/async_store_listener.dart';
|
import '../../util/async_store_listener.dart';
|
||||||
import '../../util/extensions/api.dart';
|
import '../../util/extensions/api.dart';
|
||||||
import '../../util/icons.dart';
|
import '../../util/icons.dart';
|
||||||
|
import '../../util/mobx_provider.dart';
|
||||||
import '../../util/observer_consumers.dart';
|
import '../../util/observer_consumers.dart';
|
||||||
import '../../util/share.dart';
|
import '../../util/share.dart';
|
||||||
import '../../widgets/failed_to_load.dart';
|
import '../../widgets/failed_to_load.dart';
|
||||||
|
@ -158,7 +159,7 @@ class CommunityPage extends HookWidget {
|
||||||
static Route _route(String instanceHost, CommunityStore store) {
|
static Route _route(String instanceHost, CommunityStore store) {
|
||||||
return MaterialPageRoute(
|
return MaterialPageRoute(
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
return Provider.value(
|
return MobxProvider.value(
|
||||||
value: store
|
value: store
|
||||||
..refresh(context
|
..refresh(context
|
||||||
.read<AccountsStore>()
|
.read<AccountsStore>()
|
||||||
|
|
|
@ -4,13 +4,12 @@ import 'package:lemmy_api_client/v3.dart';
|
||||||
import '../../l10n/l10n.dart';
|
import '../../l10n/l10n.dart';
|
||||||
import '../../stores/accounts_store.dart';
|
import '../../stores/accounts_store.dart';
|
||||||
import '../../util/extensions/spaced.dart';
|
import '../../util/extensions/spaced.dart';
|
||||||
import '../../util/goto.dart';
|
|
||||||
import '../../util/observer_consumers.dart';
|
import '../../util/observer_consumers.dart';
|
||||||
import '../../widgets/bottom_safe.dart';
|
import '../../widgets/bottom_safe.dart';
|
||||||
import '../../widgets/markdown_text.dart';
|
import '../../widgets/markdown_text.dart';
|
||||||
import '../../widgets/pull_to_refresh.dart';
|
import '../../widgets/pull_to_refresh.dart';
|
||||||
import '../../widgets/user_tile.dart';
|
import '../../widgets/user_tile.dart';
|
||||||
import '../modlog_page.dart';
|
import '../modlog/modlog.dart';
|
||||||
import 'community_store.dart';
|
import 'community_store.dart';
|
||||||
|
|
||||||
class CommmunityAboutTab extends StatelessWidget {
|
class CommmunityAboutTab extends StatelessWidget {
|
||||||
|
@ -99,9 +98,8 @@ class CommmunityAboutTab extends StatelessWidget {
|
||||||
style: Theme.of(context).textTheme.headline6,
|
style: Theme.of(context).textTheme.headline6,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onTap: () => goTo(
|
onTap: () => Navigator.of(context).push(
|
||||||
context,
|
ModlogPage.forCommunityRoute(
|
||||||
(context) => ModlogPage.forCommunity(
|
|
||||||
instanceHost: community.instanceHost,
|
instanceHost: community.instanceHost,
|
||||||
communityId: community.community.id,
|
communityId: community.community.id,
|
||||||
communityName: community.community.name,
|
communityName: community.community.name,
|
||||||
|
|
|
@ -5,6 +5,7 @@ import 'package:url_launcher/url_launcher.dart' as ul;
|
||||||
|
|
||||||
import '../../hooks/logged_in_action.dart';
|
import '../../hooks/logged_in_action.dart';
|
||||||
import '../../util/extensions/api.dart';
|
import '../../util/extensions/api.dart';
|
||||||
|
import '../../util/mobx_provider.dart';
|
||||||
import '../../util/observer_consumers.dart';
|
import '../../util/observer_consumers.dart';
|
||||||
import '../../widgets/bottom_modal.dart';
|
import '../../widgets/bottom_modal.dart';
|
||||||
import '../../widgets/info_table_popup.dart';
|
import '../../widgets/info_table_popup.dart';
|
||||||
|
@ -60,9 +61,10 @@ class CommunityMoreMenu extends HookWidget {
|
||||||
|
|
||||||
static void open(BuildContext context, FullCommunityView fullCommunityView) {
|
static void open(BuildContext context, FullCommunityView fullCommunityView) {
|
||||||
final store = context.read<CommunityStore>();
|
final store = context.read<CommunityStore>();
|
||||||
|
|
||||||
showBottomModal(
|
showBottomModal(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => Provider.value(
|
builder: (context) => MobxProvider.value(
|
||||||
value: store,
|
value: store,
|
||||||
child: CommunityMoreMenu(
|
child: CommunityMoreMenu(
|
||||||
fullCommunityView: fullCommunityView,
|
fullCommunityView: fullCommunityView,
|
||||||
|
|
|
@ -11,6 +11,7 @@ import '../../stores/accounts_store.dart';
|
||||||
import '../../util/async_store_listener.dart';
|
import '../../util/async_store_listener.dart';
|
||||||
import '../../util/extensions/api.dart';
|
import '../../util/extensions/api.dart';
|
||||||
import '../../util/icons.dart';
|
import '../../util/icons.dart';
|
||||||
|
import '../../util/mobx_provider.dart';
|
||||||
import '../../util/observer_consumers.dart';
|
import '../../util/observer_consumers.dart';
|
||||||
import '../../util/share.dart';
|
import '../../util/share.dart';
|
||||||
import '../../widgets/failed_to_load.dart';
|
import '../../widgets/failed_to_load.dart';
|
||||||
|
@ -100,7 +101,7 @@ class FullPostPage extends HookWidget {
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(icon: Icon(shareIcon), onPressed: sharePost),
|
IconButton(icon: Icon(shareIcon), onPressed: sharePost),
|
||||||
Provider.value(
|
MobxProvider.value(
|
||||||
value: postStore,
|
value: postStore,
|
||||||
child: const SavePostButton(),
|
child: const SavePostButton(),
|
||||||
),
|
),
|
||||||
|
@ -143,7 +144,7 @@ class FullPostPage extends HookWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
static Route route(int id, String instanceHost) => MaterialPageRoute(
|
static Route route(int id, String instanceHost) => MaterialPageRoute(
|
||||||
builder: (context) => Provider(
|
builder: (context) => MobxProvider(
|
||||||
create: (context) =>
|
create: (context) =>
|
||||||
FullPostStore(instanceHost: instanceHost, postId: id)
|
FullPostStore(instanceHost: instanceHost, postId: id)
|
||||||
..refresh(_tryGetJwt(context, instanceHost)),
|
..refresh(_tryGetJwt(context, instanceHost)),
|
||||||
|
@ -152,14 +153,14 @@ class FullPostPage extends HookWidget {
|
||||||
);
|
);
|
||||||
|
|
||||||
static Route fromPostViewRoute(PostView postView) => MaterialPageRoute(
|
static Route fromPostViewRoute(PostView postView) => MaterialPageRoute(
|
||||||
builder: (context) => Provider(
|
builder: (context) => MobxProvider(
|
||||||
create: (context) => FullPostStore.fromPostView(postView)
|
create: (context) => FullPostStore.fromPostView(postView)
|
||||||
..refresh(_tryGetJwt(context, postView.instanceHost)),
|
..refresh(_tryGetJwt(context, postView.instanceHost)),
|
||||||
child: const FullPostPage._(),
|
child: const FullPostPage._(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
static Route fromPostStoreRoute(PostStore postStore) => MaterialPageRoute(
|
static Route fromPostStoreRoute(PostStore postStore) => MaterialPageRoute(
|
||||||
builder: (context) => Provider(
|
builder: (context) => MobxProvider(
|
||||||
create: (context) => FullPostStore.fromPostStore(postStore)
|
create: (context) => FullPostStore.fromPostStore(postStore)
|
||||||
..refresh(_tryGetJwt(context, postStore.postView.instanceHost)),
|
..refresh(_tryGetJwt(context, postStore.postView.instanceHost)),
|
||||||
child: const FullPostPage._(),
|
child: const FullPostPage._(),
|
||||||
|
|
|
@ -342,6 +342,7 @@ class _SelectedList {
|
||||||
this.instanceHost,
|
this.instanceHost,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
String toString() =>
|
String toString() =>
|
||||||
'SelectedList(instanceHost: $instanceHost, listingType: $listingType)';
|
'SelectedList(instanceHost: $instanceHost, listingType: $listingType)';
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,7 @@ import '../widgets/reveal_after_scroll.dart';
|
||||||
import '../widgets/sortable_infinite_list.dart';
|
import '../widgets/sortable_infinite_list.dart';
|
||||||
import '../widgets/user_tile.dart';
|
import '../widgets/user_tile.dart';
|
||||||
import 'communities_list.dart';
|
import 'communities_list.dart';
|
||||||
import 'modlog_page.dart';
|
import 'modlog/modlog.dart';
|
||||||
import 'users_list.dart';
|
import 'users_list.dart';
|
||||||
|
|
||||||
/// Displays posts, comments, and general info about the given instance
|
/// Displays posts, comments, and general info about the given instance
|
||||||
|
@ -365,9 +365,8 @@ class _AboutTab extends HookWidget {
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Center(child: Text(L10n.of(context).modlog)),
|
title: Center(child: Text(L10n.of(context).modlog)),
|
||||||
onTap: () => goTo(
|
onTap: () => Navigator.of(context).push(
|
||||||
context,
|
ModlogPage.forInstanceRoute(instanceHost),
|
||||||
(context) => ModlogPage.forInstance(instanceHost: instanceHost),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
@ -0,0 +1,107 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../../l10n/l10n.dart';
|
||||||
|
import '../../util/mobx_provider.dart';
|
||||||
|
import '../../util/observer_consumers.dart';
|
||||||
|
import '../../widgets/bottom_safe.dart';
|
||||||
|
import '../../widgets/failed_to_load.dart';
|
||||||
|
import 'modlog_page_store.dart';
|
||||||
|
import 'modlog_table.dart';
|
||||||
|
|
||||||
|
class ModlogPage extends StatelessWidget {
|
||||||
|
final String name;
|
||||||
|
|
||||||
|
const ModlogPage._({required this.name});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(title: Text("$name's modlog")),
|
||||||
|
body: LayoutBuilder(
|
||||||
|
builder: (context, constraints) => SingleChildScrollView(
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(minHeight: constraints.maxHeight),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
const SizedBox(),
|
||||||
|
ObserverBuilder<ModlogPageStore>(
|
||||||
|
builder: (context, store) {
|
||||||
|
if (!store.hasNextPage) {
|
||||||
|
return const Center(child: Text('no more logs to show'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return store.modlogState.map(
|
||||||
|
loading: () => const Center(
|
||||||
|
child: CircularProgressIndicator.adaptive(),
|
||||||
|
),
|
||||||
|
error: (errorTerm) => Center(
|
||||||
|
child: FailedToLoad(
|
||||||
|
message: errorTerm.tr(context),
|
||||||
|
refresh: store.fetchPage,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
data: (modlog) => ModlogTable(modlog: modlog),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Column(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
child: ObserverBuilder<ModlogPageStore>(
|
||||||
|
builder: (context, store) => Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: store.hasPreviousPage &&
|
||||||
|
!store.modlogState.isLoading
|
||||||
|
? store.previousPage
|
||||||
|
: null,
|
||||||
|
child: const Icon(Icons.skip_previous),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: store.hasNextPage &&
|
||||||
|
!store.modlogState.isLoading
|
||||||
|
? store.nextPage
|
||||||
|
: null,
|
||||||
|
child: const Icon(Icons.skip_next),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const BottomSafe(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Route forInstanceRoute(String instanceHost) {
|
||||||
|
return MaterialPageRoute(
|
||||||
|
builder: (context) => MobxProvider(
|
||||||
|
create: (context) => ModlogPageStore(instanceHost)..fetchPage(),
|
||||||
|
child: ModlogPage._(name: instanceHost),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Route forCommunityRoute({
|
||||||
|
required String instanceHost,
|
||||||
|
required int communityId,
|
||||||
|
required String communityName,
|
||||||
|
}) {
|
||||||
|
return MaterialPageRoute(
|
||||||
|
builder: (context) => MobxProvider(
|
||||||
|
create: (context) =>
|
||||||
|
ModlogPageStore(instanceHost, communityId)..fetchPage(),
|
||||||
|
child: ModlogPage._(name: '!$communityName'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,170 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:lemmy_api_client/v3.dart';
|
||||||
|
|
||||||
|
import '../../l10n/l10n.dart';
|
||||||
|
import '../../util/extensions/api.dart';
|
||||||
|
import '../../util/goto.dart';
|
||||||
|
import '../../widgets/avatar.dart';
|
||||||
|
|
||||||
|
class ModlogEntry {
|
||||||
|
final DateTime when;
|
||||||
|
final PersonSafe mod;
|
||||||
|
final Widget action;
|
||||||
|
final String? reason;
|
||||||
|
|
||||||
|
const ModlogEntry._({
|
||||||
|
required this.when,
|
||||||
|
required this.mod,
|
||||||
|
required this.action,
|
||||||
|
this.reason,
|
||||||
|
});
|
||||||
|
|
||||||
|
ModlogEntry.fromModRemovePostView(
|
||||||
|
ModRemovePostView removedPost,
|
||||||
|
Widget action,
|
||||||
|
) : this._(
|
||||||
|
when: removedPost.modRemovePost.when,
|
||||||
|
mod: removedPost.moderator,
|
||||||
|
action: action,
|
||||||
|
reason: removedPost.modRemovePost.reason,
|
||||||
|
);
|
||||||
|
|
||||||
|
ModlogEntry.fromModLockPostView(
|
||||||
|
ModLockPostView lockedPost,
|
||||||
|
Widget action,
|
||||||
|
) : this._(
|
||||||
|
when: lockedPost.modLockPost.when,
|
||||||
|
mod: lockedPost.moderator,
|
||||||
|
action: action,
|
||||||
|
);
|
||||||
|
|
||||||
|
ModlogEntry.fromModStickyPostView(
|
||||||
|
ModStickyPostView stickiedPost,
|
||||||
|
Widget action,
|
||||||
|
) : this._(
|
||||||
|
when: stickiedPost.modStickyPost.when,
|
||||||
|
mod: stickiedPost.moderator,
|
||||||
|
action: action,
|
||||||
|
);
|
||||||
|
|
||||||
|
ModlogEntry.fromModRemoveCommentView(
|
||||||
|
ModRemoveCommentView removedComment,
|
||||||
|
Widget action,
|
||||||
|
) : this._(
|
||||||
|
when: removedComment.modRemoveComment.when,
|
||||||
|
mod: removedComment.moderator,
|
||||||
|
action: action,
|
||||||
|
reason: removedComment.modRemoveComment.reason,
|
||||||
|
);
|
||||||
|
|
||||||
|
ModlogEntry.fromModRemoveCommunityView(
|
||||||
|
ModRemoveCommunityView removedCommunity,
|
||||||
|
Widget action,
|
||||||
|
) : this._(
|
||||||
|
when: removedCommunity.modRemoveCommunity.when,
|
||||||
|
mod: removedCommunity.moderator,
|
||||||
|
action: action,
|
||||||
|
reason: removedCommunity.modRemoveCommunity.reason,
|
||||||
|
);
|
||||||
|
|
||||||
|
ModlogEntry.fromModBanFromCommunityView(
|
||||||
|
ModBanFromCommunityView bannedFromCommunity,
|
||||||
|
Widget action,
|
||||||
|
) : this._(
|
||||||
|
when: bannedFromCommunity.modBanFromCommunity.when,
|
||||||
|
mod: bannedFromCommunity.moderator,
|
||||||
|
action: action,
|
||||||
|
reason: bannedFromCommunity.modBanFromCommunity.reason,
|
||||||
|
);
|
||||||
|
|
||||||
|
ModlogEntry.fromModBanView(
|
||||||
|
ModBanView banned,
|
||||||
|
Widget action,
|
||||||
|
) : this._(
|
||||||
|
when: banned.modBan.when,
|
||||||
|
mod: banned.moderator,
|
||||||
|
action: action,
|
||||||
|
reason: banned.modBan.reason,
|
||||||
|
);
|
||||||
|
|
||||||
|
ModlogEntry.fromModAddCommunityView(
|
||||||
|
ModAddCommunityView addedToCommunity,
|
||||||
|
Widget action,
|
||||||
|
) : this._(
|
||||||
|
when: addedToCommunity.modAddCommunity.when,
|
||||||
|
mod: addedToCommunity.moderator,
|
||||||
|
action: action,
|
||||||
|
);
|
||||||
|
|
||||||
|
ModlogEntry.fromModTransferCommunityView(
|
||||||
|
ModTransferCommunityView transferCommunity,
|
||||||
|
Widget action,
|
||||||
|
) : this._(
|
||||||
|
when: transferCommunity.modTransferCommunity.when,
|
||||||
|
mod: transferCommunity.moderator,
|
||||||
|
action: action,
|
||||||
|
);
|
||||||
|
|
||||||
|
ModlogEntry.fromModAddView(
|
||||||
|
ModAddView added,
|
||||||
|
Widget action,
|
||||||
|
) : this._(
|
||||||
|
when: added.modAdd.when,
|
||||||
|
mod: added.moderator,
|
||||||
|
action: action,
|
||||||
|
);
|
||||||
|
|
||||||
|
TableRow build(BuildContext context) {
|
||||||
|
return TableRow(
|
||||||
|
children: [
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => Dialog(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
child: Center(
|
||||||
|
heightFactor: 1,
|
||||||
|
child: Text(when.toString()),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Center(child: Text(when.timeagoShort(context))),
|
||||||
|
),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () => goToUser.byId(
|
||||||
|
context,
|
||||||
|
mod.instanceHost,
|
||||||
|
mod.id,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Avatar(
|
||||||
|
url: mod.avatar,
|
||||||
|
noBlank: true,
|
||||||
|
radius: 10,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
' ${mod.preferredName}',
|
||||||
|
style:
|
||||||
|
TextStyle(color: Theme.of(context).colorScheme.secondary),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
action,
|
||||||
|
if (reason == null) const Center(child: Text('-')) else Text(reason!),
|
||||||
|
]
|
||||||
|
.map(
|
||||||
|
(widget) => Padding(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
child: widget,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,62 @@
|
||||||
|
import 'package:lemmy_api_client/v3.dart';
|
||||||
|
import 'package:mobx/mobx.dart';
|
||||||
|
|
||||||
|
import '../../util/async_store.dart';
|
||||||
|
import '../../util/mobx_provider.dart';
|
||||||
|
|
||||||
|
part 'modlog_page_store.g.dart';
|
||||||
|
|
||||||
|
class ModlogPageStore = _ModlogPageStore with _$ModlogPageStore;
|
||||||
|
|
||||||
|
abstract class _ModlogPageStore with Store, DisposableStore {
|
||||||
|
final String instanceHost;
|
||||||
|
final int? communityId;
|
||||||
|
|
||||||
|
_ModlogPageStore(this.instanceHost, [this.communityId]) {
|
||||||
|
addReaction(reaction((_) => page, (_) => fetchPage()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@observable
|
||||||
|
int page = 1;
|
||||||
|
|
||||||
|
final modlogState = AsyncStore<Modlog>();
|
||||||
|
|
||||||
|
@computed
|
||||||
|
bool get hasPreviousPage => page != 1;
|
||||||
|
|
||||||
|
@computed
|
||||||
|
bool get hasNextPage =>
|
||||||
|
modlogState.asyncState.whenOrNull(
|
||||||
|
data: (data, error) =>
|
||||||
|
data.removedPosts.length +
|
||||||
|
data.lockedPosts.length +
|
||||||
|
data.stickiedPosts.length +
|
||||||
|
data.removedComments.length +
|
||||||
|
data.removedCommunities.length +
|
||||||
|
data.bannedFromCommunity.length +
|
||||||
|
data.banned.length +
|
||||||
|
data.addedToCommunity.length +
|
||||||
|
data.transferredToCommunity.length +
|
||||||
|
data.added.length !=
|
||||||
|
0,
|
||||||
|
) ??
|
||||||
|
true;
|
||||||
|
|
||||||
|
@action
|
||||||
|
Future<void> fetchPage() async {
|
||||||
|
await modlogState.runLemmy(
|
||||||
|
instanceHost,
|
||||||
|
GetModlog(page: page, communityId: communityId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
void previousPage() {
|
||||||
|
page--;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
void nextPage() {
|
||||||
|
page++;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,82 @@
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'modlog_page_store.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// StoreGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
// 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 _$ModlogPageStore on _ModlogPageStore, Store {
|
||||||
|
Computed<bool>? _$hasPreviousPageComputed;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get hasPreviousPage =>
|
||||||
|
(_$hasPreviousPageComputed ??= Computed<bool>(() => super.hasPreviousPage,
|
||||||
|
name: '_ModlogPageStore.hasPreviousPage'))
|
||||||
|
.value;
|
||||||
|
Computed<bool>? _$hasNextPageComputed;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get hasNextPage =>
|
||||||
|
(_$hasNextPageComputed ??= Computed<bool>(() => super.hasNextPage,
|
||||||
|
name: '_ModlogPageStore.hasNextPage'))
|
||||||
|
.value;
|
||||||
|
|
||||||
|
final _$pageAtom = Atom(name: '_ModlogPageStore.page');
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get page {
|
||||||
|
_$pageAtom.reportRead();
|
||||||
|
return super.page;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
set page(int value) {
|
||||||
|
_$pageAtom.reportWrite(value, super.page, () {
|
||||||
|
super.page = value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
final _$fetchPageAsyncAction = AsyncAction('_ModlogPageStore.fetchPage');
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> fetchPage() {
|
||||||
|
return _$fetchPageAsyncAction.run(() => super.fetchPage());
|
||||||
|
}
|
||||||
|
|
||||||
|
final _$_ModlogPageStoreActionController =
|
||||||
|
ActionController(name: '_ModlogPageStore');
|
||||||
|
|
||||||
|
@override
|
||||||
|
void previousPage() {
|
||||||
|
final _$actionInfo = _$_ModlogPageStoreActionController.startAction(
|
||||||
|
name: '_ModlogPageStore.previousPage');
|
||||||
|
try {
|
||||||
|
return super.previousPage();
|
||||||
|
} finally {
|
||||||
|
_$_ModlogPageStoreActionController.endAction(_$actionInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void nextPage() {
|
||||||
|
final _$actionInfo = _$_ModlogPageStoreActionController.startAction(
|
||||||
|
name: '_ModlogPageStore.nextPage');
|
||||||
|
try {
|
||||||
|
return super.nextPage();
|
||||||
|
} finally {
|
||||||
|
_$_ModlogPageStoreActionController.endAction(_$actionInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return '''
|
||||||
|
page: ${page},
|
||||||
|
hasPreviousPage: ${hasPreviousPage},
|
||||||
|
hasNextPage: ${hasNextPage}
|
||||||
|
''';
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,310 @@
|
||||||
|
import 'package:flutter/gestures.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:lemmy_api_client/v3.dart';
|
||||||
|
|
||||||
|
import '../../util/extensions/api.dart';
|
||||||
|
import '../../util/goto.dart';
|
||||||
|
import '../../widgets/avatar.dart';
|
||||||
|
import 'modlog_entry.dart';
|
||||||
|
|
||||||
|
class ModlogTable extends StatelessWidget {
|
||||||
|
const ModlogTable({Key? key, required this.modlog}) : super(key: key);
|
||||||
|
|
||||||
|
final Modlog modlog;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
|
InlineSpan user(PersonSafe user) {
|
||||||
|
return TextSpan(
|
||||||
|
children: [
|
||||||
|
WidgetSpan(
|
||||||
|
child: Avatar(
|
||||||
|
url: user.avatar,
|
||||||
|
noBlank: true,
|
||||||
|
radius: 10,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextSpan(
|
||||||
|
text: ' ${user.preferredName}',
|
||||||
|
style: TextStyle(color: theme.colorScheme.secondary),
|
||||||
|
recognizer: TapGestureRecognizer()
|
||||||
|
..onTap = () => goToUser.byId(
|
||||||
|
context,
|
||||||
|
user.instanceHost,
|
||||||
|
user.id,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
InlineSpan community(CommunitySafe community) {
|
||||||
|
return TextSpan(
|
||||||
|
children: [
|
||||||
|
WidgetSpan(
|
||||||
|
child: Avatar(
|
||||||
|
url: community.icon,
|
||||||
|
noBlank: true,
|
||||||
|
radius: 10,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextSpan(
|
||||||
|
text: ' !${community.name}',
|
||||||
|
style: TextStyle(color: theme.colorScheme.secondary),
|
||||||
|
recognizer: TapGestureRecognizer()
|
||||||
|
..onTap = () => goToCommunity.byId(
|
||||||
|
context,
|
||||||
|
community.instanceHost,
|
||||||
|
community.id,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final modlogEntries = [
|
||||||
|
for (final removedPost in modlog.removedPosts)
|
||||||
|
ModlogEntry.fromModRemovePostView(
|
||||||
|
removedPost,
|
||||||
|
RichText(
|
||||||
|
text: TextSpan(
|
||||||
|
children: [
|
||||||
|
if (removedPost.modRemovePost.removed ?? false)
|
||||||
|
const TextSpan(text: 'removed')
|
||||||
|
else
|
||||||
|
const TextSpan(text: 'restored'),
|
||||||
|
const TextSpan(text: ' post '),
|
||||||
|
TextSpan(
|
||||||
|
text: '"${removedPost.post.name}"',
|
||||||
|
style: TextStyle(color: theme.colorScheme.secondary),
|
||||||
|
recognizer: TapGestureRecognizer()
|
||||||
|
..onTap = () => goToPost(
|
||||||
|
context,
|
||||||
|
removedPost.instanceHost,
|
||||||
|
removedPost.post.id,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
style: TextStyle(color: theme.colorScheme.onSurface),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
for (final lockedPost in modlog.lockedPosts)
|
||||||
|
ModlogEntry.fromModLockPostView(
|
||||||
|
lockedPost,
|
||||||
|
RichText(
|
||||||
|
text: TextSpan(
|
||||||
|
children: [
|
||||||
|
if (lockedPost.modLockPost.locked ?? false)
|
||||||
|
const TextSpan(text: 'locked')
|
||||||
|
else
|
||||||
|
const TextSpan(text: 'unlocked'),
|
||||||
|
const TextSpan(text: ' post '),
|
||||||
|
TextSpan(
|
||||||
|
text: '"${lockedPost.post.name}"',
|
||||||
|
style: TextStyle(color: theme.colorScheme.secondary),
|
||||||
|
recognizer: TapGestureRecognizer()
|
||||||
|
..onTap = () => goToPost(
|
||||||
|
context,
|
||||||
|
lockedPost.instanceHost,
|
||||||
|
lockedPost.post.id,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
style: TextStyle(color: theme.colorScheme.onSurface),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
for (final stickiedPost in modlog.stickiedPosts)
|
||||||
|
ModlogEntry.fromModStickyPostView(
|
||||||
|
stickiedPost,
|
||||||
|
RichText(
|
||||||
|
text: TextSpan(
|
||||||
|
children: [
|
||||||
|
if (stickiedPost.modStickyPost.stickied ?? false)
|
||||||
|
const TextSpan(text: 'stickied')
|
||||||
|
else
|
||||||
|
const TextSpan(text: 'unstickied'),
|
||||||
|
const TextSpan(text: ' post '),
|
||||||
|
TextSpan(
|
||||||
|
text: '"${stickiedPost.post.name}"',
|
||||||
|
style: TextStyle(color: theme.colorScheme.secondary),
|
||||||
|
recognizer: TapGestureRecognizer()
|
||||||
|
..onTap = () => goToPost(
|
||||||
|
context,
|
||||||
|
stickiedPost.instanceHost,
|
||||||
|
stickiedPost.post.id,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
style: TextStyle(color: theme.colorScheme.onSurface),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
for (final removedComment in modlog.removedComments)
|
||||||
|
ModlogEntry.fromModRemoveCommentView(
|
||||||
|
removedComment,
|
||||||
|
RichText(
|
||||||
|
text: TextSpan(
|
||||||
|
children: [
|
||||||
|
if (removedComment.modRemoveComment.removed ?? false)
|
||||||
|
const TextSpan(text: 'removed')
|
||||||
|
else
|
||||||
|
const TextSpan(text: 'restored'),
|
||||||
|
const TextSpan(text: ' comment '),
|
||||||
|
TextSpan(
|
||||||
|
text:
|
||||||
|
'"${removedComment.comment.content.replaceAll('\n', ' ')}"',
|
||||||
|
style: TextStyle(color: theme.colorScheme.secondary),
|
||||||
|
recognizer: TapGestureRecognizer()
|
||||||
|
..onTap = () => goToPost(
|
||||||
|
context,
|
||||||
|
removedComment.instanceHost,
|
||||||
|
removedComment.post.id,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const TextSpan(text: ' by '),
|
||||||
|
user(removedComment.commenter),
|
||||||
|
],
|
||||||
|
style: TextStyle(color: theme.colorScheme.onSurface),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
for (final removedCommunity in modlog.removedCommunities)
|
||||||
|
ModlogEntry.fromModRemoveCommunityView(
|
||||||
|
removedCommunity,
|
||||||
|
RichText(
|
||||||
|
text: TextSpan(
|
||||||
|
children: [
|
||||||
|
if (removedCommunity.modRemoveCommunity.removed ?? false)
|
||||||
|
const TextSpan(text: 'removed')
|
||||||
|
else
|
||||||
|
const TextSpan(text: 'restored'),
|
||||||
|
const TextSpan(text: ' community '),
|
||||||
|
community(removedCommunity.community),
|
||||||
|
],
|
||||||
|
style: TextStyle(color: theme.colorScheme.onSurface),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
for (final bannedFromCommunity in modlog.bannedFromCommunity)
|
||||||
|
ModlogEntry.fromModBanFromCommunityView(
|
||||||
|
bannedFromCommunity,
|
||||||
|
RichText(
|
||||||
|
text: TextSpan(
|
||||||
|
children: [
|
||||||
|
if (bannedFromCommunity.modBanFromCommunity.banned ?? false)
|
||||||
|
const TextSpan(text: 'banned ')
|
||||||
|
else
|
||||||
|
const TextSpan(text: 'unbanned '),
|
||||||
|
user(bannedFromCommunity.bannedPerson),
|
||||||
|
const TextSpan(text: ' from community '),
|
||||||
|
community(bannedFromCommunity.community),
|
||||||
|
],
|
||||||
|
style: TextStyle(color: theme.colorScheme.onSurface),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
for (final banned in modlog.banned)
|
||||||
|
ModlogEntry.fromModBanView(
|
||||||
|
banned,
|
||||||
|
RichText(
|
||||||
|
text: TextSpan(
|
||||||
|
children: [
|
||||||
|
if (banned.modBan.banned ?? false)
|
||||||
|
const TextSpan(text: 'banned ')
|
||||||
|
else
|
||||||
|
const TextSpan(text: 'unbanned '),
|
||||||
|
user(banned.bannedPerson),
|
||||||
|
],
|
||||||
|
style: TextStyle(color: theme.colorScheme.onSurface),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
for (final addedToCommunity in modlog.addedToCommunity)
|
||||||
|
ModlogEntry.fromModAddCommunityView(
|
||||||
|
addedToCommunity,
|
||||||
|
RichText(
|
||||||
|
text: TextSpan(
|
||||||
|
children: [
|
||||||
|
if (addedToCommunity.modAddCommunity.removed ?? false)
|
||||||
|
const TextSpan(text: 'removed ')
|
||||||
|
else
|
||||||
|
const TextSpan(text: 'appointed '),
|
||||||
|
user(addedToCommunity.moddedPerson),
|
||||||
|
const TextSpan(text: ' as mod of '),
|
||||||
|
community(addedToCommunity.community),
|
||||||
|
],
|
||||||
|
style: TextStyle(color: theme.colorScheme.onSurface),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
for (final transferredToCommunity in modlog.transferredToCommunity)
|
||||||
|
ModlogEntry.fromModTransferCommunityView(
|
||||||
|
transferredToCommunity,
|
||||||
|
RichText(
|
||||||
|
text: TextSpan(
|
||||||
|
children: [
|
||||||
|
if (transferredToCommunity.modTransferCommunity.removed ??
|
||||||
|
false)
|
||||||
|
const TextSpan(text: 'removed ')
|
||||||
|
else
|
||||||
|
const TextSpan(text: 'transferred '),
|
||||||
|
community(transferredToCommunity.community),
|
||||||
|
const TextSpan(text: ' to '),
|
||||||
|
user(transferredToCommunity.moddedPerson),
|
||||||
|
],
|
||||||
|
style: TextStyle(color: theme.colorScheme.onSurface),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
for (final added in modlog.added)
|
||||||
|
ModlogEntry.fromModAddView(
|
||||||
|
added,
|
||||||
|
RichText(
|
||||||
|
text: TextSpan(
|
||||||
|
children: [
|
||||||
|
if (added.modAdd.removed ?? false)
|
||||||
|
const TextSpan(text: 'removed ')
|
||||||
|
else
|
||||||
|
const TextSpan(text: 'apointed '),
|
||||||
|
user(added.moddedPerson),
|
||||||
|
const TextSpan(text: ' as admin'),
|
||||||
|
],
|
||||||
|
style: TextStyle(color: theme.colorScheme.onSurface),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]..sort((a, b) => b.when.compareTo(a.when));
|
||||||
|
|
||||||
|
return SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
child: SizedBox(
|
||||||
|
width: 1000,
|
||||||
|
child: Table(
|
||||||
|
border: TableBorder.all(color: theme.colorScheme.onSurface),
|
||||||
|
columnWidths: const {
|
||||||
|
0: FixedColumnWidth(80),
|
||||||
|
1: FixedColumnWidth(200),
|
||||||
|
2: FlexColumnWidth(),
|
||||||
|
3: FixedColumnWidth(200),
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
const TableRow(
|
||||||
|
children: [
|
||||||
|
Center(child: Text('when')),
|
||||||
|
Center(child: Text('mod')),
|
||||||
|
Center(child: Text('action')),
|
||||||
|
Center(child: Text('reason')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
for (final modlogEntry in modlogEntries) modlogEntry.build(context)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,574 +0,0 @@
|
||||||
import 'package:flutter/gestures.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
|
||||||
import 'package:lemmy_api_client/v3.dart';
|
|
||||||
|
|
||||||
import '../l10n/l10n.dart';
|
|
||||||
import '../util/extensions/api.dart';
|
|
||||||
import '../util/goto.dart';
|
|
||||||
import '../widgets/avatar.dart';
|
|
||||||
import '../widgets/bottom_safe.dart';
|
|
||||||
|
|
||||||
class ModlogPage extends HookWidget {
|
|
||||||
final String instanceHost;
|
|
||||||
final String name;
|
|
||||||
final int? communityId;
|
|
||||||
|
|
||||||
const ModlogPage.forInstance({
|
|
||||||
required this.instanceHost,
|
|
||||||
}) : communityId = null,
|
|
||||||
name = instanceHost;
|
|
||||||
|
|
||||||
const ModlogPage.forCommunity({
|
|
||||||
required this.instanceHost,
|
|
||||||
required int this.communityId,
|
|
||||||
required String communityName,
|
|
||||||
}) : name = '!$communityName';
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final page = useState(1);
|
|
||||||
// will be set true when a fetch returns 0 results
|
|
||||||
final isDone = useState(false);
|
|
||||||
|
|
||||||
final modlogFut = useMemoized(
|
|
||||||
() => LemmyApiV3(instanceHost).run(
|
|
||||||
GetModlog(
|
|
||||||
communityId: communityId,
|
|
||||||
page: page.value,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
[page.value],
|
|
||||||
);
|
|
||||||
|
|
||||||
return Scaffold(
|
|
||||||
appBar: AppBar(title: Text("$name's modlog")),
|
|
||||||
body: LayoutBuilder(
|
|
||||||
builder: (context, constraints) => SingleChildScrollView(
|
|
||||||
child: ConstrainedBox(
|
|
||||||
constraints: BoxConstraints(minHeight: constraints.maxHeight),
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
const SizedBox.shrink(),
|
|
||||||
FutureBuilder<Modlog>(
|
|
||||||
key: ValueKey(modlogFut),
|
|
||||||
future: modlogFut,
|
|
||||||
builder: (context, snapshot) {
|
|
||||||
if (!snapshot.hasData) {
|
|
||||||
return const Center(
|
|
||||||
child: CircularProgressIndicator.adaptive());
|
|
||||||
} else if (snapshot.hasError) {
|
|
||||||
return Center(
|
|
||||||
child: Text('Error: ${snapshot.error?.toString()}'));
|
|
||||||
}
|
|
||||||
final modlog = snapshot.requireData;
|
|
||||||
|
|
||||||
if (modlog.added.length +
|
|
||||||
modlog.addedToCommunity.length +
|
|
||||||
modlog.banned.length +
|
|
||||||
modlog.bannedFromCommunity.length +
|
|
||||||
modlog.lockedPosts.length +
|
|
||||||
modlog.removedComments.length +
|
|
||||||
modlog.removedCommunities.length +
|
|
||||||
modlog.removedPosts.length +
|
|
||||||
modlog.stickiedPosts.length ==
|
|
||||||
0) {
|
|
||||||
WidgetsBinding.instance
|
|
||||||
?.addPostFrameCallback((_) => isDone.value = true);
|
|
||||||
|
|
||||||
return const Center(child: Text('no more logs to show'));
|
|
||||||
}
|
|
||||||
|
|
||||||
return _ModlogTable(modlog: modlog);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
Column(
|
|
||||||
children: [
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(8),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
TextButton(
|
|
||||||
onPressed:
|
|
||||||
page.value != 1 ? () => page.value-- : null,
|
|
||||||
child: const Icon(Icons.skip_previous),
|
|
||||||
),
|
|
||||||
TextButton(
|
|
||||||
onPressed: isDone.value ? null : () => page.value++,
|
|
||||||
child: const Icon(Icons.skip_next),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const BottomSafe(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ModlogTable extends StatelessWidget {
|
|
||||||
const _ModlogTable({Key? key, required this.modlog}) : super(key: key);
|
|
||||||
|
|
||||||
final Modlog modlog;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final theme = Theme.of(context);
|
|
||||||
|
|
||||||
InlineSpan user(PersonSafe user) => TextSpan(
|
|
||||||
children: [
|
|
||||||
WidgetSpan(
|
|
||||||
child: Avatar(
|
|
||||||
url: user.avatar,
|
|
||||||
noBlank: true,
|
|
||||||
radius: 10,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
TextSpan(
|
|
||||||
text: ' ${user.preferredName}',
|
|
||||||
style: TextStyle(color: theme.colorScheme.secondary),
|
|
||||||
recognizer: TapGestureRecognizer()
|
|
||||||
..onTap = () => goToUser.byId(
|
|
||||||
context,
|
|
||||||
user.instanceHost,
|
|
||||||
user.id,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
InlineSpan community(CommunitySafe community) => TextSpan(
|
|
||||||
children: [
|
|
||||||
WidgetSpan(
|
|
||||||
child: Avatar(
|
|
||||||
url: community.icon,
|
|
||||||
noBlank: true,
|
|
||||||
radius: 10,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
TextSpan(
|
|
||||||
text: ' !${community.name}',
|
|
||||||
style: TextStyle(color: theme.colorScheme.secondary),
|
|
||||||
recognizer: TapGestureRecognizer()
|
|
||||||
..onTap = () => goToCommunity.byId(
|
|
||||||
context,
|
|
||||||
community.instanceHost,
|
|
||||||
community.id,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
final modlogEntries = [
|
|
||||||
for (final removedPost in modlog.removedPosts)
|
|
||||||
_ModlogEntry.fromModRemovePostView(
|
|
||||||
removedPost,
|
|
||||||
RichText(
|
|
||||||
text: TextSpan(
|
|
||||||
children: [
|
|
||||||
if (removedPost.modRemovePost.removed ?? false)
|
|
||||||
const TextSpan(text: 'removed')
|
|
||||||
else
|
|
||||||
const TextSpan(text: 'restored'),
|
|
||||||
const TextSpan(text: ' post '),
|
|
||||||
TextSpan(
|
|
||||||
text: '"${removedPost.post.name}"',
|
|
||||||
style: TextStyle(color: theme.colorScheme.secondary),
|
|
||||||
recognizer: TapGestureRecognizer()
|
|
||||||
..onTap = () => goToPost(
|
|
||||||
context,
|
|
||||||
removedPost.instanceHost,
|
|
||||||
removedPost.post.id,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
style: TextStyle(color: theme.colorScheme.onSurface),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
for (final lockedPost in modlog.lockedPosts)
|
|
||||||
_ModlogEntry.fromModLockPostView(
|
|
||||||
lockedPost,
|
|
||||||
RichText(
|
|
||||||
text: TextSpan(
|
|
||||||
children: [
|
|
||||||
if (lockedPost.modLockPost.locked ?? false)
|
|
||||||
const TextSpan(text: 'locked')
|
|
||||||
else
|
|
||||||
const TextSpan(text: 'unlocked'),
|
|
||||||
const TextSpan(text: ' post '),
|
|
||||||
TextSpan(
|
|
||||||
text: '"${lockedPost.post.name}"',
|
|
||||||
style: TextStyle(color: theme.colorScheme.secondary),
|
|
||||||
recognizer: TapGestureRecognizer()
|
|
||||||
..onTap = () => goToPost(
|
|
||||||
context,
|
|
||||||
lockedPost.instanceHost,
|
|
||||||
lockedPost.post.id,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
style: TextStyle(color: theme.colorScheme.onSurface),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
for (final stickiedPost in modlog.stickiedPosts)
|
|
||||||
_ModlogEntry.fromModStickyPostView(
|
|
||||||
stickiedPost,
|
|
||||||
RichText(
|
|
||||||
text: TextSpan(
|
|
||||||
children: [
|
|
||||||
if (stickiedPost.modStickyPost.stickied ?? false)
|
|
||||||
const TextSpan(text: 'stickied')
|
|
||||||
else
|
|
||||||
const TextSpan(text: 'unstickied'),
|
|
||||||
const TextSpan(text: ' post '),
|
|
||||||
TextSpan(
|
|
||||||
text: '"${stickiedPost.post.name}"',
|
|
||||||
style: TextStyle(color: theme.colorScheme.secondary),
|
|
||||||
recognizer: TapGestureRecognizer()
|
|
||||||
..onTap = () => goToPost(
|
|
||||||
context,
|
|
||||||
stickiedPost.instanceHost,
|
|
||||||
stickiedPost.post.id,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
style: TextStyle(color: theme.colorScheme.onSurface),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
for (final removedComment in modlog.removedComments)
|
|
||||||
_ModlogEntry.fromModRemoveCommentView(
|
|
||||||
removedComment,
|
|
||||||
RichText(
|
|
||||||
text: TextSpan(
|
|
||||||
children: [
|
|
||||||
if (removedComment.modRemoveComment.removed ?? false)
|
|
||||||
const TextSpan(text: 'removed')
|
|
||||||
else
|
|
||||||
const TextSpan(text: 'restored'),
|
|
||||||
const TextSpan(text: ' comment '),
|
|
||||||
TextSpan(
|
|
||||||
text:
|
|
||||||
'"${removedComment.comment.content.replaceAll('\n', ' ')}"',
|
|
||||||
style: TextStyle(color: theme.colorScheme.secondary),
|
|
||||||
recognizer: TapGestureRecognizer()
|
|
||||||
..onTap = () => goToPost(
|
|
||||||
context,
|
|
||||||
removedComment.instanceHost,
|
|
||||||
removedComment.post.id,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const TextSpan(text: ' by '),
|
|
||||||
user(removedComment.commenter),
|
|
||||||
],
|
|
||||||
style: TextStyle(color: theme.colorScheme.onSurface),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
for (final removedCommunity in modlog.removedCommunities)
|
|
||||||
_ModlogEntry.fromModRemoveCommunityView(
|
|
||||||
removedCommunity,
|
|
||||||
RichText(
|
|
||||||
text: TextSpan(
|
|
||||||
children: [
|
|
||||||
if (removedCommunity.modRemoveCommunity.removed ?? false)
|
|
||||||
const TextSpan(text: 'removed')
|
|
||||||
else
|
|
||||||
const TextSpan(text: 'restored'),
|
|
||||||
const TextSpan(text: ' community '),
|
|
||||||
community(removedCommunity.community),
|
|
||||||
],
|
|
||||||
style: TextStyle(color: theme.colorScheme.onSurface),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
for (final bannedFromCommunity in modlog.bannedFromCommunity)
|
|
||||||
_ModlogEntry.fromModBanFromCommunityView(
|
|
||||||
bannedFromCommunity,
|
|
||||||
RichText(
|
|
||||||
text: TextSpan(
|
|
||||||
children: [
|
|
||||||
if (bannedFromCommunity.modBanFromCommunity.banned ?? false)
|
|
||||||
const TextSpan(text: 'banned ')
|
|
||||||
else
|
|
||||||
const TextSpan(text: 'unbanned '),
|
|
||||||
user(bannedFromCommunity.bannedPerson),
|
|
||||||
const TextSpan(text: ' from community '),
|
|
||||||
community(bannedFromCommunity.community),
|
|
||||||
],
|
|
||||||
style: TextStyle(color: theme.colorScheme.onSurface),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
for (final banned in modlog.banned)
|
|
||||||
_ModlogEntry.fromModBanView(
|
|
||||||
banned,
|
|
||||||
RichText(
|
|
||||||
text: TextSpan(
|
|
||||||
children: [
|
|
||||||
if (banned.modBan.banned ?? false)
|
|
||||||
const TextSpan(text: 'banned ')
|
|
||||||
else
|
|
||||||
const TextSpan(text: 'unbanned '),
|
|
||||||
user(banned.bannedPerson),
|
|
||||||
],
|
|
||||||
style: TextStyle(color: theme.colorScheme.onSurface),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
for (final addedToCommunity in modlog.addedToCommunity)
|
|
||||||
_ModlogEntry.fromModAddCommunityView(
|
|
||||||
addedToCommunity,
|
|
||||||
RichText(
|
|
||||||
text: TextSpan(
|
|
||||||
children: [
|
|
||||||
if (addedToCommunity.modAddCommunity.removed ?? false)
|
|
||||||
const TextSpan(text: 'removed ')
|
|
||||||
else
|
|
||||||
const TextSpan(text: 'appointed '),
|
|
||||||
user(addedToCommunity.moddedPerson),
|
|
||||||
const TextSpan(text: ' as mod of '),
|
|
||||||
community(addedToCommunity.community),
|
|
||||||
],
|
|
||||||
style: TextStyle(color: theme.colorScheme.onSurface),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
for (final transferredToCommunity in modlog.transferredToCommunity)
|
|
||||||
_ModlogEntry.fromModTransferCommunityView(
|
|
||||||
transferredToCommunity,
|
|
||||||
RichText(
|
|
||||||
text: TextSpan(
|
|
||||||
children: [
|
|
||||||
if (transferredToCommunity.modTransferCommunity.removed ??
|
|
||||||
false)
|
|
||||||
const TextSpan(text: 'removed ')
|
|
||||||
else
|
|
||||||
const TextSpan(text: 'transferred '),
|
|
||||||
community(transferredToCommunity.community),
|
|
||||||
const TextSpan(text: ' to '),
|
|
||||||
user(transferredToCommunity.moddedPerson),
|
|
||||||
],
|
|
||||||
style: TextStyle(color: theme.colorScheme.onSurface),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
for (final added in modlog.added)
|
|
||||||
_ModlogEntry.fromModAddView(
|
|
||||||
added,
|
|
||||||
RichText(
|
|
||||||
text: TextSpan(
|
|
||||||
children: [
|
|
||||||
if (added.modAdd.removed ?? false)
|
|
||||||
const TextSpan(text: 'removed ')
|
|
||||||
else
|
|
||||||
const TextSpan(text: 'apointed '),
|
|
||||||
user(added.moddedPerson),
|
|
||||||
const TextSpan(text: ' as admin'),
|
|
||||||
],
|
|
||||||
style: TextStyle(color: theme.colorScheme.onSurface),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]..sort((a, b) => b.when.compareTo(a.when));
|
|
||||||
|
|
||||||
return SingleChildScrollView(
|
|
||||||
padding: const EdgeInsets.all(8),
|
|
||||||
scrollDirection: Axis.horizontal,
|
|
||||||
child: SizedBox(
|
|
||||||
width: 1000,
|
|
||||||
child: Table(
|
|
||||||
border: TableBorder.all(color: theme.colorScheme.onSurface),
|
|
||||||
columnWidths: const {
|
|
||||||
0: FixedColumnWidth(80),
|
|
||||||
1: FixedColumnWidth(200),
|
|
||||||
2: FlexColumnWidth(),
|
|
||||||
3: FixedColumnWidth(200),
|
|
||||||
},
|
|
||||||
children: [
|
|
||||||
const TableRow(
|
|
||||||
children: [
|
|
||||||
Center(child: Text('when')),
|
|
||||||
Center(child: Text('mod')),
|
|
||||||
Center(child: Text('action')),
|
|
||||||
Center(child: Text('reason')),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
for (final modlogEntry in modlogEntries) modlogEntry.build(context)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ModlogEntry {
|
|
||||||
final DateTime when;
|
|
||||||
final PersonSafe mod;
|
|
||||||
final Widget action;
|
|
||||||
final String? reason;
|
|
||||||
|
|
||||||
const _ModlogEntry({
|
|
||||||
required this.when,
|
|
||||||
required this.mod,
|
|
||||||
required this.action,
|
|
||||||
this.reason,
|
|
||||||
});
|
|
||||||
|
|
||||||
_ModlogEntry.fromModRemovePostView(
|
|
||||||
ModRemovePostView removedPost,
|
|
||||||
Widget action,
|
|
||||||
) : this(
|
|
||||||
when: removedPost.modRemovePost.when,
|
|
||||||
mod: removedPost.moderator,
|
|
||||||
action: action,
|
|
||||||
reason: removedPost.modRemovePost.reason,
|
|
||||||
);
|
|
||||||
|
|
||||||
_ModlogEntry.fromModLockPostView(
|
|
||||||
ModLockPostView lockedPost,
|
|
||||||
Widget action,
|
|
||||||
) : this(
|
|
||||||
when: lockedPost.modLockPost.when,
|
|
||||||
mod: lockedPost.moderator,
|
|
||||||
action: action,
|
|
||||||
);
|
|
||||||
|
|
||||||
_ModlogEntry.fromModStickyPostView(
|
|
||||||
ModStickyPostView stickiedPost,
|
|
||||||
Widget action,
|
|
||||||
) : this(
|
|
||||||
when: stickiedPost.modStickyPost.when,
|
|
||||||
mod: stickiedPost.moderator,
|
|
||||||
action: action,
|
|
||||||
);
|
|
||||||
|
|
||||||
_ModlogEntry.fromModRemoveCommentView(
|
|
||||||
ModRemoveCommentView removedComment,
|
|
||||||
Widget action,
|
|
||||||
) : this(
|
|
||||||
when: removedComment.modRemoveComment.when,
|
|
||||||
mod: removedComment.moderator,
|
|
||||||
action: action,
|
|
||||||
reason: removedComment.modRemoveComment.reason,
|
|
||||||
);
|
|
||||||
|
|
||||||
_ModlogEntry.fromModRemoveCommunityView(
|
|
||||||
ModRemoveCommunityView removedCommunity,
|
|
||||||
Widget action,
|
|
||||||
) : this(
|
|
||||||
when: removedCommunity.modRemoveCommunity.when,
|
|
||||||
mod: removedCommunity.moderator,
|
|
||||||
action: action,
|
|
||||||
reason: removedCommunity.modRemoveCommunity.reason,
|
|
||||||
);
|
|
||||||
|
|
||||||
_ModlogEntry.fromModBanFromCommunityView(
|
|
||||||
ModBanFromCommunityView bannedFromCommunity,
|
|
||||||
Widget action,
|
|
||||||
) : this(
|
|
||||||
when: bannedFromCommunity.modBanFromCommunity.when,
|
|
||||||
mod: bannedFromCommunity.moderator,
|
|
||||||
action: action,
|
|
||||||
reason: bannedFromCommunity.modBanFromCommunity.reason,
|
|
||||||
);
|
|
||||||
|
|
||||||
_ModlogEntry.fromModBanView(
|
|
||||||
ModBanView banned,
|
|
||||||
Widget action,
|
|
||||||
) : this(
|
|
||||||
when: banned.modBan.when,
|
|
||||||
mod: banned.moderator,
|
|
||||||
action: action,
|
|
||||||
reason: banned.modBan.reason,
|
|
||||||
);
|
|
||||||
|
|
||||||
_ModlogEntry.fromModAddCommunityView(
|
|
||||||
ModAddCommunityView addedToCommunity,
|
|
||||||
Widget action,
|
|
||||||
) : this(
|
|
||||||
when: addedToCommunity.modAddCommunity.when,
|
|
||||||
mod: addedToCommunity.moderator,
|
|
||||||
action: action,
|
|
||||||
);
|
|
||||||
|
|
||||||
_ModlogEntry.fromModTransferCommunityView(
|
|
||||||
ModTransferCommunityView transferCommunity,
|
|
||||||
Widget action,
|
|
||||||
) : this(
|
|
||||||
when: transferCommunity.modTransferCommunity.when,
|
|
||||||
mod: transferCommunity.moderator,
|
|
||||||
action: action,
|
|
||||||
);
|
|
||||||
|
|
||||||
_ModlogEntry.fromModAddView(
|
|
||||||
ModAddView added,
|
|
||||||
Widget action,
|
|
||||||
) : this(
|
|
||||||
when: added.modAdd.when,
|
|
||||||
mod: added.moderator,
|
|
||||||
action: action,
|
|
||||||
);
|
|
||||||
|
|
||||||
TableRow build(BuildContext context) => TableRow(
|
|
||||||
children: [
|
|
||||||
GestureDetector(
|
|
||||||
onTap: () {
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => Dialog(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(8),
|
|
||||||
child: Center(
|
|
||||||
heightFactor: 1,
|
|
||||||
child: Text(when.toString()),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: Center(child: Text(when.timeagoShort(context))),
|
|
||||||
),
|
|
||||||
GestureDetector(
|
|
||||||
onTap: () => goToUser.byId(
|
|
||||||
context,
|
|
||||||
mod.instanceHost,
|
|
||||||
mod.id,
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Avatar(
|
|
||||||
url: mod.avatar,
|
|
||||||
noBlank: true,
|
|
||||||
radius: 10,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
' ${mod.preferredName}',
|
|
||||||
style:
|
|
||||||
TextStyle(color: Theme.of(context).colorScheme.secondary),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
action,
|
|
||||||
if (reason == null) const Center(child: Text('-')) else Text(reason!),
|
|
||||||
]
|
|
||||||
.map(
|
|
||||||
(widget) => Padding(
|
|
||||||
padding: const EdgeInsets.all(8),
|
|
||||||
child: widget,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -7,6 +7,7 @@ import '../../../hooks/stores.dart';
|
||||||
import '../../../l10n/l10n_from_string.dart';
|
import '../../../l10n/l10n_from_string.dart';
|
||||||
import '../../../stores/accounts_store.dart';
|
import '../../../stores/accounts_store.dart';
|
||||||
import '../../../util/async_store_listener.dart';
|
import '../../../util/async_store_listener.dart';
|
||||||
|
import '../../../util/mobx_provider.dart';
|
||||||
import '../../../util/observer_consumers.dart';
|
import '../../../util/observer_consumers.dart';
|
||||||
import '../../../widgets/pull_to_refresh.dart';
|
import '../../../widgets/pull_to_refresh.dart';
|
||||||
import 'block_dialog.dart';
|
import 'block_dialog.dart';
|
||||||
|
@ -61,7 +62,7 @@ class _UserBlocksWrapper extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Provider<BlocksStore>(
|
return MobxProvider(
|
||||||
create: (context) => BlocksStore(
|
create: (context) => BlocksStore(
|
||||||
instanceHost: instanceHost,
|
instanceHost: instanceHost,
|
||||||
token: context
|
token: context
|
||||||
|
@ -119,8 +120,8 @@ class _UserBlocks extends HookWidget {
|
||||||
)
|
)
|
||||||
] else ...[
|
] else ...[
|
||||||
for (final user in store.blockedUsers!)
|
for (final user in store.blockedUsers!)
|
||||||
Provider(
|
MobxProvider.value(
|
||||||
create: (context) => user,
|
value: user,
|
||||||
key: ValueKey(user),
|
key: ValueKey(user),
|
||||||
child: const BlockPersonTile(),
|
child: const BlockPersonTile(),
|
||||||
),
|
),
|
||||||
|
@ -153,8 +154,8 @@ class _UserBlocks extends HookWidget {
|
||||||
),
|
),
|
||||||
const Divider(),
|
const Divider(),
|
||||||
for (final community in store.blockedCommunities!)
|
for (final community in store.blockedCommunities!)
|
||||||
Provider(
|
MobxProvider.value(
|
||||||
create: (context) => community,
|
value: community,
|
||||||
key: ValueKey(community),
|
key: ValueKey(community),
|
||||||
child: const BlockCommunityTile(),
|
child: const BlockCommunityTile(),
|
||||||
),
|
),
|
||||||
|
|
|
@ -8,16 +8,16 @@ import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
import '../l10n/l10n.dart';
|
import '../l10n/l10n.dart';
|
||||||
import '../util/async_store.dart';
|
import '../util/async_store.dart';
|
||||||
|
import '../util/mobx_provider.dart';
|
||||||
|
|
||||||
part 'config_store.g.dart';
|
part 'config_store.g.dart';
|
||||||
|
|
||||||
/// Store managing user-level configuration such as theme or language
|
/// Store managing user-level configuration such as theme or language
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
@LocaleConverter()
|
@LocaleConverter()
|
||||||
class ConfigStore extends _ConfigStore with _$ConfigStore {
|
class ConfigStore extends _ConfigStore with _$ConfigStore, DisposableStore {
|
||||||
static const _prefsKey = 'v1:ConfigStore';
|
static const _prefsKey = 'v1:ConfigStore';
|
||||||
late final SharedPreferences _sharedPrefs;
|
late final SharedPreferences _sharedPrefs;
|
||||||
late final ReactionDisposer _saveDisposer;
|
|
||||||
|
|
||||||
@visibleForTesting
|
@visibleForTesting
|
||||||
ConfigStore();
|
ConfigStore();
|
||||||
|
@ -28,7 +28,7 @@ class ConfigStore extends _ConfigStore with _$ConfigStore {
|
||||||
as Map<String, dynamic>,
|
as Map<String, dynamic>,
|
||||||
).._sharedPrefs = sharedPrefs;
|
).._sharedPrefs = sharedPrefs;
|
||||||
|
|
||||||
store._saveDisposer = autorun((_) => store.save());
|
store.addReaction(autorun((_) => store.save()));
|
||||||
|
|
||||||
return store;
|
return store;
|
||||||
}
|
}
|
||||||
|
@ -38,10 +38,6 @@ class ConfigStore extends _ConfigStore with _$ConfigStore {
|
||||||
|
|
||||||
await _sharedPrefs.setString(_prefsKey, serialized);
|
await _sharedPrefs.setString(_prefsKey, serialized);
|
||||||
}
|
}
|
||||||
|
|
||||||
void dispose() {
|
|
||||||
_saveDisposer();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class _ConfigStore with Store {
|
abstract class _ConfigStore with Store {
|
||||||
|
|
|
@ -13,7 +13,7 @@ class AsyncStore<T> = _AsyncStore<T> with _$AsyncStore<T>;
|
||||||
|
|
||||||
abstract class _AsyncStore<T> with Store {
|
abstract class _AsyncStore<T> with Store {
|
||||||
@observable
|
@observable
|
||||||
AsyncState<T> asyncState = const AsyncState.initial();
|
AsyncState<T> asyncState = AsyncState<T>.initial();
|
||||||
|
|
||||||
@computed
|
@computed
|
||||||
bool get isLoading => asyncState is AsyncStateLoading<T>;
|
bool get isLoading => asyncState is AsyncStateLoading<T>;
|
||||||
|
@ -41,7 +41,7 @@ abstract class _AsyncStore<T> with Store {
|
||||||
final data = refresh ? asyncState.mapOrNull(data: (data) => data) : null;
|
final data = refresh ? asyncState.mapOrNull(data: (data) => data) : null;
|
||||||
|
|
||||||
if (data == null) {
|
if (data == null) {
|
||||||
asyncState = const AsyncState.loading();
|
asyncState = AsyncState<T>.loading();
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -61,7 +61,7 @@ abstract class _AsyncStore<T> with Store {
|
||||||
if (data != null) {
|
if (data != null) {
|
||||||
asyncState = data.copyWith(errorTerm: err.toString());
|
asyncState = data.copyWith(errorTerm: err.toString());
|
||||||
} else {
|
} else {
|
||||||
asyncState = AsyncState.error(err.toString());
|
asyncState = AsyncState<T>.error(err.toString());
|
||||||
}
|
}
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
|
@ -83,10 +83,24 @@ abstract class _AsyncStore<T> with Store {
|
||||||
if (data != null) {
|
if (data != null) {
|
||||||
asyncState = data.copyWith(errorTerm: err.message);
|
asyncState = data.copyWith(errorTerm: err.message);
|
||||||
} else {
|
} else {
|
||||||
asyncState = AsyncState.error(err.message);
|
asyncState = AsyncState<T>.error(err.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// helper function for mapping [asyncState] into 3 variants
|
||||||
|
U map<U>({
|
||||||
|
required U Function() loading,
|
||||||
|
required U Function(String errorTerm) error,
|
||||||
|
required U Function(T data) data,
|
||||||
|
}) {
|
||||||
|
return asyncState.when<U>(
|
||||||
|
initial: loading,
|
||||||
|
data: (value, errorTerm) => data(value),
|
||||||
|
loading: loading,
|
||||||
|
error: error,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// State in which an async action can be
|
/// State in which an async action can be
|
||||||
|
|
|
@ -20,6 +20,7 @@ class AsyncStoreListener<T> extends SingleChildStatelessWidget {
|
||||||
Widget? child,
|
Widget? child,
|
||||||
}) : super(key: key, child: child);
|
}) : super(key: key, child: child);
|
||||||
|
|
||||||
|
@override
|
||||||
Widget buildWithChild(BuildContext context, Widget? child) {
|
Widget buildWithChild(BuildContext context, Widget? child) {
|
||||||
return ObserverListener<AsyncStore<T>>(
|
return ObserverListener<AsyncStore<T>>(
|
||||||
store: asyncStore,
|
store: asyncStore,
|
||||||
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:mobx/mobx.dart';
|
||||||
|
|
||||||
|
import 'observer_consumers.dart';
|
||||||
|
|
||||||
|
/// Provides a mobx store and disposes it if it implements [DisposableStore]
|
||||||
|
///
|
||||||
|
/// Important: this will not make [context.watch] react to changes
|
||||||
|
class MobxProvider<T extends Store> extends Provider<T> {
|
||||||
|
MobxProvider({
|
||||||
|
Key? key,
|
||||||
|
required Create<T> create,
|
||||||
|
bool? lazy,
|
||||||
|
TransitionBuilder? builder,
|
||||||
|
Widget? child,
|
||||||
|
}) : super(
|
||||||
|
key: key,
|
||||||
|
create: create,
|
||||||
|
dispose: (context, store) {
|
||||||
|
if (store is DisposableStore) store.dispose();
|
||||||
|
},
|
||||||
|
lazy: lazy,
|
||||||
|
builder: builder,
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// will not dispose the store
|
||||||
|
MobxProvider.value({
|
||||||
|
Key? key,
|
||||||
|
required T value,
|
||||||
|
TransitionBuilder? builder,
|
||||||
|
Widget? child,
|
||||||
|
}) : super.value(
|
||||||
|
key: key,
|
||||||
|
builder: builder,
|
||||||
|
value: value,
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// tracks reactions and disposes them in [DisposableStore.dispose]
|
||||||
|
mixin DisposableStore on Store {
|
||||||
|
final List<ReactionDisposer> _disposers = [];
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void addReaction(ReactionDisposer reaction) => _disposers.add(reaction);
|
||||||
|
|
||||||
|
void dispose() {
|
||||||
|
for (final disposer in _disposers) {
|
||||||
|
disposer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -11,6 +11,7 @@ import '../../util/async_store_listener.dart';
|
||||||
import '../../util/extensions/api.dart';
|
import '../../util/extensions/api.dart';
|
||||||
import '../../util/extensions/cake_day.dart';
|
import '../../util/extensions/cake_day.dart';
|
||||||
import '../../util/goto.dart';
|
import '../../util/goto.dart';
|
||||||
|
import '../../util/mobx_provider.dart';
|
||||||
import '../../util/observer_consumers.dart';
|
import '../../util/observer_consumers.dart';
|
||||||
import '../../util/text_color.dart';
|
import '../../util/text_color.dart';
|
||||||
import '../avatar.dart';
|
import '../avatar.dart';
|
||||||
|
@ -78,7 +79,7 @@ class CommentWidget extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Provider(
|
return MobxProvider(
|
||||||
create: (context) => CommentStore(
|
create: (context) => CommentStore(
|
||||||
context.read(),
|
context.read(),
|
||||||
commentTree: commentTree,
|
commentTree: commentTree,
|
||||||
|
|
|
@ -7,6 +7,7 @@ import 'package:provider/provider.dart';
|
||||||
import '../../pages/full_post/full_post.dart';
|
import '../../pages/full_post/full_post.dart';
|
||||||
import '../../util/async_store_listener.dart';
|
import '../../util/async_store_listener.dart';
|
||||||
import '../../util/extensions/api.dart';
|
import '../../util/extensions/api.dart';
|
||||||
|
import '../../util/mobx_provider.dart';
|
||||||
import 'post_actions.dart';
|
import 'post_actions.dart';
|
||||||
import 'post_body.dart';
|
import 'post_body.dart';
|
||||||
import 'post_info_section.dart';
|
import 'post_info_section.dart';
|
||||||
|
@ -28,7 +29,7 @@ class PostTile extends StatelessWidget {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Nested(
|
return Nested(
|
||||||
children: [
|
children: [
|
||||||
Provider.value(value: postStore),
|
MobxProvider.value(value: postStore),
|
||||||
Provider.value(value: fullPost),
|
Provider.value(value: fullPost),
|
||||||
AsyncStoreListener(asyncStore: postStore.savingState),
|
AsyncStoreListener(asyncStore: postStore.savingState),
|
||||||
AsyncStoreListener(asyncStore: postStore.votingState),
|
AsyncStoreListener(asyncStore: postStore.votingState),
|
||||||
|
|
|
@ -35,6 +35,7 @@ Future<void> assertNoStagedGit() async {
|
||||||
class Version {
|
class Version {
|
||||||
final int major, minor, patch, code;
|
final int major, minor, patch, code;
|
||||||
Version(this.major, this.minor, this.patch, this.code);
|
Version(this.major, this.minor, this.patch, this.code);
|
||||||
|
@override
|
||||||
String toString() => '$major.$minor.$patch+$code';
|
String toString() => '$major.$minor.$patch+$code';
|
||||||
String toStringNoCode() => '$major.$minor.$patch';
|
String toStringNoCode() => '$major.$minor.$patch';
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:lemmur/pages/modlog/modlog_page_store.dart';
|
||||||
|
import 'package:lemmur/util/async_store.dart';
|
||||||
|
import 'package:lemmy_api_client/v3.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('ModlogPageStore', () {
|
||||||
|
late ModlogPageStore store;
|
||||||
|
const instanceHost = 'lemmy.ml';
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
store = ModlogPageStore(instanceHost);
|
||||||
|
});
|
||||||
|
tearDown(() {
|
||||||
|
store.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Initial states are correct', () {
|
||||||
|
expect(store.communityId, null);
|
||||||
|
expect(store.instanceHost, instanceHost);
|
||||||
|
expect(store.hasNextPage, true);
|
||||||
|
expect(store.hasPreviousPage, false);
|
||||||
|
expect(store.modlogState.asyncState, const AsyncState<Modlog>.initial());
|
||||||
|
expect(store.page, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Fetches a page when changed', () {
|
||||||
|
store.nextPage();
|
||||||
|
|
||||||
|
expect(store.page, 2);
|
||||||
|
expect(store.hasPreviousPage, true);
|
||||||
|
expect(store.modlogState.asyncState, const AsyncState<Modlog>.loading());
|
||||||
|
|
||||||
|
store.previousPage();
|
||||||
|
|
||||||
|
expect(store.page, 1);
|
||||||
|
expect(store.hasPreviousPage, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Stops listening after disposal', () {
|
||||||
|
store
|
||||||
|
..dispose()
|
||||||
|
..nextPage();
|
||||||
|
|
||||||
|
expect(store.page, 2);
|
||||||
|
expect(store.hasPreviousPage, true);
|
||||||
|
expect(store.modlogState.asyncState, const AsyncState<Modlog>.initial());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
|
@ -98,5 +98,27 @@ void main() {
|
||||||
expect(store.errorTerm, null);
|
expect(store.errorTerm, null);
|
||||||
expect(store.asyncState, AsyncState.data(res2!));
|
expect(store.asyncState, AsyncState.data(res2!));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('maps states correctly', () {
|
||||||
|
final store = AsyncStore<int>();
|
||||||
|
|
||||||
|
loading() => 'loading';
|
||||||
|
data(data) => 'data';
|
||||||
|
error(error) => 'error';
|
||||||
|
|
||||||
|
expect(store.map(loading: loading, error: error, data: data), 'loading');
|
||||||
|
|
||||||
|
store.asyncState = const AsyncState.loading();
|
||||||
|
expect(store.map(loading: loading, error: error, data: data), 'loading');
|
||||||
|
|
||||||
|
store.asyncState = const AsyncState.data(123);
|
||||||
|
expect(store.map(loading: loading, error: error, data: data), 'data');
|
||||||
|
|
||||||
|
store.asyncState = const AsyncState.data(123, 'error');
|
||||||
|
expect(store.map(loading: loading, error: error, data: data), 'data');
|
||||||
|
|
||||||
|
store.asyncState = const AsyncState.error('error');
|
||||||
|
expect(store.map(loading: loading, error: error, data: data), 'error');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue