Merge pull request #2626 from ecency/nt/experiment-feed-stability

Nt/experiment feed stability
This commit is contained in:
Feruz M 2023-03-04 08:20:48 +02:00 committed by GitHub
commit 32ec5c0c92
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
67 changed files with 2498 additions and 3566 deletions

View File

@ -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

View File

@ -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",

View File

@ -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 = () => {

View File

@ -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>

View File

@ -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}`,
});
};

View File

@ -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>
);
};

View File

@ -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,

View 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',
},
});

View 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>
);
};

View 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>
);
};

View 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>
</>
);
};

View File

@ -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: {

View 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>
)
}

View 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;

View File

@ -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);

View File

@ -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;

View File

@ -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);

View File

@ -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

View 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;
};

View File

@ -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;
};

View File

@ -1,4 +0,0 @@
import PostDropdownView from './view/postDropdownView';
import PostDropdown from './container/postDropdownContainer';
export { PostDropdown, PostDropdownView };

View File

@ -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;

View File

@ -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>
);
};

View File

@ -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);

View File

@ -1 +1,2 @@
export * from './postHtmlRenderer';
export * from './postInteractionHandler';

View 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>
)
})

View File

@ -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';

View File

@ -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);

View File

@ -0,0 +1,3 @@
import PostOptionsModal from './container/postOptionsModal';
export { PostOptionsModal };

View File

@ -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,
},
});

View File

@ -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';

View File

@ -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);

View File

@ -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>
);
};

View File

@ -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;

View File

@ -1,5 +0,0 @@
import PostsView from './view/postsView';
import Posts from './container/postsContainer';
export { PostsView, Posts };
export default Posts;

View File

@ -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',
},
});

View File

@ -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 */

View File

@ -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>
);
};

View File

@ -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) => {

View File

@ -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);

View File

@ -1,5 +0,0 @@
import UpvoteView from './view/upvoteView';
import Upvote from './container/upvoteContainer';
export { UpvoteView, Upvote };
export default Upvote;

View File

@ -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;

View 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>
);
};

View 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;

View File

@ -0,0 +1,4 @@
import UpvotePopover from './container/upvotePopover';
export { UpvotePopover };
export default UpvotePopover;

View File

@ -1,7 +1,4 @@
const POST = 'post';
const COMMENT = 'comment';
export default {
POST,
COMMENT,
};
export enum PostTypes {
POST = 'post',
COMMENT = 'comment',
}

View File

@ -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
},
},
});

View File

@ -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;
};

View File

@ -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(
{

View File

@ -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];
}
}

View File

@ -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],

View File

@ -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 () => {

View File

@ -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',

View File

@ -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>

View File

@ -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 {

View File

@ -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(

View File

@ -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));

View File

@ -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;

View File

@ -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;

View 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;

View File

@ -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,
},
});

View File

@ -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>

View File

@ -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';

View File

@ -1,4 +1,4 @@
import { Clipboard } from 'react-native';
import Clipboard from '@react-native-clipboard/clipboard';
const readFromClipboard = async () => {
const clipboardContent = await Clipboard.getString();

View File

@ -206,6 +206,10 @@ const reduxMigrations = {
delete state.cache.comments;
return state;
},
5: (state) => {
state.cache.votesCollection = {};
return state;
},
};
export default {

View File

@ -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"