mirror of
https://github.com/ecency/ecency-mobile.git
synced 2024-12-02 11:15:35 +03:00
Merge pull request #2626 from ecency/nt/experiment-feed-stability
Nt/experiment feed stability
This commit is contained in:
commit
32ec5c0c92
@ -443,6 +443,8 @@ PODS:
|
||||
- React-jsinspector (0.70.6)
|
||||
- React-logger (0.70.6):
|
||||
- glog
|
||||
- react-native-background-timer (2.4.1):
|
||||
- React-Core
|
||||
- react-native-camera (4.2.1):
|
||||
- React-Core
|
||||
- react-native-camera/RCT (= 4.2.1)
|
||||
@ -593,7 +595,7 @@ PODS:
|
||||
- Firebase/Messaging (= 8.15.0)
|
||||
- React-Core
|
||||
- RNFBApp
|
||||
- RNGestureHandler (2.8.0):
|
||||
- RNGestureHandler (2.9.0):
|
||||
- React-Core
|
||||
- RNIap (12.4.2):
|
||||
- React-Core
|
||||
@ -720,6 +722,7 @@ DEPENDENCIES:
|
||||
- React-jsiexecutor (from `../node_modules/react-native/ReactCommon/jsiexecutor`)
|
||||
- React-jsinspector (from `../node_modules/react-native/ReactCommon/jsinspector`)
|
||||
- React-logger (from `../node_modules/react-native/ReactCommon/logger`)
|
||||
- react-native-background-timer (from `../node_modules/react-native-background-timer`)
|
||||
- react-native-camera (from `../node_modules/react-native-camera`)
|
||||
- "react-native-cameraroll (from `../node_modules/@react-native-community/cameraroll`)"
|
||||
- react-native-config (from `../node_modules/react-native-config`)
|
||||
@ -870,6 +873,8 @@ EXTERNAL SOURCES:
|
||||
:path: "../node_modules/react-native/ReactCommon/jsinspector"
|
||||
React-logger:
|
||||
:path: "../node_modules/react-native/ReactCommon/logger"
|
||||
react-native-background-timer:
|
||||
:path: "../node_modules/react-native-background-timer"
|
||||
react-native-camera:
|
||||
:path: "../node_modules/react-native-camera"
|
||||
react-native-cameraroll:
|
||||
@ -1037,6 +1042,7 @@ SPEC CHECKSUMS:
|
||||
React-jsiexecutor: b4a65947391c658450151275aa406f2b8263178f
|
||||
React-jsinspector: 60769e5a0a6d4b32294a2456077f59d0266f9a8b
|
||||
React-logger: 1623c216abaa88974afce404dc8f479406bbc3a0
|
||||
react-native-background-timer: 17ea5e06803401a379ebf1f20505b793ac44d0fe
|
||||
react-native-camera: 3eae183c1d111103963f3dd913b65d01aef8110f
|
||||
react-native-cameraroll: e2917a5e62da9f10c3d525e157e25e694d2d6dfa
|
||||
react-native-config: bcafda5b4c51491ee1b0e1d0c4e3905bc7b56c1b
|
||||
@ -1078,7 +1084,7 @@ SPEC CHECKSUMS:
|
||||
RNFBApp: e4439717c23252458da2b41b81b4b475c86f90c4
|
||||
RNFBDynamicLinks: 538840f6e3f58511f857d15df1bc25ed655dc283
|
||||
RNFBMessaging: 40dac204b4197a2661dec5be964780c6ec39bf65
|
||||
RNGestureHandler: 62232ba8f562f7dea5ba1b3383494eb5bf97a4d3
|
||||
RNGestureHandler: 071d7a9ad81e8b83fe7663b303d132406a7d8f39
|
||||
RNIap: e17233fe11083a71e0420682b0b09d497861faa1
|
||||
RNImageCropPicker: 14fe1c29298fb4018f3186f455c475ab107da332
|
||||
RNNotifee: dcb2593127f40945c4ee5fc09f61d71bbd00b9cf
|
||||
|
@ -100,6 +100,7 @@
|
||||
"react-native-actionsheet": "ecency/react-native-actionsheet",
|
||||
"react-native-animatable": "^1.3.3",
|
||||
"react-native-autoheight-webview": "^1.5.8",
|
||||
"react-native-background-timer": "^2.4.1",
|
||||
"react-native-bootsplash": "^4.3.2",
|
||||
"react-native-camera": "^4.2.1",
|
||||
"react-native-chart-kit": "^6.11.0",
|
||||
@ -113,7 +114,7 @@
|
||||
"react-native-fast-image": "^8.3.2",
|
||||
"react-native-fingerprint-scanner": "hieuvp/react-native-fingerprint-scanner",
|
||||
"react-native-flipper": "^0.164.0",
|
||||
"react-native-gesture-handler": "^2.8.0",
|
||||
"react-native-gesture-handler": "^2.9.0",
|
||||
"react-native-heic-converter": "^1.3.1",
|
||||
"react-native-highlight-words": "^1.0.1",
|
||||
"react-native-iap": "^12.4.2",
|
||||
|
@ -1,8 +1,7 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Image } from 'react-native';
|
||||
import { Image, TouchableOpacity } from 'react-native';
|
||||
import EStyleSheet from 'react-native-extended-stylesheet';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
import { TouchableOpacity } from 'react-native-gesture-handler';
|
||||
|
||||
interface AutoHeightImageProps {
|
||||
contentWidth: number;
|
||||
@ -39,7 +38,7 @@ export const AutoHeightImage = ({
|
||||
const imgStyle = {
|
||||
width: imgWidth - 10,
|
||||
height: imgHeight,
|
||||
backgroundColor: onLoadCalled ? 'transparent' : EStyleSheet.value('$primaryGray'),
|
||||
backgroundColor: onLoadCalled ? 'transparent' : EStyleSheet.value('$primaryLightBackground'),
|
||||
};
|
||||
|
||||
const _onLoad = () => {
|
||||
|
@ -1,7 +1,6 @@
|
||||
import React, { Fragment, useState, useRef, useEffect, useMemo } from 'react';
|
||||
import React, { Fragment, useState, useMemo } from 'react';
|
||||
import { ActivityIndicator, View } from 'react-native';
|
||||
import { useIntl } from 'react-intl';
|
||||
import get from 'lodash/get';
|
||||
|
||||
import { useDispatch } from 'react-redux';
|
||||
import EStyleSheet from 'react-native-extended-stylesheet';
|
||||
@ -11,41 +10,38 @@ import { delay } from '../../../utils/editor';
|
||||
|
||||
// Components
|
||||
import { CommentBody, PostHeaderDescription } from '../../postElements';
|
||||
import { Upvote } from '../../upvote';
|
||||
import { IconButton } from '../../iconButton';
|
||||
import { TextWithIcon } from '../../basicUIElements';
|
||||
|
||||
// Styles
|
||||
import styles from './commentStyles';
|
||||
import { useAppSelector } from '../../../hooks';
|
||||
import { OptionsModal } from '../../atoms';
|
||||
import { showReplyModal } from '../../../redux/actions/uiAction';
|
||||
import postTypes from '../../../constants/postTypes';
|
||||
import { PostTypes } from '../../../constants/postTypes';
|
||||
import { UpvoteButton } from '../../postCard/children/upvoteButton';
|
||||
|
||||
const CommentView = ({
|
||||
avatarSize,
|
||||
comment,
|
||||
currentAccountUsername,
|
||||
commentNumber,
|
||||
fetchPost,
|
||||
handleDeleteComment,
|
||||
handleOnEditPress,
|
||||
handleOnLongPress,
|
||||
handleOnUserPress,
|
||||
handleOnVotersPress,
|
||||
isShowComments,
|
||||
handleLinkPress,
|
||||
handleImagePress,
|
||||
handleYoutubePress,
|
||||
handleVideoPress,
|
||||
mainAuthor = { mainAuthor },
|
||||
isShowSubComments,
|
||||
hideManyCommentsButton,
|
||||
openReplyThread,
|
||||
fetchedAt,
|
||||
repliesToggle,
|
||||
incrementRepliesCount,
|
||||
handleOnToggleReplies,
|
||||
onUpvotePress,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
const actionSheet = useRef(null);
|
||||
|
||||
const currentAccount = useAppSelector((state) => state.account.currentAccount);
|
||||
const isLoggedIn = useAppSelector((state) => state.application.isLoggedIn);
|
||||
@ -56,37 +52,25 @@ const CommentView = ({
|
||||
[currentAccount],
|
||||
);
|
||||
|
||||
const [activeVotes, setActiveVotes] = useState([]);
|
||||
const activeVotes = comment?.active_votes || [];
|
||||
const [isOpeningReplies, setIsOpeningReplies] = useState(false);
|
||||
const [cacheVoteIcrement, setCacheVoteIcrement] = useState(0);
|
||||
|
||||
const childCount = comment.children;
|
||||
const { replies } = comment;
|
||||
const _depth = commentNumber || comment.level;
|
||||
const _currentUsername = currentAccountUsername || currentAccount?.username;
|
||||
|
||||
useEffect(() => {
|
||||
if (comment) {
|
||||
setActiveVotes(get(comment, 'active_votes', []));
|
||||
}
|
||||
}, [comment]);
|
||||
|
||||
const _showSubCommentsToggle = async (force = false) => {
|
||||
if ((replies && replies.length > 0) || force) {
|
||||
// setIsOpeningReplies(true);
|
||||
// await delay(10); //hack to rendering inidcator first before start loading comments
|
||||
setIsOpeningReplies(true);
|
||||
await delay(10); // hack to rendering inidcator first before start loading comments
|
||||
handleOnToggleReplies(comment.commentKey);
|
||||
// setIsOpeningReplies(false);
|
||||
setIsOpeningReplies(false);
|
||||
} else if (openReplyThread) {
|
||||
openReplyThread(comment);
|
||||
}
|
||||
};
|
||||
|
||||
const _handleCacheVoteIncrement = () => {
|
||||
// fake increment vote using based on local change
|
||||
setCacheVoteIcrement(1);
|
||||
};
|
||||
|
||||
const _handleOnReplyPress = () => {
|
||||
if (isLoggedIn) {
|
||||
dispatch(showReplyModal(comment));
|
||||
@ -116,8 +100,11 @@ const CommentView = ({
|
||||
reputation={comment.author_reputation}
|
||||
handleOnUserPress={handleOnUserPress}
|
||||
handleOnLongPress={() => handleOnLongPress(comment)}
|
||||
handleLinkPress={handleLinkPress}
|
||||
handleImagePress={handleImagePress}
|
||||
handleVideoPress={handleVideoPress}
|
||||
handleYoutubePress={handleYoutubePress}
|
||||
body={comment.body}
|
||||
created={comment.created}
|
||||
key={`key-${comment.permlink}`}
|
||||
isMuted={isMuted}
|
||||
/>
|
||||
@ -133,12 +120,17 @@ const CommentView = ({
|
||||
const _renderActionPanel = () => {
|
||||
return (
|
||||
<>
|
||||
<Upvote
|
||||
activeVotes={activeVotes}
|
||||
isShowPayoutValue
|
||||
<UpvoteButton
|
||||
content={comment}
|
||||
handleCacheVoteIncrement={_handleCacheVoteIncrement}
|
||||
parentType={postTypes.COMMENT}
|
||||
activeVotes={activeVotes}
|
||||
isShowPayoutValue={true}
|
||||
parentType={PostTypes.COMMENT}
|
||||
onUpvotePress={(anchorRect, onVotingStart) => {
|
||||
onUpvotePress({ content: comment, anchorRect, onVotingStart });
|
||||
}}
|
||||
onPayoutDetailsPress={(anchorRect) => {
|
||||
onUpvotePress({ content: comment, anchorRect, showPayoutDetails: true });
|
||||
}}
|
||||
/>
|
||||
<TextWithIcon
|
||||
iconName="heart-outline"
|
||||
@ -151,7 +143,7 @@ const CommentView = ({
|
||||
activeVotes.length > 0 &&
|
||||
handleOnVotersPress(activeVotes, comment)
|
||||
}
|
||||
text={activeVotes.length + cacheVoteIcrement}
|
||||
text={activeVotes.length}
|
||||
textStyle={styles.voteCountText}
|
||||
/>
|
||||
|
||||
@ -177,29 +169,14 @@ const CommentView = ({
|
||||
iconType="MaterialIcons"
|
||||
/>
|
||||
{!childCount && !activeVotes.length && comment.isDeletable && (
|
||||
<Fragment>
|
||||
<IconButton
|
||||
size={20}
|
||||
iconStyle={styles.leftIcon}
|
||||
style={styles.leftButton}
|
||||
name="delete-forever"
|
||||
onPress={() => actionSheet.current.show()}
|
||||
iconType="MaterialIcons"
|
||||
/>
|
||||
<OptionsModal
|
||||
ref={actionSheet}
|
||||
options={[
|
||||
intl.formatMessage({ id: 'alert.delete' }),
|
||||
intl.formatMessage({ id: 'alert.cancel' }),
|
||||
]}
|
||||
title={intl.formatMessage({ id: 'alert.delete' })}
|
||||
destructiveButtonIndex={0}
|
||||
cancelButtonIndex={1}
|
||||
onPress={(index) => {
|
||||
index === 0 ? handleDeleteComment(comment.permlink) : null;
|
||||
}}
|
||||
/>
|
||||
</Fragment>
|
||||
<IconButton
|
||||
size={20}
|
||||
iconStyle={styles.leftIcon}
|
||||
style={styles.leftButton}
|
||||
name="delete-forever"
|
||||
onPress={() => handleDeleteComment(comment.permlink)}
|
||||
iconType="MaterialIcons"
|
||||
/>
|
||||
)}
|
||||
</Fragment>
|
||||
)}
|
||||
@ -255,6 +232,7 @@ const CommentView = ({
|
||||
customStyle={{ alignItems: 'flex-start', paddingLeft: 12 }}
|
||||
showDotMenuButton={true}
|
||||
handleOnDotPress={() => handleOnLongPress(comment)}
|
||||
profileOnPress={handleOnUserPress}
|
||||
secondaryContentComponent={_renderComment()}
|
||||
/>
|
||||
</View>
|
||||
|
@ -18,9 +18,8 @@ import ROUTES from '../../../constants/routeNames';
|
||||
|
||||
// Component
|
||||
import CommentsView from '../view/commentsView';
|
||||
import { useAppSelector } from '../../../hooks';
|
||||
import { updateCommentCache } from '../../../redux/actions/cacheActions';
|
||||
import { CommentCacheStatus } from '../../../redux/reducers/cacheReducer';
|
||||
import { CacheStatus } from '../../../redux/reducers/cacheReducer';
|
||||
import { postQueries } from '../../../providers/queries';
|
||||
|
||||
const CommentsContainer = ({
|
||||
@ -56,9 +55,6 @@ const CommentsContainer = ({
|
||||
const navigation = useNavigation();
|
||||
const postsCachePrimer = postQueries.usePostsCachePrimer();
|
||||
|
||||
const lastCacheUpdate = useAppSelector((state) => state.cache.lastUpdate);
|
||||
// const cachedComments = useAppSelector((state) => state.cache.comments);
|
||||
|
||||
const [lcomments, setLComments] = useState([]);
|
||||
const [propComments, setPropComments] = useState(comments);
|
||||
|
||||
@ -73,27 +69,10 @@ const CommentsContainer = ({
|
||||
}, [commentCount, selectedFilter]);
|
||||
|
||||
useEffect(() => {
|
||||
let _comments = comments;
|
||||
if (_comments) {
|
||||
_comments = _handleCachedComment(comments);
|
||||
}
|
||||
const _comments = comments;
|
||||
setPropComments(_comments);
|
||||
}, [comments]);
|
||||
|
||||
useEffect(() => {
|
||||
const postPath = `${author || ''}/${permlink || ''}`;
|
||||
// this conditional makes sure on targetted already fetched post is updated
|
||||
// with new cache status, this is to avoid duplicate cache merging
|
||||
if (
|
||||
lastCacheUpdate &&
|
||||
lastCacheUpdate.postPath === postPath &&
|
||||
lastCacheUpdate.type === 'comment' &&
|
||||
lastCacheUpdate.updatedAt > fetchedAt
|
||||
) {
|
||||
_handleCachedComment();
|
||||
}
|
||||
}, [lastCacheUpdate]);
|
||||
|
||||
// Component Functions
|
||||
|
||||
const _sortComments = (sortOrder = 'trending', _comments) => {
|
||||
@ -184,7 +163,6 @@ const CommentsContainer = ({
|
||||
await getComments(author, permlink, name)
|
||||
.then((__comments) => {
|
||||
// favourable place for merging comment cache
|
||||
__comments = _handleCachedComment(__comments);
|
||||
__comments = _sortComments(selectedFilter, __comments);
|
||||
|
||||
setLComments(__comments);
|
||||
@ -193,67 +171,9 @@ const CommentsContainer = ({
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
} else {
|
||||
// _handleCachedComment();
|
||||
}
|
||||
};
|
||||
|
||||
// const _handleCachedComment = (passedComments = null) => {
|
||||
// const _comments = passedComments || propComments || lcomments || [];
|
||||
// const postPath = `${author || ''}/${permlink || ''}`;
|
||||
|
||||
// if (cachedComments.has(postPath)) {
|
||||
// const cachedComment = cachedComments.get(postPath);
|
||||
|
||||
// let ignoreCache = false;
|
||||
// let replaceAtIndex = -1;
|
||||
// let removeAtIndex = -1;
|
||||
// _comments.forEach((comment, index) => {
|
||||
// if (cachedComment.permlink === comment.permlink) {
|
||||
// if (cachedComment.updated < comment.updated) {
|
||||
// // comment is present with latest data
|
||||
// ignoreCache = true;
|
||||
// console.log('Ignore cache as comment is now present');
|
||||
// } else if (cachedComment.status === CommentCacheStatus.DELETED) {
|
||||
// removeAtIndex = index;
|
||||
// } else {
|
||||
// // comment is present in list but data is old
|
||||
// replaceAtIndex = index;
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
|
||||
// // means deleted comment is not being retuend in fresh data, cache needs to be ignored
|
||||
// if (cachedComment.status === CommentCacheStatus.DELETED && removeAtIndex < 0) {
|
||||
// ignoreCache = true;
|
||||
// }
|
||||
|
||||
// // manipulate comments with cached data
|
||||
// if (!ignoreCache) {
|
||||
// let newComments = [];
|
||||
// if (removeAtIndex >= 0) {
|
||||
// newComments = _comments;
|
||||
// newComments.splice(removeAtIndex, 1);
|
||||
// } else if (replaceAtIndex >= 0) {
|
||||
// _comments[replaceAtIndex] = cachedComment;
|
||||
// newComments = [..._comments];
|
||||
// } else {
|
||||
// newComments = [..._comments, cachedComment];
|
||||
// }
|
||||
|
||||
// console.log('updated comments with cached comment');
|
||||
// if (passedComments) {
|
||||
// return newComments;
|
||||
// } else if (propComments) {
|
||||
// setPropComments(newComments);
|
||||
// } else {
|
||||
// setLComments(newComments);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// return _comments;
|
||||
// };
|
||||
|
||||
const _handleOnReplyPress = (item) => {
|
||||
navigation.navigate({
|
||||
name: ROUTES.SCREENS.EDITOR,
|
||||
@ -315,7 +235,7 @@ const CommentsContainer = ({
|
||||
// remove cached entry based on parent
|
||||
if (deletedItem) {
|
||||
const cachePath = `${deletedItem.author}/${deletedItem.permlink}`;
|
||||
deletedItem.status = CommentCacheStatus.DELETED;
|
||||
deletedItem.status = CacheStatus.DELETED;
|
||||
delete deletedItem.updated;
|
||||
dispatch(updateCommentCache(cachePath, deletedItem, { isUpdate: true }));
|
||||
}
|
||||
@ -331,6 +251,7 @@ const CommentsContainer = ({
|
||||
author: comment.author,
|
||||
permlink: comment.permlink,
|
||||
},
|
||||
key: `${comment.author}/${comment.permlink}`,
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -5,11 +5,13 @@ import { useIntl } from 'react-intl';
|
||||
|
||||
// Components
|
||||
import EStyleSheet from 'react-native-extended-stylesheet';
|
||||
import { Comment, TextButton } from '../..';
|
||||
import { Comment, TextButton, UpvotePopover } from '../..';
|
||||
|
||||
// Styles
|
||||
import styles from './commentStyles';
|
||||
import { OptionsModal } from '../../atoms';
|
||||
import { PostTypes } from '../../../constants/postTypes';
|
||||
import { PostHtmlInteractionHandler } from '../../postHtmlRenderer';
|
||||
|
||||
const CommentsView = ({
|
||||
avatarSize,
|
||||
@ -42,6 +44,8 @@ const CommentsView = ({
|
||||
const [selectedComment, setSelectedComment] = useState(null);
|
||||
const intl = useIntl();
|
||||
const commentMenu = useRef<any>();
|
||||
const upvotePopoverRef = useRef();
|
||||
const postInteractionRef = useRef(null);
|
||||
|
||||
const _openCommentMenu = (item) => {
|
||||
if (commentMenu.current) {
|
||||
@ -67,6 +71,18 @@ const CommentsView = ({
|
||||
setSelectedComment(null);
|
||||
};
|
||||
|
||||
const _onUpvotePress = ({ content, anchorRect, showPayoutDetails, onVotingStart }) => {
|
||||
if (upvotePopoverRef.current) {
|
||||
upvotePopoverRef.current.showPopover({
|
||||
anchorRect,
|
||||
showPayoutDetails,
|
||||
content,
|
||||
postType: PostTypes.COMMENT,
|
||||
onVotingStart,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const menuItems = [
|
||||
intl.formatMessage({ id: 'post.copy_link' }),
|
||||
intl.formatMessage({ id: 'post.copy_text' }),
|
||||
@ -101,6 +117,10 @@ const CommentsView = ({
|
||||
handleOnReplyPress={handleOnReplyPress}
|
||||
handleOnUserPress={handleOnUserPress}
|
||||
handleOnVotersPress={handleOnVotersPress}
|
||||
handleImagePress={postInteractionRef.current?.handleImagePress}
|
||||
handleLinkPress={postInteractionRef.current?.handleLinkPress}
|
||||
handleVideoPress={postInteractionRef.current?.handleVideoPress}
|
||||
handleYoutubePress={postInteractionRef.current?.handleYoutubePress}
|
||||
isHideImage={isHideImage}
|
||||
isLoggedIn={isLoggedIn}
|
||||
showAllComments={showAllComments}
|
||||
@ -109,6 +129,7 @@ const CommentsView = ({
|
||||
marginLeft={marginLeft}
|
||||
handleOnLongPress={() => _openCommentMenu(item)}
|
||||
openReplyThread={() => _openReplyThread(item)}
|
||||
onUpvotePress={_onUpvotePress}
|
||||
fetchedAt={fetchedAt}
|
||||
incrementRepliesCount={incrementRepliesCount}
|
||||
/>
|
||||
@ -157,6 +178,8 @@ const CommentsView = ({
|
||||
cancelButtonIndex={3}
|
||||
onPress={_onMenuItemPress}
|
||||
/>
|
||||
<UpvotePopover ref={upvotePopoverRef} />
|
||||
<PostHtmlInteractionHandler ref={postInteractionRef} />
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
@ -31,7 +31,7 @@ import { PercentBar } from './percentBar';
|
||||
import { PinAnimatedInput } from './pinAnimatedInput';
|
||||
import { PostCard } from './postCard';
|
||||
import { PostDisplay } from './postView';
|
||||
import { PostDropdown } from './postDropdown';
|
||||
import { PostOptionsModal } from './postOptionsModal';
|
||||
import { PostForm } from './postForm';
|
||||
import { PostHeaderDescription, PostBody, Tags } from './postElements';
|
||||
import { DraftListItem } from './draftListItem';
|
||||
@ -57,7 +57,7 @@ import { TextInput } from './textInput';
|
||||
import { ToastNotification } from './toastNotification';
|
||||
import { ToggleSwitch } from './toggleSwitch';
|
||||
import { TransferFormItem } from './transferFormItem';
|
||||
import { Upvote } from './upvote';
|
||||
import { UpvotePopover } from './upvotePopover';
|
||||
import { UserAvatar } from './userAvatar';
|
||||
|
||||
import Logo from './logo/logo';
|
||||
@ -79,7 +79,6 @@ import { PostComments } from './postComments';
|
||||
import { LeaderBoard } from './leaderboard';
|
||||
import { Notification } from './notification';
|
||||
import { WalletHeader } from './walletHeader';
|
||||
import { Posts } from './posts';
|
||||
import { Transaction } from './transaction';
|
||||
import { VotersDisplay } from './votersDisplay';
|
||||
import { Wallet } from './wallet';
|
||||
@ -180,12 +179,11 @@ export {
|
||||
PostCard,
|
||||
PostCardPlaceHolder,
|
||||
PostDisplay,
|
||||
PostDropdown,
|
||||
PostOptionsModal,
|
||||
PostForm,
|
||||
PostHeaderDescription,
|
||||
DraftListItem,
|
||||
PostPlaceHolder,
|
||||
Posts,
|
||||
ProductItemLine,
|
||||
Profile,
|
||||
ProfileEditForm,
|
||||
@ -221,7 +219,7 @@ export {
|
||||
ToggleSwitch,
|
||||
Transaction,
|
||||
TransferFormItem,
|
||||
Upvote,
|
||||
UpvotePopover,
|
||||
UserAvatar,
|
||||
UserListItem,
|
||||
VotersDisplay,
|
||||
|
36
src/components/postCard/children/children.styles.tsx
Normal file
36
src/components/postCard/children/children.styles.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
import EStyleSheet from 'react-native-extended-stylesheet';
|
||||
|
||||
export default EStyleSheet.create({
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
upvoteButton: {
|
||||
flexDirection: 'row',
|
||||
alignSelf: 'center',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
upvoteIcon: {
|
||||
alignSelf: 'center',
|
||||
fontSize: 24,
|
||||
color: '$primaryBlue',
|
||||
marginRight: 5,
|
||||
},
|
||||
payoutTextButton: {
|
||||
alignSelf: 'center',
|
||||
},
|
||||
payoutValue: {
|
||||
alignSelf: 'center',
|
||||
fontSize: 10,
|
||||
color: '$primaryDarkGray',
|
||||
marginLeft: 8,
|
||||
},
|
||||
declinedPayout: {
|
||||
textDecorationLine: 'line-through',
|
||||
textDecorationStyle: 'solid',
|
||||
},
|
||||
boldText: {
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
});
|
95
src/components/postCard/children/postCardActionsPanel.tsx
Normal file
95
src/components/postCard/children/postCardActionsPanel.tsx
Normal file
@ -0,0 +1,95 @@
|
||||
import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { TouchableOpacity, View } from 'react-native';
|
||||
|
||||
// Components
|
||||
import { TextWithIcon } from '../../basicUIElements';
|
||||
|
||||
// Styles
|
||||
import styles from './postCardStyles';
|
||||
import { UpvoteButton } from './upvoteButton';
|
||||
import { PostTypes } from '../../../constants/postTypes';
|
||||
import { PostCardActionIds } from '../container/postCard';
|
||||
import ROUTES from '../../../constants/routeNames';
|
||||
|
||||
interface Props {
|
||||
content: any;
|
||||
reblogs: any[];
|
||||
handleCardInteraction: (
|
||||
id: PostCardActionIds,
|
||||
payload?: any,
|
||||
onCallback?: (data: any) => void,
|
||||
) => void;
|
||||
}
|
||||
|
||||
export const PostCardActionsPanel = ({ content, reblogs, handleCardInteraction }: Props) => {
|
||||
const activeVotes = content?.active_votes || [];
|
||||
|
||||
const _onVotersPress = () => {
|
||||
handleCardInteraction(PostCardActionIds.NAVIGATE, {
|
||||
name: ROUTES.SCREENS.VOTERS,
|
||||
params: {
|
||||
content,
|
||||
},
|
||||
key: content.permlink,
|
||||
});
|
||||
};
|
||||
|
||||
const _onReblogsPress = () => {
|
||||
if (reblogs?.length) {
|
||||
handleCardInteraction(PostCardActionIds.NAVIGATE, {
|
||||
name: ROUTES.SCREENS.REBLOGS,
|
||||
params: {
|
||||
reblogs,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.bodyFooter}>
|
||||
<View style={styles.leftFooterWrapper}>
|
||||
<UpvoteButton
|
||||
content={content}
|
||||
activeVotes={activeVotes}
|
||||
isShowPayoutValue={true}
|
||||
parentType={PostTypes.POST}
|
||||
onUpvotePress={(anchorRect, onVotingStart) =>
|
||||
handleCardInteraction(PostCardActionIds.UPVOTE, anchorRect, onVotingStart)
|
||||
}
|
||||
onPayoutDetailsPress={(anchorRect) =>
|
||||
handleCardInteraction(PostCardActionIds.PAYOUT_DETAILS, anchorRect)
|
||||
}
|
||||
/>
|
||||
|
||||
<TouchableOpacity style={styles.commentButton} onPress={_onVotersPress}>
|
||||
<TextWithIcon
|
||||
iconName="heart-outline"
|
||||
iconStyle={styles.commentIcon}
|
||||
iconType="MaterialCommunityIcons"
|
||||
isClickable
|
||||
text={activeVotes.length}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<View style={styles.rightFooterWrapper}>
|
||||
<TextWithIcon
|
||||
iconName="repeat"
|
||||
iconStyle={styles.commentIcon}
|
||||
iconType="MaterialIcons"
|
||||
isClickable
|
||||
text={get(reblogs, 'length', 0)}
|
||||
onPress={_onReblogsPress}
|
||||
/>
|
||||
<TextWithIcon
|
||||
iconName="comment-outline"
|
||||
iconStyle={styles.commentIcon}
|
||||
iconType="MaterialCommunityIcons"
|
||||
isClickable
|
||||
text={get(content, 'children', 0)}
|
||||
onPress={() => handleCardInteraction(PostCardActionIds.REPLY)}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
97
src/components/postCard/children/postCardContent.tsx
Normal file
97
src/components/postCard/children/postCardContent.tsx
Normal file
@ -0,0 +1,97 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { TouchableOpacity, Text, View } from 'react-native';
|
||||
|
||||
// Utils
|
||||
import FastImage from 'react-native-fast-image';
|
||||
|
||||
// Components
|
||||
|
||||
// Styles
|
||||
import styles from './postCardStyles';
|
||||
import { PostCardActionIds } from '../container/postCard';
|
||||
import getWindowDimensions from '../../../utils/getWindowDimensions';
|
||||
import ROUTES from '../../../constants/routeNames';
|
||||
|
||||
const dim = getWindowDimensions();
|
||||
const DEFAULT_IMAGE =
|
||||
'https://images.ecency.com/DQmT8R33geccEjJfzZEdsRHpP3VE8pu3peRCnQa1qukU4KR/no_image_3x.png';
|
||||
const NSFW_IMAGE =
|
||||
'https://images.ecency.com/DQmZ1jW4p7o5GyoqWyCib1fSLE2ftbewsMCt2GvbmT9kmoY/nsfw_3x.png';
|
||||
|
||||
interface Props {
|
||||
content: any;
|
||||
isHideImage: boolean;
|
||||
thumbHeight: number;
|
||||
nsfw: string;
|
||||
setThumbHeight: (postPath: string, height: number) => void;
|
||||
handleCardInteraction: (id: PostCardActionIds, payload?: any) => void;
|
||||
}
|
||||
|
||||
export const PostCardContent = ({
|
||||
content,
|
||||
isHideImage,
|
||||
thumbHeight,
|
||||
nsfw,
|
||||
setThumbHeight,
|
||||
handleCardInteraction,
|
||||
}: Props) => {
|
||||
const [calcImgHeight, setCalcImgHeight] = useState(thumbHeight || 300);
|
||||
|
||||
const _onPress = () => {
|
||||
handleCardInteraction(PostCardActionIds.NAVIGATE, {
|
||||
name: ROUTES.SCREENS.POST,
|
||||
params: {
|
||||
content,
|
||||
author: content.author,
|
||||
permlink: content.permlink,
|
||||
},
|
||||
key: `${content.author}/${content.permlink}`,
|
||||
});
|
||||
};
|
||||
|
||||
let images = { image: DEFAULT_IMAGE, thumbnail: DEFAULT_IMAGE };
|
||||
if (content.thumbnail) {
|
||||
if (nsfw !== '0' && content.nsfw) {
|
||||
images = { image: NSFW_IMAGE, thumbnail: NSFW_IMAGE };
|
||||
} else {
|
||||
images = { image: content.image, thumbnail: content.thumbnail };
|
||||
}
|
||||
} else {
|
||||
images = { image: DEFAULT_IMAGE, thumbnail: DEFAULT_IMAGE };
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.postBodyWrapper}>
|
||||
<TouchableOpacity activeOpacity={0.8} style={styles.hiddenImages} onPress={_onPress}>
|
||||
{!isHideImage && (
|
||||
<FastImage
|
||||
source={{ uri: images.image }}
|
||||
style={[
|
||||
styles.thumbnail,
|
||||
{
|
||||
width: dim.width - 18,
|
||||
height: Math.min(calcImgHeight, dim.height),
|
||||
},
|
||||
]}
|
||||
resizeMode={
|
||||
calcImgHeight < dim.height ? FastImage.resizeMode.contain : FastImage.resizeMode.cover
|
||||
}
|
||||
onLoad={(evt) => {
|
||||
if (!thumbHeight) {
|
||||
const height = (evt.nativeEvent.height / evt.nativeEvent.width) * (dim.width - 18);
|
||||
setCalcImgHeight(height);
|
||||
setThumbHeight(content.author + content.permlink, height);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<View style={[styles.postDescripton]}>
|
||||
<Text style={styles.title}>{content.title}</Text>
|
||||
<Text style={styles.summary}>{content.summary}</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
};
|
76
src/components/postCard/children/postCardHeader.tsx
Normal file
76
src/components/postCard/children/postCardHeader.tsx
Normal file
@ -0,0 +1,76 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { View } from 'react-native';
|
||||
|
||||
// Components
|
||||
import { IntlShape } from 'react-intl';
|
||||
import { PostHeaderDescription } from '../../postElements';
|
||||
import { TextWithIcon } from '../../basicUIElements';
|
||||
import { Icon } from '../../icon';
|
||||
|
||||
// Styles
|
||||
import styles from './postCardStyles';
|
||||
import { IconButton } from '../..';
|
||||
import { getTimeFromNow } from '../../../utils/time';
|
||||
import { PostCardActionIds } from '../container/postCard';
|
||||
|
||||
interface Props {
|
||||
intl: IntlShape;
|
||||
content: any;
|
||||
isHideImage: boolean;
|
||||
handleCardInteraction: (id: PostCardActionIds, payload?: any) => void;
|
||||
}
|
||||
|
||||
export const PostCardHeader = ({ intl, content, isHideImage, handleCardInteraction }: Props) => {
|
||||
const rebloggedBy = get(content, 'reblogged_by[0]', null);
|
||||
const dateString = useMemo(() => getTimeFromNow(content?.created), [content]);
|
||||
|
||||
const _handleOnTagPress = (navParams) => {
|
||||
handleCardInteraction(PostCardActionIds.NAVIGATE, navParams);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{!!rebloggedBy && (
|
||||
<TextWithIcon
|
||||
wrapperStyle={styles.reblogWrapper}
|
||||
text={`${intl.formatMessage({ id: 'post.reblogged' })} ${rebloggedBy}`}
|
||||
iconType="MaterialIcons"
|
||||
iconName="repeat"
|
||||
iconSize={16}
|
||||
textStyle={styles.reblogText}
|
||||
isClickable={true}
|
||||
onPress={() => handleCardInteraction(PostCardActionIds.USER, rebloggedBy)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<View style={styles.bodyHeader}>
|
||||
<PostHeaderDescription
|
||||
date={dateString}
|
||||
isHideImage={isHideImage}
|
||||
name={get(content, 'author')}
|
||||
profileOnPress={() => handleCardInteraction(PostCardActionIds.USER, content.author)}
|
||||
handleOnTagPress={_handleOnTagPress}
|
||||
reputation={get(content, 'author_reputation')}
|
||||
size={50}
|
||||
content={content}
|
||||
rebloggedBy={rebloggedBy}
|
||||
isPromoted={get(content, 'is_promoted')}
|
||||
/>
|
||||
{(content?.stats?.is_pinned || content?.stats?.is_pinned_blog) && (
|
||||
<Icon style={styles.pushPinIcon} size={20} name="pin" iconType="MaterialCommunityIcons" />
|
||||
)}
|
||||
<View style={styles.dropdownWrapper}>
|
||||
<IconButton
|
||||
style={styles.optionsIconContainer}
|
||||
iconStyle={styles.optionsIcon}
|
||||
iconType="MaterialCommunityIcons"
|
||||
name="dots-vertical"
|
||||
onPress={() => handleCardInteraction(PostCardActionIds.OPTIONS)}
|
||||
size={24}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
};
|
@ -12,6 +12,12 @@ export default EStyleSheet.create({
|
||||
height: 1,
|
||||
},
|
||||
},
|
||||
optionsIconContainer: {
|
||||
marginLeft: 12,
|
||||
},
|
||||
optionsIcon: {
|
||||
color: '$iconColor',
|
||||
},
|
||||
commentButton: {
|
||||
padding: 0,
|
||||
margin: 0,
|
||||
@ -36,7 +42,7 @@ export default EStyleSheet.create({
|
||||
// height: 200,
|
||||
// width: '$deviceWidth - 16',
|
||||
borderRadius: 8,
|
||||
backgroundColor: '$primaryLightGray',
|
||||
backgroundColor: '$primaryLightBackground',
|
||||
},
|
||||
hiddenImages: {
|
||||
flexDirection: 'column',
|
||||
@ -74,7 +80,7 @@ export default EStyleSheet.create({
|
||||
color: '$primaryRed',
|
||||
alignSelf: 'center',
|
||||
marginLeft: 8,
|
||||
marginRight: -16,
|
||||
marginRight: -8,
|
||||
transform: [{ rotate: '45deg' }],
|
||||
},
|
||||
leftFooterWrapper: {
|
163
src/components/postCard/children/upvoteButton.tsx
Normal file
163
src/components/postCard/children/upvoteButton.tsx
Normal file
@ -0,0 +1,163 @@
|
||||
import React, { Fragment, useCallback, useEffect, useRef, useState } from "react";
|
||||
import { findNodeHandle, NativeModules, View, TouchableOpacity, Text, Alert } from "react-native";
|
||||
import { useAppSelector } from "../../../hooks";
|
||||
import { PulseAnimation } from "../../animations";
|
||||
import { isVoted as isVotedFunc, isDownVoted as isDownVotedFunc } from '../../../utils/postParser';
|
||||
import Icon from "../../icon";
|
||||
import styles from './children.styles';
|
||||
import { FormattedCurrency } from "../../formatedElements";
|
||||
import { Rect } from "react-native-modal-popover/lib/PopoverGeometry";
|
||||
import { PostTypes } from "../../../constants/postTypes";
|
||||
|
||||
interface UpvoteButtonProps {
|
||||
content: any,
|
||||
activeVotes: any[],
|
||||
isShowPayoutValue?: boolean,
|
||||
boldPayout?: boolean,
|
||||
parentType?: PostTypes;
|
||||
onUpvotePress: (anchorRect: Rect, onVotingStart: (status:number)=>void) => void,
|
||||
onPayoutDetailsPress: (anchorRef: Rect) => void,
|
||||
}
|
||||
|
||||
export const UpvoteButton = ({
|
||||
content,
|
||||
activeVotes,
|
||||
isShowPayoutValue,
|
||||
boldPayout,
|
||||
onUpvotePress,
|
||||
onPayoutDetailsPress
|
||||
}: UpvoteButtonProps) => {
|
||||
|
||||
const upvoteRef = useRef(null);
|
||||
const detailsRef = useRef(null);
|
||||
|
||||
const currentAccount = useAppSelector((state => state.account.currentAccount));
|
||||
|
||||
const [isVoted, setIsVoted] = useState<any>(null);
|
||||
const [isDownVoted, setIsDownVoted] = useState<any>(null);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
_calculateVoteStatus();
|
||||
}, [activeVotes]);
|
||||
|
||||
|
||||
const _calculateVoteStatus = useCallback(async () => {
|
||||
|
||||
//TODO: do this heavy lifting during parsing or react-query/cache response
|
||||
const _isVoted = await isVotedFunc(activeVotes, currentAccount?.name);
|
||||
const _isDownVoted = await isDownVotedFunc(activeVotes, currentAccount?.name);
|
||||
|
||||
|
||||
setIsVoted(_isVoted && parseInt(_isVoted, 10) / 10000);
|
||||
setIsDownVoted(_isDownVoted && (parseInt(_isDownVoted, 10) / 10000) * -1);
|
||||
|
||||
}, [activeVotes]);
|
||||
|
||||
|
||||
const _getRectFromRef = (ref: any, callback: (anchorRect: Rect, onVotingStart?) => void) => {
|
||||
const handle = findNodeHandle(ref.current);
|
||||
if (handle) {
|
||||
NativeModules.UIManager.measure(handle, (x0, y0, width, height, x, y) => {
|
||||
const anchorRect: Rect = { x, y, width, height };
|
||||
callback(anchorRect)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const _onPress = () => {
|
||||
const _onVotingStart = (status) => {
|
||||
if(status > 0){
|
||||
setIsVoted(true);
|
||||
} else if (status < 0) {
|
||||
setIsDownVoted(true);
|
||||
} else {
|
||||
_calculateVoteStatus();
|
||||
}
|
||||
}
|
||||
_getRectFromRef(upvoteRef, (rect)=>{
|
||||
onUpvotePress(rect, _onVotingStart)
|
||||
});
|
||||
}
|
||||
|
||||
const _onDetailsPress = () => {
|
||||
_getRectFromRef(detailsRef, onPayoutDetailsPress)
|
||||
}
|
||||
|
||||
|
||||
const isDeclinedPayout = content?.is_declined_payout;
|
||||
const totalPayout = content?.total_payout;
|
||||
const maxPayout = content?.max_payout;
|
||||
|
||||
const payoutLimitHit = totalPayout >= maxPayout;
|
||||
const _shownPayout = payoutLimitHit && maxPayout > 0 ? maxPayout : totalPayout;
|
||||
|
||||
|
||||
|
||||
|
||||
let iconName = 'upcircleo';
|
||||
const iconType = 'AntDesign';
|
||||
let downVoteIconName = 'downcircleo';
|
||||
|
||||
if (isVoted) {
|
||||
iconName = 'upcircle';
|
||||
}
|
||||
|
||||
if (isDownVoted) {
|
||||
downVoteIconName = 'downcircle';
|
||||
}
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<TouchableOpacity
|
||||
ref={upvoteRef}
|
||||
onPress={_onPress}
|
||||
style={styles.upvoteButton}
|
||||
>
|
||||
{/* <Fragment>
|
||||
{isVoting ? (
|
||||
<View style={{ width: 19 }}>
|
||||
<PulseAnimation
|
||||
color="#357ce6"
|
||||
numPulses={1}
|
||||
diameter={20}
|
||||
speed={100}
|
||||
duration={1500}
|
||||
isShow={!isVoting}
|
||||
/>
|
||||
</View>
|
||||
) : ( */}
|
||||
<View hitSlop={{ top: 10, bottom: 10, left: 10, right: 5 }}>
|
||||
<Icon
|
||||
style={[styles.upvoteIcon, isDownVoted && { color: '#ec8b88' }]}
|
||||
active={!currentAccount}
|
||||
iconType={iconType}
|
||||
name={isDownVoted ? downVoteIconName : iconName}
|
||||
/>
|
||||
</View>
|
||||
{/* )}
|
||||
</Fragment> */}
|
||||
</TouchableOpacity>
|
||||
<View style={styles.payoutTextButton}>
|
||||
{isShowPayoutValue && (
|
||||
<TouchableOpacity ref={detailsRef} onPress={_onDetailsPress} >
|
||||
<Text
|
||||
style={[
|
||||
styles.payoutValue,
|
||||
isDeclinedPayout && styles.declinedPayout,
|
||||
boldPayout && styles.boldText,
|
||||
]}
|
||||
>
|
||||
{<FormattedCurrency value={_shownPayout || '0.000'} />}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
)
|
||||
}
|
60
src/components/postCard/container/postCard.tsx
Normal file
60
src/components/postCard/container/postCard.tsx
Normal file
@ -0,0 +1,60 @@
|
||||
import React from 'react';
|
||||
import { View } from 'react-native';
|
||||
import { PostCardActionsPanel } from '../children/postCardActionsPanel';
|
||||
import { PostCardContent } from '../children/postCardContent';
|
||||
import { PostCardHeader } from '../children/postCardHeader';
|
||||
|
||||
import styles from '../children/postCardStyles';
|
||||
|
||||
/*
|
||||
* Props Name Description Value
|
||||
*@props --> props name here description here Value Type Here
|
||||
*
|
||||
*/
|
||||
|
||||
export enum PostCardActionIds {
|
||||
USER = 'USER',
|
||||
OPTIONS = 'OPTIONS',
|
||||
UNMUTE = 'UNMUTE',
|
||||
REPLY = 'REPLY',
|
||||
UPVOTE = 'UPVOTE',
|
||||
PAYOUT_DETAILS = 'PAYOUT_DETAILS',
|
||||
NAVIGATE = 'NAVIGATE',
|
||||
}
|
||||
|
||||
const PostCard = ({
|
||||
intl,
|
||||
content,
|
||||
isHideImage,
|
||||
nsfw,
|
||||
reblogs,
|
||||
imageHeight,
|
||||
setImageHeight,
|
||||
handleCardInteraction,
|
||||
}) => {
|
||||
return (
|
||||
<View style={styles.post}>
|
||||
<PostCardHeader
|
||||
intl={intl}
|
||||
content={content}
|
||||
isHideImage={isHideImage}
|
||||
handleCardInteraction={handleCardInteraction}
|
||||
/>
|
||||
<PostCardContent
|
||||
content={content}
|
||||
isHideImage={isHideImage}
|
||||
nsfw={nsfw}
|
||||
thumbHeight={imageHeight}
|
||||
setThumbHeight={setImageHeight}
|
||||
handleCardInteraction={handleCardInteraction}
|
||||
/>
|
||||
<PostCardActionsPanel
|
||||
content={content}
|
||||
reblogs={reblogs || []}
|
||||
handleCardInteraction={handleCardInteraction}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default PostCard;
|
@ -1,161 +0,0 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import get from 'lodash/get';
|
||||
|
||||
// Services
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { getPost } from '../../../providers/hive/dhive';
|
||||
import { getPostReblogs } from '../../../providers/ecency/ecency';
|
||||
|
||||
import PostCardView from '../view/postCardView';
|
||||
|
||||
// Constants
|
||||
import { default as ROUTES } from '../../../constants/routeNames';
|
||||
import { useAppDispatch } from '../../../hooks';
|
||||
import { showProfileModal } from '../../../redux/actions/uiAction';
|
||||
import { postQueries } from '../../../providers/queries';
|
||||
|
||||
/*
|
||||
* Props Name Description Value
|
||||
*@props --> props name here description here Value Type Here
|
||||
*
|
||||
*/
|
||||
|
||||
const PostCardContainer = ({
|
||||
currentAccount,
|
||||
content,
|
||||
isHideImage,
|
||||
nsfw,
|
||||
imageHeight,
|
||||
setImageHeight,
|
||||
pageType,
|
||||
showQuickReplyModal,
|
||||
mutes,
|
||||
}) => {
|
||||
const navigation = useNavigation();
|
||||
const dispatch = useAppDispatch();
|
||||
const postsCacherPrimer = postQueries.usePostsCachePrimer();
|
||||
|
||||
const [_content, setContent] = useState(content);
|
||||
const [reblogs, setReblogs] = useState([]);
|
||||
const activeVotes = get(_content, 'active_votes', []);
|
||||
const [isMuted, setIsMuted] = useState(!!mutes && mutes.indexOf(content.author) > -1);
|
||||
|
||||
useEffect(() => {
|
||||
let isCancelled = false;
|
||||
|
||||
const fetchData = async (val) => {
|
||||
try {
|
||||
const dd = await getPostReblogs(val);
|
||||
if (!isCancelled) {
|
||||
setReblogs(dd);
|
||||
return dd;
|
||||
}
|
||||
} catch (e) {
|
||||
if (!isCancelled) {
|
||||
setReblogs([]);
|
||||
return val;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (content) {
|
||||
fetchData(content);
|
||||
}
|
||||
|
||||
return () => {
|
||||
isCancelled = true;
|
||||
};
|
||||
}, [_content]);
|
||||
|
||||
const _fetchPost = async () => {
|
||||
await getPost(
|
||||
get(_content, 'author'),
|
||||
get(_content, 'permlink'),
|
||||
get(currentAccount, 'username'),
|
||||
)
|
||||
.then((result) => {
|
||||
if (result) {
|
||||
setContent(result);
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
};
|
||||
|
||||
const _handleOnUserPress = (username) => {
|
||||
if (_content) {
|
||||
username = username || get(_content, 'author');
|
||||
dispatch(showProfileModal(username));
|
||||
}
|
||||
};
|
||||
|
||||
const _handleOnContentPress = (value) => {
|
||||
if (value) {
|
||||
postsCacherPrimer.cachePost(value);
|
||||
navigation.navigate({
|
||||
name: ROUTES.SCREENS.POST,
|
||||
params: {
|
||||
author: value.author,
|
||||
permlink: value.permlink,
|
||||
},
|
||||
key: get(value, 'permlink'),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const _handleOnVotersPress = () => {
|
||||
navigation.navigate({
|
||||
name: ROUTES.SCREENS.VOTERS,
|
||||
params: {
|
||||
activeVotes,
|
||||
content: _content,
|
||||
},
|
||||
key: get(_content, 'permlink'),
|
||||
});
|
||||
};
|
||||
|
||||
const _handleOnReblogsPress = () => {
|
||||
navigation.navigate({
|
||||
name: ROUTES.SCREENS.REBLOGS,
|
||||
params: {
|
||||
reblogs,
|
||||
},
|
||||
key: get(_content, 'permlink', get(_content, 'author', '')),
|
||||
});
|
||||
};
|
||||
|
||||
const _handleOnUnmutePress = () => {
|
||||
setIsMuted(false);
|
||||
};
|
||||
|
||||
const _handleQuickReplyModal = () => {
|
||||
showQuickReplyModal(content);
|
||||
};
|
||||
return (
|
||||
<PostCardView
|
||||
handleOnUserPress={_handleOnUserPress}
|
||||
handleOnContentPress={_handleOnContentPress}
|
||||
handleOnVotersPress={_handleOnVotersPress}
|
||||
handleOnReblogsPress={_handleOnReblogsPress}
|
||||
handleOnUnmutePress={_handleOnUnmutePress}
|
||||
content={_content}
|
||||
isHideImage={isHideImage}
|
||||
nsfw={nsfw || '1'}
|
||||
reblogs={reblogs}
|
||||
activeVotes={activeVotes}
|
||||
imageHeight={imageHeight}
|
||||
setImageHeight={setImageHeight}
|
||||
isMuted={isMuted}
|
||||
pageType={pageType}
|
||||
fetchPost={_fetchPost}
|
||||
showQuickReplyModal={_handleQuickReplyModal}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
currentAccount: state.account.currentAccount,
|
||||
nsfw: state.application.nsfw,
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(PostCardContainer);
|
@ -1,5 +1,4 @@
|
||||
import PostCardView from './view/postCardView';
|
||||
import PostCard from './container/postCardContainer';
|
||||
import PostCard from './container/postCard';
|
||||
|
||||
export { PostCardView, PostCard };
|
||||
export { PostCard };
|
||||
export default PostCard;
|
||||
|
@ -1,223 +0,0 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { TouchableOpacity, Text, View } from 'react-native';
|
||||
import { injectIntl } from 'react-intl';
|
||||
|
||||
// Utils
|
||||
import FastImage from 'react-native-fast-image';
|
||||
import { getTimeFromNow } from '../../../utils/time';
|
||||
// import bugsnagInstance from '../../../config/bugsnag';
|
||||
|
||||
// Components
|
||||
import { PostHeaderDescription } from '../../postElements';
|
||||
import { PostDropdown } from '../../postDropdown';
|
||||
import { TextWithIcon } from '../../basicUIElements';
|
||||
import { Icon } from '../../icon';
|
||||
|
||||
// STEEM
|
||||
import { Upvote } from '../../upvote';
|
||||
// Styles
|
||||
import styles from './postCardStyles';
|
||||
import { TextButton } from '../..';
|
||||
import getWindowDimensions from '../../../utils/getWindowDimensions';
|
||||
import postTypes from '../../../constants/postTypes';
|
||||
|
||||
const dim = getWindowDimensions();
|
||||
const DEFAULT_IMAGE =
|
||||
'https://images.ecency.com/DQmT8R33geccEjJfzZEdsRHpP3VE8pu3peRCnQa1qukU4KR/no_image_3x.png';
|
||||
const NSFW_IMAGE =
|
||||
'https://images.ecency.com/DQmZ1jW4p7o5GyoqWyCib1fSLE2ftbewsMCt2GvbmT9kmoY/nsfw_3x.png';
|
||||
|
||||
const PostCardView = ({
|
||||
handleOnUserPress,
|
||||
handleOnContentPress,
|
||||
handleOnVotersPress,
|
||||
handleOnReblogsPress,
|
||||
handleOnUnmutePress,
|
||||
showQuickReplyModal,
|
||||
content,
|
||||
reblogs,
|
||||
isHideImage,
|
||||
fetchPost,
|
||||
nsfw,
|
||||
intl,
|
||||
activeVotes,
|
||||
imageHeight,
|
||||
setImageHeight,
|
||||
isMuted,
|
||||
pageType,
|
||||
}) => {
|
||||
// local state to manage fake upvote if available
|
||||
const activeVotesCount = activeVotes ? activeVotes.length : 0;
|
||||
const [cacheVoteIcrement, setCacheVoteIcrement] = useState(0);
|
||||
const [calcImgHeight, setCalcImgHeight] = useState(imageHeight || 300);
|
||||
|
||||
// Component Functions
|
||||
const _handleOnUserPress = (username) => {
|
||||
if (handleOnUserPress) {
|
||||
handleOnUserPress(username);
|
||||
}
|
||||
};
|
||||
|
||||
const _handleOnContentPress = () => {
|
||||
console.log('content : ', content);
|
||||
handleOnContentPress(content);
|
||||
};
|
||||
|
||||
const _handleOnVotersPress = () => {
|
||||
handleOnVotersPress();
|
||||
};
|
||||
|
||||
const _handleOnReblogsPress = () => {
|
||||
if (reblogs && reblogs.length > 0) {
|
||||
handleOnReblogsPress();
|
||||
}
|
||||
};
|
||||
|
||||
const _handleCacheVoteIncrement = () => {
|
||||
// fake increment vote using based on local change
|
||||
setCacheVoteIcrement(1);
|
||||
};
|
||||
|
||||
const rebloggedBy = get(content, 'reblogged_by[0]', null);
|
||||
|
||||
let images = { image: DEFAULT_IMAGE, thumbnail: DEFAULT_IMAGE };
|
||||
if (content.thumbnail) {
|
||||
if (isMuted || (nsfw !== '0' && content.nsfw)) {
|
||||
images = { image: NSFW_IMAGE, thumbnail: NSFW_IMAGE };
|
||||
} else {
|
||||
images = { image: content.image, thumbnail: content.thumbnail };
|
||||
}
|
||||
} else {
|
||||
images = { image: DEFAULT_IMAGE, thumbnail: DEFAULT_IMAGE };
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.post}>
|
||||
{!!rebloggedBy && (
|
||||
<TextWithIcon
|
||||
wrapperStyle={styles.reblogWrapper}
|
||||
text={`${intl.formatMessage({ id: 'post.reblogged' })} ${rebloggedBy}`}
|
||||
iconType="MaterialIcons"
|
||||
iconName="repeat"
|
||||
iconSize={16}
|
||||
textStyle={styles.reblogText}
|
||||
isClickable={true}
|
||||
onPress={() => _handleOnUserPress(rebloggedBy)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<View style={styles.bodyHeader}>
|
||||
<PostHeaderDescription
|
||||
date={getTimeFromNow(get(content, 'created'))}
|
||||
isHideImage={isHideImage}
|
||||
name={get(content, 'author')}
|
||||
profileOnPress={_handleOnUserPress}
|
||||
reputation={get(content, 'author_reputation')}
|
||||
size={50}
|
||||
content={content}
|
||||
rebloggedBy={rebloggedBy}
|
||||
isPromoted={get(content, 'is_promoted')}
|
||||
/>
|
||||
{(content?.stats?.is_pinned || content?.stats?.is_pinned_blog) && (
|
||||
<Icon style={styles.pushPinIcon} size={20} name="pin" iconType="MaterialCommunityIcons" />
|
||||
)}
|
||||
<View style={styles.dropdownWrapper}>
|
||||
<PostDropdown
|
||||
pageType={pageType}
|
||||
content={content}
|
||||
fetchPost={fetchPost}
|
||||
isMuted={isMuted}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.postBodyWrapper}>
|
||||
<TouchableOpacity
|
||||
activeOpacity={1}
|
||||
style={styles.hiddenImages}
|
||||
onPress={_handleOnContentPress}
|
||||
>
|
||||
{!isHideImage && (
|
||||
<FastImage
|
||||
source={{ uri: images.image }}
|
||||
style={[
|
||||
styles.thumbnail,
|
||||
{
|
||||
width: dim.width - 18,
|
||||
height: Math.min(calcImgHeight, dim.height),
|
||||
},
|
||||
]}
|
||||
resizeMode={
|
||||
calcImgHeight < dim.height
|
||||
? FastImage.resizeMode.contain
|
||||
: FastImage.resizeMode.cover
|
||||
}
|
||||
onLoad={(evt) => {
|
||||
if (!imageHeight) {
|
||||
const height =
|
||||
(evt.nativeEvent.height / evt.nativeEvent.width) * (dim.width - 18);
|
||||
setCalcImgHeight(height);
|
||||
setImageHeight(content.author + content.permlink, height);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{!isMuted ? (
|
||||
<View style={[styles.postDescripton]}>
|
||||
<Text style={styles.title}>{content.title}</Text>
|
||||
<Text style={styles.summary}>{content.summary}</Text>
|
||||
</View>
|
||||
) : (
|
||||
<TextButton
|
||||
style={styles.revealButton}
|
||||
textStyle={styles.revealText}
|
||||
onPress={() => handleOnUnmutePress()}
|
||||
text={intl.formatMessage({ id: 'post.reveal_muted' })}
|
||||
/>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<View style={styles.bodyFooter}>
|
||||
<View style={styles.leftFooterWrapper}>
|
||||
<Upvote
|
||||
activeVotes={activeVotes}
|
||||
isShowPayoutValue
|
||||
content={content}
|
||||
handleCacheVoteIncrement={_handleCacheVoteIncrement}
|
||||
parentType={postTypes.POST}
|
||||
/>
|
||||
<TouchableOpacity style={styles.commentButton} onPress={_handleOnVotersPress}>
|
||||
<TextWithIcon
|
||||
iconName="heart-outline"
|
||||
iconStyle={styles.commentIcon}
|
||||
iconType="MaterialCommunityIcons"
|
||||
isClickable
|
||||
text={activeVotesCount + cacheVoteIcrement}
|
||||
onPress={_handleOnVotersPress}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<View style={styles.rightFooterWrapper}>
|
||||
<TextWithIcon
|
||||
iconName="repeat"
|
||||
iconStyle={styles.commentIcon}
|
||||
iconType="MaterialIcons"
|
||||
isClickable
|
||||
text={get(reblogs, 'length', 0)}
|
||||
onPress={_handleOnReblogsPress}
|
||||
/>
|
||||
<TextWithIcon
|
||||
iconName="comment-outline"
|
||||
iconStyle={styles.commentIcon}
|
||||
iconType="MaterialCommunityIcons"
|
||||
isClickable
|
||||
text={get(content, 'children', 0)}
|
||||
onPress={showQuickReplyModal}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default injectIntl(PostCardView);
|
@ -18,6 +18,7 @@ export const CommentsSection = ({ item, index, revealReplies, ...props }) => {
|
||||
const _enteringAnim = SlideInRight.duration(150)
|
||||
.springify()
|
||||
.delay(index * 100);
|
||||
|
||||
return (
|
||||
<Animated.View key={item.author + item.permlink} entering={_enteringAnim}>
|
||||
<Comment
|
||||
|
93
src/components/postComments/children/sortComments.ts
Normal file
93
src/components/postComments/children/sortComments.ts
Normal file
@ -0,0 +1,93 @@
|
||||
export const sortComments = (sortOrder = 'trending', _comments) => {
|
||||
const sortedComments: any[] = _comments;
|
||||
|
||||
const absNegative = (a) => a.net_rshares < 0;
|
||||
|
||||
const sortOrders = {
|
||||
trending: (a, b) => {
|
||||
if (a.renderOnTop) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (absNegative(a)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (absNegative(b)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const apayout = a.total_payout;
|
||||
const bpayout = b.total_payout;
|
||||
|
||||
if (apayout !== bpayout) {
|
||||
return bpayout - apayout;
|
||||
}
|
||||
|
||||
return 0;
|
||||
},
|
||||
reputation: (a, b) => {
|
||||
if (a.renderOnTop) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const keyA = a.author_reputation;
|
||||
const keyB = b.author_reputation;
|
||||
|
||||
if (keyA > keyB) {
|
||||
return -1;
|
||||
}
|
||||
if (keyA < keyB) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
},
|
||||
votes: (a, b) => {
|
||||
if (a.renderOnTop) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const keyA = a.active_votes.length;
|
||||
const keyB = b.active_votes.length;
|
||||
|
||||
if (keyA > keyB) {
|
||||
return -1;
|
||||
}
|
||||
if (keyA < keyB) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
},
|
||||
age: (a, b) => {
|
||||
if (a.renderOnTop) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (absNegative(a)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (absNegative(b)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const keyA = Date.parse(a.created);
|
||||
const keyB = Date.parse(b.created);
|
||||
|
||||
if (keyA > keyB) {
|
||||
return -1;
|
||||
}
|
||||
if (keyA < keyB) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
},
|
||||
};
|
||||
|
||||
sortedComments.sort(sortOrders[sortOrder]);
|
||||
|
||||
return sortedComments;
|
||||
};
|
@ -5,6 +5,7 @@ import React, {
|
||||
useState,
|
||||
useMemo,
|
||||
useEffect,
|
||||
Fragment,
|
||||
} from 'react';
|
||||
import { ActivityIndicator, Platform, RefreshControl, Text } from 'react-native';
|
||||
import { useIntl } from 'react-intl';
|
||||
@ -19,13 +20,20 @@ import { FilterBar } from '../../filterBar';
|
||||
import { postQueries } from '../../../providers/queries';
|
||||
import { useAppDispatch, useAppSelector } from '../../../hooks';
|
||||
import ROUTES from '../../../constants/routeNames';
|
||||
import { showActionModal, toastNotification } from '../../../redux/actions/uiAction';
|
||||
import { showActionModal, showProfileModal, toastNotification } from '../../../redux/actions/uiAction';
|
||||
import { writeToClipboard } from '../../../utils/clipboard';
|
||||
import { deleteComment } from '../../../providers/hive/dhive';
|
||||
import { updateCommentCache } from '../../../redux/actions/cacheActions';
|
||||
import { CommentCacheStatus } from '../../../redux/reducers/cacheReducer';
|
||||
import { CacheStatus } from '../../../redux/reducers/cacheReducer';
|
||||
|
||||
import { PostTypes } from '../../../constants/postTypes';
|
||||
|
||||
import { CommentsSection } from '../children/commentsSection';
|
||||
import { sortComments } from '../children/sortComments';
|
||||
import styles from '../children/postComments.styles';
|
||||
import { PostHtmlInteractionHandler } from '../../postHtmlRenderer';
|
||||
|
||||
|
||||
|
||||
const PostComments = forwardRef(
|
||||
(
|
||||
@ -38,6 +46,7 @@ const PostComments = forwardRef(
|
||||
onRefresh,
|
||||
handleOnCommentsLoaded,
|
||||
handleOnReplyPress,
|
||||
onUpvotePress
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
@ -53,6 +62,7 @@ const PostComments = forwardRef(
|
||||
const postsCachePrimer = postQueries.usePostsCachePrimer();
|
||||
|
||||
const writeCommentRef = useRef(null);
|
||||
const postInteractionRef = useRef<typeof PostHtmlInteractionHandler|null>(null);
|
||||
const commentsListRef = useRef<FlatList | null>(null);
|
||||
|
||||
const [selectedFilter, setSelectedFilter] = useState('trending');
|
||||
@ -60,8 +70,9 @@ const PostComments = forwardRef(
|
||||
const [shouldRenderComments, setShouldRenderComments] = useState(false);
|
||||
const [headerHeight, setHeaderHeight] = useState(0);
|
||||
|
||||
|
||||
const sortedSections = useMemo(
|
||||
() => _sortComments(selectedFilter, discussionQuery.sectionedData),
|
||||
() => sortComments(selectedFilter, discussionQuery.sectionedData),
|
||||
[discussionQuery.sectionedData, selectedFilter],
|
||||
);
|
||||
|
||||
@ -92,6 +103,8 @@ const PostComments = forwardRef(
|
||||
onRefresh();
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
const _handleOnDropdownSelect = (option, index) => {
|
||||
setSelectedFilter(option);
|
||||
@ -106,7 +119,7 @@ const PostComments = forwardRef(
|
||||
content,
|
||||
},
|
||||
key: content.permlink,
|
||||
});
|
||||
} as never);
|
||||
};
|
||||
|
||||
const _handleOnEditPress = (item) => {
|
||||
@ -118,22 +131,42 @@ const PostComments = forwardRef(
|
||||
isReply: true,
|
||||
post: item,
|
||||
},
|
||||
});
|
||||
} as never);
|
||||
};
|
||||
|
||||
const _handleDeleteComment = (_permlink) => {
|
||||
deleteComment(currentAccount, pinHash, _permlink).then(() => {
|
||||
// remove cached entry based on parent
|
||||
const _commentPath = `${currentAccount.username}/${_permlink}`;
|
||||
console.log('deleted comment', _commentPath);
|
||||
|
||||
const _deletedItem = discussionQuery.data[_commentPath];
|
||||
if (_deletedItem) {
|
||||
_deletedItem.status = CommentCacheStatus.DELETED;
|
||||
delete _deletedItem.updated;
|
||||
dispatch(updateCommentCache(_commentPath, _deletedItem, { isUpdate: true }));
|
||||
const _onConfirmDelete = async () => {
|
||||
try {
|
||||
await deleteComment(currentAccount, pinHash, _permlink);
|
||||
// remove cached entry based on parent
|
||||
const _commentPath = `${currentAccount.username}/${_permlink}`;
|
||||
console.log('deleted comment', _commentPath);
|
||||
|
||||
const _deletedItem = discussionQuery.data[_commentPath];
|
||||
if (_deletedItem) {
|
||||
_deletedItem.status = CacheStatus.DELETED;
|
||||
delete _deletedItem.updated;
|
||||
dispatch(updateCommentCache(_commentPath, _deletedItem, { isUpdate: true }));
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Failed to delete comment')
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
dispatch(showActionModal({
|
||||
title: intl.formatMessage({ id: 'delete.confirm_delete_title' }),
|
||||
buttons: [{
|
||||
text: intl.formatMessage({ id: 'alert.cancel' }),
|
||||
onPress: () => { console.log("canceled delete comment") }
|
||||
}, {
|
||||
text: intl.formatMessage({ id: 'alert.delete' }),
|
||||
onPress: _onConfirmDelete
|
||||
}]
|
||||
}))
|
||||
|
||||
|
||||
};
|
||||
|
||||
const _openReplyThread = (comment) => {
|
||||
@ -145,9 +178,13 @@ const PostComments = forwardRef(
|
||||
author: comment.author,
|
||||
permlink: comment.permlink,
|
||||
},
|
||||
});
|
||||
} as never);
|
||||
};
|
||||
|
||||
const _handleOnUserPress = (username) => {
|
||||
dispatch(showProfileModal(username));
|
||||
}
|
||||
|
||||
const _handleShowOptionsMenu = (comment) => {
|
||||
const _showCopiedToast = () => {
|
||||
dispatch(
|
||||
@ -190,6 +227,11 @@ const PostComments = forwardRef(
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const _onContentSizeChange = (x: number, y: number) => {
|
||||
// lazy render comments after post is rendered;
|
||||
if (!shouldRenderComments) {
|
||||
@ -245,129 +287,50 @@ const PostComments = forwardRef(
|
||||
handleOnEditPress={_handleOnEditPress}
|
||||
handleOnVotersPress={_handleOnVotersPress}
|
||||
handleOnLongPress={_handleShowOptionsMenu}
|
||||
handleOnUserPress={_handleOnUserPress}
|
||||
handleImagePress={postInteractionRef.current?.handleImagePress}
|
||||
handleLinkPress={postInteractionRef.current?.handleLinkPress}
|
||||
handleVideoPress={postInteractionRef.current?.handleVideoPress}
|
||||
handleYoutubePress={postInteractionRef.current?.handleYoutubePress}
|
||||
openReplyThread={_openReplyThread}
|
||||
onUpvotePress={(args) => onUpvotePress({ ...args, postType: PostTypes.COMMENT })}
|
||||
/>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
ref={commentsListRef}
|
||||
style={styles.list}
|
||||
contentContainerStyle={styles.listContent}
|
||||
ListHeaderComponent={_postContentView}
|
||||
ListEmptyComponent={_renderEmptyContent}
|
||||
data={shouldRenderComments ? sortedSections : []}
|
||||
onContentSizeChange={_onContentSizeChange}
|
||||
renderItem={_renderItem}
|
||||
keyExtractor={(item) => `${item.author}/${item.permlink}`}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={discussionQuery.isFetching}
|
||||
onRefresh={_onRefresh}
|
||||
progressBackgroundColor="#357CE6"
|
||||
tintColor={!isDarkTheme ? '#357ce6' : '#96c0ff'}
|
||||
titleColor="#fff"
|
||||
colors={['#fff']}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Fragment>
|
||||
<FlatList
|
||||
ref={commentsListRef}
|
||||
style={styles.list}
|
||||
contentContainerStyle={styles.listContent}
|
||||
ListHeaderComponent={_postContentView}
|
||||
ListEmptyComponent={_renderEmptyContent}
|
||||
data={shouldRenderComments ? sortedSections : []}
|
||||
onContentSizeChange={_onContentSizeChange}
|
||||
renderItem={_renderItem}
|
||||
keyExtractor={(item) => `${item.author}/${item.permlink}`}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={discussionQuery.isFetching}
|
||||
onRefresh={_onRefresh}
|
||||
progressBackgroundColor="#357CE6"
|
||||
tintColor={!isDarkTheme ? '#357ce6' : '#96c0ff'}
|
||||
titleColor="#fff"
|
||||
colors={['#fff']}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<PostHtmlInteractionHandler
|
||||
ref={postInteractionRef}
|
||||
/>
|
||||
</Fragment>
|
||||
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export default PostComments;
|
||||
|
||||
const _sortComments = (sortOrder = 'trending', _comments) => {
|
||||
const sortedComments: any[] = _comments;
|
||||
|
||||
const absNegative = (a) => a.net_rshares < 0;
|
||||
|
||||
const sortOrders = {
|
||||
trending: (a, b) => {
|
||||
if (a.renderOnTop) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (absNegative(a)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (absNegative(b)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const apayout = a.total_payout;
|
||||
const bpayout = b.total_payout;
|
||||
|
||||
if (apayout !== bpayout) {
|
||||
return bpayout - apayout;
|
||||
}
|
||||
|
||||
return 0;
|
||||
},
|
||||
reputation: (a, b) => {
|
||||
if (a.renderOnTop) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const keyA = a.author_reputation;
|
||||
const keyB = b.author_reputation;
|
||||
|
||||
if (keyA > keyB) {
|
||||
return -1;
|
||||
}
|
||||
if (keyA < keyB) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
},
|
||||
votes: (a, b) => {
|
||||
if (a.renderOnTop){
|
||||
return -1;
|
||||
}
|
||||
|
||||
const keyA = a.active_votes.length;
|
||||
const keyB = b.active_votes.length;
|
||||
|
||||
if (keyA > keyB) {
|
||||
return -1;
|
||||
}
|
||||
if (keyA < keyB) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
},
|
||||
age: (a, b) => {
|
||||
if (a.renderOnTop){
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (absNegative(a)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (absNegative(b)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const keyA = Date.parse(a.created);
|
||||
const keyB = Date.parse(b.created);
|
||||
|
||||
if (keyA > keyB) {
|
||||
return -1;
|
||||
}
|
||||
if (keyA < keyB) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
},
|
||||
};
|
||||
|
||||
sortedComments.sort(sortOrders[sortOrder]);
|
||||
|
||||
return sortedComments;
|
||||
};
|
||||
|
@ -1,4 +0,0 @@
|
||||
import PostDropdownView from './view/postDropdownView';
|
||||
import PostDropdown from './container/postDropdownContainer';
|
||||
|
||||
export { PostDropdown, PostDropdownView };
|
@ -1,39 +0,0 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
|
||||
// Constants
|
||||
// Components
|
||||
import { DropdownButton } from '../../dropdownButton';
|
||||
import styles from './postDropdownStyles';
|
||||
|
||||
class PostDropdownView extends PureComponent {
|
||||
/* Props
|
||||
* ------------------------------------------------
|
||||
* @prop { type } name - Description....
|
||||
*/
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
// Component Life Cycles
|
||||
|
||||
// Component Functions
|
||||
|
||||
render() {
|
||||
const { handleOnDropdownSelect, options } = this.props;
|
||||
|
||||
return (
|
||||
<DropdownButton
|
||||
isHasChildIcon
|
||||
iconName="more-vert"
|
||||
options={options}
|
||||
onSelect={handleOnDropdownSelect}
|
||||
noHighlight
|
||||
iconStyle={styles.icon}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default PostDropdownView;
|
@ -1,29 +1,20 @@
|
||||
import React, { Fragment, useState, useRef } from 'react';
|
||||
import { Linking, Modal, PermissionsAndroid, Platform, View } from 'react-native';
|
||||
import React, { Fragment, useState } from 'react';
|
||||
import { View } from 'react-native';
|
||||
import { useIntl } from 'react-intl';
|
||||
import CameraRoll from '@react-native-community/cameraroll';
|
||||
import RNFetchBlob from 'rn-fetch-blob';
|
||||
import ImageViewer from 'react-native-image-zoom-viewer';
|
||||
import ActionsSheetView from 'react-native-actions-sheet';
|
||||
|
||||
// import AutoHeightWebView from 'react-native-autoheight-webview';
|
||||
import EStyleSheet from 'react-native-extended-stylesheet';
|
||||
import { LongPressGestureHandler, State } from 'react-native-gesture-handler';
|
||||
import RootNavigation from '../../../../navigation/rootNavigation';
|
||||
|
||||
// Constants
|
||||
import { default as ROUTES } from '../../../../constants/routeNames';
|
||||
|
||||
import { PostHtmlRenderer, TextButton, VideoPlayer } from '../../..';
|
||||
import { PostHtmlRenderer, TextButton } from '../../..';
|
||||
|
||||
// Styles
|
||||
import styles from './commentBodyStyles';
|
||||
|
||||
// Services and Actions
|
||||
import { writeToClipboard } from '../../../../utils/clipboard';
|
||||
import { toastNotification } from '../../../../redux/actions/uiAction';
|
||||
|
||||
import { OptionsModal } from '../../../atoms';
|
||||
import { useAppDispatch } from '../../../../hooks';
|
||||
import { isCommunity } from '../../../../utils/communityValidation';
|
||||
import { GLOBAL_POST_FILTERS_VALUE } from '../../../../constants/options/filters';
|
||||
@ -36,7 +27,10 @@ const CommentBody = ({
|
||||
handleOnUserPress,
|
||||
handleOnPostPress,
|
||||
handleOnLongPress,
|
||||
created,
|
||||
handleVideoPress,
|
||||
handleYoutubePress,
|
||||
handleImagePress,
|
||||
handleLinkPress,
|
||||
commentDepth,
|
||||
reputation = 25,
|
||||
isMuted,
|
||||
@ -45,19 +39,9 @@ const CommentBody = ({
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const [isImageModalOpen, setIsImageModalOpen] = useState(false);
|
||||
const [postImages, setPostImages] = useState<string[]>([]);
|
||||
const [selectedImage, setSelectedImage] = useState(null);
|
||||
const [selectedLink, setSelectedLink] = useState(null);
|
||||
const [revealComment, setRevealComment] = useState(reputation > 0 && !isMuted);
|
||||
const [videoUrl, setVideoUrl] = useState(null);
|
||||
const [youtubeVideoId, setYoutubeVideoId] = useState(null);
|
||||
const [videoStartTime, setVideoStartTime] = useState(0);
|
||||
|
||||
const intl = useIntl();
|
||||
const actionImage = useRef(null);
|
||||
const actionLink = useRef(null);
|
||||
const youtubePlayerRef = useRef(null);
|
||||
|
||||
const _onLongPressStateChange = ({ nativeEvent }) => {
|
||||
if (nativeEvent.state === State.ACTIVE) {
|
||||
@ -69,60 +53,6 @@ const CommentBody = ({
|
||||
setRevealComment(true);
|
||||
};
|
||||
|
||||
const handleImagePress = (ind) => {
|
||||
if (ind === 1) {
|
||||
// open gallery mode
|
||||
setIsImageModalOpen(true);
|
||||
}
|
||||
if (ind === 0) {
|
||||
// copy to clipboard
|
||||
writeToClipboard(selectedImage).then(() => {
|
||||
dispatch(
|
||||
toastNotification(
|
||||
intl.formatMessage({
|
||||
id: 'alert.copied',
|
||||
}),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
if (ind === 2) {
|
||||
// save to local
|
||||
_saveImage(selectedImage);
|
||||
}
|
||||
|
||||
setSelectedImage(null);
|
||||
};
|
||||
|
||||
const handleLinkPress = (ind) => {
|
||||
if (ind === 1) {
|
||||
// open link
|
||||
if (selectedLink) {
|
||||
RootNavigation.navigate({
|
||||
name: ROUTES.SCREENS.WEB_BROWSER,
|
||||
params: {
|
||||
url: selectedLink,
|
||||
},
|
||||
key: selectedLink,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (ind === 0) {
|
||||
// copy to clipboard
|
||||
writeToClipboard(selectedLink).then(() => {
|
||||
dispatch(
|
||||
toastNotification(
|
||||
intl.formatMessage({
|
||||
id: 'alert.copied',
|
||||
}),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
setSelectedLink(null);
|
||||
};
|
||||
|
||||
const _handleTagPress = (tag: string, filter: string = GLOBAL_POST_FILTERS_VALUE[0]) => {
|
||||
if (tag) {
|
||||
const name = isCommunity(tag) ? ROUTES.SCREENS.COMMUNITY : ROUTES.SCREENS.TAG_RESULT;
|
||||
@ -138,22 +68,9 @@ const CommentBody = ({
|
||||
}
|
||||
};
|
||||
|
||||
const _handleSetSelectedLink = (link: string) => {
|
||||
setSelectedLink(link);
|
||||
actionLink.current.show();
|
||||
};
|
||||
|
||||
const _handleSetSelectedImage = (imageLink: string, postImgUrls: string[]) => {
|
||||
if (postImages.length !== postImgUrls.length) {
|
||||
setPostImages(postImgUrls);
|
||||
}
|
||||
setSelectedImage(imageLink);
|
||||
actionImage.current.show();
|
||||
};
|
||||
|
||||
const _handleOnPostPress = (permlink, author) => {
|
||||
if (handleOnPostPress) {
|
||||
handleOnUserPress(permlink, author);
|
||||
handleOnPostPress(permlink, author);
|
||||
return;
|
||||
}
|
||||
if (permlink) {
|
||||
@ -192,125 +109,8 @@ const CommentBody = ({
|
||||
}
|
||||
};
|
||||
|
||||
const checkAndroidPermission = async () => {
|
||||
try {
|
||||
const permission = PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE;
|
||||
await PermissionsAndroid.request(permission);
|
||||
Promise.resolve();
|
||||
} catch (error) {
|
||||
Promise.reject(error);
|
||||
}
|
||||
};
|
||||
|
||||
const _downloadImage = async (uri) => {
|
||||
return RNFetchBlob.config({
|
||||
fileCache: true,
|
||||
appendExt: 'jpg',
|
||||
})
|
||||
.fetch('GET', uri)
|
||||
.then((res) => {
|
||||
const { status } = res.info();
|
||||
|
||||
if (status == 200) {
|
||||
return res.path();
|
||||
} else {
|
||||
Promise.reject();
|
||||
}
|
||||
})
|
||||
.catch((errorMessage) => {
|
||||
Promise.reject(errorMessage);
|
||||
});
|
||||
};
|
||||
|
||||
const _saveImage = async (uri) => {
|
||||
try {
|
||||
if (Platform.OS === 'android') {
|
||||
await checkAndroidPermission();
|
||||
uri = `file://${await _downloadImage(uri)}`;
|
||||
}
|
||||
CameraRoll.saveToCameraRoll(uri)
|
||||
.then(() => {
|
||||
dispatch(
|
||||
toastNotification(
|
||||
intl.formatMessage({
|
||||
id: 'post.image_saved',
|
||||
}),
|
||||
),
|
||||
);
|
||||
})
|
||||
.catch(() => {
|
||||
dispatch(
|
||||
toastNotification(
|
||||
intl.formatMessage({
|
||||
id: 'post.image_saved_error',
|
||||
}),
|
||||
),
|
||||
);
|
||||
});
|
||||
} catch (error) {
|
||||
dispatch(
|
||||
toastNotification(
|
||||
intl.formatMessage({
|
||||
id: 'post.image_saved_error',
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const _handleYoutubePress = (videoId, startTime) => {
|
||||
if (videoId && youtubePlayerRef.current) {
|
||||
setYoutubeVideoId(videoId);
|
||||
setVideoStartTime(startTime);
|
||||
youtubePlayerRef.current.setModalVisible(true);
|
||||
}
|
||||
};
|
||||
|
||||
const _handleVideoPress = (embedUrl) => {
|
||||
if (embedUrl && youtubePlayerRef.current) {
|
||||
setVideoUrl(embedUrl);
|
||||
setVideoStartTime(0);
|
||||
youtubePlayerRef.current.setModalVisible(true);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<Modal key={`mkey-${created.toString()}`} visible={isImageModalOpen} transparent={true}>
|
||||
<ImageViewer
|
||||
imageUrls={postImages.map((url) => ({ url }))}
|
||||
enableSwipeDown
|
||||
onCancel={() => setIsImageModalOpen(false)}
|
||||
onClick={() => setIsImageModalOpen(false)}
|
||||
/>
|
||||
</Modal>
|
||||
<OptionsModal
|
||||
ref={actionImage}
|
||||
options={[
|
||||
intl.formatMessage({ id: 'post.copy_link' }),
|
||||
intl.formatMessage({ id: 'post.gallery_mode' }),
|
||||
intl.formatMessage({ id: 'post.save_to_local' }),
|
||||
intl.formatMessage({ id: 'alert.cancel' }),
|
||||
]}
|
||||
title={intl.formatMessage({ id: 'post.image' })}
|
||||
cancelButtonIndex={3}
|
||||
onPress={(index) => {
|
||||
handleImagePress(index);
|
||||
}}
|
||||
/>
|
||||
<OptionsModal
|
||||
ref={actionLink}
|
||||
options={[
|
||||
intl.formatMessage({ id: 'post.copy_link' }),
|
||||
intl.formatMessage({ id: 'alert.external_link' }),
|
||||
intl.formatMessage({ id: 'alert.cancel' }),
|
||||
]}
|
||||
title={intl.formatMessage({ id: 'post.link' })}
|
||||
cancelButtonIndex={2}
|
||||
onPress={(index) => {
|
||||
handleLinkPress(index);
|
||||
}}
|
||||
/>
|
||||
{revealComment ? (
|
||||
<LongPressGestureHandler onHandlerStateChange={_onLongPressStateChange}>
|
||||
<View>
|
||||
@ -318,13 +118,13 @@ const CommentBody = ({
|
||||
contentWidth={_contentWidth}
|
||||
body={body}
|
||||
isComment={true}
|
||||
setSelectedImage={_handleSetSelectedImage}
|
||||
setSelectedLink={_handleSetSelectedLink}
|
||||
setSelectedImage={handleImagePress}
|
||||
setSelectedLink={handleLinkPress}
|
||||
handleOnPostPress={_handleOnPostPress}
|
||||
handleOnUserPress={_handleOnUserPress}
|
||||
handleTagPress={_handleTagPress}
|
||||
handleVideoPress={_handleVideoPress}
|
||||
handleYoutubePress={_handleYoutubePress}
|
||||
handleVideoPress={handleVideoPress}
|
||||
handleYoutubePress={handleYoutubePress}
|
||||
/>
|
||||
</View>
|
||||
</LongPressGestureHandler>
|
||||
@ -336,24 +136,6 @@ const CommentBody = ({
|
||||
text={intl.formatMessage({ id: 'comments.reveal_comment' })}
|
||||
/>
|
||||
)}
|
||||
<ActionsSheetView
|
||||
ref={youtubePlayerRef}
|
||||
gestureEnabled={true}
|
||||
hideUnderlay
|
||||
containerStyle={{ backgroundColor: 'black' }}
|
||||
indicatorColor={EStyleSheet.value('$primaryWhiteLightBackground')}
|
||||
onClose={() => {
|
||||
setYoutubeVideoId(null);
|
||||
setVideoUrl(null);
|
||||
}}
|
||||
>
|
||||
<VideoPlayer
|
||||
mode={youtubeVideoId ? 'youtube' : 'uri'}
|
||||
youtubeVideoId={youtubeVideoId}
|
||||
uri={videoUrl}
|
||||
startTime={videoStartTime}
|
||||
/>
|
||||
</ActionsSheetView>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
@ -1,10 +1,8 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { View, Text, TouchableOpacity } from 'react-native';
|
||||
import { connect } from 'react-redux';
|
||||
import { injectIntl } from 'react-intl';
|
||||
|
||||
// Components
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { Tag } from '../../../basicUIElements';
|
||||
import { Icon } from '../../../icon';
|
||||
import { UserAvatar } from '../../../userAvatar';
|
||||
@ -13,7 +11,7 @@ import styles from './postHeaderDescriptionStyles';
|
||||
|
||||
import { default as ROUTES } from '../../../../constants/routeNames';
|
||||
import { IconButton } from '../../..';
|
||||
import { showProfileModal } from '../../../../redux/actions/uiAction';
|
||||
import RootNavigation from '../../../../navigation/rootNavigation';
|
||||
|
||||
// Constants
|
||||
const DEFAULT_IMAGE = require('../../../../assets/ecency.png');
|
||||
@ -23,49 +21,53 @@ class PostHeaderDescription extends PureComponent {
|
||||
|
||||
// Component Functions
|
||||
_handleOnUserPress = (username) => {
|
||||
const { profileOnPress, dispatch } = this.props;
|
||||
const { profileOnPress } = this.props;
|
||||
|
||||
if (profileOnPress) {
|
||||
profileOnPress(username);
|
||||
} else {
|
||||
dispatch(showProfileModal(username));
|
||||
}
|
||||
};
|
||||
|
||||
_handleOnTagPress = (content) => {
|
||||
const { navigation } = this.props;
|
||||
|
||||
const { handleTagPress } = this.props;
|
||||
let navParams = {};
|
||||
if (content && content.category && /hive-[1-3]\d{4,6}$/.test(content.category)) {
|
||||
navigation.navigate({
|
||||
navParams = {
|
||||
name: ROUTES.SCREENS.COMMUNITY,
|
||||
params: {
|
||||
tag: content.category,
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
if (content && content.category && !/hive-[1-3]\d{4,6}$/.test(content.category)) {
|
||||
navigation.navigate({
|
||||
navParams = {
|
||||
name: ROUTES.SCREENS.TAG_RESULT,
|
||||
params: {
|
||||
tag: content.category,
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
if (content && typeof content === 'string' && /hive-[1-3]\d{4,6}$/.test(content)) {
|
||||
navigation.navigate({
|
||||
navParams = {
|
||||
name: ROUTES.SCREENS.COMMUNITY,
|
||||
params: {
|
||||
tag: content,
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
if (content && typeof content === 'string' && !/hive-[1-3]\d{4,6}$/.test(content)) {
|
||||
navigation.navigate({
|
||||
navParams = {
|
||||
name: ROUTES.SCREENS.TAG_RESULT,
|
||||
params: {
|
||||
tag: content,
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
if (handleTagPress) {
|
||||
handleTagPress(navParams);
|
||||
} else {
|
||||
RootNavigation.navigate(navParams);
|
||||
}
|
||||
};
|
||||
|
||||
@ -177,12 +179,4 @@ class PostHeaderDescription extends PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = () => ({});
|
||||
|
||||
const mapHookToProps = () => ({
|
||||
navigation: useNavigation(),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(
|
||||
injectIntl((props) => <PostHeaderDescription {...props} {...mapHookToProps()} />),
|
||||
);
|
||||
export default injectIntl(PostHeaderDescription);
|
||||
|
@ -1 +1,2 @@
|
||||
export * from './postHtmlRenderer';
|
||||
export * from './postInteractionHandler';
|
||||
|
265
src/components/postHtmlRenderer/postInteractionHandler.tsx
Normal file
265
src/components/postHtmlRenderer/postInteractionHandler.tsx
Normal file
@ -0,0 +1,265 @@
|
||||
import React, {
|
||||
forwardRef,
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
useState,
|
||||
Fragment,
|
||||
} from 'react';
|
||||
import { Modal, PermissionsAndroid, Platform } from 'react-native';
|
||||
import { useIntl } from 'react-intl';
|
||||
import ActionsSheet from 'react-native-actions-sheet';
|
||||
import ImageViewer from 'react-native-image-zoom-viewer';
|
||||
|
||||
// Components
|
||||
import EStyleSheet from 'react-native-extended-stylesheet';
|
||||
import ROUTES from '../../constants/routeNames';
|
||||
import { toastNotification } from '../../redux/actions/uiAction';
|
||||
import { writeToClipboard } from '../../utils/clipboard';
|
||||
|
||||
import CameraRoll from '@react-native-community/cameraroll';
|
||||
import RNFetchBlob from 'rn-fetch-blob';
|
||||
import { OptionsModal } from '../atoms';
|
||||
import VideoPlayer from '../videoPlayer/videoPlayerView';
|
||||
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
|
||||
|
||||
export const PostHtmlInteractionHandler = forwardRef(({ }, ref) => {
|
||||
|
||||
const navigation = useNavigation();
|
||||
const dispatch = useDispatch();
|
||||
const intl = useIntl();
|
||||
|
||||
const actionImage = useRef(null);
|
||||
const actionLink = useRef(null);
|
||||
const youtubePlayerRef = useRef(null);
|
||||
|
||||
const [postImages, setPostImages] = useState<string[]>([]);
|
||||
const [isImageModalOpen, setIsImageModalOpen] = useState(false);
|
||||
|
||||
const [videoUrl, setVideoUrl] = useState(null);
|
||||
const [youtubeVideoId, setYoutubeVideoId] = useState(null);
|
||||
const [videoStartTime, setVideoStartTime] = useState(0);
|
||||
const [selectedImage, setSelectedImage] = useState(null);
|
||||
const [selectedLink, setSelectedLink] = useState(null);
|
||||
|
||||
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleImagePress: (url: string, postImgUrls: string[]) => {
|
||||
setPostImages(postImgUrls);
|
||||
setSelectedImage(url);
|
||||
actionImage.current?.show();
|
||||
},
|
||||
handleLinkPress: (url: string) => {
|
||||
setSelectedLink(url);
|
||||
actionLink.current?.show();
|
||||
},
|
||||
handleYoutubePress: (videoId, startTime) => {
|
||||
if (videoId && youtubePlayerRef.current) {
|
||||
setYoutubeVideoId(videoId);
|
||||
setVideoStartTime(startTime);
|
||||
youtubePlayerRef.current.setModalVisible(true);
|
||||
}
|
||||
},
|
||||
|
||||
handleVideoPress: (embedUrl) => {
|
||||
if (embedUrl && youtubePlayerRef.current) {
|
||||
setVideoUrl(embedUrl);
|
||||
setVideoStartTime(0);
|
||||
youtubePlayerRef.current.setModalVisible(true);
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
|
||||
|
||||
const checkAndroidPermission = async () => {
|
||||
try {
|
||||
const permission = PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE;
|
||||
await PermissionsAndroid.request(permission);
|
||||
Promise.resolve();
|
||||
} catch (error) {
|
||||
Promise.reject(error);
|
||||
}
|
||||
};
|
||||
|
||||
const _downloadImage = async (uri) => {
|
||||
return RNFetchBlob.config({
|
||||
fileCache: true,
|
||||
appendExt: 'jpg',
|
||||
})
|
||||
.fetch('GET', uri)
|
||||
.then((res) => {
|
||||
const { status } = res.info();
|
||||
|
||||
if (status == 200) {
|
||||
return res.path();
|
||||
} else {
|
||||
Promise.reject();
|
||||
}
|
||||
})
|
||||
.catch((errorMessage) => {
|
||||
Promise.reject(errorMessage);
|
||||
});
|
||||
};
|
||||
|
||||
const _saveImage = async (uri) => {
|
||||
try {
|
||||
if (Platform.OS === 'android') {
|
||||
await checkAndroidPermission();
|
||||
uri = `file://${await _downloadImage(uri)}`;
|
||||
}
|
||||
CameraRoll.saveToCameraRoll(uri)
|
||||
.then(() => {
|
||||
dispatch(
|
||||
toastNotification(
|
||||
intl.formatMessage({
|
||||
id: 'post.image_saved',
|
||||
}),
|
||||
),
|
||||
);
|
||||
})
|
||||
.catch(() => {
|
||||
dispatch(
|
||||
toastNotification(
|
||||
intl.formatMessage({
|
||||
id: 'post.image_saved_error',
|
||||
}),
|
||||
),
|
||||
);
|
||||
});
|
||||
} catch (error) {
|
||||
dispatch(
|
||||
toastNotification(
|
||||
intl.formatMessage({
|
||||
id: 'post.image_saved_error',
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const _handleImageOptionPress = (ind) => {
|
||||
if (ind === 1) {
|
||||
// open gallery mode
|
||||
setIsImageModalOpen(true);
|
||||
}
|
||||
if (ind === 0) {
|
||||
// copy to clipboard
|
||||
writeToClipboard(selectedImage).then(() => {
|
||||
dispatch(
|
||||
toastNotification(
|
||||
intl.formatMessage({
|
||||
id: 'alert.copied',
|
||||
}),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
if (ind === 2) {
|
||||
// save to local
|
||||
_saveImage(selectedImage);
|
||||
}
|
||||
|
||||
setSelectedImage(null);
|
||||
};
|
||||
|
||||
const _handleLinkOptionPress = (ind) => {
|
||||
if (ind === 1) {
|
||||
// open link
|
||||
if (selectedLink) {
|
||||
navigation.navigate({
|
||||
name: ROUTES.SCREENS.WEB_BROWSER,
|
||||
params: {
|
||||
url: selectedLink,
|
||||
},
|
||||
key: selectedLink,
|
||||
} as never);
|
||||
}
|
||||
}
|
||||
if (ind === 0) {
|
||||
// copy to clipboard
|
||||
writeToClipboard(selectedLink).then(() => {
|
||||
dispatch(
|
||||
toastNotification(
|
||||
intl.formatMessage({
|
||||
id: 'alert.copied',
|
||||
}),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
setSelectedLink(null);
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<Modal visible={isImageModalOpen} transparent={true}>
|
||||
<ImageViewer
|
||||
imageUrls={postImages.map((url) => ({ url }))}
|
||||
enableSwipeDown
|
||||
onCancel={() => setIsImageModalOpen(false)}
|
||||
onClick={() => setIsImageModalOpen(false)}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
|
||||
<OptionsModal
|
||||
ref={actionImage}
|
||||
options={[
|
||||
intl.formatMessage({ id: 'post.copy_link' }),
|
||||
intl.formatMessage({ id: 'post.gallery_mode' }),
|
||||
intl.formatMessage({ id: 'post.save_to_local' }),
|
||||
intl.formatMessage({ id: 'alert.cancel' }),
|
||||
]}
|
||||
title={intl.formatMessage({ id: 'post.image' })}
|
||||
cancelButtonIndex={3}
|
||||
onPress={(index) => {
|
||||
_handleImageOptionPress(index);
|
||||
}}
|
||||
/>
|
||||
|
||||
|
||||
<OptionsModal
|
||||
ref={actionLink}
|
||||
options={[
|
||||
intl.formatMessage({ id: 'post.copy_link' }),
|
||||
intl.formatMessage({ id: 'alert.external_link' }),
|
||||
intl.formatMessage({ id: 'alert.cancel' }),
|
||||
]}
|
||||
title={intl.formatMessage({ id: 'post.link' })}
|
||||
cancelButtonIndex={2}
|
||||
onPress={(index) => {
|
||||
_handleLinkOptionPress(index);
|
||||
}}
|
||||
/>
|
||||
|
||||
<ActionsSheet
|
||||
ref={youtubePlayerRef}
|
||||
gestureEnabled={true}
|
||||
hideUnderlay={true}
|
||||
containerStyle={{ backgroundColor: 'black' }}
|
||||
indicatorColor={EStyleSheet.value('$primaryWhiteLightBackground')}
|
||||
onClose={() => {
|
||||
setYoutubeVideoId(null);
|
||||
setVideoUrl(null);
|
||||
}}
|
||||
>
|
||||
<VideoPlayer
|
||||
mode={youtubeVideoId ? 'youtube' : 'uri'}
|
||||
youtubeVideoId={youtubeVideoId}
|
||||
uri={videoUrl}
|
||||
startTime={videoStartTime}
|
||||
/>
|
||||
</ActionsSheet>
|
||||
</Fragment>
|
||||
)
|
||||
})
|
@ -1,7 +1,6 @@
|
||||
import React from 'react';
|
||||
import { View, ImageBackground } from 'react-native';
|
||||
import { View, ImageBackground, TouchableHighlight } from 'react-native';
|
||||
import EStyleSheet from 'react-native-extended-stylesheet';
|
||||
import { TouchableHighlight } from 'react-native-gesture-handler';
|
||||
import { IconButton } from '..';
|
||||
import styles from './postHtmlRendererStyles';
|
||||
|
||||
|
@ -1,11 +1,13 @@
|
||||
import React, { PureComponent, Fragment } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { Alert, Share } from 'react-native';
|
||||
import { injectIntl } from 'react-intl';
|
||||
import React, { useEffect, useState, useRef, forwardRef, useImperativeHandle } from 'react';
|
||||
import { Alert, Share, Text, TouchableHighlight } from 'react-native';
|
||||
import { useIntl } from 'react-intl';
|
||||
import get from 'lodash/get';
|
||||
import EStyleSheet from 'react-native-extended-stylesheet';
|
||||
|
||||
// Services and Actions
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { FlatList } from 'react-native-gesture-handler';
|
||||
import ActionSheet from 'react-native-actions-sheet';
|
||||
import { ignoreUser, pinCommunityPost, profileUpdate, reblog } from '../../../providers/hive/dhive';
|
||||
import { addBookmark, addReport } from '../../../providers/ecency/ecency';
|
||||
import { toastNotification, setRcOffer, showActionModal } from '../../../redux/actions/uiAction';
|
||||
@ -19,12 +21,13 @@ import { writeToClipboard } from '../../../utils/clipboard';
|
||||
import { getPostUrl } from '../../../utils/post';
|
||||
|
||||
// Component
|
||||
import PostDropdownView from '../view/postDropdownView';
|
||||
import { OptionsModal } from '../../atoms';
|
||||
|
||||
import { updateCurrentAccount } from '../../../redux/actions/accountAction';
|
||||
import showLoginAlert from '../../../utils/showLoginAlert';
|
||||
import { useUserActivityMutation } from '../../../providers/queries/pointQueries';
|
||||
import { PointActivityIds } from '../../../providers/ecency/ecency.types';
|
||||
import { useAppDispatch, useAppSelector } from '../../../hooks';
|
||||
import styles from '../styles/postOptionsModal.styles';
|
||||
|
||||
/*
|
||||
* Props Name Description Value
|
||||
@ -32,46 +35,76 @@ import { PointActivityIds } from '../../../providers/ecency/ecency.types';
|
||||
*
|
||||
*/
|
||||
|
||||
class PostDropdownContainer extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
interface Props {
|
||||
pageType?: string;
|
||||
}
|
||||
|
||||
this.state = {
|
||||
options: OPTIONS,
|
||||
const PostOptionsModal = ({ pageType }: Props, ref) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
const navigation = useNavigation();
|
||||
const userActivityMutation = useUserActivityMutation();
|
||||
|
||||
const bottomSheetModalRef = useRef<ActionSheet | null>(null);
|
||||
const alertTimer = useRef<any>(null);
|
||||
const shareTimer = useRef<any>(null);
|
||||
const actionSheetTimer = useRef<any>(null);
|
||||
const reportTimer = useRef<any>(null);
|
||||
|
||||
const isLoggedIn = useAppSelector((state) => state.application.isLoggedIn);
|
||||
const currentAccount = useAppSelector((state) => state.account.currentAccount);
|
||||
const pinCode = useAppSelector((state) => state.application.pin);
|
||||
const isPinCodeOpen = useAppSelector((state) => state.application.isPinCodeOpen);
|
||||
const subscribedCommunities = useAppSelector((state) => state.communities.subscribedCommunities);
|
||||
|
||||
const [content, setContent] = useState<any>(null);
|
||||
const [options, setOptions] = useState(OPTIONS);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
show: (_content) => {
|
||||
if (!_content) {
|
||||
Alert.alert(
|
||||
intl.formatMessage({ id: 'alert.something_wrong' }),
|
||||
'Post content not passed for viewing post options',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (bottomSheetModalRef.current) {
|
||||
setContent(_content);
|
||||
bottomSheetModalRef.current.show();
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
if (content) {
|
||||
_initOptions();
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (alertTimer.current) {
|
||||
clearTimeout(alertTimer.current);
|
||||
alertTimer.current = null;
|
||||
}
|
||||
|
||||
if (shareTimer.current) {
|
||||
clearTimeout(shareTimer.current);
|
||||
shareTimer.current = null;
|
||||
}
|
||||
|
||||
if (actionSheetTimer.current) {
|
||||
clearTimeout(actionSheetTimer.current);
|
||||
actionSheetTimer.current = null;
|
||||
}
|
||||
if (reportTimer.current) {
|
||||
clearTimeout(reportTimer.current);
|
||||
reportTimer.current = null;
|
||||
}
|
||||
};
|
||||
}
|
||||
}, [content]);
|
||||
|
||||
componentDidMount = () => {
|
||||
this._initOptions();
|
||||
};
|
||||
|
||||
UNSAFE_componentWillReceiveProps = (nextProps) => {
|
||||
if (nextProps.content?.permlink !== this.props.content?.permlink) {
|
||||
this._initOptions(nextProps);
|
||||
}
|
||||
};
|
||||
|
||||
// Component Life Cycle Functions
|
||||
componentWillUnmount = () => {
|
||||
if (this.alertTimer) {
|
||||
clearTimeout(this.alertTimer);
|
||||
this.alertTimer = 0;
|
||||
}
|
||||
|
||||
if (this.shareTimer) {
|
||||
clearTimeout(this.shareTimer);
|
||||
this.shareTimer = 0;
|
||||
}
|
||||
|
||||
if (this.actionSheetTimer) {
|
||||
clearTimeout(this.actionSheetTimer);
|
||||
this.actionSheetTimer = 0;
|
||||
}
|
||||
};
|
||||
|
||||
_initOptions = (
|
||||
{ content, currentAccount, pageType, subscribedCommunities, isMuted } = this.props,
|
||||
) => {
|
||||
const _initOptions = () => {
|
||||
// check if post is owned by current user or not, if so pinned or not
|
||||
const _canUpdateBlogPin =
|
||||
!!pageType && !!content && !!currentAccount && currentAccount.name === content.author;
|
||||
@ -90,7 +123,7 @@ class PostDropdownContainer extends PureComponent {
|
||||
const _isPinnedInCommunity = !!content && content.stats?.is_pinned;
|
||||
|
||||
// cook options list based on collected flags
|
||||
const options = OPTIONS.filter((option) => {
|
||||
const _options = OPTIONS.filter((option) => {
|
||||
switch (option) {
|
||||
case 'pin-blog':
|
||||
return _canUpdateBlogPin && !_isPinnedInProfile;
|
||||
@ -105,117 +138,16 @@ class PostDropdownContainer extends PureComponent {
|
||||
}
|
||||
});
|
||||
|
||||
this.setState({ options });
|
||||
setOptions(_options);
|
||||
};
|
||||
|
||||
// Component Functions
|
||||
_handleOnDropdownSelect = async (index) => {
|
||||
const { currentAccount, content, dispatch, intl, navigation, isMuted } = this.props;
|
||||
const username = content.author;
|
||||
const isOwnProfile = !username || currentAccount.username === username;
|
||||
const { options } = this.state;
|
||||
|
||||
switch (options[index]) {
|
||||
case 'copy':
|
||||
await writeToClipboard(getPostUrl(get(content, 'url')));
|
||||
this.alertTimer = setTimeout(() => {
|
||||
dispatch(
|
||||
toastNotification(
|
||||
intl.formatMessage({
|
||||
id: 'alert.copied',
|
||||
}),
|
||||
),
|
||||
);
|
||||
this.alertTimer = 0;
|
||||
}, 300);
|
||||
break;
|
||||
|
||||
case 'reblog':
|
||||
this.actionSheetTimer = setTimeout(() => {
|
||||
dispatch(
|
||||
showActionModal({
|
||||
title: intl.formatMessage({ id: 'post.reblog_alert' }),
|
||||
buttons: [
|
||||
{
|
||||
text: intl.formatMessage({ id: 'alert.cancel' }),
|
||||
onPress: () => {},
|
||||
},
|
||||
{
|
||||
text: 'Reblog',
|
||||
onPress: () => {
|
||||
this._reblog();
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
this.actionSheetTimer = 0;
|
||||
}, 100);
|
||||
break;
|
||||
|
||||
case 'reply':
|
||||
this._redirectToReply();
|
||||
break;
|
||||
|
||||
case 'share':
|
||||
this.shareTimer = setTimeout(() => {
|
||||
this._share();
|
||||
this.shareTimer = 0;
|
||||
}, 500);
|
||||
break;
|
||||
|
||||
case 'bookmarks':
|
||||
this._addToBookmarks();
|
||||
break;
|
||||
|
||||
case 'promote':
|
||||
this._redirectToPromote(ROUTES.SCREENS.REDEEM, 1, 'promote');
|
||||
break;
|
||||
|
||||
case 'boost':
|
||||
this._redirectToPromote(ROUTES.SCREENS.REDEEM, 2, 'boost');
|
||||
break;
|
||||
|
||||
case 'report':
|
||||
this._report(get(content, 'url'));
|
||||
break;
|
||||
case 'pin-blog':
|
||||
this._updatePinnedPost();
|
||||
break;
|
||||
case 'unpin-blog':
|
||||
this._updatePinnedPost({ unpinPost: true });
|
||||
break;
|
||||
case 'pin-community':
|
||||
this._updatePinnedPostCommunity();
|
||||
break;
|
||||
case 'unpin-community':
|
||||
this._updatePinnedPostCommunity({ unpinPost: true });
|
||||
break;
|
||||
case 'edit-history':
|
||||
navigation.navigate({
|
||||
name: ROUTES.SCREENS.EDIT_HISTORY,
|
||||
params: {
|
||||
author: content?.author || '',
|
||||
permlink: content?.permlink || '',
|
||||
},
|
||||
});
|
||||
break;
|
||||
case 'mute':
|
||||
!isOwnProfile && this._muteUser();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
_muteUser = () => {
|
||||
const { currentAccount, pinCode, dispatch, intl, content, isLoggedIn, navigation } = this.props;
|
||||
const _muteUser = () => {
|
||||
const username = content.author;
|
||||
const follower = currentAccount.name;
|
||||
const following = username;
|
||||
|
||||
if (!isLoggedIn) {
|
||||
showLoginAlert({ navigation, intl });
|
||||
showLoginAlert({ intl });
|
||||
return;
|
||||
}
|
||||
ignoreUser(currentAccount, pinCode, {
|
||||
@ -238,16 +170,11 @@ class PostDropdownContainer extends PureComponent {
|
||||
);
|
||||
})
|
||||
.catch((err) => {
|
||||
this._profileActionDone({ error: err });
|
||||
_profileActionDone({ error: err });
|
||||
});
|
||||
};
|
||||
|
||||
_profileActionDone = ({ error = null }) => {
|
||||
const { intl, dispatch, content } = this.props;
|
||||
|
||||
this.setState({
|
||||
isProfileLoading: false,
|
||||
});
|
||||
const _profileActionDone = ({ error = null }: { error: any }) => {
|
||||
if (error) {
|
||||
if (error.jse_shortmsg && error.jse_shortmsg.includes('wait to transact')) {
|
||||
// when RC is not enough, offer boosting account
|
||||
@ -263,8 +190,7 @@ class PostDropdownContainer extends PureComponent {
|
||||
}
|
||||
};
|
||||
|
||||
_share = () => {
|
||||
const { content } = this.props;
|
||||
const _share = () => {
|
||||
const postUrl = getPostUrl(get(content, 'url'));
|
||||
|
||||
Share.share({
|
||||
@ -272,9 +198,7 @@ class PostDropdownContainer extends PureComponent {
|
||||
});
|
||||
};
|
||||
|
||||
_report = (url) => {
|
||||
const { dispatch, intl } = this.props;
|
||||
|
||||
const _report = (url) => {
|
||||
const _onConfirm = () => {
|
||||
addReport('content', url)
|
||||
.then(() => {
|
||||
@ -315,10 +239,9 @@ class PostDropdownContainer extends PureComponent {
|
||||
);
|
||||
};
|
||||
|
||||
_addToBookmarks = () => {
|
||||
const { content, dispatch, intl, isLoggedIn, navigation } = this.props;
|
||||
const _addToBookmarks = () => {
|
||||
if (!isLoggedIn) {
|
||||
showLoginAlert({ navigation, intl });
|
||||
showLoginAlert({ intl });
|
||||
return;
|
||||
}
|
||||
addBookmark(get(content, 'author'), get(content, 'permlink'))
|
||||
@ -342,19 +265,9 @@ class PostDropdownContainer extends PureComponent {
|
||||
});
|
||||
};
|
||||
|
||||
_reblog = () => {
|
||||
const {
|
||||
content,
|
||||
currentAccount,
|
||||
dispatch,
|
||||
intl,
|
||||
isLoggedIn,
|
||||
pinCode,
|
||||
navigation,
|
||||
userActivityMutation,
|
||||
} = this.props;
|
||||
const _reblog = () => {
|
||||
if (!isLoggedIn) {
|
||||
showLoginAlert({ navigation, intl });
|
||||
showLoginAlert({ intl });
|
||||
return;
|
||||
}
|
||||
if (isLoggedIn) {
|
||||
@ -396,9 +309,9 @@ class PostDropdownContainer extends PureComponent {
|
||||
}
|
||||
};
|
||||
|
||||
_updatePinnedPost = async ({ unpinPost }: { unpinPost: boolean } = { unpinPost: false }) => {
|
||||
const { content, currentAccount, pinCode, dispatch, intl } = this.props;
|
||||
|
||||
const _updatePinnedPost = async (
|
||||
{ unpinPost }: { unpinPost: boolean } = { unpinPost: false },
|
||||
) => {
|
||||
const params = {
|
||||
...currentAccount.about.profile,
|
||||
pinned: unpinPost ? null : content.permlink,
|
||||
@ -423,11 +336,9 @@ class PostDropdownContainer extends PureComponent {
|
||||
}
|
||||
};
|
||||
|
||||
_updatePinnedPostCommunity = async (
|
||||
const _updatePinnedPostCommunity = async (
|
||||
{ unpinPost }: { unpinPost: boolean } = { unpinPost: false },
|
||||
) => {
|
||||
const { content, currentAccount, pinCode, dispatch, intl } = this.props;
|
||||
|
||||
try {
|
||||
await pinCommunityPost(
|
||||
currentAccount,
|
||||
@ -449,9 +360,7 @@ class PostDropdownContainer extends PureComponent {
|
||||
}
|
||||
};
|
||||
|
||||
_redirectToReply = () => {
|
||||
const { content, fetchPost, isLoggedIn, navigation } = this.props;
|
||||
|
||||
const _redirectToReply = () => {
|
||||
if (isLoggedIn) {
|
||||
navigation.navigate({
|
||||
name: ROUTES.SCREENS.EDITOR,
|
||||
@ -459,14 +368,12 @@ class PostDropdownContainer extends PureComponent {
|
||||
params: {
|
||||
isReply: true,
|
||||
post: content,
|
||||
fetchPost,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
_redirectToPromote = (name, from, redeemType) => {
|
||||
const { content, isLoggedIn, navigation, isPinCodeOpen } = this.props;
|
||||
const _redirectToPromote = (name, from, redeemType) => {
|
||||
const params = {
|
||||
from,
|
||||
permlink: `${get(content, 'author')}/${get(content, 'permlink')}`,
|
||||
@ -489,47 +396,122 @@ class PostDropdownContainer extends PureComponent {
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
intl,
|
||||
currentAccount: { name },
|
||||
content,
|
||||
isMuted,
|
||||
} = this.props;
|
||||
const { options } = this.state;
|
||||
// Component Functions
|
||||
const _handleOnDropdownSelect = async (index) => {
|
||||
const username = content.author;
|
||||
const isOwnProfile = !username || currentAccount.username === username;
|
||||
|
||||
switch (options[index]) {
|
||||
case 'copy':
|
||||
await writeToClipboard(getPostUrl(get(content, 'url')));
|
||||
alertTimer.current = setTimeout(() => {
|
||||
dispatch(
|
||||
toastNotification(
|
||||
intl.formatMessage({
|
||||
id: 'alert.copied',
|
||||
}),
|
||||
),
|
||||
);
|
||||
alertTimer.current = null;
|
||||
}, 300);
|
||||
break;
|
||||
|
||||
case 'reblog':
|
||||
_reblog();
|
||||
break;
|
||||
|
||||
case 'reply':
|
||||
_redirectToReply();
|
||||
break;
|
||||
|
||||
case 'share':
|
||||
shareTimer.current = setTimeout(() => {
|
||||
_share();
|
||||
shareTimer.current = null;
|
||||
}, 500);
|
||||
break;
|
||||
|
||||
case 'bookmarks':
|
||||
_addToBookmarks();
|
||||
break;
|
||||
|
||||
case 'promote':
|
||||
_redirectToPromote(ROUTES.SCREENS.REDEEM, 1, 'promote');
|
||||
break;
|
||||
|
||||
case 'boost':
|
||||
_redirectToPromote(ROUTES.SCREENS.REDEEM, 2, 'boost');
|
||||
break;
|
||||
|
||||
case 'report':
|
||||
reportTimer.current = setTimeout(() => {
|
||||
_report(get(content, 'url'));
|
||||
}, 300);
|
||||
|
||||
break;
|
||||
case 'pin-blog':
|
||||
_updatePinnedPost();
|
||||
break;
|
||||
case 'unpin-blog':
|
||||
_updatePinnedPost({ unpinPost: true });
|
||||
break;
|
||||
case 'pin-community':
|
||||
_updatePinnedPostCommunity();
|
||||
break;
|
||||
case 'unpin-community':
|
||||
_updatePinnedPostCommunity({ unpinPost: true });
|
||||
break;
|
||||
case 'edit-history':
|
||||
navigation.navigate({
|
||||
name: ROUTES.SCREENS.EDIT_HISTORY,
|
||||
params: {
|
||||
author: content?.author || '',
|
||||
permlink: content?.permlink || '',
|
||||
},
|
||||
});
|
||||
break;
|
||||
case 'mute':
|
||||
!isOwnProfile && _muteUser();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const _renderItem = ({ item, index }: { item: string; index: number }) => {
|
||||
const _onPress = () => {
|
||||
bottomSheetModalRef.current?.hide();
|
||||
_handleOnDropdownSelect(index);
|
||||
};
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<PostDropdownView
|
||||
options={options.map((item) =>
|
||||
intl.formatMessage({ id: `post_dropdown.${item}` }).toUpperCase(),
|
||||
)}
|
||||
handleOnDropdownSelect={this._handleOnDropdownSelect}
|
||||
{...this.props}
|
||||
/>
|
||||
</Fragment>
|
||||
<TouchableHighlight
|
||||
underlayColor={EStyleSheet.value('$primaryLightBackground')}
|
||||
onPress={_onPress}
|
||||
>
|
||||
<Text style={styles.dropdownItem}>
|
||||
{intl.formatMessage({ id: `post_dropdown.${item}` }).toLocaleUpperCase()}
|
||||
</Text>
|
||||
</TouchableHighlight>
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
isLoggedIn: state.application.isLoggedIn,
|
||||
currentAccount: state.account.currentAccount,
|
||||
pinCode: state.application.pin,
|
||||
isPinCodeOpen: state.application.isPinCodeOpen,
|
||||
subscribedCommunities: state.communities.subscribedCommunities,
|
||||
});
|
||||
|
||||
const mapHooksToProps = (props) => {
|
||||
const navigation = useNavigation();
|
||||
const userActivityMutation = useUserActivityMutation();
|
||||
return (
|
||||
<PostDropdownContainer
|
||||
{...props}
|
||||
navigation={navigation}
|
||||
userActivityMutation={userActivityMutation}
|
||||
/>
|
||||
<ActionSheet
|
||||
ref={bottomSheetModalRef}
|
||||
gestureEnabled={true}
|
||||
hideUnderlay={true}
|
||||
containerStyle={styles.sheetContent}
|
||||
indicatorColor={EStyleSheet.value('$iconColor')}
|
||||
>
|
||||
<FlatList
|
||||
contentContainerStyle={styles.listContainer}
|
||||
data={options}
|
||||
renderItem={_renderItem}
|
||||
keyExtractor={(item) => item}
|
||||
/>
|
||||
</ActionSheet>
|
||||
);
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(injectIntl(mapHooksToProps));
|
||||
export default forwardRef(PostOptionsModal);
|
3
src/components/postOptionsModal/index.js
Normal file
3
src/components/postOptionsModal/index.js
Normal file
@ -0,0 +1,3 @@
|
||||
import PostOptionsModal from './container/postOptionsModal';
|
||||
|
||||
export { PostOptionsModal };
|
@ -0,0 +1,23 @@
|
||||
import EStyleSheet from 'react-native-extended-stylesheet';
|
||||
|
||||
export default EStyleSheet.create({
|
||||
icon: {
|
||||
color: '$iconColor',
|
||||
marginRight: 2.7,
|
||||
fontSize: 25,
|
||||
},
|
||||
sheetContent: {
|
||||
backgroundColor: '$modalBackground',
|
||||
},
|
||||
dropdownItem: {
|
||||
paddingHorizontal: 32,
|
||||
paddingVertical: 12,
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
color: '$primaryDarkText',
|
||||
},
|
||||
listContainer: {
|
||||
paddingTop: 16,
|
||||
paddingBottom: 40,
|
||||
},
|
||||
});
|
@ -1,7 +1,6 @@
|
||||
import { View, Text } from 'react-native';
|
||||
import { View, Text, TouchableOpacity } from 'react-native';
|
||||
import React, { forwardRef, useImperativeHandle, useRef } from 'react';
|
||||
import { View as AnimatedView } from 'react-native-animatable';
|
||||
import { TouchableOpacity } from 'react-native-gesture-handler';
|
||||
import { useIntl } from 'react-intl';
|
||||
import UserAvatar from '../../userAvatar';
|
||||
import styles from '../styles/writeCommentButton.styles';
|
||||
|
@ -1,6 +1,5 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { injectIntl } from 'react-intl';
|
||||
import { injectIntl, useIntl } from 'react-intl';
|
||||
import get from 'lodash/get';
|
||||
|
||||
// Action
|
||||
@ -16,25 +15,27 @@ import { default as ROUTES } from '../../../constants/routeNames';
|
||||
|
||||
// Component
|
||||
import PostDisplayView from '../view/postDisplayView';
|
||||
import { useAppDispatch, useAppSelector } from '../../../hooks';
|
||||
|
||||
const PostDisplayContainer = ({
|
||||
post,
|
||||
fetchPost,
|
||||
isFetchPost,
|
||||
isFetchComments,
|
||||
currentAccount,
|
||||
pinCode,
|
||||
dispatch,
|
||||
intl,
|
||||
isLoggedIn,
|
||||
isNewPost,
|
||||
parentPost,
|
||||
isPostUnavailable,
|
||||
author,
|
||||
permlink,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
const navigation = useNavigation();
|
||||
|
||||
const currentAccount = useAppSelector(state => state.account.currentAccount);
|
||||
const isLoggedIn = useAppSelector(state => state.application.isLoggedIn);
|
||||
const pinCode = useAppSelector(state => state.application.pin);
|
||||
|
||||
const [activeVotes, setActiveVotes] = useState([]);
|
||||
const [activeVotesCount, setActiveVotesCount] = useState(0);
|
||||
const [reblogs, setReblogs] = useState([]);
|
||||
@ -65,7 +66,7 @@ const PostDisplayContainer = ({
|
||||
},
|
||||
// TODO: make unic
|
||||
key: post.permlink + activeVotes.length,
|
||||
});
|
||||
} as never);
|
||||
};
|
||||
|
||||
const _handleOnReblogsPress = () => {
|
||||
@ -76,7 +77,7 @@ const PostDisplayContainer = ({
|
||||
reblogs,
|
||||
},
|
||||
key: post.permlink + reblogs.length,
|
||||
});
|
||||
} as never);
|
||||
}
|
||||
};
|
||||
|
||||
@ -89,7 +90,7 @@ const PostDisplayContainer = ({
|
||||
post,
|
||||
fetchPost: _fetchPost,
|
||||
},
|
||||
});
|
||||
} as never);
|
||||
};
|
||||
|
||||
const _handleOnEditPress = () => {
|
||||
@ -105,7 +106,7 @@ const PostDisplayContainer = ({
|
||||
post,
|
||||
fetchPost: _fetchPost,
|
||||
},
|
||||
});
|
||||
} as never);
|
||||
}
|
||||
};
|
||||
|
||||
@ -122,12 +123,15 @@ const PostDisplayContainer = ({
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
|
||||
const _fetchPost = async () => {
|
||||
if (post) {
|
||||
fetchPost(post.author, post.permlink);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<PostDisplayView
|
||||
author={author}
|
||||
@ -147,14 +151,10 @@ const PostDisplayContainer = ({
|
||||
handleOnReplyPress={_handleOnReplyPress}
|
||||
handleOnVotersPress={_handleOnVotersPress}
|
||||
handleOnReblogsPress={_handleOnReblogsPress}
|
||||
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
currentAccount: state.account.currentAccount,
|
||||
pinCode: state.application.pin,
|
||||
isLoggedIn: state.application.isLoggedIn,
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(injectIntl(PostDisplayContainer));
|
||||
export default injectIntl(PostDisplayContainer);
|
@ -12,7 +12,6 @@ import { getTimeFromNow } from '../../../utils/time';
|
||||
// Components
|
||||
import { PostHeaderDescription, PostBody, Tags } from '../../postElements';
|
||||
import { PostPlaceHolder, StickyBar, TextWithIcon, NoPost } from '../../basicUIElements';
|
||||
import { Upvote } from '../../upvote';
|
||||
import { IconButton } from '../../iconButton';
|
||||
import { ParentPost } from '../../parentPost';
|
||||
|
||||
@ -21,12 +20,14 @@ import styles from './postDisplayStyles';
|
||||
import { OptionsModal } from '../../atoms';
|
||||
import getWindowDimensions from '../../../utils/getWindowDimensions';
|
||||
import { useAppDispatch } from '../../../hooks';
|
||||
import { showReplyModal } from '../../../redux/actions/uiAction';
|
||||
import postTypes from '../../../constants/postTypes';
|
||||
import { showProfileModal, showReplyModal } from '../../../redux/actions/uiAction';
|
||||
import { PostTypes } from '../../../constants/postTypes';
|
||||
import { useUserActivityMutation } from '../../../providers/queries/pointQueries';
|
||||
import { PointActivityIds } from '../../../providers/ecency/ecency.types';
|
||||
import { WriteCommentButton } from '../children/writeCommentButton';
|
||||
import { PostComments } from '../../postComments';
|
||||
import { UpvoteButton } from '../../postCard/children/upvoteButton';
|
||||
import UpvotePopover from '../../upvotePopover';
|
||||
|
||||
const HEIGHT = getWindowDimensions().height;
|
||||
const WIDTH = getWindowDimensions().width;
|
||||
@ -56,6 +57,7 @@ const PostDisplayView = ({
|
||||
|
||||
const writeCommentRef = useRef<WriteCommentButton>();
|
||||
const postCommentsRef = useRef<PostComments>(null);
|
||||
const upvotePopoverRef = useRef<UpvotePopover>(null);
|
||||
|
||||
const [cacheVoteIcrement, setCacheVoteIcrement] = useState(0);
|
||||
const [isLoadedComments, setIsLoadedComments] = useState(false);
|
||||
@ -103,21 +105,40 @@ const PostDisplayView = ({
|
||||
}
|
||||
};
|
||||
|
||||
const _handleCacheVoteIncrement = () => {
|
||||
setCacheVoteIcrement(1);
|
||||
const _onUpvotePress = ({
|
||||
anchorRect,
|
||||
content,
|
||||
onVotingStart,
|
||||
showPayoutDetails = false,
|
||||
postType = parentPost ? PostTypes.COMMENT : PostTypes.POST,
|
||||
}: any) => {
|
||||
if (upvotePopoverRef.current) {
|
||||
upvotePopoverRef.current.showPopover({
|
||||
anchorRect,
|
||||
content,
|
||||
showPayoutDetails,
|
||||
postType,
|
||||
onVotingStart,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const _renderActionPanel = (isFixedFooter = false) => {
|
||||
return (
|
||||
<StickyBar isFixedFooter={isFixedFooter} style={styles.stickyBar}>
|
||||
<View style={[styles.stickyWrapper, { paddingBottom: insets.bottom ? insets.bottom : 8 }]}>
|
||||
<Upvote
|
||||
<UpvoteButton
|
||||
activeVotes={activeVotes}
|
||||
isShowPayoutValue
|
||||
isShowPayoutValue={true}
|
||||
content={post}
|
||||
handleCacheVoteIncrement={_handleCacheVoteIncrement}
|
||||
parentType={parentPost ? postTypes.COMMENT : postTypes.POST}
|
||||
parentType={parentPost ? PostTypes.COMMENT : PostTypes.POST}
|
||||
boldPayout={true}
|
||||
onUpvotePress={(anchorRect, onVotingStart) => {
|
||||
_onUpvotePress({ anchorRect, content: post, onVotingStart });
|
||||
}}
|
||||
onPayoutDetailsPress={(anchorRect) => {
|
||||
_onUpvotePress({ anchorRect, content: post, showPayoutDetails: true });
|
||||
}}
|
||||
/>
|
||||
<TextWithIcon
|
||||
iconName="heart-outline"
|
||||
@ -215,6 +236,13 @@ const PostDisplayView = ({
|
||||
}
|
||||
};
|
||||
|
||||
// show quick reply modal
|
||||
const _showQuickProfileModal = (username) => {
|
||||
if (username) {
|
||||
dispatch(showProfileModal(username));
|
||||
}
|
||||
};
|
||||
|
||||
const _handleOnCommentsLoaded = () => {
|
||||
setIsLoadedComments(true);
|
||||
};
|
||||
@ -241,6 +269,7 @@ const PostDisplayView = ({
|
||||
size={40}
|
||||
inlineTime={true}
|
||||
customStyle={styles.headerLine}
|
||||
profileOnPress={_showQuickProfileModal}
|
||||
/>
|
||||
<PostBody body={post.body} onLoadEnd={_handleOnPostBodyLoad} />
|
||||
{!postBodyLoading && (
|
||||
@ -277,6 +306,7 @@ const PostDisplayView = ({
|
||||
isLoading={postBodyLoading}
|
||||
postContentView={_postContentView}
|
||||
onRefresh={onRefresh}
|
||||
onUpvotePress={_onUpvotePress}
|
||||
/>
|
||||
</View>
|
||||
{post && _renderActionPanel(true)}
|
||||
@ -291,6 +321,7 @@ const PostDisplayView = ({
|
||||
cancelButtonIndex={1}
|
||||
onPress={(index) => (index === 0 ? handleOnRemovePress(get(post, 'permlink')) : null)}
|
||||
/>
|
||||
<UpvotePopover ref={upvotePopoverRef} />
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
@ -1,851 +0,0 @@
|
||||
import React, { useState, useEffect, useRef, useReducer } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { get, isEmpty } from 'lodash';
|
||||
import unionBy from 'lodash/unionBy';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { Alert, AppState } from 'react-native';
|
||||
|
||||
// HIVE
|
||||
import {
|
||||
getAccountPosts,
|
||||
getPost,
|
||||
getRankedPosts,
|
||||
getCommunity,
|
||||
} from '../../../providers/hive/dhive';
|
||||
import { getPromotePosts } from '../../../providers/ecency/ecency';
|
||||
|
||||
// Component
|
||||
import PostsView from '../view/postsView';
|
||||
|
||||
// Actions
|
||||
import {
|
||||
setFeedPosts,
|
||||
filterSelected,
|
||||
setOtherPosts,
|
||||
setInitPosts,
|
||||
} from '../../../redux/actions/postsAction';
|
||||
import { fetchLeaderboard, followUser, unfollowUser } from '../../../redux/actions/userAction';
|
||||
import {
|
||||
subscribeCommunity,
|
||||
leaveCommunity,
|
||||
fetchCommunities,
|
||||
} from '../../../redux/actions/communitiesAction';
|
||||
|
||||
import useIsMountedRef from '../../../customHooks/useIsMountedRef';
|
||||
import { setHidePostsThumbnails } from '../../../redux/actions/applicationActions';
|
||||
|
||||
const PostsContainer = ({
|
||||
changeForceLoadPostState,
|
||||
filterOptions,
|
||||
forceLoadPost,
|
||||
getFor,
|
||||
handleOnScroll,
|
||||
pageType,
|
||||
selectedOptionIndex,
|
||||
tag,
|
||||
filterOptionsValue,
|
||||
feedUsername,
|
||||
feedSubfilterOptions,
|
||||
feedSubfilterOptionsValue,
|
||||
isFeedScreen = false,
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
const intl = useIntl();
|
||||
let _postFetchTimer = null;
|
||||
|
||||
const appState = useRef(AppState.currentState);
|
||||
const appStateSubRef = useRef(null);
|
||||
|
||||
const nsfw = useSelector((state) => state.application.nsfw);
|
||||
const initPosts = useSelector((state) => state.posts.initPosts);
|
||||
const isConnected = useSelector((state) => state.application.isConnected);
|
||||
const isHideImages = useSelector((state) => state.application.hidePostsThumbnails);
|
||||
const username = useSelector((state) => state.account.currentAccount.name);
|
||||
const isLoggedIn = useSelector((state) => state.application.isLoggedIn);
|
||||
const currentAccount = useSelector((state) => state.account.currentAccount);
|
||||
const pinCode = useSelector((state) => state.application.pin);
|
||||
const leaderboard = useSelector((state) => state.user.leaderboard);
|
||||
const communities = useSelector((state) => state.communities.communities);
|
||||
const followingUsers = useSelector((state) => state.user.followingUsersInFeedScreen);
|
||||
const subscribingCommunities = useSelector(
|
||||
(state) => state.communities.subscribingCommunitiesInFeedScreen,
|
||||
);
|
||||
|
||||
const [isNoPost, setIsNoPost] = useState(false);
|
||||
const [sessionUser, setSessionUser] = useState(username);
|
||||
const [promotedPosts, setPromotedPosts] = useState([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [selectedFilterIndex, setSelectedFilterIndex] = useState(selectedOptionIndex || 0);
|
||||
const [selectedFilterValue, setSelectedFilterValue] = useState(
|
||||
filterOptionsValue && filterOptionsValue[selectedFilterIndex],
|
||||
);
|
||||
const [selectedFeedSubfilterIndex, setSelectedFeedSubfilterIndex] = useState(0);
|
||||
const [selectedFeedSubfilterValue, setSelectedFeedSubfilterValue] = useState(
|
||||
feedSubfilterOptionsValue && feedSubfilterOptions[selectedFeedSubfilterIndex],
|
||||
);
|
||||
const [recommendedUsers, setRecommendedUsers] = useState([]);
|
||||
const [recommendedCommunities, setRecommendedCommunities] = useState([]);
|
||||
const [newPostsPopupPictures, setNewPostsPopupPictures] = useState(null);
|
||||
|
||||
const _setFeedPosts = (_posts, scrollPos = 0) => {
|
||||
if (isFeedScreen) {
|
||||
dispatch(setFeedPosts(_posts, scrollPos));
|
||||
} else {
|
||||
dispatch(setOtherPosts(_posts, scrollPos));
|
||||
}
|
||||
};
|
||||
|
||||
const _setInitPosts = (_posts) => {
|
||||
if (isFeedScreen) {
|
||||
dispatch(setInitPosts(_posts));
|
||||
}
|
||||
};
|
||||
|
||||
const _scheduleLatestPostsCheck = (firstPost) => {
|
||||
const refetchTime = __DEV__ ? 50000 : 600000;
|
||||
if (_postFetchTimer) {
|
||||
clearTimeout(_postFetchTimer);
|
||||
}
|
||||
if (!firstPost) {
|
||||
return;
|
||||
}
|
||||
|
||||
// schedules refresh 30 minutes after last post creation time
|
||||
const currentTime = new Date().getTime();
|
||||
const createdAt = new Date(get(firstPost, 'created')).getTime();
|
||||
|
||||
const timeSpent = currentTime - createdAt;
|
||||
let timeLeft = refetchTime - timeSpent;
|
||||
if (timeLeft < 0) {
|
||||
timeLeft = refetchTime;
|
||||
}
|
||||
|
||||
_postFetchTimer = setTimeout(() => {
|
||||
const isLatestPostsCheck = true;
|
||||
_loadPosts(null, isLatestPostsCheck);
|
||||
}, timeLeft);
|
||||
};
|
||||
|
||||
const initCacheState = () => {
|
||||
const cachedData = {};
|
||||
|
||||
filterOptionsValue.forEach((option) => {
|
||||
if (option !== 'feed') {
|
||||
cachedData[option] = {
|
||||
posts: [],
|
||||
startAuthor: '',
|
||||
startPermlink: '',
|
||||
isLoading: false,
|
||||
scrollPosition: 0,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
if (feedSubfilterOptions) {
|
||||
feedSubfilterOptions.forEach((option) => {
|
||||
cachedData[option] = {
|
||||
posts: [],
|
||||
startAuthor: '',
|
||||
startPermlink: '',
|
||||
isLoading: false,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
isFeedScreen,
|
||||
currentFilter: selectedFilterValue,
|
||||
currentSubFilter: selectedFeedSubfilterValue,
|
||||
cachedData,
|
||||
};
|
||||
};
|
||||
|
||||
const cacheReducer = (state, action) => {
|
||||
console.log('reducer action:', action);
|
||||
|
||||
switch (action.type) {
|
||||
case 'is-filter-loading': {
|
||||
const { filter } = action.payload;
|
||||
const loading = action.payload.isLoading;
|
||||
state.cachedData[filter].isLoading = loading;
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
case 'update-filter-cache': {
|
||||
const { filter } = action.payload;
|
||||
const nextPosts = action.payload.posts;
|
||||
const { shouldReset } = action.payload;
|
||||
let _posts = nextPosts;
|
||||
|
||||
const cachedEntry = state.cachedData[filter];
|
||||
if (!cachedEntry) {
|
||||
throw new Error('No cached entry available');
|
||||
}
|
||||
|
||||
const prevPosts = cachedEntry.posts;
|
||||
|
||||
if (prevPosts.length > 0 && !shouldReset) {
|
||||
if (refreshing) {
|
||||
_posts = unionBy(_posts, prevPosts, 'permlink');
|
||||
} else {
|
||||
_posts = unionBy(prevPosts, _posts, 'permlink');
|
||||
}
|
||||
}
|
||||
// cache latest posts for main tab for returning user
|
||||
else if (isFeedScreen) {
|
||||
// schedule refetch of new posts by checking time of current post
|
||||
_scheduleLatestPostsCheck(nextPosts[0]);
|
||||
|
||||
if (filter == (get(currentAccount, 'name', null) == null ? 'hot' : 'friends')) {
|
||||
_setInitPosts(nextPosts);
|
||||
}
|
||||
}
|
||||
|
||||
// update stat
|
||||
cachedEntry.startAuthor = _posts[_posts.length - 1] && _posts[_posts.length - 1].author;
|
||||
cachedEntry.startPermlink = _posts[_posts.length - 1] && _posts[_posts.length - 1].permlink;
|
||||
cachedEntry.posts = _posts;
|
||||
|
||||
state.cachedData[filter] = cachedEntry;
|
||||
|
||||
// dispatch to redux
|
||||
if (
|
||||
filter === (state.currentFilter !== 'feed' ? state.currentFilter : state.currentSubFilter)
|
||||
) {
|
||||
_setFeedPosts(_posts);
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
case 'reset-cur-filter-cache': {
|
||||
const filter = state.currentFilter == 'feed' ? state.currentSubFilter : state.currentFilter;
|
||||
const cachedEntry = state.cachedData[filter];
|
||||
if (!cachedEntry) {
|
||||
throw new Error('No cached entry available');
|
||||
}
|
||||
cachedEntry.startAuthor = '';
|
||||
cachedEntry.startPermlink = '';
|
||||
cachedEntry.posts = [];
|
||||
|
||||
state.cachedData[filter] = cachedEntry;
|
||||
|
||||
// dispatch to redux
|
||||
_setFeedPosts([]);
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
case 'change-filter': {
|
||||
const filter = action.payload.currentFilter;
|
||||
state.currentFilter = filter;
|
||||
|
||||
const data = state.cachedData[filter !== 'feed' ? filter : state.currentSubFilter];
|
||||
_setFeedPosts(data.posts, data.scrollPosition);
|
||||
|
||||
if (filter !== 'feed' && isFeedScreen) {
|
||||
_scheduleLatestPostsCheck(data.posts[0]);
|
||||
setNewPostsPopupPictures(null);
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
case 'change-sub-filter': {
|
||||
const filter = action.payload.currentSubFilter;
|
||||
state.currentSubFilter = filter;
|
||||
|
||||
// dispatch to redux;
|
||||
const data = state.cachedData[filter];
|
||||
_setFeedPosts(data.posts, data.scrollPosition);
|
||||
if (isFeedScreen) {
|
||||
_scheduleLatestPostsCheck(data.posts[0]);
|
||||
setNewPostsPopupPictures(null);
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
case 'scroll-position-change': {
|
||||
const scrollPosition = action.payload.scrollPosition || 0;
|
||||
const filter = state.currentFilter;
|
||||
const subFilter = state.currentSubFilter;
|
||||
|
||||
const cacheFilter = filter !== 'feed' ? filter : subFilter;
|
||||
|
||||
state.cachedData[cacheFilter].scrollPosition = scrollPosition;
|
||||
return state;
|
||||
}
|
||||
|
||||
case 'reset-cache': {
|
||||
// dispatch to redux
|
||||
_setFeedPosts([]);
|
||||
_setInitPosts([]);
|
||||
|
||||
return initCacheState();
|
||||
}
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
const [cache, cacheDispatch] = useReducer(cacheReducer, {}, initCacheState);
|
||||
|
||||
const elem = useRef(null);
|
||||
const isMountedRef = useIsMountedRef();
|
||||
|
||||
useEffect(() => {
|
||||
let appStateSub;
|
||||
if (isFeedScreen) {
|
||||
appStateSub = AppState.addEventListener('change', _handleAppStateChange);
|
||||
_setFeedPosts(initPosts || []);
|
||||
} else {
|
||||
_setFeedPosts([]);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (_postFetchTimer) {
|
||||
clearTimeout(_postFetchTimer);
|
||||
}
|
||||
if (isFeedScreen && appStateSub) {
|
||||
appStateSub.remove();
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isConnected) {
|
||||
if (username !== sessionUser) {
|
||||
cacheDispatch({
|
||||
type: 'reset-cache',
|
||||
});
|
||||
setSessionUser(username);
|
||||
}
|
||||
|
||||
_loadPosts();
|
||||
_getPromotePosts();
|
||||
}
|
||||
}, [
|
||||
_getPromotePosts,
|
||||
_loadPosts,
|
||||
changeForceLoadPostState,
|
||||
username,
|
||||
forceLoadPost,
|
||||
isConnected,
|
||||
pageType,
|
||||
selectedOptionIndex,
|
||||
selectedFeedSubfilterIndex,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (forceLoadPost) {
|
||||
cacheDispatch({
|
||||
type: 'reset-cur-filter-cache',
|
||||
});
|
||||
|
||||
setSelectedFilterIndex(selectedOptionIndex || 0);
|
||||
isLoggedIn && setSelectedFeedSubfilterIndex(selectedFeedSubfilterIndex || 0);
|
||||
setIsNoPost(false);
|
||||
|
||||
setNewPostsPopupPictures(null);
|
||||
_loadPosts();
|
||||
|
||||
if (changeForceLoadPostState) {
|
||||
changeForceLoadPostState(false);
|
||||
}
|
||||
}
|
||||
}, [
|
||||
_loadPosts,
|
||||
changeForceLoadPostState,
|
||||
username,
|
||||
feedUsername,
|
||||
forceLoadPost,
|
||||
selectedOptionIndex,
|
||||
selectedFeedSubfilterIndex,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const filter = selectedFilterValue == 'feed' ? selectedFeedSubfilterValue : selectedFilterValue;
|
||||
const sAuthor = cache.cachedData[filter].startAuthor;
|
||||
const sPermlink = cache.cachedData[filter].startPermlink;
|
||||
|
||||
if (!sAuthor && !sPermlink) {
|
||||
_loadPosts(selectedFilterValue);
|
||||
}
|
||||
}, [_loadPosts, selectedFilterValue]);
|
||||
|
||||
useEffect(() => {
|
||||
if (refreshing) {
|
||||
cacheDispatch({
|
||||
type: 'scroll-position-change',
|
||||
payload: {
|
||||
scrollPosition: 0,
|
||||
},
|
||||
});
|
||||
setNewPostsPopupPictures(null);
|
||||
_loadPosts();
|
||||
}
|
||||
}, [refreshing]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!leaderboard.loading) {
|
||||
if (!leaderboard.error && leaderboard.data.length > 0) {
|
||||
_formatRecommendedUsers(leaderboard.data);
|
||||
}
|
||||
}
|
||||
}, [leaderboard]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!communities.loading) {
|
||||
if (!communities.error && communities.data?.length > 0) {
|
||||
_formatRecommendedCommunities(communities.data);
|
||||
}
|
||||
}
|
||||
}, [communities]);
|
||||
|
||||
useEffect(() => {
|
||||
const recommendeds = [...recommendedUsers];
|
||||
|
||||
Object.keys(followingUsers).forEach((following) => {
|
||||
if (!followingUsers[following].loading) {
|
||||
if (!followingUsers[following].error) {
|
||||
if (followingUsers[following].isFollowing) {
|
||||
recommendeds.forEach((item) => {
|
||||
if (item._id === following) {
|
||||
item.isFollowing = true;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
recommendeds.forEach((item) => {
|
||||
if (item._id === following) {
|
||||
item.isFollowing = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
setRecommendedUsers(recommendeds);
|
||||
}, [followingUsers]);
|
||||
|
||||
useEffect(() => {
|
||||
const recommendeds = [...recommendedCommunities];
|
||||
|
||||
Object.keys(subscribingCommunities).forEach((communityId) => {
|
||||
if (!subscribingCommunities[communityId].loading) {
|
||||
if (!subscribingCommunities[communityId].error) {
|
||||
if (subscribingCommunities[communityId].isSubscribed) {
|
||||
recommendeds.forEach((item) => {
|
||||
if (item.name === communityId) {
|
||||
item.isSubscribed = true;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
recommendeds.forEach((item) => {
|
||||
if (item.name === communityId) {
|
||||
item.isSubscribed = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
setRecommendedCommunities(recommendeds);
|
||||
}, [subscribingCommunities]);
|
||||
|
||||
const _handleAppStateChange = (nextAppState) => {
|
||||
if (appState.current.match(/inactive|background/) && nextAppState === 'active') {
|
||||
const isLatestPostsCheck = true;
|
||||
_loadPosts(null, isLatestPostsCheck);
|
||||
}
|
||||
|
||||
appState.current = nextAppState;
|
||||
console.log('AppState', appState.current);
|
||||
};
|
||||
|
||||
const _handleImagesHide = () => {
|
||||
dispatch(setHidePostsThumbnails(!isHideImages));
|
||||
};
|
||||
|
||||
const _getPromotePosts = async () => {
|
||||
if (pageType === 'profile' || pageType === 'ownProfile') {
|
||||
return;
|
||||
}
|
||||
await getPromotePosts()
|
||||
.then(async (res) => {
|
||||
if (res && res.length) {
|
||||
const _promotedPosts = await Promise.all(
|
||||
res.map((item) =>
|
||||
getPost(get(item, 'author'), get(item, 'permlink'), username, true).then(
|
||||
(post) => post,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (isMountedRef.current) {
|
||||
setPromotedPosts(_promotedPosts);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
};
|
||||
|
||||
const _matchFreshPosts = async (posts, reducerFilter) => {
|
||||
const cachedPosts = cache.cachedData[reducerFilter].posts.slice(0, 5);
|
||||
|
||||
let newPosts = [];
|
||||
posts.forEach((post, index) => {
|
||||
const newPostId = get(post, 'post_id');
|
||||
const postExist = cachedPosts.find((cPost) => get(cPost, 'post_id', 0) === newPostId);
|
||||
|
||||
if (!postExist) {
|
||||
newPosts.push(post);
|
||||
}
|
||||
});
|
||||
|
||||
const isRightFilter =
|
||||
cache.currentFilter === 'feed'
|
||||
? cache.currentSubFilter === reducerFilter
|
||||
: cache.currentFilter === reducerFilter;
|
||||
|
||||
if (newPosts.length > 0 && isRightFilter) {
|
||||
newPosts = newPosts.slice(0, 5);
|
||||
|
||||
setNewPostsPopupPictures(newPosts.map((post) => get(post, 'avatar', '')));
|
||||
} else {
|
||||
_scheduleLatestPostsCheck(posts[0]);
|
||||
}
|
||||
};
|
||||
|
||||
const _loadPosts = async (type, isLatestPostCheck = false) => {
|
||||
const filter = type || cache.currentFilter;
|
||||
const reducerFilter = filter !== 'feed' ? filter : cache.currentSubFilter;
|
||||
|
||||
const isFilterLoading = cache.cachedData[reducerFilter].isLoading;
|
||||
if (
|
||||
isFilterLoading ||
|
||||
// isLoading ||
|
||||
!isConnected ||
|
||||
(!isLoggedIn && type === 'feed') ||
|
||||
(!isLoggedIn && type === 'blog')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
if (!isConnected && (refreshing || isFilterLoading)) {
|
||||
setRefreshing(false);
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
cacheDispatch({
|
||||
type: 'is-filter-loading',
|
||||
payload: {
|
||||
filter: reducerFilter,
|
||||
isLoading: true,
|
||||
},
|
||||
});
|
||||
|
||||
const subfilter = selectedFeedSubfilterValue;
|
||||
let options = {};
|
||||
const limit = isLatestPostCheck ? 5 : 20;
|
||||
let func = null;
|
||||
|
||||
if (
|
||||
filter === 'feed' ||
|
||||
filter === 'posts' ||
|
||||
filter === 'blog' ||
|
||||
getFor === 'blog' ||
|
||||
filter === 'reblogs'
|
||||
) {
|
||||
if (filter === 'feed' && subfilter === 'communities') {
|
||||
func = getRankedPosts;
|
||||
options = {
|
||||
observer: feedUsername,
|
||||
sort: 'created',
|
||||
tag: 'my',
|
||||
};
|
||||
} else {
|
||||
func = getAccountPosts;
|
||||
options = {
|
||||
observer: feedUsername || '',
|
||||
account: feedUsername,
|
||||
limit,
|
||||
sort: filter,
|
||||
};
|
||||
|
||||
if (
|
||||
(pageType === 'profile' || pageType === 'ownProfile') &&
|
||||
(filter === 'feed' || filter === 'posts')
|
||||
) {
|
||||
options.sort = 'posts';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
func = getRankedPosts;
|
||||
options = {
|
||||
tag,
|
||||
limit,
|
||||
sort: filter,
|
||||
};
|
||||
}
|
||||
|
||||
const sAuthor = cache.cachedData[reducerFilter].startAuthor;
|
||||
const sPermlink = cache.cachedData[reducerFilter].startPermlink;
|
||||
if (sAuthor && sPermlink && !refreshing && !isLatestPostCheck) {
|
||||
options.start_author = sAuthor;
|
||||
options.start_permlink = sPermlink;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await func(options, username, nsfw);
|
||||
|
||||
if (isMountedRef.current) {
|
||||
if (result.length > 0) {
|
||||
const _posts = result;
|
||||
|
||||
if (filter === 'reblogs') {
|
||||
for (let i = _posts.length - 1; i >= 0; i--) {
|
||||
if (_posts[i].author === username) {
|
||||
_posts.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (_posts.length > 0) {
|
||||
if (isLatestPostCheck) {
|
||||
_matchFreshPosts(_posts, reducerFilter);
|
||||
} else {
|
||||
cacheDispatch({
|
||||
type: 'update-filter-cache',
|
||||
payload: {
|
||||
filter: reducerFilter,
|
||||
posts: _posts,
|
||||
shouldReset: refreshing,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (result.length === 0) {
|
||||
setIsNoPost(true);
|
||||
}
|
||||
|
||||
setRefreshing(false);
|
||||
setIsLoading(false);
|
||||
cacheDispatch({
|
||||
type: 'is-filter-loading',
|
||||
payload: {
|
||||
filter: reducerFilter,
|
||||
isLoading: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
setRefreshing(false);
|
||||
setIsLoading(false);
|
||||
cacheDispatch({
|
||||
type: 'is-filter-loading',
|
||||
payload: {
|
||||
filter: reducerFilter,
|
||||
isLoading: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const _handleOnRefreshPosts = () => {
|
||||
setRefreshing(true);
|
||||
_getPromotePosts();
|
||||
};
|
||||
|
||||
const _handleFilterOnDropdownSelect = (index) => {
|
||||
setSelectedFilterIndex(index);
|
||||
setIsNoPost(false);
|
||||
};
|
||||
|
||||
const _handleFeedSubfilterOnDropdownSelect = (index) => {
|
||||
setSelectedFeedSubfilterIndex(index);
|
||||
setIsNoPost(false);
|
||||
};
|
||||
|
||||
const _setSelectedFilterValue = (val) => {
|
||||
cacheDispatch({
|
||||
type: 'change-filter',
|
||||
payload: {
|
||||
currentFilter: val,
|
||||
},
|
||||
});
|
||||
setSelectedFilterValue(val);
|
||||
};
|
||||
|
||||
const _setSelectedFeedSubfilterValue = (val) => {
|
||||
cacheDispatch({
|
||||
type: 'change-sub-filter',
|
||||
payload: {
|
||||
currentSubFilter: val,
|
||||
},
|
||||
});
|
||||
setSelectedFeedSubfilterValue(val);
|
||||
};
|
||||
|
||||
const _getRecommendedUsers = () => dispatch(fetchLeaderboard());
|
||||
|
||||
const _formatRecommendedUsers = (usersArray) => {
|
||||
const recommendeds = usersArray.slice(0, 10);
|
||||
|
||||
recommendeds.unshift({ _id: 'good-karma' });
|
||||
recommendeds.unshift({ _id: 'ecency' });
|
||||
|
||||
recommendeds.forEach((item) => Object.assign(item, { isFollowing: false }));
|
||||
|
||||
setRecommendedUsers(recommendeds);
|
||||
};
|
||||
|
||||
const _getRecommendedCommunities = () => dispatch(fetchCommunities('', 10));
|
||||
|
||||
const _formatRecommendedCommunities = async (communitiesArray) => {
|
||||
try {
|
||||
const ecency = await getCommunity('hive-125125');
|
||||
|
||||
const recommendeds = [ecency, ...communitiesArray];
|
||||
recommendeds.forEach((item) => Object.assign(item, { isSubscribed: false }));
|
||||
|
||||
setRecommendedCommunities(recommendeds);
|
||||
} catch (err) {
|
||||
console.log(err, '_getRecommendedUsers Error');
|
||||
}
|
||||
};
|
||||
|
||||
const _handleFollowUserButtonPress = (data, isFollowing) => {
|
||||
let followAction;
|
||||
let successToastText = '';
|
||||
let failToastText = '';
|
||||
|
||||
if (!isFollowing) {
|
||||
followAction = followUser;
|
||||
|
||||
successToastText = intl.formatMessage({
|
||||
id: 'alert.success_follow',
|
||||
});
|
||||
failToastText = intl.formatMessage({
|
||||
id: 'alert.fail_follow',
|
||||
});
|
||||
} else {
|
||||
followAction = unfollowUser;
|
||||
|
||||
successToastText = intl.formatMessage({
|
||||
id: 'alert.success_unfollow',
|
||||
});
|
||||
failToastText = intl.formatMessage({
|
||||
id: 'alert.fail_unfollow',
|
||||
});
|
||||
}
|
||||
|
||||
data.follower = get(currentAccount, 'name', '');
|
||||
|
||||
dispatch(followAction(currentAccount, pinCode, data, successToastText, failToastText));
|
||||
};
|
||||
|
||||
const _handleSubscribeCommunityButtonPress = (data) => {
|
||||
let subscribeAction;
|
||||
let successToastText = '';
|
||||
let failToastText = '';
|
||||
|
||||
if (!data.isSubscribed) {
|
||||
subscribeAction = subscribeCommunity;
|
||||
|
||||
successToastText = intl.formatMessage({
|
||||
id: 'alert.success_subscribe',
|
||||
});
|
||||
failToastText = intl.formatMessage({
|
||||
id: 'alert.fail_subscribe',
|
||||
});
|
||||
} else {
|
||||
subscribeAction = leaveCommunity;
|
||||
|
||||
successToastText = intl.formatMessage({
|
||||
id: 'alert.success_leave',
|
||||
});
|
||||
failToastText = intl.formatMessage({
|
||||
id: 'alert.fail_leave',
|
||||
});
|
||||
}
|
||||
|
||||
dispatch(
|
||||
subscribeAction(currentAccount, pinCode, data, successToastText, failToastText, 'feedScreen'),
|
||||
);
|
||||
};
|
||||
|
||||
const _handleOnScroll = (event) => {
|
||||
if (handleOnScroll) {
|
||||
handleOnScroll();
|
||||
}
|
||||
|
||||
// memorize filter position
|
||||
const scrollPosition = event.nativeEvent.contentOffset.y;
|
||||
cacheDispatch({
|
||||
type: 'scroll-position-change',
|
||||
payload: {
|
||||
scrollPosition,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const _handleSetNewPostsPopupPictures = (data) => {
|
||||
setNewPostsPopupPictures(data);
|
||||
const cacheFilter =
|
||||
cache.currentFilter !== 'feed' ? cache.currentFilter : cache.currentSubFilter;
|
||||
const { posts } = cache.cachedData[cacheFilter];
|
||||
if (posts.length > 0) {
|
||||
_scheduleLatestPostsCheck(posts[0]);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<PostsView
|
||||
ref={elem}
|
||||
filterOptions={filterOptions}
|
||||
handleImagesHide={_handleImagesHide}
|
||||
handleOnScroll={_handleOnScroll}
|
||||
isHideImage={isHideImages}
|
||||
isLoggedIn={isLoggedIn}
|
||||
selectedOptionIndex={selectedOptionIndex}
|
||||
tag={tag}
|
||||
filterOptionsValue={filterOptionsValue}
|
||||
isLoading={isLoading}
|
||||
refreshing={refreshing}
|
||||
selectedFilterIndex={selectedFilterIndex}
|
||||
isNoPost={isNoPost}
|
||||
promotedPosts={promotedPosts}
|
||||
selectedFilterValue={selectedFilterValue}
|
||||
setSelectedFilterValue={_setSelectedFilterValue}
|
||||
handleFilterOnDropdownSelect={_handleFilterOnDropdownSelect}
|
||||
loadPosts={_loadPosts}
|
||||
handleOnRefreshPosts={_handleOnRefreshPosts}
|
||||
feedSubfilterOptions={feedSubfilterOptions}
|
||||
selectedFeedSubfilterIndex={selectedFeedSubfilterIndex}
|
||||
feedSubfilterOptionsValue={feedSubfilterOptionsValue}
|
||||
handleFeedSubfilterOnDropdownSelect={_handleFeedSubfilterOnDropdownSelect}
|
||||
setSelectedFeedSubfilterValue={_setSelectedFeedSubfilterValue}
|
||||
selectedFeedSubfilterValue={selectedFeedSubfilterValue}
|
||||
getRecommendedUsers={_getRecommendedUsers}
|
||||
getRecommendedCommunities={_getRecommendedCommunities}
|
||||
recommendedUsers={recommendedUsers}
|
||||
recommendedCommunities={recommendedCommunities}
|
||||
handleFollowUserButtonPress={_handleFollowUserButtonPress}
|
||||
handleSubscribeCommunityButtonPress={_handleSubscribeCommunityButtonPress}
|
||||
followingUsers={followingUsers}
|
||||
subscribingCommunities={subscribingCommunities}
|
||||
isFeedScreen={isFeedScreen}
|
||||
newPostsPopupPictures={newPostsPopupPictures}
|
||||
setNewPostsPopupPictures={_handleSetNewPostsPopupPictures}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default PostsContainer;
|
@ -1,5 +0,0 @@
|
||||
import PostsView from './view/postsView';
|
||||
import Posts from './container/postsContainer';
|
||||
|
||||
export { PostsView, Posts };
|
||||
export default Posts;
|
@ -1,81 +0,0 @@
|
||||
import EStyleSheet from 'react-native-extended-stylesheet';
|
||||
|
||||
export default EStyleSheet.create({
|
||||
container: {
|
||||
backgroundColor: '$primaryLightBackground',
|
||||
},
|
||||
placeholder: {
|
||||
backgroundColor: '$primaryBackgroundColor',
|
||||
padding: 20,
|
||||
borderStyle: 'solid',
|
||||
borderWidth: 1,
|
||||
borderTopWidth: 1,
|
||||
borderColor: '#e2e5e8',
|
||||
borderRadius: 5,
|
||||
marginRight: 0,
|
||||
marginLeft: 0,
|
||||
marginTop: 10,
|
||||
},
|
||||
tabs: {
|
||||
position: 'absolute',
|
||||
top: '$deviceWidth / 30',
|
||||
alignItems: 'center',
|
||||
},
|
||||
flatlistFooter: {
|
||||
alignContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginTop: 10,
|
||||
marginBottom: 60,
|
||||
borderColor: '$borderColor',
|
||||
},
|
||||
noImage: {
|
||||
width: 193,
|
||||
height: 189,
|
||||
},
|
||||
placeholderWrapper: {
|
||||
flex: 1,
|
||||
},
|
||||
noPostTitle: {
|
||||
textAlign: 'center',
|
||||
marginVertical: 16,
|
||||
color: '$primaryBlack',
|
||||
},
|
||||
popupContainer: {
|
||||
position: 'absolute',
|
||||
top: 80,
|
||||
left: 0,
|
||||
right: 0,
|
||||
alignItems: 'center',
|
||||
},
|
||||
popupContentContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: '$primaryBlue',
|
||||
paddingHorizontal: 0,
|
||||
paddingVertical: 2,
|
||||
borderRadius: 32,
|
||||
},
|
||||
popupText: {
|
||||
fontWeight: '500',
|
||||
color: '$white',
|
||||
marginLeft: 6,
|
||||
},
|
||||
closeIcon: {
|
||||
color: '$white',
|
||||
margin: 0,
|
||||
padding: 6,
|
||||
},
|
||||
arrowUpIcon: {
|
||||
color: '$white',
|
||||
margin: 0,
|
||||
marginHorizontal: 4,
|
||||
},
|
||||
popupImage: {
|
||||
height: 24,
|
||||
width: 24,
|
||||
borderRadius: 12,
|
||||
borderWidth: 2,
|
||||
marginLeft: -8,
|
||||
borderColor: '$primaryBlue',
|
||||
},
|
||||
});
|
@ -1,398 +0,0 @@
|
||||
/* eslint-disable react/jsx-wrap-multilines */
|
||||
import React, { useRef, useEffect } from 'react';
|
||||
import {
|
||||
FlatList,
|
||||
View,
|
||||
ActivityIndicator,
|
||||
RefreshControl,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
Button,
|
||||
} from 'react-native';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { get } from 'lodash';
|
||||
|
||||
// COMPONENTS
|
||||
import FastImage from 'react-native-fast-image';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { PostCard } from '../../postCard';
|
||||
import { FilterBar } from '../../filterBar';
|
||||
import {
|
||||
PostCardPlaceHolder,
|
||||
NoPost,
|
||||
UserListItem,
|
||||
CommunityListItem,
|
||||
TextWithIcon,
|
||||
} from '../../basicUIElements';
|
||||
import { ThemeContainer } from '../../../containers';
|
||||
import { IconButton } from '../../iconButton';
|
||||
|
||||
// Styles
|
||||
import styles from './postsStyles';
|
||||
import { default as ROUTES } from '../../../constants/routeNames';
|
||||
import globalStyles from '../../../globalStyles';
|
||||
import PostsList from '../../postsList';
|
||||
import { isDarkTheme } from '../../../redux/actions/applicationActions';
|
||||
|
||||
let _onEndReachedCalledDuringMomentum = true;
|
||||
|
||||
const PostsView = ({
|
||||
filterOptions,
|
||||
selectedOptionIndex,
|
||||
handleImagesHide,
|
||||
isLoggedIn,
|
||||
handleOnScroll,
|
||||
isLoading,
|
||||
refreshing,
|
||||
selectedFilterIndex,
|
||||
isNoPost,
|
||||
promotedPosts,
|
||||
selectedFilterValue,
|
||||
setSelectedFilterValue,
|
||||
filterOptionsValue,
|
||||
handleFilterOnDropdownSelect,
|
||||
handleOnRefreshPosts,
|
||||
loadPosts,
|
||||
feedSubfilterOptions,
|
||||
selectedFeedSubfilterIndex,
|
||||
feedSubfilterOptionsValue,
|
||||
handleFeedSubfilterOnDropdownSelect,
|
||||
setSelectedFeedSubfilterValue,
|
||||
selectedFeedSubfilterValue,
|
||||
getRecommendedUsers,
|
||||
getRecommendedCommunities,
|
||||
recommendedUsers,
|
||||
recommendedCommunities,
|
||||
handleFollowUserButtonPress,
|
||||
handleSubscribeCommunityButtonPress,
|
||||
followingUsers,
|
||||
subscribingCommunities,
|
||||
isFeedScreen,
|
||||
newPostsPopupPictures,
|
||||
setNewPostsPopupPictures,
|
||||
}) => {
|
||||
const navigation = useNavigation();
|
||||
|
||||
const intl = useIntl();
|
||||
const postsList = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (isNoPost) {
|
||||
if (selectedFilterValue === 'feed') {
|
||||
if (selectedFeedSubfilterValue === 'friends') {
|
||||
if (recommendedUsers.length === 0) {
|
||||
getRecommendedUsers();
|
||||
}
|
||||
} else {
|
||||
if (recommendedCommunities.length === 0) {
|
||||
getRecommendedCommunities();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [isNoPost, selectedFilterValue, selectedFeedSubfilterValue]);
|
||||
|
||||
const _handleFilterOnDropdownSelect = async (index) => {
|
||||
if (index === selectedFilterIndex) {
|
||||
_scrollTop();
|
||||
} else {
|
||||
if (filterOptions && filterOptions.length > 0) {
|
||||
setSelectedFilterValue(filterOptionsValue[index]);
|
||||
}
|
||||
|
||||
handleFilterOnDropdownSelect(index);
|
||||
}
|
||||
};
|
||||
|
||||
const _handleFeedSubfilterOnDropdownSelect = async (index) => {
|
||||
if (index === selectedFeedSubfilterIndex) {
|
||||
_scrollTop();
|
||||
} else {
|
||||
if (feedSubfilterOptions && feedSubfilterOptions.length > 0) {
|
||||
setSelectedFeedSubfilterValue(feedSubfilterOptionsValue[index]);
|
||||
}
|
||||
|
||||
handleFeedSubfilterOnDropdownSelect(index);
|
||||
}
|
||||
};
|
||||
|
||||
const _renderFooter = () => {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<View style={styles.flatlistFooter}>
|
||||
<ActivityIndicator animating size="large" color={isDarkTheme ? '#2e3d51' : '#f5f5f5'} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const _handleOnPressLogin = () => {
|
||||
navigation.navigate(ROUTES.SCREENS.LOGIN);
|
||||
};
|
||||
|
||||
const _renderEmptyContent = () => {
|
||||
if ((selectedFilterValue === 'feed' || selectedFilterValue === 'blog') && !isLoggedIn) {
|
||||
return (
|
||||
<NoPost
|
||||
imageStyle={styles.noImage}
|
||||
isButtonText
|
||||
defaultText={intl.formatMessage({
|
||||
id: 'profile.login_to_see',
|
||||
})}
|
||||
handleOnButtonPress={_handleOnPressLogin}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (isNoPost) {
|
||||
if (selectedFilterValue === 'feed') {
|
||||
if (selectedFeedSubfilterValue === 'friends') {
|
||||
return (
|
||||
<>
|
||||
<Text style={[globalStyles.subTitle, styles.noPostTitle]}>
|
||||
{intl.formatMessage({ id: 'profile.follow_people' })}
|
||||
</Text>
|
||||
<FlatList
|
||||
data={recommendedUsers}
|
||||
extraData={recommendedUsers}
|
||||
keyExtractor={(item, index) => `${item._id || item.id}${index}`}
|
||||
renderItem={({ item, index }) => (
|
||||
<UserListItem
|
||||
index={index}
|
||||
username={item._id}
|
||||
isHasRightItem
|
||||
rightText={
|
||||
item.isFollowing
|
||||
? intl.formatMessage({ id: 'user.unfollow' })
|
||||
: intl.formatMessage({ id: 'user.follow' })
|
||||
}
|
||||
// isRightColor={item.isFollowing}
|
||||
isLoggedIn={isLoggedIn}
|
||||
isFollowing={item.isFollowing}
|
||||
isLoadingRightAction={
|
||||
followingUsers.hasOwnProperty(item._id) && followingUsers[item._id].loading
|
||||
}
|
||||
onPressRightText={handleFollowUserButtonPress}
|
||||
handleOnPress={(username) =>
|
||||
navigation.navigate({
|
||||
name: ROUTES.SCREENS.PROFILE,
|
||||
params: {
|
||||
username,
|
||||
},
|
||||
key: username,
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
<Text style={[globalStyles.subTitle, styles.noPostTitle]}>
|
||||
{intl.formatMessage({ id: 'profile.follow_communities' })}
|
||||
</Text>
|
||||
<FlatList
|
||||
data={recommendedCommunities}
|
||||
keyExtractor={(item, index) => `${item.id || item.title}${index}`}
|
||||
renderItem={({ item, index }) => (
|
||||
<CommunityListItem
|
||||
index={index}
|
||||
title={item.title}
|
||||
about={item.about}
|
||||
admins={item.admins}
|
||||
id={item.id}
|
||||
authors={item.num_authors}
|
||||
posts={item.num_pending}
|
||||
subscribers={item.subscribers}
|
||||
isNsfw={item.is_nsfw}
|
||||
name={item.name}
|
||||
handleOnPress={(name) =>
|
||||
navigation.navigate({
|
||||
name: ROUTES.SCREENS.COMMUNITY,
|
||||
params: {
|
||||
tag: name,
|
||||
},
|
||||
})
|
||||
}
|
||||
handleSubscribeButtonPress={handleSubscribeCommunityButtonPress}
|
||||
isSubscribed={item.isSubscribed}
|
||||
isLoadingRightAction={
|
||||
subscribingCommunities.hasOwnProperty(item.name) &&
|
||||
subscribingCommunities[item.name].loading
|
||||
}
|
||||
isLoggedIn={isLoggedIn}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return <Text>{intl.formatMessage({ id: 'profile.havent_posted' })}</Text>;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.placeholderWrapper}>
|
||||
<PostCardPlaceHolder />
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const _scrollTop = () => {
|
||||
postsList.current.scrollToTop();
|
||||
};
|
||||
const _onEndReached = ({ distanceFromEnd }) => {
|
||||
if (!_onEndReachedCalledDuringMomentum) {
|
||||
loadPosts();
|
||||
_onEndReachedCalledDuringMomentum = true;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeContainer>
|
||||
{({ isDarkTheme }) => (
|
||||
<View style={styles.container}>
|
||||
{filterOptions && (
|
||||
<FilterBar
|
||||
dropdownIconName="arrow-drop-down"
|
||||
options={filterOptions.map((item) =>
|
||||
intl.formatMessage({ id: `home.${item.toLowerCase()}` }).toUpperCase(),
|
||||
)}
|
||||
selectedOptionIndex={selectedFilterIndex}
|
||||
defaultText={filterOptions[selectedOptionIndex]}
|
||||
rightIconName="view-module"
|
||||
rightIconType="MaterialIcons"
|
||||
onDropdownSelect={_handleFilterOnDropdownSelect}
|
||||
onRightIconPress={handleImagesHide}
|
||||
/>
|
||||
)}
|
||||
{isLoggedIn && selectedFilterValue === 'feed' && (
|
||||
<FilterBar
|
||||
dropdownIconName="arrow-drop-down"
|
||||
options={feedSubfilterOptions.map((item) =>
|
||||
intl.formatMessage({ id: `home.${item.toLowerCase()}` }).toUpperCase(),
|
||||
)}
|
||||
selectedOptionIndex={selectedFeedSubfilterIndex}
|
||||
defaultText={feedSubfilterOptions[selectedFeedSubfilterIndex]}
|
||||
onDropdownSelect={_handleFeedSubfilterOnDropdownSelect}
|
||||
/>
|
||||
)}
|
||||
|
||||
<PostsList
|
||||
ref={postsList}
|
||||
promotedPosts={promotedPosts}
|
||||
showsVerticalScrollIndicator={false}
|
||||
onEndReached={_onEndReached}
|
||||
onMomentumScrollBegin={() => {
|
||||
_onEndReachedCalledDuringMomentum = false;
|
||||
}}
|
||||
removeClippedSubviews
|
||||
// TODO: we can avoid 2 more rerenders by carefully moving these call to postsListContainer
|
||||
refreshing={refreshing}
|
||||
onRefresh={handleOnRefreshPosts}
|
||||
onEndReachedThreshold={1}
|
||||
ListFooterComponent={_renderFooter}
|
||||
onScrollEndDrag={handleOnScroll}
|
||||
ListEmptyComponent={_renderEmptyContent}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={refreshing}
|
||||
onRefresh={handleOnRefreshPosts}
|
||||
progressBackgroundColor="#357CE6"
|
||||
tintColor={!isDarkTheme ? '#357ce6' : '#96c0ff'}
|
||||
titleColor="#fff"
|
||||
colors={['#fff']}
|
||||
/>
|
||||
}
|
||||
isFeedScreen={isFeedScreen}
|
||||
/>
|
||||
|
||||
{newPostsPopupPictures !== null && (
|
||||
<View style={styles.popupContainer}>
|
||||
<View style={styles.popupContentContainer}>
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
_scrollTop();
|
||||
handleOnRefreshPosts();
|
||||
setNewPostsPopupPictures(null);
|
||||
}}
|
||||
>
|
||||
<View style={styles.popupContentContainer}>
|
||||
<IconButton
|
||||
iconStyle={styles.arrowUpIcon}
|
||||
iconType="MaterialCommunityIcons"
|
||||
name="arrow-up"
|
||||
onPress={() => {
|
||||
setNewPostsPopupPictures(null);
|
||||
}}
|
||||
size={12}
|
||||
/>
|
||||
|
||||
{newPostsPopupPictures.map((url, index) => (
|
||||
<FastImage
|
||||
key={`image_bubble_${url}`}
|
||||
source={{ uri: url }}
|
||||
style={[styles.popupImage, { zIndex: 10 - index }]}
|
||||
/>
|
||||
))}
|
||||
|
||||
<Text style={styles.popupText}>
|
||||
{intl.formatMessage({ id: 'home.popup_postfix' })}
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
|
||||
<IconButton
|
||||
iconStyle={styles.closeIcon}
|
||||
iconType="MaterialCommunityIcons"
|
||||
name="close"
|
||||
onPress={() => {
|
||||
setNewPostsPopupPictures(null);
|
||||
}}
|
||||
size={12}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* <FlatList
|
||||
ref={postsList}
|
||||
data={posts}
|
||||
showsVerticalScrollIndicator={false}
|
||||
renderItem={_renderItem}
|
||||
keyExtractor={(content, i) => content.permlink}
|
||||
onEndReached={_onEndReached}
|
||||
onMomentumScrollBegin={() => {
|
||||
_onEndReachedCalledDuringMomentum = false;
|
||||
}}
|
||||
removeClippedSubviews
|
||||
refreshing={refreshing}
|
||||
onRefresh={handleOnRefreshPosts}
|
||||
onEndReachedThreshold={1}
|
||||
ListFooterComponent={_renderFooter}
|
||||
onScrollEndDrag={_handleOnScroll}
|
||||
ListEmptyComponent={_renderEmptyContent}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={refreshing}
|
||||
onRefresh={handleOnRefreshPosts}
|
||||
progressBackgroundColor="#357CE6"
|
||||
tintColor={!isDarkTheme ? '#357ce6' : '#96c0ff'}
|
||||
titleColor="#fff"
|
||||
colors={['#fff']}
|
||||
/>
|
||||
}
|
||||
/> */}
|
||||
</View>
|
||||
)}
|
||||
</ThemeContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default PostsView;
|
||||
/* eslint-enable */
|
@ -1,15 +1,40 @@
|
||||
import React, { forwardRef, memo, useRef, useImperativeHandle, useState, useEffect } from 'react';
|
||||
import { get } from 'lodash';
|
||||
import { FlatListProps, FlatList, RefreshControl, ActivityIndicator, View } from 'react-native';
|
||||
import React, {
|
||||
forwardRef,
|
||||
useRef,
|
||||
useImperativeHandle,
|
||||
useState,
|
||||
useEffect,
|
||||
Fragment,
|
||||
useMemo,
|
||||
} from 'react';
|
||||
import {
|
||||
FlatListProps,
|
||||
FlatList,
|
||||
RefreshControl,
|
||||
ActivityIndicator,
|
||||
View,
|
||||
Alert,
|
||||
} from 'react-native';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { useIntl } from 'react-intl';
|
||||
import PostCard from '../../postCard';
|
||||
import styles from '../view/postsListStyles';
|
||||
import { UpvotePopover } from '../..';
|
||||
import { PostTypes } from '../../../constants/postTypes';
|
||||
import { PostOptionsModal } from '../../postOptionsModal';
|
||||
import { PostCardActionIds } from '../../postCard/container/postCard';
|
||||
import { useAppDispatch } from '../../../hooks';
|
||||
import { showProfileModal } from '../../../redux/actions/uiAction';
|
||||
import { getPostReblogs } from '../../../providers/ecency/ecency';
|
||||
import { useInjectVotesCache } from '../../../providers/queries/postQueries/postQueries';
|
||||
|
||||
export interface PostsListRef {
|
||||
scrollToTop: () => void;
|
||||
}
|
||||
|
||||
interface postsListContainerProps extends FlatListProps<any> {
|
||||
posts: any[];
|
||||
promotedPosts: Array<any>;
|
||||
isFeedScreen: boolean;
|
||||
onLoadPosts?: (shouldReset: boolean) => void;
|
||||
@ -23,6 +48,7 @@ let _onEndReachedCalledDuringMomentum = true;
|
||||
|
||||
const postsListContainer = (
|
||||
{
|
||||
posts,
|
||||
promotedPosts,
|
||||
isFeedScreen,
|
||||
onLoadPosts,
|
||||
@ -35,20 +61,63 @@ const postsListContainer = (
|
||||
ref,
|
||||
) => {
|
||||
const flatListRef = useRef(null);
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const [imageHeights, setImageHeights] = useState(new Map<string, number>());
|
||||
const navigation = useNavigation();
|
||||
|
||||
const upvotePopoverRef = useRef(null);
|
||||
const postDropdownRef = useRef(null);
|
||||
|
||||
const isHideImages = useSelector((state) => state.application.hidePostsThumbnails);
|
||||
const nsfw = useSelector((state) => state.application.hidePostsThumbnails);
|
||||
const isDarkTheme = useSelector((state) => state.application.isDarkThem);
|
||||
const posts = useSelector((state) => {
|
||||
|
||||
const cachedPosts = useSelector((state) => {
|
||||
return isFeedScreen ? state.posts.feedPosts : state.posts.otherPosts;
|
||||
});
|
||||
const votesCache = useSelector((state) => state.cache.votesCollection);
|
||||
|
||||
const mutes = useSelector((state) => state.account.currentAccount.mutes);
|
||||
|
||||
const scrollPosition = useSelector((state) => {
|
||||
return isFeedScreen ? state.posts.feedScrollPosition : state.posts.otherScrollPosition;
|
||||
});
|
||||
|
||||
const [imageHeights, setImageHeights] = useState(new Map<string, number>());
|
||||
const reblogsCollectionRef = useRef({});
|
||||
|
||||
const data = useMemo(() => {
|
||||
let _data = posts || cachedPosts;
|
||||
if (!_data || !_data.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// also skip muted posts
|
||||
_data = _data.filter((item) => {
|
||||
const isMuted = mutes && mutes.indexOf(item.author) > -1;
|
||||
return !isMuted && !!item?.author;
|
||||
});
|
||||
|
||||
const _promotedPosts = promotedPosts.filter((item) => {
|
||||
const isMuted = mutes && mutes.indexOf(item.author) > -1;
|
||||
const notInPosts = _data.filter((x) => x.permlink === item.permlink).length <= 0;
|
||||
return !isMuted && !!item?.author && notInPosts;
|
||||
});
|
||||
|
||||
// inject promoted posts in flat list data,
|
||||
_promotedPosts.forEach((pPost, index) => {
|
||||
const pIndex = index * 4 + 3;
|
||||
if (_data.length > pIndex) {
|
||||
_data.splice(pIndex, 0, pPost);
|
||||
}
|
||||
});
|
||||
|
||||
return _data;
|
||||
}, [posts, promotedPosts, cachedPosts, mutes]);
|
||||
|
||||
const cacheInjectedData = useInjectVotesCache(data);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
scrollToTop() {
|
||||
flatListRef.current?.scrollToOffset({ x: 0, y: 0, animated: true });
|
||||
@ -57,22 +126,48 @@ const postsListContainer = (
|
||||
|
||||
useEffect(() => {
|
||||
console.log('Scroll Position: ', scrollPosition);
|
||||
if (posts && posts.length == 0) {
|
||||
|
||||
if (cachedPosts && cachedPosts.length == 0) {
|
||||
flatListRef.current?.scrollToOffset({
|
||||
offset: 0,
|
||||
animated: false,
|
||||
});
|
||||
}
|
||||
}, [posts]);
|
||||
}, [cachedPosts]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log('Scroll Position: ', scrollPosition);
|
||||
flatListRef.current?.scrollToOffset({
|
||||
offset: posts && posts.length == 0 ? 0 : scrollPosition,
|
||||
offset: cachedPosts && cachedPosts.length == 0 ? 0 : scrollPosition,
|
||||
animated: false,
|
||||
});
|
||||
}, [scrollPosition]);
|
||||
|
||||
useEffect(() => {
|
||||
// fetch reblogs here
|
||||
_updateReblogsCollection();
|
||||
}, [data, votesCache]);
|
||||
|
||||
const _updateReblogsCollection = async () => {
|
||||
// improve routine using list or promises
|
||||
for (const i in data) {
|
||||
const _item = data[i];
|
||||
const _postPath = _item.author + _item.permlink;
|
||||
if (!reblogsCollectionRef.current[_postPath]) {
|
||||
try {
|
||||
const reblogs = await getPostReblogs(_item);
|
||||
reblogsCollectionRef.current = {
|
||||
...reblogsCollectionRef.current,
|
||||
[_postPath]: reblogs || [],
|
||||
};
|
||||
} catch (err) {
|
||||
console.warn('failed to fetch reblogs for post');
|
||||
reblogsCollectionRef.current = { ...reblogsCollectionRef.current, [_postPath]: [] };
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const _setImageHeightInMap = (mapKey: string, height: number) => {
|
||||
if (mapKey && height) {
|
||||
setImageHeights(imageHeights.set(mapKey, height));
|
||||
@ -98,98 +193,116 @@ const postsListContainer = (
|
||||
}
|
||||
};
|
||||
|
||||
const _renderItem = ({ item, index }: { item: any; index: number }) => {
|
||||
const e = [];
|
||||
const _handleCardInteraction = (
|
||||
id: PostCardActionIds,
|
||||
payload: any,
|
||||
content: any,
|
||||
onCallback,
|
||||
) => {
|
||||
switch (id) {
|
||||
case PostCardActionIds.USER:
|
||||
dispatch(showProfileModal(payload));
|
||||
break;
|
||||
|
||||
if (index % 3 === 0) {
|
||||
const ix = index / 3 - 1;
|
||||
if (promotedPosts[ix] !== undefined) {
|
||||
const p = promotedPosts[ix];
|
||||
const isMuted = mutes && mutes.indexOf(p.author) > -1;
|
||||
|
||||
if (
|
||||
!isMuted &&
|
||||
get(p, 'author', null) &&
|
||||
posts &&
|
||||
posts.filter((x) => x.permlink === p.permlink).length <= 0
|
||||
) {
|
||||
// get image height from cache if available
|
||||
const localId = p.author + p.permlink;
|
||||
const imgHeight = imageHeights.get(localId);
|
||||
|
||||
e.push(
|
||||
<PostCard
|
||||
key={`${p.author}-${p.permlink}-prom`}
|
||||
content={p}
|
||||
isHideImage={isHideImages}
|
||||
imageHeight={imgHeight}
|
||||
pageType={pageType}
|
||||
setImageHeight={_setImageHeightInMap}
|
||||
showQuickReplyModal={showQuickReplyModal}
|
||||
mutes={mutes}
|
||||
/>,
|
||||
);
|
||||
case PostCardActionIds.OPTIONS:
|
||||
if (postDropdownRef.current && content) {
|
||||
postDropdownRef.current.show(content);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case PostCardActionIds.NAVIGATE:
|
||||
navigation.navigate(payload);
|
||||
break;
|
||||
|
||||
case PostCardActionIds.REPLY:
|
||||
showQuickReplyModal(content);
|
||||
break;
|
||||
|
||||
case PostCardActionIds.UPVOTE:
|
||||
if (upvotePopoverRef.current && payload && content) {
|
||||
upvotePopoverRef.current.showPopover({
|
||||
anchorRect: payload,
|
||||
content,
|
||||
onVotingStart: onCallback,
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case PostCardActionIds.PAYOUT_DETAILS:
|
||||
if (upvotePopoverRef.current && payload && content) {
|
||||
upvotePopoverRef.current.showPopover({
|
||||
anchorRect: payload,
|
||||
content,
|
||||
showPayoutDetails: true,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const isMuted = mutes && mutes.indexOf(item.author) > -1;
|
||||
if (!isMuted && get(item, 'author', null)) {
|
||||
// get image height from cache if available
|
||||
const localId = item.author + item.permlink;
|
||||
const imgHeight = imageHeights.get(localId);
|
||||
const _renderItem = ({ item }: { item: any }) => {
|
||||
// get image height from cache if available
|
||||
const localId = item.author + item.permlink;
|
||||
const imgHeight = imageHeights.get(localId);
|
||||
const reblogs = reblogsCollectionRef.current[localId];
|
||||
|
||||
e.push(
|
||||
<PostCard
|
||||
key={`${item.author}-${item.permlink}`}
|
||||
content={item}
|
||||
isHideImage={isHideImages}
|
||||
imageHeight={imgHeight}
|
||||
setImageHeight={_setImageHeightInMap}
|
||||
pageType={pageType}
|
||||
showQuickReplyModal={showQuickReplyModal}
|
||||
mutes={mutes}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
return e;
|
||||
// e.push(
|
||||
return (
|
||||
<PostCard
|
||||
intl={intl}
|
||||
key={`${item.author}-${item.permlink}`}
|
||||
content={item}
|
||||
isHideImage={isHideImages}
|
||||
nsfw={nsfw}
|
||||
reblogs={reblogs}
|
||||
imageHeight={imgHeight}
|
||||
setImageHeight={_setImageHeightInMap}
|
||||
handleCardInteraction={(id: PostCardActionIds, payload: any, onCallback) =>
|
||||
_handleCardInteraction(id, payload, item, onCallback)
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
ref={flatListRef}
|
||||
data={posts}
|
||||
showsVerticalScrollIndicator={false}
|
||||
renderItem={_renderItem}
|
||||
keyExtractor={(content, index) => `${content.author}/${content.permlink}-${index}`}
|
||||
removeClippedSubviews
|
||||
onEndReachedThreshold={1}
|
||||
maxToRenderPerBatch={3}
|
||||
initialNumToRender={3}
|
||||
windowSize={5}
|
||||
extraData={imageHeights}
|
||||
onEndReached={_onEndReached}
|
||||
onMomentumScrollBegin={() => {
|
||||
_onEndReachedCalledDuringMomentum = false;
|
||||
}}
|
||||
ListFooterComponent={_renderFooter}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={isRefreshing}
|
||||
onRefresh={() => {
|
||||
if (onLoadPosts) {
|
||||
onLoadPosts(true);
|
||||
}
|
||||
}}
|
||||
progressBackgroundColor="#357CE6"
|
||||
tintColor={!isDarkTheme ? '#357ce6' : '#96c0ff'}
|
||||
titleColor="#fff"
|
||||
colors={['#fff']}
|
||||
/>
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
<Fragment>
|
||||
<FlatList
|
||||
ref={flatListRef}
|
||||
data={cacheInjectedData}
|
||||
showsVerticalScrollIndicator={false}
|
||||
renderItem={_renderItem}
|
||||
keyExtractor={(content, index) => `${content.author}/${content.permlink}-${index}`}
|
||||
removeClippedSubviews
|
||||
onEndReachedThreshold={1}
|
||||
maxToRenderPerBatch={5}
|
||||
initialNumToRender={3}
|
||||
windowSize={8}
|
||||
extraData={[imageHeights, reblogsCollectionRef.current, votesCache]}
|
||||
onEndReached={_onEndReached}
|
||||
onMomentumScrollBegin={() => {
|
||||
_onEndReachedCalledDuringMomentum = false;
|
||||
}}
|
||||
ListFooterComponent={_renderFooter}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={isRefreshing}
|
||||
onRefresh={() => {
|
||||
if (onLoadPosts) {
|
||||
onLoadPosts(true);
|
||||
reblogsCollectionRef.current = {};
|
||||
}
|
||||
}}
|
||||
progressBackgroundColor="#357CE6"
|
||||
tintColor={!isDarkTheme ? '#357ce6' : '#96c0ff'}
|
||||
titleColor="#fff"
|
||||
colors={['#fff']}
|
||||
/>
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
<UpvotePopover ref={upvotePopoverRef} parentType={PostTypes.POST} />
|
||||
<PostOptionsModal ref={postDropdownRef} pageType={pageType} />
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -2,6 +2,7 @@ import React, { useState, useEffect, useRef } from 'react';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { AppState, NativeEventSubscription, NativeScrollEvent, NativeSyntheticEvent } from 'react-native';
|
||||
import { debounce } from 'lodash';
|
||||
import BackgroundTimer from 'react-native-background-timer';
|
||||
import PostsList from '../../postsList';
|
||||
import { fetchPromotedEntries, loadPosts } from '../services/tabbedPostsFetch';
|
||||
import { LoadPostsOptions, TabContentProps, TabMeta } from '../services/tabbedPostsModels';
|
||||
@ -47,6 +48,7 @@ const TabContent = ({
|
||||
const currentAccount = useSelector((state) => state.account.currentAccount);
|
||||
const initPosts = useSelector((state) => state.posts.initPosts);
|
||||
|
||||
|
||||
const username = currentAccount.username;
|
||||
const userPinned = currentAccount.about?.profile?.pinned;
|
||||
|
||||
@ -56,7 +58,6 @@ const TabContent = ({
|
||||
const [sessionUser, setSessionUser] = useState(username);
|
||||
const [tabMeta, setTabMeta] = useState(DEFAULT_TAB_META);
|
||||
const [latestPosts, setLatestPosts] = useState<any[]>([]);
|
||||
const [postFetchTimer, setPostFetchTimer] = useState(0);
|
||||
const [enableScrollTop, setEnableScrollTop] = useState(false);
|
||||
const [curPinned, setCurPinned] = useState(pinnedPermlink);
|
||||
|
||||
@ -66,6 +67,7 @@ const TabContent = ({
|
||||
const appStateSubRef = useRef<NativeEventSubscription|null>()
|
||||
const postsRef = useRef(posts);
|
||||
const sessionUserRef = useRef(sessionUser);
|
||||
const postFetchTimerRef = useRef<any>(null);
|
||||
|
||||
//init state refs;
|
||||
postsRef.current = posts;
|
||||
@ -73,6 +75,7 @@ const TabContent = ({
|
||||
|
||||
//side effects
|
||||
useEffect(() => {
|
||||
|
||||
if (isFeedScreen) {
|
||||
appStateSubRef.current = AppState.addEventListener('change', _handleAppStateChange);
|
||||
}
|
||||
@ -84,7 +87,7 @@ const TabContent = ({
|
||||
|
||||
useEffect(() => {
|
||||
if (isConnected && (username !== sessionUser || forceLoadPosts)) {
|
||||
_initContent(false, username);
|
||||
_initContent(false, username);
|
||||
}
|
||||
}, [username, forceLoadPosts]);
|
||||
|
||||
@ -108,8 +111,9 @@ const TabContent = ({
|
||||
|
||||
const _cleanup = () => {
|
||||
_isMounted = false;
|
||||
if (postFetchTimer) {
|
||||
clearTimeout(postFetchTimer);
|
||||
if (postFetchTimerRef.current) {
|
||||
BackgroundTimer.clearTimeout(postFetchTimerRef.current)
|
||||
postFetchTimerRef.current = null;
|
||||
}
|
||||
if (isFeedScreen && appStateSubRef.current) {
|
||||
appStateSubRef.current.remove();
|
||||
@ -143,8 +147,9 @@ const TabContent = ({
|
||||
setSessionUser(_feedUsername);
|
||||
setLatestPosts([]);
|
||||
|
||||
if (postFetchTimer) {
|
||||
clearTimeout(postFetchTimer);
|
||||
if (postFetchTimerRef.current) {
|
||||
BackgroundTimer.clearTimeout(postFetchTimerRef.current);
|
||||
postFetchTimerRef.current = null;
|
||||
}
|
||||
|
||||
if (username || (filterKey !== 'friends' && filterKey !== 'communities')) {
|
||||
@ -222,19 +227,20 @@ const TabContent = ({
|
||||
//schedules post fetch
|
||||
const _scheduleLatestPostsCheck = (firstPost: any) => {
|
||||
if (firstPost) {
|
||||
if (postFetchTimer) {
|
||||
clearTimeout(postFetchTimer);
|
||||
if (postFetchTimerRef.current) {
|
||||
BackgroundTimer.clearTimeout(postFetchTimerRef.current);
|
||||
postFetchTimerRef.current = null;
|
||||
}
|
||||
|
||||
const timeLeft = calculateTimeLeftForPostCheck(firstPost);
|
||||
const _postFetchTimer = setTimeout(() => {
|
||||
postFetchTimerRef.current = BackgroundTimer.setTimeout(() => {
|
||||
const isLatestPostsCheck = true;
|
||||
_loadPosts({
|
||||
shouldReset: false,
|
||||
isLatestPostsCheck,
|
||||
});
|
||||
}, timeLeft);
|
||||
setPostFetchTimer(_postFetchTimer);
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
@ -333,7 +339,7 @@ const TabContent = ({
|
||||
<>
|
||||
<PostsList
|
||||
ref={postsListRef}
|
||||
data={posts}
|
||||
posts={posts}
|
||||
isFeedScreen={isFeedScreen}
|
||||
promotedPosts={promotedPosts}
|
||||
onLoadPosts={(shouldReset) => {
|
||||
|
@ -1,239 +0,0 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import get from 'lodash/get';
|
||||
|
||||
// Services and Actions
|
||||
import {
|
||||
setCommentUpvotePercent,
|
||||
setPostUpvotePercent,
|
||||
} from '../../../redux/actions/applicationActions';
|
||||
|
||||
// Utils
|
||||
import { getTimeFromNow } from '../../../utils/time';
|
||||
import { isVoted as isVotedFunc, isDownVoted as isDownVotedFunc } from '../../../utils/postParser';
|
||||
import parseAsset from '../../../utils/parseAsset';
|
||||
|
||||
// Component
|
||||
import UpvoteView from '../view/upvoteView';
|
||||
import { updateVoteCache } from '../../../redux/actions/cacheActions';
|
||||
import { useAppSelector } from '../../../hooks';
|
||||
import postTypes from '../../../constants/postTypes';
|
||||
|
||||
/*
|
||||
* Props Name Description Value
|
||||
*@props --> props name here description here Value Type Here
|
||||
*
|
||||
*/
|
||||
|
||||
const UpvoteContainer = (props) => {
|
||||
const {
|
||||
content,
|
||||
currentAccount,
|
||||
isLoggedIn,
|
||||
isShowPayoutValue,
|
||||
pinCode,
|
||||
postUpvotePercent,
|
||||
commentUpvotePercent,
|
||||
globalProps,
|
||||
dispatch,
|
||||
activeVotes = [],
|
||||
handleCacheVoteIncrement,
|
||||
fetchPost,
|
||||
parentType,
|
||||
boldPayout,
|
||||
} = props;
|
||||
|
||||
const [isVoted, setIsVoted] = useState(null);
|
||||
const [isDownVoted, setIsDownVoted] = useState(null);
|
||||
const [totalPayout, setTotalPayout] = useState(get(content, 'total_payout'));
|
||||
const cachedVotes = useAppSelector((state) => state.cache.votes);
|
||||
const lastCacheUpdate = useAppSelector((state) => state.cache.lastUpdate);
|
||||
|
||||
useEffect(() => {
|
||||
let _isMounted = true;
|
||||
|
||||
const _calculateVoteStatus = async () => {
|
||||
const _isVoted = await isVotedFunc(activeVotes, get(currentAccount, 'name'));
|
||||
const _isDownVoted = await isDownVotedFunc(activeVotes, get(currentAccount, 'name'));
|
||||
|
||||
if (_isMounted) {
|
||||
setIsVoted(_isVoted && parseInt(_isVoted, 10) / 10000);
|
||||
setIsDownVoted(_isDownVoted && (parseInt(_isDownVoted, 10) / 10000) * -1);
|
||||
|
||||
if (cachedVotes && cachedVotes.size > 0) {
|
||||
_handleCachedVote();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_calculateVoteStatus();
|
||||
return () => (_isMounted = false);
|
||||
}, [activeVotes]);
|
||||
|
||||
useEffect(() => {
|
||||
const postPath = `${content.author || ''}/${content.permlink || ''}`;
|
||||
// this conditional makes sure on targetted already fetched post is updated
|
||||
// with new cache status, this is to avoid duplicate cache merging
|
||||
if (
|
||||
lastCacheUpdate &&
|
||||
lastCacheUpdate.postPath === postPath &&
|
||||
content.post_fetched_at < lastCacheUpdate.updatedAt &&
|
||||
lastCacheUpdate.type === 'vote'
|
||||
) {
|
||||
_handleCachedVote();
|
||||
}
|
||||
}, [lastCacheUpdate]);
|
||||
|
||||
const _setUpvotePercent = (value) => {
|
||||
if (value) {
|
||||
if (parentType === postTypes.POST) {
|
||||
dispatch(setPostUpvotePercent(value));
|
||||
}
|
||||
if (parentType === postTypes.COMMENT) {
|
||||
dispatch(setCommentUpvotePercent(value));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const _handleCachedVote = () => {
|
||||
if (!cachedVotes || cachedVotes.size === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const postPath = `${content.author || ''}/${content.permlink || ''}`;
|
||||
const postFetchedAt = get(content, 'post_fetched_at', 0);
|
||||
|
||||
if (cachedVotes.has(postPath)) {
|
||||
const cachedVote = cachedVotes.get(postPath);
|
||||
const { expiresAt, amount, isDownvote, incrementStep } = cachedVote;
|
||||
|
||||
if (postFetchedAt > expiresAt) {
|
||||
return;
|
||||
}
|
||||
|
||||
setTotalPayout(get(content, 'total_payout') + amount);
|
||||
if (incrementStep > 0) {
|
||||
handleCacheVoteIncrement();
|
||||
}
|
||||
|
||||
setIsDownVoted(!!isDownvote);
|
||||
setIsVoted(!isDownvote);
|
||||
}
|
||||
};
|
||||
|
||||
const _onVote = (amount, isDownvote) => {
|
||||
// do all relevant processing here to show local upvote
|
||||
const amountNum = parseFloat(amount);
|
||||
|
||||
let incrementStep = 0;
|
||||
if (!isVoted && !isDownVoted) {
|
||||
incrementStep = 1;
|
||||
}
|
||||
|
||||
// update redux
|
||||
const postPath = `${content.author || ''}/${content.permlink || ''}`;
|
||||
const curTime = new Date().getTime();
|
||||
const vote = {
|
||||
votedAt: curTime,
|
||||
amount: amountNum,
|
||||
isDownvote,
|
||||
incrementStep,
|
||||
expiresAt: curTime + 30000,
|
||||
};
|
||||
dispatch(updateVoteCache(postPath, vote));
|
||||
};
|
||||
|
||||
const author = get(content, 'author');
|
||||
const isDeclinedPayout = get(content, 'is_declined_payout');
|
||||
const permlink = get(content, 'permlink');
|
||||
const pendingPayout = parseAsset(content.pending_payout_value).amount;
|
||||
const authorPayout = parseAsset(content.author_payout_value).amount;
|
||||
const curationPayout = parseAsset(content.curator_payout_value).amount;
|
||||
const promotedPayout = parseAsset(content.promoted).amount;
|
||||
const maxPayout = content.max_payout;
|
||||
|
||||
const payoutDate = getTimeFromNow(get(content, 'payout_at'));
|
||||
const beneficiaries = [];
|
||||
const beneficiary = get(content, 'beneficiaries');
|
||||
if (beneficiary) {
|
||||
beneficiary.forEach((key, index) => {
|
||||
beneficiaries.push(
|
||||
`${index !== 0 ? '\n' : ''}${get(key, 'account')}: ${(
|
||||
parseFloat(get(key, 'weight')) / 100
|
||||
).toFixed(2)}%`,
|
||||
);
|
||||
});
|
||||
}
|
||||
const minimumAmountForPayout = 0.02;
|
||||
let warnZeroPayout = false;
|
||||
if (pendingPayout > 0 && pendingPayout < minimumAmountForPayout) {
|
||||
warnZeroPayout = true;
|
||||
}
|
||||
|
||||
// assemble breakdown
|
||||
const base = get(globalProps, 'base', 0);
|
||||
const quote = get(globalProps, 'quote', 0);
|
||||
const hbdPrintRate = get(globalProps, 'hbdPrintRate', 0);
|
||||
const SBD_PRINT_RATE_MAX = 10000;
|
||||
const percent_steem_dollars = (content.percent_hbd || 10000) / 20000;
|
||||
|
||||
const pending_payout_hbd = pendingPayout * percent_steem_dollars;
|
||||
const price_per_steem = base / quote;
|
||||
|
||||
const pending_payout_hp = (pendingPayout - pending_payout_hbd) / price_per_steem;
|
||||
const pending_payout_printed_hbd = pending_payout_hbd * (hbdPrintRate / SBD_PRINT_RATE_MAX);
|
||||
const pending_payout_printed_hive =
|
||||
(pending_payout_hbd - pending_payout_printed_hbd) / price_per_steem;
|
||||
|
||||
const breakdownPayout =
|
||||
(pending_payout_printed_hbd > 0 ? `${pending_payout_printed_hbd.toFixed(3)} HBD\n` : '') +
|
||||
(pending_payout_printed_hive > 0 ? `${pending_payout_printed_hive.toFixed(3)} HIVE\n` : '') +
|
||||
(pending_payout_hp > 0 ? `${pending_payout_hp.toFixed(3)} HP` : '');
|
||||
|
||||
return (
|
||||
<UpvoteView
|
||||
author={author}
|
||||
authorPayout={authorPayout}
|
||||
curationPayout={curationPayout}
|
||||
currentAccount={currentAccount}
|
||||
globalProps={globalProps}
|
||||
handleSetUpvotePercent={_setUpvotePercent}
|
||||
isDeclinedPayout={isDeclinedPayout}
|
||||
isLoggedIn={isLoggedIn}
|
||||
isShowPayoutValue={isShowPayoutValue}
|
||||
isVoted={isVoted}
|
||||
isDownVoted={isDownVoted}
|
||||
payoutDate={payoutDate}
|
||||
pendingPayout={pendingPayout}
|
||||
permlink={permlink}
|
||||
pinCode={pinCode}
|
||||
promotedPayout={promotedPayout}
|
||||
totalPayout={totalPayout}
|
||||
maxPayout={maxPayout}
|
||||
postUpvotePercent={postUpvotePercent}
|
||||
commentUpvotePercent={commentUpvotePercent}
|
||||
parentType={parentType}
|
||||
beneficiaries={beneficiaries}
|
||||
warnZeroPayout={warnZeroPayout}
|
||||
breakdownPayout={breakdownPayout}
|
||||
fetchPost={fetchPost}
|
||||
onVote={_onVote}
|
||||
boldPayout={boldPayout}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
// Component Life Cycle Functions
|
||||
|
||||
// Component Functions
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
isLoggedIn: state.application.isLoggedIn,
|
||||
postUpvotePercent: state.application.postUpvotePercent,
|
||||
commentUpvotePercent: state.application.commentUpvotePercent,
|
||||
pinCode: state.application.pin,
|
||||
currentAccount: state.account.currentAccount,
|
||||
globalProps: state.account.globalProps,
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(UpvoteContainer);
|
@ -1,5 +0,0 @@
|
||||
import UpvoteView from './view/upvoteView';
|
||||
import Upvote from './container/upvoteContainer';
|
||||
|
||||
export { UpvoteView, Upvote };
|
||||
export default Upvote;
|
@ -1,448 +0,0 @@
|
||||
import React, { Fragment, useEffect, useState } from 'react';
|
||||
import { View, TouchableOpacity, Text } from 'react-native';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { Popover, PopoverController } from 'react-native-modal-popover';
|
||||
import Slider from '@esteemapp/react-native-slider';
|
||||
|
||||
// Utils
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { getEstimatedAmount } from '../../../utils/vote';
|
||||
|
||||
// Components
|
||||
import { Icon } from '../../icon';
|
||||
import { PulseAnimation } from '../../animations';
|
||||
import { TextButton } from '../../buttons';
|
||||
import { FormattedCurrency } from '../../formatedElements';
|
||||
// Services
|
||||
import { setRcOffer, toastNotification } from '../../../redux/actions/uiAction';
|
||||
|
||||
// STEEM
|
||||
import { vote } from '../../../providers/hive/dhive';
|
||||
|
||||
// Styles
|
||||
import styles from './upvoteStyles';
|
||||
import { useAppSelector } from '../../../hooks';
|
||||
import postTypes from '../../../constants/postTypes';
|
||||
import { useUserActivityMutation } from '../../../providers/queries';
|
||||
import { PointActivityIds } from '../../../providers/ecency/ecency.types';
|
||||
|
||||
interface UpvoteViewProps {
|
||||
isDeclinedPayout: boolean;
|
||||
isShowPayoutValue: boolean;
|
||||
totalPayout: number;
|
||||
maxPayout: number;
|
||||
payoutDeclined: boolean;
|
||||
pendingPayout: number;
|
||||
promotedPayout: number;
|
||||
authorPayout: number;
|
||||
curationPayout: number;
|
||||
payoutDate: string;
|
||||
isDownVoted: boolean;
|
||||
beneficiaries: string[];
|
||||
warnZeroPayout: boolean;
|
||||
breakdownPayout: string;
|
||||
globalProps: any;
|
||||
author: string;
|
||||
handleSetUpvotePercent: (value: number) => void;
|
||||
permlink: string;
|
||||
onVote: (amount: string, downvote: boolean) => void;
|
||||
isVoted: boolean;
|
||||
postUpvotePercent: number;
|
||||
commentUpvotePercent: number;
|
||||
parentType: string;
|
||||
boldPayout?: boolean;
|
||||
}
|
||||
|
||||
const UpvoteView = ({
|
||||
isDeclinedPayout,
|
||||
isShowPayoutValue,
|
||||
totalPayout,
|
||||
maxPayout,
|
||||
pendingPayout,
|
||||
promotedPayout,
|
||||
authorPayout,
|
||||
curationPayout,
|
||||
payoutDate,
|
||||
isDownVoted,
|
||||
beneficiaries,
|
||||
warnZeroPayout,
|
||||
breakdownPayout,
|
||||
globalProps,
|
||||
author,
|
||||
handleSetUpvotePercent,
|
||||
permlink,
|
||||
onVote,
|
||||
isVoted,
|
||||
postUpvotePercent,
|
||||
commentUpvotePercent,
|
||||
parentType,
|
||||
boldPayout,
|
||||
}: UpvoteViewProps) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
const userActivityMutation = useUserActivityMutation();
|
||||
|
||||
const isLoggedIn = useAppSelector((state) => state.application.isLoggedIn);
|
||||
const currentAccount = useAppSelector((state) => state.account.currentAccount);
|
||||
const pinCode = useAppSelector((state) => state.application.pin);
|
||||
|
||||
const [sliderValue, setSliderValue] = useState(1);
|
||||
const [amount, setAmount] = useState('0.00000');
|
||||
const [isVoting, setIsVoting] = useState(false);
|
||||
const [upvote, setUpvote] = useState(isVoted || false);
|
||||
const [downvote, setDownvote] = useState(isDownVoted || false);
|
||||
const [isShowDetails, setIsShowDetails] = useState(false);
|
||||
const [upvotePercent, setUpvotePercent] = useState(1);
|
||||
|
||||
useEffect(() => {
|
||||
_calculateEstimatedAmount();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (parentType === postTypes.POST) {
|
||||
setUpvotePercent(postUpvotePercent);
|
||||
}
|
||||
if (parentType === postTypes.COMMENT) {
|
||||
setUpvotePercent(commentUpvotePercent);
|
||||
}
|
||||
}, [postUpvotePercent, commentUpvotePercent, parentType]);
|
||||
|
||||
useEffect(() => {
|
||||
const value = isVoted || isDownVoted ? 1 : upvotePercent <= 1 ? upvotePercent : 1;
|
||||
|
||||
setSliderValue(value);
|
||||
_calculateEstimatedAmount(value);
|
||||
}, [upvotePercent]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isVoted !== null && isVoted !== upvote) {
|
||||
setUpvote(isVoted || false);
|
||||
}
|
||||
}, [isVoted]);
|
||||
|
||||
// Component Functions
|
||||
const _calculateEstimatedAmount = async (value: number = sliderValue) => {
|
||||
if (currentAccount && Object.entries(currentAccount).length !== 0) {
|
||||
setAmount(getEstimatedAmount(currentAccount, globalProps, value));
|
||||
}
|
||||
};
|
||||
|
||||
const _upvoteContent = (closePopover) => {
|
||||
if (!downvote) {
|
||||
closePopover();
|
||||
setIsVoting(true);
|
||||
handleSetUpvotePercent(sliderValue);
|
||||
|
||||
const weight = sliderValue ? Math.trunc(sliderValue * 100) * 100 : 0;
|
||||
|
||||
console.log(`casting up vote: ${weight}`);
|
||||
vote(currentAccount, pinCode, author, permlink, weight)
|
||||
.then((response) => {
|
||||
console.log('Vote response: ', response);
|
||||
// record user points
|
||||
userActivityMutation.mutate({
|
||||
pointsTy: PointActivityIds.VOTE,
|
||||
transactionId: response.id,
|
||||
});
|
||||
|
||||
if (!response || !response.id) {
|
||||
dispatch(
|
||||
toastNotification(
|
||||
intl.formatMessage(
|
||||
{ id: 'alert.something_wrong_msg' },
|
||||
{
|
||||
message: intl.formatMessage({
|
||||
id: 'alert.invalid_response',
|
||||
}),
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
setUpvote(!!sliderValue);
|
||||
setIsVoting(false);
|
||||
onVote(amount, false);
|
||||
})
|
||||
.catch((err) => {
|
||||
if (
|
||||
err &&
|
||||
err.response &&
|
||||
err.response.jse_shortmsg &&
|
||||
err.response.jse_shortmsg.includes('wait to transact')
|
||||
) {
|
||||
// when RC is not enough, offer boosting account
|
||||
setUpvote(false);
|
||||
setIsVoting(false);
|
||||
dispatch(setRcOffer(true));
|
||||
} else if (err && err.jse_shortmsg && err.jse_shortmsg.includes('wait to transact')) {
|
||||
// when RC is not enough, offer boosting account
|
||||
setUpvote(false);
|
||||
setIsVoting(false);
|
||||
dispatch(setRcOffer(true));
|
||||
} else {
|
||||
// // when voting with same percent or other errors
|
||||
let errMsg = '';
|
||||
if (err.message && err.message.indexOf(':') > 0) {
|
||||
errMsg = err.message.split(': ')[1];
|
||||
} else {
|
||||
errMsg = err.jse_shortmsg || err.error_description || err.message;
|
||||
}
|
||||
|
||||
dispatch(
|
||||
toastNotification(
|
||||
intl.formatMessage({ id: 'alert.something_wrong_msg' }, { message: errMsg }),
|
||||
),
|
||||
);
|
||||
|
||||
setIsVoting(false);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
setSliderValue(1);
|
||||
setDownvote(false);
|
||||
}
|
||||
};
|
||||
|
||||
const _downvoteContent = (closePopover) => {
|
||||
if (downvote) {
|
||||
closePopover();
|
||||
setIsVoting(true);
|
||||
handleSetUpvotePercent(sliderValue);
|
||||
|
||||
const weight = sliderValue ? Math.trunc(sliderValue * 100) * -100 : 0;
|
||||
|
||||
console.log(`casting down vote: ${weight}`);
|
||||
vote(currentAccount, pinCode, author, permlink, weight)
|
||||
.then((response) => {
|
||||
// record usr points
|
||||
userActivityMutation.mutate({
|
||||
pointsTy: PointActivityIds.VOTE,
|
||||
transactionId: response.id,
|
||||
});
|
||||
setUpvote(!!sliderValue);
|
||||
setIsVoting(false);
|
||||
onVote(amount, true);
|
||||
})
|
||||
.catch((err) => {
|
||||
dispatch(
|
||||
toastNotification(
|
||||
intl.formatMessage({ id: 'alert.something_wrong_msg' }, { message: err.message }),
|
||||
),
|
||||
);
|
||||
setUpvote(false);
|
||||
setIsVoting(false);
|
||||
});
|
||||
} else {
|
||||
setSliderValue(1);
|
||||
setDownvote(true);
|
||||
}
|
||||
};
|
||||
|
||||
const _handleOnPopoverClose = () => {
|
||||
setTimeout(() => {
|
||||
setIsShowDetails(false);
|
||||
}, 300);
|
||||
};
|
||||
|
||||
let iconName = 'upcircleo';
|
||||
const iconType = 'AntDesign';
|
||||
let downVoteIconName = 'downcircleo';
|
||||
|
||||
if (upvote) {
|
||||
iconName = 'upcircle';
|
||||
}
|
||||
|
||||
if (isDownVoted) {
|
||||
downVoteIconName = 'downcircle';
|
||||
}
|
||||
|
||||
const _percent = `${downvote ? '-' : ''}${(sliderValue * 100).toFixed(0)}%`;
|
||||
const _amount = `$${amount}`;
|
||||
|
||||
const payoutLimitHit = totalPayout >= maxPayout;
|
||||
const _shownPayout = payoutLimitHit && maxPayout > 0 ? maxPayout : totalPayout;
|
||||
|
||||
const sliderColor = downvote ? '#ec8b88' : '#357ce6';
|
||||
|
||||
const _payoutPopupItem = (label, value) => {
|
||||
return (
|
||||
<View style={styles.popoverItemContent}>
|
||||
<Text style={styles.detailsLabel}>{label}</Text>
|
||||
<Text style={styles.detailsText}>{value}</Text>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<PopoverController>
|
||||
{({ openPopover, closePopover, popoverVisible, setPopoverAnchor, popoverAnchorRect }) => (
|
||||
<Fragment>
|
||||
<TouchableOpacity
|
||||
ref={setPopoverAnchor}
|
||||
onPress={openPopover}
|
||||
style={styles.upvoteButton}
|
||||
disabled={!isLoggedIn}
|
||||
>
|
||||
<Fragment>
|
||||
{isVoting ? (
|
||||
<View style={{ width: 19 }}>
|
||||
<PulseAnimation
|
||||
color="#357ce6"
|
||||
numPulses={1}
|
||||
diameter={20}
|
||||
speed={100}
|
||||
duration={1500}
|
||||
isShow={!isVoting}
|
||||
/>
|
||||
</View>
|
||||
) : (
|
||||
<View hitSlop={{ top: 10, bottom: 10, left: 10, right: 5 }}>
|
||||
<Icon
|
||||
style={[styles.upvoteIcon, isDownVoted && { color: '#ec8b88' }]}
|
||||
active={!isLoggedIn}
|
||||
iconType={iconType}
|
||||
name={isDownVoted ? downVoteIconName : iconName}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</Fragment>
|
||||
</TouchableOpacity>
|
||||
<View style={styles.payoutTextButton}>
|
||||
{isShowPayoutValue && (
|
||||
<TextButton
|
||||
style={styles.payoutTextButton}
|
||||
textStyle={[
|
||||
styles.payoutValue,
|
||||
isDeclinedPayout && styles.declinedPayout,
|
||||
boldPayout && styles.boldText,
|
||||
]}
|
||||
text={<FormattedCurrency value={_shownPayout || '0.000'} />}
|
||||
onPress={() => {
|
||||
openPopover();
|
||||
setIsShowDetails(true);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<Popover
|
||||
contentStyle={isShowDetails ? styles.popoverDetails : styles.popoverSlider}
|
||||
arrowStyle={isShowDetails ? styles.arrow : styles.hideArrow}
|
||||
backgroundStyle={styles.overlay}
|
||||
visible={popoverVisible}
|
||||
onClose={() => {
|
||||
closePopover();
|
||||
_handleOnPopoverClose();
|
||||
}}
|
||||
fromRect={popoverAnchorRect}
|
||||
placement="top"
|
||||
supportedOrientations={['portrait', 'landscape']}
|
||||
>
|
||||
<View style={styles.popoverWrapper}>
|
||||
{isShowDetails ? (
|
||||
<View style={styles.popoverContent}>
|
||||
{promotedPayout > 0 &&
|
||||
_payoutPopupItem(
|
||||
intl.formatMessage({ id: 'payout.promoted' }),
|
||||
<FormattedCurrency value={promotedPayout} isApproximate={true} />,
|
||||
)}
|
||||
|
||||
{pendingPayout > 0 &&
|
||||
_payoutPopupItem(
|
||||
intl.formatMessage({ id: 'payout.potential_payout' }),
|
||||
<FormattedCurrency value={pendingPayout} isApproximate={true} />,
|
||||
)}
|
||||
|
||||
{authorPayout > 0 &&
|
||||
_payoutPopupItem(
|
||||
intl.formatMessage({ id: 'payout.author_payout' }),
|
||||
<FormattedCurrency value={authorPayout} isApproximate={true} />,
|
||||
)}
|
||||
|
||||
{curationPayout > 0 &&
|
||||
_payoutPopupItem(
|
||||
intl.formatMessage({ id: 'payout.curation_payout' }),
|
||||
<FormattedCurrency value={curationPayout} isApproximate={true} />,
|
||||
)}
|
||||
{payoutLimitHit &&
|
||||
_payoutPopupItem(
|
||||
intl.formatMessage({ id: 'payout.max_accepted' }),
|
||||
<FormattedCurrency value={maxPayout} isApproximate={true} />,
|
||||
)}
|
||||
|
||||
{!!breakdownPayout &&
|
||||
pendingPayout > 0 &&
|
||||
_payoutPopupItem(
|
||||
intl.formatMessage({ id: 'payout.breakdown' }),
|
||||
breakdownPayout,
|
||||
)}
|
||||
|
||||
{beneficiaries.length > 0 &&
|
||||
_payoutPopupItem(
|
||||
intl.formatMessage({ id: 'payout.beneficiaries' }),
|
||||
beneficiaries,
|
||||
)}
|
||||
|
||||
{!!payoutDate &&
|
||||
_payoutPopupItem(intl.formatMessage({ id: 'payout.payout_date' }), payoutDate)}
|
||||
|
||||
{warnZeroPayout &&
|
||||
_payoutPopupItem(intl.formatMessage({ id: 'payout.warn_zero_payout' }), '')}
|
||||
</View>
|
||||
) : (
|
||||
<Fragment>
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
_upvoteContent(closePopover);
|
||||
}}
|
||||
style={styles.upvoteButton}
|
||||
>
|
||||
<Icon
|
||||
size={20}
|
||||
style={[styles.upvoteIcon, { color: '#007ee5' }]}
|
||||
active={!isLoggedIn}
|
||||
iconType="AntDesign"
|
||||
name={iconName}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
<Text style={styles.amount}>{_amount}</Text>
|
||||
<Slider
|
||||
style={styles.slider}
|
||||
minimumTrackTintColor={sliderColor}
|
||||
trackStyle={styles.track}
|
||||
thumbStyle={styles.thumb}
|
||||
thumbTintColor="#007ee5"
|
||||
minimumValue={0.01}
|
||||
maximumValue={1}
|
||||
value={sliderValue}
|
||||
onValueChange={(value) => {
|
||||
setSliderValue(value);
|
||||
_calculateEstimatedAmount(value);
|
||||
}}
|
||||
/>
|
||||
<Text style={styles.percent}>{_percent}</Text>
|
||||
<TouchableOpacity
|
||||
onPress={() => _downvoteContent(closePopover)}
|
||||
style={styles.upvoteButton}
|
||||
>
|
||||
<Icon
|
||||
size={20}
|
||||
style={[styles.upvoteIcon, { color: '#ec8b88' }]}
|
||||
active={!isLoggedIn}
|
||||
iconType="AntDesign"
|
||||
name={downVoteIconName}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</Fragment>
|
||||
)}
|
||||
</View>
|
||||
</Popover>
|
||||
</Fragment>
|
||||
)}
|
||||
</PopoverController>
|
||||
);
|
||||
};
|
||||
|
||||
export default UpvoteView;
|
126
src/components/upvotePopover/children/payoutDetailsContent.tsx
Normal file
126
src/components/upvotePopover/children/payoutDetailsContent.tsx
Normal file
@ -0,0 +1,126 @@
|
||||
import React from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
import { View, Text } from 'react-native';
|
||||
import { useAppSelector } from '../../../hooks';
|
||||
import parseAsset from '../../../utils/parseAsset';
|
||||
import { getTimeFromNow } from '../../../utils/time';
|
||||
|
||||
import { FormattedCurrency } from '../../formatedElements';
|
||||
// Styles
|
||||
import styles from './upvoteStyles';
|
||||
|
||||
interface Props {
|
||||
content: any;
|
||||
}
|
||||
|
||||
export const PayoutDetailsContent = ({ content }: Props) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const globalProps = useAppSelector((state) => state.account.globalProps);
|
||||
|
||||
const authorPayout = parseAsset(content.author_payout_value).amount;
|
||||
const curationPayout = parseAsset(content.curator_payout_value).amount;
|
||||
const promotedPayout = parseAsset(content.promoted).amount;
|
||||
|
||||
const pendingPayout = parseAsset(content.pending_payout_value).amount;
|
||||
|
||||
const totalPayout = content.total_payout;
|
||||
|
||||
const maxPayout = content.max_payout;
|
||||
|
||||
const payoutDate = getTimeFromNow(content.payout_at);
|
||||
|
||||
const payoutLimitHit = totalPayout >= maxPayout;
|
||||
|
||||
// assemble breakdown
|
||||
const base = globalProps?.base || 0;
|
||||
const quote = globalProps?.quote || 0;
|
||||
const hbdPrintRate = globalProps?.hbdPrintRate || 0;
|
||||
const SBD_PRINT_RATE_MAX = 10000;
|
||||
const percent_steem_dollars = (content.percent_hbd || 10000) / 20000;
|
||||
|
||||
const pending_payout_hbd = pendingPayout * percent_steem_dollars;
|
||||
const price_per_steem = base / quote;
|
||||
|
||||
const pending_payout_hp = (pendingPayout - pending_payout_hbd) / price_per_steem;
|
||||
const pending_payout_printed_hbd = pending_payout_hbd * (hbdPrintRate / SBD_PRINT_RATE_MAX);
|
||||
const pending_payout_printed_hive =
|
||||
(pending_payout_hbd - pending_payout_printed_hbd) / price_per_steem;
|
||||
|
||||
const breakdownPayout =
|
||||
(pending_payout_printed_hbd > 0 ? `${pending_payout_printed_hbd.toFixed(3)} HBD\n` : '') +
|
||||
(pending_payout_printed_hive > 0 ? `${pending_payout_printed_hive.toFixed(3)} HIVE\n` : '') +
|
||||
(pending_payout_hp > 0 ? `${pending_payout_hp.toFixed(3)} HP` : '');
|
||||
|
||||
const beneficiaries = [];
|
||||
const beneficiary = content?.beneficiaries;
|
||||
if (beneficiary) {
|
||||
beneficiary.forEach((key, index) => {
|
||||
beneficiaries.push(
|
||||
`${index !== 0 ? '\n' : ''}${key?.account}: ${(parseFloat(key?.weight) / 100).toFixed(2)}%`,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
const minimumAmountForPayout = 0.02;
|
||||
let warnZeroPayout = false;
|
||||
if (pendingPayout > 0 && pendingPayout < minimumAmountForPayout) {
|
||||
warnZeroPayout = true;
|
||||
}
|
||||
|
||||
const _payoutPopupItem = (label, value) => {
|
||||
return (
|
||||
<View style={styles.popoverItemContent}>
|
||||
<Text style={styles.detailsLabel}>{label}</Text>
|
||||
<Text style={styles.detailsText}>{value}</Text>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.popoverContent}>
|
||||
{promotedPayout > 0 &&
|
||||
_payoutPopupItem(
|
||||
intl.formatMessage({ id: 'payout.promoted' }),
|
||||
<FormattedCurrency value={promotedPayout} isApproximate={true} />,
|
||||
)}
|
||||
|
||||
{pendingPayout > 0 &&
|
||||
_payoutPopupItem(
|
||||
intl.formatMessage({ id: 'payout.potential_payout' }),
|
||||
<FormattedCurrency value={pendingPayout} isApproximate={true} />,
|
||||
)}
|
||||
|
||||
{authorPayout > 0 &&
|
||||
_payoutPopupItem(
|
||||
intl.formatMessage({ id: 'payout.author_payout' }),
|
||||
<FormattedCurrency value={authorPayout} isApproximate={true} />,
|
||||
)}
|
||||
|
||||
{curationPayout > 0 &&
|
||||
_payoutPopupItem(
|
||||
intl.formatMessage({ id: 'payout.curation_payout' }),
|
||||
<FormattedCurrency value={curationPayout} isApproximate={true} />,
|
||||
)}
|
||||
{payoutLimitHit &&
|
||||
_payoutPopupItem(
|
||||
intl.formatMessage({ id: 'payout.max_accepted' }),
|
||||
<FormattedCurrency value={maxPayout} isApproximate={true} />,
|
||||
)}
|
||||
|
||||
{!!breakdownPayout &&
|
||||
pendingPayout > 0 &&
|
||||
_payoutPopupItem(intl.formatMessage({ id: 'payout.breakdown' }), breakdownPayout)}
|
||||
|
||||
{beneficiaries.length > 0 &&
|
||||
_payoutPopupItem(intl.formatMessage({ id: 'payout.beneficiaries' }), beneficiaries)}
|
||||
|
||||
{!!payoutDate &&
|
||||
_payoutPopupItem(intl.formatMessage({ id: 'payout.payout_date' }), payoutDate)}
|
||||
|
||||
{warnZeroPayout &&
|
||||
_payoutPopupItem(intl.formatMessage({ id: 'payout.warn_zero_payout' }), '')}
|
||||
</View>
|
||||
);
|
||||
};
|
403
src/components/upvotePopover/container/upvotePopover.tsx
Normal file
403
src/components/upvotePopover/container/upvotePopover.tsx
Normal file
@ -0,0 +1,403 @@
|
||||
import React, {
|
||||
Fragment,
|
||||
useState,
|
||||
useEffect,
|
||||
forwardRef,
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import get from 'lodash/get';
|
||||
|
||||
// Services and Actions
|
||||
import { Rect } from 'react-native-modal-popover/lib/PopoverGeometry';
|
||||
import { View, TouchableOpacity, Text, Alert } from 'react-native';
|
||||
import { Popover } from 'react-native-modal-popover';
|
||||
import Slider from '@esteemapp/react-native-slider';
|
||||
import { useIntl } from 'react-intl';
|
||||
import {
|
||||
setCommentUpvotePercent,
|
||||
setPostUpvotePercent,
|
||||
} from '../../../redux/actions/applicationActions';
|
||||
|
||||
// Utils
|
||||
import { isVoted as isVotedFunc, isDownVoted as isDownVotedFunc } from '../../../utils/postParser';
|
||||
|
||||
// Component
|
||||
import { updateVoteCache } from '../../../redux/actions/cacheActions';
|
||||
import { useAppDispatch, useAppSelector } from '../../../hooks';
|
||||
import { PostTypes } from '../../../constants/postTypes';
|
||||
|
||||
// Utils
|
||||
|
||||
import { getEstimatedAmount } from '../../../utils/vote';
|
||||
|
||||
// Components
|
||||
import { Icon } from '../../icon';
|
||||
|
||||
// Services
|
||||
import { setRcOffer, toastNotification } from '../../../redux/actions/uiAction';
|
||||
|
||||
// STEEM
|
||||
import { vote } from '../../../providers/hive/dhive';
|
||||
|
||||
// Styles
|
||||
import styles from '../children/upvoteStyles';
|
||||
|
||||
import { PointActivityIds } from '../../../providers/ecency/ecency.types';
|
||||
import { useUserActivityMutation } from '../../../providers/queries';
|
||||
import { PayoutDetailsContent } from '../children/payoutDetailsContent';
|
||||
import { CacheStatus } from '../../../redux/reducers/cacheReducer';
|
||||
import showLoginAlert from '../../../utils/showLoginAlert';
|
||||
import { delay } from '../../../utils/editor';
|
||||
|
||||
interface Props {}
|
||||
interface PopoverOptions {
|
||||
anchorRect: Rect;
|
||||
content: any;
|
||||
postType?: PostTypes;
|
||||
showPayoutDetails?: boolean;
|
||||
onVotingStart?: (isVoting: boolean) => void;
|
||||
}
|
||||
|
||||
/*
|
||||
* Props Name Description Value
|
||||
*@props --> props name here description here Value Type Here
|
||||
*
|
||||
*/
|
||||
|
||||
const UpvotePopover = forwardRef(({}: Props, ref) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const userActivityMutation = useUserActivityMutation();
|
||||
|
||||
const onVotingStartRef = useRef<any>(null);
|
||||
|
||||
const isLoggedIn = useAppSelector((state) => state.application.isLoggedIn);
|
||||
const postUpvotePercent = useAppSelector((state) => state.application.postUpvotePercent);
|
||||
const commentUpvotePercent = useAppSelector((state) => state.application.commentUpvotePercent);
|
||||
const pinCode = useAppSelector((state) => state.application.pin);
|
||||
const currentAccount = useAppSelector((state) => state.account.currentAccount);
|
||||
const globalProps = useAppSelector((state) => state.account.globalProps);
|
||||
|
||||
const [content, setContent] = useState<any>(null);
|
||||
const [postType, setPostType] = useState<PostTypes>(PostTypes.POST);
|
||||
const [anchorRect, setAcnhorRect] = useState<Rect | null>(null);
|
||||
const [showPayoutDetails, setShowPayoutDetails] = useState(false);
|
||||
|
||||
const [isVoted, setIsVoted] = useState<any>(null);
|
||||
const [isDownVoted, setIsDownVoted] = useState<any>(null);
|
||||
|
||||
const [sliderValue, setSliderValue] = useState(1);
|
||||
const [amount, setAmount] = useState('0.00000');
|
||||
const [upvotePercent, setUpvotePercent] = useState(1);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
showPopover: ({
|
||||
anchorRect: _anchorRect,
|
||||
content: _content,
|
||||
postType: _postType,
|
||||
showPayoutDetails: _showPayoutDetails,
|
||||
onVotingStart,
|
||||
}: PopoverOptions) => {
|
||||
if (!isLoggedIn && !_showPayoutDetails) {
|
||||
showLoginAlert({ intl });
|
||||
return;
|
||||
}
|
||||
|
||||
onVotingStartRef.current = onVotingStart;
|
||||
setPostType(_postType || PostTypes.POST);
|
||||
setContent(_content);
|
||||
setShowPayoutDetails(_showPayoutDetails || false);
|
||||
setAcnhorRect(_anchorRect);
|
||||
},
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
let _isMounted = true;
|
||||
const activeVotes = content?.active_votes || [];
|
||||
|
||||
const _calculateVoteStatus = async () => {
|
||||
const _isVoted = await isVotedFunc(activeVotes, get(currentAccount, 'name'));
|
||||
const _isDownVoted = await isDownVotedFunc(activeVotes, get(currentAccount, 'name'));
|
||||
|
||||
if (_isMounted) {
|
||||
setIsVoted(_isVoted && parseInt(_isVoted, 10) / 10000);
|
||||
setIsDownVoted(_isDownVoted && (parseInt(_isDownVoted, 10) / 10000) * -1);
|
||||
}
|
||||
};
|
||||
|
||||
_calculateVoteStatus();
|
||||
return () => {
|
||||
_isMounted = false;
|
||||
};
|
||||
}, [content]);
|
||||
|
||||
useEffect(() => {
|
||||
_calculateEstimatedAmount();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (postType === PostTypes.POST) {
|
||||
setUpvotePercent(postUpvotePercent);
|
||||
}
|
||||
if (postType === PostTypes.COMMENT) {
|
||||
setUpvotePercent(commentUpvotePercent);
|
||||
}
|
||||
}, [postUpvotePercent, commentUpvotePercent, postType]);
|
||||
|
||||
useEffect(() => {
|
||||
const value = isVoted || isDownVoted ? 1 : upvotePercent <= 1 ? upvotePercent : 1;
|
||||
|
||||
setSliderValue(value);
|
||||
_calculateEstimatedAmount(value);
|
||||
}, [upvotePercent]);
|
||||
|
||||
// Component Functions
|
||||
const _calculateEstimatedAmount = async (value: number = sliderValue) => {
|
||||
if (currentAccount && Object.entries(currentAccount).length !== 0) {
|
||||
setAmount(getEstimatedAmount(currentAccount, globalProps, value));
|
||||
}
|
||||
};
|
||||
|
||||
const _upvoteContent = async () => {
|
||||
if (!isDownVoted) {
|
||||
const _onVotingStart = onVotingStartRef.current; // keeping a reference of call to avoid mismatch in case back to back voting
|
||||
_closePopover();
|
||||
_onVotingStart ? _onVotingStart(1) : null;
|
||||
|
||||
await delay(300);
|
||||
|
||||
_setUpvotePercent(sliderValue);
|
||||
|
||||
const weight = sliderValue ? Math.trunc(sliderValue * 100) * 100 : 0;
|
||||
const _author = content?.author;
|
||||
const _permlink = content?.permlink;
|
||||
|
||||
console.log(`casting up vote: ${weight}`);
|
||||
_updateVoteCache(_author, _permlink, amount, false, CacheStatus.PENDING);
|
||||
|
||||
vote(currentAccount, pinCode, _author, _permlink, weight)
|
||||
.then((response) => {
|
||||
console.log('Vote response: ', response);
|
||||
// record user points
|
||||
userActivityMutation.mutate({
|
||||
pointsTy: PointActivityIds.VOTE,
|
||||
transactionId: response.id,
|
||||
});
|
||||
|
||||
if (!response || !response.id) {
|
||||
dispatch(
|
||||
toastNotification(
|
||||
intl.formatMessage(
|
||||
{ id: 'alert.something_wrong_msg' },
|
||||
{
|
||||
message: intl.formatMessage({
|
||||
id: 'alert.invalid_response',
|
||||
}),
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
setIsVoted(!!sliderValue);
|
||||
_updateVoteCache(_author, _permlink, amount, false, CacheStatus.PUBLISHED);
|
||||
})
|
||||
.catch((err) => {
|
||||
_updateVoteCache(_author, _permlink, amount, false, CacheStatus.FAILED);
|
||||
_onVotingStart ? _onVotingStart(0) : null;
|
||||
if (
|
||||
err &&
|
||||
err.response &&
|
||||
err.response.jse_shortmsg &&
|
||||
err.response.jse_shortmsg.includes('wait to transact')
|
||||
) {
|
||||
// when RC is not enough, offer boosting account
|
||||
setIsVoted(false);
|
||||
dispatch(setRcOffer(true));
|
||||
} else if (err && err.jse_shortmsg && err.jse_shortmsg.includes('wait to transact')) {
|
||||
// when RC is not enough, offer boosting account
|
||||
setIsVoted(false);
|
||||
dispatch(setRcOffer(true));
|
||||
} else {
|
||||
// // when voting with same percent or other errors
|
||||
let errMsg = '';
|
||||
if (err.message && err.message.indexOf(':') > 0) {
|
||||
errMsg = err.message.split(': ')[1];
|
||||
} else {
|
||||
errMsg = err.jse_shortmsg || err.error_description || err.message;
|
||||
}
|
||||
dispatch(
|
||||
toastNotification(
|
||||
intl.formatMessage({ id: 'alert.something_wrong_msg' }, { message: errMsg }),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
setSliderValue(1);
|
||||
setIsDownVoted(false);
|
||||
}
|
||||
};
|
||||
|
||||
const _downvoteContent = async () => {
|
||||
const _onVotingStart = onVotingStartRef.current; // keeping a reference of call to avoid mismatch in case back to back voting
|
||||
if (isDownVoted) {
|
||||
_closePopover();
|
||||
_onVotingStart ? _onVotingStart(-1) : null;
|
||||
|
||||
await delay(300);
|
||||
|
||||
_setUpvotePercent(sliderValue);
|
||||
|
||||
const weight = sliderValue ? Math.trunc(sliderValue * 100) * -100 : 0;
|
||||
const _author = content?.author;
|
||||
const _permlink = content?.permlink;
|
||||
|
||||
console.log(`casting down vote: ${weight}`);
|
||||
_updateVoteCache(_author, _permlink, amount, true, CacheStatus.PENDING);
|
||||
|
||||
vote(currentAccount, pinCode, _author, _permlink, weight)
|
||||
.then((response) => {
|
||||
// record usr points
|
||||
userActivityMutation.mutate({
|
||||
pointsTy: PointActivityIds.VOTE,
|
||||
transactionId: response.id,
|
||||
});
|
||||
setIsVoted(!!sliderValue);
|
||||
_updateVoteCache(_author, _permlink, amount, true, CacheStatus.PUBLISHED);
|
||||
})
|
||||
.catch((err) => {
|
||||
dispatch(
|
||||
toastNotification(
|
||||
intl.formatMessage({ id: 'alert.something_wrong_msg' }, { message: err.message }),
|
||||
),
|
||||
);
|
||||
_updateVoteCache(_author, _permlink, amount, true, CacheStatus.FAILED);
|
||||
setIsVoted(false);
|
||||
_onVotingStart ? _onVotingStart(0) : null;
|
||||
});
|
||||
} else {
|
||||
setSliderValue(1);
|
||||
setIsDownVoted(true);
|
||||
}
|
||||
};
|
||||
|
||||
const _setUpvotePercent = (value) => {
|
||||
if (value) {
|
||||
if (postType === PostTypes.POST) {
|
||||
dispatch(setPostUpvotePercent(value));
|
||||
}
|
||||
if (postType === PostTypes.COMMENT) {
|
||||
dispatch(setCommentUpvotePercent(value));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const _updateVoteCache = (author, permlink, amount, isDownvote, status) => {
|
||||
// do all relevant processing here to show local upvote
|
||||
const amountNum = parseFloat(amount);
|
||||
|
||||
let incrementStep = 0;
|
||||
if (!isVoted && !isDownVoted) {
|
||||
incrementStep = 1;
|
||||
}
|
||||
|
||||
// update redux
|
||||
const postPath = `${author || ''}/${permlink || ''}`;
|
||||
const curTime = new Date().getTime();
|
||||
const vote = {
|
||||
votedAt: curTime,
|
||||
amount: amountNum,
|
||||
isDownvote,
|
||||
incrementStep,
|
||||
voter: currentAccount.username,
|
||||
expiresAt: curTime + 30000,
|
||||
status,
|
||||
};
|
||||
dispatch(updateVoteCache(postPath, vote));
|
||||
};
|
||||
|
||||
const _closePopover = () => {
|
||||
setAcnhorRect(null);
|
||||
setTimeout(() => {
|
||||
setShowPayoutDetails(false);
|
||||
}, 300);
|
||||
};
|
||||
|
||||
if (!content) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const iconName = 'upcircleo';
|
||||
const downVoteIconName = 'downcircleo';
|
||||
|
||||
const _percent = `${isDownVoted ? '-' : ''}${(sliderValue * 100).toFixed(0)}%`;
|
||||
const _amount = `$${amount}`;
|
||||
|
||||
const sliderColor = isDownVoted ? '#ec8b88' : '#357ce6';
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<Popover
|
||||
contentStyle={showPayoutDetails ? styles.popoverDetails : styles.popoverSlider}
|
||||
arrowStyle={showPayoutDetails ? styles.arrow : styles.hideArrow}
|
||||
backgroundStyle={styles.overlay}
|
||||
visible={!!anchorRect}
|
||||
onClose={() => {
|
||||
_closePopover();
|
||||
}}
|
||||
fromRect={anchorRect || { x: 0, y: 0, width: 0, height: 0 }}
|
||||
placement="top"
|
||||
supportedOrientations={['portrait', 'landscape']}
|
||||
>
|
||||
<View style={styles.popoverWrapper}>
|
||||
{showPayoutDetails ? (
|
||||
<PayoutDetailsContent content={content} />
|
||||
) : (
|
||||
<Fragment>
|
||||
<TouchableOpacity onPress={_upvoteContent} style={styles.upvoteButton}>
|
||||
<Icon
|
||||
size={20}
|
||||
style={[styles.upvoteIcon, { color: '#007ee5' }]}
|
||||
active={!isLoggedIn}
|
||||
iconType="AntDesign"
|
||||
name={iconName}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
<Text style={styles.amount}>{_amount}</Text>
|
||||
<Slider
|
||||
style={styles.slider}
|
||||
minimumTrackTintColor={sliderColor}
|
||||
trackStyle={styles.track}
|
||||
thumbStyle={styles.thumb}
|
||||
thumbTintColor="#007ee5"
|
||||
minimumValue={0.01}
|
||||
maximumValue={1}
|
||||
value={sliderValue}
|
||||
onValueChange={(value) => {
|
||||
setSliderValue(value);
|
||||
_calculateEstimatedAmount(value);
|
||||
}}
|
||||
/>
|
||||
<Text style={styles.percent}>{_percent}</Text>
|
||||
<TouchableOpacity onPress={_downvoteContent} style={styles.upvoteButton}>
|
||||
<Icon
|
||||
size={20}
|
||||
style={[styles.upvoteIcon, { color: '#ec8b88' }]}
|
||||
active={!isLoggedIn}
|
||||
iconType="AntDesign"
|
||||
name={downVoteIconName}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</Fragment>
|
||||
)}
|
||||
</View>
|
||||
</Popover>
|
||||
</Fragment>
|
||||
);
|
||||
});
|
||||
|
||||
export default UpvotePopover;
|
4
src/components/upvotePopover/index.js
Normal file
4
src/components/upvotePopover/index.js
Normal file
@ -0,0 +1,4 @@
|
||||
import UpvotePopover from './container/upvotePopover';
|
||||
|
||||
export { UpvotePopover };
|
||||
export default UpvotePopover;
|
@ -1,7 +1,4 @@
|
||||
const POST = 'post';
|
||||
const COMMENT = 'comment';
|
||||
|
||||
export default {
|
||||
POST,
|
||||
COMMENT,
|
||||
};
|
||||
export enum PostTypes {
|
||||
POST = 'post',
|
||||
COMMENT = 'comment',
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ export const initQueryClient = () => {
|
||||
//Query client configurations go here...
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
cacheTime: 1000 * 60 * 60 * 24, // 24 hours
|
||||
cacheTime: 1000 * 60 * 60 * 24 * 6 , // 7 days cache timer
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -1,39 +1,66 @@
|
||||
import { renderPostBody } from '@ecency/render-helper';
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { Platform } from 'react-native';
|
||||
import { isArray } from 'lodash';
|
||||
import { useAppSelector } from '../../../hooks';
|
||||
import { getDiscussionCollection, getPost } from '../../hive/dhive';
|
||||
import QUERIES from '../queryKeys';
|
||||
import { Comment, CommentCacheStatus, LastUpdateMeta } from '../../../redux/reducers/cacheReducer';
|
||||
import { Comment, CacheStatus, LastUpdateMeta } from '../../../redux/reducers/cacheReducer';
|
||||
|
||||
/** hook used to return user drafts */
|
||||
export const useGetPostQuery = (_author?: string, _permlink?: string) => {
|
||||
export const useGetPostQuery = (_author?: string, _permlink?: string, initialPost?: any) => {
|
||||
const currentAccount = useAppSelector((state) => state.account.currentAccount);
|
||||
|
||||
const [author, setAuthor] = useState(_author);
|
||||
const [permlink, setPermlink] = useState(_permlink);
|
||||
|
||||
const query = useQuery([QUERIES.POST.GET, author, permlink], async () => {
|
||||
if (!author || !permlink) {
|
||||
return null;
|
||||
// post process initial post if available
|
||||
const _initialPost = useMemo(() => {
|
||||
const _post = initialPost;
|
||||
if (!_post) {
|
||||
return _post;
|
||||
}
|
||||
|
||||
try {
|
||||
const post = await getPost(author, permlink, currentAccount?.username);
|
||||
if (post?.post_id > 0) {
|
||||
return post;
|
||||
_post.body = renderPostBody(
|
||||
{ ..._post, last_update: _post.updated },
|
||||
true,
|
||||
Platform.OS !== 'ios',
|
||||
);
|
||||
|
||||
return _post;
|
||||
}, [initialPost?.body]);
|
||||
|
||||
const query = useQuery(
|
||||
[QUERIES.POST.GET, author, permlink],
|
||||
async () => {
|
||||
if (!author || !permlink) {
|
||||
return null;
|
||||
}
|
||||
|
||||
new Error('Post unavailable');
|
||||
} catch (err) {
|
||||
console.warn('Failed to get post', err);
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
try {
|
||||
const post = await getPost(author, permlink, currentAccount?.username);
|
||||
if (post?.post_id > 0) {
|
||||
return post;
|
||||
}
|
||||
|
||||
new Error('Post unavailable');
|
||||
} catch (err) {
|
||||
console.warn('Failed to get post', err);
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
{
|
||||
initialData: _initialPost,
|
||||
cacheTime: 30 * 60 * 1000, // keeps cache for 30 minutes
|
||||
},
|
||||
);
|
||||
|
||||
const data = useInjectVotesCache(query.data);
|
||||
|
||||
return {
|
||||
...query,
|
||||
data,
|
||||
setAuthor,
|
||||
setPermlink,
|
||||
};
|
||||
@ -73,6 +100,9 @@ export const useDiscussionQuery = (_author?: string, _permlink?: string) => {
|
||||
const cachedComments: { [key: string]: Comment } = useAppSelector(
|
||||
(state) => state.cache.commentsCollection,
|
||||
);
|
||||
const cachedVotes: { [key: string]: Comment } = useAppSelector(
|
||||
(state) => state.cache.votesCollection,
|
||||
);
|
||||
const lastCacheUpdate: LastUpdateMeta = useAppSelector((state) => state.cache.lastUpdate);
|
||||
|
||||
const [author, setAuthor] = useState(_author);
|
||||
@ -85,24 +115,39 @@ export const useDiscussionQuery = (_author?: string, _permlink?: string) => {
|
||||
const query = useQuery<{ [key: string]: Comment }>(
|
||||
[QUERIES.POST.GET_DISCUSSION, author, permlink],
|
||||
_fetchComments,
|
||||
{
|
||||
cacheTime: 5 * 60 * 1000, // keeps comments cache for 5 minutes
|
||||
},
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
_injectCachedComments();
|
||||
}, [query.data, cachedComments]);
|
||||
_injectCache();
|
||||
}, [query.data, cachedComments, cachedVotes]);
|
||||
|
||||
useEffect(() => {
|
||||
restructureData();
|
||||
}, [data]);
|
||||
|
||||
// inject cached comments here
|
||||
const _injectCachedComments = async () => {
|
||||
const _injectCache = async () => {
|
||||
let shouldClone = false;
|
||||
const _comments = query.data || {};
|
||||
console.log('updating with cache', _comments, cachedComments);
|
||||
if (!cachedComments || !_comments) {
|
||||
console.log('Skipping cache injection');
|
||||
return _comments;
|
||||
}
|
||||
|
||||
// process votes cache
|
||||
for (const path in cachedVotes) {
|
||||
const cachedVote = cachedVotes[path];
|
||||
if (_comments[path]) {
|
||||
console.log('injection vote cache');
|
||||
_comments[path] = _injectVoteFunc(_comments[path], cachedVote);
|
||||
}
|
||||
}
|
||||
|
||||
// process comments cache
|
||||
for (const path in cachedComments) {
|
||||
const currentTime = new Date().getTime();
|
||||
const cachedComment = cachedComments[path];
|
||||
@ -110,25 +155,29 @@ export const useDiscussionQuery = (_author?: string, _permlink?: string) => {
|
||||
const cacheUpdateTimestamp = new Date(cachedComment.updated || 0).getTime();
|
||||
|
||||
switch (cachedComment.status) {
|
||||
case CommentCacheStatus.DELETED:
|
||||
case CacheStatus.DELETED:
|
||||
if (_comments && _comments[path]) {
|
||||
delete _comments[path];
|
||||
shouldClone = true;
|
||||
}
|
||||
break;
|
||||
case CommentCacheStatus.UPDATED:
|
||||
case CommentCacheStatus.PENDING:
|
||||
case CacheStatus.UPDATED:
|
||||
case CacheStatus.PENDING:
|
||||
|
||||
// check if commentKey already exist in comments map,
|
||||
if (_comments[path]) {
|
||||
shouldClone = true;
|
||||
// check if we should update comments map with cached map based on updat timestamp
|
||||
const remoteUpdateTimestamp = new Date(_comments[path].updated).getTime();
|
||||
|
||||
if (cacheUpdateTimestamp > remoteUpdateTimestamp) {
|
||||
_comments[path] = cachedComment;
|
||||
_comments[path].body = cachedComment.body;
|
||||
}
|
||||
}
|
||||
|
||||
// if comment key do not exist, possiblky comment is a new comment, in this case, check if parent of comment exist in map
|
||||
else if (_comments[_parentPath]) {
|
||||
shouldClone = true;
|
||||
// in this case add comment key in childern and inject cachedComment in commentsMap
|
||||
_comments[path] = cachedComment;
|
||||
_comments[_parentPath].replies.push(path);
|
||||
@ -148,7 +197,7 @@ export const useDiscussionQuery = (_author?: string, _permlink?: string) => {
|
||||
}
|
||||
}
|
||||
|
||||
setData({ ..._comments });
|
||||
setData(shouldClone ? { ..._comments } : _comments);
|
||||
};
|
||||
|
||||
// traverse discussion collection to curate sections
|
||||
@ -219,3 +268,93 @@ export const useDiscussionQuery = (_author?: string, _permlink?: string) => {
|
||||
setPermlink,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param _data single post content or array of posts
|
||||
* @returns post data or array of data with votes cache injected
|
||||
*/
|
||||
export const useInjectVotesCache = (_data: any | any[]) => {
|
||||
const votesCollection = useAppSelector((state) => state.cache.votesCollection);
|
||||
const lastUpdate = useAppSelector((state) => state.cache.lastUpdate);
|
||||
const [retData, setRetData] = useState<any | any[] | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (retData && lastUpdate.type === 'vote') {
|
||||
const _postPath = lastUpdate.postPath;
|
||||
const _voteCache = votesCollection[_postPath];
|
||||
|
||||
let _postData: any = null;
|
||||
let _postIndex = -1;
|
||||
|
||||
// get post data that need updating
|
||||
const _comparePath = (item) => _postPath === `${item.author}/${item.permlink}`;
|
||||
if (isArray(retData)) {
|
||||
_postIndex = retData.findIndex(_comparePath);
|
||||
_postData = retData[_postIndex];
|
||||
} else if (retData && _comparePath(retData)) {
|
||||
_postData = retData;
|
||||
}
|
||||
|
||||
// if post available, inject cache and update state
|
||||
if (_postData) {
|
||||
_postData = _injectVoteFunc(_postData, _voteCache);
|
||||
|
||||
if (_postIndex < 0) {
|
||||
console.log('updating data', _postData);
|
||||
setRetData({ ..._postData });
|
||||
} else {
|
||||
retData[_postIndex] = _postData;
|
||||
setRetData([...retData]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [votesCollection]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!_data) {
|
||||
setRetData(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const _itemFunc = (item) => {
|
||||
if (item) {
|
||||
const _path = `${item.author}/${item.permlink}`;
|
||||
const voteCache = votesCollection[_path];
|
||||
|
||||
item = _injectVoteFunc(item, voteCache);
|
||||
}
|
||||
return item;
|
||||
};
|
||||
|
||||
const _cData = isArray(_data) ? _data.map(_itemFunc) : _itemFunc({ ..._data });
|
||||
console.log('data received', _cData.length, _cData);
|
||||
setRetData(_cData);
|
||||
}, [_data]);
|
||||
|
||||
return retData || _data;
|
||||
};
|
||||
|
||||
const _injectVoteFunc = (post, voteCache) => {
|
||||
if (
|
||||
voteCache &&
|
||||
(voteCache.status !== CacheStatus.FAILED || voteCache.status !== CacheStatus.DELETED)
|
||||
) {
|
||||
const _voteIndex = post.active_votes.findIndex((i) => i.voter === voteCache.voter);
|
||||
if (_voteIndex < 0) {
|
||||
post.total_payout += voteCache.amount * (voteCache.isDownvote ? -1 : 1);
|
||||
post.active_votes = [
|
||||
...post.active_votes,
|
||||
{
|
||||
voter: voteCache.voter,
|
||||
rshares: voteCache.isDownvote ? -1000 : 1000,
|
||||
},
|
||||
];
|
||||
} else {
|
||||
post.active_votes[_voteIndex].rshares = voteCache.isDownvote ? -1000 : 1000;
|
||||
post.active_votes = [...post.active_votes];
|
||||
}
|
||||
}
|
||||
|
||||
return post;
|
||||
};
|
||||
|
@ -20,7 +20,7 @@ import {
|
||||
import {
|
||||
ClaimCache,
|
||||
Comment,
|
||||
CommentCacheStatus,
|
||||
CacheStatus,
|
||||
Draft,
|
||||
SubscribedCommunity,
|
||||
Vote,
|
||||
@ -72,7 +72,7 @@ export const updateCommentCache = (
|
||||
comment.children = 0;
|
||||
comment.replies = [];
|
||||
comment.isDeletable = comment.isDeletable || true;
|
||||
comment.status = comment.status || CommentCacheStatus.PENDING;
|
||||
comment.status = comment.status || CacheStatus.PENDING;
|
||||
|
||||
comment.body = renderPostBody(
|
||||
{
|
||||
|
@ -15,9 +15,11 @@ import {
|
||||
DELETE_CLAIM_CACHE_ENTRY,
|
||||
} from '../constants/constants';
|
||||
|
||||
export enum CommentCacheStatus {
|
||||
export enum CacheStatus {
|
||||
PENDING = 'PENDING',
|
||||
POSTPONED = 'PUBLISHED',
|
||||
PUBLISHED = 'PUBLISHED',
|
||||
POSTPONED = 'POSTPONED',
|
||||
FAILED = 'FAILED',
|
||||
DELETED = 'DELETED',
|
||||
UPDATED = 'UPDATED',
|
||||
}
|
||||
@ -28,6 +30,7 @@ export interface Vote {
|
||||
incrementStep: number;
|
||||
votedAt: number;
|
||||
expiresAt: number;
|
||||
status: CacheStatus;
|
||||
}
|
||||
|
||||
export interface Comment {
|
||||
@ -50,7 +53,7 @@ export interface Comment {
|
||||
expiresAt?: number;
|
||||
expandedReplies?: boolean;
|
||||
renderOnTop?: boolean;
|
||||
status: CommentCacheStatus;
|
||||
status: CacheStatus;
|
||||
}
|
||||
|
||||
export interface Draft {
|
||||
@ -86,7 +89,7 @@ export interface LastUpdateMeta {
|
||||
}
|
||||
|
||||
interface State {
|
||||
votes: Map<string, Vote>;
|
||||
votesCollection: { [key: string]: Vote };
|
||||
commentsCollection: { [key: string]: Comment }; // TODO: handle comment array per post, if parent is same
|
||||
draftsCollection: { [key: string]: Draft };
|
||||
claimsCollection: ClaimsCollection;
|
||||
@ -96,7 +99,7 @@ interface State {
|
||||
}
|
||||
|
||||
const initialState: State = {
|
||||
votes: new Map(),
|
||||
votesCollection: {},
|
||||
commentsCollection: {},
|
||||
draftsCollection: {},
|
||||
claimsCollection: {},
|
||||
@ -109,10 +112,10 @@ export default function (state = initialState, action) {
|
||||
const { type, payload } = action;
|
||||
switch (type) {
|
||||
case UPDATE_VOTE_CACHE:
|
||||
if (!state.votes) {
|
||||
state.votes = new Map<string, Vote>();
|
||||
if (!state.votesCollection) {
|
||||
state.votesCollection = {};
|
||||
}
|
||||
state.votes.set(payload.postPath, payload.vote);
|
||||
state.votesCollection = { ...state.votesCollection, [payload.postPath]: payload.vote };
|
||||
return {
|
||||
...state, // spread operator in requried here, otherwise persist do not register change
|
||||
lastUpdate: {
|
||||
@ -245,19 +248,22 @@ export default function (state = initialState, action) {
|
||||
case PURGE_EXPIRED_CACHE:
|
||||
const currentTime = new Date().getTime();
|
||||
|
||||
if (state.votes && state.votes.size) {
|
||||
Array.from(state.votes).forEach((entry) => {
|
||||
if (entry[1].expiresAt < currentTime) {
|
||||
state.votes.delete(entry[0]);
|
||||
if (state.votesCollection) {
|
||||
for (const key in state.votesCollection) {
|
||||
if (state.votesCollection.hasOwnProperty(key)) {
|
||||
const vote = state.votesCollection[key];
|
||||
if (vote && (vote?.expiresAt || 0) < currentTime) {
|
||||
delete state.votesCollection[key];
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (state.commentsCollection) {
|
||||
for (const key in state.commentsCollection) {
|
||||
if (state.commentsCollection.hasOwnProperty(key)) {
|
||||
const draft = state.commentsCollection[key];
|
||||
if (draft && (draft?.expiresAt || 0) < currentTime) {
|
||||
const comment = state.commentsCollection[key];
|
||||
if (comment && (comment?.expiresAt || 0) < currentTime) {
|
||||
delete state.commentsCollection[key];
|
||||
}
|
||||
}
|
||||
|
@ -11,13 +11,11 @@ import MigrationHelpers from '../../utils/migrationHelpers';
|
||||
const transformCacheVoteMap = createTransform(
|
||||
(inboundState: any) => ({
|
||||
...inboundState,
|
||||
votes: Array.from(inboundState.votes),
|
||||
subscribedCommunities: Array.from(inboundState.subscribedCommunities),
|
||||
pointActivities: Array.from(inboundState.pointActivities),
|
||||
}),
|
||||
(outboundState) => ({
|
||||
...outboundState,
|
||||
votes: new Map(outboundState.votes),
|
||||
subscribedCommunities: new Map(outboundState.subscribedCommunities),
|
||||
pointActivities: new Map(outboundState.pointActivities),
|
||||
}),
|
||||
@ -39,7 +37,7 @@ const persistConfig = {
|
||||
key: 'root',
|
||||
// Storage Method (React Native)
|
||||
storage: AsyncStorage,
|
||||
version: 4, // New version 0, default or previous version -1, versions are useful migrations
|
||||
version: 5, // New version 0, default or previous version -1, versions are useful migrations
|
||||
// // Blacklist (Don't Save Specific Reducers)
|
||||
blacklist: ['communities', 'user', 'ui'],
|
||||
transforms: [transformCacheVoteMap, transformWalkthroughMap],
|
||||
|
@ -12,6 +12,7 @@ import {
|
||||
import notifee, { EventType } from '@notifee/react-native';
|
||||
import { isEmpty, some, get } from 'lodash';
|
||||
import messaging from '@react-native-firebase/messaging';
|
||||
import BackgroundTimer from 'react-native-background-timer';
|
||||
import { useAppDispatch, useAppSelector } from '../../../hooks';
|
||||
import { setDeviceOrientation, setLockedOrientation } from '../../../redux/actions/uiAction';
|
||||
import { orientations } from '../../../redux/constants/orientationsConstants';
|
||||
@ -53,6 +54,8 @@ export const useInitApplication = () => {
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
BackgroundTimer.start(); // ref: https://github.com/ocetnik/react-native-background-timer#ios
|
||||
|
||||
appStateSubRef.current = AppState.addEventListener('change', _handleAppStateChange);
|
||||
|
||||
// check for device landscape status and lcok orientation accordingly. Fix for orientation bug on android tablet devices
|
||||
@ -97,6 +100,8 @@ export const useInitApplication = () => {
|
||||
if (messagingEventRef.current) {
|
||||
messagingEventRef.current();
|
||||
}
|
||||
|
||||
BackgroundTimer.stop(); // ref: https://github.com/ocetnik/react-native-background-timer#ios
|
||||
};
|
||||
|
||||
const _initPushListener = async () => {
|
||||
|
@ -100,7 +100,7 @@ export default EStyleSheet.create({
|
||||
|
||||
//COIN ACTIONS STYLES
|
||||
actionBtnContainer: {
|
||||
flexGrow: 1,
|
||||
|
||||
} as ViewStyle,
|
||||
actionsContainer: {
|
||||
flexDirection: 'row',
|
||||
@ -115,6 +115,7 @@ export default EStyleSheet.create({
|
||||
borderRadius: 20,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
flexGrow: 1,
|
||||
} as ViewStyle,
|
||||
actionText: {
|
||||
color: '$primaryBlack',
|
||||
|
@ -1,7 +1,6 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { View, Text } from 'react-native';
|
||||
import { TouchableOpacity } from 'react-native-gesture-handler';
|
||||
import { View, Text, TouchableOpacity } from 'react-native';
|
||||
import styles from './children.styles';
|
||||
|
||||
interface CoinActionsProps {
|
||||
@ -21,7 +20,6 @@ export const CoinActions = ({ actions, onActionPress }: CoinActionsProps) => {
|
||||
<TouchableOpacity
|
||||
key={`action-${item}-${index}`}
|
||||
style={styles.actionContainer}
|
||||
containerStyle={styles.actionBtnContainer}
|
||||
onPress={_onPress}
|
||||
>
|
||||
<Fragment>
|
||||
|
@ -1,7 +1,6 @@
|
||||
import React, { useState } from 'react';
|
||||
import { View, Text } from 'react-native';
|
||||
import { View, Text, TouchableOpacity } from 'react-native';
|
||||
import EStyleSheet from 'react-native-extended-stylesheet';
|
||||
import { TouchableOpacity } from 'react-native-gesture-handler';
|
||||
import styles from './children.styles';
|
||||
|
||||
interface RangeOption {
|
||||
|
@ -48,6 +48,7 @@ import QUERIES from '../../../providers/queries/queryKeys';
|
||||
import bugsnapInstance from '../../../config/bugsnag';
|
||||
import { useUserActivityMutation } from '../../../providers/queries/pointQueries';
|
||||
import { PointActivityIds } from '../../../providers/ecency/ecency.types';
|
||||
import { usePostsCachePrimer } from '../../../providers/queries/postQueries/postQueries';
|
||||
|
||||
/*
|
||||
* Props Name Description Value
|
||||
@ -754,7 +755,7 @@ class EditorContainer extends Component<EditorContainerProps, any> {
|
||||
};
|
||||
|
||||
_submitEdit = async (fields) => {
|
||||
const { currentAccount, pinCode, dispatch } = this.props;
|
||||
const { currentAccount, pinCode, dispatch, postCachePrimer } = this.props;
|
||||
const { post, isEdit, isPostSending, thumbUrl, isReply } = this.state;
|
||||
|
||||
if (isPostSending) {
|
||||
@ -806,10 +807,10 @@ class EditorContainer extends Component<EditorContainerProps, any> {
|
||||
isEdit,
|
||||
)
|
||||
.then(() => {
|
||||
const author = currentAccount.name;
|
||||
this._handleSubmitSuccess();
|
||||
if (isReply) {
|
||||
AsyncStorage.setItem('temp-reply', '');
|
||||
const author = currentAccount.name;
|
||||
dispatch(
|
||||
updateCommentCache(
|
||||
`${author}/${permlink}`,
|
||||
@ -831,6 +832,16 @@ class EditorContainer extends Component<EditorContainerProps, any> {
|
||||
},
|
||||
),
|
||||
);
|
||||
} else {
|
||||
//update post query data
|
||||
postCachePrimer.cachePost({
|
||||
...post,
|
||||
title,
|
||||
body,
|
||||
json_metadata: jsonMeta,
|
||||
markdownBody: body,
|
||||
updated: new Date().toISOString()
|
||||
})
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
@ -871,20 +882,15 @@ class EditorContainer extends Component<EditorContainerProps, any> {
|
||||
};
|
||||
|
||||
_handleSubmitSuccess = () => {
|
||||
const { navigation, route } = this.props;
|
||||
const { navigation, } = this.props;
|
||||
|
||||
if (navigation) {
|
||||
navigation.goBack();
|
||||
}
|
||||
this.setState({
|
||||
isPostSending: false,
|
||||
});
|
||||
|
||||
this.stateTimer = setTimeout(() => {
|
||||
if (navigation) {
|
||||
navigation.goBack();
|
||||
}
|
||||
if (route.params?.fetchPost) {
|
||||
route.params.fetchPost();
|
||||
}
|
||||
this.setState({
|
||||
isPostSending: false,
|
||||
});
|
||||
clearTimeout(this.stateTimer);
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
_handleSubmit = (form: any) => {
|
||||
@ -1120,7 +1126,7 @@ class EditorContainer extends Component<EditorContainerProps, any> {
|
||||
handleShouldReblogChange={this._handleShouldReblogChange}
|
||||
handleSchedulePress={this._handleSchedulePress}
|
||||
handleFormChanged={this._handleFormChanged}
|
||||
handleOnBackPress={() => {}}
|
||||
handleOnBackPress={() => { }}
|
||||
handleOnSubmit={this._handleSubmit}
|
||||
initialEditor={this._initialEditor}
|
||||
isDarkTheme={isDarkTheme}
|
||||
@ -1167,6 +1173,7 @@ const mapStateToProps = (state) => ({
|
||||
const mapQueriesToProps = () => ({
|
||||
queryClient: useQueryClient(),
|
||||
userActivityMutation: useUserActivityMutation(),
|
||||
postCachePrimer: usePostsCachePrimer()
|
||||
});
|
||||
|
||||
export default gestureHandlerRootHOC(
|
||||
|
@ -1,80 +0,0 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
// Services and Action
|
||||
import { gestureHandlerRootHOC } from 'react-native-gesture-handler';
|
||||
|
||||
// Component
|
||||
import PostScreen from '../screen/postScreen';
|
||||
import { postQueries } from '../../../providers/queries';
|
||||
|
||||
/*
|
||||
* Props Name Description Value
|
||||
*@props --> content which is include all post data Object
|
||||
*
|
||||
*/
|
||||
const PostContainer = ({ currentAccount, isLoggedIn, route }) => {
|
||||
const params = route.params || {};
|
||||
|
||||
const [author, setAuthor] = useState(params.content?.author || params.author || 'demo.com');
|
||||
const [permlink, setPermlink] = useState(
|
||||
params.content?.permlink || params.permlink || 'dev-test-tag-test-going',
|
||||
);
|
||||
|
||||
// refs
|
||||
const isNewPost = useRef(route.params?.isNewPost).current;
|
||||
|
||||
const getPostQuery = postQueries.useGetPostQuery(author, permlink);
|
||||
const getParentPostQuery = postQueries.useGetPostQuery();
|
||||
|
||||
useEffect(() => {
|
||||
const post = getPostQuery.data;
|
||||
if (post) {
|
||||
if (post && post.depth > 0 && post.parent_author && post.parent_permlink) {
|
||||
getParentPostQuery.setAuthor(post.parent_author);
|
||||
getParentPostQuery.setPermlink(post.parent_permlink);
|
||||
}
|
||||
}
|
||||
}, [getPostQuery.data]);
|
||||
|
||||
// Component Functions
|
||||
const _loadPost = async (_author = null, _permlink = null) => {
|
||||
if (_author && _permlink && _author !== author && _permlink !== _permlink) {
|
||||
setAuthor(_author);
|
||||
setPermlink(_permlink);
|
||||
}
|
||||
getPostQuery.refetch();
|
||||
};
|
||||
|
||||
// useEffect(() => {
|
||||
// const { isFetch: nextIsFetch } = route.params ?? {};
|
||||
// if (nextIsFetch) {
|
||||
// const { author: _author, permlink } = route.params;
|
||||
// _loadPost(_author, permlink);
|
||||
// }
|
||||
// }, [route.params.isFetch]);
|
||||
|
||||
const _isPostUnavailable = !getPostQuery.isLoading && getPostQuery.error;
|
||||
|
||||
return (
|
||||
<PostScreen
|
||||
post={getPostQuery.data}
|
||||
currentAccount={currentAccount}
|
||||
author={author}
|
||||
permlink={permlink}
|
||||
fetchPost={_loadPost}
|
||||
isFetchComments
|
||||
isLoggedIn={isLoggedIn}
|
||||
isNewPost={isNewPost}
|
||||
parentPost={getParentPostQuery.data}
|
||||
isPostUnavailable={_isPostUnavailable}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
currentAccount: state.account.currentAccount,
|
||||
isLoggedIn: state.application.isLoggedIn,
|
||||
});
|
||||
|
||||
export default gestureHandlerRootHOC(connect(mapStateToProps)(PostContainer));
|
@ -1,5 +1,4 @@
|
||||
import PostScreen from './screen/postScreen';
|
||||
import Post from './container/postContainer';
|
||||
import Post from './screen/postScreen';
|
||||
|
||||
export { PostScreen, Post };
|
||||
export { Post };
|
||||
export default Post;
|
||||
|
@ -1,43 +0,0 @@
|
||||
import React, { Fragment } from 'react';
|
||||
|
||||
// Components
|
||||
import { BasicHeader, PostDisplay, PostDropdown } from '../../../components';
|
||||
|
||||
const PostScreen = ({
|
||||
currentAccount,
|
||||
fetchPost,
|
||||
isFetchComments,
|
||||
isLoggedIn,
|
||||
isNewPost,
|
||||
parentPost,
|
||||
post,
|
||||
isPostUnavailable,
|
||||
author,
|
||||
permlink,
|
||||
}) => {
|
||||
return (
|
||||
<Fragment>
|
||||
<BasicHeader
|
||||
isHasDropdown
|
||||
title="Post"
|
||||
content={post}
|
||||
dropdownComponent={<PostDropdown content={post} fetchPost={fetchPost} />}
|
||||
isNewPost={isNewPost}
|
||||
/>
|
||||
<PostDisplay
|
||||
author={author}
|
||||
permlink={permlink}
|
||||
currentAccount={currentAccount}
|
||||
isPostUnavailable={isPostUnavailable}
|
||||
fetchPost={fetchPost}
|
||||
isFetchComments={isFetchComments}
|
||||
isLoggedIn={isLoggedIn}
|
||||
isNewPost={isNewPost}
|
||||
parentPost={parentPost}
|
||||
post={post}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default PostScreen;
|
85
src/screens/post/screen/postScreen.tsx
Normal file
85
src/screens/post/screen/postScreen.tsx
Normal file
@ -0,0 +1,85 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { View } from 'react-native';
|
||||
|
||||
// Components
|
||||
import { BasicHeader, IconButton, PostDisplay, PostOptionsModal } from '../../../components';
|
||||
import styles from '../styles/postScreen.styles';
|
||||
|
||||
// Component
|
||||
import { postQueries } from '../../../providers/queries';
|
||||
|
||||
const PostScreen = ({ route }) => {
|
||||
const params = route.params || {};
|
||||
|
||||
// // refs
|
||||
const isNewPost = useRef(route.params?.isNewPost).current;
|
||||
const postOptionsModalRef = useRef<typeof PostOptionsModal | null>(null);
|
||||
|
||||
const [author, setAuthor] = useState(params.content?.author || params.author);
|
||||
const [permlink, setPermlink] = useState(params.content?.permlink || params.permlink);
|
||||
|
||||
const getPostQuery = postQueries.useGetPostQuery(author, permlink, params.content);
|
||||
const getParentPostQuery = postQueries.useGetPostQuery();
|
||||
|
||||
useEffect(() => {
|
||||
const post = getPostQuery.data;
|
||||
if (post) {
|
||||
if (post && post.depth > 0 && post.parent_author && post.parent_permlink) {
|
||||
getParentPostQuery.setAuthor(post.parent_author);
|
||||
getParentPostQuery.setPermlink(post.parent_permlink);
|
||||
}
|
||||
}
|
||||
}, [getPostQuery.data]);
|
||||
|
||||
// // Component Functions
|
||||
const _loadPost = async (_author = null, _permlink = null) => {
|
||||
if (_author && _permlink && _author !== author && _permlink !== _permlink) {
|
||||
setAuthor(_author);
|
||||
setPermlink(_permlink);
|
||||
}
|
||||
getPostQuery.refetch();
|
||||
};
|
||||
|
||||
const _isPostUnavailable = !getPostQuery.isLoading && getPostQuery.error;
|
||||
|
||||
const _onPostOptionsBtnPress = (content = getPostQuery.data) => {
|
||||
if (postOptionsModalRef.current) {
|
||||
postOptionsModalRef.current.show(content);
|
||||
}
|
||||
};
|
||||
|
||||
const _postOptionsBtn = (
|
||||
<IconButton
|
||||
iconStyle={styles.optionsIcon}
|
||||
iconType="MaterialCommunityIcons"
|
||||
name="dots-vertical"
|
||||
onPress={_onPostOptionsBtnPress}
|
||||
size={24}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<BasicHeader
|
||||
isHasDropdown={true}
|
||||
title="Post"
|
||||
content={getPostQuery.data}
|
||||
dropdownComponent={_postOptionsBtn}
|
||||
isNewPost={isNewPost}
|
||||
/>
|
||||
<PostDisplay
|
||||
author={author}
|
||||
permlink={permlink}
|
||||
isPostUnavailable={_isPostUnavailable}
|
||||
fetchPost={_loadPost}
|
||||
isFetchComments={true}
|
||||
isNewPost={isNewPost}
|
||||
parentPost={getParentPostQuery.data}
|
||||
post={getPostQuery.data}
|
||||
/>
|
||||
<PostOptionsModal ref={postOptionsModalRef} />
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default PostScreen;
|
@ -1,9 +1,10 @@
|
||||
import EStyleSheet from 'react-native-extended-stylesheet';
|
||||
|
||||
export default EStyleSheet.create({
|
||||
icon: {
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
optionsIcon: {
|
||||
color: '$iconColor',
|
||||
marginRight: 2.7,
|
||||
fontSize: 25,
|
||||
},
|
||||
});
|
@ -16,10 +16,20 @@ import PostsResultsContainer from '../container/postsResultsContainer';
|
||||
|
||||
import { getTimeFromNow } from '../../../../../../utils/time';
|
||||
import styles from './postsResultsStyles';
|
||||
import { useAppDispatch } from '../../../../../../hooks';
|
||||
import { showProfileModal } from '../../../../../../redux/actions/uiAction';
|
||||
|
||||
const filterOptions = ['relevance', 'popularity', 'newest'];
|
||||
|
||||
const PostsResults = ({ navigation, searchValue }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const _showProfileModal = (username) => {
|
||||
if (username) {
|
||||
dispatch(showProfileModal(username));
|
||||
}
|
||||
};
|
||||
|
||||
const _renderItem = (item, index) => {
|
||||
const reputation =
|
||||
get(item, 'author_rep', undefined) || get(item, 'author_reputation', undefined);
|
||||
@ -35,6 +45,7 @@ const PostsResults = ({ navigation, searchValue }) => {
|
||||
reputation={Math.floor(reputation)}
|
||||
size={36}
|
||||
content={item}
|
||||
profileOnPress={_showProfileModal}
|
||||
/>
|
||||
<View style={[styles.postDescription]}>
|
||||
<Text style={styles.title}>{item.title}</Text>
|
||||
|
@ -1,11 +1,19 @@
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import { View, Text, Platform, ScrollView, KeyboardAvoidingView, Alert } from 'react-native';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
Platform,
|
||||
ScrollView,
|
||||
KeyboardAvoidingView,
|
||||
Alert,
|
||||
TouchableOpacity,
|
||||
} from 'react-native';
|
||||
import { WebView } from 'react-native-webview';
|
||||
import { injectIntl } from 'react-intl';
|
||||
import Slider from '@esteemapp/react-native-slider';
|
||||
import get from 'lodash/get';
|
||||
import Animated, { BounceInRight } from 'react-native-reanimated';
|
||||
import { TouchableOpacity, FlatList } from 'react-native-gesture-handler';
|
||||
import { FlatList } from 'react-native-gesture-handler';
|
||||
|
||||
// Constants
|
||||
import { debounce } from 'lodash';
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Clipboard } from 'react-native';
|
||||
import Clipboard from '@react-native-clipboard/clipboard';
|
||||
|
||||
const readFromClipboard = async () => {
|
||||
const clipboardContent = await Clipboard.getString();
|
||||
|
@ -206,6 +206,10 @@ const reduxMigrations = {
|
||||
delete state.cache.comments;
|
||||
return state;
|
||||
},
|
||||
5: (state) => {
|
||||
state.cache.votesCollection = {};
|
||||
return state;
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
|
13
yarn.lock
13
yarn.lock
@ -9074,6 +9074,11 @@ react-native-autoheight-webview@^1.5.8:
|
||||
deprecated-react-native-prop-types "^2.3.0"
|
||||
prop-types "^15.7.2"
|
||||
|
||||
react-native-background-timer@^2.4.1:
|
||||
version "2.4.1"
|
||||
resolved "https://registry.yarnpkg.com/react-native-background-timer/-/react-native-background-timer-2.4.1.tgz#a3bc1cafa8c1e3aeefd0611de120298b67978a0f"
|
||||
integrity sha512-TE4Kiy7jUyv+hugxDxitzu38sW1NqjCk4uE5IgU2WevLv7sZacaBc6PZKOShNRPGirLl1NWkaG3LDEkdb9Um5g==
|
||||
|
||||
react-native-blob-util@^0.16.0:
|
||||
version "0.16.3"
|
||||
resolved "https://registry.yarnpkg.com/react-native-blob-util/-/react-native-blob-util-0.16.3.tgz#de6b2a78c7dfd09665d033602a6b108a3b85937b"
|
||||
@ -9200,10 +9205,10 @@ react-native-flipper@^0.164.0:
|
||||
resolved "https://registry.yarnpkg.com/react-native-flipper/-/react-native-flipper-0.164.0.tgz#64f6269a86a13a72e30f53ba9f5281d2073a7697"
|
||||
integrity sha512-iJhIe3rqx6okuzBp4AJsTa2b8VRAOGzoLRFx/4HGbaGvu8AurZjz8TTQkhJsRma8dsHN2b6KKZPvGGW3wdWzvA==
|
||||
|
||||
react-native-gesture-handler@^2.8.0:
|
||||
version "2.8.0"
|
||||
resolved "https://registry.yarnpkg.com/react-native-gesture-handler/-/react-native-gesture-handler-2.8.0.tgz#ef9857871c10663c95a51546225b6e00cd4740cf"
|
||||
integrity sha512-poOSfz/w0IyD6Qwq7aaIRRfEaVTl1ecQFoyiIbpOpfNTjm2B1niY2FLrdVQIOtIOe+K9nH55Qal04nr4jGkHdQ==
|
||||
react-native-gesture-handler@^2.9.0:
|
||||
version "2.9.0"
|
||||
resolved "https://registry.yarnpkg.com/react-native-gesture-handler/-/react-native-gesture-handler-2.9.0.tgz#2f63812e523c646f25b9ad660fc6f75948e51241"
|
||||
integrity sha512-a0BcH3Qb1tgVqUutc6d3VuWQkI1AM3+fJx8dkxzZs9t06qA27QgURYFoklpabuWpsUTzuKRpxleykp25E8m7tg==
|
||||
dependencies:
|
||||
"@egjs/hammerjs" "^2.0.17"
|
||||
hoist-non-react-statics "^3.3.0"
|
||||
|
Loading…
Reference in New Issue
Block a user