Merge pull request #2348 from ecency/nt/drafts-cache-extension

Nt/drafts cache extension
This commit is contained in:
Feruz M 2022-06-09 16:23:43 +03:00 committed by GitHub
commit d1fa363303
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 202 additions and 195 deletions

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect, useState, useCallback } from 'react';
import EStyleSheet from 'react-native-extended-stylesheet';
import styles from './quickReplyModalStyles';
import { View, Text, Alert, TouchableOpacity, Keyboard, Platform } from 'react-native';
@ -14,11 +14,12 @@ import {
updateDraftCache,
} from '../../redux/actions/cacheActions';
import { default as ROUTES } from '../../constants/routeNames';
import get from 'lodash/get';
import {get, debounce} from 'lodash';
import { navigate } from '../../navigation/service';
import { postBodySummary } from '@ecency/render-helper';
import { Draft } from '../../redux/reducers/cacheReducer';
import { RootState } from '../../redux/store/store';
import comment from '../../constants/options/comment';
export interface QuickReplyModalContentProps {
fetchPost?: any;
@ -43,7 +44,7 @@ export const QuickReplyModalContent = ({
const [commentValue, setCommentValue] = useState('');
const [isSending, setIsSending] = useState(false);
const [quickCommentDraft, setQuickCommentDraft] = useState<Draft>(null);
const headerText =
selectedPost && (selectedPost.summary || postBodySummary(selectedPost, 150, Platform.OS as any));
@ -60,7 +61,6 @@ export const QuickReplyModalContent = ({
if (drafts.has(draftId) && currentAccount.name === drafts.get(draftId).author) {
const quickComment: Draft = drafts.get(draftId);
setCommentValue(quickComment.body);
setQuickCommentDraft(quickComment);
} else {
setCommentValue('');
}
@ -73,25 +73,24 @@ export const QuickReplyModalContent = ({
};
// add quick comment value into cache
const _addQuickCommentIntoCache = () => {
const date = new Date();
const updatedStamp = date.toISOString().substring(0, 19);
const _addQuickCommentIntoCache = (value = commentValue) => {
const quickCommentDraftData: Draft = {
author: currentAccount.name,
body: commentValue,
created: quickCommentDraft ? quickCommentDraft.created : updatedStamp,
updated: updatedStamp,
expiresAt: date.getTime() + 604800000, // 7 days expiry time
body: value
};
//add quick comment cache entry
dispatch(updateDraftCache(draftId, quickCommentDraftData));
};
// handle close press
const _handleClosePress = () => {
sheetModalRef.current?.setModalVisible(false);
};
// navigate to post on summary press
const _handleOnSummaryPress = () => {
Keyboard.dismiss();
@ -105,6 +104,7 @@ export const QuickReplyModalContent = ({
});
};
// handle submit reply
const _submitReply = async () => {
let stateTimer;
@ -207,12 +207,22 @@ export const QuickReplyModalContent = ({
params: {
isReply: true,
post: selectedPost,
quickReplyText: commentValue,
fetchPost,
},
});
}
};
const _deboucedCacheUpdate = useCallback(debounce(_addQuickCommentIntoCache, 500),[])
const _onChangeText = (value) => {
setCommentValue(value);
_deboucedCacheUpdate(value)
}
//VIEW_RENDERERS
const _renderSheetHeader = () => (
@ -284,9 +294,7 @@ export const QuickReplyModalContent = ({
<View style={styles.inputContainer}>
<TextInput
innerRef={inputRef}
onChangeText={(value) => {
setCommentValue(value);
}}
onChangeText={_onChangeText}
value={commentValue}
// autoFocus
placeholder={intl.formatMessage({

View File

@ -2,93 +2,92 @@ import { renderPostBody } from '@ecency/render-helper';
import { Platform } from 'react-native';
import { makeJsonMetadataReply } from '../../utils/editor';
import {
UPDATE_VOTE_CACHE,
PURGE_EXPIRED_CACHE,
UPDATE_COMMENT_CACHE,
DELETE_COMMENT_CACHE_ENTRY,
UPDATE_DRAFT_CACHE,
DELETE_DRAFT_CACHE_ENTRY,
} from '../constants/constants';
UPDATE_VOTE_CACHE,
PURGE_EXPIRED_CACHE,
UPDATE_COMMENT_CACHE,
DELETE_COMMENT_CACHE_ENTRY,
UPDATE_DRAFT_CACHE,
DELETE_DRAFT_CACHE_ENTRY,
} from '../constants/constants';
import { Comment, Draft, Vote } from '../reducers/cacheReducer';
export const updateVoteCache = (postPath:string, vote:Vote) => ({
payload:{
postPath,
vote
},
type: UPDATE_VOTE_CACHE
})
interface CommentCacheOptions {
isUpdate?:boolean;
parentTags?:Array<string>;
}
export const updateVoteCache = (postPath: string, vote: Vote) => ({
payload: {
postPath,
vote
},
type: UPDATE_VOTE_CACHE
})
export const updateCommentCache = (commentPath:string, comment:Comment, options:CommentCacheOptions = {isUpdate:false}) => {
console.log("body received:", comment.markdownBody);
const updated = new Date();
updated.setSeconds(updated.getSeconds() - 5); //make cache delayed by 5 seconds to avoid same updated stamp in post data
const updatedStamp = updated.toISOString().substring(0, 19); //server only return 19 character time string without timezone part
if(options.isUpdate && !comment.created){
throw new Error("For comment update, created prop must be provided from original comment data to update local cache");
}
interface CommentCacheOptions {
isUpdate?: boolean;
parentTags?: Array<string>;
}
if(!options.parentTags && !comment.json_metadata){
throw new Error("either of json_metadata in comment data or parentTags in options must be provided");
}
export const updateCommentCache = (commentPath: string, comment: Comment, options: CommentCacheOptions = { isUpdate: false }) => {
comment.created = comment.created || updatedStamp; //created will be set only once for new comment;
comment.updated = comment.updated || updatedStamp;
comment.expiresAt = comment.expiresAt || updated.getTime() + 6000000;//600000;
comment.active_votes = comment.active_votes || [];
comment.net_rshares = comment.net_rshares || 0;
comment.author_reputation = comment.author_reputation || 25;
comment.total_payout = comment.total_payout || 0;
comment.json_metadata = comment.json_metadata || makeJsonMetadataReply(options.parentTags)
comment.isDeletable = comment.isDeletable || true;
console.log("body received:", comment.markdownBody);
const updated = new Date();
updated.setSeconds(updated.getSeconds() - 5); //make cache delayed by 5 seconds to avoid same updated stamp in post data
const updatedStamp = updated.toISOString().substring(0, 19); //server only return 19 character time string without timezone part
comment.body = renderPostBody({
author:comment.author,
permlink:comment.permlink,
last_update:comment.updated,
body:comment.markdownBody,
}, true, Platform.OS === 'android');
if (options.isUpdate && !comment.created) {
throw new Error("For comment update, created prop must be provided from original comment data to update local cache");
}
return ({
payload:{
commentPath,
comment
},
type: UPDATE_COMMENT_CACHE
})
}
if (!options.parentTags && !comment.json_metadata) {
throw new Error("either of json_metadata in comment data or parentTags in options must be provided");
}
export const deleteCommentCacheEntry = (commentPath:string) => ({
payload:commentPath,
type: DELETE_COMMENT_CACHE_ENTRY
})
export const updateDraftCache = (id:string, draft:Draft) => ({
payload:{
id,
draft
comment.created = comment.created || updatedStamp; //created will be set only once for new comment;
comment.updated = comment.updated || updatedStamp;
comment.expiresAt = comment.expiresAt || updated.getTime() + 6000000;//600000;
comment.active_votes = comment.active_votes || [];
comment.net_rshares = comment.net_rshares || 0;
comment.author_reputation = comment.author_reputation || 25;
comment.total_payout = comment.total_payout || 0;
comment.json_metadata = comment.json_metadata || makeJsonMetadataReply(options.parentTags)
comment.isDeletable = comment.isDeletable || true;
comment.body = renderPostBody({
author: comment.author,
permlink: comment.permlink,
last_update: comment.updated,
body: comment.markdownBody,
}, true, Platform.OS === 'android');
return ({
payload: {
commentPath,
comment
},
type: UPDATE_DRAFT_CACHE
type: UPDATE_COMMENT_CACHE
})
}
export const deleteDraftCacheEntry = (id:string) => ({
payload:id,
type: DELETE_DRAFT_CACHE_ENTRY
})
export const deleteCommentCacheEntry = (commentPath: string) => ({
payload: commentPath,
type: DELETE_COMMENT_CACHE_ENTRY
})
export const updateDraftCache = (id: string, draft: Draft) => ({
payload: {
id,
draft
},
type: UPDATE_DRAFT_CACHE
})
export const deleteDraftCacheEntry = (id: string) => ({
payload: id,
type: DELETE_DRAFT_CACHE_ENTRY
})
export const purgeExpiredCache = () => ({
type: PURGE_EXPIRED_CACHE
})
export const purgeExpiredCache = () => ({
type: PURGE_EXPIRED_CACHE
})

View File

@ -113,6 +113,7 @@ export const UPDATE_COMMENT_CACHE = 'UPDATE_COMMENT_CACHE';
export const DELETE_COMMENT_CACHE_ENTRY = 'DELETE_COMMENT_CACHE_ENTRY';
export const UPDATE_DRAFT_CACHE = 'UPDATE_DRAFT_CACHE';
export const DELETE_DRAFT_CACHE_ENTRY = 'DELETE_DRAFT_CACHE_ENTRY';
export const DEFAULT_USER_DRAFT_ID = 'DEFAULT_USER_DRAFT_ID_';
// TOOLTIPS
export const REGISTER_TOOLTIP = 'REGISTER_TOOLTIP';

View File

@ -28,10 +28,12 @@ export interface Comment {
export interface Draft {
author: string,
body?:string,
created?:string,
updated?:string,
expiresAt:number;
body:string,
title?:string,
tags?:string,
created?:number,
updated?:number,
expiresAt?:number;
}
interface State {
@ -93,7 +95,16 @@ const initialState:State = {
if(!state.drafts){
state.drafts = new Map<string, Draft>();
}
state.drafts.set(payload.id, payload.draft);
const curTime = new Date().getTime();
const curDraft = state.drafts.get(payload.id);
const payloadDraft = payload.draft;
payloadDraft.created = curDraft ? curDraft.created : curTime;
payloadDraft.updated = curTime;
payloadDraft.expiresAt = curTime + 604800000 // 7 days ms
state.drafts.set(payload.id, payloadDraft);
return {
...state, //spread operator in requried here, otherwise persist do not register change
lastUpdate: {

View File

@ -1,7 +1,7 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { injectIntl } from 'react-intl';
import { Alert, Platform } from 'react-native';
import { Alert } from 'react-native';
import ImagePicker from 'react-native-image-crop-picker';
import get from 'lodash/get';
import AsyncStorage from '@react-native-community/async-storage';
@ -25,7 +25,6 @@ import {
reblog,
postComment,
} from '../../../providers/hive/dhive';
import { setDraftPost, getDraftPost } from '../../../realm/realm';
// Constants
import { default as ROUTES } from '../../../constants/routeNames';
@ -45,8 +44,8 @@ import {
import EditorScreen from '../screen/editorScreen';
import bugsnapInstance from '../../../config/bugsnag';
import { removeBeneficiaries, setBeneficiaries } from '../../../redux/actions/editorActions';
import { TEMP_BENEFICIARIES_ID } from '../../../redux/constants/constants';
import { updateCommentCache } from '../../../redux/actions/cacheActions';
import { DEFAULT_USER_DRAFT_ID, TEMP_BENEFICIARIES_ID } from '../../../redux/constants/constants';
import { deleteDraftCacheEntry, updateCommentCache, updateDraftCache } from '../../../redux/actions/cacheActions';
/*
* Props Name Description Value
@ -90,7 +89,7 @@ class EditorContainer extends Component<any, any> {
const { currentAccount, navigation } = this.props;
const username = currentAccount && currentAccount.name ? currentAccount.name : '';
let isReply;
let quickReplyText;
let draftId;
let isEdit;
let post;
let _draft;
@ -133,12 +132,19 @@ class EditorContainer extends Component<any, any> {
}
if (navigationParams.isReply) {
({ isReply, quickReplyText } = navigationParams);
({ isReply } = navigationParams);
if(post){
draftId = `${currentAccount.name}/${post.author}/${post.permlink}`
}
this.setState({
isReply,
quickReplyText,
draftId,
autoFocusText: true,
});
if (draftId) {
this._getStorageDraft(username, isReply, { _id: draftId });
}
}
if (navigationParams.isEdit) {
@ -158,7 +164,7 @@ class EditorContainer extends Component<any, any> {
}
}
if (!isEdit && !_draft && !hasSharedIntent) {
if (!isEdit && !_draft && !draftId && !hasSharedIntent) {
this._fetchDraftsForComparison(isReply);
}
this._requestKeyboardFocus();
@ -194,49 +200,54 @@ class EditorContainer extends Component<any, any> {
}
_getStorageDraft = async (username, isReply, paramDraft) => {
if (isReply) {
const draftReply = await AsyncStorage.getItem('temp-reply');
const { drafts } = this.props;
if (draftReply) {
if (isReply) {
const _draft = drafts.get(paramDraft._id);
if (_draft && _draft.body) {
this.setState({
draftPost: {
body: draftReply,
body: _draft.body,
},
});
}
} else {
getDraftPost(username, paramDraft && paramDraft._id).then((result) => {
//if result is return and param draft available, compare timestamp, use latest
//if no draft, use result anayways
if (result && (!paramDraft || paramDraft.timestamp < result.timestamp)) {
this.setState({
draftPost: {
body: get(result, 'body', ''),
title: get(result, 'title', ''),
tags: get(result, 'tags', '').split(','),
isDraft: paramDraft ? true : false,
draftId: paramDraft ? paramDraft._id : null,
},
});
}
//TOOD: get draft from redux after reply side is complete
const _draftId = paramDraft ? paramDraft._id : DEFAULT_USER_DRAFT_ID + username;
const _localDraft = drafts.get(_draftId);
//if above fails with either no result returned or timestamp is old,
// and use draft form nav param if available.
else if (paramDraft) {
const _tags = paramDraft.tags.includes(' ')
? paramDraft.tags.split(' ')
: paramDraft.tags.split(',');
this.setState({
draftPost: {
title: paramDraft.title,
body: paramDraft.body,
tags: _tags,
},
isDraft: true,
draftId: paramDraft._id,
});
}
});
//if _draft is returned and param draft is available, compare timestamp, use latest
//if no draft, use result anayways
if (_localDraft && (!paramDraft || paramDraft.timestamp < _localDraft.updated)) {
this.setState({
draftPost: {
body: get(_localDraft, 'body', ''),
title: get(_localDraft, 'title', ''),
tags: get(_localDraft, 'tags', '').split(','),
isDraft: paramDraft ? true : false,
draftId: paramDraft ? paramDraft._id : null,
},
});
}
//if above fails with either no result returned or timestamp is old,
// and use draft form nav param if available.
else if (paramDraft) {
const _tags = paramDraft.tags.includes(' ')
? paramDraft.tags.split(' ')
: paramDraft.tags.split(',');
this.setState({
draftPost: {
title: paramDraft.title,
body: paramDraft.body,
tags: _tags,
},
isDraft: true,
draftId: paramDraft._id,
});
}
;
}
};
@ -251,14 +262,14 @@ class EditorContainer extends Component<any, any> {
};
/**
* this fucntion is run if editor is access used mid tab or reply section
* this fucntion is run if editor is access fused mid tab or reply section
* it fetches fresh drafts and run some comparions to load one of following
* empty editor, load non-remote draft or most recent remote draft based on timestamps
* prompts user as well
* @param isReply
**/
_fetchDraftsForComparison = async (isReply) => {
const { currentAccount, isLoggedIn, intl, dispatch } = this.props;
const { currentAccount, isLoggedIn, intl, dispatch, drafts } = this.props;
const username = get(currentAccount, 'name', '');
//initilizes editor with reply or non remote id less draft
@ -282,28 +293,29 @@ class EditorContainer extends Component<any, any> {
return;
}
const drafts = await getDrafts(username);
const idLessDraft = await getDraftPost(username);
const remoteDrafts = await getDrafts(username);
const idLessDraft = drafts.get(DEFAULT_USER_DRAFT_ID + username)
const loadRecentDraft = () => {
//if no draft available means local draft is recent
if (drafts.length == 0) {
if (remoteDrafts.length == 0) {
_getStorageDraftGeneral(false);
return;
}
//sort darts based on timestamps
drafts.sort((d1, d2) =>
remoteDrafts.sort((d1, d2) =>
new Date(d1.modified).getTime() < new Date(d2.modified).getTime() ? 1 : -1,
);
const _draft = drafts[0];
const _draft = remoteDrafts[0];
//if unsaved local draft is more latest then remote draft, use that instead
//if editor was opened from draft screens, this code will be skipped anyways.
if (
idLessDraft &&
(idLessDraft.title !== '' || idLessDraft.tags !== '' || idLessDraft.body !== '') &&
new Date(_draft.modified).getTime() < idLessDraft.timestamp
new Date(_draft.modified).getTime() < idLessDraft.updated
) {
_getStorageDraftGeneral(false);
return;
@ -317,7 +329,7 @@ class EditorContainer extends Component<any, any> {
this._getStorageDraft(username, isReply, _draft);
};
if (drafts.length > 0 || (idLessDraft && idLessDraft.timestamp > 0)) {
if (remoteDrafts.length > 0 || (idLessDraft && idLessDraft.updated > 0)) {
this.setState({
onLoadDraftPress: loadRecentDraft,
});
@ -523,9 +535,14 @@ class EditorContainer extends Component<any, any> {
};
_saveDraftToDB = async (fields, saveAsNew = false) => {
const { isDraftSaved, draftId, thumbIndex } = this.state;
const { isDraftSaved, draftId, thumbIndex, isReply } = this.state;
const { currentAccount, dispatch, intl } = this.props;
if (isReply) {
return;
}
const beneficiaries = this._extractBeneficiaries();
try {
@ -574,16 +591,8 @@ class EditorContainer extends Component<any, any> {
//clear local copy if darft save is successful
const username = get(currentAccount, 'name', '');
setDraftPost(
{
title: '',
body: '',
tags: '',
timestamp: 0,
},
username,
saveAsNew ? draftId : undefined
);
dispatch(deleteDraftCacheEntry(draftId || (DEFAULT_USER_DRAFT_ID + username)))
}
@ -626,26 +635,28 @@ class EditorContainer extends Component<any, any> {
return;
}
const { currentAccount } = this.props;
const { currentAccount, dispatch } = this.props;
const username = currentAccount && currentAccount.name ? currentAccount.name : '';
const draftField = {
...fields,
title: fields.title,
body: fields.body,
tags: fields.tags && fields.tags.length > 0 ? fields.tags.toString() : '',
};
author: username,
}
//save reply data
if (isReply && draftField.body !== null) {
await AsyncStorage.setItem('temp-reply', draftField.body);
dispatch(updateDraftCache(draftId, draftField))
//save existing draft data locally
} else if (draftId) {
setDraftPost(draftField, username, draftId);
dispatch(updateDraftCache(draftId, draftField))
}
//update editor data locally
else if (!isReply) {
setDraftPost(draftField, username);
dispatch(updateDraftCache(DEFAULT_USER_DRAFT_ID + username, draftField));
}
};
@ -744,15 +755,7 @@ class EditorContainer extends Component<any, any> {
}
//post publish updates
setDraftPost(
{
title: '',
body: '',
tags: '',
timestamp: 0,
},
currentAccount.name,
);
dispatch(deleteDraftCacheEntry(DEFAULT_USER_DRAFT_ID + currentAccount.name))
dispatch(removeBeneficiaries(TEMP_BENEFICIARIES_ID))
if (draftId) {
@ -1139,15 +1142,8 @@ class EditorContainer extends Component<any, any> {
}),
),
);
setDraftPost(
{
title: '',
body: '',
tags: '',
timestamp: 0,
},
currentAccount.name,
);
dispatch(deleteDraftCacheEntry(DEFAULT_USER_DRAFT_ID + currentAccount.name))
setTimeout(() => {
navigation.replace(ROUTES.SCREENS.DRAFTS,
@ -1167,17 +1163,10 @@ class EditorContainer extends Component<any, any> {
_initialEditor = () => {
const {
currentAccount: { name },
dispatch
} = this.props;
setDraftPost(
{
title: '',
body: '',
tags: '',
timestamp: 0,
},
name,
);
dispatch(deleteDraftCacheEntry(DEFAULT_USER_DRAFT_ID + name))
this.setState({
uploadedImage: null,
@ -1271,7 +1260,8 @@ const mapStateToProps = (state) => ({
isDefaultFooter: state.account.isDefaultFooter,
isLoggedIn: state.application.isLoggedIn,
pinCode: state.application.pin,
beneficiariesMap: state.editor.beneficiariesMap
beneficiariesMap: state.editor.beneficiariesMap,
drafts: state.cache.drafts,
});
export default connect(mapStateToProps)(injectIntl(EditorContainer));

View File

@ -367,7 +367,6 @@ class EditorScreen extends Component {
isLoggedIn,
isPostSending,
isReply,
quickReplyText,
isUploading,
post,
uploadedImage,
@ -406,8 +405,7 @@ class EditorScreen extends Component {
</Modal>
);
};
console.log('fields :', fields);
console.log('quickReplyText : ', quickReplyText);
return (
<View style={globalStyles.defaultContainer}>
@ -451,7 +449,7 @@ console.log('quickReplyText : ', quickReplyText);
)}
<MarkdownEditor
componentID="body"
draftBody={isReply ? quickReplyText : fields && fields.body}
draftBody={fields && fields.body}
handleOnTextChange={this._setWordsCount}
handleFormUpdate={this._handleFormUpdate}
handleIsFormValid={this._handleIsFormValid}