HECKIN CHONKER (null safety migration without scripts)

This commit is contained in:
krawieck 2021-04-09 00:11:44 +02:00
parent 9d90aa9aeb
commit 83235534f5
61 changed files with 894 additions and 879 deletions

View File

@ -36,8 +36,6 @@ extension on CommentSortType {
return (b, a) => return (b, a) =>
a.comment.counts.score.compareTo(b.comment.counts.score); a.comment.counts.score.compareTo(b.comment.counts.score);
} }
throw Exception('unreachable');
} }
} }
@ -45,9 +43,7 @@ class CommentTree {
CommentView comment; CommentView comment;
List<CommentTree> children; List<CommentTree> children;
CommentTree(this.comment, [this.children]) { CommentTree(this.comment, [this.children = const []]);
children ??= [];
}
/// takes raw linear comments and turns them into a CommentTree /// takes raw linear comments and turns them into a CommentTree
static List<CommentTree> fromList(List<CommentView> comments) { static List<CommentTree> fromList(List<CommentView> comments) {

View File

@ -18,20 +18,20 @@ class AssetGenImage extends AssetImage {
final String _assetName; final String _assetName;
Image image({ Image image({
Key key, Key? key,
ImageFrameBuilder frameBuilder, ImageFrameBuilder? frameBuilder,
ImageLoadingBuilder loadingBuilder, ImageLoadingBuilder? loadingBuilder,
ImageErrorWidgetBuilder errorBuilder, ImageErrorWidgetBuilder? errorBuilder,
String semanticLabel, String? semanticLabel,
bool excludeFromSemantics = false, bool excludeFromSemantics = false,
double width, double? width,
double height, double? height,
Color color, Color? color,
BlendMode colorBlendMode, BlendMode? colorBlendMode,
BoxFit fit, BoxFit? fit,
AlignmentGeometry alignment = Alignment.center, AlignmentGeometry alignment = Alignment.center,
ImageRepeat repeat = ImageRepeat.noRepeat, ImageRepeat repeat = ImageRepeat.noRepeat,
Rect centerSlice, Rect? centerSlice,
bool matchTextDirection = false, bool matchTextDirection = false,
bool gaplessPlayback = false, bool gaplessPlayback = false,
bool isAntiAlias = false, bool isAntiAlias = false,

View File

@ -10,8 +10,8 @@ class Debounce {
final VoidCallback callback; final VoidCallback callback;
const Debounce({ const Debounce({
@required this.loading, required this.loading,
@required this.callback, required this.callback,
}); });
void call() => callback(); void call() => callback();
@ -24,7 +24,7 @@ Debounce useDebounce(
Duration delayDuration = const Duration(seconds: 1), Duration delayDuration = const Duration(seconds: 1),
]) { ]) {
final loading = useState(false); final loading = useState(false);
final timerHandle = useRef<Timer>(null); final timerHandle = useRef<Timer?>(null);
cancel() { cancel() {
timerHandle.current?.cancel(); timerHandle.current?.cancel();

View File

@ -12,10 +12,10 @@ class DelayedLoading {
final VoidCallback cancel; final VoidCallback cancel;
const DelayedLoading({ const DelayedLoading({
@required this.pending, required this.pending,
@required this.loading, required this.loading,
@required this.start, required this.start,
@required this.cancel, required this.cancel,
}); });
} }
@ -26,7 +26,7 @@ DelayedLoading useDelayedLoading(
[Duration delayDuration = const Duration(milliseconds: 500)]) { [Duration delayDuration = const Duration(milliseconds: 500)]) {
final loading = useState(false); final loading = useState(false);
final pending = useState(false); final pending = useState(false);
final timerHandle = useRef<Timer>(null); final timerHandle = useRef<Timer?>(null);
return DelayedLoading( return DelayedLoading(
loading: loading.value, loading: loading.value,

View File

@ -2,10 +2,5 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import '../widgets/infinite_scroll.dart'; import '../widgets/infinite_scroll.dart';
InfiniteScrollController useInfiniteScrollController() { InfiniteScrollController useInfiniteScrollController() =>
final controller = useMemoized(() => InfiniteScrollController()); useMemoized(() => InfiniteScrollController());
useEffect(() => controller.dispose, []);
return controller;
}

View File

@ -14,14 +14,13 @@ import 'stores.dart';
VoidCallback Function( VoidCallback Function(
void Function(Jwt token) action, [ void Function(Jwt token) action, [
String message, String? message,
]) useLoggedInAction(String instanceHost, {bool any = false}) { ]) useAnyLoggedInAction() {
final context = useContext(); final context = useContext();
final store = useAccountsStore(); final store = useAccountsStore();
return (action, [message]) { return (action, [message]) {
if (any && store.hasNoAccount || if (store.hasNoAccount) {
!any && store.isAnonymousFor(instanceHost)) {
return () { return () {
ScaffoldMessenger.of(context).showSnackBar(SnackBar( ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(message ?? 'you have to be logged in to do that'), content: Text(message ?? 'you have to be logged in to do that'),
@ -31,7 +30,29 @@ VoidCallback Function(
)); ));
}; };
} }
final token = store.defaultTokenFor(instanceHost); return () => action(store.defaultToken!);
};
}
VoidCallback Function(
void Function(Jwt token) action, [
String? message,
]) useLoggedInAction(String instanceHost) {
final context = useContext();
final store = useAccountsStore();
return (action, [message]) {
if (store.isAnonymousFor(instanceHost)) {
return () {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(message ?? 'you have to be logged in to do that'),
action: SnackBarAction(
label: 'log in',
onPressed: () => goTo(context, (_) => AccountsConfigPage())),
));
};
}
final token = store.defaultTokenFor(instanceHost)!;
return () => action(token); return () => action(token);
}; };
} }

View File

@ -3,7 +3,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
/// creates an [AsyncSnapshot] from the Future returned from the valueBuilder. /// creates an [AsyncSnapshot] from the Future returned from the valueBuilder.
/// [keys] can be used to rebuild the Future /// [keys] can be used to rebuild the Future
AsyncSnapshot<T> useMemoFuture<T>(Future<T> Function() valueBuilder, AsyncSnapshot<T?> useMemoFuture<T>(Future<T> Function() valueBuilder,
[List<Object> keys = const <dynamic>[]]) => [List<Object> keys = const <Object>[]]) =>
useFuture(useMemoized<Future<T>>(valueBuilder, keys), useFuture(useMemoized<Future<T>>(valueBuilder, keys),
preserveState: false, initialData: null); preserveState: false, initialData: null);

View File

@ -5,9 +5,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'memo_future.dart'; import 'memo_future.dart';
class Refreshable<T> { class Refreshable<T> {
const Refreshable({@required this.snapshot, @required this.refresh}) const Refreshable({required this.snapshot, required this.refresh});
: assert(snapshot != null),
assert(refresh != null);
final AsyncSnapshot<T> snapshot; final AsyncSnapshot<T> snapshot;
final AsyncCallback refresh; final AsyncCallback refresh;
@ -20,9 +18,9 @@ class Refreshable<T> {
/// ///
/// `keys` will re-run the initial fetching thus yielding a /// `keys` will re-run the initial fetching thus yielding a
/// loading state in the AsyncSnapshot /// loading state in the AsyncSnapshot
Refreshable<T> useRefreshable<T>(AsyncValueGetter<T> fetcher, Refreshable<T?> useRefreshable<T>(AsyncValueGetter<T> fetcher,
[List<Object> keys = const <dynamic>[]]) { [List<Object> keys = const <Object>[]]) {
final newData = useState<T>(null); final newData = useState<T?>(null);
final snapshot = useMemoFuture(() async { final snapshot = useMemoFuture(() async {
newData.value = null; newData.value = null;
return fetcher(); return fetcher();

View File

@ -6,7 +6,7 @@ export 'l10n_api.dart';
export 'l10n_from_string.dart'; export 'l10n_from_string.dart';
abstract class LocaleSerde { abstract class LocaleSerde {
static Locale fromJson(String json) { static Locale? fromJson(String? json) {
if (json == null) return null; if (json == null) return null;
final lang = json.split('-'); final lang = json.split('-');

View File

@ -6,25 +6,25 @@ extension SortTypeL10n on SortType {
String tr(BuildContext context) { String tr(BuildContext context) {
switch (this) { switch (this) {
case SortType.hot: case SortType.hot:
return L10n.of(context).hot; return L10n.of(context)!.hot;
case SortType.new_: case SortType.new_:
return L10n.of(context).new_; return L10n.of(context)!.new_;
case SortType.topYear: case SortType.topYear:
return L10n.of(context).top_year; return L10n.of(context)!.top_year;
case SortType.topMonth: case SortType.topMonth:
return L10n.of(context).top_month; return L10n.of(context)!.top_month;
case SortType.topWeek: case SortType.topWeek:
return L10n.of(context).top_week; return L10n.of(context)!.top_week;
case SortType.topDay: case SortType.topDay:
return L10n.of(context).top_day; return L10n.of(context)!.top_day;
case SortType.topAll: case SortType.topAll:
return L10n.of(context).top_all; return L10n.of(context)!.top_all;
case SortType.newComments: case SortType.newComments:
return L10n.of(context).new_comments; return L10n.of(context)!.new_comments;
case SortType.active: case SortType.active:
return L10n.of(context).active; return L10n.of(context)!.active;
case SortType.mostComments: case SortType.mostComments:
return L10n.of(context).most_comments; return L10n.of(context)!.most_comments;
default: default:
throw Exception('unreachable'); throw Exception('unreachable');
} }
@ -35,13 +35,13 @@ extension PostListingTypeL10n on PostListingType {
String tr(BuildContext context) { String tr(BuildContext context) {
switch (this) { switch (this) {
case PostListingType.all: case PostListingType.all:
return L10n.of(context).all; return L10n.of(context)!.all;
case PostListingType.community: case PostListingType.community:
return L10n.of(context).community; return L10n.of(context)!.community;
case PostListingType.local: case PostListingType.local:
return L10n.of(context).local; return L10n.of(context)!.local;
case PostListingType.subscribed: case PostListingType.subscribed:
return L10n.of(context).subscribed; return L10n.of(context)!.subscribed;
default: default:
throw Exception('unreachable'); throw Exception('unreachable');
} }
@ -52,17 +52,17 @@ extension SearchTypeL10n on SearchType {
String tr(BuildContext context) { String tr(BuildContext context) {
switch (this) { switch (this) {
case SearchType.all: case SearchType.all:
return L10n.of(context).all; return L10n.of(context)!.all;
case SearchType.comments: case SearchType.comments:
return L10n.of(context).comments; return L10n.of(context)!.comments;
case SearchType.communities: case SearchType.communities:
return L10n.of(context).communities; return L10n.of(context)!.communities;
case SearchType.posts: case SearchType.posts:
return L10n.of(context).posts; return L10n.of(context)!.posts;
case SearchType.url: case SearchType.url:
return L10n.of(context).url; return L10n.of(context)!.url;
case SearchType.users: case SearchType.users:
return L10n.of(context).users; return L10n.of(context)!.users;
default: default:
throw Exception('unreachable'); throw Exception('unreachable');
} }

View File

@ -147,255 +147,255 @@ extension L10nFromString on String {
String tr(BuildContext context) { String tr(BuildContext context) {
switch (this) { switch (this) {
case L10nStrings.settings: case L10nStrings.settings:
return L10n.of(context).settings; return L10n.of(context)!.settings;
case L10nStrings.password: case L10nStrings.password:
return L10n.of(context).password; return L10n.of(context)!.password;
case L10nStrings.email_or_username: case L10nStrings.email_or_username:
return L10n.of(context).email_or_username; return L10n.of(context)!.email_or_username;
case L10nStrings.posts: case L10nStrings.posts:
return L10n.of(context).posts; return L10n.of(context)!.posts;
case L10nStrings.comments: case L10nStrings.comments:
return L10n.of(context).comments; return L10n.of(context)!.comments;
case L10nStrings.modlog: case L10nStrings.modlog:
return L10n.of(context).modlog; return L10n.of(context)!.modlog;
case L10nStrings.community: case L10nStrings.community:
return L10n.of(context).community; return L10n.of(context)!.community;
case L10nStrings.url: case L10nStrings.url:
return L10n.of(context).url; return L10n.of(context)!.url;
case L10nStrings.title: case L10nStrings.title:
return L10n.of(context).title; return L10n.of(context)!.title;
case L10nStrings.body: case L10nStrings.body:
return L10n.of(context).body; return L10n.of(context)!.body;
case L10nStrings.nsfw: case L10nStrings.nsfw:
return L10n.of(context).nsfw; return L10n.of(context)!.nsfw;
case L10nStrings.post: case L10nStrings.post:
return L10n.of(context).post; return L10n.of(context)!.post;
case L10nStrings.save: case L10nStrings.save:
return L10n.of(context).save; return L10n.of(context)!.save;
case L10nStrings.subscribed: case L10nStrings.subscribed:
return L10n.of(context).subscribed; return L10n.of(context)!.subscribed;
case L10nStrings.local: case L10nStrings.local:
return L10n.of(context).local; return L10n.of(context)!.local;
case L10nStrings.all: case L10nStrings.all:
return L10n.of(context).all; return L10n.of(context)!.all;
case L10nStrings.replies: case L10nStrings.replies:
return L10n.of(context).replies; return L10n.of(context)!.replies;
case L10nStrings.mentions: case L10nStrings.mentions:
return L10n.of(context).mentions; return L10n.of(context)!.mentions;
case L10nStrings.from: case L10nStrings.from:
return L10n.of(context).from; return L10n.of(context)!.from;
case L10nStrings.to: case L10nStrings.to:
return L10n.of(context).to; return L10n.of(context)!.to;
case L10nStrings.deleted_by_creator: case L10nStrings.deleted_by_creator:
return L10n.of(context).deleted_by_creator; return L10n.of(context)!.deleted_by_creator;
case L10nStrings.more: case L10nStrings.more:
return L10n.of(context).more; return L10n.of(context)!.more;
case L10nStrings.mark_as_read: case L10nStrings.mark_as_read:
return L10n.of(context).mark_as_read; return L10n.of(context)!.mark_as_read;
case L10nStrings.mark_as_unread: case L10nStrings.mark_as_unread:
return L10n.of(context).mark_as_unread; return L10n.of(context)!.mark_as_unread;
case L10nStrings.reply: case L10nStrings.reply:
return L10n.of(context).reply; return L10n.of(context)!.reply;
case L10nStrings.edit: case L10nStrings.edit:
return L10n.of(context).edit; return L10n.of(context)!.edit;
case L10nStrings.delete: case L10nStrings.delete:
return L10n.of(context).delete; return L10n.of(context)!.delete;
case L10nStrings.restore: case L10nStrings.restore:
return L10n.of(context).restore; return L10n.of(context)!.restore;
case L10nStrings.yes: case L10nStrings.yes:
return L10n.of(context).yes; return L10n.of(context)!.yes;
case L10nStrings.no: case L10nStrings.no:
return L10n.of(context).no; return L10n.of(context)!.no;
case L10nStrings.avatar: case L10nStrings.avatar:
return L10n.of(context).avatar; return L10n.of(context)!.avatar;
case L10nStrings.banner: case L10nStrings.banner:
return L10n.of(context).banner; return L10n.of(context)!.banner;
case L10nStrings.display_name: case L10nStrings.display_name:
return L10n.of(context).display_name; return L10n.of(context)!.display_name;
case L10nStrings.bio: case L10nStrings.bio:
return L10n.of(context).bio; return L10n.of(context)!.bio;
case L10nStrings.email: case L10nStrings.email:
return L10n.of(context).email; return L10n.of(context)!.email;
case L10nStrings.matrix_user: case L10nStrings.matrix_user:
return L10n.of(context).matrix_user; return L10n.of(context)!.matrix_user;
case L10nStrings.sort_type: case L10nStrings.sort_type:
return L10n.of(context).sort_type; return L10n.of(context)!.sort_type;
case L10nStrings.type: case L10nStrings.type:
return L10n.of(context).type; return L10n.of(context)!.type;
case L10nStrings.show_nsfw: case L10nStrings.show_nsfw:
return L10n.of(context).show_nsfw; return L10n.of(context)!.show_nsfw;
case L10nStrings.send_notifications_to_email: case L10nStrings.send_notifications_to_email:
return L10n.of(context).send_notifications_to_email; return L10n.of(context)!.send_notifications_to_email;
case L10nStrings.delete_account: case L10nStrings.delete_account:
return L10n.of(context).delete_account; return L10n.of(context)!.delete_account;
case L10nStrings.saved: case L10nStrings.saved:
return L10n.of(context).saved; return L10n.of(context)!.saved;
case L10nStrings.communities: case L10nStrings.communities:
return L10n.of(context).communities; return L10n.of(context)!.communities;
case L10nStrings.users: case L10nStrings.users:
return L10n.of(context).users; return L10n.of(context)!.users;
case L10nStrings.theme: case L10nStrings.theme:
return L10n.of(context).theme; return L10n.of(context)!.theme;
case L10nStrings.language: case L10nStrings.language:
return L10n.of(context).language; return L10n.of(context)!.language;
case L10nStrings.hot: case L10nStrings.hot:
return L10n.of(context).hot; return L10n.of(context)!.hot;
case L10nStrings.new_: case L10nStrings.new_:
return L10n.of(context).new_; return L10n.of(context)!.new_;
case L10nStrings.old: case L10nStrings.old:
return L10n.of(context).old; return L10n.of(context)!.old;
case L10nStrings.top: case L10nStrings.top:
return L10n.of(context).top; return L10n.of(context)!.top;
case L10nStrings.chat: case L10nStrings.chat:
return L10n.of(context).chat; return L10n.of(context)!.chat;
case L10nStrings.admin: case L10nStrings.admin:
return L10n.of(context).admin; return L10n.of(context)!.admin;
case L10nStrings.by: case L10nStrings.by:
return L10n.of(context).by; return L10n.of(context)!.by;
case L10nStrings.not_a_mod_or_admin: case L10nStrings.not_a_mod_or_admin:
return L10n.of(context).not_a_mod_or_admin; return L10n.of(context)!.not_a_mod_or_admin;
case L10nStrings.not_an_admin: case L10nStrings.not_an_admin:
return L10n.of(context).not_an_admin; return L10n.of(context)!.not_an_admin;
case L10nStrings.couldnt_find_post: case L10nStrings.couldnt_find_post:
return L10n.of(context).couldnt_find_post; return L10n.of(context)!.couldnt_find_post;
case L10nStrings.not_logged_in: case L10nStrings.not_logged_in:
return L10n.of(context).not_logged_in; return L10n.of(context)!.not_logged_in;
case L10nStrings.site_ban: case L10nStrings.site_ban:
return L10n.of(context).site_ban; return L10n.of(context)!.site_ban;
case L10nStrings.community_ban: case L10nStrings.community_ban:
return L10n.of(context).community_ban; return L10n.of(context)!.community_ban;
case L10nStrings.downvotes_disabled: case L10nStrings.downvotes_disabled:
return L10n.of(context).downvotes_disabled; return L10n.of(context)!.downvotes_disabled;
case L10nStrings.invalid_url: case L10nStrings.invalid_url:
return L10n.of(context).invalid_url; return L10n.of(context)!.invalid_url;
case L10nStrings.locked: case L10nStrings.locked:
return L10n.of(context).locked; return L10n.of(context)!.locked;
case L10nStrings.couldnt_create_comment: case L10nStrings.couldnt_create_comment:
return L10n.of(context).couldnt_create_comment; return L10n.of(context)!.couldnt_create_comment;
case L10nStrings.couldnt_like_comment: case L10nStrings.couldnt_like_comment:
return L10n.of(context).couldnt_like_comment; return L10n.of(context)!.couldnt_like_comment;
case L10nStrings.couldnt_update_comment: case L10nStrings.couldnt_update_comment:
return L10n.of(context).couldnt_update_comment; return L10n.of(context)!.couldnt_update_comment;
case L10nStrings.no_comment_edit_allowed: case L10nStrings.no_comment_edit_allowed:
return L10n.of(context).no_comment_edit_allowed; return L10n.of(context)!.no_comment_edit_allowed;
case L10nStrings.couldnt_save_comment: case L10nStrings.couldnt_save_comment:
return L10n.of(context).couldnt_save_comment; return L10n.of(context)!.couldnt_save_comment;
case L10nStrings.couldnt_get_comments: case L10nStrings.couldnt_get_comments:
return L10n.of(context).couldnt_get_comments; return L10n.of(context)!.couldnt_get_comments;
case L10nStrings.report_reason_required: case L10nStrings.report_reason_required:
return L10n.of(context).report_reason_required; return L10n.of(context)!.report_reason_required;
case L10nStrings.report_too_long: case L10nStrings.report_too_long:
return L10n.of(context).report_too_long; return L10n.of(context)!.report_too_long;
case L10nStrings.couldnt_create_report: case L10nStrings.couldnt_create_report:
return L10n.of(context).couldnt_create_report; return L10n.of(context)!.couldnt_create_report;
case L10nStrings.couldnt_resolve_report: case L10nStrings.couldnt_resolve_report:
return L10n.of(context).couldnt_resolve_report; return L10n.of(context)!.couldnt_resolve_report;
case L10nStrings.invalid_post_title: case L10nStrings.invalid_post_title:
return L10n.of(context).invalid_post_title; return L10n.of(context)!.invalid_post_title;
case L10nStrings.couldnt_create_post: case L10nStrings.couldnt_create_post:
return L10n.of(context).couldnt_create_post; return L10n.of(context)!.couldnt_create_post;
case L10nStrings.couldnt_like_post: case L10nStrings.couldnt_like_post:
return L10n.of(context).couldnt_like_post; return L10n.of(context)!.couldnt_like_post;
case L10nStrings.couldnt_find_community: case L10nStrings.couldnt_find_community:
return L10n.of(context).couldnt_find_community; return L10n.of(context)!.couldnt_find_community;
case L10nStrings.couldnt_get_posts: case L10nStrings.couldnt_get_posts:
return L10n.of(context).couldnt_get_posts; return L10n.of(context)!.couldnt_get_posts;
case L10nStrings.no_post_edit_allowed: case L10nStrings.no_post_edit_allowed:
return L10n.of(context).no_post_edit_allowed; return L10n.of(context)!.no_post_edit_allowed;
case L10nStrings.couldnt_save_post: case L10nStrings.couldnt_save_post:
return L10n.of(context).couldnt_save_post; return L10n.of(context)!.couldnt_save_post;
case L10nStrings.site_already_exists: case L10nStrings.site_already_exists:
return L10n.of(context).site_already_exists; return L10n.of(context)!.site_already_exists;
case L10nStrings.couldnt_update_site: case L10nStrings.couldnt_update_site:
return L10n.of(context).couldnt_update_site; return L10n.of(context)!.couldnt_update_site;
case L10nStrings.invalid_community_name: case L10nStrings.invalid_community_name:
return L10n.of(context).invalid_community_name; return L10n.of(context)!.invalid_community_name;
case L10nStrings.community_already_exists: case L10nStrings.community_already_exists:
return L10n.of(context).community_already_exists; return L10n.of(context)!.community_already_exists;
case L10nStrings.community_moderator_already_exists: case L10nStrings.community_moderator_already_exists:
return L10n.of(context).community_moderator_already_exists; return L10n.of(context)!.community_moderator_already_exists;
case L10nStrings.community_follower_already_exists: case L10nStrings.community_follower_already_exists:
return L10n.of(context).community_follower_already_exists; return L10n.of(context)!.community_follower_already_exists;
case L10nStrings.not_a_moderator: case L10nStrings.not_a_moderator:
return L10n.of(context).not_a_moderator; return L10n.of(context)!.not_a_moderator;
case L10nStrings.couldnt_update_community: case L10nStrings.couldnt_update_community:
return L10n.of(context).couldnt_update_community; return L10n.of(context)!.couldnt_update_community;
case L10nStrings.no_community_edit_allowed: case L10nStrings.no_community_edit_allowed:
return L10n.of(context).no_community_edit_allowed; return L10n.of(context)!.no_community_edit_allowed;
case L10nStrings.system_err_login: case L10nStrings.system_err_login:
return L10n.of(context).system_err_login; return L10n.of(context)!.system_err_login;
case L10nStrings.community_user_already_banned: case L10nStrings.community_user_already_banned:
return L10n.of(context).community_user_already_banned; return L10n.of(context)!.community_user_already_banned;
case L10nStrings.couldnt_find_that_username_or_email: case L10nStrings.couldnt_find_that_username_or_email:
return L10n.of(context).couldnt_find_that_username_or_email; return L10n.of(context)!.couldnt_find_that_username_or_email;
case L10nStrings.password_incorrect: case L10nStrings.password_incorrect:
return L10n.of(context).password_incorrect; return L10n.of(context)!.password_incorrect;
case L10nStrings.registration_closed: case L10nStrings.registration_closed:
return L10n.of(context).registration_closed; return L10n.of(context)!.registration_closed;
case L10nStrings.invalid_password: case L10nStrings.invalid_password:
return L10n.of(context).invalid_password; return L10n.of(context)!.invalid_password;
case L10nStrings.passwords_dont_match: case L10nStrings.passwords_dont_match:
return L10n.of(context).passwords_dont_match; return L10n.of(context)!.passwords_dont_match;
case L10nStrings.captcha_incorrect: case L10nStrings.captcha_incorrect:
return L10n.of(context).captcha_incorrect; return L10n.of(context)!.captcha_incorrect;
case L10nStrings.invalid_username: case L10nStrings.invalid_username:
return L10n.of(context).invalid_username; return L10n.of(context)!.invalid_username;
case L10nStrings.bio_length_overflow: case L10nStrings.bio_length_overflow:
return L10n.of(context).bio_length_overflow; return L10n.of(context)!.bio_length_overflow;
case L10nStrings.couldnt_update_user: case L10nStrings.couldnt_update_user:
return L10n.of(context).couldnt_update_user; return L10n.of(context)!.couldnt_update_user;
case L10nStrings.couldnt_update_private_message: case L10nStrings.couldnt_update_private_message:
return L10n.of(context).couldnt_update_private_message; return L10n.of(context)!.couldnt_update_private_message;
case L10nStrings.couldnt_update_post: case L10nStrings.couldnt_update_post:
return L10n.of(context).couldnt_update_post; return L10n.of(context)!.couldnt_update_post;
case L10nStrings.couldnt_create_private_message: case L10nStrings.couldnt_create_private_message:
return L10n.of(context).couldnt_create_private_message; return L10n.of(context)!.couldnt_create_private_message;
case L10nStrings.no_private_message_edit_allowed: case L10nStrings.no_private_message_edit_allowed:
return L10n.of(context).no_private_message_edit_allowed; return L10n.of(context)!.no_private_message_edit_allowed;
case L10nStrings.post_title_too_long: case L10nStrings.post_title_too_long:
return L10n.of(context).post_title_too_long; return L10n.of(context)!.post_title_too_long;
case L10nStrings.email_already_exists: case L10nStrings.email_already_exists:
return L10n.of(context).email_already_exists; return L10n.of(context)!.email_already_exists;
case L10nStrings.user_already_exists: case L10nStrings.user_already_exists:
return L10n.of(context).user_already_exists; return L10n.of(context)!.user_already_exists;
case L10nStrings.unsubscribe: case L10nStrings.unsubscribe:
return L10n.of(context).unsubscribe; return L10n.of(context)!.unsubscribe;
case L10nStrings.subscribe: case L10nStrings.subscribe:
return L10n.of(context).subscribe; return L10n.of(context)!.subscribe;
case L10nStrings.messages: case L10nStrings.messages:
return L10n.of(context).messages; return L10n.of(context)!.messages;
case L10nStrings.banned_users: case L10nStrings.banned_users:
return L10n.of(context).banned_users; return L10n.of(context)!.banned_users;
case L10nStrings.delete_account_confirm: case L10nStrings.delete_account_confirm:
return L10n.of(context).delete_account_confirm; return L10n.of(context)!.delete_account_confirm;
case L10nStrings.new_password: case L10nStrings.new_password:
return L10n.of(context).new_password; return L10n.of(context)!.new_password;
case L10nStrings.verify_password: case L10nStrings.verify_password:
return L10n.of(context).verify_password; return L10n.of(context)!.verify_password;
case L10nStrings.old_password: case L10nStrings.old_password:
return L10n.of(context).old_password; return L10n.of(context)!.old_password;
case L10nStrings.show_avatars: case L10nStrings.show_avatars:
return L10n.of(context).show_avatars; return L10n.of(context)!.show_avatars;
case L10nStrings.search: case L10nStrings.search:
return L10n.of(context).search; return L10n.of(context)!.search;
case L10nStrings.send_message: case L10nStrings.send_message:
return L10n.of(context).send_message; return L10n.of(context)!.send_message;
case L10nStrings.top_day: case L10nStrings.top_day:
return L10n.of(context).top_day; return L10n.of(context)!.top_day;
case L10nStrings.top_week: case L10nStrings.top_week:
return L10n.of(context).top_week; return L10n.of(context)!.top_week;
case L10nStrings.top_month: case L10nStrings.top_month:
return L10n.of(context).top_month; return L10n.of(context)!.top_month;
case L10nStrings.top_year: case L10nStrings.top_year:
return L10n.of(context).top_year; return L10n.of(context)!.top_year;
case L10nStrings.top_all: case L10nStrings.top_all:
return L10n.of(context).top_all; return L10n.of(context)!.top_all;
case L10nStrings.most_comments: case L10nStrings.most_comments:
return L10n.of(context).most_comments; return L10n.of(context)!.most_comments;
case L10nStrings.new_comments: case L10nStrings.new_comments:
return L10n.of(context).new_comments; return L10n.of(context)!.new_comments;
case L10nStrings.active: case L10nStrings.active:
return L10n.of(context).active; return L10n.of(context)!.active;
default: default:
return this; return this;

View File

@ -16,8 +16,7 @@ import 'add_instance.dart';
class AddAccountPage extends HookWidget { class AddAccountPage extends HookWidget {
final String instanceHost; final String instanceHost;
const AddAccountPage({@required this.instanceHost}) const AddAccountPage({required this.instanceHost});
: assert(instanceHost != null);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -29,12 +28,12 @@ class AddAccountPage extends HookWidget {
final loading = useDelayedLoading(); final loading = useDelayedLoading();
final selectedInstance = useState(instanceHost); final selectedInstance = useState(instanceHost);
final icon = useState<String>(null); final icon = useState<String?>(null);
useEffect(() { useEffect(() {
LemmyApiV3(selectedInstance.value) LemmyApiV3(selectedInstance.value)
.run(const GetSite()) .run(const GetSite())
.then((site) => icon.value = site.siteView.site.icon); .then((site) => icon.value = site.siteView?.site.icon);
return null; return null;
}, [selectedInstance.value]); }, [selectedInstance.value]);
@ -69,9 +68,9 @@ class AddAccountPage extends HookWidget {
SizedBox( SizedBox(
height: 150, height: 150,
child: FullscreenableImage( child: FullscreenableImage(
url: icon.value, url: icon.value!,
child: CachedNetworkImage( child: CachedNetworkImage(
imageUrl: icon.value, imageUrl: icon.value!,
errorWidget: (_, __, ___) => const SizedBox.shrink(), errorWidget: (_, __, ___) => const SizedBox.shrink(),
), ),
), ),
@ -111,13 +110,13 @@ class AddAccountPage extends HookWidget {
autofocus: true, autofocus: true,
controller: usernameController, controller: usernameController,
decoration: decoration:
InputDecoration(labelText: L10n.of(context).email_or_username), InputDecoration(labelText: L10n.of(context)!.email_or_username),
), ),
const SizedBox(height: 5), const SizedBox(height: 5),
TextField( TextField(
controller: passwordController, controller: passwordController,
obscureText: true, obscureText: true,
decoration: InputDecoration(labelText: L10n.of(context).password), decoration: InputDecoration(labelText: L10n.of(context)!.password),
), ),
ElevatedButton( ElevatedButton(
onPressed: usernameController.text.isEmpty || onPressed: usernameController.text.isEmpty ||

View File

@ -19,8 +19,8 @@ class AddInstancePage extends HookWidget {
useValueListenable(instanceController); useValueListenable(instanceController);
final accountsStore = useAccountsStore(); final accountsStore = useAccountsStore();
final isSite = useState<bool>(null); final isSite = useState<bool?>(null);
final icon = useState<String>(null); final icon = useState<String?>(null);
final prevInput = usePrevious(instanceController.text); final prevInput = usePrevious(instanceController.text);
final debounce = useDebounce(() async { final debounce = useDebounce(() async {
if (prevInput == instanceController.text) return; if (prevInput == instanceController.text) return;
@ -32,7 +32,7 @@ class AddInstancePage extends HookWidget {
} }
try { try {
icon.value = icon.value =
(await LemmyApiV3(inst).run(const GetSite())).siteView.site.icon; (await LemmyApiV3(inst).run(const GetSite())).siteView?.site.icon;
isSite.value = true; isSite.value = true;
// ignore: avoid_catches_without_on_clauses // ignore: avoid_catches_without_on_clauses
} catch (e) { } catch (e) {
@ -70,9 +70,9 @@ class AddInstancePage extends HookWidget {
SizedBox( SizedBox(
height: 150, height: 150,
child: FullscreenableImage( child: FullscreenableImage(
url: icon.value, url: icon.value!,
child: CachedNetworkImage( child: CachedNetworkImage(
imageUrl: icon.value, imageUrl: icon.value!,
errorWidget: (_, __, ___) => const SizedBox.shrink(), errorWidget: (_, __, ___) => const SizedBox.shrink(),
), ),
)) ))

View File

@ -15,10 +15,8 @@ class CommunitiesListPage extends StatelessWidget {
SortType sortType, SortType sortType,
) fetcher; ) fetcher;
const CommunitiesListPage({Key key, @required this.fetcher, this.title = ''}) const CommunitiesListPage({Key? key, required this.fetcher, this.title = ''})
: assert(fetcher != null), : super(key: key);
assert(title != null),
super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -46,9 +44,8 @@ class CommunitiesListPage extends StatelessWidget {
class CommunitiesListItem extends StatelessWidget { class CommunitiesListItem extends StatelessWidget {
final CommunityView community; final CommunityView community;
const CommunitiesListItem({Key key, @required this.community}) const CommunitiesListItem({Key? key, required this.community})
: assert(community != null), : super(key: key);
super(key: key);
@override @override
Widget build(BuildContext context) => ListTile( Widget build(BuildContext context) => ListTile(
@ -57,7 +54,7 @@ class CommunitiesListItem extends StatelessWidget {
? Opacity( ? Opacity(
opacity: 0.7, opacity: 0.7,
child: MarkdownText( child: MarkdownText(
community.community.description, community.community.description!,
instanceHost: community.instanceHost, instanceHost: community.instanceHost,
), ),
) )

View File

@ -7,6 +7,7 @@ import 'package:fuzzy/fuzzy.dart';
import 'package:lemmy_api_client/v3.dart'; import 'package:lemmy_api_client/v3.dart';
import '../hooks/delayed_loading.dart'; import '../hooks/delayed_loading.dart';
import '../hooks/logged_in_action.dart';
import '../hooks/refreshable.dart'; import '../hooks/refreshable.dart';
import '../hooks/stores.dart'; import '../hooks/stores.dart';
import '../util/extensions/api.dart'; import '../util/extensions/api.dart';
@ -37,7 +38,7 @@ class CommunitiesTab extends HookWidget {
.map( .map(
(instanceHost) => LemmyApiV3(instanceHost) (instanceHost) => LemmyApiV3(instanceHost)
.run(const GetSite()) .run(const GetSite())
.then((e) => e.siteView.site), .then((e) => e.siteView!.site),
) )
.toList(); .toList();
@ -52,8 +53,8 @@ class CommunitiesTab extends HookWidget {
sort: SortType.active, sort: SortType.active,
savedOnly: false, savedOnly: false,
personId: personId:
accountsStore.defaultTokenFor(instanceHost).payload.sub, accountsStore.defaultTokenFor(instanceHost)!.payload.sub,
auth: accountsStore.defaultTokenFor(instanceHost).raw, auth: accountsStore.defaultTokenFor(instanceHost)!.raw,
)) ))
.then((e) => e.follows), .then((e) => e.follows),
) )
@ -84,7 +85,7 @@ class CommunitiesTab extends HookWidget {
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
child: Text( child: Text(
communitiesRefreshable.snapshot.error?.toString() ?? communitiesRefreshable.snapshot.error?.toString() ??
instancesRefreshable.snapshot.error?.toString(), instancesRefreshable.snapshot.error!.toString(),
), ),
) )
], ],
@ -115,8 +116,8 @@ class CommunitiesTab extends HookWidget {
} }
} }
final instances = instancesRefreshable.snapshot.data; final instances = instancesRefreshable.snapshot.data!;
final communities = communitiesRefreshable.snapshot.data final communities = communitiesRefreshable.snapshot.data!
..forEach((e) => ..forEach((e) =>
e.sort((a, b) => a.community.name.compareTo(b.community.name))); e.sort((a, b) => a.community.name.compareTo(b.community.name)));
@ -128,7 +129,7 @@ class CommunitiesTab extends HookWidget {
return IconButton( return IconButton(
onPressed: () { onPressed: () {
filterController.clear(); filterController.clear();
primaryFocus.unfocus(); primaryFocus?.unfocus();
}, },
icon: const Icon(Icons.clear), icon: const Icon(Icons.clear),
); );
@ -236,26 +237,24 @@ class _CommunitySubscribeToggle extends HookWidget {
final String instanceHost; final String instanceHost;
const _CommunitySubscribeToggle( const _CommunitySubscribeToggle(
{@required this.instanceHost, @required this.communityId, Key key}) {required this.instanceHost, required this.communityId, Key? key})
: assert(instanceHost != null), : super(key: key);
assert(communityId != null),
super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
final subbed = useState(true); final subbed = useState(true);
final delayed = useDelayedLoading(); final delayed = useDelayedLoading();
final accountsStore = useAccountsStore(); final loggedInAction = useLoggedInAction(instanceHost);
handleTap() async { handleTap(Jwt token) async {
delayed.start(); delayed.start();
try { try {
await LemmyApiV3(instanceHost).run(FollowCommunity( await LemmyApiV3(instanceHost).run(FollowCommunity(
communityId: communityId, communityId: communityId,
follow: !subbed.value, follow: !subbed.value,
auth: accountsStore.defaultTokenFor(instanceHost).raw, auth: token.raw,
)); ));
subbed.value = !subbed.value; subbed.value = !subbed.value;
} on Exception catch (err) { } on Exception catch (err) {
@ -268,7 +267,7 @@ class _CommunitySubscribeToggle extends HookWidget {
} }
return InkWell( return InkWell(
onTap: delayed.pending ? () {} : handleTap, onTap: delayed.pending ? () {} : loggedInAction(handleTap),
child: Container( child: Container(
decoration: delayed.loading decoration: delayed.loading
? null ? null

View File

@ -28,26 +28,22 @@ import 'modlog_page.dart';
/// Displays posts, comments, and general info about the given community /// Displays posts, comments, and general info about the given community
class CommunityPage extends HookWidget { class CommunityPage extends HookWidget {
final CommunityView _community; final CommunityView? _community;
final String instanceHost; final String instanceHost;
final String communityName; final String? communityName;
final int communityId; final int? communityId;
const CommunityPage.fromName({ const CommunityPage.fromName({
@required this.communityName, required String this.communityName,
@required this.instanceHost, required this.instanceHost,
}) : assert(communityName != null), }) : communityId = null,
assert(instanceHost != null),
communityId = null,
_community = null; _community = null;
const CommunityPage.fromId({ const CommunityPage.fromId({
@required this.communityId, required int this.communityId,
@required this.instanceHost, required this.instanceHost,
}) : assert(communityId != null), }) : communityName = null,
assert(instanceHost != null),
communityName = null,
_community = null; _community = null;
CommunityPage.fromCommunityView(this._community) CommunityPage.fromCommunityView(CommunityView this._community)
: instanceHost = _community.instanceHost, : instanceHost = _community.instanceHost,
communityId = _community.community.id, communityId = _community.community.id,
communityName = _community.community.name; communityName = _community.community.name;
@ -76,7 +72,7 @@ class CommunityPage extends HookWidget {
final community = () { final community = () {
if (fullCommunitySnap.hasData) { if (fullCommunitySnap.hasData) {
return fullCommunitySnap.data.communityView; return fullCommunitySnap.data!.communityView;
} else if (_community != null) { } else if (_community != null) {
return _community; return _community;
} else { } else {
@ -173,8 +169,8 @@ class CommunityPage extends HookWidget {
color: theme.cardColor, color: theme.cardColor,
child: TabBar( child: TabBar(
tabs: [ tabs: [
Tab(text: L10n.of(context).posts), Tab(text: L10n.of(context)!.posts),
Tab(text: L10n.of(context).comments), Tab(text: L10n.of(context)!.comments),
const Tab(text: 'About'), const Tab(text: 'About'),
], ],
), ),
@ -224,14 +220,13 @@ class CommunityPage extends HookWidget {
class _CommunityOverview extends StatelessWidget { class _CommunityOverview extends StatelessWidget {
final CommunityView community; final CommunityView community;
final String instanceHost; final String instanceHost;
final int onlineUsers; final int? onlineUsers;
const _CommunityOverview({ const _CommunityOverview({
@required this.community, required this.community,
@required this.instanceHost, required this.instanceHost,
@required this.onlineUsers, required this.onlineUsers,
}) : assert(instanceHost != null), });
assert(goToInstance != null);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -257,7 +252,7 @@ class _CommunityOverview extends StatelessWidget {
), ),
), ),
FullscreenableImage( FullscreenableImage(
url: community.community.icon, url: community.community.icon!,
child: Avatar( child: Avatar(
url: community.community.icon, url: community.community.icon,
radius: 83 / 2, radius: 83 / 2,
@ -270,9 +265,9 @@ class _CommunityOverview extends StatelessWidget {
return Stack(children: [ return Stack(children: [
if (community.community.banner != null) if (community.community.banner != null)
FullscreenableImage( FullscreenableImage(
url: community.community.banner, url: community.community.banner!,
child: CachedNetworkImage( child: CachedNetworkImage(
imageUrl: community.community.banner, imageUrl: community.community.banner!,
errorWidget: (_, __, ___) => const SizedBox.shrink(), errorWidget: (_, __, ___) => const SizedBox.shrink(),
), ),
), ),
@ -280,7 +275,7 @@ class _CommunityOverview extends StatelessWidget {
child: Padding( child: Padding(
padding: const EdgeInsets.only(top: 45), padding: const EdgeInsets.only(top: 45),
child: Column(children: [ child: Column(children: [
if (community.community.icon != null) icon, if (icon != null) icon,
// NAME // NAME
Center( Center(
child: Padding( child: Padding(
@ -289,7 +284,7 @@ class _CommunityOverview extends StatelessWidget {
overflow: TextOverflow.ellipsis, // TODO: fix overflowing overflow: TextOverflow.ellipsis, // TODO: fix overflowing
text: TextSpan( text: TextSpan(
style: style:
theme.textTheme.subtitle1.copyWith(shadows: [shadow]), theme.textTheme.subtitle1?.copyWith(shadows: [shadow]),
children: [ children: [
const TextSpan( const TextSpan(
text: '!', text: '!',
@ -346,7 +341,7 @@ class _CommunityOverview extends StatelessWidget {
), ),
Text(onlineUsers == null Text(onlineUsers == null
? 'xx' ? 'xx'
: compactNumber(onlineUsers)), : compactNumber(onlineUsers!)),
const Spacer(), const Spacer(),
], ],
), ),
@ -364,14 +359,14 @@ class _CommunityOverview extends StatelessWidget {
class _AboutTab extends StatelessWidget { class _AboutTab extends StatelessWidget {
final CommunityView community; final CommunityView community;
final List<CommunityModeratorView> moderators; final List<CommunityModeratorView>? moderators;
final int onlineUsers; final int? onlineUsers;
const _AboutTab({ const _AboutTab({
Key key, Key? key,
@required this.community, required this.community,
@required this.moderators, required this.moderators,
@required this.onlineUsers, required this.onlineUsers,
}) : super(key: key); }) : super(key: key);
@override @override
@ -384,7 +379,7 @@ class _AboutTab extends StatelessWidget {
if (community.community.description != null) ...[ if (community.community.description != null) ...[
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 15), padding: const EdgeInsets.symmetric(horizontal: 15),
child: MarkdownText(community.community.description, child: MarkdownText(community.community.description!,
instanceHost: community.instanceHost), instanceHost: community.instanceHost),
), ),
const _Divider(), const _Divider(),
@ -396,10 +391,10 @@ class _AboutTab extends StatelessWidget {
padding: const EdgeInsets.symmetric(horizontal: 15), padding: const EdgeInsets.symmetric(horizontal: 15),
children: [ children: [
Chip( Chip(
label: Text(L10n.of(context) label: Text(L10n.of(context)!
.number_of_users_online(onlineUsers ?? 0))), .number_of_users_online(onlineUsers ?? 0))),
Chip( Chip(
label: Text(L10n.of(context) label: Text(L10n.of(context)!
.number_of_subscribers(community.counts.subscribers))), .number_of_subscribers(community.counts.subscribers))),
Chip( Chip(
label: Text( label: Text(
@ -422,16 +417,16 @@ class _AboutTab extends StatelessWidget {
communityName: community.community.name, communityName: community.community.name,
), ),
), ),
child: Text(L10n.of(context).modlog), child: Text(L10n.of(context)!.modlog),
), ),
), ),
const _Divider(), const _Divider(),
if (moderators != null && moderators.isNotEmpty) ...[ if (moderators != null && moderators!.isNotEmpty) ...[
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 15), padding: const EdgeInsets.symmetric(horizontal: 15),
child: Text('Mods:', style: theme.textTheme.subtitle2), child: Text('Mods:', style: theme.textTheme.subtitle2),
), ),
for (final mod in moderators) for (final mod in moderators!)
// TODO: add user picture, maybe make it into reusable component // TODO: add user picture, maybe make it into reusable component
ListTile( ListTile(
title: Text( title: Text(
@ -463,7 +458,7 @@ class _FollowButton extends HookWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
final isSubbed = useState(community.subscribed ?? false); final isSubbed = useState(community.subscribed);
final delayed = useDelayedLoading(Duration.zero); final delayed = useDelayedLoading(Duration.zero);
final loggedInAction = useLoggedInAction(community.instanceHost); final loggedInAction = useLoggedInAction(community.instanceHost);
@ -493,7 +488,7 @@ class _FollowButton extends HookWidget {
return ElevatedButtonTheme( return ElevatedButtonTheme(
data: ElevatedButtonThemeData( data: ElevatedButtonThemeData(
style: theme.elevatedButtonTheme.style.copyWith( style: theme.elevatedButtonTheme.style?.copyWith(
shape: MaterialStateProperty.all(const StadiumBorder()), shape: MaterialStateProperty.all(const StadiumBorder()),
textStyle: MaterialStateProperty.all(theme.textTheme.subtitle1), textStyle: MaterialStateProperty.all(theme.textTheme.subtitle1),
), ),
@ -518,8 +513,8 @@ class _FollowButton extends HookWidget {
? const Icon(Icons.remove, size: 18) ? const Icon(Icons.remove, size: 18)
: const Icon(Icons.add, size: 18), : const Icon(Icons.add, size: 18),
label: Text(isSubbed.value label: Text(isSubbed.value
? L10n.of(context).unsubscribe ? L10n.of(context)!.unsubscribe
: L10n.of(context).subscribe), : L10n.of(context)!.subscribe),
), ),
), ),
), ),

View File

@ -23,18 +23,20 @@ import 'full_post.dart';
/// Fab that triggers the [CreatePost] modal /// Fab that triggers the [CreatePost] modal
class CreatePostFab extends HookWidget { class CreatePostFab extends HookWidget {
final CommunityView community; final CommunityView? community;
const CreatePostFab({this.community}); const CreatePostFab({this.community});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final loggedInAction = useLoggedInAction(null, any: true); final loggedInAction = useAnyLoggedInAction();
return FloatingActionButton( return FloatingActionButton(
onPressed: loggedInAction((_) => showCupertinoModalPopup( onPressed: loggedInAction((_) => showCupertinoModalPopup(
context: context, context: context,
builder: (_) => CreatePostPage.toCommunity(community))), builder: (_) => community == null
? const CreatePostPage()
: CreatePostPage.toCommunity(community!))),
child: const Icon(Icons.add), child: const Icon(Icons.add),
); );
} }
@ -42,10 +44,10 @@ class CreatePostFab extends HookWidget {
/// Modal for creating a post to some community in some instance /// Modal for creating a post to some community in some instance
class CreatePostPage extends HookWidget { class CreatePostPage extends HookWidget {
final CommunityView community; final CommunityView? community;
const CreatePostPage() : community = null; const CreatePostPage() : community = null;
const CreatePostPage.toCommunity(this.community); const CreatePostPage.toCommunity(CommunityView this.community);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -61,7 +63,8 @@ class CreatePostPage extends HookWidget {
final delayed = useDelayedLoading(); final delayed = useDelayedLoading();
final imagePicker = useImagePicker(); final imagePicker = useImagePicker();
final imageUploadLoading = useState(false); final imageUploadLoading = useState(false);
final pictrsDeleteToken = useState<PictrsUploadFile>(null); final pictrsDeleteToken = useState<PictrsUploadFile?>(null);
final loggedInAction = useLoggedInAction(selectedInstance.value);
final allCommunitiesSnap = useMemoFuture( final allCommunitiesSnap = useMemoFuture(
() => LemmyApiV3(selectedInstance.value) () => LemmyApiV3(selectedInstance.value)
@ -69,7 +72,7 @@ class CreatePostPage extends HookWidget {
type: PostListingType.all, type: PostListingType.all,
sort: SortType.hot, sort: SortType.hot,
limit: 9999, limit: 9999,
auth: accStore.defaultTokenFor(selectedInstance.value).raw, auth: accStore.defaultTokenFor(selectedInstance.value)?.raw,
)) ))
.then( .then(
(value) { (value) {
@ -80,14 +83,13 @@ class CreatePostPage extends HookWidget {
[selectedInstance.value], [selectedInstance.value],
); );
uploadPicture() async { uploadPicture(Jwt token) async {
try { try {
final pic = await imagePicker.getImage(source: ImageSource.gallery); final pic = await imagePicker.getImage(source: ImageSource.gallery);
// pic is null when the picker was cancelled // pic is null when the picker was cancelled
if (pic != null) { if (pic != null) {
imageUploadLoading.value = true; imageUploadLoading.value = true;
final token = accStore.defaultTokenFor(selectedInstance.value);
final pictrs = PictrsApi(selectedInstance.value); final pictrs = PictrsApi(selectedInstance.value);
final upload = final upload =
await pictrs.upload(filePath: pic.path, auth: token.raw); await pictrs.upload(filePath: pic.path, auth: token.raw);
@ -105,10 +107,8 @@ class CreatePostPage extends HookWidget {
} }
} }
removePicture() { removePicture(PictrsUploadFile deleteToken) {
PictrsApi(selectedInstance.value) PictrsApi(selectedInstance.value).delete(deleteToken).catchError((_) {});
.delete(pictrsDeleteToken.value)
.catchError((_) {});
pictrsDeleteToken.value = null; pictrsDeleteToken.value = null;
urlController.text = ''; urlController.text = '';
@ -140,10 +140,10 @@ class CreatePostPage extends HookWidget {
List<DropdownMenuItem<int>> communitiesList() { List<DropdownMenuItem<int>> communitiesList() {
if (allCommunitiesSnap.hasData) { if (allCommunitiesSnap.hasData) {
return allCommunitiesSnap.data.map(communityDropDownItem).toList(); return allCommunitiesSnap.data!.map(communityDropDownItem).toList();
} else { } else {
if (selectedCommunity.value != null) { if (selectedCommunity.value != null) {
return [communityDropDownItem(selectedCommunity.value)]; return [communityDropDownItem(selectedCommunity.value!)];
} else { } else {
return const [ return const [
DropdownMenuItem( DropdownMenuItem(
@ -163,8 +163,8 @@ class CreatePostPage extends HookWidget {
borderRadius: BorderRadius.all(Radius.circular(10)))), borderRadius: BorderRadius.all(Radius.circular(10)))),
child: DropdownButtonHideUnderline( child: DropdownButtonHideUnderline(
child: DropdownButton<int>( child: DropdownButton<int>(
value: selectedCommunity.value?.community?.id, value: selectedCommunity.value?.community.id,
hint: Text(L10n.of(context).community), hint: Text(L10n.of(context)!.community),
onChanged: (communityId) => selectedCommunity.value = onChanged: (communityId) => selectedCommunity.value =
allCommunitiesSnap.data allCommunitiesSnap.data
?.firstWhere((e) => e.community.id == communityId), ?.firstWhere((e) => e.community.id == communityId),
@ -179,7 +179,7 @@ class CreatePostPage extends HookWidget {
enabled: pictrsDeleteToken.value == null, enabled: pictrsDeleteToken.value == null,
controller: urlController, controller: urlController,
decoration: InputDecoration( decoration: InputDecoration(
labelText: L10n.of(context).url, labelText: L10n.of(context)!.url,
suffixIcon: const Icon(Icons.link), suffixIcon: const Icon(Icons.link),
), ),
), ),
@ -191,8 +191,9 @@ class CreatePostPage extends HookWidget {
: Icon(pictrsDeleteToken.value == null : Icon(pictrsDeleteToken.value == null
? Icons.add_photo_alternate ? Icons.add_photo_alternate
: Icons.close), : Icons.close),
onPressed: onPressed: pictrsDeleteToken.value == null
pictrsDeleteToken.value == null ? uploadPicture : removePicture, ? loggedInAction(uploadPicture)
: () => removePicture(pictrsDeleteToken.value!),
tooltip: tooltip:
pictrsDeleteToken.value == null ? 'Add picture' : 'Delete picture', pictrsDeleteToken.value == null ? 'Add picture' : 'Delete picture',
) )
@ -202,7 +203,7 @@ class CreatePostPage extends HookWidget {
controller: titleController, controller: titleController,
minLines: 1, minLines: 1,
maxLines: 2, maxLines: 2,
decoration: InputDecoration(labelText: L10n.of(context).title), decoration: InputDecoration(labelText: L10n.of(context)!.title),
); );
final body = IndexedStack( final body = IndexedStack(
@ -213,7 +214,7 @@ class CreatePostPage extends HookWidget {
keyboardType: TextInputType.multiline, keyboardType: TextInputType.multiline,
maxLines: null, maxLines: null,
minLines: 5, minLines: 5,
decoration: InputDecoration(labelText: L10n.of(context).body), decoration: InputDecoration(labelText: L10n.of(context)!.body),
), ),
Padding( Padding(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
@ -225,7 +226,7 @@ class CreatePostPage extends HookWidget {
], ],
); );
handleSubmit() async { handleSubmit(Jwt token) async {
if (selectedCommunity.value == null || titleController.text.isEmpty) { if (selectedCommunity.value == null || titleController.text.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar( ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
content: Text('Choosing a community and a title is required'), content: Text('Choosing a community and a title is required'),
@ -235,8 +236,6 @@ class CreatePostPage extends HookWidget {
final api = LemmyApiV3(selectedInstance.value); final api = LemmyApiV3(selectedInstance.value);
final token = accStore.defaultTokenFor(selectedInstance.value);
delayed.start(); delayed.start();
try { try {
final res = await api.run(CreatePost( final res = await api.run(CreatePost(
@ -244,7 +243,7 @@ class CreatePostPage extends HookWidget {
body: bodyController.text.isEmpty ? null : bodyController.text, body: bodyController.text.isEmpty ? null : bodyController.text,
nsfw: nsfw.value, nsfw: nsfw.value,
name: titleController.text, name: titleController.text,
communityId: selectedCommunity.value.community.id, communityId: selectedCommunity.value!.community.id,
auth: token.raw, auth: token.raw,
)); ));
unawaited(goToReplace(context, (_) => FullPostPage.fromPostView(res))); unawaited(goToReplace(context, (_) => FullPostPage.fromPostView(res)));
@ -285,17 +284,20 @@ class CreatePostPage extends HookWidget {
children: [ children: [
Checkbox( Checkbox(
value: nsfw.value, value: nsfw.value,
onChanged: (val) => nsfw.value = val, onChanged: (val) {
if (val != null) nsfw.value = val;
},
), ),
Text(L10n.of(context).nsfw) Text(L10n.of(context)!.nsfw)
], ],
), ),
), ),
TextButton( TextButton(
onPressed: delayed.pending ? () {} : handleSubmit, onPressed:
delayed.pending ? () {} : loggedInAction(handleSubmit),
child: delayed.loading child: delayed.loading
? const CircularProgressIndicator() ? const CircularProgressIndicator()
: Text(L10n.of(context).post), : Text(L10n.of(context)!.post),
) )
], ],
), ),

View File

@ -20,13 +20,11 @@ import '../widgets/write_comment.dart';
class FullPostPage extends HookWidget { class FullPostPage extends HookWidget {
final int id; final int id;
final String instanceHost; final String instanceHost;
final PostView post; final PostView? post;
const FullPostPage({@required this.id, @required this.instanceHost}) const FullPostPage({required this.id, required this.instanceHost})
: assert(id != null), : post = null;
assert(instanceHost != null), FullPostPage.fromPostView(PostView this.post)
post = null;
FullPostPage.fromPostView(this.post)
: id = post.post.id, : id = post.post.id,
instanceHost = post.instanceHost; instanceHost = post.instanceHost;
@ -64,8 +62,8 @@ class FullPostPage extends HookWidget {
// VARIABLES // VARIABLES
final post = fullPostRefreshable.snapshot.hasData final post = fullPostRefreshable.snapshot.hasData
? fullPostRefreshable.snapshot.data.postView ? fullPostRefreshable.snapshot.data!.postView
: this.post; : this.post!;
final fullPost = fullPostRefreshable.snapshot.data; final fullPost = fullPostRefreshable.snapshot.data;
@ -129,7 +127,7 @@ class FullPostPage extends HookWidget {
children: [ children: [
const SizedBox(height: 15), const SizedBox(height: 15),
PostWidget(post, fullPost: true), PostWidget(post, fullPost: true),
if (fullPostRefreshable.snapshot.hasData) if (fullPost != null)
CommentSection( CommentSection(
newComments.value.followedBy(fullPost.comments).toList(), newComments.value.followedBy(fullPost.comments).toList(),
postCreatorId: fullPost.postView.creator.id) postCreatorId: fullPost.postView.creator.id)

View File

@ -32,13 +32,14 @@ class HomeTab extends HookWidget {
final isc = useInfiniteScrollController(); final isc = useInfiniteScrollController();
final theme = Theme.of(context); final theme = Theme.of(context);
final instancesIcons = useMemoFuture(() async { final instancesIcons = useMemoFuture(() async {
final instances = accStore.instances.toList(growable: false); final sites = await Future.wait(accStore.instances.map((e) =>
final sites = await Future.wait(instances.map( LemmyApiV3(e)
(e) => LemmyApiV3(e).run(const GetSite()).catchError((e) => null))); .run<FullSiteView?>(const GetSite())
.catchError((e) => null)));
return { return {
for (var i = 0; i < sites.length; i++) for (final site in sites)
instances[i]: sites[i].siteView.site.icon if (site != null) site.instanceHost: site.siteView?.site.icon
}; };
}); });
@ -48,7 +49,8 @@ class HomeTab extends HookWidget {
// - listingType == subscribed on an instance that has no longer a logged in account // - listingType == subscribed on an instance that has no longer a logged in account
// - instanceHost of a removed instance // - instanceHost of a removed instance
useEffect(() { useEffect(() {
if (accStore.isAnonymousFor(selectedList.value.instanceHost) && if ((selectedList.value.instanceHost == null ||
accStore.isAnonymousFor(selectedList.value.instanceHost!)) &&
selectedList.value.listingType == PostListingType.subscribed || selectedList.value.listingType == PostListingType.subscribed ||
!accStore.instances.contains(selectedList.value.instanceHost)) { !accStore.instances.contains(selectedList.value.instanceHost)) {
selectedList.value = _SelectedList( selectedList.value = _SelectedList(
@ -60,7 +62,8 @@ class HomeTab extends HookWidget {
return null; return null;
}, [ }, [
accStore.isAnonymousFor(selectedList.value.instanceHost), selectedList.value.instanceHost == null ||
accStore.isAnonymousFor(selectedList.value.instanceHost!),
accStore.hasNoAccount, accStore.hasNoAccount,
accStore.instances.length, accStore.instances.length,
]); ]);
@ -84,10 +87,10 @@ class HomeTab extends HookWidget {
), ),
ListTile( ListTile(
title: Text( title: Text(
L10n.of(context).subscribed, L10n.of(context)!.subscribed,
style: TextStyle( style: TextStyle(
color: accStore.hasNoAccount color: accStore.hasNoAccount
? theme.textTheme.bodyText1.color.withOpacity(0.4) ? theme.textTheme.bodyText1?.color?.withOpacity(0.4)
: null, : null,
), ),
), ),
@ -119,7 +122,7 @@ class HomeTab extends HookWidget {
instance.toUpperCase(), instance.toUpperCase(),
style: TextStyle( style: TextStyle(
color: color:
theme.textTheme.bodyText1.color.withOpacity(0.7)), theme.textTheme.bodyText1?.color?.withOpacity(0.7)),
), ),
onTap: () => goToInstance(context, instance), onTap: () => goToInstance(context, instance),
dense: true, dense: true,
@ -127,14 +130,14 @@ class HomeTab extends HookWidget {
visualDensity: const VisualDensity( visualDensity: const VisualDensity(
vertical: VisualDensity.minimumDensity), vertical: VisualDensity.minimumDensity),
leading: (instancesIcons.hasData && leading: (instancesIcons.hasData &&
instancesIcons.data[instance] != null) instancesIcons.data![instance] != null)
? Padding( ? Padding(
padding: const EdgeInsets.only(left: 20), padding: const EdgeInsets.only(left: 20),
child: SizedBox( child: SizedBox(
width: 25, width: 25,
height: 25, height: 25,
child: CachedNetworkImage( child: CachedNetworkImage(
imageUrl: instancesIcons.data[instance], imageUrl: instancesIcons.data![instance]!,
height: 25, height: 25,
width: 25, width: 25,
), ),
@ -144,10 +147,10 @@ class HomeTab extends HookWidget {
), ),
ListTile( ListTile(
title: Text( title: Text(
L10n.of(context).subscribed, L10n.of(context)!.subscribed,
style: TextStyle( style: TextStyle(
color: accStore.isAnonymousFor(instance) color: accStore.isAnonymousFor(instance)
? theme.textTheme.bodyText1.color.withOpacity(0.4) ? theme.textTheme.bodyText1?.color?.withOpacity(0.4)
: null), : null),
), ),
onTap: accStore.isAnonymousFor(instance) onTap: accStore.isAnonymousFor(instance)
@ -162,7 +165,7 @@ class HomeTab extends HookWidget {
leading: const SizedBox(width: 20), leading: const SizedBox(width: 20),
), ),
ListTile( ListTile(
title: Text(L10n.of(context).local), title: Text(L10n.of(context)!.local),
onTap: () => pop(_SelectedList( onTap: () => pop(_SelectedList(
listingType: PostListingType.local, listingType: PostListingType.local,
instanceHost: instance, instanceHost: instance,
@ -170,7 +173,7 @@ class HomeTab extends HookWidget {
leading: const SizedBox(width: 20), leading: const SizedBox(width: 20),
), ),
ListTile( ListTile(
title: Text(L10n.of(context).all), title: Text(L10n.of(context)!.all),
onTap: () => pop(_SelectedList( onTap: () => pop(_SelectedList(
listingType: PostListingType.all, listingType: PostListingType.all,
instanceHost: instance, instanceHost: instance,
@ -229,7 +232,7 @@ class HomeTab extends HookWidget {
Flexible( Flexible(
child: Text( child: Text(
title, title,
style: theme.appBarTheme.textTheme.headline6, style: theme.appBarTheme.textTheme?.headline6,
overflow: TextOverflow.fade, overflow: TextOverflow.fade,
softWrap: false, softWrap: false,
), ),
@ -249,15 +252,13 @@ class HomeTab extends HookWidget {
/// Infinite list of posts /// Infinite list of posts
class InfiniteHomeList extends HookWidget { class InfiniteHomeList extends HookWidget {
final Function onStyleChange;
final InfiniteScrollController controller; final InfiniteScrollController controller;
final _SelectedList selectedList; final _SelectedList selectedList;
const InfiniteHomeList({ const InfiniteHomeList({
@required this.selectedList, required this.selectedList,
this.onStyleChange, required this.controller,
this.controller, });
}) : assert(selectedList != null);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -323,7 +324,7 @@ class InfiniteHomeList extends HookWidget {
? (page, limit, sort) => ? (page, limit, sort) =>
generalFetcher(page, limit, sort, selectedList.listingType) generalFetcher(page, limit, sort, selectedList.listingType)
: fetcherFromInstance( : fetcherFromInstance(
selectedList.instanceHost, selectedList.listingType), selectedList.instanceHost!, selectedList.listingType),
controller: controller, controller: controller,
); );
} }
@ -331,13 +332,13 @@ class InfiniteHomeList extends HookWidget {
class _SelectedList { class _SelectedList {
/// when null it implies the 'EVERYTHING' mode /// when null it implies the 'EVERYTHING' mode
final String instanceHost; final String? instanceHost;
final PostListingType listingType; final PostListingType listingType;
const _SelectedList({ const _SelectedList({
@required this.listingType, required this.listingType,
this.instanceHost, this.instanceHost,
}) : assert(listingType != null); });
String toString() => String toString() =>
'SelectedList(instanceHost: $instanceHost, listingType: $listingType)'; 'SelectedList(instanceHost: $instanceHost, listingType: $listingType)';

View File

@ -45,6 +45,8 @@ class InboxPage extends HookWidget {
); );
} }
final selectedInstance = selected.value!;
toggleUnreadOnly() { toggleUnreadOnly() {
unreadOnly.value = !unreadOnly.value; unreadOnly.value = !unreadOnly.value;
isc.clear(); isc.clear();
@ -60,7 +62,7 @@ class InboxPage extends HookWidget {
isc.clear(); isc.clear();
}, },
title: 'select instance', title: 'select instance',
groupValue: selected.value, groupValue: selectedInstance,
buttonBuilder: (context, displayString, onPressed) => TextButton( buttonBuilder: (context, displayString, onPressed) => TextButton(
style: TextButton.styleFrom( style: TextButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 15), padding: const EdgeInsets.symmetric(horizontal: 15),
@ -73,7 +75,7 @@ class InboxPage extends HookWidget {
Flexible( Flexible(
child: Text( child: Text(
displayString, displayString,
style: theme.appBarTheme.textTheme.headline6, style: theme.appBarTheme.textTheme?.headline6,
overflow: TextOverflow.fade, overflow: TextOverflow.fade,
softWrap: false, softWrap: false,
), ),
@ -93,9 +95,9 @@ class InboxPage extends HookWidget {
], ],
bottom: TabBar( bottom: TabBar(
tabs: [ tabs: [
Tab(text: L10n.of(context).replies), Tab(text: L10n.of(context)!.replies),
Tab(text: L10n.of(context).mentions), Tab(text: L10n.of(context)!.mentions),
Tab(text: L10n.of(context).messages), Tab(text: L10n.of(context)!.messages),
], ],
), ),
), ),
@ -106,8 +108,8 @@ class InboxPage extends HookWidget {
controller: isc, controller: isc,
defaultSort: SortType.new_, defaultSort: SortType.new_,
fetcher: (page, batchSize, sortType) => fetcher: (page, batchSize, sortType) =>
LemmyApiV3(selected.value).run(GetReplies( LemmyApiV3(selectedInstance).run(GetReplies(
auth: accStore.defaultTokenFor(selected.value).raw, auth: accStore.defaultTokenFor(selectedInstance)!.raw,
sort: sortType, sort: sortType,
limit: batchSize, limit: batchSize,
page: page, page: page,
@ -124,8 +126,8 @@ class InboxPage extends HookWidget {
controller: isc, controller: isc,
defaultSort: SortType.new_, defaultSort: SortType.new_,
fetcher: (page, batchSize, sortType) => fetcher: (page, batchSize, sortType) =>
LemmyApiV3(selected.value).run(GetPersonMentions( LemmyApiV3(selectedInstance).run(GetPersonMentions(
auth: accStore.defaultTokenFor(selected.value).raw, auth: accStore.defaultTokenFor(selectedInstance)!.raw,
sort: sortType, sort: sortType,
limit: batchSize, limit: batchSize,
page: page, page: page,
@ -142,9 +144,9 @@ class InboxPage extends HookWidget {
child: Text('no messages'), child: Text('no messages'),
), ),
controller: isc, controller: isc,
fetcher: (page, batchSize) => LemmyApiV3(selected.value).run( fetcher: (page, batchSize) => LemmyApiV3(selectedInstance).run(
GetPrivateMessages( GetPrivateMessages(
auth: accStore.defaultTokenFor(selected.value).raw, auth: accStore.defaultTokenFor(selectedInstance)!.raw,
limit: batchSize, limit: batchSize,
page: page, page: page,
unreadOnly: unreadOnly.value, unreadOnly: unreadOnly.value,
@ -167,10 +169,9 @@ class PrivateMessageTile extends HookWidget {
final bool hideOnRead; final bool hideOnRead;
const PrivateMessageTile({ const PrivateMessageTile({
@required this.privateMessageView, required this.privateMessageView,
this.hideOnRead = false, this.hideOnRead = false,
}) : assert(privateMessageView != null), });
assert(hideOnRead != null);
static const double _iconSize = 16; static const double _iconSize = 16;
@override @override
@ -189,7 +190,7 @@ class PrivateMessageTile extends HookWidget {
final toMe = useMemoized(() => final toMe = useMemoized(() =>
pmv.value.recipient.originInstanceHost == pmv.value.instanceHost && pmv.value.recipient.originInstanceHost == pmv.value.instanceHost &&
pmv.value.recipient.id == pmv.value.recipient.id ==
accStore.defaultTokenFor(pmv.value.instanceHost)?.payload?.sub); accStore.defaultTokenFor(pmv.value.instanceHost)?.payload.sub);
final otherSide = final otherSide =
useMemoized(() => toMe ? pmv.value.creator : pmv.value.recipient); useMemoized(() => toMe ? pmv.value.creator : pmv.value.recipient);
@ -239,7 +240,7 @@ class PrivateMessageTile extends HookWidget {
instanceHost: pmv.value.instanceHost, instanceHost: pmv.value.instanceHost,
query: DeletePrivateMessage( query: DeletePrivateMessage(
privateMessageId: pmv.value.privateMessage.id, privateMessageId: pmv.value.privateMessage.id,
auth: accStore.defaultTokenFor(pmv.value.instanceHost)?.raw, auth: accStore.defaultTokenFor(pmv.value.instanceHost)!.raw,
deleted: !deleted.value, deleted: !deleted.value,
), ),
onSuccess: (val) => deleted.value = val.privateMessage.deleted, onSuccess: (val) => deleted.value = val.privateMessage.deleted,
@ -251,7 +252,7 @@ class PrivateMessageTile extends HookWidget {
instanceHost: pmv.value.instanceHost, instanceHost: pmv.value.instanceHost,
query: MarkPrivateMessageAsRead( query: MarkPrivateMessageAsRead(
privateMessageId: pmv.value.privateMessage.id, privateMessageId: pmv.value.privateMessage.id,
auth: accStore.defaultTokenFor(pmv.value.instanceHost)?.raw, auth: accStore.defaultTokenFor(pmv.value.instanceHost)!.raw,
read: !read.value, read: !read.value,
), ),
// TODO: add notification for notifying parent list // TODO: add notification for notifying parent list
@ -280,8 +281,8 @@ class PrivateMessageTile extends HookWidget {
Row( Row(
children: [ children: [
Text( Text(
'${toMe ? L10n.of(context).from : L10n.of(context).to} ', '${toMe ? L10n.of(context)!.from : L10n.of(context)!.to} ',
style: TextStyle(color: theme.textTheme.caption.color), style: TextStyle(color: theme.textTheme.caption?.color),
), ),
InkWell( InkWell(
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
@ -292,7 +293,7 @@ class PrivateMessageTile extends HookWidget {
Padding( Padding(
padding: const EdgeInsets.only(right: 5), padding: const EdgeInsets.only(right: 5),
child: CachedNetworkImage( child: CachedNetworkImage(
imageUrl: otherSide.avatar, imageUrl: otherSide.avatar!,
height: 20, height: 20,
width: 20, width: 20,
imageBuilder: (context, imageProvider) => Container( imageBuilder: (context, imageProvider) => Container(
@ -336,7 +337,7 @@ class PrivateMessageTile extends HookWidget {
const SizedBox(height: 5), const SizedBox(height: 5),
if (pmv.value.privateMessage.deleted) if (pmv.value.privateMessage.deleted)
Text( Text(
L10n.of(context).deleted_by_creator, L10n.of(context)!.deleted_by_creator,
style: const TextStyle(fontStyle: FontStyle.italic), style: const TextStyle(fontStyle: FontStyle.italic),
) )
else else
@ -346,19 +347,19 @@ class PrivateMessageTile extends HookWidget {
TileAction( TileAction(
icon: moreIcon, icon: moreIcon,
onPressed: showMoreMenu, onPressed: showMoreMenu,
tooltip: L10n.of(context).more, tooltip: L10n.of(context)!.more,
), ),
if (toMe) ...[ if (toMe) ...[
TileAction( TileAction(
iconColor: read.value ? theme.accentColor : null, iconColor: read.value ? theme.accentColor : null,
icon: Icons.check, icon: Icons.check,
tooltip: L10n.of(context).mark_as_read, tooltip: L10n.of(context)!.mark_as_read,
onPressed: handleRead, onPressed: handleRead,
delayedLoading: readDelayed, delayedLoading: readDelayed,
), ),
TileAction( TileAction(
icon: Icons.reply, icon: Icons.reply,
tooltip: L10n.of(context).reply, tooltip: L10n.of(context)!.reply,
onPressed: () { onPressed: () {
showCupertinoModalPopup( showCupertinoModalPopup(
context: context, context: context,
@ -371,7 +372,7 @@ class PrivateMessageTile extends HookWidget {
] else ...[ ] else ...[
TileAction( TileAction(
icon: Icons.edit, icon: Icons.edit,
tooltip: L10n.of(context).edit, tooltip: L10n.of(context)!.edit,
onPressed: () async { onPressed: () async {
final val = await showCupertinoModalPopup<PrivateMessageView>( final val = await showCupertinoModalPopup<PrivateMessageView>(
context: context, context: context,
@ -383,8 +384,8 @@ class PrivateMessageTile extends HookWidget {
delayedLoading: deleteDelayed, delayedLoading: deleteDelayed,
icon: deleted.value ? Icons.restore : Icons.delete, icon: deleted.value ? Icons.restore : Icons.delete,
tooltip: deleted.value tooltip: deleted.value
? L10n.of(context).restore ? L10n.of(context)!.restore
: L10n.of(context).delete, : L10n.of(context)!.delete,
onPressed: handleDelete, onPressed: handleDelete,
), ),
] ]

View File

@ -30,9 +30,8 @@ class InstancePage extends HookWidget {
final Future<FullSiteView> siteFuture; final Future<FullSiteView> siteFuture;
final Future<List<CommunityView>> communitiesFuture; final Future<List<CommunityView>> communitiesFuture;
InstancePage({@required this.instanceHost}) InstancePage({required this.instanceHost})
: assert(instanceHost != null), : siteFuture = LemmyApiV3(instanceHost).run(const GetSite()),
siteFuture = LemmyApiV3(instanceHost).run(const GetSite()),
communitiesFuture = LemmyApiV3(instanceHost).run(const ListCommunities( communitiesFuture = LemmyApiV3(instanceHost).run(const ListCommunities(
type: PostListingType.local, sort: SortType.hot, limit: 6)); type: PostListingType.local, sort: SortType.hot, limit: 6));
@ -44,7 +43,7 @@ class InstancePage extends HookWidget {
final accStore = useAccountsStore(); final accStore = useAccountsStore();
final scrollController = useScrollController(); final scrollController = useScrollController();
if (!siteSnap.hasData) { if (!siteSnap.hasData || siteSnap.data!.siteView == null) {
return Scaffold( return Scaffold(
appBar: AppBar(), appBar: AppBar(),
body: Center( body: Center(
@ -57,7 +56,9 @@ class InstancePage extends HookWidget {
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
child: Text('ERROR: ${siteSnap.error}'), child: Text('ERROR: ${siteSnap.error}'),
) )
] else ] else if (siteSnap.data!.siteView == null)
const Text('ERROR')
else
const CircularProgressIndicator(semanticsLabel: 'loading') const CircularProgressIndicator(semanticsLabel: 'loading')
], ],
), ),
@ -65,7 +66,8 @@ class InstancePage extends HookWidget {
); );
} }
final site = siteSnap.data; final site = siteSnap.data!;
final siteView = site.siteView!;
void _share() => share('https://$instanceHost', context: context); void _share() => share('https://$instanceHost', context: context);
@ -110,7 +112,7 @@ class InstancePage extends HookWidget {
fade: true, fade: true,
scrollController: scrollController, scrollController: scrollController,
child: Text( child: Text(
site.siteView.site.name, siteView.site.name,
style: TextStyle(color: colorOnCard), style: TextStyle(color: colorOnCard),
), ),
), ),
@ -122,11 +124,11 @@ class InstancePage extends HookWidget {
], ],
flexibleSpace: FlexibleSpaceBar( flexibleSpace: FlexibleSpaceBar(
background: Stack(children: [ background: Stack(children: [
if (site.siteView.site.banner != null) if (siteView.site.banner != null)
FullscreenableImage( FullscreenableImage(
url: site.siteView.site.banner, url: siteView.site.banner!,
child: CachedNetworkImage( child: CachedNetworkImage(
imageUrl: site.siteView.site.banner, imageUrl: siteView.site.banner!,
errorWidget: (_, __, ___) => const SizedBox.shrink(), errorWidget: (_, __, ___) => const SizedBox.shrink(),
), ),
), ),
@ -136,20 +138,20 @@ class InstancePage extends HookWidget {
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.only(top: 40), padding: const EdgeInsets.only(top: 40),
child: site.siteView.site.icon == null child: siteView.site.icon == null
? const SizedBox(height: 100, width: 100) ? const SizedBox(height: 100, width: 100)
: FullscreenableImage( : FullscreenableImage(
url: site.siteView.site.icon, url: siteView.site.icon!,
child: CachedNetworkImage( child: CachedNetworkImage(
width: 100, width: 100,
height: 100, height: 100,
imageUrl: site.siteView.site.icon, imageUrl: siteView.site.icon!,
errorWidget: (_, __, ___) => errorWidget: (_, __, ___) =>
const Icon(Icons.warning), const Icon(Icons.warning),
), ),
), ),
), ),
Text(site.siteView.site.name, Text(siteView.site.name,
style: theme.textTheme.headline6), style: theme.textTheme.headline6),
Text(instanceHost, style: theme.textTheme.caption) Text(instanceHost, style: theme.textTheme.caption)
], ],
@ -164,8 +166,8 @@ class InstancePage extends HookWidget {
color: theme.cardColor, color: theme.cardColor,
child: TabBar( child: TabBar(
tabs: [ tabs: [
Tab(text: L10n.of(context).posts), Tab(text: L10n.of(context)!.posts),
Tab(text: L10n.of(context).comments), Tab(text: L10n.of(context)!.comments),
const Tab(text: 'About'), const Tab(text: 'About'),
], ],
), ),
@ -212,17 +214,18 @@ class _AboutTab extends HookWidget {
final Future<List<CommunityView>> communitiesFuture; final Future<List<CommunityView>> communitiesFuture;
final String instanceHost; final String instanceHost;
const _AboutTab(this.site, const _AboutTab(
{@required this.communitiesFuture, @required this.instanceHost}) this.site, {
: assert(communitiesFuture != null), required this.communitiesFuture,
assert(instanceHost != null); required this.instanceHost,
});
void goToBannedUsers(BuildContext context) { void goToBannedUsers(BuildContext context) {
goTo( goTo(
context, context,
(_) => UsersListPage( (_) => UsersListPage(
users: site.banned.reversed.toList(), users: site.banned.reversed.toList(),
title: L10n.of(context).banned_users, title: L10n.of(context)!.banned_users,
), ),
); );
} }
@ -246,24 +249,38 @@ class _AboutTab extends HookWidget {
auth: accStore.defaultTokenFor(instanceHost)?.raw, auth: accStore.defaultTokenFor(instanceHost)?.raw,
), ),
), ),
title: 'Communities of ${site.siteView.site.name}', title: 'Communities of ${site.siteView?.site.name}',
), ),
); );
} }
final siteView = site.siteView;
if (siteView == null) {
return const SingleChildScrollView(
child: Center(
child: Padding(
padding: EdgeInsets.all(16),
child: Text('error'),
)));
}
return SingleChildScrollView( return SingleChildScrollView(
child: SafeArea( child: SafeArea(
top: false, top: false,
child: Column( child: Column(
children: [ children: [
Padding( if (siteView.site.description != null) ...[
padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 15), Padding(
child: MarkdownText( padding:
site.siteView.site.description, const EdgeInsets.symmetric(horizontal: 15, vertical: 15),
instanceHost: instanceHost, child: MarkdownText(
siteView.site.description!,
instanceHost: instanceHost,
),
), ),
), const _Divider(),
const _Divider(), ],
SizedBox( SizedBox(
height: 32, height: 32,
child: ListView( child: ListView(
@ -271,17 +288,18 @@ class _AboutTab extends HookWidget {
padding: const EdgeInsets.symmetric(horizontal: 15), padding: const EdgeInsets.symmetric(horizontal: 15),
children: [ children: [
Chip( Chip(
label: Text(L10n.of(context) label: Text(L10n.of(context)!
.number_of_users_online(site.online))), .number_of_users_online(site.online))),
Chip( Chip(
label: Text(L10n.of(context) label: Text(L10n.of(context)!
.number_of_users(site.siteView.counts.users))), .number_of_users(site.siteView!.counts.users))),
Chip( Chip(
label: Text( label: Text(
'${site.siteView.counts.communities} communities')), '${site.siteView!.counts.communities} communities')),
Chip(label: Text('${site.siteView.counts.posts} posts')), Chip(label: Text('${site.siteView!.counts.posts} posts')),
Chip( Chip(
label: Text('${site.siteView.counts.comments} comments')), label:
Text('${site.siteView!.counts.comments} comments')),
].spaced(8), ].spaced(8),
), ),
), ),
@ -290,12 +308,12 @@ class _AboutTab extends HookWidget {
title: Center( title: Center(
child: Text( child: Text(
'Trending communities:', 'Trending communities:',
style: theme.textTheme.headline6.copyWith(fontSize: 18), style: theme.textTheme.headline6?.copyWith(fontSize: 18),
), ),
), ),
), ),
if (commSnap.hasData) if (commSnap.hasData)
for (final c in commSnap.data) for (final c in commSnap.data!)
ListTile( ListTile(
onTap: () => goToCommunity.byId( onTap: () => goToCommunity.byId(
context, c.instanceHost, c.community.id), context, c.instanceHost, c.community.id),
@ -321,7 +339,7 @@ class _AboutTab extends HookWidget {
title: Center( title: Center(
child: Text( child: Text(
'Admins:', 'Admins:',
style: theme.textTheme.headline6.copyWith(fontSize: 18), style: theme.textTheme.headline6?.copyWith(fontSize: 18),
), ),
), ),
), ),
@ -329,18 +347,18 @@ class _AboutTab extends HookWidget {
ListTile( ListTile(
title: Text(u.person.originDisplayName), title: Text(u.person.originDisplayName),
subtitle: u.person.bio != null subtitle: u.person.bio != null
? MarkdownText(u.person.bio, instanceHost: instanceHost) ? MarkdownText(u.person.bio!, instanceHost: instanceHost)
: null, : null,
onTap: () => goToUser.fromPersonSafe(context, u.person), onTap: () => goToUser.fromPersonSafe(context, u.person),
leading: Avatar(url: u.person.avatar), leading: Avatar(url: u.person.avatar),
), ),
const _Divider(), const _Divider(),
ListTile( ListTile(
title: Center(child: Text(L10n.of(context).banned_users)), title: Center(child: Text(L10n.of(context)!.banned_users)),
onTap: () => goToBannedUsers(context), onTap: () => goToBannedUsers(context),
), ),
ListTile( ListTile(
title: Center(child: Text(L10n.of(context).modlog)), title: Center(child: Text(L10n.of(context)!.modlog)),
onTap: () => goTo( onTap: () => goTo(
context, context,
(context) => ModlogPage.forInstance(instanceHost: instanceHost), (context) => ModlogPage.forInstance(instanceHost: instanceHost),

View File

@ -21,10 +21,7 @@ class ManageAccountPage extends HookWidget {
final String instanceHost; final String instanceHost;
final String username; final String username;
const ManageAccountPage( const ManageAccountPage({required this.instanceHost, required this.username});
{@required this.instanceHost, @required this.username})
: assert(instanceHost != null),
assert(username != null);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -32,9 +29,9 @@ class ManageAccountPage extends HookWidget {
final userFuture = useMemoized(() async { final userFuture = useMemoized(() async {
final site = await LemmyApiV3(instanceHost).run( final site = await LemmyApiV3(instanceHost).run(
GetSite(auth: accountStore.tokenFor(instanceHost, username).raw)); GetSite(auth: accountStore.tokenFor(instanceHost, username)!.raw));
return site.myUser; return site.myUser!;
}); });
return Scaffold( return Scaffold(
@ -51,7 +48,7 @@ class ManageAccountPage extends HookWidget {
return const Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator());
} }
return _ManageAccount(user: userSnap.data); return _ManageAccount(user: userSnap.data!);
}, },
), ),
); );
@ -59,9 +56,7 @@ class ManageAccountPage extends HookWidget {
} }
class _ManageAccount extends HookWidget { class _ManageAccount extends HookWidget {
const _ManageAccount({Key key, @required this.user}) const _ManageAccount({Key? key, required this.user}) : super(key: key);
: assert(user != null),
super(key: key);
final LocalUserSettingsView user; final LocalUserSettingsView user;
@ -91,12 +86,12 @@ class _ManageAccount extends HookWidget {
final newPasswordVerifyController = useTextEditingController(); final newPasswordVerifyController = useTextEditingController();
final oldPasswordController = useTextEditingController(); final oldPasswordController = useTextEditingController();
final informAcceptedAvatarRef = useRef<VoidCallback>(null); final informAcceptedAvatarRef = useRef<VoidCallback?>(null);
final informAcceptedBannerRef = useRef<VoidCallback>(null); final informAcceptedBannerRef = useRef<VoidCallback?>(null);
final deleteAccountPasswordController = useTextEditingController(); final deleteAccountPasswordController = useTextEditingController();
final token = accountsStore.tokenFor(user.instanceHost, user.person.name); final token = accountsStore.tokenFor(user.instanceHost, user.person.name)!;
handleSubmit() async { handleSubmit() async {
saveDelayedLoading.start(); saveDelayedLoading.start();
@ -132,8 +127,8 @@ class _ManageAccount extends HookWidget {
email: emailController.text.isEmpty ? null : emailController.text, email: emailController.text.isEmpty ? null : emailController.text,
)); ));
informAcceptedAvatarRef.current(); informAcceptedAvatarRef.current?.call();
informAcceptedBannerRef.current(); informAcceptedBannerRef.current?.call();
ScaffoldMessenger.of(context).showSnackBar(const SnackBar( ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
content: Text('User settings saved'), content: Text('User settings saved'),
@ -152,28 +147,28 @@ class _ManageAccount extends HookWidget {
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: Text( title: Text(
'${L10n.of(context).delete_account} @${user.instanceHost}@${user.person.name}'), '${L10n.of(context)!.delete_account} @${user.instanceHost}@${user.person.name}'),
content: Column( content: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text(L10n.of(context).delete_account_confirm), Text(L10n.of(context)!.delete_account_confirm),
const SizedBox(height: 10), const SizedBox(height: 10),
TextField( TextField(
controller: deleteAccountPasswordController, controller: deleteAccountPasswordController,
obscureText: true, obscureText: true,
decoration: decoration:
InputDecoration(hintText: L10n.of(context).password), InputDecoration(hintText: L10n.of(context)!.password),
) )
], ],
), ),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.of(context).pop(false), onPressed: () => Navigator.of(context).pop(false),
child: Text(L10n.of(context).no), child: Text(L10n.of(context)!.no),
), ),
TextButton( TextButton(
onPressed: () => Navigator.of(context).pop(true), onPressed: () => Navigator.of(context).pop(true),
child: Text(L10n.of(context).yes), child: Text(L10n.of(context)!.yes),
), ),
], ],
), ),
@ -209,7 +204,7 @@ class _ManageAccount extends HookWidget {
children: [ children: [
_ImagePicker( _ImagePicker(
user: user, user: user,
name: L10n.of(context).avatar, name: L10n.of(context)!.avatar,
initialUrl: avatar.current, initialUrl: avatar.current,
onChange: (value) => avatar.current = value, onChange: (value) => avatar.current = value,
informAcceptedRef: informAcceptedAvatarRef, informAcceptedRef: informAcceptedAvatarRef,
@ -217,42 +212,42 @@ class _ManageAccount extends HookWidget {
const SizedBox(height: 8), const SizedBox(height: 8),
_ImagePicker( _ImagePicker(
user: user, user: user,
name: L10n.of(context).banner, name: L10n.of(context)!.banner,
initialUrl: banner.current, initialUrl: banner.current,
onChange: (value) => banner.current = value, onChange: (value) => banner.current = value,
informAcceptedRef: informAcceptedBannerRef, informAcceptedRef: informAcceptedBannerRef,
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Text(L10n.of(context).display_name, style: theme.textTheme.headline6), Text(L10n.of(context)!.display_name, style: theme.textTheme.headline6),
TextField(controller: displayNameController), TextField(controller: displayNameController),
const SizedBox(height: 8), const SizedBox(height: 8),
Text(L10n.of(context).bio, style: theme.textTheme.headline6), Text(L10n.of(context)!.bio, style: theme.textTheme.headline6),
TextField( TextField(
controller: bioController, controller: bioController,
minLines: 4, minLines: 4,
maxLines: 10, maxLines: 10,
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Text(L10n.of(context).email, style: theme.textTheme.headline6), Text(L10n.of(context)!.email, style: theme.textTheme.headline6),
TextField(controller: emailController), TextField(controller: emailController),
const SizedBox(height: 8), const SizedBox(height: 8),
Text(L10n.of(context).matrix_user, style: theme.textTheme.headline6), Text(L10n.of(context)!.matrix_user, style: theme.textTheme.headline6),
TextField(controller: matrixUserController), TextField(controller: matrixUserController),
const SizedBox(height: 8), const SizedBox(height: 8),
Text(L10n.of(context).new_password, style: theme.textTheme.headline6), Text(L10n.of(context)!.new_password, style: theme.textTheme.headline6),
TextField( TextField(
controller: newPasswordController, controller: newPasswordController,
obscureText: true, obscureText: true,
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Text(L10n.of(context).verify_password, Text(L10n.of(context)!.verify_password,
style: theme.textTheme.headline6), style: theme.textTheme.headline6),
TextField( TextField(
controller: newPasswordVerifyController, controller: newPasswordVerifyController,
obscureText: true, obscureText: true,
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Text(L10n.of(context).old_password, style: theme.textTheme.headline6), Text(L10n.of(context)!.old_password, style: theme.textTheme.headline6),
TextField( TextField(
controller: oldPasswordController, controller: oldPasswordController,
obscureText: true, obscureText: true,
@ -264,7 +259,7 @@ class _ManageAccount extends HookWidget {
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text(L10n.of(context).type), Text(L10n.of(context)!.type),
const Text( const Text(
'This has currently no effect on lemmur', 'This has currently no effect on lemmur',
style: TextStyle(fontSize: 10), style: TextStyle(fontSize: 10),
@ -290,7 +285,7 @@ class _ManageAccount extends HookWidget {
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text(L10n.of(context).sort_type), Text(L10n.of(context)!.sort_type),
const Text( const Text(
'This has currently no effect on lemmur', 'This has currently no effect on lemmur',
style: TextStyle(fontSize: 10), style: TextStyle(fontSize: 10),
@ -308,24 +303,30 @@ class _ManageAccount extends HookWidget {
const SizedBox(height: 8), const SizedBox(height: 8),
CheckboxListTile( CheckboxListTile(
value: showAvatars.value, value: showAvatars.value,
onChanged: (checked) => showAvatars.value = checked, onChanged: (checked) {
title: Text(L10n.of(context).show_avatars), if (checked != null) showAvatars.value = checked;
},
title: Text(L10n.of(context)!.show_avatars),
subtitle: const Text('This has currently no effect on lemmur'), subtitle: const Text('This has currently no effect on lemmur'),
dense: true, dense: true,
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
CheckboxListTile( CheckboxListTile(
value: showNsfw.value, value: showNsfw.value,
onChanged: (checked) => showNsfw.value = checked, onChanged: (checked) {
title: Text(L10n.of(context).show_nsfw), if (checked != null) showNsfw.value = checked;
},
title: Text(L10n.of(context)!.show_nsfw),
subtitle: const Text('This has currently no effect on lemmur'), subtitle: const Text('This has currently no effect on lemmur'),
dense: true, dense: true,
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
CheckboxListTile( CheckboxListTile(
value: sendNotificationsToEmail.value, value: sendNotificationsToEmail.value,
onChanged: (checked) => sendNotificationsToEmail.value = checked, onChanged: (checked) {
title: Text(L10n.of(context).send_notifications_to_email), if (checked != null) sendNotificationsToEmail.value = checked;
},
title: Text(L10n.of(context)!.send_notifications_to_email),
dense: true, dense: true,
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
@ -337,7 +338,7 @@ class _ManageAccount extends HookWidget {
height: 20, height: 20,
child: CircularProgressIndicator(), child: CircularProgressIndicator(),
) )
: Text(L10n.of(context).save), : Text(L10n.of(context)!.save),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
ElevatedButton( ElevatedButton(
@ -345,7 +346,7 @@ class _ManageAccount extends HookWidget {
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
primary: Colors.red, primary: Colors.red,
), ),
child: Text(L10n.of(context).delete_account.toUpperCase()), child: Text(L10n.of(context)!.delete_account.toUpperCase()),
), ),
const BottomSafe(), const BottomSafe(),
], ],
@ -356,25 +357,23 @@ class _ManageAccount extends HookWidget {
/// Picker and cleanuper for local images uploaded to pictrs /// Picker and cleanuper for local images uploaded to pictrs
class _ImagePicker extends HookWidget { class _ImagePicker extends HookWidget {
final String name; final String name;
final String initialUrl; final String? initialUrl;
final LocalUserSettingsView user; final LocalUserSettingsView user;
final ValueChanged<String> onChange; final ValueChanged<String?>? onChange;
/// _ImagePicker will set the ref to a callback that can inform _ImagePicker /// _ImagePicker will set the ref to a callback that can inform _ImagePicker
/// that the current picture is accepted /// that the current picture is accepted
/// and should no longer allow for deletion of it /// and should no longer allow for deletion of it
final Ref<VoidCallback> informAcceptedRef; final Ref<VoidCallback?> informAcceptedRef;
const _ImagePicker({ const _ImagePicker({
Key key, Key? key,
@required this.initialUrl, required this.initialUrl,
@required this.name, required this.name,
@required this.user, required this.user,
@required this.onChange, required this.onChange,
@required this.informAcceptedRef, required this.informAcceptedRef,
}) : assert(name != null), }) : super(key: key);
assert(user != null),
super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -383,7 +382,7 @@ class _ImagePicker extends HookWidget {
final initialUrl = useRef(this.initialUrl); final initialUrl = useRef(this.initialUrl);
final theme = Theme.of(context); final theme = Theme.of(context);
final url = useState(initialUrl.current); final url = useState(initialUrl.current);
final pictrsDeleteToken = useState<PictrsUploadFile>(null); final pictrsDeleteToken = useState<PictrsUploadFile?>(null);
final imagePicker = useImagePicker(); final imagePicker = useImagePicker();
final accountsStore = useAccountsStore(); final accountsStore = useAccountsStore();
@ -398,14 +397,15 @@ class _ImagePicker extends HookWidget {
final upload = await PictrsApi(user.instanceHost).upload( final upload = await PictrsApi(user.instanceHost).upload(
filePath: pic.path, filePath: pic.path,
auth: auth: accountsStore
accountsStore.tokenFor(user.instanceHost, user.person.name).raw, .tokenFor(user.instanceHost, user.person.name)!
.raw,
); );
pictrsDeleteToken.value = upload.files[0]; pictrsDeleteToken.value = upload.files[0];
url.value = url.value =
pathToPictrs(user.instanceHost, pictrsDeleteToken.value.file); pathToPictrs(user.instanceHost, pictrsDeleteToken.value!.file);
onChange?.call(url.value); onChange?.call(url.value!);
} }
} on Exception catch (_) { } on Exception catch (_) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
@ -415,10 +415,11 @@ class _ImagePicker extends HookWidget {
delayedLoading.cancel(); delayedLoading.cancel();
} }
removePicture({bool updateState = true}) { removePicture({
PictrsApi(user.instanceHost) bool updateState = true,
.delete(pictrsDeleteToken.value) required PictrsUploadFile pictrsToken,
.catchError((_) {}); }) {
PictrsApi(user.instanceHost).delete(pictrsToken).catchError((_) {});
if (updateState) { if (updateState) {
pictrsDeleteToken.value = null; pictrsDeleteToken.value = null;
@ -436,7 +437,10 @@ class _ImagePicker extends HookWidget {
return () { return () {
// remove picture from pictrs when exiting // remove picture from pictrs when exiting
if (pictrsDeleteToken.value != null) { if (pictrsDeleteToken.value != null) {
removePicture(updateState: false); removePicture(
updateState: false,
pictrsToken: pictrsDeleteToken.value!,
);
} }
}; };
}, []); }, []);
@ -462,13 +466,14 @@ class _ImagePicker extends HookWidget {
else else
IconButton( IconButton(
icon: const Icon(Icons.close), icon: const Icon(Icons.close),
onPressed: removePicture, onPressed: () =>
removePicture(pictrsToken: pictrsDeleteToken.value!),
) )
], ],
), ),
if (url.value != null) if (url.value != null)
CachedNetworkImage( CachedNetworkImage(
imageUrl: url.value, imageUrl: url.value!,
errorWidget: (_, __, ___) => const Icon(Icons.error), errorWidget: (_, __, ___) => const Icon(Icons.error),
), ),
], ],

View File

@ -26,7 +26,7 @@ class MediaViewPage extends HookWidget {
final isDragging = useState(false); final isDragging = useState(false);
final offset = useState(Offset.zero); final offset = useState(Offset.zero);
final prevOffset = usePrevious(offset.value); final prevOffset = usePrevious(offset.value) ?? Offset.zero;
notImplemented() { notImplemented() {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar( ScaffoldMessenger.of(context).showSnackBar(const SnackBar(

View File

@ -12,22 +12,18 @@ import '../widgets/bottom_safe.dart';
class ModlogPage extends HookWidget { class ModlogPage extends HookWidget {
final String instanceHost; final String instanceHost;
final String name; final String name;
final int communityId; final int? communityId;
const ModlogPage.forInstance({ const ModlogPage.forInstance({
@required this.instanceHost, required this.instanceHost,
}) : assert(instanceHost != null), }) : communityId = null,
communityId = null,
name = instanceHost; name = instanceHost;
const ModlogPage.forCommunity({ const ModlogPage.forCommunity({
@required this.instanceHost, required this.instanceHost,
@required this.communityId, required int this.communityId,
@required String communityName, required String communityName,
}) : assert(instanceHost != null), }) : name = '!$communityName';
assert(communityId != null),
assert(communityName != null),
name = '!$communityName';
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -65,7 +61,7 @@ class ModlogPage extends HookWidget {
return Center( return Center(
child: Text('Error: ${snapshot.error?.toString()}')); child: Text('Error: ${snapshot.error?.toString()}'));
} }
final modlog = snapshot.data; final modlog = snapshot.requireData;
if (modlog.added.length + if (modlog.added.length +
modlog.addedToCommunity.length + modlog.addedToCommunity.length +
@ -78,7 +74,7 @@ class ModlogPage extends HookWidget {
modlog.stickiedPosts.length == modlog.stickiedPosts.length ==
0) { 0) {
WidgetsBinding.instance WidgetsBinding.instance
.addPostFrameCallback((_) => isDone.value = true); ?.addPostFrameCallback((_) => isDone.value = true);
return const Center(child: Text('no more logs to show')); return const Center(child: Text('no more logs to show'));
} }
@ -118,9 +114,7 @@ class ModlogPage extends HookWidget {
} }
class _ModlogTable extends StatelessWidget { class _ModlogTable extends StatelessWidget {
const _ModlogTable({Key key, @required this.modlog}) const _ModlogTable({Key? key, required this.modlog}) : super(key: key);
: assert(modlog != null),
super(key: key);
final Modlog modlog; final Modlog modlog;
@ -179,7 +173,7 @@ class _ModlogTable extends StatelessWidget {
RichText( RichText(
text: TextSpan( text: TextSpan(
children: [ children: [
if (removedPost.modRemovePost.removed) if (removedPost.modRemovePost.removed ?? false)
const TextSpan(text: 'removed') const TextSpan(text: 'removed')
else else
const TextSpan(text: 'restored'), const TextSpan(text: 'restored'),
@ -205,7 +199,7 @@ class _ModlogTable extends StatelessWidget {
RichText( RichText(
text: TextSpan( text: TextSpan(
children: [ children: [
if (lockedPost.modLockPost.locked) if (lockedPost.modLockPost.locked ?? false)
const TextSpan(text: 'locked') const TextSpan(text: 'locked')
else else
const TextSpan(text: 'unlocked'), const TextSpan(text: 'unlocked'),
@ -231,7 +225,7 @@ class _ModlogTable extends StatelessWidget {
RichText( RichText(
text: TextSpan( text: TextSpan(
children: [ children: [
if (stickiedPost.modStickyPost.stickied) if (stickiedPost.modStickyPost.stickied ?? false)
const TextSpan(text: 'stickied') const TextSpan(text: 'stickied')
else else
const TextSpan(text: 'unstickied'), const TextSpan(text: 'unstickied'),
@ -257,7 +251,7 @@ class _ModlogTable extends StatelessWidget {
RichText( RichText(
text: TextSpan( text: TextSpan(
children: [ children: [
if (removedComment.modRemoveComment.removed) if (removedComment.modRemoveComment.removed ?? false)
const TextSpan(text: 'removed') const TextSpan(text: 'removed')
else else
const TextSpan(text: 'restored'), const TextSpan(text: 'restored'),
@ -286,7 +280,7 @@ class _ModlogTable extends StatelessWidget {
RichText( RichText(
text: TextSpan( text: TextSpan(
children: [ children: [
if (removedCommunity.modRemoveCommunity.removed) if (removedCommunity.modRemoveCommunity.removed ?? false)
const TextSpan(text: 'removed') const TextSpan(text: 'removed')
else else
const TextSpan(text: 'restored'), const TextSpan(text: 'restored'),
@ -303,7 +297,7 @@ class _ModlogTable extends StatelessWidget {
RichText( RichText(
text: TextSpan( text: TextSpan(
children: [ children: [
if (bannedFromCommunity.modBanFromCommunity.banned) if (bannedFromCommunity.modBanFromCommunity.banned ?? false)
const TextSpan(text: 'banned ') const TextSpan(text: 'banned ')
else else
const TextSpan(text: 'unbanned '), const TextSpan(text: 'unbanned '),
@ -321,7 +315,7 @@ class _ModlogTable extends StatelessWidget {
RichText( RichText(
text: TextSpan( text: TextSpan(
children: [ children: [
if (banned.modBan.banned) if (banned.modBan.banned ?? false)
const TextSpan(text: 'banned ') const TextSpan(text: 'banned ')
else else
const TextSpan(text: 'unbanned '), const TextSpan(text: 'unbanned '),
@ -337,7 +331,7 @@ class _ModlogTable extends StatelessWidget {
RichText( RichText(
text: TextSpan( text: TextSpan(
children: [ children: [
if (addedToCommunity.modAddCommunity.removed) if (addedToCommunity.modAddCommunity.removed ?? false)
const TextSpan(text: 'removed ') const TextSpan(text: 'removed ')
else else
const TextSpan(text: 'appointed '), const TextSpan(text: 'appointed '),
@ -355,7 +349,7 @@ class _ModlogTable extends StatelessWidget {
RichText( RichText(
text: TextSpan( text: TextSpan(
children: [ children: [
if (added.modAdd.removed) if (added.modAdd.removed ?? false)
const TextSpan(text: 'removed ') const TextSpan(text: 'removed ')
else else
const TextSpan(text: 'apointed '), const TextSpan(text: 'apointed '),
@ -402,16 +396,14 @@ class _ModlogEntry {
final DateTime when; final DateTime when;
final PersonSafe mod; final PersonSafe mod;
final Widget action; final Widget action;
final String reason; final String? reason;
const _ModlogEntry({ const _ModlogEntry({
@required this.when, required this.when,
@required this.mod, required this.mod,
@required this.action, required this.action,
this.reason, this.reason,
}) : assert(when != null), });
assert(mod != null),
assert(action != null);
_ModlogEntry.fromModRemovePostView( _ModlogEntry.fromModRemovePostView(
ModRemovePostView removedPost, ModRemovePostView removedPost,
@ -539,7 +531,7 @@ class _ModlogEntry {
), ),
), ),
action, action,
if (reason == null) const Center(child: Text('-')) else Text(reason), if (reason == null) const Center(child: Text('-')) else Text(reason!),
] ]
.map( .map(
(widget) => Padding( (widget) => Padding(

View File

@ -80,7 +80,7 @@ class UserProfileTab extends HookWidget {
Text( Text(
// TODO: fix overflow issues // TODO: fix overflow issues
displayValue, displayValue,
style: theme.appBarTheme.textTheme.headline6, style: theme.appBarTheme.textTheme?.headline6,
overflow: TextOverflow.fade, overflow: TextOverflow.fade,
), ),
const Icon(Icons.expand_more), const Icon(Icons.expand_more),
@ -91,8 +91,8 @@ class UserProfileTab extends HookWidget {
actions: actions, actions: actions,
), ),
body: UserProfile( body: UserProfile(
userId: accountsStore.defaultToken.payload.sub, userId: accountsStore.defaultToken!.payload.sub,
instanceHost: accountsStore.defaultInstanceHost, instanceHost: accountsStore.defaultInstanceHost!,
), ),
); );
} }

View File

@ -13,15 +13,24 @@ class SavedPage extends HookWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final accountStore = useAccountsStore(); final accountStore = useAccountsStore();
if (accountStore.hasNoAccount) {
Scaffold(
appBar: AppBar(),
body: const Center(
child: Text('no account found'),
),
);
}
return DefaultTabController( return DefaultTabController(
length: 2, length: 2,
child: Scaffold( child: Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(L10n.of(context).saved), title: Text(L10n.of(context)!.saved),
bottom: TabBar( bottom: TabBar(
tabs: [ tabs: [
Tab(text: L10n.of(context).posts), Tab(text: L10n.of(context)!.posts),
Tab(text: L10n.of(context).comments), Tab(text: L10n.of(context)!.comments),
], ],
), ),
), ),
@ -29,27 +38,27 @@ class SavedPage extends HookWidget {
children: [ children: [
InfinitePostList( InfinitePostList(
fetcher: (page, batchSize, sortType) => fetcher: (page, batchSize, sortType) =>
LemmyApiV3(accountStore.defaultInstanceHost).run( LemmyApiV3(accountStore.defaultInstanceHost!).run(
GetPosts( GetPosts(
type: PostListingType.all, type: PostListingType.all,
sort: sortType, sort: sortType,
savedOnly: true, savedOnly: true,
page: page, page: page,
limit: batchSize, limit: batchSize,
auth: accountStore.defaultToken.raw, auth: accountStore.defaultToken!.raw,
), ),
), ),
), ),
InfiniteCommentList( InfiniteCommentList(
fetcher: (page, batchSize, sortType) => fetcher: (page, batchSize, sortType) =>
LemmyApiV3(accountStore.defaultInstanceHost).run( LemmyApiV3(accountStore.defaultInstanceHost!).run(
GetComments( GetComments(
type: CommentListingType.all, type: CommentListingType.all,
sort: sortType, sort: sortType,
savedOnly: true, savedOnly: true,
page: page, page: page,
limit: batchSize, limit: batchSize,
auth: accountStore.defaultToken.raw, auth: accountStore.defaultToken!.raw,
), ),
), ),
), ),

View File

@ -15,11 +15,9 @@ class SearchResultsPage extends HookWidget {
final String query; final String query;
SearchResultsPage({ SearchResultsPage({
@required this.instanceHost, required this.instanceHost,
@required this.query, required this.query,
}) : assert(instanceHost != null), }) : assert(instanceHost.isNotEmpty),
assert(query != null),
assert(instanceHost.isNotEmpty),
assert(query.isNotEmpty); assert(query.isNotEmpty);
@override @override
@ -31,10 +29,10 @@ class SearchResultsPage extends HookWidget {
bottom: TabBar( bottom: TabBar(
isScrollable: true, isScrollable: true,
tabs: [ tabs: [
Tab(text: L10n.of(context).posts), Tab(text: L10n.of(context)!.posts),
Tab(text: L10n.of(context).comments), Tab(text: L10n.of(context)!.comments),
Tab(text: L10n.of(context).users), Tab(text: L10n.of(context)!.users),
Tab(text: L10n.of(context).communities), Tab(text: L10n.of(context)!.communities),
], ],
), ),
), ),
@ -68,18 +66,16 @@ class _SearchResultsList extends HookWidget {
final String instanceHost; final String instanceHost;
const _SearchResultsList({ const _SearchResultsList({
@required this.type, required this.type,
@required this.query, required this.query,
@required this.instanceHost, required this.instanceHost,
}) : assert(type != null), });
assert(query != null),
assert(instanceHost != null);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final accStore = useAccountsStore(); final accStore = useAccountsStore();
return SortableInfiniteList( return SortableInfiniteList<Object>(
fetcher: (page, batchSize, sort) async { fetcher: (page, batchSize, sort) async {
final s = await LemmyApiV3(instanceHost).run(Search( final s = await LemmyApiV3(instanceHost).run(Search(
q: query, q: query,

View File

@ -1,3 +1,4 @@
import 'package:collection/collection.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
@ -18,7 +19,7 @@ class SearchTab extends HookWidget {
final accStore = useAccountsStore(); final accStore = useAccountsStore();
// null if there are no added instances // null if there are no added instances
final instanceHost = useState( final instanceHost = useState(
accStore.instances.firstWhere((_) => true, orElse: () => null), accStore.instances.firstWhereOrNull((_) => true),
); );
if (instanceHost.value == null) { if (instanceHost.value == null) {
@ -29,17 +30,18 @@ class SearchTab extends HookWidget {
), ),
); );
} }
return Scaffold( return Scaffold(
appBar: AppBar(), appBar: AppBar(),
body: GestureDetector( body: GestureDetector(
onTapDown: (_) => primaryFocus.unfocus(), onTapDown: (_) => primaryFocus?.unfocus(),
child: ListView( child: ListView(
padding: const EdgeInsets.symmetric(horizontal: 20), padding: const EdgeInsets.symmetric(horizontal: 20),
children: [ children: [
TextField( TextField(
controller: searchInputController, controller: searchInputController,
textAlign: TextAlign.center, textAlign: TextAlign.center,
decoration: InputDecoration(hintText: L10n.of(context).search), decoration: InputDecoration(hintText: L10n.of(context)!.search),
), ),
const SizedBox(height: 5), const SizedBox(height: 5),
Row( Row(
@ -52,7 +54,7 @@ class SearchTab extends HookWidget {
Expanded( Expanded(
child: RadioPicker<String>( child: RadioPicker<String>(
values: accStore.instances.toList(), values: accStore.instances.toList(),
groupValue: instanceHost.value, groupValue: instanceHost.value!,
onChanged: (value) => instanceHost.value = value, onChanged: (value) => instanceHost.value = value,
), ),
), ),
@ -63,10 +65,10 @@ class SearchTab extends HookWidget {
onPressed: () => goTo( onPressed: () => goTo(
context, context,
(c) => SearchResultsPage( (c) => SearchResultsPage(
instanceHost: instanceHost.value, instanceHost: instanceHost.value!,
query: searchInputController.text, query: searchInputController.text,
)), )),
child: Text(L10n.of(context).search), child: Text(L10n.of(context)!.search),
) )
], ],
), ),

View File

@ -21,7 +21,7 @@ class SettingsPage extends StatelessWidget {
@override @override
Widget build(BuildContext context) => Scaffold( Widget build(BuildContext context) => Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(L10n.of(context).settings), title: Text(L10n.of(context)!.settings),
), ),
body: ListView( body: ListView(
children: [ children: [
@ -66,7 +66,7 @@ class AppearanceConfigPage extends HookWidget {
title: Text(describeEnum(theme)), title: Text(describeEnum(theme)),
groupValue: configStore.theme, groupValue: configStore.theme,
onChanged: (selected) { onChanged: (selected) {
configStore.theme = selected; if (selected != null) configStore.theme = selected;
}, },
), ),
SwitchListTile( SwitchListTile(
@ -79,7 +79,7 @@ class AppearanceConfigPage extends HookWidget {
const SizedBox(height: 12), const SizedBox(height: 12),
const _SectionHeading('General'), const _SectionHeading('General'),
ListTile( ListTile(
title: Text(L10n.of(context).language), title: Text(L10n.of(context)!.language),
trailing: SizedBox( trailing: SizedBox(
width: 120, width: 120,
child: RadioPicker<Locale>( child: RadioPicker<Locale>(
@ -117,11 +117,11 @@ class AccountsConfigPage extends HookWidget {
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.of(context).pop(false), onPressed: () => Navigator.of(context).pop(false),
child: Text(L10n.of(context).no), child: Text(L10n.of(context)!.no),
), ),
TextButton( TextButton(
onPressed: () => Navigator.of(context).pop(true), onPressed: () => Navigator.of(context).pop(true),
child: Text(L10n.of(context).yes), child: Text(L10n.of(context)!.yes),
), ),
], ],
), ),
@ -142,11 +142,11 @@ class AccountsConfigPage extends HookWidget {
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.of(context).pop(false), onPressed: () => Navigator.of(context).pop(false),
child: Text(L10n.of(context).no), child: Text(L10n.of(context)!.no),
), ),
TextButton( TextButton(
onPressed: () => Navigator.of(context).pop(true), onPressed: () => Navigator.of(context).pop(true),
child: Text(L10n.of(context).yes), child: Text(L10n.of(context)!.yes),
), ),
], ],
), ),
@ -301,7 +301,7 @@ class _SectionHeading extends StatelessWidget {
return Padding( return Padding(
padding: const EdgeInsets.only(left: 20), padding: const EdgeInsets.only(left: 20),
child: Text(text.toUpperCase(), child: Text(text.toUpperCase(),
style: theme.textTheme.subtitle2.copyWith(color: theme.accentColor)), style: theme.textTheme.subtitle2?.copyWith(color: theme.accentColor)),
); );
} }
} }

View File

@ -10,20 +10,16 @@ import 'write_message.dart';
/// Page showing posts, comments, and general info about a user. /// Page showing posts, comments, and general info about a user.
class UserPage extends HookWidget { class UserPage extends HookWidget {
final int userId; final int? userId;
final String instanceHost; final String instanceHost;
final Future<FullPersonView> _userDetails; final Future<FullPersonView> _userDetails;
UserPage({@required this.userId, @required this.instanceHost}) UserPage({required this.userId, required this.instanceHost})
: assert(userId != null), : _userDetails = LemmyApiV3(instanceHost).run(GetPersonDetails(
assert(instanceHost != null),
_userDetails = LemmyApiV3(instanceHost).run(GetPersonDetails(
personId: userId, savedOnly: true, sort: SortType.active)); personId: userId, savedOnly: true, sort: SortType.active));
UserPage.fromName({@required this.instanceHost, @required String username}) UserPage.fromName({required this.instanceHost, required String username})
: assert(instanceHost != null), : userId = null,
assert(username != null),
userId = null,
_userDetails = LemmyApiV3(instanceHost).run(GetPersonDetails( _userDetails = LemmyApiV3(instanceHost).run(GetPersonDetails(
username: username, savedOnly: true, sort: SortType.active)); username: username, savedOnly: true, sort: SortType.active));
@ -33,7 +29,7 @@ class UserPage extends HookWidget {
final body = () { final body = () {
if (userDetailsSnap.hasData) { if (userDetailsSnap.hasData) {
return UserProfile.fromFullPersonView(userDetailsSnap.data); return UserProfile.fromFullPersonView(userDetailsSnap.data!);
} else if (userDetailsSnap.hasError) { } else if (userDetailsSnap.hasError) {
return const Center(child: Text('Could not find that user.')); return const Center(child: Text('Could not find that user.'));
} else { } else {
@ -46,11 +42,11 @@ class UserPage extends HookWidget {
appBar: AppBar( appBar: AppBar(
actions: [ actions: [
if (userDetailsSnap.hasData) ...[ if (userDetailsSnap.hasData) ...[
SendMessageButton(userDetailsSnap.data.personView.person), SendMessageButton(userDetailsSnap.data!.personView.person),
IconButton( IconButton(
icon: const Icon(Icons.share), icon: const Icon(Icons.share),
onPressed: () => share( onPressed: () => share(
userDetailsSnap.data.personView.person.actorId, userDetailsSnap.data!.personView.person.actorId,
context: context, context: context,
), ),
), ),

View File

@ -11,9 +11,8 @@ class UsersListPage extends StatelessWidget {
final String title; final String title;
final List<PersonViewSafe> users; final List<PersonViewSafe> users;
const UsersListPage({Key key, @required this.users, this.title}) const UsersListPage({Key? key, required this.users, this.title = ''})
: assert(users != null), : super(key: key);
super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -23,7 +22,7 @@ class UsersListPage extends StatelessWidget {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
backgroundColor: theme.cardColor, backgroundColor: theme.cardColor,
title: Text(title ?? ''), title: Text(title),
), ),
body: ListView.builder( body: ListView.builder(
itemBuilder: (context, i) => UsersListItem(user: users[i]), itemBuilder: (context, i) => UsersListItem(user: users[i]),
@ -36,9 +35,7 @@ class UsersListPage extends StatelessWidget {
class UsersListItem extends StatelessWidget { class UsersListItem extends StatelessWidget {
final PersonViewSafe user; final PersonViewSafe user;
const UsersListItem({Key key, @required this.user}) const UsersListItem({Key? key, required this.user}) : super(key: key);
: assert(user != null),
super(key: key);
@override @override
Widget build(BuildContext context) => ListTile( Widget build(BuildContext context) => ListTile(
@ -47,7 +44,7 @@ class UsersListItem extends StatelessWidget {
? Opacity( ? Opacity(
opacity: 0.5, opacity: 0.5,
child: MarkdownText( child: MarkdownText(
user.person.bio, user.person.bio!,
instanceHost: user.instanceHost, instanceHost: user.instanceHost,
), ),
) )

View File

@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:lemmy_api_client/v3.dart'; import 'package:lemmy_api_client/v3.dart';
import '../hooks/stores.dart'; import '../hooks/logged_in_action.dart';
import '../l10n/l10n.dart'; import '../l10n/l10n.dart';
import '../util/extensions/api.dart'; import '../util/extensions/api.dart';
import '../widgets/markdown_mode_icon.dart'; import '../widgets/markdown_mode_icon.dart';
@ -14,16 +14,14 @@ class WriteMessagePage extends HookWidget {
final String instanceHost; final String instanceHost;
/// if it's non null then this page is used for edit /// if it's non null then this page is used for edit
final PrivateMessage privateMessage; final PrivateMessage? privateMessage;
final bool _isEdit; final bool _isEdit;
const WriteMessagePage.send({ const WriteMessagePage.send({
@required this.recipient, required this.recipient,
@required this.instanceHost, required this.instanceHost,
}) : assert(recipient != null), }) : privateMessage = null,
assert(instanceHost != null),
privateMessage = null,
_isEdit = false; _isEdit = false;
WriteMessagePage.edit(PrivateMessageView pmv) WriteMessagePage.edit(PrivateMessageView pmv)
@ -34,22 +32,21 @@ class WriteMessagePage extends HookWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final accStore = useAccountsStore();
final showFancy = useState(false); final showFancy = useState(false);
final bodyController = final bodyController =
useTextEditingController(text: privateMessage?.content); useTextEditingController(text: privateMessage?.content);
final loading = useState(false); final loading = useState(false);
final loggedInAction = useLoggedInAction(instanceHost);
final submit = _isEdit ? L10n.of(context)!.save : 'send';
final title = _isEdit ? 'Edit message' : L10n.of(context)!.send_message;
final submit = _isEdit ? L10n.of(context).save : 'send'; handleSubmit(Jwt token) async {
final title = _isEdit ? 'Edit message' : L10n.of(context).send_message;
handleSubmit() async {
if (_isEdit) { if (_isEdit) {
loading.value = true; loading.value = true;
try { try {
final msg = await LemmyApiV3(instanceHost).run(EditPrivateMessage( final msg = await LemmyApiV3(instanceHost).run(EditPrivateMessage(
auth: accStore.defaultTokenFor(instanceHost)?.raw, auth: token.raw,
privateMessageId: privateMessage.id, privateMessageId: privateMessage!.id,
content: bodyController.text, content: bodyController.text,
)); ));
Navigator.of(context).pop(msg); Navigator.of(context).pop(msg);
@ -65,7 +62,7 @@ class WriteMessagePage extends HookWidget {
loading.value = true; loading.value = true;
try { try {
await LemmyApiV3(instanceHost).run(CreatePrivateMessage( await LemmyApiV3(instanceHost).run(CreatePrivateMessage(
auth: accStore.defaultTokenFor(instanceHost)?.raw, auth: token.raw,
content: bodyController.text, content: bodyController.text,
recipientId: recipient.id, recipientId: recipient.id,
)); ));
@ -124,7 +121,7 @@ class WriteMessagePage extends HookWidget {
Align( Align(
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
child: TextButton( child: TextButton(
onPressed: loading.value ? () {} : handleSubmit, onPressed: loading.value ? () {} : loggedInAction(handleSubmit),
child: loading.value child: loading.value
? const SizedBox( ? const SizedBox(
height: 20, height: 20,

View File

@ -20,18 +20,18 @@ class AccountsStore extends ChangeNotifier {
/// `tokens['instanceHost']['username']` /// `tokens['instanceHost']['username']`
@protected @protected
@JsonKey(defaultValue: {'lemmy.ml': {}}) @JsonKey(defaultValue: {'lemmy.ml': {}})
Map<String, Map<String, Jwt>> tokens; late Map<String, Map<String, Jwt>> tokens;
/// default account for a given instance /// default account for a given instance
/// map where keys are instanceHosts and values are usernames /// map where keys are instanceHosts and values are usernames
@protected @protected
@JsonKey(defaultValue: {}) @JsonKey(defaultValue: {})
Map<String, String> defaultAccounts; late Map<String, String> defaultAccounts;
/// default account for the app /// default account for the app
/// It is in a form of `username@instanceHost` /// It is in a form of `username@instanceHost`
@protected @protected
String defaultAccount; String? defaultAccount;
static Future<AccountsStore> load() async { static Future<AccountsStore> load() async {
final prefs = await _prefs; final prefs = await _prefs;
@ -63,8 +63,8 @@ class AccountsStore extends ChangeNotifier {
.toList() .toList()
.forEach(defaultAccounts.remove); .forEach(defaultAccounts.remove);
if (defaultAccount != null) { if (defaultAccount != null) {
final instance = defaultAccount.split('@')[1]; final instance = defaultAccount!.split('@')[1];
final username = defaultAccount.split('@')[0]; final username = defaultAccount!.split('@')[0];
// if instance or username doesn't exist, remove // if instance or username doesn't exist, remove
if (!instances.contains(instance) || if (!instances.contains(instance) ||
!usernamesFor(instance).contains(username)) { !usernamesFor(instance).contains(username)) {
@ -97,23 +97,23 @@ class AccountsStore extends ChangeNotifier {
} }
} }
String get defaultUsername { String? get defaultUsername {
if (defaultAccount == null) { // if (defaultAccount == null) {
return null; // return null;
} // }
return defaultAccount.split('@')[0]; return defaultAccount?.split('@')[0];
} }
String get defaultInstanceHost { String? get defaultInstanceHost {
if (defaultAccount == null) { // if (defaultAccount == null) {
return null; // return null;
} // }
return defaultAccount.split('@')[1]; return defaultAccount?.split('@')[1];
} }
String defaultUsernameFor(String instanceHost) { String? defaultUsernameFor(String instanceHost) {
if (isAnonymousFor(instanceHost)) { if (isAnonymousFor(instanceHost)) {
return null; return null;
} }
@ -121,29 +121,29 @@ class AccountsStore extends ChangeNotifier {
return defaultAccounts[instanceHost]; return defaultAccounts[instanceHost];
} }
Jwt get defaultToken { Jwt? get defaultToken {
if (defaultAccount == null) { if (defaultAccount == null) {
return null; return null;
} }
final userTag = defaultAccount.split('@'); final userTag = defaultAccount!.split('@');
return tokens[userTag[1]][userTag[0]]; return tokens[userTag[1]]?[userTag[0]];
} }
Jwt defaultTokenFor(String instanceHost) { Jwt? defaultTokenFor(String instanceHost) {
if (isAnonymousFor(instanceHost)) { if (isAnonymousFor(instanceHost)) {
return null; return null;
} }
return tokens[instanceHost][defaultAccounts[instanceHost]]; return tokens[instanceHost]?[defaultAccounts[instanceHost]];
} }
Jwt tokenFor(String instanceHost, String username) { Jwt? tokenFor(String instanceHost, String username) {
if (!usernamesFor(instanceHost).contains(username)) { if (!usernamesFor(instanceHost).contains(username)) {
return null; return null;
} }
return tokens[instanceHost][username]; return tokens[instanceHost]?[username];
} }
/// sets globally default account /// sets globally default account
@ -169,7 +169,7 @@ class AccountsStore extends ChangeNotifier {
return true; return true;
} }
return tokens[instanceHost].isEmpty; return tokens[instanceHost]!.isEmpty;
} }
/// `true` if no added instance has an account assigned to it /// `true` if no added instance has an account assigned to it
@ -182,7 +182,7 @@ class AccountsStore extends ChangeNotifier {
/// Usernames that are assigned to a given instance /// Usernames that are assigned to a given instance
Iterable<String> usernamesFor(String instanceHost) => Iterable<String> usernamesFor(String instanceHost) =>
tokens[instanceHost].keys; tokens[instanceHost]?.keys ?? const Iterable.empty();
/// adds a new account /// adds a new account
/// if it's the first account ever the account is /// if it's the first account ever the account is
@ -203,10 +203,11 @@ class AccountsStore extends ChangeNotifier {
usernameOrEmail: usernameOrEmail, usernameOrEmail: usernameOrEmail,
password: password, password: password,
)); ));
final userData = final userData = await lemmy
await lemmy.run(GetSite(auth: token.raw)).then((value) => value.myUser); .run(GetSite(auth: token.raw))
.then((value) => value.myUser!);
tokens[instanceHost][userData.person.name] = token.copyWith( tokens[instanceHost]![userData.person.name] = token.copyWith(
payload: token.payload.copyWith(sub: userData.person.id), payload: token.payload.copyWith(sub: userData.person.id),
); );
@ -252,7 +253,11 @@ class AccountsStore extends ChangeNotifier {
} }
Future<void> removeAccount(String instanceHost, String username) async { Future<void> removeAccount(String instanceHost, String username) async {
tokens[instanceHost].remove(username); if (!tokens.containsKey(instanceHost)) {
throw Exception("instance doesn't exist");
}
tokens[instanceHost]!.remove(username);
await _assignDefaultAccounts(); await _assignDefaultAccounts();
notifyListeners(); notifyListeners();

View File

@ -15,7 +15,7 @@ class ConfigStore extends ChangeNotifier {
static const prefsKey = 'v1:ConfigStore'; static const prefsKey = 'v1:ConfigStore';
static final _prefs = SharedPreferences.getInstance(); static final _prefs = SharedPreferences.getInstance();
ThemeMode _theme; late ThemeMode _theme;
@JsonKey(defaultValue: ThemeMode.system) @JsonKey(defaultValue: ThemeMode.system)
ThemeMode get theme => _theme; ThemeMode get theme => _theme;
set theme(ThemeMode theme) { set theme(ThemeMode theme) {
@ -24,7 +24,7 @@ class ConfigStore extends ChangeNotifier {
save(); save();
} }
bool _amoledDarkMode; late bool _amoledDarkMode;
@JsonKey(defaultValue: false) @JsonKey(defaultValue: false)
bool get amoledDarkMode => _amoledDarkMode; bool get amoledDarkMode => _amoledDarkMode;
set amoledDarkMode(bool amoledDarkMode) { set amoledDarkMode(bool amoledDarkMode) {
@ -33,11 +33,11 @@ class ConfigStore extends ChangeNotifier {
save(); save();
} }
Locale _locale; Locale? _locale;
// default value is set in the `load` method because json_serializable does // default value is set in the `load` method because json_serializable does
// not accept non-literals as constant values // not accept non-literals as constant values
@JsonKey(fromJson: LocaleSerde.fromJson, toJson: LocaleSerde.toJson) @JsonKey(fromJson: LocaleSerde.fromJson, toJson: LocaleSerde.toJson)
Locale get locale => _locale; Locale get locale => _locale ?? const Locale('en');
set locale(Locale locale) { set locale(Locale locale) {
_locale = locale; _locale = locale;
notifyListeners(); notifyListeners();
@ -49,7 +49,7 @@ class ConfigStore extends ChangeNotifier {
return _$ConfigStoreFromJson( return _$ConfigStoreFromJson(
jsonDecode(prefs.getString(prefsKey) ?? '{}') as Map<String, dynamic>, jsonDecode(prefs.getString(prefsKey) ?? '{}') as Map<String, dynamic>,
).._locale ??= const Locale('en'); );
} }
Future<void> save() async { Future<void> save() async {

View File

@ -24,7 +24,7 @@ ThemeData _themeFactory({bool dark = false, bool amoled = false}) {
iconTheme: IconThemeData(color: theme.colorScheme.onSurface), iconTheme: IconThemeData(color: theme.colorScheme.onSurface),
textTheme: TextTheme( textTheme: TextTheme(
headline6: theme.textTheme.headline6 headline6: theme.textTheme.headline6
.copyWith(fontSize: 20, fontWeight: FontWeight.w500), ?.copyWith(fontSize: 20, fontWeight: FontWeight.w500),
), ),
), ),
tabBarTheme: TabBarTheme( tabBarTheme: TabBarTheme(

View File

@ -13,9 +13,9 @@ import 'util/goto.dart';
/// Decides where does a link link to. Either somewhere in-app: /// Decides where does a link link to. Either somewhere in-app:
/// opens the correct page, or outside of the app: opens in a browser /// opens the correct page, or outside of the app: opens in a browser
Future<void> linkLauncher({ Future<void> linkLauncher({
@required BuildContext context, required BuildContext context,
@required String url, required String url,
@required String instanceHost, required String instanceHost,
}) async { }) async {
push(Widget Function() builder) { push(Widget Function() builder) {
goTo(context, (c) => builder()); goTo(context, (c) => builder());
@ -46,8 +46,8 @@ Future<void> linkLauncher({
final matchedInstance = match?.group(1); final matchedInstance = match?.group(1);
final rest = match?.group(2); final rest = match?.group(2);
if (matchedInstance != null && instances.any((e) => e == match.group(1))) { if (matchedInstance != null && instances.any((e) => e == match?.group(1))) {
if (rest.isEmpty || rest == '/') { if (rest == null || rest.isEmpty || rest == '/') {
return push(() => InstancePage(instanceHost: matchedInstance)); return push(() => InstancePage(instanceHost: matchedInstance));
} }
final split = rest.split('/'); final split = rest.split('/');
@ -107,7 +107,7 @@ Future<void> linkLauncher({
Future<void> openInBrowser(String url) async { Future<void> openInBrowser(String url) async {
if (await ul.canLaunch(url)) { if (await ul.canLaunch(url)) {
return await ul.launch(url); await ul.launch(url);
} else { } else {
throw Exception(); throw Exception();
// TODO: handle opening links to stuff in app // TODO: handle opening links to stuff in app

View File

@ -1,4 +1,3 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:lemmy_api_client/v3.dart'; import 'package:lemmy_api_client/v3.dart';
@ -7,28 +6,24 @@ import '../hooks/delayed_loading.dart';
/// Executes an API action that uses [DelayedLoading], has a try-catch /// Executes an API action that uses [DelayedLoading], has a try-catch
/// that displays a [SnackBar] when the action fails /// that displays a [SnackBar] when the action fails
Future<void> delayedAction<T>({ Future<void> delayedAction<T>({
@required BuildContext context, required BuildContext context,
@required DelayedLoading delayedLoading, required DelayedLoading delayedLoading,
@required String instanceHost, required String instanceHost,
@required LemmyApiQuery<T> query, required LemmyApiQuery<T> query,
Function(T) onSuccess, void Function(T)? onSuccess,
Function(T) cleanup, void Function(T?)? cleanup,
}) async { }) async {
assert(delayedLoading != null); T? val;
assert(instanceHost != null);
assert(query != null);
assert(context != null);
T val;
try { try {
delayedLoading.start(); delayedLoading.start();
val = await LemmyApiV3(instanceHost).run<T>(query); val = await LemmyApiV3(instanceHost).run<T>(query);
onSuccess?.call(val); onSuccess?.call(val as T);
// ignore: avoid_catches_without_on_clauses // ignore: avoid_catches_without_on_clauses
} catch (e) { } catch (e) {
ScaffoldMessenger.of(context) ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text(e.toString()))); .showSnackBar(SnackBar(content: Text(e.toString())));
} finally {
cleanup?.call(val);
delayedLoading.cancel();
} }
cleanup?.call(val);
delayedLoading.cancel();
} }

View File

@ -34,8 +34,9 @@ extension CommunityDisplayNames on CommunitySafe {
extension UserDisplayNames on PersonSafe { extension UserDisplayNames on PersonSafe {
String get displayName { String get displayName {
if (preferredUsername != null && preferredUsername.isNotEmpty) { final prefName = preferredUsername;
return preferredUsername; if (prefName != null && prefName.isNotEmpty) {
return prefName;
} }
return '@$name'; return '@$name';

View File

@ -6,11 +6,10 @@ import 'package:share/share.dart';
/// on platforms that do not support native sharing /// on platforms that do not support native sharing
Future<void> share( Future<void> share(
String text, { String text, {
String subject, String? subject,
Rect sharePositionOrigin, Rect? sharePositionOrigin,
@required BuildContext context, required BuildContext context,
}) async { }) async {
assert(context != null);
try { try {
return await Share.share( return await Share.share(

View File

@ -23,7 +23,12 @@ class AboutTile extends HookWidget {
return await PackageInfo.fromPlatform(); return await PackageInfo.fromPlatform();
} on MissingPluginException { } on MissingPluginException {
// when we get here it means PackageInfo does not support this platform // when we get here it means PackageInfo does not support this platform
return PackageInfo(version: ''); return PackageInfo(
appName: 'lemmur',
packageName: '',
version: '',
buildNumber: '',
);
} }
}, },
); );
@ -31,13 +36,16 @@ class AboutTile extends HookWidget {
final changelogSnap = final changelogSnap =
useMemoFuture(() => assetBundle.loadString('CHANGELOG.md')); useMemoFuture(() => assetBundle.loadString('CHANGELOG.md'));
if (!packageInfoSnap.hasData || !changelogSnap.hasData) {
return const SizedBox.shrink();
}
final packageInfo = packageInfoSnap.data; final packageInfo = packageInfoSnap.data;
final changelog = changelogSnap.data; final changelog = changelogSnap.data;
if (!packageInfoSnap.hasData ||
!changelogSnap.hasData ||
packageInfo == null ||
changelog == null) {
return const SizedBox.shrink();
}
return AboutListTile( return AboutListTile(
icon: const Icon(Icons.info), icon: const Icon(Icons.info),
aboutBoxChildren: [ aboutBoxChildren: [

View File

@ -6,13 +6,13 @@ import 'package:flutter/material.dart';
/// Can be disabled with `noBlank` /// Can be disabled with `noBlank`
class Avatar extends StatelessWidget { class Avatar extends StatelessWidget {
const Avatar({ const Avatar({
Key key, Key? key,
@required this.url, required this.url,
this.radius = 25, this.radius = 25,
this.noBlank = false, this.noBlank = false,
}) : super(key: key); }) : super(key: key);
final String url; final String? url;
final double radius; final double radius;
final bool noBlank; final bool noBlank;
@ -27,7 +27,9 @@ class Avatar extends StatelessWidget {
); );
}(); }();
if (url == null) { final imageUrl = url;
if (imageUrl == null) {
return blankWidget; return blankWidget;
} }
@ -35,7 +37,7 @@ class Avatar extends StatelessWidget {
child: CachedNetworkImage( child: CachedNetworkImage(
height: radius * 2, height: radius * 2,
width: radius * 2, width: radius * 2,
imageUrl: url, imageUrl: imageUrl,
fit: BoxFit.cover, fit: BoxFit.cover,
errorWidget: (_, __, ___) => blankWidget, errorWidget: (_, __, ___) => blankWidget,
), ),

View File

@ -3,16 +3,15 @@ import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
/// Should be spawned with a [showBottomModal], not routed to. /// Should be spawned with a [showBottomModal], not routed to.
class BottomModal extends StatelessWidget { class BottomModal extends StatelessWidget {
final String title; final String? title;
final EdgeInsets padding; final EdgeInsets padding;
final Widget child; final Widget child;
const BottomModal({ const BottomModal({
this.title, this.title,
this.padding = EdgeInsets.zero, this.padding = EdgeInsets.zero,
@required this.child, required this.child,
}) : assert(padding != null), });
assert(child != null);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -40,7 +39,7 @@ class BottomModal extends StatelessWidget {
Padding( Padding(
padding: const EdgeInsets.only(left: 70), padding: const EdgeInsets.only(left: 70),
child: Text( child: Text(
title, title!,
style: theme.textTheme.subtitle2, style: theme.textTheme.subtitle2,
textAlign: TextAlign.left, textAlign: TextAlign.left,
), ),
@ -65,10 +64,10 @@ class BottomModal extends StatelessWidget {
} }
/// Helper function for showing a [BottomModal] /// Helper function for showing a [BottomModal]
Future<T> showBottomModal<T>({ Future<T?> showBottomModal<T>({
@required BuildContext context, required BuildContext context,
@required WidgetBuilder builder, required WidgetBuilder builder,
String title, String? title,
EdgeInsets padding = EdgeInsets.zero, EdgeInsets padding = EdgeInsets.zero,
}) => }) =>
showCustomModalBottomSheet<T>( showCustomModalBottomSheet<T>(

View File

@ -33,7 +33,7 @@ class CommentWidget extends HookWidget {
final bool wasVoted; final bool wasVoted;
final bool canBeMarkedAsRead; final bool canBeMarkedAsRead;
final bool hideOnRead; final bool hideOnRead;
final int userMentionId; final int? userMentionId;
static const colors = [ static const colors = [
Colors.pink, Colors.pink,
@ -95,10 +95,7 @@ class CommentWidget extends HookWidget {
final accStore = useAccountsStore(); final accStore = useAccountsStore();
final isMine = commentTree.comment.comment.creatorId == final isMine = commentTree.comment.comment.creatorId ==
accStore accStore.defaultTokenFor(commentTree.comment.instanceHost)?.payload.sub;
.defaultTokenFor(commentTree.comment.instanceHost)
?.payload
?.sub;
final selectable = useState(false); final selectable = useState(false);
final showRaw = useState(false); final showRaw = useState(false);
final collapsed = useState(false); final collapsed = useState(false);
@ -221,7 +218,7 @@ class CommentWidget extends HookWidget {
if (isDeleted.value) { if (isDeleted.value) {
return Flexible( return Flexible(
child: Text( child: Text(
L10n.of(context).deleted_by_creator, L10n.of(context)!.deleted_by_creator,
style: const TextStyle(fontStyle: FontStyle.italic), style: const TextStyle(fontStyle: FontStyle.italic),
), ),
); );
@ -294,7 +291,7 @@ class CommentWidget extends HookWidget {
icon: Icons.more_horiz, icon: Icons.more_horiz,
onPressed: () => _openMoreMenu(context), onPressed: () => _openMoreMenu(context),
delayedLoading: delayedDeletion, delayedLoading: delayedDeletion,
tooltip: L10n.of(context).more, tooltip: L10n.of(context)!.more,
), ),
_SaveComment(commentTree.comment), _SaveComment(commentTree.comment),
if (!isDeleted.value && if (!isDeleted.value &&
@ -303,7 +300,7 @@ class CommentWidget extends HookWidget {
TileAction( TileAction(
icon: Icons.reply, icon: Icons.reply,
onPressed: loggedInAction((_) => reply()), onPressed: loggedInAction((_) => reply()),
tooltip: L10n.of(context).reply, tooltip: L10n.of(context)!.reply,
), ),
TileAction( TileAction(
icon: Icons.arrow_upward, icon: Icons.arrow_upward,
@ -369,7 +366,7 @@ class CommentWidget extends HookWidget {
if (isOP) _CommentTag('OP', theme.accentColor), if (isOP) _CommentTag('OP', theme.accentColor),
if (comment.creator.admin) if (comment.creator.admin)
_CommentTag( _CommentTag(
L10n.of(context).admin.toUpperCase(), L10n.of(context)!.admin.toUpperCase(),
theme.accentColor, theme.accentColor,
), ),
if (comment.creator.banned) if (comment.creator.banned)
@ -417,14 +414,14 @@ class CommentWidget extends HookWidget {
class _MarkAsRead extends HookWidget { class _MarkAsRead extends HookWidget {
final CommentView commentView; final CommentView commentView;
final ValueChanged<bool> onChanged; final ValueChanged<bool>? onChanged;
final int userMentionId; final int? userMentionId;
const _MarkAsRead( const _MarkAsRead(
this.commentView, { this.commentView, {
@required this.onChanged, required this.onChanged,
@required this.userMentionId, required this.userMentionId,
}) : assert(commentView != null); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -432,18 +429,19 @@ class _MarkAsRead extends HookWidget {
final comment = commentView.comment; final comment = commentView.comment;
final instanceHost = commentView.instanceHost; final instanceHost = commentView.instanceHost;
final loggedInAction = useLoggedInAction(instanceHost);
final isRead = useState(comment.read); final isRead = useState(comment.read);
final delayedRead = useDelayedLoading(); final delayedRead = useDelayedLoading();
Future<void> handleMarkAsSeen() => delayedAction<FullCommentView>( Future<void> handleMarkAsSeen(Jwt token) => delayedAction<FullCommentView>(
context: context, context: context,
delayedLoading: delayedRead, delayedLoading: delayedRead,
instanceHost: instanceHost, instanceHost: instanceHost,
query: MarkCommentAsRead( query: MarkCommentAsRead(
commentId: comment.id, commentId: comment.id,
read: !isRead.value, read: !isRead.value,
auth: accStore.defaultTokenFor(instanceHost)?.raw, auth: token.raw,
), ),
onSuccess: (val) { onSuccess: (val) {
isRead.value = val.commentView.comment.read; isRead.value = val.commentView.comment.read;
@ -451,14 +449,15 @@ class _MarkAsRead extends HookWidget {
}, },
); );
Future<void> handleMarkMentionAsSeen() => delayedAction<PersonMentionView>( Future<void> handleMarkMentionAsSeen(Jwt token) =>
delayedAction<PersonMentionView>(
context: context, context: context,
delayedLoading: delayedRead, delayedLoading: delayedRead,
instanceHost: instanceHost, instanceHost: instanceHost,
query: MarkPersonMentionAsRead( query: MarkPersonMentionAsRead(
personMentionId: userMentionId, personMentionId: userMentionId!,
read: !isRead.value, read: !isRead.value,
auth: accStore.defaultTokenFor(instanceHost)?.raw, auth: token.raw,
), ),
onSuccess: (val) { onSuccess: (val) {
isRead.value = val.personMention.read; isRead.value = val.personMention.read;
@ -469,12 +468,13 @@ class _MarkAsRead extends HookWidget {
return TileAction( return TileAction(
icon: Icons.check, icon: Icons.check,
delayedLoading: delayedRead, delayedLoading: delayedRead,
onPressed: onPressed: userMentionId != null
userMentionId != null ? handleMarkMentionAsSeen : handleMarkAsSeen, ? loggedInAction(handleMarkMentionAsSeen)
: loggedInAction(handleMarkAsSeen),
iconColor: isRead.value ? Theme.of(context).accentColor : null, iconColor: isRead.value ? Theme.of(context).accentColor : null,
tooltip: isRead.value tooltip: isRead.value
? L10n.of(context).mark_as_unread ? L10n.of(context)!.mark_as_unread
: L10n.of(context).mark_as_read, : L10n.of(context)!.mark_as_read,
); );
} }
} }
@ -487,7 +487,7 @@ class _SaveComment extends HookWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final loggedInAction = useLoggedInAction(comment.instanceHost); final loggedInAction = useLoggedInAction(comment.instanceHost);
final isSaved = useState(comment.saved ?? false); final isSaved = useState(comment.saved);
final delayed = useDelayedLoading(); final delayed = useDelayedLoading();
handleSave(Jwt token) => delayedAction<FullCommentView>( handleSave(Jwt token) => delayedAction<FullCommentView>(
@ -529,7 +529,7 @@ class _CommentTag extends StatelessWidget {
child: Text(text, child: Text(text,
style: TextStyle( style: TextStyle(
color: textColorBasedOnBackground(bgColor), color: textColorBasedOnBackground(bgColor),
fontSize: Theme.of(context).textTheme.bodyText1.fontSize - 5, fontSize: Theme.of(context).textTheme.bodyText1!.fontSize! - 5,
fontWeight: FontWeight.w800, fontWeight: FontWeight.w800,
)), )),
), ),

View File

@ -26,7 +26,7 @@ class CommentSection extends HookWidget {
CommentSection( CommentSection(
List<CommentView> rawComments, { List<CommentView> rawComments, {
@required this.postCreatorId, required this.postCreatorId,
this.sortType = CommentSortType.hot, this.sortType = CommentSortType.hot,
}) : comments = }) : comments =
CommentTree.sortList(sortType, CommentTree.fromList(rawComments)), CommentTree.sortList(sortType, CommentTree.fromList(rawComments)),
@ -76,7 +76,7 @@ class CommentSection extends HookWidget {
}, },
child: Row( child: Row(
children: [ children: [
Text((sortPairs[sorting.value][1] as String).tr(context)), Text((sortPairs[sorting.value]![1] as String).tr(context)),
const Icon(Icons.arrow_drop_down), const Icon(Icons.arrow_drop_down),
], ],
), ),

View File

@ -10,9 +10,9 @@ class FullscreenableImage extends StatelessWidget {
final Widget child; final Widget child;
const FullscreenableImage({ const FullscreenableImage({
Key key, Key? key,
@required this.url, required this.url,
@required this.child, required this.child,
}) : super(key: key); }) : super(key: key);
@override @override

View File

@ -6,7 +6,7 @@ import '../hooks/ref.dart';
import 'bottom_safe.dart'; import 'bottom_safe.dart';
class InfiniteScrollController { class InfiniteScrollController {
VoidCallback clear; late VoidCallback clear;
InfiniteScrollController() { InfiniteScrollController() {
usedBeforeCreation() => throw Exception( usedBeforeCreation() => throw Exception(
@ -14,10 +14,6 @@ class InfiniteScrollController {
clear = usedBeforeCreation; clear = usedBeforeCreation;
} }
void dispose() {
clear = null;
}
} }
/// `ListView.builder` with asynchronous data fetching and no `itemCount` /// `ListView.builder` with asynchronous data fetching and no `itemCount`
@ -36,13 +32,13 @@ class InfiniteScroll<T> extends HookWidget {
/// is considered finished /// is considered finished
final Future<List<T>> Function(int page, int batchSize) fetcher; final Future<List<T>> Function(int page, int batchSize) fetcher;
final InfiniteScrollController controller; final InfiniteScrollController? controller;
/// Widget to be added at the beginning of the list /// Widget to be added at the beginning of the list
final Widget leading; final Widget leading;
/// Padding for the [ListView.builder] /// Padding for the [ListView.builder]
final EdgeInsetsGeometry padding; final EdgeInsetsGeometry? padding;
/// Widget that will be displayed if there are no items /// Widget that will be displayed if there are no items
final Widget noItems; final Widget noItems;
@ -53,13 +49,11 @@ class InfiniteScroll<T> extends HookWidget {
this.padding, this.padding,
this.loadingWidget = this.loadingWidget =
const ListTile(title: Center(child: CircularProgressIndicator())), const ListTile(title: Center(child: CircularProgressIndicator())),
@required this.itemBuilder, required this.itemBuilder,
@required this.fetcher, required this.fetcher,
this.controller, this.controller,
this.noItems = const SizedBox.shrink(), this.noItems = const SizedBox.shrink(),
}) : assert(itemBuilder != null), }) : assert(batchSize > 0);
assert(fetcher != null),
assert(batchSize > 0);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -69,7 +63,7 @@ class InfiniteScroll<T> extends HookWidget {
useEffect(() { useEffect(() {
if (controller != null) { if (controller != null) {
controller.clear = () { controller?.clear = () {
data.value = []; data.value = [];
hasMore.current = true; hasMore.current = true;
}; };
@ -82,7 +76,9 @@ class InfiniteScroll<T> extends HookWidget {
return RefreshIndicator( return RefreshIndicator(
onRefresh: () async { onRefresh: () async {
controller.clear(); data.value = [];
hasMore.current = true;
await HapticFeedback.mediumImpact(); await HapticFeedback.mediumImpact();
await Future.delayed(const Duration(seconds: 1)); await Future.delayed(const Duration(seconds: 1));
}, },

View File

@ -3,13 +3,10 @@ import 'package:flutter/material.dart';
import 'bottom_modal.dart'; import 'bottom_modal.dart';
void showInfoTablePopup({ void showInfoTablePopup({
@required BuildContext context, required BuildContext context,
@required Map<String, dynamic> table, required Map<String, dynamic> table,
String title, String? title,
}) { }) {
assert(context != null);
assert(table != null);
showBottomModal( showBottomModal(
context: context, context: context,
title: title, title: title,

View File

@ -4,7 +4,7 @@ import 'package:flutter/material.dart';
/// used mostly for pages where markdown editor is used /// used mostly for pages where markdown editor is used
/// ///
/// brush icon is rotated to look similarly to build icon /// brush icon is rotated to look similarly to build icon
Widget markdownModeIcon({@required bool fancy}) => fancy Widget markdownModeIcon({required bool fancy}) => fancy
? const Icon(Icons.build) ? const Icon(Icons.build)
: const RotatedBox( : const RotatedBox(
quarterTurns: 1, quarterTurns: 1,

View File

@ -15,8 +15,7 @@ class MarkdownText extends StatelessWidget {
final bool selectable; final bool selectable;
const MarkdownText(this.text, const MarkdownText(this.text,
{@required this.instanceHost, this.selectable = false}) {required this.instanceHost, this.selectable = false});
: assert(instanceHost != null);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -32,9 +31,10 @@ class MarkdownText extends StatelessWidget {
), ),
code: theme.textTheme.bodyText1 code: theme.textTheme.bodyText1
// TODO: use a font from google fonts maybe? the defaults aren't very pretty // TODO: use a font from google fonts maybe? the defaults aren't very pretty
.copyWith(fontFamily: Platform.isIOS ? 'Courier' : 'monospace'), ?.copyWith(fontFamily: Platform.isIOS ? 'Courier' : 'monospace'),
), ),
onTapLink: (text, href, title) { onTapLink: (text, href, title) {
if (href == null) return;
linkLauncher(context: context, url: href, instanceHost: instanceHost) linkLauncher(context: context, url: href, instanceHost: instanceHost)
.catchError( .catchError(
(e) => ScaffoldMessenger.of(context).showSnackBar(SnackBar( (e) => ScaffoldMessenger.of(context).showSnackBar(SnackBar(

View File

@ -33,7 +33,7 @@ enum MediaType {
none, none,
} }
MediaType whatType(String url) { MediaType whatType(String? url) {
if (url == null || url.isEmpty) return MediaType.none; if (url == null || url.isEmpty) return MediaType.none;
// TODO: make detection more nuanced // TODO: make detection more nuanced
@ -95,13 +95,13 @@ class PostWidget extends HookWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
void _openLink() => linkLauncher( void _openLink(String url) =>
context: context, url: post.post.url, instanceHost: instanceHost); linkLauncher(context: context, url: url, instanceHost: instanceHost);
final urlDomain = () { final urlDomain = () {
if (whatType(post.post.url) == MediaType.none) return null; if (whatType(post.post.url) == MediaType.none) return null;
return urlHost(post.post.url); return urlHost(post.post.url!);
}(); }();
/// assemble info section /// assemble info section
@ -141,7 +141,7 @@ class PostWidget extends HookWidget {
text: TextSpan( text: TextSpan(
style: TextStyle( style: TextStyle(
fontSize: 15, fontSize: 15,
color: theme.textTheme.bodyText1.color), color: theme.textTheme.bodyText1?.color),
children: [ children: [
const TextSpan( const TextSpan(
text: '!', text: '!',
@ -179,10 +179,10 @@ class PostWidget extends HookWidget {
text: TextSpan( text: TextSpan(
style: TextStyle( style: TextStyle(
fontSize: 13, fontSize: 13,
color: theme.textTheme.bodyText1.color), color: theme.textTheme.bodyText1?.color),
children: [ children: [
TextSpan( TextSpan(
text: L10n.of(context).by, text: L10n.of(context)!.by,
style: const TextStyle( style: const TextStyle(
fontWeight: FontWeight.w300), fontWeight: FontWeight.w300),
), ),
@ -206,7 +206,7 @@ class PostWidget extends HookWidget {
if (post.post.nsfw) const TextSpan(text: ' · '), if (post.post.nsfw) const TextSpan(text: ' · '),
if (post.post.nsfw) if (post.post.nsfw)
TextSpan( TextSpan(
text: L10n.of(context).nsfw, text: L10n.of(context)!.nsfw,
style: style:
const TextStyle(color: Colors.red)), const TextStyle(color: Colors.red)),
if (urlDomain != null) if (urlDomain != null)
@ -260,13 +260,13 @@ class PostWidget extends HookWidget {
const Spacer(), const Spacer(),
InkWell( InkWell(
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
onTap: _openLink, onTap: () => _openLink(post.post.url!),
child: Stack( child: Stack(
children: [ children: [
ClipRRect( ClipRRect(
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
child: CachedNetworkImage( child: CachedNetworkImage(
imageUrl: post.post.thumbnailUrl, imageUrl: post.post.thumbnailUrl!,
width: 70, width: 70,
height: 70, height: 70,
fit: BoxFit.cover, fit: BoxFit.cover,
@ -297,11 +297,11 @@ class PostWidget extends HookWidget {
return Padding( return Padding(
padding: const EdgeInsets.all(10), padding: const EdgeInsets.all(10),
child: InkWell( child: InkWell(
onTap: _openLink, onTap: () => _openLink(post.post.url!),
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border.all( border: Border.all(
color: Theme.of(context).iconTheme.color.withAlpha(170)), color: Theme.of(context).iconTheme.color!.withAlpha(170)),
borderRadius: BorderRadius.circular(5)), borderRadius: BorderRadius.circular(5)),
child: Padding( child: Padding(
padding: const EdgeInsets.all(10), padding: const EdgeInsets.all(10),
@ -312,7 +312,7 @@ class PostWidget extends HookWidget {
const Spacer(), const Spacer(),
Text('$urlDomain ', Text('$urlDomain ',
style: theme.textTheme.caption style: theme.textTheme.caption
.apply(fontStyle: FontStyle.italic)), ?.apply(fontStyle: FontStyle.italic)),
const Icon(Icons.launch, size: 12), const Icon(Icons.launch, size: 12),
], ],
), ),
@ -322,7 +322,7 @@ class PostWidget extends HookWidget {
child: Text( child: Text(
post.post.embedTitle ?? '', post.post.embedTitle ?? '',
style: theme.textTheme.subtitle1 style: theme.textTheme.subtitle1
.apply(fontWeightDelta: 2), ?.apply(fontWeightDelta: 2),
maxLines: 2, maxLines: 2,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
@ -330,12 +330,12 @@ class PostWidget extends HookWidget {
], ],
), ),
if (post.post.embedDescription != null && if (post.post.embedDescription != null &&
post.post.embedDescription.isNotEmpty) post.post.embedDescription!.isNotEmpty)
Row( Row(
children: [ children: [
Flexible( Flexible(
child: Text( child: Text(
post.post.embedDescription, post.post.embedDescription!,
maxLines: 4, maxLines: 4,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
@ -355,9 +355,9 @@ class PostWidget extends HookWidget {
assert(post.post.url != null); assert(post.post.url != null);
return FullscreenableImage( return FullscreenableImage(
url: post.post.url, url: post.post.url!,
child: CachedNetworkImage( child: CachedNetworkImage(
imageUrl: post.post.url, imageUrl: post.post.url!,
errorWidget: (_, __, ___) => const Icon(Icons.warning), errorWidget: (_, __, ___) => const Icon(Icons.warning),
progressIndicatorBuilder: (context, url, progress) => progressIndicatorBuilder: (context, url, progress) =>
CircularProgressIndicator(value: progress.progress), CircularProgressIndicator(value: progress.progress),
@ -375,7 +375,7 @@ class PostWidget extends HookWidget {
Expanded( Expanded(
flex: 999, flex: 999,
child: Text( child: Text(
L10n.of(context).number_of_comments(post.counts.comments), L10n.of(context)!.number_of_comments(post.counts.comments),
overflow: TextOverflow.fade, overflow: TextOverflow.fade,
softWrap: false, softWrap: false,
), ),
@ -411,13 +411,13 @@ class PostWidget extends HookWidget {
if (whatType(post.post.url) != MediaType.other && if (whatType(post.post.url) != MediaType.other &&
whatType(post.post.url) != MediaType.none) whatType(post.post.url) != MediaType.none)
postImage() postImage()
else if (post.post.url != null && post.post.url.isNotEmpty) else if (post.post.url != null && post.post.url!.isNotEmpty)
linkPreview(), linkPreview(),
if (post.post.body != null && fullPost) if (post.post.body != null && fullPost)
Padding( Padding(
padding: const EdgeInsets.all(10), padding: const EdgeInsets.all(10),
child: MarkdownText( child: MarkdownText(
post.post.body, post.post.body!,
instanceHost: instanceHost, instanceHost: instanceHost,
selectable: true, selectable: true,
), ),
@ -446,7 +446,7 @@ class PostWidget extends HookWidget {
heightFactor: 0.8, heightFactor: 0.8,
child: Padding( child: Padding(
padding: const EdgeInsets.all(10), padding: const EdgeInsets.all(10),
child: MarkdownText(post.post.body, child: MarkdownText(post.post.body!,
instanceHost: instanceHost)), instanceHost: instanceHost)),
), ),
), ),
@ -469,7 +469,7 @@ class PostWidget extends HookWidget {
} else { } else {
return Padding( return Padding(
padding: const EdgeInsets.all(10), padding: const EdgeInsets.all(10),
child: MarkdownText(post.post.body, child: MarkdownText(post.post.body!,
instanceHost: instanceHost)); instanceHost: instanceHost));
} }
}, },
@ -489,8 +489,7 @@ class _Voting extends HookWidget {
final bool wasVoted; final bool wasVoted;
_Voting(this.post) _Voting(this.post)
: assert(post != null), : wasVoted = (post.myVote ?? VoteType.none) != VoteType.none;
wasVoted = (post.myVote ?? VoteType.none) != VoteType.none;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View File

@ -12,8 +12,8 @@ class PostListOptions extends StatelessWidget {
final bool styleButton; final bool styleButton;
const PostListOptions({ const PostListOptions({
@required this.onSortChanged, required this.onSortChanged,
@required this.sortValue, required this.sortValue,
this.styleButton = true, this.styleButton = true,
}) : assert(sortValue != null); }) : assert(sortValue != null);

View File

@ -6,31 +6,29 @@ import 'bottom_modal.dart';
class RadioPicker<T> extends StatelessWidget { class RadioPicker<T> extends StatelessWidget {
final List<T> values; final List<T> values;
final T groupValue; final T groupValue;
final ValueChanged<T> onChanged; final ValueChanged<T>? onChanged;
/// Map a given value to a string for display /// Map a given value to a string for display
final String Function(T) mapValueToString; final String Function(T)? mapValueToString;
final String title; final String? title;
/// custom button builder. When null, an OutlinedButton is used /// custom button builder. When null, an OutlinedButton is used
final Widget Function( final Widget Function(
BuildContext context, String displayValue, VoidCallback onPressed) BuildContext context, String displayValue, VoidCallback onPressed)?
buttonBuilder; buttonBuilder;
final Widget trailing; final Widget? trailing;
const RadioPicker({ const RadioPicker({
Key key, Key? key,
@required this.values, required this.values,
@required this.groupValue, required this.groupValue,
@required this.onChanged, required this.onChanged,
this.mapValueToString, this.mapValueToString,
this.buttonBuilder, this.buttonBuilder,
this.title, this.title,
this.trailing, this.trailing,
}) : assert(values != null), }) : super(key: key);
assert(groupValue != null),
super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -63,7 +61,7 @@ class RadioPicker<T> extends StatelessWidget {
title: Text(mapValueToString(value)), title: Text(mapValueToString(value)),
onChanged: (value) => Navigator.of(context).pop(value), onChanged: (value) => Navigator.of(context).pop(value),
), ),
if (trailing != null) trailing if (trailing != null) trailing!
], ],
), ),
); );

View File

@ -14,9 +14,9 @@ class RevealAfterScroll extends HookWidget {
final bool fade; final bool fade;
const RevealAfterScroll({ const RevealAfterScroll({
@required this.scrollController, required this.scrollController,
@required this.child, required this.child,
@required this.after, required this.after,
this.transition = 15, this.transition = 15,
this.fade = false, this.fade = false,
}) : assert(scrollController != null), }) : assert(scrollController != null),

View File

@ -14,7 +14,7 @@ class SavePostButton extends HookWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isSaved = useState(post.saved ?? false); final isSaved = useState(post.saved);
final savedIcon = isSaved.value ? Icons.bookmark : Icons.bookmark_border; final savedIcon = isSaved.value ? Icons.bookmark : Icons.bookmark_border;
final loading = useDelayedLoading(); final loading = useDelayedLoading();
final loggedInAction = useLoggedInAction(post.instanceHost); final loggedInAction = useLoggedInAction(post.instanceHost);

View File

@ -16,21 +16,19 @@ typedef FetcherWithSorting<T> = Future<List<T>> Function(
class SortableInfiniteList<T> extends HookWidget { class SortableInfiniteList<T> extends HookWidget {
final FetcherWithSorting<T> fetcher; final FetcherWithSorting<T> fetcher;
final Widget Function(T) itemBuilder; final Widget Function(T) itemBuilder;
final InfiniteScrollController controller; final InfiniteScrollController? controller;
final Function onStyleChange; final Function? onStyleChange;
final Widget noItems; final Widget noItems;
final SortType defaultSort; final SortType defaultSort;
const SortableInfiniteList({ const SortableInfiniteList({
@required this.fetcher, required this.fetcher,
@required this.itemBuilder, required this.itemBuilder,
this.controller, this.controller,
this.onStyleChange, this.onStyleChange,
this.noItems, this.noItems = const SizedBox.shrink(),
this.defaultSort = SortType.active, this.defaultSort = SortType.active,
}) : assert(fetcher != null), });
assert(itemBuilder != null),
assert(defaultSort != null);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -62,12 +60,12 @@ class SortableInfiniteList<T> extends HookWidget {
class InfinitePostList extends StatelessWidget { class InfinitePostList extends StatelessWidget {
final FetcherWithSorting<PostView> fetcher; final FetcherWithSorting<PostView> fetcher;
final InfiniteScrollController controller; final InfiniteScrollController? controller;
const InfinitePostList({ const InfinitePostList({
@required this.fetcher, required this.fetcher,
this.controller, this.controller,
}) : assert(fetcher != null); });
Widget build(BuildContext context) => SortableInfiniteList<PostView>( Widget build(BuildContext context) => SortableInfiniteList<PostView>(
onStyleChange: () {}, onStyleChange: () {},
@ -85,12 +83,12 @@ class InfinitePostList extends StatelessWidget {
class InfiniteCommentList extends StatelessWidget { class InfiniteCommentList extends StatelessWidget {
final FetcherWithSorting<CommentView> fetcher; final FetcherWithSorting<CommentView> fetcher;
final InfiniteScrollController controller; final InfiniteScrollController? controller;
const InfiniteCommentList({ const InfiniteCommentList({
@required this.fetcher, required this.fetcher,
this.controller, this.controller,
}) : assert(fetcher != null); });
Widget build(BuildContext context) => SortableInfiniteList<CommentView>( Widget build(BuildContext context) => SortableInfiniteList<CommentView>(
itemBuilder: (comment) => CommentWidget( itemBuilder: (comment) => CommentWidget(

View File

@ -9,20 +9,17 @@ class TileAction extends StatelessWidget {
final IconData icon; final IconData icon;
final VoidCallback onPressed; final VoidCallback onPressed;
final String tooltip; final String tooltip;
final DelayedLoading delayedLoading; final DelayedLoading? delayedLoading;
final Color iconColor; final Color? iconColor;
const TileAction({ const TileAction({
Key key, Key? key,
this.delayedLoading, this.delayedLoading,
this.iconColor, this.iconColor,
@required this.icon, required this.icon,
@required this.onPressed, required this.onPressed,
@required this.tooltip, required this.tooltip,
}) : assert(icon != null), }) : super(key: key);
assert(onPressed != null),
assert(tooltip != null),
super(key: key);
@override @override
Widget build(BuildContext context) => IconButton( Widget build(BuildContext context) => IconButton(
@ -34,7 +31,7 @@ class TileAction extends StatelessWidget {
: Icon( : Icon(
icon, icon,
color: iconColor ?? color: iconColor ??
Theme.of(context).iconTheme.color.withAlpha(190), Theme.of(context).iconTheme.color?.withAlpha(190),
), ),
splashRadius: 25, splashRadius: 25,
onPressed: delayedLoading?.pending ?? false ? () {} : onPressed, onPressed: delayedLoading?.pending ?? false ? () {} : onPressed,

View File

@ -22,16 +22,13 @@ class UserProfile extends HookWidget {
final String instanceHost; final String instanceHost;
final int userId; final int userId;
final FullPersonView _fullUserView; final FullPersonView? _fullUserView;
const UserProfile({@required this.userId, @required this.instanceHost}) const UserProfile({required this.userId, required this.instanceHost})
: assert(userId != null), : _fullUserView = null;
assert(instanceHost != null),
_fullUserView = null;
UserProfile.fromFullPersonView(this._fullUserView) UserProfile.fromFullPersonView(FullPersonView this._fullUserView)
: assert(_fullUserView != null), : userId = _fullUserView.personView.person.id,
userId = _fullUserView.personView.person.id,
instanceHost = _fullUserView.instanceHost; instanceHost = _fullUserView.instanceHost;
@override @override
@ -62,8 +59,8 @@ class UserProfile extends HookWidget {
]), ]),
); );
} }
final fullPersonView = userDetailsSnap.data!;
final userView = userDetailsSnap.data.personView; final userView = fullPersonView.personView;
return DefaultTabController( return DefaultTabController(
length: 3, length: 3,
@ -83,8 +80,8 @@ class UserProfile extends HookWidget {
color: theme.cardColor, color: theme.cardColor,
child: TabBar( child: TabBar(
tabs: [ tabs: [
Tab(text: L10n.of(context).posts), Tab(text: L10n.of(context)!.posts),
Tab(text: L10n.of(context).comments), Tab(text: L10n.of(context)!.comments),
const Tab(text: 'About'), const Tab(text: 'About'),
], ],
), ),
@ -119,7 +116,7 @@ class UserProfile extends HookWidget {
)) ))
.then((val) => val.comments), .then((val) => val.comments),
), ),
_AboutTab(userDetailsSnap.data), _AboutTab(fullPersonView),
]), ]),
), ),
); );
@ -146,9 +143,9 @@ class _UserOverview extends HookWidget {
if (userView.person.banner != null) if (userView.person.banner != null)
// TODO: for some reason doesnt react to presses // TODO: for some reason doesnt react to presses
FullscreenableImage( FullscreenableImage(
url: userView.person.banner, url: userView.person.banner!,
child: CachedNetworkImage( child: CachedNetworkImage(
imageUrl: userView.person.banner, imageUrl: userView.person.banner!,
errorWidget: (_, __, ___) => const SizedBox.shrink(), errorWidget: (_, __, ___) => const SizedBox.shrink(),
), ),
) )
@ -207,9 +204,9 @@ class _UserOverview extends HookWidget {
child: ClipRRect( child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(12)), borderRadius: const BorderRadius.all(Radius.circular(12)),
child: FullscreenableImage( child: FullscreenableImage(
url: userView.person.avatar, url: userView.person.avatar!,
child: CachedNetworkImage( child: CachedNetworkImage(
imageUrl: userView.person.avatar, imageUrl: userView.person.avatar!,
errorWidget: (_, __, ___) => const SizedBox.shrink(), errorWidget: (_, __, ___) => const SizedBox.shrink(),
), ),
), ),
@ -255,7 +252,7 @@ class _UserOverview extends HookWidget {
), ),
const SizedBox(width: 4), const SizedBox(width: 4),
Text( Text(
L10n.of(context) L10n.of(context)!
.number_of_posts(userView.counts.postCount), .number_of_posts(userView.counts.postCount),
style: TextStyle(color: colorOnTopOfAccentColor), style: TextStyle(color: colorOnTopOfAccentColor),
), ),
@ -273,7 +270,7 @@ class _UserOverview extends HookWidget {
), ),
const SizedBox(width: 4), const SizedBox(width: 4),
Text( Text(
L10n.of(context) L10n.of(context)!
.number_of_comments(userView.counts.commentCount), .number_of_comments(userView.counts.commentCount),
style: TextStyle(color: colorOnTopOfAccentColor), style: TextStyle(color: colorOnTopOfAccentColor),
), ),
@ -338,7 +335,7 @@ class _AboutTab extends HookWidget {
child: const Divider(), child: const Divider(),
); );
communityTile(String name, String icon, int id) => ListTile( communityTile(String name, String? icon, int id) => ListTile(
dense: true, dense: true,
onTap: () => goToCommunity.byId(context, instanceHost, id), onTap: () => goToCommunity.byId(context, instanceHost, id),
title: Text('!$name'), title: Text('!$name'),
@ -371,7 +368,7 @@ class _AboutTab extends HookWidget {
if (userDetails.personView.person.bio != null) ...[ if (userDetails.personView.person.bio != null) ...[
Padding( Padding(
padding: wallPadding, padding: wallPadding,
child: MarkdownText(userDetails.personView.person.bio, child: MarkdownText(userDetails.personView.person.bio!,
instanceHost: instanceHost)), instanceHost: instanceHost)),
divider, divider,
], ],
@ -380,7 +377,7 @@ class _AboutTab extends HookWidget {
title: Center( title: Center(
child: Text( child: Text(
'Moderates:', 'Moderates:',
style: theme.textTheme.headline6.copyWith(fontSize: 18), style: theme.textTheme.headline6?.copyWith(fontSize: 18),
), ),
), ),
), ),
@ -396,7 +393,7 @@ class _AboutTab extends HookWidget {
title: Center( title: Center(
child: Text( child: Text(
'Subscribed:', 'Subscribed:',
style: theme.textTheme.headline6.copyWith(fontSize: 18), style: theme.textTheme.headline6?.copyWith(fontSize: 18),
), ),
), ),
), ),

View File

@ -1,9 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:lemmur/hooks/logged_in_action.dart';
import 'package:lemmy_api_client/v3.dart'; import 'package:lemmy_api_client/v3.dart';
import '../hooks/delayed_loading.dart'; import '../hooks/delayed_loading.dart';
import '../hooks/stores.dart';
import '../l10n/l10n.dart'; import '../l10n/l10n.dart';
import 'markdown_mode_icon.dart'; import 'markdown_mode_icon.dart';
import 'markdown_text.dart'; import 'markdown_text.dart';
@ -13,19 +13,20 @@ import 'markdown_text.dart';
/// or `null` if cancelled /// or `null` if cancelled
class WriteComment extends HookWidget { class WriteComment extends HookWidget {
final Post post; final Post post;
final Comment comment; final Comment? comment;
const WriteComment.toPost(this.post) : comment = null; const WriteComment.toPost(this.post) : comment = null;
const WriteComment.toComment({@required this.comment, @required this.post}) const WriteComment.toComment({
: assert(comment != null), required Comment this.comment,
assert(post != null); required this.post,
});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final controller = useTextEditingController(); final controller = useTextEditingController();
final showFancy = useState(false); final showFancy = useState(false);
final delayed = useDelayedLoading(); final delayed = useDelayedLoading();
final accStore = useAccountsStore(); final loggedInAction = useLoggedInAction(post.instanceHost);
final preview = () { final preview = () {
final body = () { final body = () {
@ -38,27 +39,21 @@ class WriteComment extends HookWidget {
); );
}(); }();
if (post != null) { return Column(
return Column( children: [
children: [ SelectableText(
SelectableText( post.name,
post.name, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600),
style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600), ),
), const SizedBox(height: 4),
const SizedBox(height: 4), body,
body, ],
], );
);
}
return body;
}(); }();
handleSubmit() async { handleSubmit(Jwt token) async {
final api = LemmyApiV3(post.instanceHost); final api = LemmyApiV3(post.instanceHost);
final token = accStore.defaultTokenFor(post.instanceHost);
delayed.start(); delayed.start();
try { try {
final res = await api.run(CreateComment( final res = await api.run(CreateComment(
@ -119,10 +114,11 @@ class WriteComment extends HookWidget {
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [
TextButton( TextButton(
onPressed: delayed.pending ? () {} : handleSubmit, onPressed:
delayed.pending ? () {} : loggedInAction(handleSubmit),
child: delayed.loading child: delayed.loading
? const CircularProgressIndicator() ? const CircularProgressIndicator()
: Text(L10n.of(context).post), : Text(L10n.of(context)!.post),
) )
], ],
), ),

View File

@ -105,7 +105,7 @@ packages:
name: cached_network_image name: cached_network_image
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.5.1" version: "3.0.0"
characters: characters:
dependency: transitive dependency: transitive
description: description:
@ -168,14 +168,14 @@ packages:
name: crypto name: crypto
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.0.0" version: "3.0.1"
cupertino_icons: cupertino_icons:
dependency: "direct main" dependency: "direct main"
description: description:
name: cupertino_icons name: cupertino_icons
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.1.3" version: "1.0.2"
dart_style: dart_style:
dependency: transitive dependency: transitive
description: description:
@ -222,14 +222,14 @@ packages:
name: flutter_blurhash name: flutter_blurhash
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.5.0" version: "0.6.0"
flutter_cache_manager: flutter_cache_manager:
dependency: transitive dependency: transitive
description: description:
name: flutter_cache_manager name: flutter_cache_manager
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.2" version: "3.0.1"
flutter_hooks: flutter_hooks:
dependency: "direct main" dependency: "direct main"
description: description:
@ -262,14 +262,14 @@ packages:
name: flutter_plugin_android_lifecycle name: flutter_plugin_android_lifecycle
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.0" version: "2.0.1"
flutter_speed_dial: flutter_speed_dial:
dependency: "direct main" dependency: "direct main"
description: description:
name: flutter_speed_dial name: flutter_speed_dial
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.2.5" version: "3.0.5"
flutter_test: flutter_test:
dependency: "direct dev" dependency: "direct dev"
description: flutter description: flutter
@ -293,7 +293,7 @@ packages:
name: fuzzy name: fuzzy
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.3.0" version: "0.4.0-nullsafety.0"
glob: glob:
dependency: transitive dependency: transitive
description: description:
@ -342,7 +342,7 @@ packages:
name: image_picker name: image_picker
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.7.3" version: "0.7.4"
image_picker_for_web: image_picker_for_web:
dependency: transitive dependency: transitive
description: description:
@ -356,7 +356,7 @@ packages:
name: image_picker_platform_interface name: image_picker_platform_interface
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.0" version: "2.0.1"
intl: intl:
dependency: "direct main" dependency: "direct main"
description: description:
@ -398,7 +398,7 @@ packages:
name: latinize name: latinize
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.0.2" version: "0.1.0-nullsafety.0"
lemmy_api_client: lemmy_api_client:
dependency: "direct main" dependency: "direct main"
description: description:
@ -433,7 +433,7 @@ packages:
name: matrix4_transform name: matrix4_transform
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.1.7" version: "2.0.0"
meta: meta:
dependency: transitive dependency: transitive
description: description:
@ -454,7 +454,7 @@ packages:
name: modal_bottom_sheet name: modal_bottom_sheet
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.0+1" version: "2.0.0"
nested: nested:
dependency: transitive dependency: transitive
description: description:
@ -468,7 +468,7 @@ packages:
name: octo_image name: octo_image
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.3.0" version: "1.0.0+1"
package_config: package_config:
dependency: transitive dependency: transitive
description: description:
@ -482,7 +482,7 @@ packages:
name: package_info name: package_info
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.4.3+4" version: "2.0.0"
path: path:
dependency: transitive dependency: transitive
description: description:
@ -517,7 +517,7 @@ packages:
name: path_provider_platform_interface name: path_provider_platform_interface
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.0" version: "2.0.1"
path_provider_windows: path_provider_windows:
dependency: transitive dependency: transitive
description: description:
@ -538,14 +538,14 @@ packages:
name: petitparser name: petitparser
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "4.0.2" version: "4.1.0"
photo_view: photo_view:
dependency: "direct main" dependency: "direct main"
description: description:
name: photo_view name: photo_view
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.10.3" version: "0.11.1"
platform: platform:
dependency: transitive dependency: transitive
description: description:
@ -559,7 +559,7 @@ packages:
name: plugin_platform_interface name: plugin_platform_interface
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.3" version: "2.0.0"
pool: pool:
dependency: transitive dependency: transitive
description: description:
@ -580,7 +580,7 @@ packages:
name: provider name: provider
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "4.3.3" version: "5.0.0"
pub_semver: pub_semver:
dependency: transitive dependency: transitive
description: description:
@ -601,7 +601,7 @@ packages:
name: rxdart name: rxdart
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.25.0" version: "0.26.0"
share: share:
dependency: "direct main" dependency: "direct main"
description: description:
@ -615,28 +615,42 @@ packages:
name: shared_preferences name: shared_preferences
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.5.7+3" version: "2.0.5"
shared_preferences_linux:
dependency: transitive
description:
name: shared_preferences_linux
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
shared_preferences_macos: shared_preferences_macos:
dependency: transitive dependency: transitive
description: description:
name: shared_preferences_macos name: shared_preferences_macos
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.0.1+11" version: "2.0.0"
shared_preferences_platform_interface: shared_preferences_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: shared_preferences_platform_interface name: shared_preferences_platform_interface
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.4" version: "2.0.0"
shared_preferences_web: shared_preferences_web:
dependency: transitive dependency: transitive
description: description:
name: shared_preferences_web name: shared_preferences_web
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.1.2+7" version: "2.0.0"
shared_preferences_windows:
dependency: transitive
description:
name: shared_preferences_windows
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
shelf: shelf:
dependency: transitive dependency: transitive
description: description:
@ -739,7 +753,7 @@ packages:
name: timeago name: timeago
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.30" version: "3.0.2"
timing: timing:
dependency: transitive dependency: transitive
description: description:
@ -760,42 +774,42 @@ packages:
name: url_launcher name: url_launcher
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "5.7.10" version: "6.0.3"
url_launcher_linux: url_launcher_linux:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_linux name: url_launcher_linux
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.0.1+4" version: "2.0.0"
url_launcher_macos: url_launcher_macos:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_macos name: url_launcher_macos
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.0.1+9" version: "2.0.0"
url_launcher_platform_interface: url_launcher_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_platform_interface name: url_launcher_platform_interface
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.9" version: "2.0.2"
url_launcher_web: url_launcher_web:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_web name: url_launcher_web
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.1.5+3" version: "2.0.0"
url_launcher_windows: url_launcher_windows:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_windows name: url_launcher_windows
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.0.1+3" version: "2.0.0"
uuid: uuid:
dependency: transitive dependency: transitive
description: description:
@ -844,7 +858,7 @@ packages:
name: xml name: xml
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "5.0.2" version: "5.1.0"
yaml: yaml:
dependency: transitive dependency: transitive
description: description:
@ -854,4 +868,4 @@ packages:
version: "3.1.0" version: "3.1.0"
sdks: sdks:
dart: ">=2.12.0 <3.0.0" dart: ">=2.12.0 <3.0.0"
flutter: ">=1.24.0-10" flutter: ">=1.24.0-10.2.pre"

View File

@ -18,34 +18,34 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
version: 0.4.1+14 version: 0.4.1+14
environment: environment:
sdk: ">=2.7.0 <3.0.0" sdk: ">=2.12.0 <3.0.0"
dependencies: dependencies:
# widgets # widgets
flutter_speed_dial: ^1.2.5 flutter_speed_dial: ^3.0.5
photo_view: ^0.10.2 photo_view: ^0.11.1
markdown: ^4.0.0 markdown: ^4.0.0
flutter_markdown: ^0.6.1 flutter_markdown: ^0.6.1
cached_network_image: ^2.2.0+1 cached_network_image: ^3.0.0
modal_bottom_sheet: ^1.0.0+1 modal_bottom_sheet: ^2.0.0
# native # native
share: ^2.0.1 share: ^2.0.1
url_launcher: ^5.5.1 url_launcher: ^6.0.3
shared_preferences: ">=0.5.0 <2.0.0" shared_preferences: ^2.0.5
package_info: ^0.4.3 package_info: ^2.0.0
image_picker: ^0.7.3 image_picker: ^0.7.4
# state management # state management
flutter_hooks: ^0.16.0 flutter_hooks: ^0.16.0
provider: ^4.3.1 provider: ^5.0.0
# utils # utils
timeago: ^2.0.27 timeago: ^3.0.2
fuzzy: <1.0.0 fuzzy: ^0.4.0-nullsafety.0
lemmy_api_client: ^0.14.0 lemmy_api_client: ^0.14.0
intl: ^0.17.0 intl: ^0.17.0
matrix4_transform: ^1.1.7 matrix4_transform: ^2.0.0
json_annotation: ^4.0.1 json_annotation: ^4.0.1
flutter: flutter:
@ -55,7 +55,7 @@ dependencies:
# The following adds the Cupertino Icons font to your application. # The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons. # Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^0.1.3 cupertino_icons: ^1.0.2
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

View File

@ -30,7 +30,7 @@ ${keys.map((key) => " static const $key = '$key';").join('\n')}
extension L10nFromString on String { extension L10nFromString on String {
String tr(BuildContext context) { String tr(BuildContext context) {
switch (this) { switch (this) {
${keysWithoutVariables.map((key) => " case L10nStrings.$key:\n return L10n.of(context).$key;").join('\n')} ${keysWithoutVariables.map((key) => " case L10nStrings.$key:\n return L10n.of(context)!.$key;").join('\n')}
default: default:
return this; return this;