Merge pull request #2138 from ecency/sa/quick-reply-on-post

[WIP] Quick Reply Modal
This commit is contained in:
Feruz M 2022-01-18 22:14:46 +02:00 committed by GitHub
commit c6f10ccbdf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 433 additions and 19 deletions

View File

@ -78,7 +78,7 @@
"react": "16.13.1",
"react-intl": "^3.9.2",
"react-native": "0.63.4",
"react-native-actions-sheet": "^0.4.2",
"react-native-actions-sheet": "^0.5.8",
"react-native-actionsheet": "ecency/react-native-actionsheet",
"react-native-animatable": "^1.3.3",
"react-native-autoheight-webview": "^1.5.8",
@ -105,6 +105,7 @@
"react-native-modal-translucent": "^5.0.0",
"react-native-navigation-bar-color": "^1.0.0",
"react-native-os": "^1.0.1",
"react-native-portalize": "^1.0.7",
"react-native-progress": "^5.0.0",
"react-native-push-notification": "^7.3.1",
"react-native-qrcode-svg": "^6.0.3",

View File

@ -27,7 +27,7 @@ export default class SummaryAreaView extends PureComponent {
return (
<View style={globalStyles.containerHorizontal16}>
<Text style={styles.summaryText}>{summary}</Text>
<Text style={[styles.summaryText, this.props.style]}>{summary}</Text>
</View>
);
}

View File

