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:
|
||||
rules:
|
||||
- annotate_overrides
|
||||
- avoid_bool_literals_in_conditional_expressions
|
||||
- avoid_catching_errors
|
||||
- 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 'stores/accounts_store.dart';
|
||||
import 'stores/config_store.dart';
|
||||
import 'util/mobx_provider.dart';
|
||||
|
||||
Future<void> mainCommon(AppConfig appConfig) async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
@ -22,15 +23,14 @@ Future<void> mainCommon(AppConfig appConfig) async {
|
|||
_setupLogger(appConfig, logConsoleStore);
|
||||
_setupTimeago();
|
||||
|
||||
final configStore = ConfigStore.load(sharedPrefs);
|
||||
final accountsStore = await AccountsStore.load();
|
||||
|
||||
runApp(
|
||||
MultiProvider(
|
||||
providers: [
|
||||
Provider.value(value: configStore),
|
||||
MobxProvider(create: (context) => ConfigStore.load(sharedPrefs)),
|
||||
ChangeNotifierProvider.value(value: accountsStore),
|
||||
Provider.value(value: logConsoleStore),
|
||||
MobxProvider.value(value: logConsoleStore),
|
||||
],
|
||||
child: const MyApp(),
|
||||
),
|
||||
|
|
|
@ -10,6 +10,7 @@ import '../../util/async_store.dart';
|
|||
import '../../util/async_store_listener.dart';
|
||||
import '../../util/extensions/api.dart';
|
||||
import '../../util/icons.dart';
|
||||
import '../../util/mobx_provider.dart';
|
||||
import '../../util/observer_consumers.dart';
|
||||
import '../../util/share.dart';
|
||||
import '../../widgets/failed_to_load.dart';
|
||||
|
@ -158,7 +159,7 @@ class CommunityPage extends HookWidget {
|
|||
static Route _route(String instanceHost, CommunityStore store) {
|
||||
return MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return Provider.value(
|
||||
return MobxProvider.value(
|
||||
value: store
|
||||
..refresh(context
|
||||
.read<AccountsStore>()
|
||||
|
|
|
@ -4,13 +4,12 @@ import 'package:lemmy_api_client/v3.dart';
|
|||
import '../../l10n/l10n.dart';
|
||||
import '../../stores/accounts_store.dart';
|
||||
import '../../util/extensions/spaced.dart';
|
||||
import '../../util/goto.dart';
|
||||
import '../../util/observer_consumers.dart';
|
||||
import '../../widgets/bottom_safe.dart';
|
||||
import '../../widgets/markdown_text.dart';
|
||||
import '../../widgets/pull_to_refresh.dart';
|
||||
import '../../widgets/user_tile.dart';
|
||||
import '../modlog_page.dart';
|
||||
import '../modlog/modlog.dart';
|
||||
import 'community_store.dart';
|
||||
|
||||
class CommmunityAboutTab extends StatelessWidget {
|
||||
|
@ -99,9 +98,8 @@ class CommmunityAboutTab extends StatelessWidget {
|
|||
style: Theme.of(context).textTheme.headline6,
|
||||
),
|
||||
),
|
||||
onTap: () => goTo(
|
||||
context,
|
||||
(context) => ModlogPage.forCommunity(
|
||||
onTap: () => Navigator.of(context).push(
|
||||
ModlogPage.forCommunityRoute(
|
||||
instanceHost: community.instanceHost,
|
||||
communityId: community.community.id,
|
||||
communityName: community.community.name,
|
||||
|
|
|
@ -5,6 +5,7 @@ import 'package:url_launcher/url_launcher.dart' as ul;
|
|||
|
||||
import '../../hooks/logged_in_action.dart';
|
||||
import '../../util/extensions/api.dart';
|
||||
import '../../util/mobx_provider.dart';
|
||||
import '../../util/observer_consumers.dart';
|
||||
import '../../widgets/bottom_modal.dart';
|
||||
import '../../widgets/info_table_popup.dart';
|
||||
|
@ -60,9 +61,10 @@ class CommunityMoreMenu extends HookWidget {
|
|||
|
||||
static void open(BuildContext context, FullCommunityView fullCommunityView) {
|
||||
final store = context.read<CommunityStore>();
|
||||
|
||||
showBottomModal(
|
||||
context: context,
|
||||
builder: (context) => Provider.value(
|
||||
builder: (context) => MobxProvider.value(
|
||||
value: store,
|
||||
child: CommunityMoreMenu(
|
||||
fullCommunityView: fullCommunityView,
|
||||
|
|
|
@ -11,6 +11,7 @@ import '../../stores/accounts_store.dart';
|
|||
import '../../util/async_store_listener.dart';
|
||||
import '../../util/extensions/api.dart';
|
||||
import '../../util/icons.dart';
|
||||
import '../../util/mobx_provider.dart';
|
||||
import '../../util/observer_consumers.dart';
|
||||
import '../../util/share.dart';
|
||||
import '../../widgets/failed_to_load.dart';
|
||||
|
@ -100,7 +101,7 @@ class FullPostPage extends HookWidget {
|
|||
),
|
||||
actions: [
|
||||
IconButton(icon: Icon(shareIcon), onPressed: sharePost),
|
||||
Provider.value(
|
||||
MobxProvider.value(
|
||||
value: postStore,
|
||||
child: const SavePostButton(),
|
||||
),
|
||||
|
@ -143,7 +144,7 @@ class FullPostPage extends HookWidget {
|
|||
}
|
||||
|
||||
static Route route(int id, String instanceHost) => MaterialPageRoute(
|
||||
builder: (context) => Provider(
|
||||
builder: (context) => MobxProvider(
|
||||
create: (context) =>
|
||||
FullPostStore(instanceHost: instanceHost, postId: id)
|
||||
..refresh(_tryGetJwt(context, instanceHost)),
|
||||
|
@ -152,14 +153,14 @@ class FullPostPage extends HookWidget {
|
|||
);
|
||||
|
||||
static Route fromPostViewRoute(PostView postView) => MaterialPageRoute(
|
||||
builder: (context) => Provider(
|
||||
builder: (context) => MobxProvider(
|
||||
create: (context) => FullPostStore.fromPostView(postView)
|
||||
..refresh(_tryGetJwt(context, postView.instanceHost)),
|
||||
child: const FullPostPage._(),
|
||||
),
|
||||
);
|
||||
static Route fromPostStoreRoute(PostStore postStore) => MaterialPageRoute(
|
||||
builder: (context) => Provider(
|
||||
builder: (context) => MobxProvider(
|
||||
create: (context) => FullPostStore.fromPostStore(postStore)
|
||||
..refresh(_tryGetJwt(context, postStore.postView.instanceHost)),
|
||||
child: const FullPostPage._(),
|
||||
|
|
|
@ -342,6 +342,7 @@ class _SelectedList {
|
|||
this.instanceHost,
|
||||
});
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
'SelectedList(instanceHost: $instanceHost, listingType: $listingType)';
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ import '../widgets/reveal_after_scroll.dart';
|
|||
import '../widgets/sortable_infinite_list.dart';
|
||||
import '../widgets/user_tile.dart';
|
||||
import 'communities_list.dart';
|
||||
import 'modlog_page.dart';
|
||||
import 'modlog/modlog.dart';
|
||||
import 'users_list.dart';
|
||||
|
||||
/// Displays posts, comments, and general info about the given instance
|
||||
|
@ -365,9 +365,8 @@ class _AboutTab extends HookWidget {
|
|||
),
|
||||
ListTile(
|
||||
title: Center(child: Text(L10n.of(context).modlog)),
|
||||
onTap: () => goTo(
|
||||
context,
|
||||
(context) => ModlogPage.forInstance(instanceHost: instanceHost),
|
||||
onTap: () => Navigator.of(context).push(
|
||||
ModlogPage.forInstanceRoute(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 '../../../stores/accounts_store.dart';
|
||||
import '../../../util/async_store_listener.dart';
|
||||
import '../../../util/mobx_provider.dart';
|
||||
import '../../../util/observer_consumers.dart';
|
||||
import '../../../widgets/pull_to_refresh.dart';
|
||||
import 'block_dialog.dart';
|
||||
|
@ -61,7 +62,7 @@ class _UserBlocksWrapper extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Provider<BlocksStore>(
|
||||
return MobxProvider(
|
||||
create: (context) => BlocksStore(
|
||||
instanceHost: instanceHost,
|
||||
token: context
|
||||
|
@ -119,8 +120,8 @@ class _UserBlocks extends HookWidget {
|
|||
)
|
||||
] else ...[
|
||||
for (final user in store.blockedUsers!)
|
||||
Provider(
|
||||
create: (context) => user,
|
||||
MobxProvider.value(
|
||||
value: user,
|
||||
key: ValueKey(user),
|
||||
child: const BlockPersonTile(),
|
||||
),
|
||||
|
@ -153,8 +154,8 @@ class _UserBlocks extends HookWidget {
|
|||
),
|
||||
const Divider(),
|
||||
for (final community in store.blockedCommunities!)
|
||||
Provider(
|
||||
create: (context) => community,
|
||||
MobxProvider.value(
|
||||
value: community,
|
||||
key: ValueKey(community),
|
||||
child: const BlockCommunityTile(),
|
||||
),
|
||||
|
|
|
@ -8,16 +8,16 @@ import 'package:shared_preferences/shared_preferences.dart';
|
|||
|
||||
import '../l10n/l10n.dart';
|
||||
import '../util/async_store.dart';
|
||||
import '../util/mobx_provider.dart';
|
||||
|
||||
part 'config_store.g.dart';
|
||||
|
||||
/// Store managing user-level configuration such as theme or language
|
||||
@JsonSerializable()
|
||||
@LocaleConverter()
|
||||
class ConfigStore extends _ConfigStore with _$ConfigStore {
|
||||
class ConfigStore extends _ConfigStore with _$ConfigStore, DisposableStore {
|
||||
static const _prefsKey = 'v1:ConfigStore';
|
||||
late final SharedPreferences _sharedPrefs;
|
||||
late final ReactionDisposer _saveDisposer;
|
||||
|
||||
@visibleForTesting
|
||||
ConfigStore();
|
||||
|
@ -28,7 +28,7 @@ class ConfigStore extends _ConfigStore with _$ConfigStore {
|
|||
as Map<String, dynamic>,
|
||||
).._sharedPrefs = sharedPrefs;
|
||||
|
||||
store._saveDisposer = autorun((_) => store.save());
|
||||
store.addReaction(autorun((_) => store.save()));
|
||||
|
||||
return store;
|
||||
}
|
||||
|
@ -38,10 +38,6 @@ class ConfigStore extends _ConfigStore with _$ConfigStore {
|
|||
|
||||
await _sharedPrefs.setString(_prefsKey, serialized);
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
_saveDisposer();
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _ConfigStore with Store {
|
||||
|
|
|
@ -13,7 +13,7 @@ class AsyncStore<T> = _AsyncStore<T> with _$AsyncStore<T>;
|
|||
|
||||
abstract class _AsyncStore<T> with Store {
|
||||
@observable
|
||||
AsyncState<T> asyncState = const AsyncState.initial();
|
||||
AsyncState<T> asyncState = AsyncState<T>.initial();
|
||||
|
||||
@computed
|
||||
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;
|
||||
|
||||
if (data == null) {
|
||||
asyncState = const AsyncState.loading();
|
||||
asyncState = AsyncState<T>.loading();
|
||||
}
|
||||
|
||||
try {
|
||||
|
@ -61,7 +61,7 @@ abstract class _AsyncStore<T> with Store {
|
|||
if (data != null) {
|
||||
asyncState = data.copyWith(errorTerm: err.toString());
|
||||
} else {
|
||||
asyncState = AsyncState.error(err.toString());
|
||||
asyncState = AsyncState<T>.error(err.toString());
|
||||
}
|
||||
rethrow;
|
||||
}
|
||||
|
@ -83,10 +83,24 @@ abstract class _AsyncStore<T> with Store {
|
|||
if (data != null) {
|
||||
asyncState = data.copyWith(errorTerm: err.message);
|
||||
} 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
|
||||
|
|
|
@ -20,6 +20,7 @@ class AsyncStoreListener<T> extends SingleChildStatelessWidget {
|
|||
Widget? child,
|
||||
}) : super(key: key, child: child);
|
||||
|
||||
@override
|
||||
Widget buildWithChild(BuildContext context, Widget? child) {
|
||||
return ObserverListener<AsyncStore<T>>(
|
||||
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/cake_day.dart';
|
||||
import '../../util/goto.dart';
|
||||
import '../../util/mobx_provider.dart';
|
||||
import '../../util/observer_consumers.dart';
|
||||
import '../../util/text_color.dart';
|
||||
import '../avatar.dart';
|
||||
|
@ -78,7 +79,7 @@ class CommentWidget extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Provider(
|
||||
return MobxProvider(
|
||||
create: (context) => CommentStore(
|
||||
context.read(),
|
||||
commentTree: commentTree,
|
||||
|
|
|
@ -7,6 +7,7 @@ import 'package:provider/provider.dart';
|
|||
import '../../pages/full_post/full_post.dart';
|
||||
import '../../util/async_store_listener.dart';
|
||||
import '../../util/extensions/api.dart';
|
||||
import '../../util/mobx_provider.dart';
|
||||
import 'post_actions.dart';
|
||||
import 'post_body.dart';
|
||||
import 'post_info_section.dart';
|
||||
|
@ -28,7 +29,7 @@ class PostTile extends StatelessWidget {
|
|||
Widget build(BuildContext context) {
|
||||
return Nested(
|
||||
children: [
|
||||
Provider.value(value: postStore),
|
||||
MobxProvider.value(value: postStore),
|
||||
Provider.value(value: fullPost),
|
||||
AsyncStoreListener(asyncStore: postStore.savingState),
|
||||
AsyncStoreListener(asyncStore: postStore.votingState),
|
||||
|
|
|
@ -35,6 +35,7 @@ Future<void> assertNoStagedGit() async {
|
|||
class Version {
|
||||
final int major, minor, patch, code;
|
||||
Version(this.major, this.minor, this.patch, this.code);
|
||||
@override
|
||||
String toString() => '$major.$minor.$patch+$code';
|
||||
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.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