Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add drafts for posts and comments #700

Merged
merged 11 commits into from
Sep 8, 2023
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
- Added liveness and latency indicators for instances in profile switcher - contribution from @micahmo
- Add option to disabling graying out read posts - contribution from @micahmo
- Downvote actions will be disabled when instances have downvotes disabled
- Automatically save drafts for posts and comments - contribution from @micahmo

### Changed

Expand Down
5 changes: 4 additions & 1 deletion lib/community/pages/community_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ class _CommunityPageState extends State<CommunityPage> with AutomaticKeepAliveCl
bool isFabSummoned = true;
bool enableFab = false;
bool isActivePage = true;
bool showBackButton = false;

@override
void initState() {
Expand All @@ -68,6 +69,8 @@ class _CommunityPageState extends State<CommunityPage> with AutomaticKeepAliveCl
isActivePage = widget.pageController!.page == 0;
});
BackButtonInterceptor.add(_handleBack);

showBackButton = Navigator.of(context).canPop() && currentCommunityBloc?.state.communityId != null && widget.scaffoldKey?.currentState?.isDrawerOpen != true;
}

@override
Expand Down Expand Up @@ -181,7 +184,7 @@ class _CommunityPageState extends State<CommunityPage> with AutomaticKeepAliveCl
}
},
),
leading: Navigator.of(context).canPop() && currentCommunityBloc?.state.communityId != null && widget.scaffoldKey?.currentState?.isDrawerOpen != true
leading: showBackButton
? IconButton(
icon: Icon(
Icons.arrow_back_rounded,
Expand Down
54 changes: 53 additions & 1 deletion lib/community/pages/create_post_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,16 @@ import 'package:thunder/utils/instance.dart';
class CreatePostPage extends StatefulWidget {
final int communityId;
final FullCommunityView? communityInfo;
final void Function(DraftPost? draftPost)? onUpdateDraft;
final DraftPost? previousDraftPost;

const CreatePostPage({super.key, required this.communityId, this.communityInfo});
const CreatePostPage({
super.key,
required this.communityId,
this.communityInfo,
this.previousDraftPost,
this.onUpdateDraft,
});

@override
State<CreatePostPage> createState() => _CreatePostPageState();
Expand All @@ -36,6 +44,7 @@ class _CreatePostPageState extends State<CreatePostPage> {
bool imageUploading = false;
bool postImageUploading = false;
String url = "";
DraftPost newDraftPost = DraftPost();

final TextEditingController _bodyTextController = TextEditingController();
final TextEditingController _titleTextController = TextEditingController();
Expand All @@ -50,12 +59,31 @@ class _CreatePostPageState extends State<CreatePostPage> {
_titleTextController.addListener(() {
if (_titleTextController.text.isEmpty && !isSubmitButtonDisabled) setState(() => isSubmitButtonDisabled = true);
if (_titleTextController.text.isNotEmpty && isSubmitButtonDisabled) setState(() => isSubmitButtonDisabled = false);

widget.onUpdateDraft?.call(newDraftPost..title = _titleTextController.text);
});

_urlTextController.addListener(() {
url = _urlTextController.text;
debounce(const Duration(milliseconds: 1000), _updatePreview, [url]);

widget.onUpdateDraft?.call(newDraftPost..url = _urlTextController.text);
});

_bodyTextController.addListener(() {
widget.onUpdateDraft?.call(newDraftPost..text = _bodyTextController.text);
});

if (widget.previousDraftPost != null) {
_titleTextController.text = widget.previousDraftPost!.title ?? '';
_urlTextController.text = widget.previousDraftPost!.url ?? '';
_bodyTextController.text = widget.previousDraftPost!.text ?? '';

WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
await Future.delayed(const Duration(milliseconds: 300));
showSnackbar(context, AppLocalizations.of(context)!.restoredPostFromDraft);
});
}
}

@override
Expand Down Expand Up @@ -87,6 +115,7 @@ class _CreatePostPageState extends State<CreatePostPage> {
onPressed: isSubmitButtonDisabled
? null
: () {
newDraftPost.saveAsDraft = false;
url != ''
? context.read<CommunityBloc>().add(CreatePostEvent(name: _titleTextController.text, body: _bodyTextController.text, nsfw: isNSFW, url: url))
: context.read<CommunityBloc>().add(CreatePostEvent(name: _titleTextController.text, body: _bodyTextController.text, nsfw: isNSFW));
Expand Down Expand Up @@ -298,3 +327,26 @@ class _CreatePostPageState extends State<CreatePostPage> {
}
}
}

class DraftPost {
String? title;
String? url;
String? text;
bool saveAsDraft = true;

DraftPost({this.title, this.url, this.text});

Map<String, dynamic> toJson() => {
'title': title,
'url': url,
'text': text,
};

static fromJson(Map<String, dynamic> json) => DraftPost(
title: json['title'],
url: json['url'],
text: json['text'],
);

bool get isNotEmpty => title?.isNotEmpty == true || url?.isNotEmpty == true || text?.isNotEmpty == true;
}
43 changes: 40 additions & 3 deletions lib/community/widgets/community_sidebar.dart
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
import 'dart:async';
import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:intl/intl.dart';

import 'package:lemmy_api_client/v3.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:swipeable_page_route/swipeable_page_route.dart';

import 'package:thunder/account/bloc/account_bloc.dart';
import 'package:thunder/community/bloc/community_bloc.dart';
import 'package:thunder/core/auth/bloc/auth_bloc.dart';
import 'package:thunder/core/enums/local_settings.dart';
import 'package:thunder/core/singletons/preferences.dart';
import 'package:thunder/shared/snackbar.dart';
import 'package:thunder/shared/user_avatar.dart';
import 'package:thunder/utils/instance.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';

import '../../shared/common_markdown_body.dart';
import '../../thunder/bloc/thunder_bloc.dart';
Expand Down Expand Up @@ -129,11 +136,26 @@ class _CommunitySidebarState extends State<CommunitySidebar> with TickerProvider
Expanded(
child: ElevatedButton(
onPressed: isUserLoggedIn
? () {
? () async {
HapticFeedback.mediumImpact();
CommunityBloc communityBloc = context.read<CommunityBloc>();
AccountBloc accountBloc = context.read<AccountBloc>();
ThunderBloc thunderBloc = context.read<ThunderBloc>();

SharedPreferences prefs = (await UserPreferences.instance).sharedPreferences;
hjiangsu marked this conversation as resolved.
Show resolved Hide resolved
DraftPost? newDraftPost;
DraftPost? previousDraftPost;
String draftId = '${LocalSettings.draftsCache.name}-${widget.communityInfo!.communityView.community.id}';
String? draftPostJson = prefs.getString(draftId);
if (draftPostJson != null) {
previousDraftPost = DraftPost.fromJson(jsonDecode(draftPostJson));
}
Timer timer = Timer.periodic(const Duration(seconds: 10), (Timer t) {
if (newDraftPost?.isNotEmpty == true) {
prefs.setString(draftId, jsonEncode(newDraftPost!.toJson()));
}
});

Navigator.of(context).push(
SwipeablePageRoute(
builder: (context) {
Expand All @@ -143,11 +165,26 @@ class _CommunitySidebarState extends State<CommunitySidebar> with TickerProvider
BlocProvider<AccountBloc>.value(value: accountBloc),
BlocProvider<ThunderBloc>.value(value: thunderBloc)
],
child: CreatePostPage(communityId: widget.communityInfo!.communityView.community.id, communityInfo: widget.communityInfo),
child: CreatePostPage(
communityId: widget.communityInfo!.communityView.community.id,
communityInfo: widget.communityInfo,
previousDraftPost: previousDraftPost,
onUpdateDraft: (p) => newDraftPost = p,
),
);
},
),
);
).whenComplete(() async {
timer.cancel();

if (newDraftPost?.saveAsDraft == true && newDraftPost?.isNotEmpty == true) {
await Future.delayed(const Duration(milliseconds: 300));
showSnackbar(context, AppLocalizations.of(context)!.postSavedAsDraft);
prefs.setString(draftId, jsonEncode(newDraftPost!.toJson()));
} else {
prefs.remove(draftId);
}
});
}
: null,
style: TextButton.styleFrom(
Expand Down
42 changes: 39 additions & 3 deletions lib/core/enums/fab_action.dart
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
import 'dart:async';
import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:lemmy_api_client/v3.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:swipeable_page_route/swipeable_page_route.dart';
import 'package:thunder/account/bloc/account_bloc.dart';
import 'package:thunder/community/bloc/community_bloc.dart';
import 'package:thunder/community/pages/community_page.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:thunder/community/pages/create_post_page.dart';
import 'package:thunder/core/auth/bloc/auth_bloc.dart';
import 'package:thunder/core/enums/local_settings.dart';
import 'package:thunder/core/models/post_view_media.dart';
import 'package:thunder/core/singletons/preferences.dart';
import 'package:thunder/post/bloc/post_bloc.dart';
import 'package:thunder/shared/snackbar.dart';
import 'package:thunder/thunder/bloc/thunder_bloc.dart';
Expand Down Expand Up @@ -83,7 +89,7 @@ enum FeedFabAction {
}
}

void execute(BuildContext context, CommunityState state, {CommunityBloc? bloc, CommunityPage? widget, void Function()? override, SortType? sortType}) {
void execute(BuildContext context, CommunityState state, {CommunityBloc? bloc, CommunityPage? widget, void Function()? override, SortType? sortType}) async {
if (override != null) {
override();
}
Expand Down Expand Up @@ -118,6 +124,21 @@ enum FeedFabAction {
} else {
ThunderBloc thunderBloc = context.read<ThunderBloc>();
AccountBloc accountBloc = context.read<AccountBloc>();

SharedPreferences prefs = (await UserPreferences.instance).sharedPreferences;
DraftPost? newDraftPost;
DraftPost? previousDraftPost;
String draftId = '${LocalSettings.draftsCache.name}-${state.communityId!}';
String? draftPostJson = prefs.getString(draftId);
if (draftPostJson != null) {
previousDraftPost = DraftPost.fromJson(jsonDecode(draftPostJson));
}
Timer timer = Timer.periodic(const Duration(seconds: 10), (Timer t) {
if (newDraftPost?.isNotEmpty == true) {
prefs.setString(draftId, jsonEncode(newDraftPost!.toJson()));
}
});

Navigator.of(context).push(
SwipeablePageRoute(
builder: (context) {
Expand All @@ -127,11 +148,26 @@ enum FeedFabAction {
BlocProvider<ThunderBloc>.value(value: thunderBloc),
BlocProvider<AccountBloc>.value(value: accountBloc),
],
child: CreatePostPage(communityId: state.communityId!, communityInfo: state.communityInfo),
child: CreatePostPage(
communityId: state.communityId!,
communityInfo: state.communityInfo,
previousDraftPost: previousDraftPost,
onUpdateDraft: (p) => newDraftPost = p,
),
);
},
),
);
).whenComplete(() async {
timer.cancel();

if (newDraftPost?.saveAsDraft == true && newDraftPost?.isNotEmpty == true) {
await Future.delayed(const Duration(milliseconds: 300));
showSnackbar(context, AppLocalizations.of(context)!.postSavedAsDraft);
prefs.setString(draftId, jsonEncode(newDraftPost!.toJson()));
} else {
prefs.remove(draftId);
}
});
}
}
}
Expand Down
5 changes: 5 additions & 0 deletions lib/core/enums/local_settings.dart
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ enum LocalSettings {
postFabLongPressAction(name: 'settings_post_fab_long_press_action', label: ''),
enableCommentNavigation(name: 'setting_enable_comment_navigation', label: 'Enable Comment Navigation Buttons'),
combineNavAndFab(name: 'setting_combine_nav_and_fab', label: 'Combine FAB and Navigation Buttons'),

draftsCache(name: 'drafts_cache', label: ''),
micahmo marked this conversation as resolved.
Show resolved Hide resolved
;

const LocalSettings({
Expand All @@ -107,4 +109,7 @@ enum LocalSettings {

/// The label of the setting as seen in the Settings page
final String label;

/// Defines the settings that are excluded from import/export
static List<LocalSettings> importExportExcludedSettings = [LocalSettings.draftsCache];
}
3 changes: 2 additions & 1 deletion lib/core/singletons/preferences.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'dart:io';
import 'package:flutter_file_dialog/flutter_file_dialog.dart';
import 'package:path_provider/path_provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:thunder/core/enums/local_settings.dart';

class UserPreferences {
late SharedPreferences sharedPreferences;
Expand All @@ -25,7 +26,7 @@ class UserPreferences {
// Export SharedPreferences data to selected JSON file
static Future<void> exportToJson() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
Map<String, dynamic> data = prefs.getKeys().fold({}, (prev, key) {
Map<String, dynamic> data = prefs.getKeys().where((key) => !LocalSettings.importExportExcludedSettings.any((excluded) => key.startsWith(excluded.name))).fold({}, (prev, key) {
prev[key] = prefs.get(key);
return prev;
});
Expand Down
Loading
Loading