@ -92,6 +92,7 @@ import { CustomiseFiltersModal } from './customiseFiltersModal';
import { ForegroundNotification } from './foregroundNotification';
import { PostHtmlRenderer } from './postHtmlRenderer';
import { QuickProfileModal } from './organisms';
import QuickReplyModal from './quickReplyModal/quickReplyModalView';
// Basic UI Elements
import {
@ -232,4 +233,5 @@ export {
ForegroundNotification,
PostHtmlRenderer,
QuickProfileModal,
QuickReplyModal,
};

View File

@ -29,6 +29,7 @@ const PostCardContainer = ({
imageHeight,
setImageHeight,
pageType,
showQuickReplyModal,
}) => {
const dispatch = useAppDispatch();
@ -124,6 +125,9 @@ const PostCardContainer = ({
setIsMuted(false);
};
const _handleQuickReplyModal = () => {
showQuickReplyModal(content);
};
return (
<PostCardView
handleOnUserPress={_handleOnUserPress}
@ -140,6 +144,7 @@ const PostCardContainer = ({
setImageHeight={setImageHeight}
isMuted={isMuted}
fetchPost={_fetchPost}
showQuickReplyModal={_handleQuickReplyModal}
/>
);
};

View File

@ -32,6 +32,7 @@ const PostCardView = ({
handleOnVotersPress,
handleOnReblogsPress,
handleOnUnmutePress,
showQuickReplyModal,
content,
reblogs,
isHideImage,
@ -56,6 +57,7 @@ const PostCardView = ({
};
const _handleOnContentPress = () => {
console.log('content : ', content);
handleOnContentPress(content);
};
@ -198,6 +200,7 @@ const PostCardView = ({
iconType="MaterialCommunityIcons"
isClickable
text={get(content, 'children', 0)}
onPress={showQuickReplyModal}
/>
</View>
</View>

View File

@ -17,6 +17,7 @@ interface postsListContainerProps extends FlatListProps<any> {
isLoading:boolean;
isRefreshing:boolean;
pageType:'main'|'profile'|'ownProfile'|'community';
showQuickReplyModal:(post:any)=>void;
}
let _onEndReachedCalledDuringMomentum = true;
@ -28,6 +29,7 @@ const postsListContainer = ({
isRefreshing,
isLoading,
pageType,
showQuickReplyModal,
...props
}:postsListContainerProps, ref) => {
@ -124,6 +126,7 @@ const postsListContainer = ({
imageHeight={imgHeight}
setImageHeight = {_setImageHeightInMap}
pageType={pageType}
showQuickReplyModal={showQuickReplyModal}
/>,
);
}
@ -142,6 +145,7 @@ const postsListContainer = ({
imageHeight={imgHeight}
setImageHeight = {_setImageHeightInMap}
pageType={pageType}
showQuickReplyModal={showQuickReplyModal}
/>,
);
}

View File

@ -0,0 +1,86 @@
import EStyleSheet from 'react-native-extended-stylesheet';
export default EStyleSheet.create({
sheetContent: {
backgroundColor: '$primaryBackgroundColor',
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
zIndex: 999,
},
container:{
flex:1,
},
modalContainer: {
// paddingVertical: 4,
},
cancelButton: {
marginRight: 10,
},
modalHeader: {
paddingHorizontal: 12,
paddingVertical:8,
},
titleBtnTxt: {
fontSize: 18,
fontWeight: 'bold',
color: '$primaryBlack',
},
summaryStyle:{
fontSize:12,
},
inputContainer: {
paddingHorizontal: 16,
paddingVertical:8,
height:120,
},
textInput: {
color: '$primaryBlack',
fontSize: 16,
flexGrow: 1,
fontWeight: '500',
},
footer: {
flexDirection: 'row',
justifyContent: 'flex-end',
alignItems: 'center',
paddingHorizontal:16,
},
commentBtn: {
width: 100,
height: 40,
alignItems: 'center',
justifyContent: 'center',
},
replySection: {
paddingTop: 10,
paddingBottom: 0,
},
accountTile: {
height: 60,
flexDirection: 'row',
paddingHorizontal: 16,
alignItems: 'center',
justifyContent: 'space-between',
},
avatarAndNameContainer: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal:16,
paddingTop:12,
paddingBottom:8,
},
nameContainer: {
marginLeft: 2,
},
name: {
marginLeft: 4,
color: '$primaryDarkGray',
},
});

View File

@ -0,0 +1,242 @@
import React, { useEffect, useImperativeHandle, useRef, useState } from 'react';
import ActionSheet from 'react-native-actions-sheet';
import EStyleSheet from 'react-native-extended-stylesheet';
import styles from './quickReplyModalStyles';
import { forwardRef } from 'react';
import { View, Text, Alert, TouchableOpacity, Keyboard } from 'react-native';
import { useIntl } from 'react-intl';
import { IconButton, MainButton, SummaryArea, TextButton, TextInput, UserAvatar } from '..';
import { useSelector, useDispatch } from 'react-redux';
import { generateReplyPermlink } from '../../utils/editor';
import { postComment } from '../../providers/hive/dhive';
import { toastNotification } from '../../redux/actions/uiAction';
import { useAppSelector } from '../../hooks';
import { default as ROUTES } from '../../constants/routeNames';
import get from 'lodash/get';
import { navigate } from '../../navigation/service';
import { Portal } from 'react-native-portalize';
export interface QuickReplyModalProps {}
const QuickReplyModal = ({}: QuickReplyModalProps, ref) => {
const intl = useIntl();
const dispatch = useDispatch();
const currentAccount = useSelector((state) => state.account.currentAccount);
const pinCode = useSelector((state) => state.application.pin);
const [selectedPost, setSelectedPost] = useState(null);
const [commentValue, setCommentValue] = useState('');
const [isSending, setIsSending] = useState(false);
const [isKeyboardVisible, setKeyboardVisible] = useState(false);
const sheetModalRef = useRef<ActionSheet>();
const inputRef = useRef<TextInput>(null);
// keyboard listener hook for checking if keyboard is active/inactove
useEffect(() => {
const keyboardDidShowListener = Keyboard.addListener('keyboardDidShow', () => {
setKeyboardVisible(true); // or some other action
});
const keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', () => {
setKeyboardVisible(false); // or some other action
});
return () => {
keyboardDidHideListener.remove();
keyboardDidShowListener.remove();
};
}, []);
// reset the state when post changes
useEffect(() => {
setCommentValue('');
}, [selectedPost]);
//CALLBACK_METHODS
useImperativeHandle(ref, () => ({
show: (post: any) => {
setSelectedPost(post);
sheetModalRef.current?.setModalVisible(true);
// wait for modal to open and then show the keyboard
setTimeout(() => {
inputRef.current?.focus();
}, 1000);
},
}));
// handlers
// handle close press
const _handleClosePress = () => {
sheetModalRef.current?.setModalVisible(false);
setKeyboardVisible(false);
};
// navigate to post on summary press
const _handleOnSummaryPress = () => {
Keyboard.dismiss();
sheetModalRef.current?.setModalVisible(false);
navigate({
routeName: ROUTES.SCREENS.POST,
params: {
content: selectedPost,
},
key: get(selectedPost, 'permlink'),
});
};
// handle submit reply
const _submitReply = async () => {
let stateTimer;
if (!commentValue) {
return;
}
if (isSending) {
return;
}
if (currentAccount) {
setIsSending(true);
const permlink = generateReplyPermlink(selectedPost.author);
const parentAuthor = selectedPost.author;
const parentPermlink = selectedPost.permlink;
const parentTags = selectedPost.json_metadata.tags;
console.log(
currentAccount,
pinCode,
parentAuthor,
parentPermlink,
permlink,
commentValue,
parentTags,
);
const status = await postComment(
currentAccount,
pinCode,
parentAuthor,
parentPermlink,
permlink,
commentValue,
parentTags,
)
.then(() => {
stateTimer = setTimeout(() => {
setIsSending(false);
sheetModalRef.current?.setModalVisible(false);
setCommentValue('');
dispatch(
toastNotification(
intl.formatMessage({
id: 'alert.success',
}),
),
);
clearTimeout(stateTimer);
}, 3000);
})
.catch((error) => {
console.log(error);
Alert.alert(
intl.formatMessage({
id: 'alert.fail',
}),
error.message || JSON.stringify(error),
);
stateTimer = setTimeout(() => {
setIsSending(false);
clearTimeout(stateTimer);
}, 500);
});
console.log('status : ', status);
}
};
//VIEW_RENDERERS
const _renderSheetHeader = () => (
<View style={styles.modalHeader}>
<IconButton
name="close"
iconType="MaterialCommunityIcons"
size={28}
color={EStyleSheet.value('$primaryBlack')}
iconStyle={{}}
onPress={() => _handleClosePress()}
/>
</View>
);
const _renderSummary = () => (
<TouchableOpacity onPress={() => _handleOnSummaryPress()}>
<SummaryArea style={styles.summaryStyle} summary={selectedPost.summary} />
</TouchableOpacity>
);
const _renderAvatar = () => (
<View style={styles.avatarAndNameContainer}>
<UserAvatar noAction username={currentAccount.username} />
<View style={styles.nameContainer}>
<Text style={styles.name}>{`@${currentAccount.username}`}</Text>
</View>
</View>
);
const _renderContent = () => {
return (
<View style={styles.modalContainer}>
{_renderSummary()}
{_renderAvatar()}
<View style={styles.inputContainer}>
<TextInput
innerRef={inputRef}
onChangeText={setCommentValue}
value={commentValue}
// autoFocus
placeholder={intl.formatMessage({
id: 'quick_reply.placeholder',
})}
placeholderTextColor="#c1c5c7"
autoCapitalize="none"
style={styles.textInput}
multiline={true}
numberOfLines={5}
textAlignVertical="top"
/>
</View>
<View style={styles.footer}>
<TextButton
style={styles.cancelButton}
onPress={_handleClosePress}
text={intl.formatMessage({
id: 'quick_reply.close',
})}
/>
<MainButton
style={styles.commentBtn}
onPress={() => _submitReply()}
text={intl.formatMessage({
id: 'quick_reply.reply',
})}
isLoading={isSending}
/>
</View>
</View>
);
};
return (
<Portal>
<ActionSheet
ref={sheetModalRef}
gestureEnabled={true}
keyboardShouldPersistTaps="handled"
containerStyle={styles.sheetContent}
keyboardHandlerEnabled
indicatorColor={EStyleSheet.value('$primaryWhiteLightBackground')}
>
{selectedPost && _renderContent()}
</ActionSheet>
</Portal>
);
};
export default forwardRef(QuickReplyModal as any);

View File

@ -10,6 +10,7 @@ import { AppState, NativeScrollEvent, NativeSyntheticEvent } from 'react-native'
import { PostsListRef } from '../../postsList/container/postsListContainer';
import ScrollTopPopup from './scrollTopPopup';
import { debounce } from 'lodash';
import { QuickReplyModal } from '../..';
const DEFAULT_TAB_META = {
startAuthor:'',
@ -62,6 +63,7 @@ const TabContent = ({
const appState = useRef(AppState.currentState);
const postsRef = useRef(posts);
const sessionUserRef = useRef(sessionUser);
const quickReplyModalRef = useRef(null)
//init state refs;
postsRef.current = posts;
@ -313,6 +315,16 @@ const TabContent = ({
}
};
// show quick reply modal
const _showQuickReplyModal = (post:any) => {
// console.log('post: ', post);
if (isLoggedIn) {
quickReplyModalRef.current.show(post);
} else {
console.log('Not LoggedIn');
}
}
return (
@ -334,6 +346,7 @@ const TabContent = ({
isLoading={tabMeta.isLoading}
ListEmptyComponent={_renderEmptyContent}
pageType={pageType}
showQuickReplyModal={_showQuickReplyModal}
/>
<ScrollTopPopup
popupAvatars={latestPosts.map(post=>post.avatar || '')}
@ -344,6 +357,7 @@ const TabContent = ({
setEnableScrollTop(false);
}}
/>
<QuickReplyModal ref={quickReplyModalRef} />
</>
);
};

View File

@ -707,5 +707,11 @@
"day":"days",
"month":"months",
"year":"years"
},
"quick_reply":{
"placeholder":"Add a comment",
"comment": "Comment",
"reply": "REPLY",
"close":"CLOSE"
}
}

View File

@ -3,8 +3,8 @@ import 'react-native-gesture-handler';
import { Provider, connect } from 'react-redux';
import { PersistGate } from 'redux-persist/integration/react';
import { IntlProvider } from 'react-intl';
import firebase from '@react-native-firebase/app';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { Host } from 'react-native-portalize';
import { flattenMessages } from './utils/flattenMessages';
import messages from './config/locales';
@ -15,7 +15,9 @@ const _renderApp = ({ locale }) => (
<PersistGate loading={null} persistor={persistor}>
<IntlProvider locale={locale} messages={flattenMessages(messages[locale])}>
<SafeAreaProvider>
<Host>
<Application />
</Host>
</SafeAreaProvider>
</IntlProvider>
</PersistGate>

View File

@ -37,6 +37,7 @@ import { SERVER_LIST } from '../../constants/options/api';
import { b64uEnc } from '../../utils/b64';
import bugsnagInstance from '../../config/bugsnag';
import bugsnapInstance from '../../config/bugsnag';
import { makeJsonMetadataReply } from '../../utils/editor';
global.Buffer = global.Buffer || require('buffer').Buffer;
@ -1292,6 +1293,54 @@ export const postContent = (
throw err;
});
/**
* Broadcasts a comment to post
* @param account currentAccount object
* @param pin encrypted pin taken from redux
* @param {*} parentAuthor author of parent post or in case of reply to comment author of parent comment
* @param {*} parentPermlink permlink of parent post or in case of reply to comment author of parent comment
* @param {*} permlink perlink of comment to be make
* @param {*} body body of comment
* @param {*} parentTags tags of parent post or parent comment
* @param {*} isEdit optional to avoid tracking activity in case of comment editing
* @returns
*/
export const postComment = (
account,
pin,
parentAuthor,
parentPermlink,
permlink,
body,
parentTags,
isEdit = false,
) =>
_postContent(
account,
pin,
parentAuthor,
parentPermlink,
permlink,
'',
body,
makeJsonMetadataReply(parentTags || 'ecency'),
null,
null,
)
.then((resp) => {
const t = 110;
const { id } = resp;
if (!isEdit) {
userActivity(t, id);
}
return resp;
})
.catch((err) => {
console.warn('Failed to post conent', err);
bugsnagInstance.notify(err);
throw err;
});
/**
* @method postComment post a comment/reply
* @param comment comment object { author, permlink, ... }

View File

@ -23,6 +23,7 @@ import {
grantPostingPermission,
signImage,
reblog,
postComment,
} from '../../../providers/hive/dhive';
import { setDraftPost, getDraftPost } from '../../../realm/realm';
@ -737,7 +738,7 @@ class EditorContainer extends Component {
_submitReply = async (fields) => {
const { currentAccount, pinCode } = this.props;
const { rewardType, isPostSending } = this.state;
const { isPostSending } = this.state;
if (isPostSending) {
return;
@ -749,27 +750,21 @@ class EditorContainer extends Component {
});
const { post } = this.state;
const jsonMeta = makeJsonMetadataReply(post.json_metadata.tags || ['ecency']);
const permlink = generateReplyPermlink(post.author);
const author = currentAccount.name;
const options = null;
const parentAuthor = post.author;
const parentPermlink = post.permlink;
const voteWeight = null;
const parentTags = post.json_metadata.tags;
await postContent(
await postComment(
currentAccount,
pinCode,
parentAuthor,
parentPermlink,
permlink,
'',
fields.body,
jsonMeta,
options,
voteWeight,
parentTags,
)
.then(() => {
AsyncStorage.setItem('temp-reply', '');

View File

@ -8441,10 +8441,10 @@ react-lifecycles-compat@^3.0.4:
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
react-native-actions-sheet@^0.4.2:
version "0.4.9"
resolved "https://registry.yarnpkg.com/react-native-actions-sheet/-/react-native-actions-sheet-0.4.9.tgz#2446f97eca4cc3674128c445665b1f8774a5d64a"
integrity sha512-4FuybHH+psq738w/6OIfdUEL4/5pG43yH+C0YeX22jQo0tYppqxVqQr3ITiXHF2PDJtdIiV7egy8Xncuuw5n9w==
react-native-actions-sheet@^0.5.8:
version "0.5.8"
resolved "https://registry.yarnpkg.com/react-native-actions-sheet/-/react-native-actions-sheet-0.5.8.tgz#9367a7a2cbadd7b0cff82eb5e72c27578c13222d"
integrity sha512-bucvVl2lU0k0ogq6dDmJTKDa28SOGM8sMdr14qflnbqcSYEJH0YT+DWoJXzsocdNvXMBsdqM5J8bX+k6weIKFg==
react-native-actionsheet@ecency/react-native-actionsheet:
version "2.4.2"
@ -8637,6 +8637,11 @@ react-native-os@^1.0.1:
resolved "https://registry.yarnpkg.com/react-native-os/-/react-native-os-1.2.6.tgz#1bb16d78ccad1143972183a04f443cf1af9fbefa"
integrity sha512-OlT+xQAcvkcnf7imgXiu+myMkqDt4xw2bP5SlVo19hEn5XHBkPMLX7dk3sSGxxncH/ToMDsf1KLyrPabNVtadA==
react-native-portalize@^1.0.7:
version "1.0.7"
resolved "https://registry.yarnpkg.com/react-native-portalize/-/react-native-portalize-1.0.7.tgz#8b3c742a06f863654d526ea1075a8596625f8482"
integrity sha512-icqopPh9ZSV+I8C5LlZN9pQJ0OMeBDNqHhP80+qDx0hOGEcsDC09wgjogbEMfJE0GcMDM7PDYtyQkqa9gIqd1g==
react-native-progress@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/react-native-progress/-/react-native-progress-5.0.0.tgz#f5ac6ceaeee27f184c660b00f29419e82a9d0ab0"