Merge branch 'development' of https://github.com/ecency/ecency-mobile into sa/IAP-QR

This commit is contained in:
Sadaqat Ali 2022-06-10 16:49:46 +05:00
commit aaad438287
13 changed files with 351 additions and 435 deletions

View File

@ -1,15 +1,12 @@
import React, { useState, Fragment, useRef } from 'react';
import { View, Text, ActivityIndicator, SafeAreaView } from 'react-native';
import { injectIntl } from 'react-intl';
import { useSelector } from 'react-redux';
import moment from 'moment';
// Components
import { TextButton, Modal, BeneficiaryModal } from '../..';
import { TextButton } from '../..';
import { IconButton } from '../../iconButton';
import { DropdownButton } from '../../dropdownButton';
import { TextInput } from '../../textInput';
import { DateTimePicker } from '../../dateTimePicker';
// Constants
// Styles
@ -28,7 +25,6 @@ const BasicHeaderView = ({
intl,
isDraftSaved,
isDraftSaving,
draftId,
isFormValid,
isHasDropdown,
isHasIcons,
@ -46,23 +42,14 @@ const BasicHeaderView = ({
title,
handleOnSubmit,
handleOnSearch,
handleDatePickerChange,
handleRewardChange,
handleBeneficiaries,
enableViewModeToggle,
handleSettingsPress,
showThumbSelectionModal,
}) => {
const [isInputVisible, setIsInputVisible] = useState(false);
const [beneficiaryModal, setBeneficiaryModal] = useState(false);
const [showScheduleModal, setShowScheduleModal] = useState(false);
const [scheduledDate, setScheduledDate] = useState('');
const username = useSelector((state) => state.account.currentAccount.name);
const settingMenuRef = useRef(null);
const rewardMenuRef = useRef(null);
const scheduleRef = useRef(null);
/**
*
@ -90,27 +77,6 @@ const BasicHeaderView = ({
handleOnSearch(value);
};
const _handleSettingMenuSelect = (index) => {
switch (index) {
case 0:
setShowScheduleModal(true);
break;
case 1:
if (showThumbSelectionModal) {
showThumbSelectionModal();
}
break;
case 2:
rewardMenuRef.current.show();
break;
case 3:
setBeneficiaryModal(true);
break;
default:
break;
}
};
const _handleRewardMenuSelect = (index) => {
let rewardType = 'default';
@ -131,32 +97,7 @@ const BasicHeaderView = ({
}
};
const _handleOnSaveBeneficiaries = (beneficiaries) => {
const _beneficiaries = beneficiaries.map((item) => ({
account: item.account,
weight: item.weight,
}));
setBeneficiaryModal(false);
if (handleBeneficiaries) {
handleBeneficiaries(_beneficiaries);
}
};
const _handleDatePickerChange = (datePickerValue) => {
setScheduledDate(datePickerValue);
};
const _onPressDone = () => {
let dateString = scheduledDate;
if (dateString === '') {
dateString = moment().format();
}
setScheduledDate('');
handleDatePickerChange(dateString);
setShowScheduleModal(false);
};
/**
*
@ -291,57 +232,8 @@ const BasicHeaderView = ({
</Fragment>
)}
</View>
<Modal
isOpen={beneficiaryModal}
isFullScreen
isCloseButton
presentationStyle="formSheet"
handleOnModalClose={() => setBeneficiaryModal(false)}
title={intl.formatMessage({ id: 'editor.beneficiaries' })}
animationType="slide"
style={styles.beneficiaryModal}
>
<BeneficiaryModal
username={username}
handleOnSaveBeneficiaries={_handleOnSaveBeneficiaries}
draftId={draftId}
/>
</Modal>
<Modal
isFullScreen={false}
isOpen={showScheduleModal}
isBottomModal
isTransparent
isRadius
coverScreen={false}
title={intl.formatMessage({ id: 'editor.schedule_modal_title' })}
hasRightText
rightText="Done"
onPressRightText={_onPressDone}
onBackdropPress={() => setShowScheduleModal(false)}
>
<SafeAreaView style={styles.dateTimeModal}>
<DateTimePicker
type="datetime"
onChanged={_handleDatePickerChange}
disabled={!isFormValid}
ref={scheduleRef}
/>
</SafeAreaView>
</Modal>
<OptionsModal
ref={settingMenuRef}
options={[
intl.formatMessage({ id: 'editor.setting_schedule' }),
intl.formatMessage({ id: 'editor.setting_thumb' }),
intl.formatMessage({ id: 'editor.setting_reward' }),
intl.formatMessage({ id: 'editor.setting_beneficiary' }),
intl.formatMessage({ id: 'alert.cancel' }),
]}
cancelButtonIndex={4}
title={intl.formatMessage({ id: 'editor.options' })}
onPress={_handleSettingMenuSelect}
/>
<OptionsModal
ref={rewardMenuRef}
options={[
@ -354,6 +246,7 @@ const BasicHeaderView = ({
title="Reward"
onPress={_handleRewardMenuSelect}
/>
</SafeAreaView>
);
};

View File

@ -53,9 +53,13 @@ import { walkthrough } from '../../../redux/constants/walkthroughConstants';
const MIN_BODY_INPUT_HEIGHT = 300;
//These variable keep track of body text input state,
//this helps keep load on minimal compared to both useState and useRef;
var bodyText = '';
var bodySelection = {start: 0, end: 0};
const MarkdownEditorView = ({
draftBody,
handleIsFormValid,
handleOpenImagePicker,
intl,
isPreviewActive,
@ -64,9 +68,6 @@ const MarkdownEditorView = ({
isUploading,
initialFields,
onChange,
handleOnTextChange,
handleIsValid,
componentID,
uploadedImage,
isEdit,
post,
@ -82,8 +83,6 @@ const MarkdownEditorView = ({
}) => {
const dispatch = useDispatch();
const [text, setText] = useState(draftBody || '');
const [selection, setSelection] = useState({ start: 0, end: 0 });
const [editable, setEditable] = useState(true);
const [bodyInputHeight, setBodyInputHeight] = useState(MIN_BODY_INPUT_HEIGHT);
const [isSnippetsOpen, setIsSnippetsOpen] = useState(false);
@ -103,9 +102,14 @@ const MarkdownEditorView = ({
const draftBtnTooltipRegistered = draftBtnTooltipState.get(walkthrough.EDITOR_DRAFT_BTN);
const headerText = post && (post.summary || postBodySummary(post, 150, Platform.OS));
useEffect(() => {
bodyText = '';
bodySelection = {start:0, end:0};
}, []);
useEffect(() => {
if (!isPreviewActive) {
_setTextAndSelection({ selection: { start: 0, end: 0 }, text });
_setTextAndSelection({ selection: { start: 0, end: 0 }, text: bodyText });
}
}, [isPreviewActive]);
@ -121,7 +125,7 @@ const MarkdownEditorView = ({
}, [onLoadDraftPress]);
useEffect(() => {
if (text === '' && draftBody !== '') {
if (bodyText === '' && draftBody !== '') {
let draftBodyLength = draftBody.length;
_setTextAndSelection({
selection: { start: draftBodyLength, end: draftBodyLength },
@ -165,8 +169,8 @@ const MarkdownEditorView = ({
useEffect(() => {
if (uploadedImage && uploadedImage.shouldInsert && !isUploading) {
applyMediaLink({
text,
selection,
text: bodyText,
selection: bodySelection,
setTextAndSelection: _setTextAndSelection,
items: [{ url: uploadedImage.url, text: uploadedImage.hash }],
});
@ -178,7 +182,7 @@ const MarkdownEditorView = ({
}, [uploadedImage, isUploading]);
useEffect(() => {
setText(draftBody);
bodyText = draftBody;
}, [draftBody]);
useEffect(() => {
@ -190,17 +194,7 @@ const MarkdownEditorView = ({
}
}, [autoFocusText]);
useEffect(() => {
const nextText = text.replace(text, '');
if (nextText && nextText.length > 0) {
_changeText(text);
if (handleIsFormValid) {
handleIsFormValid(text);
}
}
}, [text]);
const changeUser = async () => {
dispatch(toggleAccountsBottomSheet(!isVisibleAccountsBottomSheet));
@ -208,8 +202,8 @@ const MarkdownEditorView = ({
const _onApplyUsername = (username) => {
applyUsername({
text,
selection,
text: bodyText,
selection: bodySelection,
setTextAndSelection: _setTextAndSelection,
username,
});
@ -217,23 +211,17 @@ const MarkdownEditorView = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
const _changeText = useCallback((input) => {
setText(input);
bodyText = input;
//NOTE: onChange method is called by direct parent of MarkdownEditor that is PostForm, do not remove
if (onChange) {
onChange(input);
}
}, []);
if (handleIsValid) {
handleIsValid(componentID, !!(input && input.length));
}
if (handleOnTextChange) {
handleOnTextChange(input);
}
});
const _handleOnSelectionChange = async (event) => {
setSelection(event.nativeEvent.selection);
bodySelection = event.nativeEvent.selection;
};
const _handleOnContentSizeChange = async (event) => {
@ -243,35 +231,38 @@ const MarkdownEditorView = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
const _setTextAndSelection = useCallback(({ selection: _selection, text: _text }) => {
console.log('_text : ', _text);
// console.log('_text : ', _text);
inputRef.current.setNativeProps({
text: _text,
});
// Workaround for iOS selection update issue
const isIos = Platform.OS === 'ios';
if (isIos) {
setTimeout(() => {
inputRef.current.setNativeProps({
selection: _selection,
});
setSelection(_selection);
}, 100);
} else {
const _updateSelection = () => {
bodySelection = _selection
inputRef.current.setNativeProps({
selection: _selection,
});
setSelection(_selection);
}
setIsSnippetsOpen(false);
_changeText(_text);
});
console.log('text : ', text);
// Workaround for iOS selection update issue
if (Platform.OS === 'ios') {
setTimeout(() => {
_updateSelection();
}, 100);
} else {
_updateSelection()
}
if (isSnippetsOpen) {
setIsSnippetsOpen(false);
}
_changeText(_text);
}, []);
const _renderPreview = () => (
<ScrollView style={styles.previewContainer}>
{text ? (
<PostBody body={renderPostBody(text, true, Platform.OS === 'ios' ? false : true)} />
{bodyText ? (
<PostBody body={renderPostBody(bodyText, true, Platform.OS === 'ios' ? false : true)} />
) : (
<Text>...</Text>
)}
@ -280,8 +271,8 @@ const MarkdownEditorView = ({
const _handleOnSnippetReceived = (snippetText) => {
applySnippet({
text,
selection,
text: bodyText,
selection: bodySelection,
setTextAndSelection: _setTextAndSelection,
snippetText: `\n${snippetText}\n`,
});
@ -295,8 +286,8 @@ const MarkdownEditorView = ({
if (items.length) {
applyMediaLink({
text,
selection,
text: bodyText,
selection: bodySelection,
setTextAndSelection: _setTextAndSelection,
items,
});
@ -305,8 +296,8 @@ const MarkdownEditorView = ({
const _handleOnAddLinkPress = () => {
insertLinkModalRef.current?.showModal({
selectedText: text.slice(selection.start, selection.end),
selection: selection,
selectedText: bodyText.slice(bodySelection.start, bodySelection.end),
selection: bodySelection,
});
inputRef.current?.blur();
};
@ -315,7 +306,7 @@ const MarkdownEditorView = ({
};
const _handleInsertLink = ({ snippetText, selection }) => {
applySnippet({
text,
text: bodyText,
selection,
setTextAndSelection: _setTextAndSelection,
snippetText,
@ -332,7 +323,12 @@ const MarkdownEditorView = ({
iconType={item.iconType}
name={item.icon}
onPress={() =>
item.onPress({ text, selection, setTextAndSelection: _setTextAndSelection, item })
item.onPress({
text: bodyText,
selection: bodySelection,
setTextAndSelection: _setTextAndSelection,
item
})
}
/>
</View>
@ -427,7 +423,7 @@ const MarkdownEditorView = ({
const _handleClear = (index) => {
if (index === 0) {
initialFields();
setText('');
_setTextAndSelection({ text: '', selection: { start: 0, end: 0 } });
}
};
@ -513,7 +509,7 @@ const MarkdownEditorView = ({
const _innerContent = (
<>
{isAndroidOreo() ? _renderEditorWithoutScroll() : _renderEditorWithScroll()}
<UsernameAutofillBar text={text} selection={selection} onApplyUsername={_onApplyUsername} />
<UsernameAutofillBar text={bodyText} selection={bodySelection} onApplyUsername={_onApplyUsername} />
{_renderFloatingDraftButton()}
{!isPreviewActive && _renderEditorButtons()}
</>

View File

@ -1,4 +1,4 @@
import React, {useState, useEffect} from 'react';
import React, {useState, useEffect, useCallback} from 'react';
import { View, FlatList, Text, TouchableOpacity } from "react-native"
import { UserAvatar } from '../..';
import { lookupAccounts } from '../../../providers/hive/dhive';
@ -22,21 +22,25 @@ export const UsernameAutofillBar = ({text, selection, onApplyUsername}:Props) =>
useEffect(() => {
if (selection.start === selection.end && text) {
const word = extractWordAtIndex(text, selection.start);
console.log('selection word is: ', word);
if (word.startsWith('@') && word.length > 3) {
_handleUserSearch(word.substring(1));
} else {
setSearchedUsers([]);
setQuery('')
_handleUserSearch.cancel();
}
_processTextForSearch(text, selection.start);
}
}, [text, selection])
const _processTextForSearch = useCallback(debounce((text:string, index:number) => {
const word = extractWordAtIndex(text, index);
console.log('selection word is: ', word);
if (word.startsWith('@') && word.length > 1) {
_handleUserSearch(word.substring(1));
} else {
setSearchedUsers([]);
setQuery('')
_handleUserSearch.cancel();
}
}, 300, {leading:true}),[]);
const _handleUserSearch = debounce(async (username) => {
const _handleUserSearch = useCallback(debounce(async (username) => {
if(query !== username){
let users = [];
if (username) {
@ -47,7 +51,7 @@ export const UsernameAutofillBar = ({text, selection, onApplyUsername}:Props) =>
setSearchedUsers(users);
}
}, 200, {leading:true});
}, 200, {leading:true}), []);

View File

@ -1,4 +1,5 @@
import React, { PureComponent, Fragment } from 'react';
import { debounce } from 'lodash';
class PostFormView extends PureComponent {
constructor(props) {
@ -18,9 +19,12 @@ class PostFormView extends PureComponent {
};
_handleOnChange = (componentID, value, isValid = null) => {
const { handleFormUpdate } = this.props;
const { handleFormUpdate, handleBodyChange } = this.props;
console.log('update fields state :', componentID, value);
handleFormUpdate(componentID, value, !!isValid || !!value);
if (componentID === 'body') {
handleBodyChange(value);
}
};
render() {
@ -33,7 +37,10 @@ class PostFormView extends PureComponent {
return React.cloneElement(child, {
onSubmitEditing: (item) =>
this._handleOnSubmitEditing(child.props.returnKeyType, item),
onChange: (value) => this._handleOnChange(child.props.componentID, value),
onChange: debounce(
(value) => this._handleOnChange(child.props.componentID, value),
500,
),
returnKeyType: isFormValid ? 'done' : 'next',
isPreviewActive,
});

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

@ -69,6 +69,7 @@ export const UploadsGalleryModal = forwardRef(({
}, [uploadedImage])
//save image to user gallery
const _addUploadedImageToGallery = async () => {
try {
@ -138,6 +139,8 @@ export const UploadsGalleryModal = forwardRef(({
setShowModal(false);
}
//renders footer with add snipept button and shows new snippet modal
const _renderFloatingPanel = () => {
@ -242,7 +245,7 @@ export const UploadsGalleryModal = forwardRef(({
};
const _renderHeaderContent = (
const _renderHeaderContent = () => (
<>
{isUploading && <ProgressBar progress={uploadProgress} />}
</>
@ -251,30 +254,33 @@ export const UploadsGalleryModal = forwardRef(({
const _renderContent = (
<View style={styles.container}>
<View style={styles.bodyWrapper}>
{_renderHeaderContent}
<FlatList
data={mediaUploads}
keyExtractor={(item) => `item_${item.url}`}
renderItem={_renderItem}
ListEmptyComponent={_renderEmptyContent}
ListFooterComponent={<View style={styles.listEmptyFooter} />}
extraData={indices}
numColumns={2}
refreshControl={
<RefreshControl
refreshing={isLoading}
onRefresh={_getMediaUploads}
/>
}
/>
const _renderContent = () => {
console.log("Rendering uploaded images")
return (
<View style={styles.container}>
<View style={styles.bodyWrapper}>
{_renderHeaderContent()}
<FlatList
data={mediaUploads}
keyExtractor={(item) => `item_${item.url}`}
renderItem={_renderItem}
ListEmptyComponent={_renderEmptyContent}
ListFooterComponent={<View style={styles.listEmptyFooter} />}
extraData={indices}
numColumns={2}
refreshControl={
<RefreshControl
refreshing={isLoading}
onRefresh={_getMediaUploads}
/>
}
/>
</View>
{_renderFloatingPanel()}
</View>
{_renderFloatingPanel()}
</View>
)
)
}
return (
@ -290,7 +296,7 @@ export const UploadsGalleryModal = forwardRef(({
animationType="slide"
style={styles.modalStyle}
>
{_renderContent}
{showModal && _renderContent()}
</Modal>
);

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

@ -114,7 +114,7 @@ const PostOptionsModal = forwardRef(({
handleThumbSelection(index)
}
const _renderContent = (
const _renderContent = () => (
<View style={styles.fillSpace}>
<KeyboardAwareScrollView style={styles.fillSpace} >
<View style={styles.container}>
@ -214,7 +214,7 @@ const PostOptionsModal = forwardRef(({
animationType="slide"
style={styles.modalStyle}
>
{_renderContent}
{_renderContent()}
</Modal>
);

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

@ -39,7 +39,6 @@ class EditorScreen extends Component {
* ------------------------------------------------
* @prop { type } name - Description....
*/
thumbSelectionModalRef = null;
postOptionsModalRef = null;
constructor(props) {
@ -206,12 +205,6 @@ class EditorScreen extends Component {
}
};
_showThumbSelectionModal = () => {
const { fields } = this.state;
if (this.thumbSelectionModalRef) {
this.thumbSelectionModalRef.show(fields.body);
}
};
_handleScheduleChange = (datetime:string|null) => {
this.setState({
@ -367,7 +360,6 @@ class EditorScreen extends Component {
isLoggedIn,
isPostSending,
isReply,
quickReplyText,
isUploading,
post,
uploadedImage,
@ -406,8 +398,8 @@ class EditorScreen extends Component {
</Modal>
);
};
console.log('fields :', fields);
console.log('quickReplyText : ', quickReplyText);
return (
<View style={globalStyles.defaultContainer}>
@ -430,11 +422,11 @@ console.log('quickReplyText : ', quickReplyText);
isReply={isReply}
quickTitle={wordsCount > 0 && `${wordsCount} words`}
rightButtonText={rightButtonText}
showThumbSelectionModal={this._showThumbSelectionModal}
handleSettingsPress={this._handleSettingsPress}
/>
<PostForm
handleFormUpdate={this._handleFormUpdate}
handleBodyChange={this._setWordsCount}
handleOnSubmit={this._handleOnSubmit}
isFormValid={isFormValid}
isPreviewActive={isPreviewActive}
@ -451,10 +443,7 @@ console.log('quickReplyText : ', quickReplyText);
)}
<MarkdownEditor
componentID="body"
draftBody={isReply ? quickReplyText : fields && fields.body}
handleOnTextChange={this._setWordsCount}
handleFormUpdate={this._handleFormUpdate}
handleIsFormValid={this._handleIsFormValid}
draftBody={fields && fields.body}
isFormValid={isFormValid}
handleOpenImagePicker={handleOnImagePicker}
intl={intl}
@ -476,12 +465,9 @@ console.log('quickReplyText : ', quickReplyText);
uploadProgress={uploadProgress}
/>
</PostForm>
{_renderCommunityModal()}
<ThumbSelectionModal
ref={(componentRef) => (this.thumbSelectionModalRef = componentRef)}
thumbIndex={thumbIndex}
onThumbSelection={this._handleOnThumbSelection}
/>
<PostOptionsModal
ref={(componentRef) => (this.postOptionsModalRef = componentRef)}
body={fields.body}

View File

@ -45,25 +45,40 @@ export const generatePermlink = (title, random = false) => {
};
export const extractWordAtIndex = (text:string, index:number) => {
const RANGE = 50;
const _start = index - RANGE;
const _end = index + RANGE;
const _length = text.length;
const textChunk = text.substring(_start > 0 ? _start : 0, _end < _length ? _end : _length);
const indexChunk = index < 50 ? index : (
_length - index < 50 ? textChunk.length - (_length - index) :
RANGE
);
console.log('char at index: ', textChunk[indexChunk]);
const END_REGEX = /[\s,]/
let word = '';
for(let i = index; i >= 0 && (!END_REGEX.test(text[i]) || i === index); i--){
if(text[i]){
word += text[i];
for(let i = indexChunk; i >= 0 && (!END_REGEX.test(textChunk[i]) || i === indexChunk); i--){
if(textChunk[i]){
word += textChunk[i];
}
}
word = word.split('').reverse().join('');
if(!END_REGEX.test(text[index])){
for(let i = index + 1; i < text.length && !END_REGEX.test(text[i]); i++){
if(text[i]){
word += text[i];
if(!END_REGEX.test(textChunk[indexChunk])){
for(let i = indexChunk + 1; i < textChunk.length && !END_REGEX.test(textChunk[i]); i++){
if(textChunk[i]){
word += textChunk[i];
}
}
}
return word;
}
export const generateReplyPermlink = (toAuthor) => {