Skip to content

Commit

Permalink
Merge pull request #700 from micahmo/feature/post-comment-drafts
Browse files Browse the repository at this point in the history
Add drafts for posts and comments
  • Loading branch information
hjiangsu authored Sep 8, 2023
2 parents a0bbc87 + 87674eb commit d36a033
Show file tree
Hide file tree
Showing 20 changed files with 545 additions and 95 deletions.
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;
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: ''),
;

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

0 comments on commit d36a033

Please sign in to comment.